C++ 对象模型 — 构造语义学
默认构造函数
- 在ARM(Annotated Reference Manual) 中:默认构造函数在 需要的 时候由编译器产生 产生出来。这里的需要为编译器需要,而不是程序需要,程序需要由程序员指定。
- C++ 标准修改为:对于 class X, 如果没有任何用户声明的构造函数,那么会隐式生成一 个默认构造函数,隐式生成的构造函数是 trival(没啥用的) constructor.
- 存在四种情况的合成构造函数为 “notrival”, 其它情况为 “trival”
带有默认构造函数的成员类对象
如果一个类没有任何构造函数,但它有一个拥有默认构造函数的成员对象,那么这个类的隐 式默认构造函数就是 “notrival” 的,编译器需要合成一个默认构造函数,这个过程会在构 造函数真正需要被调用的时候才会发生。
为了解决 C++ 在不同编译模块中合成多个默认构造函数,需要将合成的默认构造函数、拷 贝构造函数、析构函数以及赋值拷贝操作以内联方式完成;如果函数比较复杂,就合成一个 显示的非内联实体。
带有默认构造的基类
如果没有任何用户声明的构造函数的类派生自一个带有默认构造函数的基类,那么这个派生 类的默认构造为 “notrival” 并合并出来;它将根据声明次序调用上一层基类的默认构造函 数,对于后继类而言,合成的构造函数和明确指出的默认构造函数没有什么差异。
带有虚函数的类
- 类声明一个虚函数
- 类派生自一个继承链,其中一个或更多的虚基类
不管哪一种,编译器会详细记录合成一个默认构造的必要信息。
- 编译器生成一个虚函数表(vtbl)存放类的虚函数地址
- 每个类对象都有一个额外的虚函数指针(vptr)在被编译器合成出来,包含 vtbl 的地址
带有一个虚基类的类
这种实现在不同的编译器的实现有极大的差异。共通点是:必须使虚基类在其每一个派生类 中的位置能够于执行期准备妥当。
总结
在合成的默认构造函数中,只有基类子对象和成员会被初始化,其它的非静态数据成员如整 数、整数指针,整数数组等不会初始化。这些对程序或许需要,但对于编译器并非必须。如 想将指针初始化为 nullptr, 这是程序员的事而非编译器。
误解:
- 任何类如果没有定义默认构造函数,就会被合成出来
- 合成默认构造函数会明确设定类内的每个数据成员的默认值
拷贝构造函数
有三种情况会以一个对象的内容作为一个类对象初值:
- 对一个对象做明确的初始化操作
- 当对象被当作参数交给函数时
- 当函数传回一个类对象时
当类明确定义了拷贝构造函数,那么在大部份情况下,当以一个同类对象实体作为初值时, 那么会调用该拷贝构造函数,可能会导致产生临时对象或产生程序代码蜕变(或者都有)。
默认成员逐次初始化
当没有显示的拷贝构造函数时,内部以默认的成员逐次初始化完成的:把每一个内建对象或派 生的数据成员的值从某个对象拷贝一份到另一个对象身上。不过不会拷贝其中的成员类对象, 而是以递归的方式施行成员逐次初始化。
概念上,对于一个 class X, 这个操作是有拷贝构造函数实现。但设计良好的编译器可以为 大部分类对象生成逐位拷贝,因为它们有逐位拷贝语义。
默认构造函数和拷贝构造函数在必要的时候由编译器合成, 必要 指当类不展现逐位拷贝 语义时。
一个类对象可以从两种方式复制得到:
- 初始化
- 赋值
C++标准同样把拷贝构造函数区分为 “trival” 和 “notrival” 两种,只有 “notrival” 的 实体才会被合成到程序中。设定一个拷贝构造函数是否为 “trival” 的标准在于类是否展现 出逐位拷贝语义
非逐位拷贝构造语义
- 当类有一个成员对象,这个对象类声明有一个拷贝构造函数(不管是class明确指定或是 编译器合成)
- 当类继承自一个基类,而这个基类存在一个拷贝时
- 当类声明了一个或多个虚函数时
- 当类声明一个继承串链,其中有一个或多个虚基类
重新设置虚表指针
编译器对于每一个新产生的类对象的 vptr 适当的初始化,如使用派生类对象值初始化存在 虚函数的基类时,要将 vptr 指向基类的虚函数表,而不再派生类的。
处理虚基类子对象
如果一个类对象已另一个对象作为初值,而后者有一个虚基类子对象,那么”逐位拷贝“也会 失效。编译器必须让派生类中的虚基类子对象位置在执行期准备妥当,而 “逐位拷贝” 可能 会破坏这个位置。所以编译器必须自己合成拷贝构造函数做出仲裁。
成员初始化列表
必须使用初始化列表的情况:
- 当初始化一个引用成员
- 当初始化一个常量成员
- 当调用一个基类的构造函数,而它拥有一组参数
- 当调用一个成员类的构造函数,而它拥有一组参数
执行顺序:
- 在成员参数列表中的初始化按照声明的顺序执行而不是列表中的顺序
- 初始化列表在用户定义的代码先执行