4人参与 • 2025-12-08 • C/C++
从字面上理解,多态就是“多种形态”。在程序设计里,它指的是:
使用统一的接口,却可以对不同类型的对象做出不同的具体行为。
更具体一点:
virtual 虚函数 + 继承 + 基类指针/引用。“静态”的含义是:绑定发生在编译期。
常见的静态多态形式有三个:函数重载、运算符重载、模板。
同名函数,根据参数列表的不同进行区分:
void print(int x) {
std::cout << "int: " << x << std::endl;
}
void print(double x) {
std::cout << "double: " << x << std::endl;
}
void print(const std::string& s) {
std::cout << "string: " << s << std::endl;
}
int main() {
print(10); // 调用 print(int)
print(3.14); // 调用 print(double)
print("hello"); // 字面量转成 std::string,调用 print(const std::string&)
}
在这里,“多态”的表现是:同一个名字 print,可以处理不同的类型。
编译器会在编译期进行“重载决议”,选出最合适的一个版本。这就是静态多态。
补充一个和继承相关的点:
如果派生类中重新定义了与基类同名但参数不同的函数,会发生“名字隐藏”。要想保留基类的其他重载,可以用using base::func;把基类同名重载导入作用域。
运算符重载本质上也是一种函数重载,区别只是语法形式更自然。编译器在编译期决定调用哪个重载,所以它也是静态多态。
struct point {
int x, y;
point(int x, int y) : x(x), y(y) {}
point operator+(const point& other) const {
return point(x + other.x, y + other.y);
}
};
int main() {
point a(1, 2), b(3, 4);
point c = a + b; // 实际是调用 a.operator+(b)
std::cout << c.x << ", " << c.y << std::endl; // 4, 6
}
“同一个运算符 +” 对于不同类型(例如 int + int、point + point)会产生不同的行为,同样属于静态多态。
模板是 c++ 中实现静态多态最强大的工具。函数模板和类模板都属于参数化多态,在编译期根据类型参数生成具体代码。
template <typename t>
t add(t a, t b) {
return a + b; // 只要求 t 支持 operator+
}
int main() {
std::cout << add(1, 2) << std::endl; // 实例化出 add<int>
std::cout << add(1.5, 2.5) << std::endl; // 实例化出 add<double>
std::cout << add(std::string("a"), "b") << std::endl; // 实例化出 add<std::string>
}
这里的 add 在源代码里只写了一份,但编译器会根据实际调用自动生成多个版本。
本质上,它也是一种“接口相同(add),但根据类型不同产生不同行为”的多态,只是全部发生在编译期。
模板和函数重载还可以配合使用(例如
std::sort接受不同类型的迭代器、不同的比较器),本质上依然是静态多态的一种组合形式。
静态多态的“主角”是“类型”和“模板参数”,它解决的是“类型不一样怎么共享代码”。
动态多态的“主角”是“对象的实际类型”,解决的是“一群有共同接口的对象,具体用哪个实现要到运行期才知道”。
c++ 中要用到动态多态,基本需要三个条件:
virtual;经典例子:
class shape {
public:
virtual void draw() { // 虚函数
std::cout << "shape::draw" << std::endl;
}
virtual ~shape() = default; // 虚析构,后面会讲
};
class circle : public shape {
public:
void draw() override { // override 明确表明“重写基类虚函数”
std::cout << "circle::draw" << std::endl;
}
};
class rect : public shape {
public:
void draw() override {
std::cout << "rect::draw" << std::endl;
}
};
void render(shape& s) {
s.draw(); // 这里发生动态绑定
}
int main() {
circle c;
rect r;
render(c); // 调用 circle::draw
render(r); // 调用 rect::draw
}
这里 render 只认识 shape& 这个“统一接口”,但传入不同的实际对象(circle 或 rect)时,会在运行期调用不同版本的 draw。这就是运行期多态。
注意:
如果是值传递,比如void render(shape s),那么会发生对象切片(object slicing),派生类部分被“切掉”,只剩下基类部分,动态多态就失效了。因此,多态场景下要习惯性使用指针或引用。
典型实现(大多数主流编译器采用类似思路)是这样的:
每个含有虚函数的类,编译器都会为它生成一张虚函数表(vtable),里面是一串“函数指针”;
每个对象里会隐藏一个指针(通常叫 vptr),指向它所属类的那张虚函数表;
当你写 p->func1() 时,如果 func1 是虚函数,编译器会把它翻译成类似:
// 伪代码 p->vptr[func1_index](p);
即:从对象中取出 vptr,根据函数在虚表中的位置,找到对应的函数指针,然后调用。
情景:base类写了两个虚函数func1和func2,在子类derived类中重写了func1没有重写func2
class base {
public:
virtual void func1();
virtual void func2();
};
class derived : public base {
public:
void func1() override;
// 没有重写 func2()
};
那么典型的虚表布局可以想象为:
base 的虚表大致为:
| index | 函数 |
|---|---|
| 0 | base::func1 |
| 1 | base::func2 |
derived 的虚表大致为:
| index | 函数 |
|---|---|
| 0 | derived::func1 |
| 1 | base::func2 |
也就是说:
派生类重写了哪个虚函数,对应虚表条目就改成指向派生类实现;没重写的虚函数,虚表里仍然指向基类实现。
这带来的一个重要结论是:
在构造函数或析构函数内部调用虚函数时,不会表现出“派生类版本”,而是调用当前构造/析构阶段对应类的版本。这是为了避免访问尚未构造/已经销毁的派生类成员。
有时我们只关心接口,不希望有人直接创建这个类的实例,就可以使用纯虚函数定义一个抽象类:
class shape {
public:
virtual void draw() = 0; // 纯虚函数
virtual ~shape() = default;
};
特点:
shape s; // 编译错误;抽象类非常适合用来作为“接口基类”,例如游戏引擎中常见的 gameobject 基类,定义一组必须实现的接口如 update(), render() 等。
动态多态中,一个非常重要但容易忽略的点是:基类析构函数要声明为 virtual。
典型情景:
class base {
public:
virtual ~base() { // 必须是虚析构
std::cout << "base dtor\n";
}
};
class derived : public base {
public:
~derived() {
std::cout << "derived dtor\n";
}
};
int main() {
base* p = new derived();
delete p;
}
如果 ~base() 不是虚函数,那么 delete p; 只会调用 base 的析构函数,而不会调用 derived 的析构函数,导致派生类中资源泄漏。这在实际工程里非常危险。
只要你打算通过 base* 或 base& 以多态方式管理对象生命周期,就应该把基类析构函数声明为 virtual。
默认参数是静态绑定的:它们在编译期根据静态类型来决定。
class base {
public:
virtual void func(int x = 1) {
std::cout << "base: " << x << std::endl;
}
};
class derived : public base {
public:
void func(int x = 2) override {
std::cout << "derived: " << x << std::endl;
}
};
int main() {
derived d;
base* p = &d;
p->func(); // 输出什么?
}
这里:
derived::func(虚函数,运行期绑定);p 的静态类型 base* 为准,所以默认值是 1。最终输出:derived: 1。
所以建议:不要依赖虚函数的默认参数来区分行为,或者干脆在基类中避免给虚函数提供默认参数。
derived d; base b = d; // 发生对象切片
此时 b 只是一个独立的 base 对象,派生类部分被“切掉了”,多态自然不存在了。
因此,多态设计中一般采用 base* 或 base& ,而不是按值传递/按值存储。
简单对比一下两者的特点:
| 特性 | 静态多态(重载/模板) | 动态多态(虚函数) |
|---|---|---|
| 绑定时机 | 编译期 | 运行期 |
| 性能开销 | 无虚表开销,通常更快 | 通过虚表间接调用,有一点调用开销 |
| 代码体积 | 模板实例化可能生成很多代码 | 一般较稳定 |
| 灵活性 | 编译期就要知道所有类型 | 可以运行期决定具体类型 |
| 典型使用场景 | stl 算法、通用工具库、数值计算等 | 插件系统、ui 系统、游戏对象系统等 |
| 需要的语言特性 | 函数重载、运算符重载、模板 | 继承、虚函数、基类指针/引用 |
两者不是“谁更高级”的关系,而是各有适用场景:
比如写一个简单版本的 for_each:
template <typename it, typename func>
void my_for_each(it first, it last, func f) {
for (; first != last; ++first) {
f(*first);
}
}
int main() {
std::vector<int> v{1, 2, 3};
my_for_each(v.begin(), v.end(), [](int x) {
std::cout << x << " ";
});
}
func 可以是函数指针、函数对象、lambda;it 可以是各种迭代器;这就是典型的静态多态用法,也是 stl 的设计思想。
假设一个游戏里有不同的实体:玩家、怪物、npc,都需要 update():
class entity {
public:
virtual void update(float dt) = 0; // 纯虚函数
virtual ~entity() = default;
};
class player : public entity {
public:
void update(float dt) override {
// 处理玩家输入、移动等
}
};
class monster : public entity {
public:
void update(float dt) override {
// ai 行为
}
};
void updateall(std::vector<std::unique_ptr<entity>>& entities, float dt) {
for (auto& e : entities) {
e->update(dt); // 动态多态,运行期调用对应实体的 update
}
}
在这里:
std::vector<std::unique_ptr<entity>>;player 还是 monster,全部通过多态调用 update;典型地,这种需要“运行时混合多种类型”的场景,非常适合用动态多态。
到此这篇关于c++多态详解之从静态多态到动态多态的文章就介绍到这了,更多相关c++从静态多态到动态多态内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论