c++高频总结一
智能指针实现原理
智能指针是现代 C++ 中用于管理动态分配资源(如堆内存)的重要工具,其核心原理在于RAII(Resource Acquisition Is Initialization)。以下是智能指针实现的详细原理,包括常见类型的智能指针(std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
)。
1. std::unique_ptr
的实现原理
std::unique_ptr
是一种独占所有权的智能指针,一个对象只能由一个 unique_ptr
所拥有。其原理主要包括:
核心特性:
- 单一所有权:不能复制,但可以通过
std::move
转移所有权。 - 自动销毁:当
unique_ptr
超出作用域时,自动释放所管理的资源。
实现细节:
- 内部通过一个裸指针存储资源(如
T* ptr
)。 - 构造函数和析构函数负责资源的初始化和释放。
- 禁用拷贝构造和拷贝赋值操作,确保唯一性。
- 提供移动构造和移动赋值操作,将资源所有权从一个
unique_ptr
转移到另一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:
T* ptr; // 所管理的资源
Deleter deleter; // 自定义删除器
public:
// 构造函数
explicit unique_ptr(T* p = nullptr) : ptr(p) {}
// 禁用拷贝构造和拷贝赋值
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造和移动赋值
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(other.release());
}
return *this;
}
// 析构函数
~unique_ptr() {
deleter(ptr); // 释放资源
}
// 操作方法
T* release() noexcept { // 放弃所有权
T* temp = ptr;
ptr = nullptr;
return temp;
}
void reset(T* p = nullptr) {
deleter(ptr); // 删除旧资源
ptr = p;
}
T* get() const noexcept { return ptr; }
};
2. std::shared_ptr
的实现原理
std::shared_ptr
是一种共享所有权的智能指针,多个 shared_ptr
可以共享同一个资源,其销毁由最后一个 shared_ptr
完成。
核心特性:
- 共享所有权:多个
shared_ptr
可以管理同一个资源。 - 引用计数:通过引用计数追踪资源的所有者数量。
- 线程安全:引用计数操作通常是原子操作。
实现细节:
- 使用一个控制块(control block)存储引用计数和资源指针。
- 每次
shared_ptr
的拷贝或赋值会增加引用计数,每次销毁会减少引用计数。 - 当引用计数归零时,释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
template <typename T>
class shared_ptr {
private:
T* ptr; // 所管理的资源
struct control_block {
size_t ref_count; // 引用计数
size_t weak_count; // 弱引用计数
control_block() : ref_count(1), weak_count(0) {}
} * ctrl;
public:
// 构造函数
explicit shared_ptr(T* p = nullptr)
: ptr(p), ctrl(p ? new control_block() : nullptr) {}
// 拷贝构造
shared_ptr(const shared_ptr& other)
: ptr(other.ptr), ctrl(other.ctrl) {
if (ctrl) ++ctrl->ref_count;
}
// 赋值操作
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
reset();
ptr = other.ptr;
ctrl = other.ctrl;
if (ctrl) ++ctrl->ref_count;
}
return *this;
}
// 析构函数
~shared_ptr() {
reset();
}
// 重置
void reset() {
if (ctrl && --ctrl->ref_count == 0) {
delete ptr; // 释放资源
if (ctrl->weak_count == 0)
delete ctrl; // 删除控制块
}
ptr = nullptr;
ctrl = nullptr;
}
size_t use_count() const {
return ctrl ? ctrl->ref_count : 0;
}
T* get() const noexcept { return ptr; }
};
3. std::weak_ptr
的实现原理
std::weak_ptr
是对 std::shared_ptr
所管理资源的弱引用,它不影响引用计数,主要用于解决循环引用问题。
核心特性:
- 不影响资源的生命周期。
- 可通过
lock()
方法获得shared_ptr
,前提是资源尚未被销毁。
实现细节:
- 与
shared_ptr
共享控制块,但不增加引用计数。 - 控制块中包含弱引用计数
weak_count
,用于管理weak_ptr
的生命周期。 - 只有当引用计数和弱引用计数都为 0 时,控制块才会被销毁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template <typename T>
class weak_ptr {
private:
T* ptr; // 所管理的资源
shared_ptr<T>::control_block* ctrl; // 共享控制块
public:
weak_ptr() : ptr(nullptr), ctrl(nullptr) {}
weak_ptr(const shared_ptr<T>& sp)
: ptr(sp.ptr), ctrl(sp.ctrl) {
if (ctrl) ++ctrl->weak_count;
}
~weak_ptr() {
if (ctrl && --ctrl->weak_count == 0 && ctrl->ref_count == 0) {
delete ctrl; // 删除控制块
}
}
shared_ptr<T> lock() const {
if (ctrl && ctrl->ref_count > 0) {
return shared_ptr<T>(*this);
}
return shared_ptr<T>();
}
};
4. 常见问题与优化
- 循环引用:
shared_ptr
的循环引用会导致资源泄漏,weak_ptr
用于解决此问题。 - 性能问题:
shared_ptr
的引用计数增加和减少通常是原子操作,在高并发环境中可能带来性能开销。 - 自定义删除器:智能指针支持自定义删除器(如释放文件句柄、关闭套接字)。
智能指针的核心思想是通过对象生命周期的管理,确保资源在适当的时间被释放,从而避免内存泄漏和悬挂指针问题。
weak_count
是智能指针的控制块中一个重要的成员变量,其作用是用于管理 weak_ptr
的生命周期,并辅助 shared_ptr
的控制块在资源完全不被引用时正确销毁。它在 shared_ptr
和 weak_ptr
的交互中起到了关键作用。
以下是 weak_count
在 shared_ptr
和 weak_ptr
中的具体作用:
1. weak_count
在 shared_ptr
中的作用
- 辅助控制块的销毁:
shared_ptr
管理的资源会在引用计数 (ref_count
) 归零时销毁,但控制块本身只有在weak_count
也为零时才会销毁。- 当
ref_count
变为 0 时,资源(如堆内存)被释放。 - 当
ref_count
和weak_count
都变为 0 时,控制块本身被销毁。
- 当
为什么需要 weak_count
?
weak_ptr
不会增加资源的引用计数 (ref_count
),但它需要依赖控制块来判断资源是否有效。因此,即使资源被销毁,控制块需要保留,直到所有的 weak_ptr
都被销毁为止。
2. weak_count
在 weak_ptr
中的作用
- 追踪弱引用的数量:每创建一个新的
weak_ptr
,weak_count
会增加;每销毁一个weak_ptr
,weak_count
会减少。 - 延长控制块的生命周期:即使所有的
shared_ptr
都已经销毁(ref_count == 0
),只要weak_count > 0
,控制块仍然会保留,供weak_ptr
检查资源状态。
weak_ptr
的核心功能
weak_ptr
使用weak_count
确保可以安全地调用其lock()
方法来判断资源是否有效。- 当
weak_count == 0
且ref_count == 0
时,控制块会被销毁。
控制块中的状态示例
假设有以下智能指针的状态变化:
- 创建
shared_ptr
ref_count = 1
weak_count = 0
(控制块被创建,但没有弱引用)
- 创建一个
weak_ptr
ref_count = 1
weak_count = 1
(weak_ptr
对控制块有弱引用)
- 复制
shared_ptr
ref_count = 2
weak_count = 1
- 销毁所有
shared_ptr
ref_count = 0
(资源被销毁)weak_count = 1
(控制块依然存在,供weak_ptr
查询)
- 销毁所有
weak_ptr
ref_count = 0
weak_count = 0
(控制块被销毁)
代码演示
以下代码展示了 weak_count
的作用和生命周期管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(10); // 创建 shared_ptr
std::weak_ptr<int> wp = sp; // 创建 weak_ptr
std::cout << "Initial state:\n";
std::cout << "use_count: " << sp.use_count() << "\n"; // ref_count = 1
std::cout << "weak_ptr expired: " << wp.expired() << "\n"; // 未过期
sp.reset(); // 销毁 shared_ptr
std::cout << "\nAfter shared_ptr reset:\n";
std::cout << "weak_ptr expired: " << wp.expired() << "\n"; // 已过期
if (auto locked = wp.lock()) { // 尝试通过 weak_ptr 获取 shared_ptr
std::cout << "Locked value: " << *locked << "\n";
} else {
std::cout << "Resource has been destroyed.\n";
}
return 0;
}
输出结果:
1
2
3
4
5
6
7
Initial state:
use_count: 1
weak_ptr expired: 0
After shared_ptr reset:
weak_ptr expired: 1
Resource has been destroyed.
总结
- 在
shared_ptr
中,weak_count
确保控制块只有在所有shared_ptr
和weak_ptr
都销毁后才会释放。 - 在
weak_ptr
中,weak_count
确保即使资源已经销毁,控制块仍然可以被使用来判断资源的状态。 weak_count
和ref_count
共同作用,实现了高效的引用管理和资源回收,避免了循环引用等问题。
智能指针,里面的计数器何时会改变
在智能指针(std::shared_ptr
和 std::weak_ptr
)中,计数器是用来管理资源的生命周期的,具体包括 引用计数器(ref_count
) 和 弱引用计数器(weak_count
)。这些计数器会在以下情况下发生变化:
1. ref_count
(引用计数器)变化时机
ref_count
用于跟踪当前资源被多少个 shared_ptr
管理。其值会在以下情况下变化:
增加时
创建新的
shared_ptr
: 当使用已有的shared_ptr
复制出另一个shared_ptr
,或通过std::make_shared
创建新的shared_ptr
时,ref_count
会增加。1 2
std::shared_ptr<int> sp1 = std::make_shared<int>(42); // ref_count = 1 std::shared_ptr<int> sp2 = sp1; // ref_count = 2
通过
weak_ptr
锁定资源: 当调用weak_ptr::lock()
方法返回一个新的shared_ptr
时,若资源未被销毁,ref_count
会增加。1 2 3 4
std::weak_ptr<int> wp = sp1; if (auto sp3 = wp.lock()) { // 资源有效,ref_count = 3 // 使用 sp3 }
减少时
销毁
shared_ptr
: 当一个shared_ptr
超出作用域或通过reset()
方法被显式销毁时,ref_count
会减少。1
sp2.reset(); // ref_count = 2 -> 1
赋值新的资源: 当一个
shared_ptr
被赋值为另一个资源时,旧资源的引用计数减少,新资源的引用计数增加。1 2
std::shared_ptr<int> sp3 = sp1; // sp1 和 sp3 共享资源,ref_count = 2 sp3 = std::make_shared<int>(99); // 原资源 ref_count = 1,新资源 ref_count = 1
归零时
- 释放资源: 当
ref_count
减少到 0 时,shared_ptr
会销毁所管理的资源(通常通过控制块中的自定义删除器调用delete
)。
2. weak_count
(弱引用计数器)变化时机
weak_count
用于跟踪当前控制块被多少个 weak_ptr
使用。其值会在以下情况下变化:
增加时
创建新的
weak_ptr
: 每当从一个shared_ptr
或另一个weak_ptr
创建新的weak_ptr
时,weak_count
会增加。1 2 3
std::shared_ptr<int> sp = std::make_shared<int>(42); // ref_count = 1, weak_count = 0 std::weak_ptr<int> wp1 = sp; // weak_count = 1 std::weak_ptr<int> wp2 = wp1; // weak_count = 2
减少时
销毁
weak_ptr
: 当一个weak_ptr
超出作用域或显式调用reset()
方法时,weak_count
会减少。1
wp1.reset(); // weak_count = 2 -> 1
归零时
销毁控制块: 当
weak_count
和ref_count
都归零时,控制块会被销毁。1 2
sp.reset(); // ref_count = 0 -> 资源销毁 wp2.reset(); // weak_count = 0 -> 控制块销毁
3. 综合计数器变化流程
以下是一个完整的计数器变化示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp1 = std::make_shared<int>(42); // ref_count = 1, weak_count = 0
std::weak_ptr<int> wp1 = sp1; // ref_count = 1, weak_count = 1
{
std::shared_ptr<int> sp2 = sp1; // ref_count = 2, weak_count = 1
std::weak_ptr<int> wp2 = sp2; // ref_count = 2, weak_count = 2
} // sp2 和 wp2 超出作用域 -> ref_count = 1, weak_count = 1
sp1.reset(); // ref_count = 0 -> 资源销毁,weak_count = 1
if (wp1.expired()) { // 检查资源是否被销毁
std::cout << "Resource has been destroyed.\n";
}
return 0;
}
输出
1
Resource has been destroyed.
总结:计数器变化规则
ref_count
计数规则:- 每当一个
shared_ptr
被创建、复制或通过weak_ptr::lock
返回时增加。 - 每当一个
shared_ptr
被销毁或重新赋值时减少。 - 当
ref_count == 0
时,资源被销毁。
- 每当一个
weak_count
计数规则:- 每当一个新的
weak_ptr
被创建时增加。 - 每当一个
weak_ptr
被销毁或reset
时减少。 - 当
weak_count == 0
且ref_count == 0
时,控制块被销毁。
- 每当一个新的
通过这种机制,智能指针可以高效、安全地管理动态资源,避免内存泄漏和悬挂指针问题,同时解决了循环引用的问题。
智能指针和管理的对象分别在哪个区
在 C++ 中,智能指针和它所管理的对象通常位于不同的内存区域,这取决于它们的具体类型和分配方式。以下是对智能指针和管理对象所在内存区域的分析:
1. 智能指针本身的存储位置
栈区(Stack): 智能指针(如
std::unique_ptr
、std::shared_ptr
)本质上是一个普通的对象,通常存储在栈上(如果它是局部变量)。1 2 3
void func() { std::shared_ptr<int> sp = std::make_shared<int>(42); // sp 本身存储在栈上 } // sp 离开作用域后自动销毁
堆区(Heap): 如果智能指针是通过动态分配创建的(如
new
或std::make_unique
),它会存储在堆上。1 2
auto sp = std::make_shared<std::shared_ptr<int>>(std::make_shared<int>(42)); // 内部的 shared_ptr 是堆分配的
全局或静态区: 如果智能指针是全局变量或静态变量,则它存储在全局区。
1 2
static std::shared_ptr<int> sp = std::make_shared<int>(42); // sp 存储在静态区
2. 智能指针管理的对象的存储位置
堆区(Heap): 智能指针管理的资源通常是动态分配的对象,存储在堆区。智能指针通过控制块管理这些资源,并在适当的时机释放它们。
1 2
std::shared_ptr<int> sp = std::make_shared<int>(42); // 被管理的 int 对象存储在堆区
- 对于
std::make_shared
,shared_ptr
的控制块和所管理的对象可能分配在一起。 - 对于
new
创建的对象,它们独立分配在堆区。
- 对于
其他区域(需要特别配置): 虽然通常存储在堆区,但资源也可以是其他内存区域的指针(如栈上的对象或共享内存),但这种用法需要小心,避免非动态分配对象的误释放。
1 2 3 4
int x = 42; std::shared_ptr<int> sp(&x, [](int*) { // 自定义删除器:空操作,避免释放栈上的对象 });
3. 控制块的存储位置
对于 std::shared_ptr
和 std::weak_ptr
,控制块是一个单独分配的结构,用于存储引用计数和资源管理信息:
堆区(Heap):
- 默认情况下,
std::shared_ptr
的控制块存储在堆上。 - 如果使用
std::make_shared
,控制块和对象可能分配在同一内存块中,优化了内存分配和访问效率。
1
std::shared_ptr<int> sp = std::make_shared<int>(42); // 控制块和对象可能共用内存
- 默认情况下,
独立堆分配: 如果使用
new
手动分配资源,控制块和对象会分别分配。1
std::shared_ptr<int> sp(new int(42)); // 控制块和对象分别分配
4. 不同情况下的存储位置总结
对象类型 | 存储区域 |
---|---|
智能指针本身 | 栈区(局部变量),堆区(动态分配),或全局区(静态变量) |
智能指针管理的对象 | 通常在堆区,特殊情况可能在其他区域(需自定义删除器) |
控制块(shared_ptr ) | 通常在堆区,可与对象共用内存(std::make_shared ) |
注意事项
- 资源与智能指针的生命周期绑定:
- 智能指针本身超出作用域会自动释放所管理的资源。
- 不要用智能指针管理非堆分配的对象(如栈对象),除非提供自定义删除器。
- 内存优化:
std::make_shared
优化了内存分配,推荐使用。- 控制块的分配位置由实现决定,但通常与资源分开(除非使用
std::make_shared
)。
总结示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <memory>
int main() {
// 1. 智能指针本身在栈上,资源在堆上
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 2. 手动分配资源,控制块和对象分开存储
std::shared_ptr<int> sp2(new int(99));
// 3. 非动态分配对象,用自定义删除器避免误释放
int x = 100;
std::shared_ptr<int> sp3(&x, [](int*) {
// 空操作,避免释放栈对象
});
std::cout << "sp: " << *sp << ", sp2: " << *sp2 << ", sp3: " << *sp3 << "\n";
return 0;
}
输出:
1
sp: 42, sp2: 99, sp3: 100
面向对象的特性:多态原理
面向对象的特性之一:多态
多态是面向对象编程的核心特性之一,指的是程序中调用方法时可以根据对象的实际类型执行不同的行为。多态使代码更具有扩展性和灵活性。
多态的主要类型
- 编译时多态(静态多态):
- 通过函数重载、运算符重载等在编译阶段实现。
- 决定方法调用在编译期间绑定。
- 例如,C++ 的函数重载和模板。
- 运行时多态(动态多态):
- 通过虚函数(virtual functions)和继承实现。
- 决定方法调用在运行期间绑定。
- 例如,C++ 的虚函数。
多态实现的核心:虚函数表(vtable)
在 C++ 中,动态多态是通过 虚函数表(vtable) 和 虚函数指针(vptr) 实现的。
1. 虚函数的基本原理
- 当一个类包含虚函数时,编译器会为这个类生成一个虚函数表(vtable)。
- 虚函数表是一个指针数组,其中每个元素是指向该类中虚函数实现的函数指针。
- 每个包含虚函数的对象都会有一个 虚函数指针(vptr),它指向该类的虚函数表。
2. 虚函数调用过程
- 当通过基类指针或引用调用虚函数时,编译器通过对象的
vptr
找到该对象对应的vtable
。 - 然后在
vtable
中查找对应的函数指针,并调用实际的函数。
虚函数表的构造过程
类结构
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
class Base {
public:
virtual void show() { std::cout << "Base::show()" << std::endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived::show()" << std::endl; }
};
生成的虚函数表
对于
Base
类,编译器生成一个虚函数表(假设称为Base_vtable
):1 2 3 4 5 6
Base_vtable: +----------------+ | Base::show() | +----------------+ | Base::~Base() | +----------------+
对于
Derived
类,编译器生成一个虚函数表(假设称为Derived_vtable
):1 2 3 4 5 6
Derived_vtable: +----------------+ | Derived::show()| +----------------+ | Base::~Base() | +----------------+
对象结构
每个对象都有一个指向虚函数表的
1
vptr
:
1 2 3 4 5 6 7 8 9
Base object: +--------+ | vptr | --> Base_vtable +--------+ Derived object: +--------+ | vptr | --> Derived_vtable +--------+
调用示例
1
2
3
4
5
6
int main() {
Base* b = new Derived();
b->show(); // 输出: Derived::show()
delete b;
return 0;
}
调用过程
编译器在
1
b->show()
时,生成以下伪代码:
1
b->vptr[0](); // 在虚函数表中查找第一个虚函数
b
是指向Derived
对象的基类指针,因此其vptr
指向Derived_vtable
。在
Derived_vtable
的第一个条目中找到Derived::show()
,因此调用该函数。
多态的优势
- 扩展性:
- 可以通过基类指针调用派生类的方法,而无需修改已有代码。
- 灵活性
- 允许在运行时动态决定调用的函数,支持复杂的运行时行为。
- 可维护性
- 基类提供统一的接口,派生类实现具体行为,降低代码耦合度。
虚函数的性能开销
动态多态的实现需要额外的性能开销:
- 存储开销:
- 每个含虚函数的对象都需要一个
vptr
。 - 每个类需要一个虚函数表。
- 每个含虚函数的对象都需要一个
- 运行时开销:
- 通过
vptr
和vtable
查找函数指针比直接调用普通函数多了一次间接访问。
- 通过
- 缓存效率:
- 使用虚函数可能会导致 CPU 缓存性能降低,因为函数指针的间接调用可能难以预测。
尽管如此,这些开销在现代硬件中通常是可以接受的,换来的灵活性和可扩展性在大多数场景中是值得的。
多态的限制
构造函数中不可调用虚函数:
- 在构造函数中,
vptr
尚未初始化完毕,调用虚函数会执行当前类的实现,而不是派生类的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Base { public: Base() { show(); } // 在构造函数中调用虚函数 virtual void show() { std::cout << "Base::show()" << std::endl; } }; class Derived : public Base { public: void show() override { std::cout << "Derived::show()" << std::endl; } }; int main() { Derived d; // 输出: Base::show() return 0; }
- 在构造函数中,
必须使用指针或引用:
- 只有通过基类指针或引用调用虚函数时,才能实现动态多态。
不适用于静态类型绑定:
- 如果函数不是虚函数,则不会触发动态多态。
总结
- 多态的核心是基类的接口和派生类的实现相分离,使得调用基类方法时可以根据对象的实际类型选择合适的实现。
- 静态多态通过编译时函数绑定实现(如模板、函数重载)。
- 动态多态通过虚函数、虚函数表(vtable)和虚函数指针(vptr)实现,允许在运行时决定函数绑定。
- 动态多态虽然有一定的性能开销,但它提供了强大的灵活性和代码复用能力,是面向对象编程的重要特性。
介绍一下虚函数,虚函数怎么实现的
虚函数简介
虚函数是面向对象编程中多态性(Polymorphism)的核心,用于实现运行时动态绑定。在 C++ 中,虚函数使用 virtual
关键字声明,允许派生类重写基类的函数行为。当通过基类指针或引用调用虚函数时,会根据对象的实际类型动态决定调用哪个函数。
虚函数的实现机制
在 C++ 中,虚函数的实现依赖于以下两部分:
- 虚函数表(vtable):
- 每个含虚函数的类对应一个虚函数表,存储该类所有虚函数的函数指针。
- 虚函数指针(vptr):
- 每个含虚函数的对象都有一个隐式的虚函数指针,指向所属类的虚函数表。
通过这两个机制,C++ 实现了动态多态。
虚函数的实现细节
1. 虚函数表(vtable)
- 结构:
- 虚函数表是一个函数指针数组,每个条目指向一个虚函数的具体实现。
- 基类的虚函数表包含基类的虚函数实现的指针。
- 派生类的虚函数表会覆盖继承自基类的虚函数指针,并添加派生类特有的虚函数。
- 条目指针的选择规则:
- 如果派生类重写了基类的虚函数,则派生类的虚函数表条目指向派生类的实现。
- 如果派生类未重写基类的虚函数,则虚函数表条目保留指向基类实现的指针。
2. 虚函数指针(vptr)
- vptr 的作用:
- 每个对象都有一个
vptr
,指向该对象所属类的虚函数表。 - 通过
vptr
,对象在运行时能够找到正确的虚函数表。
- 每个对象都有一个
- vptr 的设置:
- 当对象构造时,编译器会自动初始化
vptr
指向正确的虚函数表。 - 如果对象属于派生类,
vptr
会在派生类构造函数中被设置为指向派生类的虚函数表。
- 当对象构造时,编译器会自动初始化
虚函数的调用过程
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base::show" << endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override { cout << "Derived::show" << endl; }
};
int main() {
Base* b = new Derived(); // 基类指针指向派生类对象
b->show(); // 调用派生类的 show()
delete b;
return 0;
}
调用过程分析
当
b->show()
被调用时,编译器生成类似以下伪代码:1
b->vptr[0](); // 从 b 的虚函数表中查找第一个虚函数
b
是Base*
类型,但它指向的是Derived
对象,因此:
b->vptr
指向的是Derived
类的虚函数表。- 在
Derived_vtable
中,show
条目指向Derived::show
。
最终,程序调用的是
Derived::show()
。
虚函数的关键实现点
1. 编译器生成虚函数表
- 虚函数表的生成规则:
- 如果类中包含虚函数,编译器会为该类生成一个虚函数表。
- 如果派生类重写了基类的虚函数,则派生类的虚函数表会替换虚函数表中对应的条目。
2. vptr 的设置
- 构造函数的作用:
- 在构造函数中,
vptr
被设置为指向当前对象所属类的虚函数表。 - 如果对象属于派生类,
vptr
会先指向基类的虚函数表(基类构造函数阶段),然后在派生类构造函数中重定向为派生类的虚函数表。
- 在构造函数中,
虚函数表示例
代码结构
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void func1() {}
virtual void func2() {}
virtual ~Base() {}
};
class Derived : public Base {
public:
void func1() override {}
void func3() {}
};
虚函数表
Base
类的虚函数表:1 2 3 4 5 6 7 8
Base_vtable: +-------------------+ | Base::func1() | +-------------------+ | Base::func2() | +-------------------+ | Base::~Base() | +-------------------+
Derived
类的虚函数表:1 2 3 4 5 6 7 8
Derived_vtable: +-------------------+ | Derived::func1() | // 覆盖 Base::func1() +-------------------+ | Base::func2() | // 继承自 Base +-------------------+ | Base::~Base() | // 继承自 Base +-------------------+
虚函数的性能开销
虚函数的动态绑定提供了强大的灵活性,但也带来了性能开销:
- 间接调用开销:
- 虚函数调用需要通过
vptr
和vtable
查找目标函数地址,比普通函数调用多了一次间接访问。
- 虚函数调用需要通过
- 额外存储开销:
- 每个类需要一个虚函数表。
- 每个对象需要一个
vptr
指针。
- 内存布局复杂性:
- 虚函数表和
vptr
的存在使得对象的内存布局变得更加复杂。
- 虚函数表和
虚函数的使用注意事项
构造函数中不要调用虚函数:
- 在构造函数中调用虚函数时,
vptr
尚未完成初始化,因此调用的是当前构造函数所属类的虚函数实现,而不是派生类的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Base { public: Base() { show(); } // 调用虚函数 virtual void show() { cout << "Base::show" << endl; } }; class Derived : public Base { public: void show() override { cout << "Derived::show" << endl; } }; int main() { Derived d; // 输出: Base::show return 0; }
- 在构造函数中调用虚函数时,
虚函数的调用需要通过指针或引用:
- 如果通过对象直接调用函数,即使是虚函数,也会进行静态绑定。
1 2
Derived d; d.show(); // 静态绑定,调用 Derived::show()
总结
- 虚函数的作用:
- 通过动态绑定实现运行时多态,使程序更灵活和可扩展。
- 虚函数的实现机制:
- 依赖虚函数表(vtable)和虚函数指针(vptr)。
vptr
指向对象所属类的虚函数表,动态决定调用哪个函数。
- 虚函数的限制:
- 构造函数中调用虚函数会导致行为不符合预期。
- 静态绑定和动态绑定的选择取决于调用方式。
虚函数是面向对象编程的核心机制之一,其实现虽然带来了额外的开销,但换来了代码的灵活性和可维护性。