C++的多态技术(虚函数)详解
- 引言
- 一、多态的概念
- 二、虚函数
- 2.1、父类指针保存子类空间地址 带来的问题
- 2.2、虚函数的定义
- 2.3、虚函数的动态绑定机制
- 2.4、重载、重定义、重写的区别
- 三、纯虚函数
- 3.1、纯虚函数的定义方式
- 3.2、纯虚函数的案例:饮品制作
- 3.3、虚函数和纯虚函数的区别
- 四、虚析构函数
- 五、纯虚析构函数
- 5.1、纯虚析构函数的定义
- 5.2、虚析构函数和纯虚析构函数的区别
- 总结
引言
💡 作者简介:专注于C/C++高性能程序设计和开发,理论与代码实践结合,让世界没有难学的技术。包括C/C++、Linux、MySQL、Redis、TCP/IP、协程、网络编程等。
👉
🎖️ CSDN实力新星,社区专家博主
👉
🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。
👉
🔔 专栏地址:C++从零开始到精通
👉
🔔 博客主页:https://blog.csdn.net/Long_xu
🔔 上一篇:【032】C++高级开发之继承机制详解(最全讲解)
一、多态的概念
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。多态(Polymorphism)是一种面向对象编程的特性,它指的是不同对象可以通过同一接口(方法)来进行访问和操作的能力。在多态中,同一类型的对象在不同的时间和情况下会呈现出不同的行为,这使得代码更加灵活、可扩展和易于维护。
多态包括静态多态(编译时多态)和动态多态(运行时多态)。静态多态指的是函数重载和运算符重载,它们在编译时根据形参类型或运算符进行选择。动态多态指的是通过继承和虚函数实现的多态性,它在程序运行时根据对象的实际类型进行选择。
- 静态多态:编译时多态,是早绑定;包括函数重载、运算符重载、重定义。
- 动态多态:运行时多态,是晚绑定;虚函数实现。
多态是面向对象编程中最重要的特性之一,它可以大大提高代码的可重用性和灵活性,使得程序可以适应不同的需求和环境。
二、虚函数
如果我们需要通过父类可以操作它所派生出来的所有子类,就需要设计一个算法,这个算法需要传一个它们的公共参数:父类指针(引用)。这样就可以通过一个父类访问所有由它派生出来的子类,
父类指针(引用)保存子类空间地址的目的是让算法通用。
2.1、父类指针保存子类空间地址 带来的问题
#include <iostream>
using namespace std;
class Animal {
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal {
public:
void speak()
{
cout << "狗在汪汪" << endl;
}
};
int main()
{
Animal *p = new Dog;
p->speak();
delete p;
return 0;
}
输出:
动物在说话
需求是p->speak()希望得到的是“狗在汪汪”而不是“动物在说话”,也就是希望通过父类指针操作子类。这就是要解决的问题。
接下来要讲的就是父类如何调用子类;也就是虚函数。
2.2、虚函数的定义
虚函数是一种在基类中声明的函数,它在派生类中可以被重写(覆盖)的函数。在基类中使用关键字“virtual”声明的函数就是虚函数。虚函数通过动态绑定实现了多态性,使得程序可以根据实际对象类型来调用相应的函数。在运行时,系统会根据被调用的对象的类型来确定调用哪一个重写函数。虚函数是C++中实现多态的关键所在。
定义方式:在成员函数前面加virtual修饰。
注意:子类重写父类的虚函数时,返回值类型、参数类型、个数顺序 必须完全一致。
多态的条件:有继承、子类重写父类的虚函数、父类指针指向子类空间。
示例:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal {
public:
void speak()
{
cout << "狗在汪汪" << endl;
}
};
class Cat :public Animal {
public:
void speak()
{
cout << "猫在喵喵" << endl;
}
};
int main()
{
Animal *p = new Dog;
p->speak();
Animal *p2 = new Cat;
p2->speak();
delete p2;
delete p;
return 0;
}
输出:
狗在汪汪
猫在喵喵
这样就实现了父类指针操作子类的目的。
2.3、虚函数的动态绑定机制
上述的例子中,Animal的类的结构:
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::speak
如歌一个类的成员函数被virtual修饰,那么这个函数就是虚函数。类就会产生一个虚函数指针(vfptr)指向了一张虚函数表(vftable)。如果这个没有涉及到继承,这时的虚函数表中记录的就是当前虚函数入口地址。
上述的例子中,Dog的类的结构:
class Dog size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Dog::$vftable@:
| &Dog_meta
| 0
0 | &Dog::speak
如果涉及到继承,继承的时候子类会把父类的虚函数指针和虚函数表继承过来;当重新虚函数时会更新虚函数表。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal {
public:
void speak()
{
cout << "狗在汪汪" << endl;
}
};
class Cat :public Animal {
public:
void speak()
{
cout << "猫在喵喵" << endl;
}
};
//设计一个算法
void showAnimal(Animal *p)
{
p->speak();
delete p;
}
int main()
{
showAnimal(new Dog);
showAnimal(new Cat);
return 0;
}
输出:
狗在汪汪
猫在喵喵
2.4、重载、重定义、重写的区别
C++中的重载、重定义和重写是面向对象编程(OOP)中常见的概念,区别如下:
-
重载(Overload):指在同一个作用域中,可以有多个同名函数,它们的参数列表不同(参数的顺序、个数、类型不同)都可以重载;但是函数的返回值类型不能作为重载条件(函数重载、运算符重载)。编译器会根据不同的参数列表来区分它们。例如,可以定义两个同名函数print(),一个输出整数,一个输出字符串,这样就发生了函数的重载。
-
重定义(Redefinition):有继承、子类重定义父类的同名函数(非虚函数),参数顺序、个数、类型可以不同;子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)。
-
重写(Override):指在派生类中重新定义一个与基类中同名、同参数列表、同返回类型的虚函数,用派生类的实现覆盖基类的实现。通过重写,可以实现动态绑定,使得程序可以根据实际对象类型来调用相应的函数。有继承,子类重写父类的虚函数,返回值类型、函数名、参数顺序、个数、类型都必须一致。
小结:重载是在同一作用域内多次定义同名函数,参数列表不同;重定义是在同一作用域内多次定义同名函数,参数列表相同;重写是子类重新定义父类的虚函数。
三、纯虚函数
如果基类一定派生出子类,而子类一定会重写父类的虚函数,这种情形下父类的虚函数中的函数体就没有了意义,那么可不可以不写父类虚函数的函数体呢?可以的,这就是纯虚函数。
3.1、纯虚函数的定义方式
语法:
class 类名{
public:
// 纯虚函数
virtual 函数返回值类型 函数名(参数列表)=0;
};
例如:
class Animal {
public:
virtual void speak()=0;
};
- 一旦类中有纯虚函数,那么这个类就是抽象类。
- 抽象类不能实例化对象。
- 抽象类必须被继承,同时子类必须重写父类的所有纯虚函数,否则子类也是抽象类。
- 抽象类主要目的是设计类的接口。
#include <iostream>
using namespace std;
class Animal {
public:
// 纯虚函数
virtual void speak() = 0;
};
class Dog :public Animal {
public:
// 子类一定会重写父类的虚函数
void speak()
{
cout << "狗在汪汪" << endl;
}
};
class Cat :public Animal {
public:
// 子类一定会重写父类的虚函数
void speak()
{
cout << "猫在喵喵" << endl;
}
};
//设计一个算法
void showAnimal(Animal *p)
{
p->speak();
delete p;
}
int main()
{
showAnimal(new Dog);
showAnimal(new Cat);
return 0;
}
3.2、纯虚函数的案例:饮品制作
#include <iostream>
using namespace std;
class AbstractDrinking {
public:
// 烧水
virtual void Boil() = 0;
// 冲泡
virtual void Brew() = 0;
// 倒入杯中
virtual void PourInCup() = 0;
// 加入佐料
virtual void PutSomething() = 0;
// 规定流程
void MakeDrinking() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};
class Coffe :public AbstractDrinking {
public:
// 烧水
void Boil()
{
cout << "烧矿泉水" << endl;
}
// 冲泡
void Brew()
{
cout << "冲咖啡" << endl;
}
// 倒入杯中
void PourInCup()
{
cout << "咖啡倒入杯中" << endl;
}
// 加入佐料
void PutSomething()
{
cout << "加入牛奶" << endl;
}
};
class Tea :public AbstractDrinking {
public:
// 烧水
void Boil()
{
cout << "烧自来水" << endl;
}
// 冲泡
void Brew()
{
cout << "冲茶叶" << endl;
}
// 倒入杯中
void PourInCup()
{
cout << "茶倒入杯中" << endl;
}
// 加入佐料
void PutSomething()
{
cout << "加入柠檬" << endl;
}
};
void makeDrinking(AbstractDrinking *p)
{
p->MakeDrinking();
delete p;
}
int main()
{
makeDrinking(new Coffe);
cout << "---------------------" << endl;
makeDrinking(new Tea);
return 0;
}
输出:
烧矿泉水
冲咖啡
咖啡倒入杯中
加入牛奶
---------------------
烧自来水
冲茶叶
茶倒入杯中
加入柠檬
3.3、虚函数和纯虚函数的区别
C++中的虚函数和纯虚函数都是为了实现多态性而存在的:
-
虚函数:在基类中声明的函数,可以在派生类中被覆盖(重写)。虚函数在基类中有一个默认的实现,但在派生类中可以重新实现,实现与基类虚函数的方法签名相同。派生类中定义虚函数时,必须使用关键字“virtual”来声明。也就是虚函数使用virtual修饰,有函数体,不会导致父类变为抽象类。
-
纯虚函数:在基类中声明的函数,在基类中没有实现,必须在派生类中实现。纯虚函数使用“=0”来声明,例如“virtual void foo() = 0;”。纯虚函数没有默认实现,所以派生类必须实现它们。也就是纯虚函数有virtual修饰,=0,没有函数体,导致父类为抽象类,子类必须重写父类的所有纯虚函数。
区别:
-
虚函数可以有实现,纯虚函数没有实现。
-
虚函数可以在基类中有默认的实现,纯虚函数没有默认实现。
-
虚函数不一定要在派生类中实现,但纯虚函数必须在派生类中实现。
-
如果一个类中有纯虚函数,那么它就是抽象类。不能创建抽象类的对象,只能创建它的派生类对象。
虚函数是可选的,它有一个默认的实现,但可以在派生类中重新实现;纯虚函数是必须实现的,它没有默认的实现,只有声明,必须在派生类中实现。虚函数用于实现多态性,而纯虚函数用于定义接口。
四、虚析构函数
虚析构函数的定义:使用virtual修饰析构函数。
目的:通过父类指针释放整个子类空间。
先看一下没有虚析构的例子,在释放内存空间时产生什么问题:
#include <iostream>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal构造函数" << endl;
}
// 纯虚函数
virtual void speak() = 0;
~Animal()
{
cout << "Animal析构函数" << endl;
}
};
class Dog :public Animal {
public:
Dog()
{
cout << "Dog构造函数" << endl;
}
// 子类一定会重写父类的虚函数
void speak()
{
cout << "狗在汪汪" << endl;
}
~Dog()
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Animal *p = new Dog;
p->speak();
delete p;
return 0;
}
输出:
Animal构造函数
Dog构造函数
狗在汪汪
Animal析构函数
可以看到,子类没有析构,这会造成内存泄漏的。这时,我们把它设置为虚析构,就可以释放子类的内存空间:
#include <iostream>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal构造函数" << endl;
}
// 纯虚函数
virtual void speak() = 0;
virtual ~Animal()
{
cout << "Animal析构函数" << endl;
}
};
class Dog :public Animal {
public:
Dog()
{
cout << "Dog构造函数" << endl;
}
// 子类一定会重写父类的虚函数
void speak()
{
cout << "狗在汪汪" << endl;
}
~Dog()
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Animal *p = new Dog;
p->speak();
delete p;
return 0;
}
输出:
Animal构造函数
Dog构造函数
狗在汪汪
Dog析构函数
Animal析构函数
原理剖析:
- 构造的顺序:父类–>成员–>子类。
- 析构的顺序:子类–>成员–>父类。
当设置虚析构时,子类在继承时会把虚函数表继承过来,并且更新了虚函数表,所以到编译器调用析构函数时会自动找到虚函数表,此时虚函数表更新为子类的析构函数地址,所以就可以调用子类的析构函数。
五、纯虚析构函数
5.1、纯虚析构函数的定义
纯虚析构的本质是析构函数,复杂各个类的回收工作。而且析构函数不能被继承。
注意:
- 必须为纯虚析构函数提供一个函数体。
- 纯虚析构函数必须类外实现。
示例:
#include <iostream>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal构造函数" << endl;
}
// 纯虚函数
virtual void speak() = 0;
// 纯虚析构函数
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal纯虚析构函数" << endl;
}
class Dog :public Animal {
public:
Dog()
{
cout << "Dog构造函数" << endl;
}
// 子类一定会重写父类的虚函数
void speak()
{
cout << "狗在汪汪" << endl;
}
~Dog()
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Animal *p = new Dog;
p->speak();
delete p;
return 0;
}
5.2、虚析构函数和纯虚析构函数的区别
C++中的虚析构和纯虚析构都是为了实现多态性而存在的,它们之间的区别如下:
-
虚析构:在基类中声明为虚函数的析构函数。虚析构函数用于在析构对象时调用其派生类的析构函数,确保释放所有内存空间。虚析构函数在基类中有默认的实现,可以在派生类中重新实现。也就是虚析构使用virtual修饰,有函数体,不会导致父类变成抽象类。
-
纯虚析构:在基类中声明为纯虚函数的析构函数。纯虚析构函数没有实现,需要在派生类中实现。纯虚析构函数用于定义接口,让派生类必须实现自己的析构函数,确保释放所有内存空间。也就是纯虚析构函数使用virtual修饰,=0,函数体必须类外实现,会导致父类变成抽象类。
区别:
-
虚析构函数在基类中有默认实现,而纯虚析构函数没有默认实现,需要在派生类中实现。
-
虚析构函数可以不在派生类中重新实现,但纯虚析构函数必须在派生类中实现。
-
如果一个类中有纯虚析构函数,那么它就是抽象类。不能创建抽象类的对象,只能创建它的派生类对象。
虚析构函数是在基类中声明为虚函数的析构函数,用于在析构对象时调用其派生类的析构函数;纯虚析构函数是在基类中声明为纯虚函数的析构函数,需要在派生类中实现,用于定义接口。虚析构函数有默认实现,而纯虚析构函数没有默认实现。
总之,不管是虚析构函数纯虚析构,目的都是希望通过父类指针释放子类的整个内存空间。
总结
C++中的虚函数是实现多态性的重要手段,它具有以下特性:
-
虚函数是在基类中声明的成员函数,在派生类中可以被覆盖(重写)。
-
在基类中,虚函数可以有默认的实现,在派生类中可以使用关键字“override”来重新实现它。
-
如果一个类至少有一个虚函数,那么它就是一个多态类,可以通过基类指针或引用访问派生类对象。
-
在使用基类指针或引用访问派生类对象时,如果虚函数没有被覆盖,那么将调用基类的虚函数;如果被覆盖了,那么将调用派生类的虚函数。
-
虚函数可以被定义为纯虚函数,这样基类就成为了抽象类,不能创建抽象类的对象,只能创建它的派生类对象。
-
虚函数可以是析构函数,这样在销毁对象时可以正确地调用派生类的析构函数,避免内存泄漏的问题。
虚函数是实现多态性的重要手段,在C++中使用非常广泛。它可以让我们在基类中定义通用的操作,然后在派生类中实现具体的细节,从而实现代码重用和扩展性。同时,虚函数还可以让我们通过基类指针或引用访问派生类对象,实现面向对象编程中的多态性。