C++ 多态
多态(Polymorphism)是 C++ 面向对象编程的重要特性之一,指同一个接口可以根据对象类型表现出不同的行为。
例如,狗和猫都属于动物,它们都有发出声音的行为,但狗会汪汪叫,猫会喵喵叫。
在 C++ 中,可以定义一个动物基类 Animal,再创建 Dog 和 Cat 派生类。通过基类提供的统一接口,就可以调用不同动物各自的行为。
C++ 多态主要分为两种:
- 编译时多态(静态多态):在编译阶段确定调用的函数,例如函数重载、运算符重载和模板。
- 运行时多态(动态多态):通过虚函数和继承机制,在运行时根据对象的实际类型选择函数实现。
本章节主要介绍运行时多态,包括虚函数、动态绑定、纯虚函数、抽象类以及虚析构函数。
多态的基本原理
在 C++ 中,实现运行时多态通常需要满足以下条件:
- 存在基类与派生类的继承关系。
- 基类声明了虚函数(
virtual)。 - 派生类重写了对应的虚函数。
- 通过基类指针或引用调用虚函数。
例如:
Animal* animal = &dog; animal->sound();
虽然 animal 是 Animal* 类型,但它实际指向 Dog 对象,因此调用的是 Dog 类中的 sound()。
这种在运行时根据对象的动态类型选择虚函数实现的机制,称为动态绑定(Dynamic Binding)。
实例 1:动物发声
下面通过一个完整实例,演示如何使用虚函数实现多态。
实例
using namespace std;
// 动物基类
class Animal {
public:
// 定义虚函数
virtual void sound() const {
cout << "Animal makes a sound" << endl;
}
// 虚析构函数
virtual ~Animal() = default;
};
// 狗类
class Dog : public Animal {
public:
// 重写基类虚函数
void sound() const override {
cout << "Dog barks" << endl;
}
};
// 猫类
class Cat : public Animal {
public:
void sound() const override {
cout << "Cat meows" << endl;
}
};
int main() {
Dog dog;
Cat cat;
// 基类指针指向不同的派生类对象
Animal* animal = &dog;
animal->sound();
animal = &cat;
animal->sound();
return 0;
}
程序执行结果:
Dog barks Cat meows
代码分析
1、定义基类
Animal 类定义了虚函数 sound(),允许派生类提供不同的实现。
virtual void sound() const {
cout << "Animal makes a sound" << endl;
}
2、派生类重写虚函数
Dog 和 Cat 都继承自 Animal,并使用 override 重写 sound()。
3、通过基类指针调用函数
当基类指针指向 Dog 对象时,调用 Dog::sound();当它指向 Cat 对象时,调用 Cat::sound()。
因此,即使调用代码相同,程序仍然可以根据对象的实际类型执行不同的函数。
注意:实例使用栈上创建的对象,不需要通过 new 和 delete 手动管理内存。
实例 2:计算不同形状的面积
多态的一个重要用途,是通过统一接口处理不同类型的对象。
例如,矩形和三角形都需要计算面积,但它们的面积计算公式不同。
我们可以定义一个 Shape 基类,提供 area() 接口,再由不同的形状类重写具体计算方法。
实例
using namespace std;
// 形状基类
class Shape {
protected:
double width;
double height;
public:
Shape(double w, double h)
: width(w), height(h) {}
virtual double area() const {
return 0;
}
virtual ~Shape() = default;
};
// 矩形类
class Rectangle : public Shape {
public:
Rectangle(double w, double h)
: Shape(w, h) {}
double area() const override {
return width * height;
}
};
// 三角形类
class Triangle : public Shape {
public:
Triangle(double w, double h)
: Shape(w, h) {}
double area() const override {
return width * height / 2;
}
};
int main() {
Rectangle rec(10, 7);
Triangle tri(10, 5);
Shape* shape = &rec;
cout << "Rectangle Area: " << shape->area() << endl;
shape = &tri;
cout << "Triangle Area: " << shape->area() << endl;
return 0;
}
程序执行结果:
Rectangle Area: 70 Triangle Area: 25
程序分析:
Shape:形状基类,声明虚函数area()。Rectangle:矩形类,通过width * height计算面积。Triangle:三角形类,通过width * height / 2计算面积。Shape*:基类指针,可以指向不同的派生类对象。override:明确表示派生类正在重写基类的虚函数。
当增加新的形状类型时,例如圆形,只需要继承 Shape 并实现 area(),现有通过 Shape 接口进行计算的代码通常无需修改。
需要注意,以上 Shape 类并不是抽象类,因为它的 area() 是具有普通实现的虚函数,而不是纯虚函数。
虚函数
虚函数(Virtual Function)是在基类中使用 virtual 关键字声明的非静态成员函数。
派生类可以重写虚函数,使程序在通过基类指针或引用调用该函数时,根据对象的动态类型选择对应的实现。
例如:
class Base {
public:
virtual void show() {
// 基类实现
}
};
虚函数具有以下特点:
- 可以在基类中提供默认实现。
- 派生类可以选择是否重写。
- 通过基类指针或引用调用时,可以发生动态绑定。
- 派生类中的对应重写函数仍然是虚函数,即使没有再次写出
virtual。
没有 virtual 会怎样?
如果基类成员函数没有声明为虚函数,那么通过基类指针调用该函数时,通常根据指针的静态类型确定调用哪个版本。
实例
using namespace std;
class Animal {
public:
void sound() const {
cout << "Animal sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() const {
cout << "Dog barks" << endl;
}
};
int main() {
Dog dog;
Animal* animal = &dog;
// 通过基类指针调用
animal->sound();
// 直接调用派生类函数
dog.sound();
return 0;
}
执行结果:
Animal sound Dog barks
因为基类中的 sound() 不是虚函数,所以 animal->sound() 调用的是基类版本。
如果在基类的 sound() 前添加 virtual,那么通过基类指针调用时,就可以选择派生类的重写版本。
override 关键字
从 C++11 开始,可以使用 override 明确声明派生类函数用于重写基类虚函数。
例如:
class Base {
public:
virtual void show() const {}
};
class Derived : public Base {
public:
void show() const override {}
};
override 不是实现动态绑定的必要条件,但建议在重写虚函数时使用。
如果函数签名不匹配,编译器会报告错误。例如:
class Base {
public:
virtual void show() const {}
};
class Derived : public Base {
public:
// 编译错误:缺少 const,不能重写基类函数
void show() override {}
};
这样可以减少因为参数、const 限定符等不匹配而导致的错误。
final 关键字
从 C++11 开始,可以使用 final 禁止派生类进一步重写某个虚函数,或者禁止其他类继续继承某个类。
class Base {
public:
virtual void show() {}
};
class Derived : public Base {
public:
void show() final {}
};
以上代码中,Derived::show() 不能被更下层的派生类重写。
如果将 final 用于类:
class Dog final {
public:
void sound() {}
};
那么 Dog 不能再作为其他类的基类。
动态绑定
动态绑定(Dynamic Binding),也称为运行时绑定,是指虚函数调用的具体实现可以在程序运行时根据对象的动态类型确定。
与之相对的是静态绑定(Static Binding),即在编译阶段确定函数调用的目标。
| 对比项 | 静态绑定 | 动态绑定 |
|---|---|---|
| 确定时机 | 编译阶段 | 运行阶段 |
| 常见场景 | 普通成员函数、函数重载 | 通过基类指针或引用进行虚函数调用 |
| 是否需要虚函数 | 不需要 | 通常需要 |
| 主要特点 | 调用目标通常可以直接确定 | 根据对象动态类型选择实现 |
需要注意,虚函数调用不一定都必须在运行时进行动态查找。编译器在能够确定目标的情况下,也可能进行去虚拟化(Devirtualization)等优化。
虚析构函数
如果一个类作为多态基类使用,并且允许通过基类指针删除派生类对象,通常应该将基类析构函数声明为 virtual。
这样可以确保删除派生类对象时,正确执行派生类和基类的析构过程。
实例
using namespace std;
class Animal {
public:
virtual void sound() const {
cout << "Animal sound" << endl;
}
// 虚析构函数
virtual ~Animal() {
cout << "Animal destroyed" << endl;
}
};
class Dog : public Animal {
public:
void sound() const override {
cout << "Dog barks" << endl;
}
~Dog() override {
cout << "Dog destroyed" << endl;
}
};
int main() {
Animal* animal = new Dog();
animal->sound();
// 通过基类指针删除派生类对象
delete animal;
return 0;
}
执行结果:
Dog barks Dog destroyed Animal destroyed
调用 delete animal 时,会先执行 Dog 的析构函数,再执行 Animal 的析构函数。
注意:如果基类析构函数不是虚函数,通过基类指针删除派生类对象通常会产生未定义行为,而不只是简单地导致内存泄漏。
在现代 C++ 中,通常建议优先使用局部对象或智能指针管理资源,减少手动使用 new 和 delete。
纯虚函数
纯虚函数(Pure Virtual Function)是在虚函数声明后使用 = 0 指定的函数。
纯虚函数通常用于定义基类接口,要求具体派生类提供自己的实现。
基本语法:
virtual 返回类型 函数名(参数列表) = 0;
例如:
class Shape {
public:
virtual double area() const = 0;
};
包含尚未实现的纯虚函数的类是抽象类(Abstract Class),不能直接创建对象。
例如:
Shape shape; // 编译错误:不能实例化抽象类
不过,可以声明指向抽象类的指针或引用,用于操作具体的派生类对象。
实例:抽象类与纯虚函数
下面将前面形状类中的 area() 改为纯虚函数,使不同的形状类必须提供计算面积的方法,才能实例化对象。
实例
using namespace std;
// 抽象基类
class Shape {
public:
// 纯虚函数
virtual double area() const = 0;
virtual ~Shape() = default;
};
// 矩形
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h)
: width(w), height(h) {}
double area() const override {
return width * height;
}
};
// 三角形
class Triangle : public Shape {
private:
double width;
double height;
public:
Triangle(double w, double h)
: width(w), height(h) {}
double area() const override {
return width * height / 2;
}
};
int main() {
Rectangle rec(10, 5);
Triangle tri(10, 6);
// 基类引用可以绑定派生类对象
Shape& shape1 = rec;
Shape& shape2 = tri;
cout << "Rectangle Area: " << shape1.area() << endl;
cout << "Triangle Area: " << shape2.area() << endl;
return 0;
}
程序执行结果:
Rectangle Area: 50 Triangle Area: 30
程序分析:
Shape:抽象基类,定义纯虚函数area()。Rectangle:提供矩形面积的计算实现。Triangle:提供三角形面积的计算实现。Shape&:基类引用,可以绑定派生类对象并调用其虚函数。override:检查派生类函数是否正确重写了基类虚函数。
如果派生类没有实现继承的全部纯虚函数,那么该派生类仍然是抽象类,不能直接实例化。
此外,纯虚函数也可以在类外提供函数定义,但这不会改变类的抽象性质。纯虚析构函数是一个特殊情况,即使声明为 = 0,仍然必须提供定义。
虚函数与纯虚函数的区别
| 对比项 | 虚函数 | 纯虚函数 |
|---|---|---|
| 声明方式 | virtual void show(); |
virtual void show() = 0; |
| 是否允许提供实现 | 允许 | 允许在类外提供定义 |
| 派生类是否必须重写 | 不必须 | 具体派生类必须提供非纯的最终重写版本 |
| 是否使类成为抽象类 | 不会仅因为普通虚函数而成为抽象类 | 若存在未实现的纯虚函数,则属于抽象类 |
| 主要用途 | 提供可重写的行为 | 定义必须由具体派生类实现的接口 |
虚函数表与实现机制
在很多 C++ 编译器中,运行时多态通过虚函数表(Virtual Table,简称 vtable)和虚函数指针(通常称为 vptr)实现。
虚函数表通常保存与虚函数动态分派有关的信息,对象则可能通过内部指针关联相应的虚函数表。
例如:
Animal* animal = &dog; animal->sound();
程序可以根据对象的动态类型找到对应的 sound() 实现。
注意:C++ 标准规定了虚函数调用的行为,但没有强制要求编译器必须使用特定的虚函数表或虚函数指针布局。
另外,普通虚函数动态分派不依赖 RTTI(运行时类型信息)功能。RTTI 主要用于 dynamic_cast、typeid 等运行时类型识别场景。
使用多态的注意事项
在实际 C++ 开发中,使用多态还需要注意以下问题:
- 优先使用 override:在派生类中重写虚函数时添加
override,让编译器检查函数签名。 - 正确设计析构函数:允许通过基类指针删除对象时,基类通常需要公共虚析构函数。
- 避免对象切片:将派生类对象按值赋给基类对象时,派生类特有部分可能被丢弃,通常应使用引用或指针保持多态行为。
- 避免不必要的动态分派:如果不需要运行时多态,可以考虑普通函数、函数重载或模板。
- 谨慎在构造和析构期间调用虚函数:此时不会分派到尚未构造或已经析构的更下层派生类部分。
多态适合不同类型对象需要通过统一接口处理的场景,例如图形系统、游戏对象、插件接口和设备驱动等。
但并不是所有继承关系都需要虚函数。应根据实际需求决定是否使用运行时多态。
总结
C++ 多态使不同类型的对象能够通过统一接口表现出不同的行为。
在运行时多态中,最重要的几个概念包括:
virtual:声明虚函数,使派生类能够重写并支持动态分派。override:显式声明函数重写关系,并由编译器进行检查。final:禁止继续重写虚函数或继承指定类。= 0:声明纯虚函数,使对应类成为抽象类。- 虚析构函数:保证通过适当的基类指针删除派生类对象时能够正确析构。
对于初学者,建议先掌握基类指针、虚函数和函数重写的关系,再进一步学习抽象类、纯虚函数和多态接口设计。
lqd052
172***2008@qq.com
参考地址
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错,错误提示为:
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
lqd052
172***2008@qq.com
参考地址
ZZsprite
125***39009@qq.com
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;
形成多态必须具备三个条件:
1、必须存在继承关系;
2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;
ZZsprite
125***39009@qq.com
Coder
cm6***01@qq.com
动态联编的实现机制 VTABLE
编译器对每个包含虚函数的类创建一个虚函数表VTABLE,表中每一项指向一个虚函数的地址,即VTABLE表可以看成一个函数指针的数组,每个虚函数的入口地址就是这个数组的一个元素。
每个含有虚函数的类都有各自的一张虚函数表VTABLE。每个派生类的VTABLE继承了它各个基类的VTABLE,如果基类VTABLE中包含某一项(虚函数的入口地址),则其派生类的VTABLE中也将包含同样的一项,但是两项的值可能不同。如果派生类中重载了该项对应的虚函数,则派生类VTABLE的该项指向重载后的虚函数,如果派生类中没有对该项对应的虚函数进行重新定义,则使用基类的这个虚函数地址。
在创建含有虚函数的类的对象的时候,编译器会在每个对象的内存布局中增加一个vptr指针项,该指针指向本类的VTABLE。在通过指向基类对象的指针(设为bp)调用一个虚函数时,编译器生成的代码是先获取所指对象的vtb1指针,然后调用vtb1所指向类的VTABLE中的对应项(具体虚函数的入口地址)。
当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;
当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。
包含一个虚函数和几个虚函数的类的长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。
派生类与基类同名的虚函数在VTABLE中有相同的索引号(或序号)。
Coder
cm6***01@qq.com
白菜
109***6982@qq.com
虚函数这里说的有些乱,因为 C++ 写法奇葩略多。其实可以简单理解。
虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。
在一个类中如果存在未定义的虚函数,那么不能直接使用该类的实例,可以理解因为未定义 virtual 函数,其类是抽象的,无法实例化。将报错误:
这和其它语言的抽象类,抽象方法是类似的——我们必须实现抽象类,否则无法实例化。(virtual 和 abstract还是有些区别的)
也就是说,如果存在以下代码:
using namespace std; class Base { public: virtual void tall(); }; class People : Base { public: void tall() { cout << "people" << endl; }; };那么,在 main 方法中,我们不能使用 Base base; 这行代码,此时的 tall 没有实现,函数表(vtable)的引用是未定义的,故而无法执行。但我们可以使用 People people; 然后 people.tall(); 或 (&people)->tall(); 因为People实现或者说重写、覆盖了 Base 的纯虚方法 tall(),使其在 People 类中有了定义,函数表挂上去了,于是可以诞生实例了。
int main() { // Base base;//不可用 People people;//可用 people.tall(); (&people)->tall(); return 0; }上述的是针对虚函数而言,普通的函数,即使我们只声明,不定义,也不会产生上述不可用的问题。
白菜
109***6982@qq.com
ILSYT
175***4680@qq.com
父类的虚函数或纯虚函数在子类中依然是虚函数。有时我们并不希望父类的某个函数在子类中被重写,在 C++11 及以后可以用关键字 final 来避免该函数再次被重写。
例:
#include<iostream> using namespace std; class Base { public: virtual void func() { cout<<"This is Base"<<endl; } }; class _Base:public Base { public: void func() final//正确,func在Base中是虚函数 { cout<<"This is _Base"<<endl; } }; class __Base:public _Base { /* public://不正确,func在_Base中已经不再是虚函数,不能再被重写 void func() { cout<<"This is __Base"<<endl; }*/ }; int main() { _Base a; __Base b; Base* ptr=&a; ptr->func(); ptr=&b; _Base* ptr2=&b; ptr->func(); ptr2->func(); }以上程序运行结果:
如果不希望一个类被继承,也可以使用 final 关键字。
格式如下:
class Class_name final { ... };则该类将不能被继承。
ILSYT
175***4680@qq.com
初次
130***1320@qq.com
visual studio code1、多态的理念主要就是用到纯虚函数或者虚函数这个,这个c++的特殊处理函数利用父对象指针来访问子对象,然后根据子对象的函数来调用。
#include <iostream> using namespace std; class parent { public: parent(int a =0):a(a){}; ~parent(){}; virtual void show_classname() = 0; private: int a; }; class child1 : public parent { public: child1(int a= 0, int b = 0):parent(a),b(b){}; ~child1(){}; void show_classname(){ cout << "class name is child1" <<endl; } private: int b; }; class child2 : public parent { public: child2(int a=0,int c =0):parent(a),c(c){}; ~child2(){}; void show_classname() { cout<< "class name is child2" <<endl; } private: int c; }; int main() { child1 b; child2 c; parent *a; a= &b; cout << "a ---> b"<<endl; a->show_classname(); a= &c; cout << "a ---> c"<<endl; a->show_classname(); }结果:
初次
130***1320@qq.com
泊宇
aiy***ouai@163.com
A 类继承 B 类,A 类重写了 B 类的虚函数 B2,而 B 类的 B1 中调用了 B2,则 A 类使用 B1 时,运行时 B1 中的 B2 将执行 A 类中重写的虚函数B2。
例程如下:
#include <iostream> using namespace std; class B { public: void B1(void) { B2(); } virtual void B2(void) { cout << "Hello, B2" << endl; } }; class A:public B { public: virtual void B2(void) { cout << "Hello, A" << endl; } }; int main() { A *pA=new A; pA->B1(); cout << "Hello, world!" << endl; return 0; }执行结果:
泊宇
aiy***ouai@163.com