文章

c++高频总结一

智能指针实现原理

智能指针是现代 C++ 中用于管理动态分配资源(如堆内存)的重要工具,其核心原理在于RAII(Resource Acquisition Is Initialization)。以下是智能指针实现的详细原理,包括常见类型的智能指针(std::unique_ptrstd::shared_ptrstd::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_ptrweak_ptr 的交互中起到了关键作用。

以下是 weak_countshared_ptrweak_ptr 中的具体作用:


1. weak_countshared_ptr 中的作用

  • 辅助控制块的销毁shared_ptr 管理的资源会在引用计数 (ref_count) 归零时销毁,但控制块本身只有在 weak_count 也为零时才会销毁。
    • ref_count 变为 0 时,资源(如堆内存)被释放。
    • ref_countweak_count 都变为 0 时,控制块本身被销毁。

为什么需要 weak_count

weak_ptr 不会增加资源的引用计数 (ref_count),但它需要依赖控制块来判断资源是否有效。因此,即使资源被销毁,控制块需要保留,直到所有的 weak_ptr 都被销毁为止。


2. weak_countweak_ptr 中的作用

  • 追踪弱引用的数量:每创建一个新的 weak_ptrweak_count 会增加;每销毁一个 weak_ptrweak_count 会减少。
  • 延长控制块的生命周期:即使所有的 shared_ptr 都已经销毁(ref_count == 0),只要 weak_count > 0,控制块仍然会保留,供 weak_ptr 检查资源状态。

weak_ptr 的核心功能

  • weak_ptr 使用 weak_count 确保可以安全地调用其 lock() 方法来判断资源是否有效。
  • weak_count == 0ref_count == 0 时,控制块会被销毁。

控制块中的状态示例

假设有以下智能指针的状态变化:

  1. 创建 shared_ptr
    • ref_count = 1
    • weak_count = 0(控制块被创建,但没有弱引用)
  2. 创建一个 weak_ptr
    • ref_count = 1
    • weak_count = 1weak_ptr 对控制块有弱引用)
  3. 复制 shared_ptr
    • ref_count = 2
    • weak_count = 1
  4. 销毁所有 shared_ptr
    • ref_count = 0(资源被销毁)
    • weak_count = 1(控制块依然存在,供 weak_ptr 查询)
  5. 销毁所有 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_ptrweak_ptr 都销毁后才会释放。
  • weak_ptr 中,weak_count 确保即使资源已经销毁,控制块仍然可以被使用来判断资源的状态。
  • weak_countref_count 共同作用,实现了高效的引用管理和资源回收,避免了循环引用等问题。

智能指针,里面的计数器何时会改变

在智能指针(std::shared_ptrstd::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_countref_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.

总结:计数器变化规则

  1. ref_count 计数规则
    • 每当一个 shared_ptr 被创建、复制或通过 weak_ptr::lock 返回时增加。
    • 每当一个 shared_ptr 被销毁或重新赋值时减少。
    • ref_count == 0 时,资源被销毁。
  2. weak_count 计数规则
    • 每当一个新的 weak_ptr 被创建时增加。
    • 每当一个 weak_ptr 被销毁或 reset 时减少。
    • weak_count == 0ref_count == 0 时,控制块被销毁。

通过这种机制,智能指针可以高效、安全地管理动态资源,避免内存泄漏和悬挂指针问题,同时解决了循环引用的问题。

智能指针和管理的对象分别在哪个区

在 C++ 中,智能指针和它所管理的对象通常位于不同的内存区域,这取决于它们的具体类型和分配方式。以下是对智能指针和管理对象所在内存区域的分析:


1. 智能指针本身的存储位置

  • 栈区(Stack): 智能指针(如 std::unique_ptrstd::shared_ptr)本质上是一个普通的对象,通常存储在栈上(如果它是局部变量)。

    1
    2
    3
    
    void func() {
        std::shared_ptr<int> sp = std::make_shared<int>(42); // sp 本身存储在栈上
    } // sp 离开作用域后自动销毁
    
  • 堆区(Heap): 如果智能指针是通过动态分配创建的(如 newstd::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_sharedshared_ptr 的控制块和所管理的对象可能分配在一起。
    • 对于 new 创建的对象,它们独立分配在堆区。
  • 其他区域(需要特别配置): 虽然通常存储在堆区,但资源也可以是其他内存区域的指针(如栈上的对象或共享内存),但这种用法需要小心,避免非动态分配对象的误释放。

    1
    2
    3
    4
    
    int x = 42;
    std::shared_ptr<int> sp(&x, [](int*) {
        // 自定义删除器:空操作,避免释放栈上的对象
    });
    

3. 控制块的存储位置

对于 std::shared_ptrstd::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

注意事项

  1. 资源与智能指针的生命周期绑定
    • 智能指针本身超出作用域会自动释放所管理的资源。
    • 不要用智能指针管理非堆分配的对象(如栈对象),除非提供自定义删除器。
  2. 内存优化
    • 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

面向对象的特性:多态原理

面向对象的特性之一:多态

多态是面向对象编程的核心特性之一,指的是程序中调用方法时可以根据对象的实际类型执行不同的行为。多态使代码更具有扩展性和灵活性。

多态的主要类型

  1. 编译时多态(静态多态)
    • 通过函数重载、运算符重载等在编译阶段实现。
    • 决定方法调用在编译期间绑定。
    • 例如,C++ 的函数重载和模板。
  2. 运行时多态(动态多态)
    • 通过虚函数(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; }
};

生成的虚函数表

  1. 对于 Base 类,编译器生成一个虚函数表(假设称为 Base_vtable):

    1
    2
    3
    4
    5
    6
    
    Base_vtable:
    +----------------+
    | Base::show()   |
    +----------------+
    | Base::~Base()  |
    +----------------+
    
  2. 对于 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. 编译器在

    1
    
    b->show()
    

    时,生成以下伪代码:

    1
    
    b->vptr[0](); // 在虚函数表中查找第一个虚函数
    
  2. b 是指向 Derived 对象的基类指针,因此其 vptr 指向 Derived_vtable

  3. Derived_vtable 的第一个条目中找到 Derived::show(),因此调用该函数。


多态的优势

  1. 扩展性:
    • 可以通过基类指针调用派生类的方法,而无需修改已有代码。
  2. 灵活性
    • 允许在运行时动态决定调用的函数,支持复杂的运行时行为。
  3. 可维护性
    • 基类提供统一的接口,派生类实现具体行为,降低代码耦合度。

虚函数的性能开销

动态多态的实现需要额外的性能开销:

  1. 存储开销:
    • 每个含虚函数的对象都需要一个 vptr
    • 每个类需要一个虚函数表。
  2. 运行时开销:
    • 通过 vptrvtable 查找函数指针比直接调用普通函数多了一次间接访问。
  3. 缓存效率:
    • 使用虚函数可能会导致 CPU 缓存性能降低,因为函数指针的间接调用可能难以预测。

尽管如此,这些开销在现代硬件中通常是可以接受的,换来的灵活性和可扩展性在大多数场景中是值得的。


多态的限制

  1. 构造函数中不可调用虚函数

    • 在构造函数中,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;
    }
    
  2. 必须使用指针或引用

    • 只有通过基类指针或引用调用虚函数时,才能实现动态多态。
  3. 不适用于静态类型绑定

    • 如果函数不是虚函数,则不会触发动态多态。

总结

  • 多态的核心是基类的接口和派生类的实现相分离,使得调用基类方法时可以根据对象的实际类型选择合适的实现。
  • 静态多态通过编译时函数绑定实现(如模板、函数重载)。
  • 动态多态通过虚函数、虚函数表(vtable)和虚函数指针(vptr)实现,允许在运行时决定函数绑定。
  • 动态多态虽然有一定的性能开销,但它提供了强大的灵活性和代码复用能力,是面向对象编程的重要特性。

介绍一下虚函数,虚函数怎么实现的

虚函数简介

虚函数是面向对象编程中多态性(Polymorphism)的核心,用于实现运行时动态绑定。在 C++ 中,虚函数使用 virtual 关键字声明,允许派生类重写基类的函数行为。当通过基类指针或引用调用虚函数时,会根据对象的实际类型动态决定调用哪个函数。


虚函数的实现机制

在 C++ 中,虚函数的实现依赖于以下两部分:

  1. 虚函数表(vtable):
    • 每个含虚函数的类对应一个虚函数表,存储该类所有虚函数的函数指针。
  2. 虚函数指针(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;
}

调用过程分析

  1. b->show()被调用时,编译器生成类似以下伪代码:

    1
    
    b->vptr[0](); // 从 b 的虚函数表中查找第一个虚函数
    
  2. bBase*类型,但它指向的是 Derived

    对象,因此:

    • b->vptr 指向的是 Derived 类的虚函数表。
    • Derived_vtable 中,show 条目指向 Derived::show
  3. 最终,程序调用的是 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() {}
};

虚函数表

  1. Base 类的虚函数表

    1
    2
    3
    4
    5
    6
    7
    8
    
    Base_vtable:
    +-------------------+
    | Base::func1()     |
    +-------------------+
    | Base::func2()     |
    +-------------------+
    | Base::~Base()     |
    +-------------------+
    
  2. Derived 类的虚函数表

    1
    2
    3
    4
    5
    6
    7
    8
    
    Derived_vtable:
    +-------------------+
    | Derived::func1()  | // 覆盖 Base::func1()
    +-------------------+
    | Base::func2()     | // 继承自 Base
    +-------------------+
    | Base::~Base()     | // 继承自 Base
    +-------------------+
    

虚函数的性能开销

虚函数的动态绑定提供了强大的灵活性,但也带来了性能开销:

  1. 间接调用开销:
    • 虚函数调用需要通过 vptrvtable 查找目标函数地址,比普通函数调用多了一次间接访问。
  2. 额外存储开销:
    • 每个类需要一个虚函数表。
    • 每个对象需要一个 vptr 指针。
  3. 内存布局复杂性:
    • 虚函数表和 vptr 的存在使得对象的内存布局变得更加复杂。

虚函数的使用注意事项

  1. 构造函数中不要调用虚函数

    • 在构造函数中调用虚函数时,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;
    }
    
  2. 虚函数的调用需要通过指针或引用

    • 如果通过对象直接调用函数,即使是虚函数,也会进行静态绑定。
    1
    2
    
    Derived d;
    d.show(); // 静态绑定,调用 Derived::show()
    

总结

  • 虚函数的作用
    • 通过动态绑定实现运行时多态,使程序更灵活和可扩展。
  • 虚函数的实现机制
    • 依赖虚函数表(vtable)和虚函数指针(vptr)。
    • vptr 指向对象所属类的虚函数表,动态决定调用哪个函数。
  • 虚函数的限制
    • 构造函数中调用虚函数会导致行为不符合预期。
    • 静态绑定和动态绑定的选择取决于调用方式。

虚函数是面向对象编程的核心机制之一,其实现虽然带来了额外的开销,但换来了代码的灵活性和可维护性。

本文由作者按照 CC BY 4.0 进行授权