回顾cpp-多态-虚函数表

封装->继承->多态

  • 普通虚函数 check
  • 虚析构函数 check
  • 纯虚函数
  • 抽象类
  • 接口类
  • RTTI
  • 异常处理
  • 隐藏 覆盖 check
  • 早绑定 晚绑定 check
  • 虚函数表 check

虚函数实现原理

虚函数实现原理是

函数指针:

  • 对象指针—>指针指向对象
  • 函数指针—>指针指向函数

函数存在内存中,可以通过指针指向这段代码的开头,那么函数就会从头一直向下执行。如图:

比如可以通过Func1_ptr 得到fun3()函数入口,并开始执行。函数指针与普通指针一样,存储着内存的地址,这个地址就是函数的首地址。


虚函数实现原理是虚函数表指针。假如有两个类ShapeCircle类:
父类Shape

1
2
3
4
5
6
7
8
9
class Shape{
public:
virtual ~Shape(){} // virtual 析构函数
virtual double calcArea(){ // 虚函数
return 0;
}
protected:
int m_iEdge;
};

此时,当实例化一个Shape对象时,Shape对象中包含如下左侧内容

Shape对象中,除了m_iEdge外,还有一个成员vftable_ptr称为虚函数表指针

  1. 这个指针指向一个虚函数表,此虚函数表与Shape类的定义同时出现
  2. 此表占空间,从起始位置0xCCFF处开始,也就是vftable_ptr这里存储内容为0xCCFF
  3. 此表只有一个,通过Shape实例化出的所有对象都指向同一个虚函数表,即所有通过Shape实例化的对象,其vftable_ptr都存储0xCCFF。也就是说每一个实例的vftable_ptr都指向Shape类的虚函数表。言外之意,虚函数表属于类,而非类对象

在父类Shape的虚函数表中一定定义了一个函数指针:calcArea_ptr,它是calcArea函数入口地址,即calcArea_ptr中存储0x33550x3355Shape类calcArea函数地址)。调用calcArea时,先通过vftable_ptr 找到虚函数表,再通过位置偏移找到虚函数的入口地址,从而最终找到calcArea计算面积。


上述过程实例化Shape父类对象。
当定义了子类Circle:

1
2
3
4
5
6
7
class Circle : public Shape{   // 共有继承Shape
public:
Circle(double r);
// 没有自己的calcArea
private:
double m_dR;
};

注意Circle中并没有calcArea()函数,也就说,Circle使用Shape类的calcArea计算面积
当实例化Circle子类对象后。此Circle对象中存储的内容如下:

因为Circle中没有定义虚函数,但它从父类中继承了虚函数calcArea,所以在实例化一个Circle对象时,也会产生一个虚函数表(这个虚拟性是从父类继承过来的)。注意此虚函数表是Circle类自己的虚函数表,起始地址为0x6688,而Shape类的虚函数表起始地址是0xCCFF。但是Circle虚函数表中计算面积的指针calcArea_ptr是一样的,都存储0x3355(因为是继承过来的)。

这就能够保证在Circle中访问父类Shape的calcArea时,也能够通过虚函数表指针找到自己的虚函数表,从而找到父类Shape的calcArea


如果子类Circle定义中定义了自己的calcArea函数,即子类的calcArea有自己的函数地址0x4B2C

1
2
3
4
5
6
7
class Circle : public Shape{   // 共有继承Shape
public:
Circle(double r);
double calcAera(); // 定义了自己的calcArea,覆盖父类calcArea
private:
double m_dR;
};

当实例化一个Circle类对象时,Shape类没有任何变化(当然,父类不会因为子类的变化而改变呀!),但是Circle会有变化:如图:

Circle虚函数表是一样的(即表地址是一样的),但是因为Circle自己定义了自己的calcArea方法,所以calcArea_ptr所指向的也是Circle自己的calcArea地址0x4B2C,换句话说,原先calcArea_ptr中父类的calcArea地址被Circle自己的calcArea地址覆盖。所以,此时如果使用Shape的指针指向Circle的对象,执行子类的虚函数calcArea。这便是virtual的功能

上述过程就是多态原理

函数的覆盖与隐藏

  • 覆盖即上述过程:

    当Circle没有自己的calcArea()时,Circle的虚函数表中calcArea_ptr存的是0x3355,即父类calcArea()的地址。

    当Circle有自己的calcArea()时,Circle的虚函数表中calcArea_ptr存的是0x4B2C,即子类自己的calcArea()的地址。
    此时虚函数表中,子类的虚函数地址覆盖父类虚函数地址

  • 隐藏与多态无关:

    与多态(virtual)无关,即当父类子类中没有virtual虚函数时,也就是说这个类我不希望他有多塔的性质,父类子类出现了同名函数,且父类指针分别指向子类对象,父类同名函数隐藏了子类同名函数。(毕竟这个子类对象类型是父类,回顾继承篇

敲黑板首先判断需不需要这个父类有多态的性质,如果需要,这个父类中一定要有virtual函数,才会有虚函数表。虚函数表属于类,而非对象。

虚析构函数原理

回顾上一篇笔记,当父类中析构函数都为virtual析构函数时,通过父类指针指向子类对象,最后通过delete 接父类指针就可以,先执行子类析构函数紧接着执行父类析构函数。这个过程与虚函数表有关。

强调一下前提:先执行子类析构函数紧接着执行父类析构函数(也就是说通过父类指针执行到了子类的析构函数,这就是为什么先执行子类析构函数)

在父类Shape中加上虚虚析构函数:

1
2
3
4
5
6
7
8
9
class Shape{
public:
virtual double calcAera(){ // 虚函数
return 0;
}
virtual ~Shape(){} // 虚析构函数
protected:
int m_iEdge;
};

定义子类,写上子类虚析构函数:

1
2
3
4
5
6
7
8
class Circle : public Shape{   // 
public:
Circle(double r){};
virtual double calcAera(){};
virtual ~Circle(){}; // 子类虚析构函数
private:
double m_dR;
};

此时用父类指针指向子类对象,且delete接父类指针:

1
2
3
4
5
6
7
int mian(){
Shape* shape = new Circle(1.0);

delete shape;
shape = NULL;
return 0;
}

虚函数表如何工作:

当在Shape中定义了虚析构函数,Shape类的虚函数表中就会有一个Shape类的虚函数指针~Shape_ptr(指向父类析构函数),

同时在Circle类中也会产生一个子类析构函数的函数指针~Circle_ptr(指向子类析构函数)。注意上述两个析构函数指针同时出现

如果使用Shape的指针指向Circle的对象,执行子类的虚函数calcArea。当delete接Shape指针时,通过Shape找到Circle类的vftable_ptr(Shape类指针指向Circle类对象),进而找到虚函数表,最后找到Circle类的析构函数,从而使得Circle类析构函数得意执行。最后执行Shape类析构函数

{有疑惑,父类指针指向子类对象,会执行子类虚函数?(delete父类的指针时,程序会去找父类的指针指向的地址,该地址就是子类头部虚函数表指针的地址,进而找到子类虚函数表,最后执行子类析构函数) delete时,发生了什么?回顾继承篇}

现实中的虚函数

明确概念:

  1. 对象的大小:实例化对象中数据成员所占内存大小,(不包括成员函数)。
  2. 对象的地址:通过类实例化一个对象,这个对象的所占的内存单元的首地址。
  3. 对象成员的地址:一个对象中每一个成员所占据的地址。因为每个成员的数据类型不同,所以占用不同大小的内存。
  4. 虚函数表指针:当实例化一个对象后,这个对象的第一个内存中所存储的指针,这个指针就是虚函数表的指针。就是上述所有的vftable_ptr。可以根据这个特点,通过计算对象的大小来证明虚函数表示的存在。

当没有virtual时

假如有两个类:

父类Shape:

1
2
3
4
5
6
7
8
class Shape{   
public:
Shape(){}
~Shape(){}
double calcArea(){
std::cout<<"Shape->calc area"<<std::endl;
}
};

子类Circle:

1
2
3
4
5
6
7
class Circle : public Shape{
public:
Circle(int r){ m_iR = r; }
~Circle(){}
private:
int m_iR;
};

执行1:

1
2
3
4
Shape shape;                  // 实例化一个对象
cout<<sizeof(shape)<<endl; // 该对象的大小
int* p = (int*)&shape; // 该对象的地址
cout<<p<<endl; // 对象起始地址

结果1:

1
2
1                 // 对于一个数据成员都没有的类对象,c++ 用一个内存单元来标记它。 
0x7fff478ce38f // 对象起始地址

执行2:

1
2
3
4
5
Circle circle(100);
cout<<sizeof(circle)<<endl;
int* q= (int*)&circle;
cout<<q<<endl; // 指针q中内容
cout<<(unsigned int)(*q)<<endl; // 指针q中内容的内容

结果2:

1
2
3
4                  // int 型数据占4个内存单元
0x7ffdca8b3e20 // 对象起始地址
100 // 起始地址中的内容

当有virtual时

父类与子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape{
public:
Shape(){}
virtual ~Shape(){}
virtual double calcArea(){ } // 虚函数
};

class Circle : public Shape{
public:
Circle(int r){ m_iR = r; }
virtual ~Circle(){}
private:
int m_iR;
};

执行:

1
2
3
4
5
6
7
8
9
10
11
Shape shape;
cout<<sizeof(shape)<<endl;
int* p = (int*)&shape;
cout<<(unsigned int)(*p)<<endl;

Circle circle(100);
int* q= (int*)&circle;
cout<<(unsigned int)(*q)<<endl;
q++;
q++;
cout<<(unsigned int)(*q)<<endl;

返回:

1
2
3
4
8              
4198160
4198120
100

shape对象大小是8,Shape类对象地址中第一个内容是虚函数表地址4198160
Circle类对象地址中的第一个内容是虚函数表地址4198120,之后移动指针2次,便是存储数据成员100的位置。