1、每个派生类对象包含一个基类部分。因此派生类对象也是基类对象。可以将派生类对象的引用转换为基类子对象的引用,同理指针。
2、没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。
3、编译器不会自动将派生类型对象转换为基类类型对象。
4、用派生类对象对基类对象进行初始化(或赋值)时,将发生切割。
示例
Item_base item; // object of base type Bulk_item bulk; // object of derived type // ok: uses Item_base::Item_base(const Item_base&) constructor Item_base item(bulk); // bulk is "sliced down" to its Item_base portion // ok: calls Item_base::operator=(const Item_base&) item = bulk; // bulk is "sliced down" to its Item_base portion,把非基类部分切掉
5、派生类到基类的可访问性
如果是 public 继承,则用户代码和后代类(member functions of subsequently derived classes)都可以使用派生类到基类的转换。如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是 private 继承,则从 private 继承类派生的类不能转换为基类。如果是protected 继承,则后续派生类的成员(the members of subsequently derived classes)可以转换为基类类型。
无论是什么派生访问标号,派生类本身都可以访问基类的 public 成员,因此,派生类本身的成员和友元总是可以访问派生类到基类的转换。
6、从基类到派生类的自动转换是不存在的,需要派生类对象时不能使用基类对象。
7、编译器确定转换是否合法,只看指针或引用的静态类型。在这些情况下,如果知道从基类到派生类的转换是安全的(指针或引用),就可以使用static_cast强制编译器进行转换。或者,可以用dynamic_cast申请在运行时进行检查。
8、每个派生类对象由派生类中定义的(非static)成员加上一个或多个基类子对象构成;当构造、复制、赋值和撤销派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
9、每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员,而通过将基类构造函数包含在构造函数初始化列表中间接初始化继承成员。
初始化顺序为:首先初始化基类,然后根据声明次序初始化派生类的成员。一个类只能初始化自己的直接基类。
10、重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数体中对这些成员赋值。
11、只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。
示例
//定义派生类复制构造函数 class Base { //... }; class Derived :public Base { //Base::Base(const Base&) not invoked automatically Derived(const Derived& d):Base(d) //其他成员初始化。这里的Base(d)必不可少,否则 //将运行Base的默认构造函数初始化对象的基类部分 //,而不是通过基类的复制构造函数。 { //... } }; //定义派生类的赋值操作符 //如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。 //Base::operator=(const Base&) not invoked automatically Derived& Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); //给基类赋值,必不可少。 // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
12、派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。
示例
class Base { //... }; class Derived:public Base { public: //Base::~Base invoked automatically ~Derived() { //do what it takes to clean up derived members } };
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
13、虚析构函数
删除指向动态分配对象的指针时,需要在释放对象的内存之前运行析构函数清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。如果有虚成员函数,析构函数应该是虚的。
如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数。基类虚析构函数(复制,赋值,析构)是三法则的一个例外。
即使析构函数没有工作要做,继承层次中的根类也应该定义一个虚析构函数。
14、构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。(虚函数用于动态联编,是在运行时通过类型来确定函数的调用,而构造函数是在内存分配之前调用的,不可能知道是哪个类型,所以不能为虚函数)
构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。
撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。
在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为
了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在
基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定。
要理解这种行为,考虑如果从基类构造函数(或析构函数)调用虚函数的派生类版本会怎么样。虚函数的派生类版本很可能会访问派生类对象的成员,毕竟,如果派生类版本不需要使用派生类对象的成员,派生类多半能够使用基类中的定义。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化,实际上,如果允许这样的访问,程序很可能会崩溃。
15、如果在构造函数中调用一个虚函数,被调用的只是这个函数的本地版本,虚机制在构造函数中不工作。因为虚函数机制使得可以调用派生类中的函数,而有可能派生类对象(的函数)还没有初始化,这将出问题。 The state of the VPTR
You should keep in mind that constructors and destructors are the only places
16、Virtual or not. Inside a destructor, only the “local” version of the member
考虑到,虚析构函数调用派生类对象的虚成员函数,而有可能派生类对象的该成员函数它已经删除了,因此编译器决定只调用本地版本。 Notice that the same is
true(指构造函数不能为虚函数,而虚机制在析构函数中不起用途) for the
参考
[1] http://blog.163.com/zhoumhan_0351/blog/static/3995422720100284731826/
[2] 关于虚析构函数和构造函数讨论(virtual constructors)
http://blog.163.com/zhoumhan_0351/blog/static/39954227201032092854732/
[3] 多态性和虚函数(Polymorphism&Virtual Functions)
http://blog.163.com/zhoumhan_0351/blog/static/39954227201031995829238/