关于C++虚函数的一些东西

时间:2022-08-22 01:02:11

先上概念,C++的多态性:系统在运行时根据对象类型,来确定调用哪个重载的成员函数的能力。

多态性是通过虚函数实现的。成员函数之前加了virtual,即成为虚函数。

有虚成员函数的类,编译器在其每个对象的开始处自动加一个指针,称为虚表指针,因为它指向一个表,称为虚函数表,表的元素是函数指针,指向该类的虚成员函数代码块。

该类的所有对象共享一张表。关于虚表指针和虚函数表的具体信息,可以参考皓叔的  虚函数表解析

 

虚函数的定义要遵循以下规则: 

1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数或者返回类型不同,那么即使加上了virtual关键字,也不会实现多态的。
【此时基类/派生类对象只能直接访问各自定义的函数,虽然派生类对象的虚表里有基类的虚函数指针,但是派生类对象不能直接调用】

 

2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。

 

3.静态成员函数不能是虚函数,因为静态成员函数是属于类的,不属于任意对象,只作用在类的静态变量上。
【访问虚函数需要通过对象的虚表指针访问虚表,来获得虚函数入口】
【静态成员函数也不能是const成员函数,因为编译器会在对象的const函数中自动插入一个const T *this参数,而静态成员函数不属于对象】

 

4.内联(inline)函数不能是虚函数。即使虚函数在类的内部定义定义,编译的时候系统仍然将它看做是非内联的。
【内联函数在编译时可能会展开代码,这样内存的代码区就没有该函数的代码了,已经不是一个函数的概念了,自然虚表里面也没法保存函数指针了】

 

5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。

 

6.析构函数可以是虚函数,而且通常声名为虚函数。
【有派生类的基类,其析构函数必须为虚函数,这样析构的时候会先析构派生类对象,再析构基类对象,否则派生类的部分就没被析构】

 

看到上面的规则1,有三个概念要注意: 

Overload(重载):将语义、功能相似的几个函数用同一个名字表示,但<参数>或<参数与返回值都>不同(参数个数、类型或顺序不同),即函数重载。
(1)相同的范围(在同一个类中或同一个文件内的普通函数);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

Override(覆盖):是指派生类函数覆盖基类函数,只能是虚函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)返回值参数相同;
(4)基类函数必须有virtual 关键字。

Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 【规则1】
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

  

以下的例子转自 何海涛的博客

  

一个失去多态性的例子:在构造函数中调用虚函数

 1 class A  2 {  3 public:  4         A()    //自定义默认构造函数
 5  {  6    Print();  7  }  8         virtual void Print()  9  { 10           cout << "A is constructed." << endl; 11  } 12 }; 13  
14 class B: public A 15 { 16 public: 17  B() 18  { 19    Print(); 20  } 21  
22         virtual void Print()    //“覆盖”,但实际可能不是这样
23  { 24           cout << "B is constructed." << endl; 25  } 26 }; 27 
28 int main(int argc, char *argv[]) 29 { 30     A *pa = new B;  //加括号或者括号有参数的,调用相应的自定义的构造函数,没有括号,则调用默认构造函数或者唯一的构造函数
31  delete pa; 32 
33     return 0; 34 }

以上代码输出

1 A is constructed. 2 B is constructed.

B的构造函数时,会先调用B基类A的构造函数。

A的构造函数里调用了Print,由于此时对象的类型B的部分还没有构造好,本质上它只是A的一个对象,其虚表指针指向的是类型A的虚函数表。

接着调用类型B的构造函数,并调用Print。此时已经开始构造B,因此此时调用的PrintB::Print

因此虚函数在构造函数中调用时,已经失去了虚函数的动态绑定特性。

 

普通成员函数中调用虚函数保持多态性

class A { public: void print() { doPrint(); //调用虚函数
 } virtual void doPrint() { cout << "A::doPrint" << endl; } }; class B: public A { public: virtual void doPrint()    //虚函数覆盖
 { cout << "B::doPrint" << endl; } }; int main(int argc, char *argv[]) { A a; a.print(); B b; b.print(); return 0; }

以上代码输出

1 A::doPrint 2 B::doPrint

在print中调用doPrint时,doPrint()的写法和this->doPrint()是等价的,因此将根据实际的类型调用对应的doPrint

 

虚函数的缺省参数

 1 class A  2 {  3 public:  4     virtual void fun(char c = 'A')    //缺省参数  5  {  6         cout << "A::fun " << c << endl;  7  }  8 };  9 
10 class B: public A 11 { 12 public: 13     virtual void fun(char c = 'B')    //缺省参数,虚函数覆盖 14  { 15         cout << "B::fun " << c << endl; 16  } 17 }; 18 
19 int main(int argc, char *argv[]) 20 { 21  B b; 22     A &a = b; 23 
24     a.fun();    //基类引用派生类对象,动态绑定 25 
26     return 0; 27 }

以上代码输出

1 B::fun  A

由于基类的a是一个指向B对象的引用,因此在运行的时候会调用B::Fun。动态绑定的过程。

缺省参数是在编译期决定的。

编译时,编译器只知道a是一个类型A的引用,具体指向什么类型在编译期是不能确定的,因此会按照A::fun的声明把缺省参数c设为'a'。