继类和对象(二)之后 ,我么你来到了类和对象(三)。
1. 详解构造函数
这篇我们将详细讲解一下构造函数,建议没看过上片的先去看类和对象(二)。这会我们将对类和对象进行详细的讲解。
1.1 构造函数体赋值
你知道吗?创建对象的时候编译器会调用构造函数,去给成员对象赋一个初始值。
看这一段代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体内可以多次赋值。
由此C++引入了初始话列表
1.2 初始化列表
前文讲到类中有构造函数会给类中变量进行赋值,但是这并不能称作是初始化。因为初始化只能初始化一次,而构造函数内可以多次赋值。
初始化列表:以一个 冒号“:”开始,接着是一个以 逗号分隔的数据成员列表,每个 "成员变量"后面跟一个 放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】1. 每个成员变量在初始化列表中 只能出现一次(初始化只能初始化一次)2. 类中包含以下成员,必须放在初始化列表位置进行初始化:1)引用成员变量2)const成员变量3)自定义类型成员(且该类没有默认构造函数时)
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
}
4. 成员变量在类中 声明次序就是其在初始化列表中的 初始化顺序,与其在初始化列表中的先后次序无关
我们来看这道题目
class A{public:A(int a):_a1(a),_a2(_a1){}void Print() {cout<<_a1<<" "<<_a2<<endl;}private:int _a2;int _a1;};int main() {A aa(1);aa.Print();}A. 输出1 1B.程序崩溃C.编译不通过D.输出1 随机值
大家猜结果是多少?
答案是:D
正是因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
1.3 explicit关键字
构造函数不仅可以构造与初始化对象, 对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:1. 构造函数只有一个参数2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值3. 全缺省构造函数+
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作
用
}
但是上述代码可读性不是很好, 用explicit修饰构造函数,将会禁止构造函数的隐式转换。
2. Static成员
2.1 概念
声明为 static的类成员称为 类的静态成员,用 static修饰的 成员变量,称之为 静态成员变量;用 static修饰的 成员函数,称之为 静态成员函数。 静态成员变量一定要在类外进行初始化
2.2 特性
1. 静态成员为 所有类对象所共享,不属于某个具体的对象,存放在静态区2. 静态成员变量必须在 类外定义,定义时不添加static关键字,类中只是声明3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问4. 静态成员函数 没有隐藏的this指针,不能访问任何非静态成员5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制6. sizeof(类),不包括static成员
3. 友元
友元分为:友元函数和友元类
之前我们讲过全局函数不能访问类中的私域成员变量,但是如果使用友元的其实是可以访问的。
3.1友元函数
class Date
{
friend bool operator==(const Date& d1,const Date& d2);//友元的运用
public:
// ……
private:
int _year;
int _month;
int _day;
}
bool operator==(const Date& d1,const Date& d2)
{
//……
}
这里我们写了友元。
1.友元函数可以访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能使用const修饰
3.友元函数可以在类定义的任何地方声明,不是受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。1.友元关系是单向的,不具有交换性。就比如说A可以认为B是它的朋友,但是B不一定认为A是B的朋友。2.友元关系不能传递比如B是A的友元,C是B的友元,则不能说明C时A的友元。3.友元关系不能继承,之后我们会再说的
4. 内部类
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:1. 内部类可以定义在外部类的public、protected、private都是可以的。2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
这里B是A的内部类,B天生就是A的友元。
5. 匿名对象
C++中存在匿名对象,我们可以参考之前学习的匿名结构体。
匿名对象的声明对象生命周期很短。
匿名对象的定义:
//A aa1();这是错误的无法分清是对象定义还是一个函数声明
A();// 生命周期只在这一行,到了下一行它会自动调用析构函数
但是匿名对象一般只在一些特殊的情况才有用:
- 临时计算:当需要创建一个对象仅用于某个表达式的计算,不需要这个对象有名字和持久存在时,可以使用匿名对象。
- 函数参数:如果函数接受一个对象作为参数,且调用者无需保留该对象的副本,则可以在函数调用时创建一个匿名对象。
- 函数返回值:当函数返回一个对象,且调用者不需要知道该对象的确切类型或名称时,可以返回一个匿名对象。
- 单参构造函数:对于单参数的构造函数,可以通过等号(=)来初始化对象,这种情况下会发生隐式转换,创建匿名对象。
- 简化代码:在某些情况下,使用匿名对象可以避免定义和初始化一个命名对象的额外步骤,从而使代码更加简洁。
- 避免命名冲突:在命名空间受限或者为了避免变量名冲突的情况下,使用匿名对象可以减少命名上的复杂性。
总的来说,匿名对象的生命周期仅限于创建它的那一行代码,一旦这行代码执行完毕,匿名对象就会自动销毁,因此它的作用域非常有限。需要注意的是,虽然匿名对象在某些情况下很有用,但过度使用可能会导致代码难以理解和维护,因此应当谨慎使用。
6.拷贝构造的一些编译器优化
1.连续一个表达式中,拷贝构造+拷贝构造->优化成一个拷贝构造
在VS2022中员函数返回临时变量会减少一次拷贝函数的调用
7.类与对象总结
类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性, 那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。