声明:
以下操作在x86程序中,涉及的指针都是4bytes。如果要其他平台下,部分代码选需要改动,如果是x64程序,则需要考虑指针是8bytes问题等等
一、多态概念
多态是指多种形态,完成某个行为,当不同的对象去完成时会产生出不同的状态
具体样例:买票-对于不同对象完成一件事,不同的状态
- 成人票:30$
- 学生票:15$
- 军人票:优先买票
二、多态使用及实现
2.1 多态构造条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person,当Person对象买票全价,Student买票半价。
构成多态需要三个条件:
- 多态是发生在继承关系中
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:即使被virtual修饰的类成员函数被称为虚函数
class Person { public: virtual void BuyTicket(){cout << "买票-全价" << endl;} }
当前使用vertual与菱形虚拟继承使用virtua属于一词多义的关系
2.3 虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同)、称子类的虚函数重写了基类的虚函数。
多态对于虚函数重写态度:指向谁调用谁的虚函数,在讲述析构函数常见题将会有更深了解。
2.4 虚函数使用场景
使用事项:
- 父子类完成虚函数重写(三同:函数名、参数、返回值类型)
- 父类的指针或者引用去调用虚函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
2.5 虚函数重写特殊场景
2.5.1 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。属于父子类继承关系即可。
即基类虚函数返回值基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{}; class B : public A {}; class Person { public: // virtual A* f() {return new A;} virtual Person* f(){return new Person;} }; class Student : public Person { public: //virtual B* f() {return new B;} virtual Student* f(){return new Student;} }
2.5.2 析构函数重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字还是析构函数名不同,都与基类的析构函数构成重写。
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor函数名
多态及其析构函数考察(常考)
class Person { public: ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }
分析问题:
如果是单纯定义Person p,Student s
话,不管析构函数是不是析构函数不重要,也不会出现什么问题。但是对于上面这种父类指针类型指向开辟父类或者子类的空间,如果Person和Student析构函数没有完成虚函数的重写就会出现问题
Person
的析构函数不是虚函数,因此,如果你使用 delete p2;
释放 p2
指向的 Student
对象,只会调用 Person
类的析构函数,而不会调用 Student
类的析构函数。这样会导致Student
对象中的资源没有得到正确释放,重复析构是一种未定义的行为,可能导致内存泄漏或者程序崩退。
具体说明:
如果析构函数没有完成虚函数的重写,会根据对象的类型,去调用析构函数,因为编译器只知道这个是一个指向基类的指针。就会调用基类的析构。
这里从底层来看,编译器只能通过指针类型去推断调用对应的析构函数
- p1->destructor + operator delete(p1)
- p2->destructor + operator delete(p2)
2.5.3 派生类可以不加virtual
重写基类虚函数,派生类虚函数不加virtual关键字修饰,可以构成重写。由于继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但是该种写法不是很规范,不建议这样使用。
2.6 大坑题(深度理解)
具体解析:
- 这里
p->test
调用B
类对象的test
函数,但是test
函数属于类A
,它被继承到了类B
中。由于没有在类B
中被重写,test
函数的实现仍然是类A
中的版本,this是A*类型,很好解释了这一块。- 在类
A
中,func
是一个虚函数,这意味着在运行时,调用哪个版本的func
取决于指向对象的实际类型。p
是指向B
类对象的指针,所以p->func()
会调用类B
的func
函数。- 这里需要注意的是,默认参数在编译时已经被绑定到函数调用上。test 函数在类 A 中定义,而
A::func(int val = 1)
使用默认参数 1,因此在test
中调用func()
时,将使用默认参数 1。- 虚函数重写,重写的是函数体的实现
三、C++ 11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但事实有些情况下**由于疏忽,可能会导致无法构成重写,而着这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结构才来debug会得不偿失。**对此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
引入场景:让父类不能被子类继承
C++98方法:父类构造函数设置为私有,子类的构造无法生成和实现,导致子类对象无法实例化。
class Car { public: private: Car(){} }; class Benz :public Car { public: }; int main() { Benz b; return 0; }
C++11方法:采用final关键字表示最终类
3.1 final与override使用
final:修饰虚函数,表示该虚函数不能再被重写。通俗一点比喻:老爹不给你留家底了,想子类体会下人生。
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() {cout << "Benz-舒适" << endl;} };
override:检查派生类虚函数是否重写基类某个虚函数,进行语法检查,如果没有发生重写将会编译报错。注意这个只是检查。
class Car { public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override {cout << "Benz-舒适" << endl;} }
四、重载、覆盖(重写)、隐藏(重定义)区分
关于隐藏与重写语言存在重叠,可以看作重写属于隐藏的自己。因为构成重写需求比隐藏多。
五、抽象类
5.1 抽象类概念
在虚函数的后面写上 = 0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化对象。
派生类继承后也不能实例化出对象。只当重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外虚函数更体现出了接口继承。
class Car { public: virtual void Drive() = 0; }; class Benz : public Car { }; int main() { Benz z; z.Drive(); return 0; }
5.2 抽象类与override区别
- 抽象类:强制要求派生类完成虚函数的重写,否则都不能实例化对象
- override:检查语句,帮助检查语法是否有问题,没有重写将编译器报错
六、实现继承与接口继承
实现继承:
- 普通函数的继承属于实现继承,由派生类继承了基类函数,可调用函数,继承函数实现
接口继承:
- 虚函数的继承属于接口继承,由派生类继承了基类虚函数的接口,目的是为了重写,其中重写是指函数体的实现,达成多态,继承的是接口
对此,如果不实现多态,不要把函数定义成虚函数。
【C++】面向对象编程的三大特性:深入解析多态机制(二)https://developer.aliyun.com/article/1617395