一、多态的概念及实现
1、什么是多态?
多态是 C++ 面向对象编程的重要特性之一,分为编译时多态(静态多态)和运行时多态(动态多态)。本文重点讲解运行时多态。
编译时多态主要通过函数重载和函数模板实现,通过不同的参数类型达到多种形态。而运行时多态是指完成某个行为(函数)时,传不同的对象来完成不同的行为,从而达到多种形态。
C++ 多态分为编译时多态和运行时多态,重点在于通过基类指针或引用调用虚函数实现动态绑定。核心机制涉及虚函数表(vtable)和虚函数表指针(vfptr)。派生类重写基类虚函数时需注意参数、返回值及析构函数的特殊性。纯虚函数定义抽象类,强制子类实现接口。理解底层原理有助于避免内存泄漏并掌握动态绑定的本质。

多态是 C++ 面向对象编程的重要特性之一,分为编译时多态(静态多态)和运行时多态(动态多态)。本文重点讲解运行时多态。
编译时多态主要通过函数重载和函数模板实现,通过不同的参数类型达到多种形态。而运行时多态是指完成某个行为(函数)时,传不同的对象来完成不同的行为,从而达到多种形态。
例如买票行为:普通人全价,学生优惠,军人优先。又如动物叫的行为:猫对象调用输出'喵',狗对象调用输出'汪汪'。
类成员函数前面加 virtual 修饰,则该成员函数被称为虚函数。注意非成员函数不能加 virtual 修饰。
class Person {
public:
// 这里在成员函数 BuyTicket 前面加了 virtual,则 BuyTicket 就是虚函数
virtual void BuyTicket() {
cout << "买票 - 全价" << endl;
}
};
虚函数的重写 (Override) 发生在派生类,基类中必须有一个被 virtual 修饰的虚函数。
重写是实现多态的关键。它的定义是:在派生类中提供一个与基类中某个虚函数具有完全相同函数名、参数列表、返回值类型的函数。
虚函数重写的目的就是为了在运行时,当通过基类指针或引用指向派生类对象时,能够调用派生类的版本,而不是基类的版本。这就是'动态绑定'。
// Person 类与 Student 类中的 buyticket 构成重写
class Person {
public:
virtual void buyticket() {
cout << "全价票" << endl;
}
};
class Student : public Person {
public:
virtual void buyticket() {
cout << "半价票" << endl;
}
};
注意:在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,也可以构成重写(因为子类继承基类后,基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
// A 类与 B 类中的 func 构成重写
class A {
public:
virtual void func() {}
};
class B : public A {
public:
void func() {}
};
C++11 提供了 override 关键字,可以帮助用户检测是否重写,防止因函数名写错或参数写错导致无法构成重写而在编译期间不报错的情况。
class Person {
public:
virtual void buyticket() {}
};
class Student : public Person {
public:
virtual void buyticket() override {}
};
如果不想让派生类重写这个虚函数,可以用 final 去修饰。
说明:只有基类的指针或者引用才能同时指向基类和派生类的对象;派生类必须对基类的虚函数完成重写/覆盖,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
// 基类
class Person {
public:
virtual void buyticket() {
cout << "全价票" << endl;
}
};
// 派生类
class Student : public Person {
public:
virtual void buyticket() override {
cout << "半价票" << endl;
}
};
void Func(Person* ptr) { // 基类的指针来接收参数
ptr->buyticket(); // 用基类的指针调用虚函数
}
int main() {
Person p; // 基类对象
Student s; // 派生类对象
Func(&p); // 将基类对象作为实参,则调用基类的虚函数
Func(&s); // 将派生类对象作为实参,则调用派生类的虚函数
return 0;
}
以下程序输出结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
答案:B
分析:题目中创建了一个指向 B 类类型对象的指针 p,通过指针 p 来调用 test 函数。由于 test 函数是基类 A 的成员函数,所以隐含的参数 this 指针是 A* 类型的。同时基类与子类中 func 函数构成虚函数的重写,虽然派生类的 func 函数并没有加 virtual 关键字,但是由于继承的原因,仍然构成重写,满足多态条件。此时会根据真正的实参的类型来决定调用哪个虚函数(动态绑定),指针 p 是派生类类型的,所以会调用派生类 B 中的 func 函数。
但是,默认参数是静态绑定的(编译时根据调用者的类决定):test() 属于基类 A,因此 func() 的默认参数 val 会使用 A 中声明的 val = 1(而非 B 中声明的 val = 0)。
所以,选择 B:B -> 1。
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写。编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor。
为什么要这样设计?当一个基类的指针 ptr 指向派生类对象时,如果基类的析构函数不实现为虚函数,那么当 delete ptr 释放该对象时,由于静态绑定,就只调用基类的析构函数。如果派生类对象中有额外的资源,就会导致内存泄漏。
所以,C++ 中只要基类的析构函数实现为虚函数,无论派生类析构函数加不加 virtual 关键字,都会与基类的析构函数构成重写,即此时满足多态。编译器会根据 ptr 指针真正指向的类类型对象来调用相应的析构函数(动态绑定)。
// 基类
class A {
public:
virtual ~A() {} // 虚函数
protected:
int _a;
};
// 子类
class B : public A {
public:
~B() { delete[]_b; _b = nullptr; }
protected:
int* _b = new int[10]; // 派生类中有额外的资源
};
// 用一个基类的指针指向派生类对象
int main() {
A* a1 = new B;
delete a1;
return 0;
}
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person {
public:
virtual Person* BuyTicket() { // 返回值为基类的指针
cout << "买票 - 全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual Student* BuyTicket() { // 返回值为派生类的指针
cout << "买票 - 打折" << endl;
return nullptr;
}
};
void Func(Person* ptr) {
ptr->BuyTicket();
}
int main() {
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
// 基类(抽象类)
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;
}
};
int main() {
// 错误示范:编译报错:error C2259: 'Car': 无法实例化抽象类
// Car car;
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
在 32 位机器下,一个指针占 4 个字节。如果一个类有虚函数,对象大小会增加一个指针的大小。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base {
public:
// 重写基类的 func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main() {
Base b;
Derive d;
return 0;
}
满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
class Person {
public:
virtual void buyticket() { cout << "全价票" << endl; }
protected:
string _name;
};
class Student : public Person {
public:
virtual void buyticket() { cout << "半价票" << endl; }
protected:
int _id;
};
class Soldier : public Person {
public:
virtual void buyticket() { cout << "优先买票" << endl; }
protected:
int _codename;
};
多态是 C++ 面向对象编程非常重要的一个特性,多态在我们处理一些具有相似特性的问题时,有着非常重要的作用,同时在面试中也有许多的考点和细节。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online