多态实现的基础是虚函数。
首先最常见的一个问题就是:一个类的构造函数可不可以是虚函数?
嗯,这个答案当然是否定的,一个类的构造函数它不可能是虚函数。
我们知道虚函数是多态的基础,谈到多态就必须要讲到虚函数表(vtable),
子类和父类通过调用虚函数表不同位置的函数指针,来实现对同一个同名函数的不同调用。
那么一个对象如何去找到虚表呢?
就是通过对象中的虚函数表指针。
虚函数表指针是位于对象内存空间首位的一个指针,它指向虚函数表的位置。
虚函数表指针只有在对象被构造之后,也就是对象的构造函数被调用之后,它才会被生成。如果构造函数被设置为了虚函数,那么就进入了一个死锁。
我们来看一个例子来证明虚函数表指针的存在:
class Person {
public:
Person();
int water;
void flow();
void drink();
};
sizeof(person) 为 4
将flow和drink函数变为虚函数后:
class Person {
public:
Person();
int water;
virtual void flow();
virtual void drink();
};
sizeof(person) 为 16
由于函数被设置为了虚函数,所以这个对象内存头部会增加一个虚函数表指针。
虚函数表指针的大小为8个字节,但为什么这个对象的总大小由4个字节变成了16个字节呢?
这里面涉及到了内存对齐的问题,整个对象的占用大小必须是对象内部最大成员的整数倍。
64位系统的指针占用8个字节,int变量占用4个字节,4 + 8 = 12,整体占用必须是8的倍数,所以为16字节。
这里附上内存对齐的二原则:
- 前面的地址必须是后面的地址正数倍,不是就补齐。
- 整个对象占用空间必须是最大字节的整数倍。
这里拓展一下虚函数表
虚函数表是装着函数指针的数组,每一个类都会有自己的虚函数表,注意这里说的是每一个类。
子类和父类都会有一张虚函数表,子类会继承父类的虚函数表,然后将父类中的虚函数覆盖重写为自己所实现的函数,这样去调用的时候就会找到自己想要实现的函数,也就实现了多态。
这里生成三个类,有一个线性的继承关系:
using namespace std;
class Person {
public:
Person();
int gender;
virtual void flow() ;
virtual void drink();
virtual ~ Person();
};
class Male : public Person {
public:
Male(){
height = 175;
}
int height;
int hair;
~ Male() {
cout << "析构Male" << endl;
}
};
class Child : public Male {
public:
~Child() {
cout << "析构child" << endl;
}
};
PS:可以打印出虚表的内存地址:
using namespace std;
int main()
{
Person* person = new Person;
Male* male = new Male;
Child* child = new Child;
std::cout << "person虚表地址: " << (int *) * (int *)(&person) << std::endl;
std::cout << "male虚表地址: " << (int *) * (int *)(&male) << std::endl;
std::cout << "child虚表地址: " << (int *) * (int *)(&child) << std::endl;
return 0;
}
结果:
person虚表地址: 0x6bc01700
male虚表地址: 0x6bc01710
child虚表地址: 0x6bc01730
平台:macos clang
三个虚表的内存地址不同,说明是不同的虚表,注意虚表的位置一般位于.data段,是一个全局变量,不同平台可能位置不同,但一般都是一个全局变量。
析构函数可以是虚函数吗?
那么析构函数可不可以是虚函数呢?这个答案是肯定的,而且在有继承的情况下,父类析构函数必须要被设置为虚函数,要么可能就会存在内存泄漏的问题。
这是为什么呢?因为子类对象是包含有父类对象的全部信息的,在析构子类对象的时候,需要将父类部分占用内存一并释放掉。
调用子类的析构函数会同时虚构其父类对象,这是一种编译器决定的析构顺序。析构函数的调用次序是先析构子类的对象,然后再去虚构父类的对象。
如果父类的析构函数不被设置为虚函数的话,那么如果有继承发生,析构时就只会析构父类的对象。
int main()
{
Person * person = new Child();
delete person;
return 0;
}
结果:
析构child
析构Male
析构Person
那么其实还有一个问题,为什么只能用父类的指针去指向子类的对象而不能反过来?
如果从感性的角度去解释的话,那么就是子类对象也是父类对象,但是父类对象并不是子类对象。
男人都是是人,但不能说人都是男人。
如果在代码中强制定义一个子类的指针去指向父类的对象,那么就会报错,但是我们可以通过强制类型转换的方式来通过编译,但需要注意的是这种行为是非常危险的,同时它会带来很多困惑。
我们用强制类型转换进行一个小测验:
其中flow是虚函数
class Person {
public:
Person();
int gender;
virtual void flow();
virtual void drink();
virtual ~ Person();
};
class Male : public Person {
public:
Male(){
height = 175;
}
int height;
int hair;
~ Male() {
cout << "析构Male" << endl;
}
};
class Child : public Male {
public:
Child() {
cout << "构造child" << endl;
height = 130;
}
~Child() {
cout << "析构child" << endl;
}
void flow() {
cout << "child flow" << endl;
}
};
int main()
{
Child* child = (Child *)(new Person);
child->flow();
cout << "height值为 " << child->height;
return 0;
}
结果:
构造person
person flow
height值为 0
结果说明调用的是person的构造函数,并没有调用child的构造函数,flow函数调用的也是person的虚函数。child指针中的height值为0,其值也并不是父类中的height值,而是一个未经构造过的默认值。
结论:
定义一个子类指针,指向一个被强制类型转换成子类的父类对象,其虚函数表仍为父类的,其this指针仍然是指向子类的指针,而且这个this指针指向的是未经构造函数构造过的,采用默认值的对象。
这种操作容易带来很多困扰,所以不要这样搞。
纯虚函数
其实写到这里就剩下最后一个纯虚函数没有涉及到了,在类中把一个函数定义为纯虚函数,那么这个类是不能被实体化的。
感性来讲就是如果说类为是"人类"的话,它没有血肉,只是一个概念,所以不会形成实体。
对于父类中的纯虚函数,子类必须重新实现,这样才能够被实例化。一个没有实现父类纯虚函数的类是不能够被实例化的。但是同样可以用父类的指针指向子类对象。同时,父类的构造函数还会被调用,很神奇。
class Male : public Person {
public:
Male(){
cout << "构造person" << endl;
}
int height;
int hair;
~ Male() {
cout << "析构Male" << endl;
}
};
class Child : public Male {
public:
Child() {
cout << "构造child" << endl;
height = 130;
}
~Child() {
cout << "析构child" << endl;
}
void flow() {
cout << "child flow" << endl;
}
};
using namespace std;
int main()
{
Person* peron = new Child;
peron->flow();
return 0;
}
结果:
构造person
构造child
child flow
假如直接构造Person对象会发生什么?
假如子类没有实现纯虚函数会发生什么?