0%

专题知识点2:虚函数表(虚函数、析构函数、多态、重写)

2020年4月6日 下午10:34

为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

  1. 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏
  2. C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

C++中析构函数的作用

  1. 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
  2. 析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  3. 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
  4. 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
  5. 类析构顺序:
    1. 1)派生类本身的析构函数;
    2. 2)对象成员析构函数;
    3. 3)基类析构函数。

静态函数和虚函数的区别

  1. 静态函数在编译的时候就已经确定运行时机
  2. 虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

C++中static关键字的作用

  1. 代码块外部:对于函数定义和代码块之外的变量声明
    1. static修改标识符的链接属性,由默认的external变为internal
    2. 作用域和存储类型不改变,这些符号只能在声明它们的源文件中访问。
  2. 代码块内部:对于代码块内部的变量声明
    1. static修改标识符的存储类型,由自动变量改为静态变量,作用域和链接属性不变。
    2. 这种变量在程序执行之前就创建,在程序执行的整个周期都存在。
  3. 函数:对于被static修饰的普通函数
    1. 其只能在定义它的源文件中使用,不能在其他源文件中被引用
  4. 类:对于被static修饰的类成员变量和成员函数
    1. 它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。

虚函数和多态

  1. 多态的实现主要分为静态多态和动态多态
    1. 静态多态主要是重载,在编译的时候就已经确定;
    2. 动态多态是用虚函数机制实现的,在运行期间动态绑定。
    3. 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  2. 虚函数的实现
    1. ::在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。::
    2. 如果在基类中使用vitual虚函数,::那么编译器看的是指针的内容,而不是它的类型::。 C++ 多态 :一起看1

多态机制深入:

C++多态的实现方式总结_c/c++_小凡的专栏-CSDN博客
虚函数在C++中的实现机制就是用虚表和虚指针。也就是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{public:
virtual void f();
virtual void g();
private:
int a
};

class B : public A
{
public:
void g();
private:
int b;
};//A、B实现省略
  1. 因为A有virtual void f()和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:
    1
    2
    A::f 的地址
    A::g 的地址
  2. B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:
    1
    2
    A::f 的地址
    B::g 的地址
    • 注意:因为B: :g是重写了的,所以B的虚表的g放的是B: :g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
  • 然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:
    1
    2
    3
    vptr : 指向B的虚表vtableB
    int a: 继承A的成员
    int b: B成员

重载 重写

  1. 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
  2. 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写