C++在布局以及存取时间上的额外负担是由以下因素导致的
- vitual function 机制,用来支持一个有效率的执行期绑定
- vitrual base class,用以实现“多次出现在继承体系中的base class”,有一个单一而被共享的实例
- 多重继承的额外负担
C++是如何支持虚函数的:
- 每个class产生指向每一个虚函数的指针,放在表格中,这个表格就是虚表,虚表存放在只读数据段中
- 每个类对象被插入了一个指针,指向虚表,这个指针就是虚表指针,虚表指针的设定和重置都由每一个class的构造、析构和赋值运算符自动完成,每一个类所关联的type_info object(用来支持runtime type identification)也是经过虚表指出的,通常放在虚表中的第一个位置。
一个类有多大?
1) 其非静态成员属性的总和大小
2) 加上内存对齐
3) 加上为了支持“虚机制”而产生的额外的负担
当我们实现多态的时候,使用基类指针调用函数的时候,编译器会确认以下两件事情
1) 该接口在指针对应类型中存在
2) 该接口的访问类型
当我们用一个子类对象直接初始化一个基类对象的时候,会不会修改基类对象的vptr呢?
不会的,编译器在初始化和赋值操作之间做了仲裁,编译器必须确保某个对象含有一个或一个以上的vptr,那些vptr的内容不会被基类初始化或改变
当一个基类对象被直接初始化为一个派生类对象的时候,派生类对象就会被切割(不包括vptr)以塞入较小的基类内存中,多态于是不再呈现,而一个严格的编译器可以在编译期间解析一个通过此对象而触发的虚函数的调用(直接通过函数地址调用),而不使用虚函数机制,如果此时将虚函数定义为inline,那么效率上会有更大的收获
为什么pointer或reference能够支持多态
因为他们并不引发内存中任何与类型有关的委托操作;会受到影响的只有他们所指向的内存的大小和解释方式而已
默认构造函数
默认构造函数会在被需要的时候被编译器创建出来。
在这里我们需要区分对象的初始化部分中,那些是编译器的责任,那些是程序员的责任
什么时候会合成一个默认构造函数呢,当编译器需要它的时候!此外,被合成出来的构造函数只执行编译器所需的行为,因此不会将内置基本类型进行初始化。
什么时候默认构造函数被需要了呢?
(即编译器会生成该构造函数)
类中含有具有默认构造的成员类对象
- 编译器需要为该类合成出一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生
- 在C++各个不同的编译单元中,编译器如何避免合成出多个默认构造函数?解决办法是把合成的默认构造函数,拷贝构造函数、析构函数、赋值运算符函数都以内联的方式完成,一个inline函数具有静态链接,作用域仅限于该文件,如果函数过于复杂,不适合做成内联,就会合成出一个explicit non-inline static的函数
- 如果我们提供了无参构造函数后会怎样?这时由于我们定义的自己的无参构造函数,那么编译器就无法提供默认的构造函数了,在大前提下,编译器会在已存在的构造函数中安插一些代码,使其在user code被执行之前,先调用必要的默认构造函数
- 如果有多个类成员对象都要求构造函数初始化操作,那么C++语言会按照这些类成员对象在类中声明的顺序来调用其对应的构造函数。这一点由编译器完成,它为每一个构造函数安插一些程序代码,以成员声明顺序调用每一个成员对应的默认构造。
继承自拥有默认构造函数的基类
- 合成出的默认构造函数将会调用基类的默认构造函数(按照继承的顺序)
- 如果设计者提供了多个构造函数,但就是没有默认的构造函数时,编译器将不会提供默认构造函数,这时编译器会扩展每一个构造函数,将需要调用的所有默认构造函数的代码加进去。
- 如果同时存在着带有默认构造函数的类类型成员,那么他们对应的默认构造函数将会在基类默认构造函数调用完成之后调用
带有虚函数的类
-
Class声明或继承一个虚函数
-
编译期间会发生:1)一个虚函数表会被编译器产生出来,里面存放类的虚函数地址。2)在每一个类对象中,一个额外的vptr会被编译器合成出来,里面存放虚表的地址
-
对于没有任何构造函数的类,编译器会为他们合成一个默认构造函数,以便正确的初始化每一个类对象的vptr,如果已经存在构造函数,那么就应该插入一些代码来完成vptr的设置
类派生自一个继承链,其中有一个或多个虚基类
- 需要处理虚基类指针
总结
对于以上四种情况会导致编译器为没有声明构造函数的类合成一个默认构造函数,这些被合成出来的函数只负责满足编译器的需要(只有基类对象,类类型成员被初始化,其他的非静态成员不会初始化)。除此之外的情况,他们的默认构造函数实际上并不会被合成出来。
在生成的默认构造函数中,只有基类对象和类类型成员属性会被初始化,其他的非静态成员属性(整数、指针、数组)都不会被初始化
以下两个说法都是错误的
任何类如果没有定义默认构造函数,就会被合成出一个来
编译器合成出来的默认构造函数会显示设定类内每一个成员的默认值
拷贝构造
拷贝构造使用的场景
1) 对一个object做显示的初始化操作
2) 当object被当做参数交给某个函数
3) 当object作为函数的返回值
注:拷贝构造函数可以有多个参数,但其他的参数需要拥有默认参数
如果一个类没有提供一个拷贝构造会发生什么?
对于nontrivial的实例才会被合成于程序之中。而决定一个拷贝构造函数是否为trivial的标准在于类是否展现出所谓的“bitwise copy semantics”。如果展现出“bitwise copy semantics”,那么就不会产生拷贝构造,反之则会产生拷贝构造。
非“bitwise copy semantics”的几种情况
1) 当类中含有一个类类型对象,而后者声明有一个拷贝构造(无论是显示声明还是由编译器合成)
2) 当类继承自一个拥有拷贝构造函数的基类(同上)
3) 当类中声明了一个或多个虚函数时。当发生切片时,vptr指向的是基类的虚表,而不是直接将派生类对象的虚表指针,直接复制过来
4) 当类派生自一个继承链,其中有一个或多个虚基类的情况