构造函数语意
默认构造
- 编译器在需要的时候会合成默认构造,被合成的构造只执行编译器所需要的行动,像一些数据成员的初始化操作并不执行,因为那是程序员的职责。下面是几种默认构造合成的情况:
- 成员变量中存在,带有默认构造的对象
- 这个合成仅仅会在构造真正需要被调用的时候才会发生
- 如果有多个成员都要求构造函数初始化,按照class中的声明顺序来,在用户代码之前调用成员的构造
- 带有默认构造的基类:一个没有任何构造的类派生自一个有默认构造函数的基类,需要被合成出来调用基类的默认构造
- 如果子类有除了默认构造之外的构造,编译器会扩张现有的一个构造,将调用所有必要的默认构造的代码加进去
- 如果存在带默认构造的成员,先基类构造,再成员默认构造
- 带有一个虚函数的类,两种情况下需要合成默认构造:(因为需要初始化虚表指针)
- class声明或者继承自一个虚函数
- class派生自一个继承串,其中有一个或更多的虚基类
- 带有一个虚基类的类,通过派生类访问成员时需要知道编译器产生指针指向虚基类
- 显式定义默认构造:T() = default
- 两个误解:都是不成立的
- 任何class如果没有定义构造函数,编译器都会合成
- 编译器合成的构造函数会明确class内的每一个数据成员的默认值
- 不支持静态构造函数
- 构造函数不能被声明成常量成员函数
- 避免使用默认构造函数:一个类如果没有缺省构造,在三个方面会有问题:
- 不能建立对象的数组,可以利用指针数组代替,但是有缺点:增加了内存分配量,可以为数组分配原始内存,这样就避免了浪费内存
- 无法在许多基于模板的容器里面使用——通过仔细设计模板可以杜绝缺省构造函数的使用
- 不提供缺省构造的虚基类很难与其进行合作
拷贝构造
决定一个cpctor能否被编译器合成的标准在于class能否展现出位逐次拷贝,如果能展现出位逐次拷贝,就不需要合成
- 有三种情况会调用cpctor(note:后两种不是调用拷贝复制运算符=):
- 一个对象初始化另一个
- 参数值传递
- 返回值拷贝
- 一个类什么时候不展示位逐次拷贝(要合成cpctor)?前两种情况将合成的构造安插到拷贝构造中
- 当class有一个成员对象,且这个成员对象有一个cpctor时(不管这个cpctor是明确定义还是编译器合成的)
- 当class继承自一个基类,且基类有cpctor(不管这个cpctor是明确定义还是编译器合成的)
- 当class声明了一个或多个虚函数的时候,需要cpctor构造初始化虚表指针vptr
- 当class派生自一个继承串链,其中有一个或者多个虚基类
- 拷贝构造要还是不要?
- 如果支持位拷贝,不需要提供拷贝构造,因为编译器实施了最好的行为,既快速又安全
- 如果需要拷贝构造,使用memcpy会更有效率,不管使用memcpy还是memeset,都只在class不含任何编译器产生的内部成员时才安全,如果又虚函数或者虚基类就不安全
- 类的拷贝操作在什么情况下可以直接使用memcpy?
- 拷贝构造函数通常不应该是explicit的,因为如果没有拷贝赋值运算符,=会被转换位拷贝构造的调用
- 关于拷贝构造和拷贝运算符:基本上声明时会使用拷贝构造函数,赋值语句会使用拷贝运算符
MyClass a{5}; // 拷贝构造
MyClass b = a; // 拷贝构造 因为这也是声明
a = b; // 赋值 operator=
程序转化语义
- 明确的初始化操作:
- 重写每一个定义,其中的初始化操作会被剥离
- class的拷贝构造的调用操作会被安插进去
- 参数的初始化
- 返回值的初始化,以下例子也是在编译层面做优化NRV
X bar()
{
X xx;
return xx;
}
// 转化过程(可能每个编译器的实现不一样)
- 首先加一个额外参数,类型为对象的引用;
- 在return之前安插一个拷贝构造操作,return 空(这就是编译器省略拷贝构造的唯一一种情况)
void bar(X& _result)
{
X xx;
xx.X::X();
_result.X::X(xx);
return;
}
如果是函数指针也是同理
- 在程序员层面优化:
X bar(const T& y, const T& z){
X xx;
return xx;
}
// 这个会要求xx被membersize地拷贝到编译器所产生的结果中,使用以下替代:
X bar(const T& y, const T& z){
return X(y, z);
}
// 效率会比较高,避免了一次拷贝构造:
void bar(X& __result, const T& y, const T& z){
__result.X::X(y, z);
return;
}