本篇将开启 C++
三大特性中的多态篇章,多态允许你以统一的方式处理不同类型的对象,通过相同的接口来调用不同的实现方法。这意味着你可以编写通用的代码,而这些代码可以在运行时根据对象的实际类型来执行特定的操作
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
✏️举个例子:
比如买高铁票的时候,我们都属于 Person
类,买的时候会显示为全价,那么我们又属于 Student
类,继承于 Person
类,这时买的时候又会显示为半价,假设两个类都有 BuyTicket
函数,那么相同的函数在继承的基础上,能够实现不同的功能,这就是多态
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
被 virtual
修饰的类成员函数称为虚函数,注意这里和菱形虚拟继承的 virtual
没有关系,不过使用了同一个关键字而已
🔥值得注意的是:
虚函数是实现多态的重要组成部分,将上面举的例子以代码形式实现如下:
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,比如 Student
继承了 Person
,Person
对象买票全价,Student
对象买票半价
那么在继承中要构成多态还有两个条件:
🔥值得注意的是: 多态构成条件缺一不可,如果多态产生问题,子类没有对某个方法进行重写,那么子类对象在调用该方法时,就会沿着继承链向上查找,找到父类中对应的方法并调用
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void Func(Person& people)
{
people.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
Person
类的 BuyTicket
和 Student
类的 BuyTicket
构成重写
虚函数的重写: 又叫覆盖,派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型
、函数名字
、参数列表
完全相同),称子类的虚函数重写了基类的虚函数
🔥值得注意的是: 在重写父类虚函数时,子类的虚函数在不加 virtual
关键字时,虽然也可以构成重写(因为继承后父类的虚函数被继承下来了在子类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
class A {};
class B : public A {};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
return new B;
}
};
协变是重写的一种特殊情况,简单来说协变就是派生类重写基类虚函数时,与基类虚函数返回值类型不同
,且要求父类虚函数类型和子类虚函数类型必须是父子关系的引用和指针
🔥值得注意的是: 必须都是引用或者都是指针,不能一个是引用一个是指针
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
这里单纯讲解很难理解,所以以一段代码场景+一些提问
来解析:
🚩析构函数+virtual,是不是虚函数重写?
是,虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor
🚩为什么要处理成统一名字?
因为要让两个析构函数构成重写
🚩为什么要让他们构成重写?
假设我们上面的这个代码没有加 virtual
,运行代码如下:
观察可以发现子类 Student
部分没有得到释放,那么 ptr
指向的空间就会造成内存泄漏
根据 C++
内存管理学的知识可知
p
->destructor()
+operator delete
这里只能调用 p
这个类型的析构函数,但是我们为了实现能够调用指向空间的析构函数,期望是个多态调用,而不是普通调用,所以必须让这两个析构函数构成重写
🔥值得注意的是:
如果父类的析构函数不是虚函数
,那么将按指针本身的类型(即父类)来析构。这可能会导致子类部分的资源没有被正确释放,产生内存泄漏等问题
如果父类的析构函数是虚函数
,那么会按照指针实际指向的对象类型(即子类)来析构
🚩final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
🔥值得注意的是:
假设有个 A
类和 B
类,不想让 B
类继承 A
类,那么可以写做:class A final
,避免 A
类被继承,这是 C++11
才支持的,在这之前使用的是将 A
的构造函数私有化的方法
🚩override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car
{
public:
virtual void Drive()
{}
};
class Benz :public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();//访问Benz的虚函数
Car* pBMW = new BMW;
pBMW->Drive();//访问BMW的虚函数
}
在虚函数的后面写上 = 0
,则这个函数为纯虚函数
,包含纯虚函数的类叫做抽象类
(也叫接口类
)
抽象类不能实例化出对象,即只要有纯虚函数就不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
🔥值得注意的是:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
✏️以下我们通过多个例子进行详细解析:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main
{
Base b;
return 0;
}
sizeof(Base)是多少?
想必大部分人第一次做这道题都会觉得是
1
,但运行后发现答案是8
很奇怪,所以我们转到调试查看
发现除了 _b
以外,还多一个 _vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针( v
代表 virtual
,f
代表 function
)
通常虚函数都被放在代码段
,_vfptr
就是虚函数的地址,被存放在虚函数表,虚函数表放在只读数据段
,也就是常量区
,所以虚函数表本质上是个函数指针数组
,虚函数表是在编译期间生成的
✏️那么多个虚函数是怎样实现多态的,举个例子:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
还是转到监视窗口调试查看:
实际上虚函数表是按照一定规则实现的:
🔥值得注意的是:
b
对象和子类 d
对象虚表是不一样的,这里我们发现 Func1
完成了重写,所以 d
的虚表中存的是重写的 Derive::Func1
,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写
是语法的叫法,覆盖
是原理层的叫法Func2
继承下来后是虚函数,所以放进了虚表,Func3
也继承下来了,但是不是虚函数,所以不会放进虚表nullptr
那么回归到多态的实现条件:
我们可以提出两个问题:
🚩为什么不是子类指针或者引用?
class Animal
{
public:
virtual void speak()
{
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal
{
public:
void speak() override
{
cout << "Dog barks" << endl;
}
};
class Cat : public Animal
{
public:
void speak() override
{
cout << "Cat meows" << endl;
}
};
int main() {
Dog dog;
Animal* animalPtr = &dog; // 父类指针指向子类对象
animalPtr->speak(); // 运行时根据实际对象类型调用Dog的speak函数
Cat cat;
Animal& animalRef = cat; // 父类引用绑定到子类对象
animalRef.speak(); // 运行时根据实际对象类型调用Cat的speak函数
return 0;
}
这里的子类 Dog
和 Cat
都继承于父类 Animal
,就是因为是父类的指针或引用才能想调用哪个子类都行
如果是子类的指针或引用,比如有个 Dog
类的指针 Dog* dogPtr
,它只能指向 Dog
类对象,没办法指向 Cat
类对象。如果想用它去调用 speak
函数,不管怎样都是调用 Dog
类的 speak
函数,不能根据实际对象类型(Cat
或其他子类)来动态调用不同的 speak
函数,就实现不了多态了
🚩为什么不能是父类对象?
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
int main()
{
Person ps;
Student st;
ps = st;
return 0;
}
如果是使用对象,而不是指针或引用,子类中特有的成员变量和函数将被截断,丢失子类的特性
而使用父类指针或引用指向子类对象时,不会发生切片,能够完整保留子类对象的所有信息,从而可以访问子类重写的虚函数以实现多态
🔥值得注意的是: 子类对象赋值给父类对象的时候,不会拷贝虚函数表过去,如果拷贝了,那么父类虚函数表中的虚函数就变成子类虚函数了,就失去多态的意义了
所以总结: 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用时编译时确认好的
静态绑定: 又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定: 又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
A
: 继承
B
: 封装
C
: 多态
D
: 抽象A
: 继承
B
: 模板
C
: 对象的自身引用
D
: 动态绑定A
:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B
:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C
:优先使用继承,而不是组合,是面向对象设计的第二原则
D
:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现A
:声明纯虚函数的类不能实例化对象
B
:声明纯虚函数的类是虚基类
C
:子类必须实现基类的纯虚函数
D
:纯虚函数必须是空函数A
:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B
:内联函数不能是虚函数
C
:派生类必须重新定义基类的虚函数
D
:虚函数可以是一个static型的函数A
:一个类只能有一张虚表
B
:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C
:虚表是在运行期间动态生成的
D
:一个类的不同对象共享该类的虚表A
:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B
:A类对象和B类对象前4个字节存储的都是虚基表的地址
C
:A类对象和B类对象前4个字节存储的虚表地址相同
D
:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表参考答案:1.
A
2.D
3.C
4.A
5.B
6.D
7.D
#include<iostream>
using namespace std;
class A
{
public:
A(const char* s)
{
cout << s << endl;
}
~A()
{}
};
class B :virtual public A
{
public:
B(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class C :virtual public A
{
public:
C(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4)
:B(s1, s2)
, C(s1, s3)
, A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
A
:class A class B class C class DB
:class D class B class C class AC
:class D class C class B class AD
:class A class C class B class D
解析: 这是个菱形虚拟继承,所以 A
只会被调用一次,D
类里的初始化列表是按声明的顺序来初始化的,所以按 ABCD
的顺序,因此答案选 A
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A
:p1 == p2 == p3B
:p1 < p2 < p3C
:p1 == p3 != p2D
:p1 != p2 != p3
解析: 画图理解即可,选 C
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
A
: A->0B
: B->1C
: A->1D
: B->0E
: 编译出错F
: 以上都不正确
解析: 这题绝大多数人肯定会选到 D
,这题的知识点确实比较偏,首先我们要知道多态重写的是实现,即只有 {}
内的内容是多态的,实际上子类的函数头其实相当于是从父类拷贝过来的,因此函数头的内容还是调用的父类的,所以答案选 B
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有