通过下面的代码来说明:
#include <iostream>
#include <stdlib.h>#include <string>
using namespace std;
/**
* 定义动物类:Animal
* 成员函数:eat()、move()
*/
class Animal
{
public:
// 构造函数
Animal(){cout << "Animal" << endl;}
// 析构函数
virtual ~Animal(){cout << "~Animal" << endl;}
// 成员函数eat()
void eat(){cout << "Animal -- eat" << endl;}
// 成员函数move()
void move(){cout << "Animal -- move" << endl;}
// int m_iLegs;
// short m_hands;
// char m_face;
// int m_iHead;
};
class A
{
public:
A(){cout<<"A"<<endl;}
virtual ~A(){cout<<"~A"<<endl;}
};
/**
* 定义狗类:Dog
* 此类公有继承动物类
* 成员函数:父类中的成员函数
*/
class Dog : public Animal,public A
{
public:
// 构造函数
Dog(int legs)
{
cout << "Dog" << endl;
m_iLegs = legs;
}
// 析构函数
~Dog(){cout << "~Dog" << endl;}
// 成员函数eat()
void eat(){cout << "Dog -- eat" << endl;}
// 成员函数move()
void move(){cout << "Dog -- move" << endl;}
public:
int m_iLegs;
};
int main(void)
{
// 通过父类对象实例化狗类
// Animal * p = new Dog;
// Animal animal;
// cout<<sizeof(animal)<<endl;
// int * q = (int * )&animal;
// cout<<*(q)<<endl;
Dog dog(4);
cout<<sizeof(dog)<<endl;
int * p = (int*)&dog;
cout<<&dog<<endl;
cout<<p<<endl;
p++;
cout<<p<<endl;
p++;
cout<<"p++: "<<p<<endl;
cout<<(unsigned int)(*p)<<endl;
// cout<< dog.m_iLegs<<endl;
// cout<< (int *)p<<endl;
// 调用成员函数
// p->eat();
// p->move();
// 释放内存
// delete p;
// p = NULL;
return 0;
}
当两个virtual都不加的时候,输出结果:
Animal
A
Dog
4
0x7ffc94887060
0x7ffc94887060
0x7ffc94887064
p++: 0x7ffc94887068
2491969640
~Dog
~A
~Animal
分析:此时,由于函数不占用对象内存大小,有专门的程序代码区来存放程序的二进制代码,因此Dog对象dog的内存大小只是其成员变量的大小,此时有一个int型的成员变量m_iLegs,因此此时输出的sizeof(dog)的大小是4个字节
当加上其中一个virtual以后,比如Animal的析构函数成了虚析构函数,输出结果:
Animal
A
Dog
16
0x7fffaf8dd880
0x7fffaf8dd880
0x7fffaf8dd884
p++: 0x7fffaf8dd888
4
~Dog
~A
~Animal
分析:此时,Dog从Animal继承,因为虚析构函数的特性是可以继承的,所以dog的析构函数也是虚析构函数,虽然没写virtual,但是编译器会自动为其加上virtual。这样,Dog的对象的内存中除了存放了自己的成员变量以为,还存放了一个虚函数表指针,在64位机器上,指针大小为8个字节,又因为高位对齐原则,所以这一次输出的sizeof(dog)的值得大小是16个字节,(8(指针)+4(int)+4空内存)
如果再将类A的virtual加上,其实不光是析构函数,声明其他函数为虚函数也一样,这时,输出的结果如下:
Animal
A
Dog
24
0x7ffe2707d310
0x7ffe2707d310
0x7ffe2707d314
p++: 0x7ffe2707d318
4198544
~Dog
~A
~Animal
分析:此时Dog类的对象中将含有两个虚函数表,输出的结果将是两个虚函数指针加整型成员变量+4个空内存的大小=24个字节了。
这里注意两点:
(1)高位对齐!就是输出的大小都是高位的整数倍,这个例子中都是8的整数倍;
(2)在对象的内存空间中,按照继承顺序,指向继承来的两个虚函数表的虚函数表指针将排在第一位和第二位,占据前16个字节,然后才是int型的成员变量占用四个字节,所以p要p++四次才能到达int型变量的首地址,才能输出正确的值4,p++一次前进4个字节。如下代码所示:
Dog dog(4);
cout<<sizeof(dog)<<endl;
int * p = (int*)&dog;
cout<<&dog<<endl;
cout<<p<<endl;
p++;
cout<<p<<endl;
p++;
cout<<p<<endl;
p++;
cout<<p<<endl;
p++;
cout<<"p++: "<<p<<endl;
cout<<(unsigned int)(*p)<<endl;
输出结果为:
Animal
A
Dog
24
0x7ffe12b78160
0x7ffe12b78160
0x7ffe12b78164
0x7ffe12b78168
0x7ffe12b7816c
p++: 0x7ffe12b78170
4
~Dog
~A
~Animal
1.总结虚函数的实现原理:
当类中有虚函数或者虚析构函数时,在实例化类的对象时,对象内存中除了成员变量的大小,还有一个虚函数表指针,而且虚函数表指针放在内存的最前面,虚函数表指针会指向一个虚函数表,而以为Shape类中含有虚函数,这个虚函数表将于Shape类的定义同时出现,在计算机中虚函数表也是占用一定到的内存空间的,且虚函数表由于一旦产生就具有不变性,所以编译器就会经量把它放到稳定(或者说是只读)的内存区。虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata)。
在上例中虚函数表的起始位置是0xCCFF,那么虚函数表指针的值就是0xCCFF,每个类只有一张虚函数表,所有类的对象都共用同一张虚函数表。所有对象都含有相同的虚函数表指针值0xCCFF,以确保所有的对象含有的虚函数表指针都指向正确的虚函数表。虚函数表中存放的是所有的虚函数地址。计算时,先找到虚函数表指针,通过指针的偏移找到虚函数的指针,然后就可以调用虚函数。
上例图中,Circle派生自Shape,Circle从父类派生了虚函数,于是它也有了自己的虚函数表,这两个表的起始地址是不一样的,但是表中calcArea()函数的起始地址是一样的,这也就是说,两张不同的虚函数表中的函数指针可能指向同一个函数。
当Circle类定义了自己的虚函数,如下图所示,
由于此时,Circle类自己定义了calcArea()函数,所以将会覆盖掉父类的函数。
2.总结虚析构函数的是实现原理:
虚析构函数的特点是当将父类的析构函数声明为虚析构函数以后,再用父类的指针去指向子类的对象,并用delete去销毁父类的指针的时候,不会再只调用父类的析构函数,而是会先调用子类的析构函数再调用父类的析构函数,即会释放掉子类对象了,不会再因为子类对象得不到释放而产生内存泄露。这种情况也和虚函数表有关。实现过程如下:当声明父类析构函数为虚析构函数以后,在子类和父类的虚函数表中将都出现虚析构函数的函数指针,如下两幅图所示。当用父类的指针指向子类的对象,用delete Shape释放对象时,会通过Shape指针找到子类Circle的虚函数表指针,从而找到虚函数表,从而通过偏移找到虚析构函数的地址,从而调用子类Circle的析构函数,然后也会调用父类的析构函数。
多态的实现原理如下:当用父类的指针去指向子类对象时,会拿到子类的虚函数表指针,然后找到虚函数表,通过虚函数表指针的偏移,找到要调用的虚函数的函数指针,从而实现函数的调用。注意这里的偏移必须是和父类的偏移量是一样的。
与本节内容有关,补充两个概念:函数的隐藏和覆盖
函数的隐藏:没有定义多态的情况下,即没有加virtual的前提下,如果定义了父类和子类,父类和子类出现了同名的函数,就称子类的函数把同名的父类的函数给隐藏了。
函数的覆盖:是针对多态来说的。如果定义了父类和子类,父类中定义了公共的虚函数,如果此时子类中没有定义同名的虚函数,那么在子类的虚函数表中将会写上父类的该虚函数的函数入口地址,如果在子类中定义了同名虚函数的话,那么在子类的虚函数表中将会把原来的父类的虚函数地址覆盖掉,覆盖成子类的虚函数的函数地址,这种情况就称为函数的覆盖。