目录
一.多态
什么是多态?先来直观感受一下
struct person {//普通类
virtual void ticket() { cout << "全票" << endl; }
int _id;
};
struct stutent:public person {//学生类,继承自person类
virtual void ticket() { cout << "半票" << endl; }
int _num;
};
void display(person* p) {//调用票价函数
p->ticket();
}
int main() {
person* Jige = new person;//分别定义一个person和stutent对象
stutent* Caixukun = new stutent;
display(Jige);
display(Caixukun);
}
运行结果如下:
注意:display函数传入的参数是一个父类对象的指针,同样是去调用一个父类对象的成员函数,为什么会有不同的展示结果?
这里解释一下对于传入Caixukun对象:Caixukun对象是一个stutent类,根据继承的赋值兼容特性,display函数是把Caixukun切片作为变量,切片是一个父类对象。
这就是多态的特点:指向谁调用谁,同一事件不同结果
二.多态两要素
如何实现多态呢?或者说如何去写一个多态调用呢?看下面
多态有两个要素:
- 构成虚函数重写(virtual关键字)
- 父类的指针或引用去调用虚函数
虚函数重写:
父类和子类函数名、参数类型、返回值类型完全相同,且父类加上virtual关键字。
虚函数重写的特例:
1.返回值协变。
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变是编译器自动识别,也满足多态。
比如:
struct person {
virtual person* f() { return new person; }
int _id;
};
struct stutent:public person {
virtual stutent* f() { return new stutent; }
int _num;
};
2.析构函数
析构函数经过编译后函数名都统一处理为destructor。父类和子类的析构函数不用加virtual关键字也构成重写,而且即使函数名不同,也构成重写。
这是为什么呢?
如果析构函数不满足重写:那下面这种情况就极有可能发生内存泄漏
struct person {
~person() { cout << "~person()" << endl;; }
int _id;
};
struct stutent:public person {
~stutent() { cout << "~stutent()" << endl;; }
int _num;
};
int main() {
person* Jige = new person;
person* Caixukun = new stutent;//两个不同类型对象都用perosn指针来接收
delete Jige;
delete Caixukun;
}
我们知道delete是封装的free()函数和对应的析构函数的,如果是指向父类对象,this->person::destructor就会去调用父类的析构,指向派生类,this->stutent::destrcutor就会去调用子类以及自动调用父类析构;如果是通过派生类切片来释放空间,因为是person类指针,就只会去调用父类的析构,而不会析构子类,造成内存泄漏。
new 和 new[]是对malloc和构造函数的封装,delete和delete[]是对free和析构函数的封装。调用delete函数时会把对象指针传给this,通过this调用其析构函数。
加上virtual关键字构成重写后,就可以避免这个问题
struct person {
virtual ~person() { cout << "~person()" << endl;; }
int _id;
};
struct stutent:public person {
virtual ~stutent() { cout << "~stutent()" << endl;; }
int _num;
};
int main() {
person* Jige = new person;
person* Caixukun = new stutent;//两个不同类型对象都用perosn指针来接收
delete Jige;
delete Caixukun;
}
3.派生类可以不写virtual
在实现多态时,派生类可以不写virtual关键字,但父类必须写。
三.多态实现原理
多态这么神奇,作为面向对象三大特性之一,是如何实现指向谁调用谁的呢?
关键在于一个结构:虚函数表(简称虚表)
虚函数表
虚函数表是存储虚函数地址的一张表(类似于函数指针数组),如果一个类中有虚函数,就会生成虚函数表,存储虚函数地址。
我们先来观察一下虚函数表在对象中的存储结构。
一个person类占8个字节的内存,并不是只有一个int类型变量,一个stutent类占12个字节,好像都比预期值多了4个字节;通过监视窗口我们可以发现类中还有一个_vfptr数组,而这个_vfptr表就占4个字节,而且最先声明。实际上这就是虚函数表,每个元素就是虚函数地址。
可以发现_vfptr中元素的值就是地址,且和内存窗口中看到的地址相同,这就是虚函数对应的地址。
这是父类的内存结构,那么派生类呢?虚表也会被继承吗?
可以看到,派生类只有一张虚表,但是注意虚表中是重写后的虚函数地址,通过观察前面的域作用限定符stutent::可以发现。所以得出结论:继承后父类的虚表也会被继承,且共用同一张虚表,如果虚函数构成重写则覆盖父类的虚函数。
多态
现在我们可以知道多态是如何实现的了。
重写的虚函数被放进虚表中,通过指向不同对象的父类指针或引用调用虚函数时,会在虚表中找对应的虚函数地址,然后call虚函数,由于重写后父类和子类中虚表不同,如果是指向派生类对象就调用派生类重写的虚函数,如果是指向父类对象就调用父类虚函数从而实现多态调用,伟大的面向对象!!!。
四.虚表详解
void display(person* p) {
p->ticket();
}
struct base {
virtual void func1() { cout << "void base::func1()" << endl; }
virtual void func2() { cout << "void base::func2()" << endl; }
int a;
};
struct derive :public base{
virtual void func2() { cout << "void derive::func2()" << endl; }
virtual void func3() { cout << "void derive::func3()" << endl; }
virtual void func4() { cout << "void derive::func4()" << endl; }
int b;
};
int main() {
base a;
derive b;
a.a = 1;
b.a = 1;
b.b = 2;
}
派生类derive有4个虚函数,但是在虚表中只有两个重写的虚函数,这并不是矛盾的,要怪就怪VS编译器的监视窗口。这里最好还是用内存窗口去观察。
在虚表中,很明显前8个字节是func1()和func2()的地址,后面8个字节也很像指针,但是又不能观察,怎么办呢?
这里提出一个方法:打印虚表
打印虚表
打印虚表需要虚函数地址,而我们一旦得到虚函数地址,不仅可以打印,还可以调用,如果不是函数地址的话调用就会崩溃。所以这样我们就可以验证后面八个字节是不是虚函数地址了。
把循环改成4个,试试调用后面8个字节
简直是打开了新世界,验证了后面8个字节就是派生类虚函数地址。
所以派生类中的虚表继承自基类,且共用一张虚表。
struct base {
virtual void func1() { cout << "void base::func1()" << endl; }
virtual void func2() { cout << "void base::func2()" << endl; }
int a;
};
struct derive :public base{
virtual void func2() { cout << "void derive::func2()" << endl; }
virtual void func3() { cout << "void derive::func3()" << endl; }
virtual void func4() { cout << "void derive::func4()" << endl; }
int b;
};
typedef void (*vfp)();
void print_VFPTR(vfp*vf) {
for (int i = 0; i < 4; i++) {
printf("0x%p\n", vf[i]);
vfp p = vf[i];
p();
}
}
int main() {
base a;
derive b;
a.a = 1;
b.a = 1;
b.b = 2;
vfp* p = (vfp*)(*(int*)(&b));
print_VFPTR(p);
}
五.抽象类
函数头部 =0 的函数称为纯虚函数,包含纯虚函数的类称为抽象类,抽象类不能实例化。顾名思义,抽象就是不存在的东西,所以抽象类也不能被实例化出对象。
如果继承抽象类,派生类也是抽象类。所以抽象类的意义就在于强制重写纯虚函数。
还有个意义,就是通过指针或引用进行多态调用,说明抽象类可以定义指针和引用。
六.final和override关键字
final
在父类虚函数后加final关键字表示该虚函数不能被重写
如何使一个类无法被继承?
把父类构造函数放进私有中,屌不屌!!!
使用final关键字修饰类,表示最终类,不能被继承
override
用override可以检查是否重写成功,不成功会报错。
封装的特性我们下次再写一篇博客具体介绍一下,对于面向对象之多态的特性就先到这里,欢迎广大读者提出建议和指正。