说明:
不是很清楚的点,用下划线。
解答,用斜体;
重点,用粗体加粗;
第五章 构造、析构、拷贝 语意学
5.1 纯虚函数
-
拥有纯虚函数的类,为抽象类,不可能拥有实例(不可能创造出一个对象)。
但若抽象类中有数据成员,则需要一个显式的构造函数去初始化它。
不要把析构函数声明为 pure(纯)。
不要给一个虚函数后面加 const。
5.2 “无继承”情况下的对象构造
当一个class导入一个虚函数时,会发生下列事情:
- 每一个class object多负担一个vptr;
- 自己定义的构造函数被附加了一些代码,实现vptr的初始化;
- 合成拷贝构造函数、赋值构造函数,因为vptr不能用默认的bitwise方式复制了;
5.3 继承体系下的对象构造
在继承下,编译器会扩充每一个constructor,扩充程度视继承体系而定。
constructor的调用伴随了哪些步骤?
初始化列表(member initialization list)的data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。(如*this->x = 0;)
如果有一个member并没有在初始化列表中,但它在一个default constructor,那么该default constructor 必须被调用(手动)。
在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初始值,指定适当的virtual table(s)。
-
在那之前,所有上一层的base class constructors 必须被调用,以base class 的声明顺序为顺序(与初始化列表的顺序没有关联)。
如果base class 被列于初始化列表中,那么任何明确指定参数都应该传递过去。
如果base class 没有列于初始化列表,那么调用default constructor。
如果base class 是多重继承下的第二或后面的base class ,那么this指针必须有所调整。
-
在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅。
- 如果class 被列于初始化列表中,那么如果有任何明确指定的参数,都应该传递过去,若没有列于初始化列表中,则调用default constructor。
- 此外,class中的每一个virtual base class subobject的偏移量必须在执行期可存取。
- 如果class object 是最底层的class,某constructors可能被调用;某些用以支持这个行为的机制必须被放进来。
【注】在那之前,是指在用户代码执行前。
其中的虚拟继承
为了防止重复对virtual base class调用构造函数,规定:只有在继承体系最深层的object才可以对virtual父类进行调用构造初始化。
其中的vptr语意学
vptr在constructor何时被初始化?
在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。
5.4 对象复制语意学
在复制操作时,需要一个面对自我拷贝的过滤过程:
if( this == &rhs) return *this;
当设计一个class,并以一个class object 指定另一个class object时,有三种选择:
什么都不做,实施默认行为。
提供一个explicit copy assignment operator。
明确拒绝一个class object指定给另一个class object。
一个class对于默认的copy assignment operator,在以下情况下不会表现出 bitwise copy语意:
当一个class的 base class 有一个copy assignment operator时,
当一个class 的 member object,而其 class 有一个 copy assignment operator 时,
当一个class 声明了任何 virtual functions 时,
当class继承一个 virtual base class 时。但尽可能不要允许一个virtual base class的拷贝操作,也尽量不要在其中声明数据。
构造这样的一个继承体系:
class Base {
public: virtual ~Base() {}
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base {
public: void show() { cout << "Derived" << endl; }
};
子类Derived类重写了基类Base中的show方法。 编写下面的测试代码:
Base b;
Derived d;
b.show();
d.show();
结果是:
Base
Derived
Base的对象调用了Base的方法,而Derived的对象调用了Derived的方法。因为直接用对象来调用成员函数时不会开启多态机制,故编译器直接根据b和d各自的类型就可以确定调用哪个show函数了,也就是在这两句调用中,编译器为它们每一个都确定了一个唯一的入口地址。这实际上类似于一个重载多态,虽然这两个show函数拥有不同的作用域。 那这样呢: Base b; Derived d; b.show(); b = d; b.show(); 现在,一个Base的对象被赋值为子类Derived的对象。
那这样呢:
Base b;
Derived d;
b.show();
b = d;
b.show();
现在,一个Base的对象被赋值为子类Derived的对象。
结果是:
Base
Base
对于熟悉Java的人而言,这不可理解。但实际上,C++不是Java,它更像C。“b = d”的意思,并不是Java中的“让一个指向Base类的引用指向它的子类对象”,而是“把Base类的子类对象中的Base子对象分割出来,赋值给b”。所以,只要b的类型始终是Base,那么b.show()调用的永远都是Base类中的show函数;换句话说,编译器总是把Base中的那个show函数的入口地址作为b.show()的入口地址。这根本就没用上多态。
单继承下的重写多态
那我们再这样:
Base b;
Derived d;
Base *p = &b;
p->show();
p = &d;
p->show();
这时,结果就对了:
Base
Derived
p是一个指向基类对象的指针,第一次它指向一个Base对象,p->show()调用了Base类的show函数;而第二次它指向了一个Derived对象,p->show()调用了Derived类的show函数。
总结:也就是说,只有是指针或者引用才是真正的多态,将子对象赋给父类对象其实类型向上转型
个人觉得C++容易弄混淆的地方(持续更新):
1.const和指针的修饰问题
const char * a; //一个指针a指向const char
char const *a; //这两个是a指向的内容是常量,不能改变
char * const a; //首先a 是指针然后还是const
const (char*) a; //这两个是a指针本身是常量,指针本身不能改变
其实,可以看出如果const修饰的char(也就是类型本身或者是 *variable对指针的解引用)就是指针指向的内容是常量,反之就是修饰指针本身的。那我们可以总结一个识别方法就是:看const 两边(当然有的只有一边)的类型是类型(指针指向的内容)就是类型变量本身是常量(如const char * a和char const a 的const两边是char,a)。
当然两者都是常量就是:const char * const a;第一个const是类型常量,第二个才是指针常量。同样给出 const char &a ;const char *a;在传递参数时使用。
2.数组和指针的组合问题
char * a[M]; 这是指针数组,就是每一个元素是指针的数组,每个元素都要初始化。a[M]一看就是数组,这个数组每一个元素是char *,所以可以将char *扩展为一维数组然后a[M]就是二维数组了。其实就是M个指针。
char (a)[N]; 这是一个指针,这个指针指向N个char元素,即指向数组的指针,其实就是一个指针。把(a)看着一个变量,这个变量是指向N个元素的指针,所以只是一个一维数组。把char (*a)[N]看成是char b[N]就可以了。
3.C++变量的初始化
对于内置类型局部变量不进行初始化,但是分配地址,全局变量会进行默认初始化。对于类类型局部变量(没有显式初始化)会进行默认初始化(有默认构造函数,否则报错),但其内部的内置数据成员不会进行初始化(如果在默认构造函数没有进行初始化)。数组也是同样。
参考文章
《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记:https://dsqiu.iteye.com/blog/1669614
c++,为什么要引入虚拟继承 :https://www.cnblogs.com/mylinux/p/4725833.html
RTTI、虚函数和虚基类的实现方式、开销分析及使用指导:http://baiy.cn/doc/cpp/inside_rtti.htm