目录
初始化列表
这是我们之前“初始化”类成员变量的方式
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
在逻辑层面上看,这样的构造函数毫无疑问完成了初始化的功能,但是从语法层面上说,初始化指的是初始化一次,而在函数体中却可以多次被赋值,这种行为叫做赋初值。
一些成员是声明即定义,为了给每一个成员变量找一个定义初值的位置,我们引出了初始化列表这一概念。
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
Date(int year, int month, int day)
:year(year)
,month(month)
,day(day)
{}
与缺省比较
缺省这一概念是c++11新出来的概念,在这之前,如果有const成员变量,或者引用类型需要在定义的时候初始化,c++规定:不管是否显示写初始化列表,编译器每个成员变量都会在初始化列表初始化,这也是定义的位置。而缺省是在声明的时候给的,所以总体来说,初始化列表的优先级最高。
自定义类型作为成员变量初始化
我们说过,自定义类型初始化会调用自身的默认构造(无参,全缺省,编译器默认生成),那如果我不是默认构造怎么办呢?不能在声明的时候传递参数吧 ,那是定义的法子,所以这时初始化列表就尤为重要。
声明类B
可以看到成功调用了B的构造函数,接下来去掉缺省我们就利用起来我们的初始化列表,既然你不让我在声明的时候定义,我就在定义的地方定义
成功运行
接下来看一段稍微有点绕的代码,看完后希望能对其有个深刻的认识
class B
{
public:
B(int num = 1)
:_a(2)
{
_a = num;
cout << "B()" << _a << endl;
}
private:
int _a;
};
class A
{
public:
A()
:_bb(2)//成员变量定义的地方
{
_n = 2;
B _bb(3);
}
private:
int _n=3;
B _bb;
};
int main()
{
A a;//类定义的地方
return 0;
}
step1:定义a,进入构造函数A()的初始化列表查找_n,发现没有_n后使用缺省值代替。
step2:接着找_bb(2)调用B的默认构造,使用传递的2代替了num缺省的1,并输出B()2 (:_bb()会调用B()中构造函数的缺省而非A中声明的缺省)
step3:进入A()函数体内依次执行赋值操作,执行第二行时调用B的构造函数,并输出B()3,
题外话:这里写个空的构造函数A(){ }或者直接不写A的构造函数也能编译,根据声明顺序依次使用缺省或调用自定义类型的构造函数
总结:可以观察到初始化优先级是最高的。而且初始化列表解决了在不用缺省的条件下,实现了const、引用类型和自定义类型无默认构造的初始化。至于其他类型,你可以根据自己的喜好去使用,像我刚才写的代码可读性十分差,而且不易理解,所以我们建议尽量所有成员在初始化列表初始化
初始化顺序
规定:初始化顺序取决于类中声明的顺序而不是初始化列表的顺序
就刚才的例子,我们将B类的声明放在_n变量之前,然后执行这样的操作就会发生这样的错误
(这里n前少写了下划线,不过错误是一样的)希望大家在定义时注意这一点。
explicit关键字
隐式类型转换
在讲explicit之前,我想先给大家看个东西。
class B
{
public:
B(int a)
{
_a = a;
}
private:
int _a = 0;
};
class A
{
public:
A()
{
_bb = 3;//赋值重载
}
private:
B _bb = 3;
};
int main()
{
A a;//类定义的地方
return 0;
}
大家有没有发现有什么不对劲的地方?
第一处是在_bb = 3处,第二处是在B_bb = 3处。
自定义类型怎么接收了一个整形呢,编译器居然还没有报错,是bug吗?当然不是。
我们都知道在 int s =0; double i = s;过程中会发生精度变换,产生临时变量再赋给double类型,那类类型是否也存在同样的机制呢,答案是当然。
机制:构造+拷贝构造 +优化——>构造(左值可以不为const)
拷贝构造大概是这样实现的,原理是调用构造创建临时变量后在传递给拷贝构造实现值拷贝。因为被优化了所以没有输出。
B(const B& bb)//权限缩小
:_a(bb._a)
{
cout << "const B& aa" << endl;
}
注意:如果引用类对象这样写,需要加上const修饰
const B& _bb = 3;
引用类型直接用来绑定一个临时对象。在这种情况下,引用作为内置类型不被优化,编译器会创建一个临时的B对象,并将3作为参数传递给B的构造函数进行隐式转换。
感兴趣的自己可以去看一些这些方面的详细原理,我们只需简单理解为调用构造函数而不是赋值就行了.
c++98支持这样的单参数的构造函数,
在c++11后,支持了多参数的构造函数,类似于结构体的定义方式,但本质确是去调用对应的构造函数。
//多参构造函数
B(int num1,int num2)
:_a(num1)
,_b(num2)
{
cout << "multiple" << endl;
}
//A中声明成员变量
B _b = { 2,3 };
//const& _b = {2,3};
explicit应用
如果不想通过这种方式隐式调用构造函数,可以在构造函数前加上explicit关键字
从错误信息我们可以得知,常量引用类赋值与普通类赋值都被禁止隐式调用构造函数了,这便是explicit的作用。
Static静态成员
思考:如何创建一个类统计程序创建多少个类对象
方法一:定义全局变量
我们这里举这个极端的例子是想说明命名冲突问题,count定义在全局与库中的名称冲突了。
解决方案:部分展开c++命名空间或局部展开或自己建立命名空间等
可以看到成功计算了结果,但在全局定义还有另一个问题,就是数据一旦被篡改,就会导致结果出错,虽然听上去不可能,但难保有人不会迷迷糊糊地做出这样的事情,于是我们可以使用下面的办法——静态成员来解决这一问题。
静态成员变量的声明在类里,作为特殊的成员函数,它不属于某个对象,而是属于所有对象,属于整个类。
这就意味着在类域里可以不与类外发生命名冲突,更不会在类外随便更改它的值。
静态成员变量和成员变量一样,需要一个定义的位置,(声明给缺省并未分配空间)因为它不属于某个对象,所以必须在类外单独处理。
int D::count = 0;
如果单独在类内初始化需要加上const修饰(无初始化列表用不了缺省)
定义完静态类后,又面临着如何使用的问题,去掉private,我们可以像下面这样使用:
注意:因为它属于整个类,所以如果函数形参出现同名的变量可能会被覆盖掉,尽可能用不同名代替。
class MyClass {
public:
static int a;
MyClass(int a) {
this->a = a; // 使用形参 a 来初始化静态变量 a
}
};
int MyClass::a = 0;
int main() {
MyClass obj1(1);
cout << obj1.a << endl; // 输出 2
}
如果想要保持封装性,我们可以在类里用函数的方式获取静态成员变量。 这里我们可以使用静态成员函数或非静态成员函数的方式去访问。
//非静态
int Getstatic_c()//类里
{
return count;
}
//类外
D d1;
cout << d1.Getstatic_c() << endl;
通过创建类的方式调用要考虑是否创建出来的类增加了对象的创建次数。
//静态
static int Getstatic_c()//类里
{
return count;
}
cout << D::Getstatic_c() << endl;//类外
注意:静态成员函数无this指针,不能访问其他的非静态成员变量。注意
匿名对象
在有些场景下,我们可以通过定义匿名对象的方式实现某些一次性功能,
好处:
- 简化代码:匿名对象可以使代码更简洁,减少不必要的命名和变量定义。
- 优化性能:对于一些临时变量或者只使用一次的对象,使用匿名对象可以避免创建多余的对象,从而提高程序运行效率。
- 方便调用函数:当需要调用某个函数但又不需要保存其返回值时,可以使用匿名对象来调用该函数,从而避免了创建无用变量。
- 简化类型转换:在进行类型转换时,有时候会需要创建一个临时的对象来完成转换。如果这个对象只使用一次,则可以使用匿名对象来简化代码。
格式:类名+() (注意是类名不是对象名)
例如刚才我们定义对象来输出count的值就可以这样修改:
可以看到,在调用完构造函数后立马调用析构函数,说明它的生命周期就只有定义的这一行。
场景2:
以后我们还会用到它,对此先有一个理解。
友元函数
前面日期类我们使用过友元函数,它使我们访问内部成员变量成为可能,但是友元增加了耦合度,破坏了封装,尽量少使用。
🍎友元分为友元函数和友元类
友元函数定义在类外,可以直接访问在类的私有成员。
友元函数声明:
//cin&&cout重载声明
friend ostream& operator<<(ostream& out,const Date &d)
friend istream& operator>>(istream& in,const Date &d)
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(无this)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类声明:
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递(如果C是B的友元, B是A的友元,则不能说明C时A的友元。)
- 友元关系不能继承
class top
{
friend class D;//友元类声明
public:
private:
int a = 0;
};
class D
{
public:
D(int x)
{
_tt.a = x;
}
private:
top _tt;//先声明再使用
};
内部类
顾名思义,内部类就是在类里面定义一个类,受外部类的类域限制
内部类是外部类的友元类
内部类可以定义在外部类的public、protected、private。
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
sizeof(外部类)=外部类,和内部类没有任何关系。
我们用代码来论证这些特性。
class out_c
{
public:
class in_c
{
public:
out_c& Fun(const out_c& aa)
{
cout << a << endl;//aa.a
cout << aa.b << endl;
return aa;
}
private:
int _a;
};
private:
static int a;
int b = 1;
};
int out_c::a = 0;
注意:如果将临时对象传递给采用指向对象的引用作为参数的函数,则该引用必须是 const
引用
输出结果:
编译器优化
刚才我们在谈explicit关键字时说到编译器存在优化行为,我们来系统地观察一下
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
}
场景一: A a1 = 1 的完整过程是(临时)构造+拷贝构造优化成构造
场景二:
直接创建局部变量接收,无优化
场景三:
都是构造和拷贝构造优化成构造
场景四:
引用做形参:三次构造,无优化 (加const是因为匿名对象和参数2具有常性)
场景五:
构造+拷贝构造+拷贝构造优化成构造+拷贝构造
在返回aa时的拷贝构造生成临时变量与将该临时变量拷贝给a对象优化成了一个拷贝。
场景六:
构造+拷贝构造+拷贝构造优化为构造
场景七:
单独写含义发生变化,返回A()时构造和拷贝构造优化成构造,但多了赋值重载,如果像fun3那样写就直接不优化了。
总结:
接收返回值对象,尽量用拷贝构造接收,不要赋值接收
函数返回对象时,尽量用匿名对象
尽量用const& 传参(能接受可读可写对象和临时对象)