什么是多态
从字面上理解就是多种形态的意思。而多态一词最初源自希腊语,其含义便是“多种形式”,意思是是具有多种形式或形态的情形,在C++语言中多态有着更广泛的含义。在C++ primer一书中把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。百度百科上提到在面向对象语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数实现的。
只从概念描述是无法深刻清晰的理解它的,下面我们就具体分析一下。
1、对象类型
这里所说的对象类型可以用下面的图来体现:我们通过代码举例说明一下
1 class Derived1:public Base 2 {}; 3 class Derived2:public Base 4 {}: 5 int main() 6 { 7 Derived1* p1 = new Derived1; 8 Base = p1; 9 Derived2* p2 = new Derived1; 10 Base = p2; 11 return = p2 12 }
静态类型在编译时总是已知的,他是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知道。
2、静态多态与动态多态
静态多态和动态多态的区别其实用下面的图就可以体现:
静态多态
静态多态也称为静态绑定或早绑。编译器在编译期间完成的, 编译器根据函数实参的类型(可能会进行隐式类型转换) , 可推断出要调用那个函数, 如果有对应的函数就调用该函数, 否则出现编译错误。
1 int Add(int left,int right) 2 { 3 return left + right; 4 } 5 float Add(float left, float right) 6 { 7 return left + right; 8 } 9 int main() 10 { 11 cout<<Add(1,2)<<endl; //调用int Add()函数 12 cout<<Add(1.34f,3.21f)<<endl; //调float Add()函数 13 return 0; 14 }
这里我把重载归为了静态多态。重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定)。正是由于重载的这种性质,也有结论认为:重载只是一种语言特性,与多态无关,与面向对象也无关。基于面向对象来说重载的概念并不属于“面向对象编程”。但是多态性又是一个比较广泛的概念。这里为了便于理解先不去深度分析它们的区别,等多态的其它部分具体分析完毕,我们再来说。
动态多态
动态绑定: 在程序执行期间(非编译期) 判断所引 用对象的实际类型, 根据其实际类型调用相应的方法。使用virtual关键字修饰类的成员 函数时, 指明该函数为虚函数, 派生类需要重新实现, 编译器将实现动态绑定。
1 class CBase 2 { 3 public : 4 virtual void FunTest1(int _iTest) { 5 cout << "CBase: : FunTest1() " << endl; 6 } 7 void FunTest2(int _iTest) { 8 cout << "CBase: : FunTest2() " << endl; 9 } 10 virtual void FunTest3(int _iTest1) { 11 cout << "CBase: : FunTest3() " << endl; 12 } 13 virtual void FunTest4(int _iTest) { 14 cout << "CBase: : FunTest4() " << endl; 15 } 16 }; 17 class CDerived : public CBase 18 { 19 public : 20 virtual void FunTest1(int _iTest){ 21 cout << "CDerived: : FunTest1() " << endl; 22 } 23 virtual void FunTest2(int _iTest) { 24 cout << "CDerived: : FunTest2() " << endl; 25 } 26 void FunTest3(int _iTest1) { cout << "CDerived: : FunTest3() " << endl; 27 } 28 virtual void FunTest4(int _iTest1, int _iTest2){ 29 cout << "CDerived: : FunTest4() " << endl; 30 } 31 }; 32 int main(){ 33 CBase* pBase = new CDerived; 34 pBase->FunTest1(0); 35 pBase->FunTest2(0); 36 pBase->FunTest3(0); 37 pBase->FunTest4(0);
37 delete pBase; 38 getchar(); 39 return 0; 40 }
当我们使用基类的指针或引用调用基类中定义的一个函数时时,我们并不知道该函数真正的对象是什么类型,因为他可能是一个基类的对象,也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会知道到及执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
析构函数、静态类型函数、友元函数、内联函数与虚函数
1 class CTest 2 { 3 publi c: 4 /*virtual */CTest() 5 {} 6 /*virtual */stati c i nt FunTest() 7 {} 8 /*virtual */CTest& operator =(const CTest& _test) { 9 return *thi s; 10 } 11 /*virtual*/ fri end voi d FunTestFri end() ;
12 }
什么是虚函数
其实在前面的虚拟继承中我们已经用到了虚函数这个概念,在那里我们是为了解决菱形普通继承中访问二义性的问题,但在多态中,他有更大的作用。百度百科中对虚函数是这么说的:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。形象的解释为“求同存异”,它的作用就是实现多态性。
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
1 class A 2 { 3 public: 4 virtual void print(){cout<<"This is A"<<endl;} 5 }; 6 class B : public A 7 { 8 public: 9 void print(){cout<<"ThisisB"<<endl;} 10 }; 11 int main() 12 { 13 A a; 14 B b; 15 A *p1 = &a; 16 A *p2 = &b; 17 p1->print(); 18 p2->print(); 19 return 0; 20 }
析构函数与虚函数
当在析构函数前面加virtual关键字时报错:,我们来分析一下原因。
1、虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
2、构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。
静态类型函数与虚函数
当我们在静态类型函数前加virtual关键字时报错:分析:
1、 static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
2、static函数没有this指针,并且不会进入虚函数表的。当通过指针或者引用调用时根本无法把this指针传递给static函数,从而无法体现出多态。静态成员函数与普通成员函数的差别就在于缺少this指针,没有这个this指针自然也就无从知道name是哪一个对象的成员了。
友元函数与虚函数
当我们在友元函数前加virtual关键字时报错:
因为C++
不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
内联成员函数与虚函数
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline
函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
赋值运算符的重载与虚函数
当我们把赋值运算符的重载定义为虚函数时编译可以通过,但是一般不建议这么做虽然可以将operator=定义为虚函数, 但使用时容易混淆。
1、无法给派生类的自有成员赋值;
2、调用虚函数要进行查虚表等一系列操作,效率下降。
析构函数与虚函数
析构函数设为虚函数的作用:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
1 #include <stdafx.h> 2 #include <stdio.h> 3 class A 4 { 5 public: 6 A(); 7 virtual~A(); 8 }; 9 A::A() 10 {} 11 A::~A() 12 { 13 printf("Delete class APn"); 14 } 15 class B : public A 16 { 17 public: 18 B(); 19 ~B(); 20 }; 21 B::B() 22 { } 23 B::~B() 24 { 25 printf("Delete class BPn"); 26 } 27 int main(int argc, char* argv[]) 28 { 29 A *b=new B; 30 delete b; 31 return 0; 32 }
输出结果为:Delete class B Delete class A
如果把A的virtual去掉:那就变成了Delete class A也就是说不会删除派生类里的剩余部分内容,也即不调用派生类的虚函数
析构函数总结:
1. 如果我们定义了一个构造函数,编译器就不会再为我们生成默认构造函数了。
2. 编译器生成的析构函数是非虚的,除非是一个子类,其父类有个虚析构,此时的函数虚特性来自父类。
3. 有虚函数的类,几乎可以确定要有个虚析构函数。
4. 如果一个类不可能是基类就不要申明析构函数为虚函数,虚函数是要耗费空间的。
5. 析构函数的异常退出会导致析构不完全,从而有内存泄露。最好是提供一个管理类,在管理类中提供一个方法来析构,调用者再根据这个方法的结果决定下一步的操作。
6. 在构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。
7.在析构函数中也不要调用虚函数。在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。
8. 记得在写派生类的拷贝函数时,调用基类的拷贝函数拷贝基类的部分。
总结:
1、 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)。
2、 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3、 只 有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。
4、 如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、 构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆。
6、 不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、 最好将基类的析构函数声明为虚函数。(因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、 虚表是所有类对象实例共用的。