文章目录
- 前言
- 一、多态的概念
- 二、多态的定义及实现
- 1. 多态的构成条件
- 2. 虚函数
- 3. 虚函数的重写
- 4. 基类的指针或者引用调用虚函数
- 5. 虚函数重写的两个例外
- 1)协变
- 2)析构函数的重写
- 🥳重点面试题:多态构造函数相关
- 🥰总结:
- 三、C++11 override 和 final
- 1. final
- 2. override
- 四、重载、覆盖(重写)、隐藏(重定义)的对比
- 五、设计不想被继承类,如何设计?
- 方法1:基类构造函数私有 (C++98)
- 方法2:基类加一个final (C++11)
- 总结
前言
今天我们一起来学习C++面向对象三大特点最后一个特点:多态🥳🥳🥳
需要声明的,本节课件中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。
如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。
一、多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。
我们来看几个现实生活中多态的例子:
场景一::比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票
场景二:最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的
活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5
毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =
random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫
得到的不一样的红包,这也是一种多态行为。
二、多态的定义及实现
1. 多态的构成条件
在继承中要构成多态还有两个条件:
- 必须通过基类的
指针
或者引用
调用虚函数 - 被调用的函数必须是
虚函数
,且派生类必须对基类的虚函数进行重写
2. 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
3. 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
但是,子类写virtual,父类不写是不可以的!
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;}
运行结果:
5. 虚函数重写的两个例外
1)协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
多态不是要重写虚函数吗?两个函数不是要三同吗?这里就是说返回值可以不同了,但是返回值必须是父类返回一个父类指针或引用,子类返回对应的指针或引用,顺序不能错。
class A
{};class B : public A
{};class Person {
public:virtual A* BuyTicket() const { cout << "买票-全价" << endl;return 0;}
};class Student : public Person {
public:virtual B* BuyTicket() const { cout << "买票-半价" << endl;return 0;}
};
2)析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
🥳重点面试题:多态构造函数相关
- 析构函数可以是虚函数吗?为什么需要是虚函数?
答:可以。将析构函数声明为虚函数的主要原因是确保通过基类指针删除子类对象时,能够正确调用子类的析构函数,从而释放子类特有的资源。
- 析构函数加virtual,是不是虚函数重写?
答:是,因为类析构函数都被处理成destructor这个统一的名字,这是编译器做的,我们不用操心,目的就是为了多态。
- 为什么要这么处理呢?处理成destructor这个统一的名字.
答:因为要让他们构成重写,符合多态的基本语法,进行多态。
- 那为什么要让他们构成重写呢?
因为下面的场景:
假设有下面的场景,子类中有资源,我们希望这里p->destructor()是一个多态调用,而不是普通调用。
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;delete[] ptr;}protected:int* ptr = new int[10];
};
假设有一个父类的指针,给他new出来一个子类对象。
在清除delete p时,就相当于先调用 p 的析构函数,再free掉空间。
如果这是一个普通调用,那么就会调用Person的析构函数,这样就会造成内存泄漏,因此我们希望这里是一个多态调用
p = new Student;
delete p; // p->destructor() + operator delete(p)// 这里我们期望p->destructor()是一个多态调用,而不是普通调用
🥰总结:
三、C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。
1. final
final:修饰虚函数,表示该虚函数不能再被重写。
final是最终的意思,意思就是到这里就截止了,不能再继续继承下去了,后面还会对final修饰类讲解,表示这个类不能继承。
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};
这里编译器会报错:
2. override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
父类写了virtual是可以的:
class Car{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
如果父函数不写virtual就会报错:
四、重载、覆盖(重写)、隐藏(重定义)的对比
五、设计不想被继承类,如何设计?
class A
{};class B : public A
{};int main()
{B bb;return 0;
}
方法1:基类构造函数私有 (C++98)
- 可以把A类的构造函数变为私有,这样B类在创建对象时就会出现两个问题,A类的构造函数时私有,在B类中是不可见的,而且B类对象的创建必须要调用父类的构造函数,以此达到A类不被继承的目的。
但是,这样一来,A类自己也无法创建对象了,因此我们可以在A类public作用域下写一个函数,这个接口来实行构造函数的作用。
再然后,这样一来就牵扯出先有鸡还是先有蛋的问题,这个函数需要对象调用,创建对象又需要这个函数,因此我们需要将这个函数放到static静态堆中去,已完成我们的目的。
class A
{
public:static A CreatAobj(){return A();}
private:A();
};class B : public A
{};int main()
{A aa = A::CreatAobj();//B bb;return 0;
}
- 也可以将析构函数私有
将析构函数私有,B就无法继承A,对于A,用指针构造,提供接口释放
#include<iostream>
using namespace std;
class A
{
public:static void Delete(A* aa){delete aa;}
private:~A() {};
};class B : public A
{};int main()
{A* aa = new A;A::Delete(aa);//B bb;return 0;
}
方法2:基类加一个final (C++11)
写A类时,后面加一个final,从语法上定义A类不能被继承。
class A final
{};class B : public A
{};int main()
{B bb;return 0;
}
总结
多态是面向对象编程中的一个重要特性,它允许对象以多种形式出现,主要通过继承和接口实现。以下是多态的用法及注意事项的总结:
用法:
- 方法重写:子类可以重写父类的方法,运行时根据对象的实际类型决定调用哪个方法。
注意事项:
- 基类指针/引用:多态通常通过基类指针或引用来调用子类的方法,要确保这些指针/引用指向的对象是有效的。
- 虚函数:为了实现运行时多态,基类的方法必须声明为虚函数(使用
virtual
关键字),确保可以在子类中重写。 - 析构函数:如果使用多态,建议基类的析构函数也声明为虚函数,以确保正确析构派生类对象。
- 协变:父类与子类返回值也可以是某一个类父子关系的指针或引用。
到这里,多态的用法就结束啦,谢谢佬们的支持!!!😘😘😘😘