15、多态和虚函数
除了数据抽象和继承外,面向对象的编程技术的第三个里程碑是多态polymorphism,在C++中是通过虚函数来实现的。
它提供了另外一种机制将接口何实现分开,多态提高了代码可读性,使得代码便于管理及程序更容易扩展。
封装技术将属性和行为组合起来构成了新的数据类型。访问控制通过声明为private来将接口和实现分开。虚函数处理的是解析类型的问题,继承使得对象既可以作为本身类型又可以作为base类型,这种特性使得可以将很多类型看做为一个基本类型,这样一段代码可以适用于很多种数据类型。
C++程序员的变革
C程序员通过三步来认识C++,首先更好的C,即使用函数前必须声明,对变量的使用更严格;是基于对象的C++,代码组织结构包括属性,行为,构造,析构,继承等。
但是虚函数更多的强调类型的概念,而非简单的将代码封装起来,其是C++中的难点,如果不使用虚函数,那么你就不懂OOP。虚函数和类型概念紧密相连,和过程控制语言不同,虚函数只能从设计的角度去理解。
向上类型转换upcasting
对象既可以作为本身类型又可以作为base类型,同时通过base类型的地址可以操纵之。将对象的地址作为基类对象的地址看待就是upcasting
//: C15:Instrument2.cpp // Inheritance & upcasting #include <iostream> using namespace std; enum note { middleC, Csharp, Eflat }; // Etc. class Instrument { public: void play(note) const { cout << "Instrument::play" << endl; } }; // Wind objects are Instruments // because they have the same interface: class Wind : public Instrument { public: // Redefine interface function: void play(note) const { cout << "Wind::play" << endl; } }; void tune(Instrument& i) { // ... i.play(middleC); } int main() { Wind flute; tune(flute); // Upcasting } ///:~ Instrument::play |
Wind具备Instrument的基本属性
重新定义了play,将隐藏Instrument的play方法
Flute对象传递给tune时无需明显的类型转换,因为wind具备Instrument的所有接口,将其转换为Instrument只是缩小了flute的接口而已;同理指向Wind的指针也可自动转换为指向Instrument的指针 |
问题
输出为Instrument::play,不是所希望的结果,因为对象明显是Wind,任何从Instrument派生的类都应该具备自己的play,并且调用自己的play,为了搞懂这些,需要理解绑定binding的概念。
函数调用的绑定
将函数调用和函数体结合起来就是绑定,在程序运行之前由编译器和连接器实施的绑定就是静态绑定。在过程控制的C语言中只有静态绑定的概念。
编译器只知道一个Instrument类型地址时,其不知道如何调用正确的函数,所以实施了静态绑定。解决方案是late bingding,即在运行时根据对象类型实施绑定,也叫动态绑定或者运行绑定。因此必须有某种机制在运行时获得对象的类型信息。编译器仍然不知道对象的类型,但其插入相关代码来获得对象类型信息。
虚函数
为了实现特定函数的延迟绑定,C++要求在函数前用virtual关键字声明。只有虚函数才能实现延迟绑定,并且只有在使用基类地址的情况下,即引用或者指针。
只有声明需要virtual关键字,定义definition不需要。如果某函数在基类中声明为virtual的,则其在所有派生类别中都是virtual的,无论派生类中重新定义时是否使用virtual。在派生类中重新定义基类中的虚函数(名字相同,参数个数和类型都相同,返回值类型相同),称为overriding。在基类中声明相应函数为virtual,使用virtual机制时,派生类中与其同名的函数将被自动调用。
在基类的play前加上virtual关键字即可。
//: C15:Instrument3.cpp // Late binding with the virtual keyword #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } }; // Wind objects are Instruments // because they have the same interface: class Wind : public Instrument { public: // Override interface function: void play(note) const { cout << "Wind::play" << endl; } }; void tune(Instrument& i) { // ... i.play(middleC); } int main() { Wind flute; tune(flute); // Upcasting } ///:~ Wind::play |
声明play 为virtual的
Overriding时,const不同是否有影响?
得到希望的结果 |
可扩展性
在设计良好的OOP程序中,多数函数将和tune的模式相近,其只和基类接口通信。这种程序是可扩展的,无论派生类如何变化,操作基类接口的函数不用任何改动就能够自适应这种变化。
下面所有新类都可以适应原来的tune函数。
//: C15:Instrument4.cpp // Extensibility in OOP #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } virtual char* what() const { return "Instrument"; } // Assume this will modify the object: virtual void adjust(int) {} }; class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind"; } void adjust(int) {} };
class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind"; } }; // Identical function from before: void tune(Instrument& i) { // ... i.play(middleC); } // New function: void f(Instrument& i) { i.adjust(1); }
// Upcasting during array initialization: Instrument* A[] = { new Wind, new Percussion, new Stringed, new Brass, }; int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); } ///:~
| class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; } char* what() const { return "Percussion"; } void adjust(int) {} }; class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; } char* what() const { return "Stringed"; } void adjust(int) {} };
class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; } char* what() const { return "Brass"; } };
此时Woodwind的play将覆盖Wind的play,层层覆盖,总是调用最后层的play方法
没有覆盖overriding基类中的adjust,将使用最后一层即Wind的adjust方法。编译器将确保可以调用适当的virtual函数。
在初始化时,其将自动进行类型upcasting,转换为基类,但将自动覆盖基类中的虚函数
Wind::play Percussion::play Stringed::play Brass::play Woodwind::play
|
Tune接收所有的派生类型对象,并且获得了期望的结果,相当于发送一个消息给对象,让其自动决定如何操作。那么基类该出现在什么时候呢?如何进行程序扩展啊?有时可能不容易发现基类接口,但这并不是设计错误,对于C++中紧凑的模块类来说,重新设计为虚的并没有太大影响,因为改动不会很轻易扩散影响到其他模块。
C++如何实现late binding?
编译器实现了late binding的相关工作,当你声明某函数为virtual时,编译器将自动进行相关处理。
Virtual关键字告诉编译器不要实施early binding,只要操作基类的地址,即可获得正确的结果。为了实现这个功能,编译器为每个类生成了个虚函数表VTABLE,其包括了该类所包含的所有虚函数的地址。在每个含有虚函数的类中,编译器自动为每个类生成一个vpointer指向该类的虚函数表。当通过基类指针调用相关虚函数时,编译器将自动插入相关代码获得VPTR,然后在虚函数表中查找函数地址。
为每个类建立VTABLE,初始化VPTR,插入相关代码,这些工作都由编译器自动完成。使用虚函数,尽管编译器不知道类型信息,相关虚函数仍然能够正确调用。
存储类型信息
在任何类中没有显式的类型信息,但是先前的现象告诉我们必然在每个对象中存储了类型信息,否则无法在运行时获得类型信息。但类型信息是隐藏的,通过类的大小可以检测出来。
//: C15:Sizes.cpp // Object sizes with/without virtual functions #include <iostream> using namespace std; class NoVirtual { int a; public: void x() const {} int i() const { return 1; } };
int main() { cout << "int: " << sizeof(int) << endl; cout << "NoVirtual: " << sizeof(NoVirtual) << endl; cout << "void* : " << sizeof(void*) << endl; cout << "OneVirtual: " << sizeof(OneVirtual) << endl; cout << "TwoVirtuals: " << sizeof(TwoVirtuals) << endl; } ///:~ int: 4 NoVirtual: 4 void* : 4 OneVirtual: 8 TwoVirtuals: 8
| class OneVirtual { int a; public: virtual void x() const {} int i() const { return 1; } }; class TwoVirtuals { int a; public: virtual void x() const {} virtual int i() const { return 1; } };
没有虚函数时,类的大小正如int一样; OneVirtual时多出了一个void类型指针的大小;这说明了编译器在每个类中插入了一个VPTR指针;TwoVirtuals时增加的大小仍然为4,是因为每个类只有以个VPTR,其指向一个VTABLE,在表中包含了该类所有的虚函数地址。 |
这个例子需要至少一个数据成员,当类没有数据成员时,编译器将强制对象为非0大小,因为每个对象必须有个显式的地址,若对象大小为0,则没有为其分配存储空间。因此编译器将为其生成一个默认成员,但当有虚函数时,则VPTR将占据这个默认位置。
所有类没有int成员时,大小信息如下:
int: 4
NoVirtual: 1
void* : 4
OneVirtual: 4
TwoVirtuals: 4
由上可知,默认成员为一个字节大小。当有虚函数时,VPTR将占据此位置,四个字节。
图解虚函数
各种类型的对象具备和instrument相同的接口,因此可以响应相同的信息。因此其地址都可以放在instrument类型的指针数组中,但是编译器只知道其指向了一个instrument对象。
当创建含有虚函数的类或者从含有虚函数的类派生而来,编译器将为每个类创建唯一个VTABLE。在该表中包含了本类的或者从基类继承而来的虚函数的地址,当派生类中没有overriding基类中的虚函数时,将采用前一个基类的虚函数地址。当采用简单的继承机制时,每个对象只有一个VPTR,其由构造函数初始化指向该类的VTABLE。
当通过基类地址调用虚函数时,因为编译器并没有完整信息实现early binding,故其将插入代码获取虚函数地址。所有类在相同的位置即通常为第一个位置含有VPTR指针,因此编译器可以通过VPTR获得VTABLE,其中所有虚函数地址按照相同顺序排列。
不管具体的类型信息,adjust函数地址都在VPTR+2处。
编译器背后的事
i.adjust(1); inside the function f(Instrument& i): push 1 push si mov bx, word ptr [si] call word ptr [bx+4] add sp, 4 |
参数入栈 基类对象的指针入栈,即this 获取VPTR,其在对象的第一个存储域中 调用adjust方法 清除si |
起始地址处的值为this指针,其在调用成员函数时自动入栈,所以成员函数知道其操作的为哪类对象。在成员函数调用之前,除指定的参数为还有this指针入栈。
C++函数调用时,其参数从右之左入栈,这和C是一样的,其为了支持可变参数。
不管你在派生类中如何顺序overriding基类函数,VTABLE中的虚函数顺序是一定的了。
安装vpointer指针
VPTR任何时候需要指向适当的VTABLE,可以确保VPTR得到初始化就是在构造函数中。编译器生成了一个默认的构造函数,其初始化VPTR。
不同的对象
Upcasting只涉及地址,但若编译器有一个对象,知道其精确数据类型,则任何函数调用都不将采用延时绑定。
//: C15:Early.cpp // Early binding & virtual functions #include <iostream> #include <string> using namespace std; class Pet { public: virtual string speak() const { return ""; } }; class Dog : public Pet { public: string speak() const { return "Bark!"; } }; int main() { Dog ralph; Pet* p1 = &ralph; Pet& p2 = ralph; Pet p3; // Late binding for both: cout << "p1->speak() = " << p1->speak() <<endl; cout << "p2.speak() = " << p2.speak() << endl; // Early binding (probably): cout << "p3.speak() = " << p3.speak() << endl; } ///:~
|
P1和p2使用的是地址,意味着类型信息去不全。
P3类型知道,将采用静态绑定。
p1->speak() = Bark! p2.speak() = Bark! p3.speak() = 调用的是基类函数。 |
为什么要虚函数?
为了实现正确的函数调用,虚函数机制显得很重要,那么为什么其是一个option呢?因为C++的哲学是其效率不高,在进行函数调用前需要特定的代码来获得VPTR进而查找VTABLE,这占用了时间和空间。
但一些面向对象的语言如JAVA、PATHON等将强制执行虚机制,但C++从C而来,关注效率。另外内联函数不能是virtual的,因为虚函数需要显式函数地址将其放在VTABLE中。当不关注效率时,可以大量使用virtual。
抽象基类和纯虚函数
在设计中,当只想基类作为派生类的接口,即不想使用基类创建对象,只想upcast为基类从而使用其接口,这可以通过抽象基类实现,前提是基类至少有一个纯虚函数。
当继承抽象基类时,所有的纯虚函数必须定义实现,否则派生类也变为抽象类。纯虚函数使得可以将成员函数作为接口放在基类中而不用提供有意义的函数体。基类中的纯虚函数都是无效函数,若调用则会出现错误,所以不能用抽象基类来创建对象。
Instrument的目的只是为所有派生类提供公用接口,其原因是对于不同的子类型,其可以展现不同的作用。只有当你希望通过公共接口来操作一系列类时才定义抽象基类,基类中的函数并不需要实现。抽象基类的对象无任何意义,抽象基类只表示公共接口而非特定实现。为了防止用户用抽象类定义对象,可以使用纯虚函数机制,这样编译器就可以捕捉错误。
声明纯虚函数的语法为:
virtual void f() = 0;
这告诉编译器在VTABLE中为该虚函数留一个位置,而不填充虚函数的地址。只要有一个成员函数被声明为纯虚函数,VTABLE表就是不完整的。当创建该类的对象时,编译器将给出错误信息告警。
只含有纯虚函数的类称作纯抽象类。
//: C15:Instrument5.cpp // Pure abstract base classes #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: // Pure virtual functions: virtual void play(note) const = 0; virtual char* what() const = 0; // Assume this will modify the object: }; // Rest of the file is the same ... class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind"; } void adjust(int) {} }; class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; } char* what() const { return "Percussion"; } void adjust(int) {} };
int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); } ///:~
| class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; } char* what() const { return "Stringed"; } void adjust(int) {} }; class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; } char* what() const { return "Brass"; } }; class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind"; } }; // Identical function from before: void tune(Instrument& i) { // ... i.play(middleC); } // New function: void f(Instrument& i) { i.adjust(1); }
|
纯虚函数不允许以值传递方式将基类作为函数参数,此时将发生object slicing,即只是将派生类的基类部分传递进来了,是静态绑定,相当于产生了一个基类对象,而具备纯虚函数的基类对象是无意义的。因此声明为派生类,则只能通过引用或指针的方式使用派生类的对象。
某个纯虚函数并不意味着不能使用其他函数的函数体,有时尽管基类函数是virtual的,你仍然希望调用基类的成员函数。原则是尽量将公共部分代码放在基类中,这样既节省代码空间,又能容易传播更改。
在基类中进行纯虚函数定义
也可以在基类中提供纯虚函数的定义,纯虚函数的所有特性仍然存在,但另外的好处在于其提供了部分公共代码供派生类的函数调用,而非每个派生类都重复定义该类代码。
//: C15:PureVirtualDefinitions.cpp // Pure virtual base definitions #include <iostream> using namespace std; class Pet { public: virtual void speak() const = 0; virtual void eat() const = 0; // Inline pure virtual definitions illegal: //!virtual void sleep() const = 0 {} }; // OK, not defined inline void Pet::eat() const { cout << "Pet::eat()" << endl; } void Pet::speak() const { cout << "Pet::speak()" << endl; } class Dog : public Pet { public: // Use the common Pet code: void speak() const { Pet::speak(); } void eat() const { Pet::eat(); } }; int main() { Dog simba; // Richard's dog simba.speak(); simba.eat(); } ///:~ |
内联函数没有显式地址不能为虚函数
在基类中提供纯虚函数的定义,但派生类中仍然要实现
使用基类部分提供的公共代码
Pet::speak() Pet::eat()
|
Pet的VTABLE仍然是空的,未填充地址,在派生类中才是完整的。在基类中提供纯虚函数的定义允许你将普通的虚函数更改为纯虚函数而不影响现有的代码。
继承和虚函数表
当继承基类并override部分虚函数时,编译器为派生类创建新的VATABE,未override的虚函数将采用基类成员函数来填充VTABLE,因此只要能创建该对象,那么其VTABLE就是完整的。
那么但你继承基类并添加新的虚函数时,将发生什么呢?
//: C15:AddingVirtuals.cpp // Adding virtuals in derivation #include <iostream> #include <string> using namespace std; class Pet { string pname; public: Pet(const string& petName) : pname(petName) {} virtual string name() const { return pname; } virtual string speak() const { return ""; } }; class Dog : public Pet { string name; public: Dog(const string& petName) : Pet(petName) {} // New virtual function in the Dog class: virtual string sit() const { return Pet::name() + " sits"; } string speak() const { // Override return Pet::name() + " says 'Bark!'"; } }; int main() { Pet* p[] = {new Pet("generic"),new Dog("bob")}; cout << "p[0]->speak() = " << p[0]->speak() << endl; cout << "p[1]->speak() = " << p[1]->speak() << endl; //! cout << "p[1]->sit() = " //!<< p[1]->sit() << endl; // Illegal } ///:~
|
使用基类部分提供的公共代码Pet::name()
没有override name
p[0]->speak() =
p[1]->speak() = bob says 'Bark!'
p[1]为pet指针,但基类没有sit接口,因此无法通过基类指针来调用派生类中新添的虚函数
|
在dog的VTABLE中,speak的地址和在pet的VTABLE中一致,同理若pug继承dog,则其sit的位置和dog中一致,因为对于所有的派生类,编译器产生代码安照相同的偏移量来获取VTABLE中的虚函数地址,无论对象属于何种派生类型,其VTABLE中相同成员的顺序是一样的。
编译器只知道其为一个指向基类对象的指针,该指针可以指向不同的派生类,某些可能不含有sit,此时调用sit就是错误的,因此编译器将防止通过基类指针调用只在派生类中含有的虚函数。
当你知道基类指针指向的明确对象时,若希望调用派生类中的成员函数,则需要强制转换:
((Dog*)p[1])->sit()
此时你需要明确知道对象类型,但此违背了虚函数的机制,其是自动获得指向的对象的虚函数的。但有时候你需要知道所有对象的类型,其保存在一个容器中,这种机制称RTTI运行时的类型识别,该机制专门处理如何将base指针down转换为派生类指针。
对象剥离
采用多态技术是值传递和地址传递有很大的区别,因为地址大小总是一样的;传递派生类的地址和基类的地址是一样的,因为派生类中包含了基类。值传递时,派生对象将剥离只剩下基类部分。
//: C15:ObjectSlicing.cpp #include <iostream> #include <string> using namespace std; class Pet { string pname; public: Pet(const string& name) : pname(name) {} virtual string name() const { return pname; } virtual string description() const { return "This is " + pname; } }; class Dog : public Pet { string favoriteActivity; public: Dog(const string& name, const string& activity) : Pet(name), favoriteActivity(activity) {} string description() const { return Pet::name() + " likes to " + favoriteActivity; } }; void describe(Pet p) { // Slices the object cout << p.description() << endl; } int main() { Pet p("Alfred"); Dog d("Fluffy", "sleep"); describe(p); describe(d); } ///:~
|
This is Alfred This is Fluffy
|
两个原因:
首先,当调用describe时,只有Pet大小的东西在栈上分配释放,因此多余的部分将被剥离;
其次,因为是值传递,编译器知道具体的数据类型,将调用pet的拷贝构造函数,其将VPTR初始化为PET的VTABLE,然后将dog的基类部分拷贝至p中,因此p将变成完完全全的pet对象。
向上转换为基类对象是很少见的,应该尽量避免,若将description声明为纯虚函数,则编译器将禁止基类对象的值传递,因为抽象类不能够创建对象。这是纯虚函数的一个重要特性,其可以防止对象剥离。
重载和覆盖
在派生类中重新定义基类的成员函数时将隐藏基类中所有同名的函数。
//: C15:NameHiding2.cpp // Virtual functions restrict overloading #include <iostream> #include <string> using namespace std; class Base { public: virtual int f() const { cout << "Base::f()/n"; return 1; } virtual void f(string) const {} virtual void g() const {} };
class Derived1 : public Base { public: void g() const {} };
int main() { string s("hello"); Derived1 d1; int x = d1.f(); d1.f(s); Derived2 d2; x = d2.f(); //!d2.f(s); // string version hidden Derived4 d4; x = d4.f(1); //!x = d4.f(); // f() version hidden //!d4.f(s); // string version hidden Base& br = d4; // Upcast //!br.f(1); // Derived version unavailable br.f(); // Base version available br.f(s); // Base version abailable } ///:~ | class Derived2 : public Base { public: // Overriding a virtual function: int f() const { cout << "Derived2::f()/n"; return 2; } };
class Derived3 : public Base { public: // Cannot change return type for virtual: //! void f() const{ cout << "Derived3::f()/n";} }; class Derived4 : public Base { public: // Change argument list: int f(int) const { cout << "Derived4::f()/n"; return 4; } };
Base::f()
Derived2::f() 覆盖基类的某重载函数时将隐藏其他所有重载版本,无论被覆盖的是否为虚函数。
Derived4::f() 改变了参数形式,不是覆盖,将隐藏基类中所有同名函数。
无法通过基类指针操作其不含有的成员函数 Base::f() 此时为基类指针,f()和f(string)并没有被覆盖。 |
Derived3中,覆盖虚函数时不能改变返回值类型,但是非虚函数可以改变返回值类型。因为在通过基类指针多态调用虚函数时,其期待的返回值为int类型,若此时基类指针指向的是Derived3类型时,则将调用Derived3的void f() const,没有返回值。
可变的返回类型
Derived3表明重载虚函数时不能改变其返回值类型,但有些情况可以稍微改变返回值类型,可以利用upcasting技术,返回值派生类的指针或者引用可以自动转换为基类的指针或引用。
实际验证不能编译通过
//: C15:VariantReturn.cpp // Returning a pointer or reference to a derived // type during ovverriding #include <iostream> #include <string> using namespace std; class PetFood { public: virtual string foodType() const = 0; }; class Pet { public: virtual string type() const = 0; virtual PetFood* eats() = 0; }; class Bird : public Pet { public: string type() const { return "Bird"; } class BirdFood : public PetFood { public: string foodType() const { return "Bird food"; } }; // Upcast to base type: PetFood* eats() { return &bf; } private: BirdFood bf; };
| class Cat : public Pet { public: string type() const { return "Cat"; } class CatFood : public PetFood { public: string foodType() const { return "Birds"; } }; // Return exact type instead: CatFood* eats() { return &cf; } ThinkingInCPlusPlus.cpp(451) : error C2555: 'Cat::eats' : overriding virtual function differs from 'Pet::eats' only by return type or calling convention private: CatFood cf; }; int main() { Bird b; Cat c; ThinkingInCPlusPlus.cpp(457) : error C2259: 'Cat' : cannot instantiate abstract class due to following members: Pet* p[] = { &b, &c, }; for(int i = 0; i < sizeof p / sizeof *p; i++) cout << p[i]->type() << " eats " << p[i]->eats()->foodType() << endl; // Can return the exact type: Cat::CatFood* cf = c.eats(); Bird::BirdFood* bf; // Cannot return the exact type: //!bf = b.eats(); // Must downcast: bf = dynamic_cast<Bird::BirdFood*>(b.eats()); } ///:~ |
虚函数和构造函数
当创建含有虚函数的对象时,必须初始化VPTR为适当的VTABLE,在可能调用任何虚函数时其必须初始化完毕,而构造函数负责创建对象,因此初始VPTR的任务也应该由其在对象创建完毕前完成。编译器将自动在构造函数里面添加相关初始化VPTR的代码。
构造函数初始化VPTR,检查this指针,调用基类构造函数,因此整个开销可能是比较大的,当大量调用构造函数时,应避免使用inline构造函数。因为代码量大幅增加,而性能并没有提升。
构造函数的调用顺序
派生类只能访问自己的成员变量,而不能访问基类的私有成员变量,因此只能由基类的构造函数初始化,因此应确保所有基类的构造函数得以调用。如果在派生类的初始化列表中不显式的调用基类的构造函数,则编译器会自动调用基类的默认构造函数,若基类没有默认构造函数,则编译不通过。
继承的的时候,派生类可以访问基类所有的public和protected成员,因此必须确保所有可用的对象已经正确初始化,唯一的方式是确保首先调用基类的构造函数以及相关成员对象的构造函数。
在构造函数内部调用虚函数
在普通的成员函数中调用虚函数时仍然是在运行时解析虚函数调用,因为此时对象并不知道其是属于成员函数所在的类或者是派生类。但是在构造函数中,虚机制没有作用,只有“本地”的虚函数会被调用,有两个原因:
首先,在构造函数中你只能知道基类对象已经初始化,但不知道谁将继承你,若调用派生类中对应的虚成员函数,则其可能操作尚未初始化的成员;
其次,构造函数首先初始化VPTR,此表只能根据基类和当前类来初始化,其在对象的生命周期里将不变,除非还有其他类以此为基类。如果紧接着调用下一步的构造函数,则其将VPTR指向自己的VTABLE,如此向后,直到最后一层。当进行虚函数调用时,只能利用当前的VPTR去获得对应的虚函数。
析构函数和虚析构函数
构造函数不能是virtual的,但析构函数可以,并且有时候必须如此。
当前的析构函数总是知道基类的成员目前是可用的,因此在析构函数中调用基类的成员函数是安全的。析构函数首先做自身的清除工作,接着调用上一层的析构函数,依次类推。析构函数知道其自身由谁派生而来,但不知道谁会继承自己,因此只会调用自身和基类的析构函数。
若通过基类指针操作对象如delete,因为不知道基类指针指向的具体类型,因此安全万能的做法只能调用基类的析构函数。虚机制同样适用于析构函数。
//: C15:VirtualDestructors.cpp
// Behavior of virtual vs. non-virtual destructor
#include <iostream>
using namespace std;
class Base1 {
public:
~Base1() { cout << "~Base1()/n"; }
};
class Derived1 : public Base1 {
public:
~Derived1() { cout << "~Derived1()/n"; }
};
class Base2 {
public:
virtual ~Base2() { cout << "~Base2()/n"; }
};
class Derived2 : public Base2 {
public:
~Derived2() { cout << "~Derived2()/n"; }
};
int main() {
Base1* bp = new Derived1; // Upcast
delete bp;
Base2* b2p = new Derived2; // Upcast
delete b2p;
} ///:~
~Base1()
~Derived2()
~Base2()
delete b2p调用了所有的析构函数,达到了期望。实际上将析构函数声明为virtual是一种良好的习惯,其没有任何副作用,并且可以有效避免内存泄漏。析构函数可以是虚的是因为析构时已经知道了对象的类型,而构造时尚不知道对象的最后类型。
纯虚析构函数
使用纯虚析构函数时必须提供函数体,因为如果你漏掉了析构函数的定义,那么将调用谁呢?因此编译器和链接器确保含有析构函数的定义。虚析构函数和纯虚析构函数的唯一区别是含有纯虚析构函数的类不能创建该类的对象。
//: C15:PureVirtualDestructors.cpp
// Pure virtual destructors
// require a function body
#include <iostream>
using namespace std;
class Pet {
public:
virtual ~Pet() = 0;
};
Pet::~Pet() {
cout << "~Pet()" << endl;
}
class Dog : public Pet {
public:
~Dog() {
cout << "~Dog()" << endl;
}
};
int main() {
Pet* p = new Dog; // Upcast
delete p; // Virtual destructor call
} ///:~
但和其他的纯虚函数不同的是,在派生类中无需提供基类的析构函数定义。通常若在派生类中不override基类的纯需函数,该派生类将变成抽象类,但对于析构函数不是如此。那么纯虚析构函数有合用呢?唯一有用的情况是但基类只有析构函数一个虚函数且希望禁止创建基类对象时。若某类含有虚函数,则将析构函数设定为虚函数是一种良好习惯。
析构函数中调用虚函数
和构造函数一样,析构函数中调用虚函数,只能调用本地版本,基本原理通构造函数一样,唯一的区别是对于构造函数,类型信息尚不完全,调用析构函数时知道类型信息,但对应的虚函数不再可用,已经析构了。
//: C15:VirtualsInDestructors.cpp
// Virtual calls inside destructors
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() {
cout << "Base1()/n";
f();
}
virtual void f() { cout << "Base::f()/n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "~Derived()/n"; }
void f() { cout << "Derived::f()/n"; }
};
int main() {
Base* bp = new Derived; // Upcast
delete bp;
} ///:~
向下转换
向下转换并不一定安全,但显式的类型转换dynamic_cast是安全的,因为只有但转换合适时才返回成功的指针,否则返回NULL。
//: C15:DynamicCast.cpp
#include <iostream>
using namespace std;
class Pet { public: virtual ~Pet(){}};
class Dog : public Pet {};
class Cat : public Pet {};
int main() {
Pet* b = new Cat; // Upcast
// Try to cast it to Dog*:
Dog* d1 = dynamic_cast<Dog*>(b);
// Try to cast it to Cat*:
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;
cout << "d2 = " << (long)d2 << endl;
} ///:~
warning C4541: 'dynamic_cast' used on polymorphic type 'class Pet' with /GR-; unpredictable behavior may result
运行失败!!!!
使用dynamic_cast时必须有虚函数,因为其是根据VTABLE来确定类型的。必须检查返回值以确定是否转换成功。dynamic_cast需要额外的消耗来获得类型信息,当你知道指针的确切类型时,可以使用静态转换static_cast,若类型不匹配,将给出编译错误信息,其可以防止出错,而不象普通的强制类型转换。但更普遍的做法是dynamic_cast。
//: C15:StaticHierarchyNavigation.cpp
// Navigating class hierarchies with static_cast
#include <iostream>
#include <typeinfo>
using namespace std;
class Shape { public: virtual ~Shape() {}; };
class Circle : public Shape {};
class Square : public Shape {};
class Other {};
int main() {
Circle c;
Shape* s = &c; // Upcast: normal and OK
// More explicit but unnecessary:
s = static_cast<Shape*>(&c);
// (Since upcasting is such a safe and common
// operation, the cast becomes cluttering)
Circle* cp = 0;
Square* sp = 0;
// Static Navigation of class hierarchies
// requires extra type information:
if(typeid(s) == typeid(cp)) // C++ RTTI
cp = static_cast<Circle*>(s);
if(typeid(s) == typeid(sp))
sp = static_cast<Square*>(s);
if(cp != 0)
cout << "It's a circle!" << endl;
if(sp != 0)
cout << "It's a square!" << endl;
// Static navigation is ONLY an efficiency hack;
// dynamic_cast is always safer. However:
// Other* op = static_cast<Other*>(s);
// Conveniently gives an error message, while
Other* op2 = (Other*)s;
// does not
} ///:~