深入探索C++多态:静态与动态绑定的奥秘
- [一. 多态](#一. 多态)
-
- [1.1 定义](#1.1 定义)
- [1.2 多态定义及实现](#1.2 多态定义及实现)
-
- [1.2.1 多态构成条件](#1.2.1 多态构成条件)
-
- [1.2.1.1 实现多态两个必要条件](#1.2.1.1 实现多态两个必要条件)
- [1.2.1.2 虚函数](#1.2.1.2 虚函数)
- [1.2.1.3 虚函数的重写/覆盖](#1.2.1.3 虚函数的重写/覆盖)
- [1.2.1.4 协变](#1.2.1.4 协变)
- [1.2.1.5 析构函数重写](#1.2.1.5 析构函数重写)
- [1.2.1.6 override和final关键字](#1.2.1.6 override和final关键字)
- [1.2.1.7 重载/重写/隐藏的对⽐](#1.2.1.7 重载/重写/隐藏的对⽐)
- [1.3 纯虚函数和抽象类](#1.3 纯虚函数和抽象类)
- [1.4 多态原理](#1.4 多态原理)
-
- [1.4.1 虚函数表指针](#1.4.1 虚函数表指针)
- [1.4.2 原理](#1.4.2 原理)
- [1.4.3 静态绑定与动态绑定](#1.4.3 静态绑定与动态绑定)
- [1.4.4 虚函数表](#1.4.4 虚函数表)
- [二. 最后](#二. 最后)
本文章介绍面向对象编程的三大特性中的多态之一。
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对 C++ 感兴趣的朋友,让我们一起进步!
一. 多态
1.1 定义
多态使得不同类型的对象可以通过相同的接口进行交互,而每个对象的具体行为是根据其自身类型来决定的,分为编译时多态和运行时多态。举个例子:对于普通人来说,票价全价,学生来说,票价半价,军人来说,优先买票等,指在根据不同的对象完成不同买票行为。
下面用示例代码来展现该行为(如下):
cpp
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
Person p1;
Student s1;
Func(&p1);
Func(&s1);
return 0;
}
结果如下:
通过传递不同的对象,来实现不同的行为,上述传递Person类和Student类,分别对应的行为是全价和半价。
1.2 多态定义及实现
1.2.1 多态构成条件
多态的实现原理通过虚函数表,虚表由虚函数构成,即 Virtual 修饰的函数,还需要虚表指针进行定位函数调用。
1.2.1.1 实现多态两个必要条件
- 必须是基类的指针或引用调用虚函数
- 被调用的函数必须是虚函数,并且派生类需对基类被调用的虚函数重写/覆盖
1.2.1.2 虚函数
注意:必须是成员函数,不是成员函数的报错示例:
error: 类声明的外部说明符无效。
定义:类成员函数被virtual修饰,这个成员函数被称为虚函数。
1.2.1.3 虚函数的重写/覆盖
定义:派生类有一个与基类完成相同的虚函数(即派生类函数与基类函数的返回值类型、函数名字,参数列表完全形同,缺一不可),称为派生类的虚函数重写了基类的虚函数。注意:派生类的虚函数不加 virtual 修饰,也构成重写,但是不规范,不建议使用。
- 例题:
cpp
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
- 解释:
调用func前面是A*类型,是基类的指针调用,派生类对该函数进行了重写,所以构成多态。指向派生类,所以调用派生类的func,而派生类B前面省略了virtual,编译器会将派生类调用的func修改成:virtual void func(int val = 1){ std::cout<<"B->"<< val <std::endl;}所以叫作对基类虚函数的重写。即B-1,所以答案是B。
1.2.1.4 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同时。简单点说就是:基类指针返回基类对象的指针或引用,派生类返回派生类对象的指针或引用,称为协变。
- 示例代码:
cpp
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()//返回基类指针对象
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()//返回派生类指针对象
{
cout << "买票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
上述举了一个关于协变的例子。
1.2.1.5 析构函数重写
注意:需将对基类的析构函数进行重写。下面给个代码例子来解释:
cpp
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
virtual ~B() override
{
cout << "~B()->delete:" << _p << endl;
delete [] _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B; //这是C++的静态绑定特性:非虚函数调用仅由指针的静态类型(此处为A * )决定。
delete p1;
delete p2;
return 0;
}
注意:C++的静态绑定特性:非虚函数调用仅由指针的静态类型决定。当基类的析构函数没有被修饰为虚函数,进行静态绑定,非虚函数调用仅由指针的静态类型(A*)决定。下面的B对象的资源不会被释放,造成内存泄漏。。当基类的析构函数被修饰为虚函数,构成多态,运行时绑定p2指针指向B对象,调用B对象的析构函数,成功将B对象的资源释放。
所以建议:最好将基类的析构函数修饰为虚函数,避免内存泄漏。
1.2.1.6 override和final关键字
- override:检查成员函数是否构成重写
- final:被修饰的虚函数不能被重写
1.2.1.7 重载/重写/隐藏的对⽐
- 重载:两个函数在同一作用域,函数名相同,参数不同,参数类型或者个数不同,返回值不关心,即可构成重载。
- 重写:两个函数作用域在不同的作用域(一般是基类和派生类),函数名,参数,返回值必须全部相同,协变除外,且两个函数必须是虚函数,派生类可以不显示写virtual,但不建议。
- 隐藏:两个函数作用域在不同的作用域(一般是基类和派生类),两个函数名相同即可,其它的不关心,父类与派生类的成员变量名相同也构成隐藏。
1.3 纯虚函数和抽象类
- 纯虚函数:在虚函数后面 加上=0,则这个函数成为纯虚函数。
示例:
cpp
class Car
{
public:
virtual void Drive() = 0;
};
- 抽象类:包含纯虚函数的类称为抽象类,派生类可以继承抽象类,对纯虚函数进行重写,完成不同的功能。
示例代码:
cpp
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
1.4 多态原理
1.4.1 虚函数表指针
看看下面程序在32为程序的运行结果是什么?
- 示例代码:
cpp
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;//类内包含一张纯虚函数表,也就是函数指针数组
cout << sizeof(b) << endl;
return 0;
}
输出结果为12,因为里面还存在虚函数表指针,指针在32位机器占4字节。
如图:
1.4.2 原理
构成多态时,运行到指定的对象的虚表中确定对应的虚函数,不再在编译时绑定,称为静态绑定,这是称为动态绑定。
1.4.3 静态绑定与动态绑定
- 静态绑定:静态绑定是指在程序编译时就确定了方法调用的具体实现。在静态绑定中,编译器根据对象的编译时类型来决定调用哪个方法或访问哪个成员。静态绑定通常发生在方法重载(method overloading)或成员变量访问的场景。
- 动态绑定:动态绑定是指在程序运行时根据对象的实际类型来决定调用哪个方法或访问哪个成员。动态绑定通常发生在方法重写(method overriding)和多态(polymorphism)的场景中。
1.4.4 虚函数表
- 基类对象的虚函数表中存放基类所有的虚函数地址。不同类型的对象具有不同的虚函数表。
- 派生类虚函数表里的内容相对复杂,包括基类虚函数地址,派生类重写基类虚函数地址完成覆盖,派生类自己虚函数的地址。
- 虚函数表本质是一个存放虚函数指针的指针数组。
- 虚函数存在哪?存在代码段,虚函数地址存在虚函数表中。
- 虚函数表存在哪?这个C++并没有标准答案,VS存放在代码段(常量区)。
二. 最后
本文深入探讨了C++多态性的核心概念与实现机制,涵盖多态定义、虚函数、协变、析构函数重写、override/final关键字、重载/重写/隐藏对比、纯虚函数与抽象类,以及多态原理如虚函数表指针、静态/动态绑定等,是C++面向对象编程的进阶指南。