目录
1 多态性
1.1 编译整体过程
1.2 两种形式的多态
2 相关概念
2.1 虚函数
2.1.1 虚函数实现原理
2.2.2 虚函数为何不可以声明为 inline ?
2.2.3 构造函数为什么不能为虚函数?
2.2.4 析构函数为什么要虚函数?
2.2.5 构造函数和析构函数可以调用虚函数吗,为什么?
2.2.6 哪些函数不能是虚函数?
2.2 接口继承与实现继承
1 多态性同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。
在C++中,多态性的实现和联编(也称绑定)这一概念有关。C++支持两种多态性:编译时多态性,运行时多态性。
1.1 编译整体过程- 1. 预处理:包含宏替换,条件编译,include导入文件(针对C/C++)
- 2. 编译: 包含词法分析,语法分析,语义分析,中间代码生成与优化,生成汇编文件
- 3. 汇编: 将汇编文件编译成2进制的机器码
- 4. 链接: 将目标文件与外部符号进行链接,得到一个二进制可执行文件
联编:一个源程序经过编译、链接,成为可执行文件的过程。
函数的联编:在编译或运行将函数的调用,与相应的函数体连接在一起的过程。
- 静态联编(前期联编):在运行之前就完成的联编称之为~。
- 静态联编支持的多态性称为编译时多态性(静态多态性)。
- 编译时多态性:在C++中,编译时多态性是通过函数重载和模板实现的。
- 比如利用函数重载机制,在调用同名函数时,编译系统会根据实参的具体情况,确定索要调用的是哪个函数。
- 比如当编译器遇到一个模板定义时,它并不生成代码。只有当遇到实例化出模板的一个特定版本时,编译器才会生成代码。
- 编译时多态性:在C++中,编译时多态性是通过函数重载和模板实现的。
- 静态联编支持的多态性称为编译时多态性(静态多态性)。
- 动态联编(后期联编):在程序运行之时才完成的联编称之为~
- 动态联编所支持的多态性称为运行时多态(动态多态)。
- 运行时多态性:在C++中,运行时多态性是通过虚函数来实现的。
- 比如运行基类指针指向派生类的对象(Animal*a=new Dog(); a->eat();),以调用派生类的函数。
- 那么在继承中要构成多态还需要两个条件:
- a. 调用函数的对象必须是指针或者引用。
- b. 被调用的函数必须是虚函数,且完成了虚函数的重写。
- 运行时多态性:在C++中,运行时多态性是通过虚函数来实现的。
- 动态联编所支持的多态性称为运行时多态(动态多态)。
虚函数是在基类中被声明为 virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载。
- 虚函数实现原理:虚函数表和虚函数指针。
抽象类:包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
- 纯虚函数: virtual int fun() = 0;
- 纯虚函数引入原因?
- 1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
- 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以 派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。
每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:
x86下:sizeof(B) = 8 (因为long的4 + vptr的4)
相关概念:static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 是通过 this 指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
2.2.2 虚函数为何不可以声明为 inline ?虚函数用于实现运行时的多态。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。总之,虚函数要求在运行时进行类型确定,而内敛函数要求在编译期完成相关的函数替换;
2.2.3 构造函数为什么不能为虚函数?a. 从存储空间角度,指向 vtable 虚函数表的指针是存储在对象的内存空间的。假设构造函数是虚的,但是对象还没有实例化,也就是内存空间还没有,怎么找 vtable 呢?所以构造函数不能是虚函数。
b. 从使用角度,构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
2.2.4 析构函数为什么要虚函数?a. 从使用角度,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
b. 因此,C++中基类采用 virtual 虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时,就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
2.2.5 构造函数和析构函数可以调用虚函数吗,为什么?1) 在 C++中,不提倡在构造函数和析构函数中调用虚函数;
2) 构造函数和析构函数调用虚函数时都不使用动态联编,并不会发挥虚函数动态绑定的特性,跟普通函数没区别;此时,则运行的是为构造函数或析构函数自身类型定义的版本;
3) 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数是不安全的,故而 C++不会进行动态联编;
4) 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。
2.2.6 哪些函数不能是虚函数?- 1) 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
- 2) 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
- 3) 静态函数,静态函数不属于对象属于类,静态成员函数没有 this 指针,因此静态函数设置为虚函数没有任何意义。
-
4) 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
- 5) 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
- 实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现本身(自己使用父类的方法,不需要在子类中再定义)。
-
接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。