C++ <多态>详解:从概念到底层实现
目录
1. 多态的概念
2. 多态的定义及实现
2.1 多态的构成条件
2.2 虚函数与重写
特殊重写场景:
2.3 关键字:override 与 final
3. 纯虚函数与抽象类
4. 多态的底层原理
4.1 虚函数表指针(_vfptr)
4.2 动态绑定与静态绑定
5. 常见面试题解析
6. 总结
多态是 C++ 面向对象编程的核心特性之一,它允许不同对象对同一消息作出不同响应。理解多态不仅能写出更灵活的代码,也是面试中的高频考点。本文将从概念、实现条件、关键特性到底层原理,全面剖析 C++ 多态的精髓。
1. 多态的概念
多态(polymorphism)字面意思是 "多种形态",在 C++ 中分为两类:
- 编译时多态:通过函数重载和函数模板实现,编译器在编译阶段根据参数类型匹配相应函数,属于静态多态。
- 运行时多态:程序运行时根据传递的对象类型决定调用哪个函数,这是本文的重点。
运行时多态的经典案例:
- 买票行为:普通人全价、学生半价、军人优先
- 动物叫行为:猫 "喵"、狗 "汪"
//买票的多态实例
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() //可以不加virtual 不规范{cout << "买票-半价" << endl;}
};void Func(Person& ptr) //基类的引用或指针
{ptr.BuyTicket();
}// 动物叫的多态示例
class Animal {
public:virtual void talk() const {}
};
class Dog : public Animal {
public:virtual void talk() const { cout << "汪汪" << endl; }
};
class Cat : public Animal {
public:virtual void talk() const { cout << "喵" << endl; }
};
// 同一函数接收不同对象,表现不同行为
void letsHear(const Animal& animal) {animal.talk();
}
2. 多态的定义及实现
2.1 多态的构成条件
多态需满足继承关系基础上的两个核心条件:
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 p;Student s;Func(p); // 输出"买票-全价"Func(s); // 输出"买票-半价"
}
2.2 虚函数与重写
- 虚函数:用
virtual
修饰的类成员函数(非成员函数不能用virtual
)。 - 重写规则:派生类虚函数需与基类虚函数的函数名、参数列表、返回值完全一致(协变除外)。
注意:派生类重写时可省略
virtual
(不推荐),但仍视为虚函数。
特殊重写场景:
-
协变:返回值可改为基类 / 派生类的指针 / 引用(需满足继承关系)
class A {}; class B : public A {}; class Person { public:virtual A* BuyTicket() { return nullptr; } // 基类指针 }; class Student : public Person { public:virtual B* BuyTicket() { return nullptr; } // 派生类指针(协变) };
析构函数重写:基类析构函数为虚函数时,派生类析构函数自动视为重写(编译器统一处理析构函数名为
destructor
)class A { public:virtual ~A() { cout << "~A()" << endl; } // 虚析构 }; class B : public A { public:~B() { cout << "~B()" << endl; } // 自动重写 }; // 正确释放派生类资源,避免内存泄漏 int main() {A* p = new B;delete p; // 先调用~B(),再调用~A() }
2.3 关键字:override 与 final
- override:强制检查派生类函数是否重写基类虚函数,未重写则编译报错。
- final:修饰虚函数时,禁止派生类重写该函数;修饰类时,禁止被继承。
// C++11提供了override,可以帮助⽤⼾检测是否重写。 class Car { public:virtual Dirve(){} };class Benz : public Car { public:virtual Dirve() override{cout << "Benz-舒适" << endl;} };int main() {return 0; }
class Car { public:virtual void Drive() final {} // 禁止重写 }; class Benz : public Car { public:virtual void Drive() {} // 编译报错:无法重写final函数 };
3. 纯虚函数与抽象类
- 纯虚函数:在虚函数后加
=0
,无需实现(语法允许实现但无意义)。 - 抽象类:包含纯虚函数的类,不能实例化对象,派生类需重写纯虚函数才能实例化。
class Car { public:virtual void Drive() = 0; // 纯虚函数 }; class Benz : public Car { public:virtual void Drive() { cout << "Benz-舒适" << endl; } // 重写后可实例化 }; int main() {// Car car; // 报错:抽象类不能实例化Car* p = new Benz; // 正确p->Drive(); }
4. 多态的底层原理
4.1 虚函数表指针(_vfptr)
含有虚函数的类,其对象会额外包含一个虚函数表指针(
_vfptr
),指向虚函数表(简称虚表)。虚表是存储虚函数地址的指针数组,同类型对象共享同一张虚表。
class Base {
public:virtual void Func1() { cout << "Func1()" << endl; }
protected:int _b = 1;char _ch = 'x';
};
int main() {Base b;cout << sizeof(b) << endl; // 32位下为12字节(4字节指针 + 4字节int + 4字节对齐)
}
4.2 动态绑定与静态绑定
- 静态绑定:编译时确定函数调用地址(非虚函数或不满足多态条件时)。
- 动态绑定:运行时通过对象的虚表查找函数地址(满足多态条件时)。
虚表结构示例:
- 基类虚表:存储基类所有虚函数地址。
- 派生类虚表:先复制基类虚表,再用派生类重写的虚函数地址覆盖对应位置,最后添加派生类自身的虚函数地址。
5. 常见面试题解析
问题:以下程序输出结果是?
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() {B* p = new B;p->test(); // 输出:B->1
}
解析:
test()
是基类函数,调用func()
时满足多态,实际调用派生类B::func
。- 虚函数重写仅覆盖函数体(即函数的实现),默认参数仍使用基类的(val=1),故输出
B->1
。
6. 总结
多态是 C++ 面向对象的核心特性,通过虚函数重写和虚表机制实现,允许同一接口呈现不同行为。关键要点:
- 运行时多态需满足:基类指针 / 引用 + 虚函数重写。
- 虚表存储虚函数地址,派生类虚表覆盖基类重写函数地址。
- 基类析构函数建议设为虚函数,避免派生类资源泄漏。
- 抽象类通过纯虚函数强制派生类实现特定接口,提升代码规范性。
掌握多态不仅能写出更灵活的代码,也是理解 C++ 底层机制的重要一步。实际开发中,合理使用多态可显著提高代码的可扩展性和维护性。