C++在布局以及存取时间上的额外负担是由以下因素导致的
- vitual function 机制,用来支持一个有效率的执行期绑定
- vitrual base class,用以实现“多次出现在继承体系中的base class”,有一个单一而被共享的实例
- 多重继承的额外负担,发生在一个派生类和其第二或后继的基类之间的转换
C++是如何支持虚函数的
- 每个class产生指向每一个虚函数的指针,放在表格中,这个表格就是虚表
- 每个类对象被插入了一个指针,指向虚表,这个指针就是虚表指针,虚表指针的设定和重置都由每一个class的构造、析构和赋值运算符自动完成,每一个类所关联的type_info object(用来支持runtime type identification)也是经过虚表指出的,通常放在虚表中的第一个位置
一个对象有多大
- 其非静态成员的总和大小
- 任何犹豫对其的需求而填补上的空间(可能存在于成员之间,也可能存在于集合边界)
- 加上为了支持虚机制而产生的任何额外负担
什么时候编译器会产生默认构造函数
对于class X,如果没有任何的用户定义构造函数,那么会有一个默认构造函数被隐式声明出来,这个默认的构造函数将是一个trivial(没有什么用的)的构造函数
默认构造函数在被需要的时候被编译器产生出来,这里的被需要分为两种情况,一种是程序的需要,这种需要应该由程序员来满足,将会提供一个trivial的构造函数,另一种是编译器的需要,产生一个notrivial的构造函数,该函数只完成编译器所需的动作。所以以下类产生一个notrivial的默认构造函数。
class Foo
{
public:
int value;
Foo *pNext;
};
void foo_bar()
{
Foo bar;
if (bar.value || bar.pNext)
{
}
}
产生以下错误
哪些情况下编译器提供的默认构造函数是nontrivial
- 如果一个类没有任何构造函数,但它内含一个对象成员,而该对象成员有一个默认构造函数,那么这个类的隐式默认构造函数就是nontrivial。编译器需要为该类合成出一个默认构造函数,不过这个合成动作只有在构造函数真正需要被调用的时候才会发生
- 如果一个没有任何构造函数的类派生自一个带有默认构造函数的基类,那么这个派生类的默认构造函数会被视为nontrivial,并因此需要被合成出来,它将调用基类的默认构造函数(按照其声明的次序),对于一个后继派生的类来说,这个合成的构造函数和一个被显式提供的默认构造函数没有什么区别
- 带有虚函数的类,为了实现虚机制,编译器必须为类对象的vptr设定初值,放置适当的虚表地址,对于类所定义的每一个构造函数,编译器会安插一些代码来做这样的事。对于那些没有声明任何构造函数的类来说,编译器会为它们合成一个默认构造函数用来正确的初始化每一个类对象的vptr
- 带有一个虚基类的类,虚基类指针是在类对象构造期间完成的,对于类所定义的每一个构造函数,编译器会安插那些允许每一个虚基类在执行期存取操作的代码。如果类没有声明任何构造函数,编译器必须为它合成一个默认构造函数
以上四种情况,会造成编译器必须为未声明构造函数的类合成一个默认构造函数,C++标准把那些合成物称为implicit nontrivial default constructors。被合成出来的构造函数只能满足编译器而非程序的需要。它通过调用对象成员的构造函数或者基类的构造函数或者是为每一个对象初始化其虚函数机制或者虚基类机制完成。至于没有存在那4种情况而又没有声明任何构造函数的类,我们说他们拥有的是implicit trivial default constructors,他们实际上不会被构造出来
在合成的默认构造函数中,只有基类subobject和对象成员会被初始化,其他的非静态成员都不会被初始化。这些初始化操作对程序而言或者有需要,但是对编译器则非必要,这些工作应由程序员完成
在C++各个不同的编译模块中,编译器如何避免合成出多个默认构造函数
解决办法是把合成的默认构造函数、拷贝构造函数、析构函数、赋值运算符函数都以inline的方式完成。一个inline的函数具有静态链接,不会被文件以外看到,如果函数太复杂,不适合做成inline,就会合成一个explicit non-inline static实例。
拷贝构造函数发生什么情况下
- 对一个对象做显式的初始化操作
- 当对象被当做参数交给某个函数时
- 当函数传回一个类对象
拷贝构造的动作
如果类没有提供一个显式的拷贝构造函数,当类对象以相同类的另一个对象作为初值时,其内部是以所谓的default memberwise initialization手法完成的,也就是把每一个内建的或派生的数据成员的值,从某个对象拷贝一份到另一个对象身上。不过它并不会拷贝其中的类对象成员,而是以递归的方式实施memberwise initialization。以上操作是通过拷贝构造函数来完成的。
拷贝构造函数和默认构造函数都只有在必要的情况下才由编译器产生出来,对于拷贝构造函数来说其中的必要是指“类不展现bitwise copy semantics”
C++标准说,如果一个类没有声明一个拷贝构造函数,就会有隐式的声明或隐式的定义出现,和构造函数一样,C++标准把拷贝构造函数同样分为trivial和nontrivial两种,只有nontrivial的实例才会被合成于程序之中,决定一个拷贝构造函数是否为trivial的标准在于类是否展现所谓的bitwise copy semantic.
当我们用一个类对象初始化另一个类对象的时候,如果类的设计者定义了一个拷贝构造函数,那么就是使用拷贝构造函数来完成赋值,但是如果类没有显式定义拷贝构造函数,那么是否会有一个编译器合成的实例被调用取决于该类是否展现“bitwise copy semantics”。
编译器合成出的拷贝构造,可以调用类对象成员的拷贝构造,并且整数、指针、数组等noclass 成员也都会被复制
什么时候一个类不展现所谓的bitwise copy semantics?
- 当类内含一个对象成员而后者的类声明有一个拷贝构造时(不论显式声明还是隐式声明)
- 当类继承自一个具有拷贝构造的基类时(不论显式声明还是隐式声明)
- 当类声明了一个或多个虚函数时
- 当 类派生自一个有一个或多个虚基类的继承串链时
在前两种情况中,编译器必须将成员或基类的拷贝构造函数调用操作安插到被合成的拷贝构造中。如果对于同一类对象之间的初始化动作(使用Bear初始化Bear),那么可以直接依靠bitwise copy semantic完成,但是如果通过派生类对象初始化基类对象的时候,编译器就必须合成一个拷贝构造函数将vptr适当的初始化。如果对于同一类对象之间的初始化动作(使用),那么可以直接依靠bitwise copy semantic完成,但是如果通过派生类对象初始化基类对象的时候,为了完成正确的初值设定,编译器必须合成一个拷贝构造安插一些代码以设定虚基类指针的初值,对每一个对象成员执行必要的memberwise初始化操作,以及执行其他的内存相关工作
在以上四种情况下类不再保持bitwise copy semantic,而且默认拷贝构造如果没有被声明的话,会被视为nontrivial,在这4种情况下,如果缺乏一个已声明的拷贝构造,编译器为了正确处理一个类对象作为另一个类对象的初值,必须合成一个拷贝构造。
来源:CSDN
作者:weixin_43718250
链接:https://blog.csdn.net/weixin_43718250/article/details/102880695