本文最后更新于 2025-01-17,学习久了要注意休息哟

第一章 C++11 基础特性

1.1 空指针类型 nullptr

在程序开发中,为了防止野指针的出现,我们经常会将指针初始化为 NULL。这种方式在一定程度上可以避免野指针的使用。

但是,在底层实现中,NULL 实际上是一个数字 0,这意味着在使用 NULL 时,它和整数 0 没有本质上的区别。

因此,当我们进行函数重载,涉及指针类型和整数类型时,NULL 可能会引发歧义,导致编译器无法正确选择函数重载。例如:

#include <iostream>

void func(int x) {
    std::cout << "Called func(int)" << std::endl;
}

void func(int* p) {
    std::cout << "Called func(int*)" << std::endl;
}

int main() {
    func(0);   // 调用 func(int),没有歧义
    func(NULL); // 由于 NULL 是 0,这里仍然调用 func(int),而非 func(int*)
    return 0;
}

所以,在C++ 11 中引入了一个新的特征,nullptr , 可以解决这种问题

#include <iostream>

void func(int x) {
    std::cout << "Called func(int)" << std::endl;
}

void func(int* p) {
    if (p == nullptr) {
        std::cout << "Called func(int*) with nullptr" << std::endl;
    } else {
        std::cout << "Called func(int*)" << std::endl;
    }
}

int main() {
    func(nullptr); // 正确调用 func(int*),避免了歧义
    return 0;
}

NULL 在底层是 0,容易引发类型歧义。

nullptr 是 C++11 引入的新特性,用于表示空指针,避免了 NULL0 在重载中的冲突。

使用 nullptr 更加安全、明确,建议在现代 C++ 中优先使用 nullptr 表示空指针。

1.2 常量表达式 constexpr

constexpr 是 C++11 引入的关键字,==用于声明常量表达式==(Constant Expression)。它的主要作用是在编译时计算表达式的值,从而提升程序性能和代码的安全性。

使用 constexpr 可以确保变量或函数的结果在编译阶段确定,而不是在运行时计算。

1.2.1 常量表达式

在定义常量时,constconstexpr 都用于定义不可修改的值,且都可以在编译阶段计算出结果。然而,constexpr 更加严格,因为它确保值在编译时就能确定,而 const 则允许在运行时初始化常量。

普通类型

在普通类型中,constconstexpr 的作用看起来相似,但 constexpr 的值必须在编译期计算,而 const 可能在运行时计算。

const int i = 80;        // i 是一个常量,可以在编译时确定
const int j = i + 10;    // j 也是常量,编译时确定值为 90

constexpr int k = 80;    // k 是一个常量表达式,编译时确定
constexpr int l = k + 10; // l 是常量表达式,编译时确定值为 90

自定义类型

在自定义类型中,constexpr 可用于指定构造函数和成员函数,使得对象可以在编译时构造和计算。这样可以确保对象的值在编译阶段就能确定,而不仅仅是在运行时。

#include <iostream>

class Point {
public:
    constexpr Point(int x, int y) : x_(x), y_(y) {} // constexpr 构造函数
    constexpr int getX() const { return x_; }       // constexpr 成员函数
    constexpr int getY() const { return y_; }       // constexpr 成员函数

private:
    int x_;
    int y_;
};

int main() {
    constexpr Point p(3, 4);  // p 是一个常量表达式对象,编译时构造
    constexpr int x = p.getX(); // x 编译时确定为 3
    constexpr int y = p.getY(); // y 编译时确定为 4

    std::cout << "Point x: " << x << ", y: " << y << std::endl;
    return 0;
}

1.2.2 常量表达式函数

常量表达式函数 (constexpr 函数) 是在编译期可以计算结果的函数,通常用于实现编译时常量计算。在 constexpr 函数中,要求函数体内容符合特定的约束条件,以确保其在编译时可计算。

1、函数

1、函数必须要有返回值,并且return 返回的表达式必须是常量表达式。

#include <iostream>

// 定义一个 constexpr 函数计算整数的平方
constexpr int square(int x) {
    return x * x; // 返回的表达式必须是常量表达式
}

int main() {
    constexpr int result = square(4); // 编译时计算 result 为 16
    std::cout << "Square of 4 is: " << result << std::endl;
    return 0;
}

C++17之后的标准是运行返回值类型不是常量表达式。

2、函数在使用之前,必须有对应的定义语句。

#include <iostream>

// 先定义 constexpr 函数
constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    constexpr int result = add(3, 5); // 在调用前,add 函数已定义
    std::cout << "Sum of 3 and 5 is: " << result << std::endl;
    return 0;
}

3、整个函数的函数体中,不能出现非常量表达式之外的语句。

(using 指令、typedef 语句以及 static_assert 断言、return语句除外)。

#include <iostream>
#include <string>

using namespace std;

// 错误示例:C++11/14 中 constexpr 函数中不能包含 for 循环
constexpr int func1() {
    constexpr int a = 100;
    constexpr int b = 10;
    // 在 C++11 和 C++14 中,for 循环在 constexpr 函数中是不允许的
    // for (int i = 0; i < b; ++i) {
    //     cout << "i: " << i << endl;
    // }
    return a + b;
}

// 正确示例:仅包含允许的 constexpr 表达式
constexpr int func2() {
    using mytype = int;            // 允许使用 using 指令
    constexpr mytype a = 100;      // 定义 constexpr 常量
    constexpr mytype b = 10;
    constexpr mytype c = a * b;
    return c - (a + b);            // 返回计算结果
}

int main() {
    constexpr int a = func1();     // 编译时计算 func1 的结果
    cout << "Result of func1: " << a << endl;
    
    constexpr int b = func2();     // 编译时计算 func2 的结果
    cout << "Result of func2: " << b << endl;

    return 0;
}

常量表达式

2、修饰模板函数

在模板函数中,constexpr 的使用会受到模板参数类型的影响,因为模板类型的不确定性意味着,实例化后的模板函数是否能满足常量表达式函数的要求也不确定。因此,constexpr 修饰的模板函数在使用时必须保证实际类型和操作符合常量表达式的要求,才能在编译时进行计算。

#include <iostream>

using namespace std;

// 定义 constexpr 模板函数,计算整数的次方
template <typename T>
constexpr T power(T base, int exponent) {
    T result = 1;
    for (int i = 0; i < exponent; ++i) {
        result *= base;
    }
    return result;
}

int main() {
    constexpr int result = power(2, 3); // 编译期计算 2 的 3 次方
    std::cout << "2^3 = " << result << std::endl; // 输出 8
    
    // 这里使用一个非 constexpr 类型的调用
    double base = 2.0;
    std::cout << "2.0^3 = " << power(base, 3) << std::endl; // 运行时计算
    return 0;
}

3、修饰构造函数

constexpr 构造函数中,函数体必须为空,并且必须使用初始化列表为所有成员赋值。这样做的目的是确保对象的初始化在编译期完成,符合常量表达式的要求。

#include <iostream>

class Point {
public:
    // constexpr 构造函数:必须使用初始化列表并且函数体为空
    constexpr Point(int x, int y) : x_(x), y_(y) {}

    constexpr int getX() const { return x_; }
    constexpr int getY() const { return y_; }

private:
    int x_;
    int y_;
};

int main() {
    constexpr Point p(10, 20); // 在编译期构造 Point 对象 p
    std::cout << "Point x: " << p.getX() << ", y: " << p.getY() << std::endl; // 输出 x: 10, y: 20
    return 0;
}

1.3 原始字面量 <有用>

原始字面量(Raw Literals)是一种在 C++ 中表示字符串的方式,主要用于避免在字符串中频繁使用转义字符。通过原始字面量,能够直接在字符串中包含特殊字符(如 \n\t 等),无需添加额外的转义符,提升代码的可读性。

主要作用

  1. 提高可读性:减少频繁的转义字符,使代码更加清晰易读。
  2. 便于处理长文本:特别是在处理多行字符串时,原始字面量可以保持文本的原有格式,减少转义字符的使用。

格式

R"xxx(原始字面量)xxx"
R       // 标识为原始字面量
xxx     // 任意字母组合,必须成对出现并保持相同
()      // 括号内为字面量内容

示例程序

#include <iostream>
#include <string>

using namespace std;

int main() {
    // 使用原始字面量定义文件路径,避免转义字符
    string path_1 = R"(E:\【04工作文件】华清远见\【第3阶段】_01_C++QT开发)";
    
    // 使用自定义分隔符 `table`,避免与路径内容冲突
    string path_2 = R"table(E:\【04工作文件】华清远见\【第3阶段】_01_C++QT开发)table";
    
    // 使用原始字面量表示多行文本,保持文本的格式
    string text = R"table(
    张三 男 18岁;
    李四 男 20岁;
    )table";

    return 0;
}

第二章 常用特性

2.1 auto 类型推导 <有用>

C++11 引入了 auto 关键字,可以让编译器根据初始化表达式的类型来自动推导变量的类型。auto 的引入简化了代码,尤其在类型冗长的声明中显得更为便捷和清晰。

主要作用

简化代码:通过自动推导,减少了复杂类型声明的繁琐,提升了代码的可读性。

减少错误:减少手动声明类型的错误,编译器根据初始值自动推导,能有效避免类型不匹配。

适应变化:当变量的类型发生变化时,无需修改变量的声明,保持代码的灵活性和可维护性。

基本使用方法

#include <iostream>
#include <vector>

int main() {
    auto a = 5;                 // 自动推导为 int
    auto b = 3.14;              // 自动推导为 double
    auto c = "Hello, World!";   // 自动推导为 const char*

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
    std::cout << "c = " << c << std::endl;

    return 0;
}

2.1.1 使用限制

1、不能作为函数参数使用

void func(auto x) { // 错误:不能将 auto 用于函数参数
    // ...
}

2、不能用于类的非静态成员变量的初始化

class MyClass {
    auto value = 10; // 错误:非静态成员变量不能使用 auto
};

3、不能使用auto关键字定义数组

auto arr[5] = {1, 2, 3, 4, 5}; // 错误:不能使用 auto 定义数组

4、无法使用auto推导出模板参数

template <typename T>
struct Test{}

int func()
{
    Test<double> t;
    Test<auto> t1 = t;           // error, 无法推导出模板类型
    return 0;
}

2.1.2 常用示例

1、容器遍历

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // 使用 auto 简化迭代器类型的声明
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用范围 for 循环,进一步简化
    for (auto value : vec) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

2.2 Lambda 表达式<贼有用>

Lambda 表达式是 C++11 引入的一种简洁的函数表示方式,用于在代码中定义匿名函数,适合于短小的函数逻辑,尤其是在需要将函数作为参数传递时。它可以直接在函数内部创建并使用,不需要额外定义函数。

[capture](parameters) opt  -> return_type { body; }
    capture		// 捕获列表
    parameters	// 参数列表
    opt			// 选项 
            // mutable
            // exception
	return_type	// 返回值类型 会自动推导
    body		// 函数体

2.2.1 语法

1、捕获列表

捕获列表用于控制 Lambda 表达式对外部变量的访问方式,可以按值或按引用捕获外部变量。

  • []:不捕获任何变量。
  • [&]:按引用捕获外部作用域中的所有变量,允许在函数体内修改这些变量。
  • [=]:按值捕获外部作用域中的所有变量,作为副本传入,副本在函数体内部是只读的。
  • [=, &val]:按值捕获所有变量,按引用捕获指定变量 val
  • [val]:按值捕获指定变量 val,不捕获其他变量。
  • [&val]:按引用捕获指定变量 val,不捕获其他变量。
  • [this]:捕获当前类的 this 指针,允许访问类的成员变量和成员函数。

示例程序

#include <iostream>
#include <functional>
using namespace std;

class Test {
public:
    void output(int x, int y) {
        // auto x1 = [] { return m_number; };         // error: 未捕获 this,无法访问 m_number
        auto x2 = [=] { return m_number + x + y; };   // ok: 按值捕获所有变量
        auto x3 = [&] { return m_number + x + y; };   // ok: 按引用捕获所有变量
        auto x4 = [this] { return m_number; };        // ok: 捕获 this 指针访问成员变量
        // auto x5 = [this] { return m_number + x + y; }; // error: 未捕获 x 和 y
        auto x6 = [this, x, y] { return m_number + x + y; }; // ok: 捕获 this,并按值捕获 x 和 y
        auto x7 = [this] { return m_number++; };      // ok: 修改成员变量
    }
    int m_number = 100;
};

示例程序

int main() {
    int a = 10, b = 20;

    // auto f1 = [] { return a; };                     // error: 未捕获 a
    auto f2 = [&] { return a++; };                   // ok: 按引用捕获 a,允许修改
    auto f3 = [=] { return a; };                     // ok: 按值捕获 a
    // auto f4 = [=] { return a++; };                  // error: 按值捕获的变量为只读
    // auto f5 = [a] { return a + b; };                // error: 未捕获 b
    auto f6 = [a, &b] { return a + (b++); };         // ok: 按值捕获 a,按引用捕获 b
    auto f7 = [=, &b] { return a + (b++); };         // ok: 按值捕获 a,按引用捕获 b

    return 0;
}

2、返回值

Lambda 表达式通常无需显式指定返回值类型,编译器会根据 return 语句自动推导。如果返回类型不易推导或存在多个 return 分支,建议使用 -> return_type 明确指定返回类型。

示例代码

// 显式指定返回类型
auto f = [](int a) -> int {
    return a + 10;
};

// 自动推导返回值类型
auto f2 = [](int i) {
    return i;
};

// 错误示例,无法推导返回值类型
// auto f1 = []() {
//     return {1, 2}; // 错误:基于列表初始化的返回值,编译器无法推导
// };

2.2.3 声明和调用

Lambda 表达式在声明时即定义了匿名函数,既可以直接在定义后调用,也可以赋值给变量后再调用。Lambda 表达式的声明和调用方式灵活,适合在多种场景中使用。

1. 直接声明并调用

Lambda 表达式可以在声明后立即调用,非常适合一些一次性使用的简短逻辑。

#include <iostream>



int main() {
    // 直接声明并调用 Lambda 表达式
    auto result = [](int a, int b) -> int {
        return a + b;
    }(3, 4); // 直接调用,传入参数 3 和 4
    std::cout << "Result: " << result << std::endl; // 输出 Result: 7

    return 0;
}

2. 赋值给变量后调用

可以将 Lambda 表达式赋值给一个 auto 类型的变量,通过该变量多次调用该表达式,适合需要复用的逻辑。

#include <iostream>

int main() {
    // 声明 Lambda 表达式并赋值给变量
    auto add = [](int a, int b) -> int {
        return a + b;
    };

    // 使用变量进行调用
    std::cout << "3 + 4 = " << add(3, 4) << std::endl; // 输出 3 + 4 = 7
    std::cout << "5 + 6 = " << add(5, 6) << std::endl; // 输出 5 + 6 = 11

    return 0;
}

3. 作为函数参数进行调用

Lambda 表达式可以作为参数传递给其他函数,比如在 STL 算法中,Lambda 表达式经常用于定义排序规则或自定义的操作。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> nums = {1, 5, 3, 2, 4};

    // 使用 Lambda 表达式作为参数传递给 std::sort 进行排序
    std::sort(nums.begin(), nums.end(), [](int a, int b) {
        return a < b; // 按升序排序
    });

    // 输出排序后的结果
    for (auto n : nums) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 输出:1 2 3 4 5

    return 0;
}

第三章 特性优化

3.1 委托构造函数

==可以自己调用自己的构造函数==

委托构造函数是 C++11 引入的一个新特性。

允许一个构造函数调用同一个类的其他构造函数,目的是减少代码重复,简化构造逻辑。

#include <iostream>
using namespace std;

class Student {
public:
    // 委托构造函数
    Student(string name) : Student(name, 0) {
        cout << "单参数构造函数被调用" << endl;
    }
    
    Student(string name, int age) : name(name), age(age) {
        cout << "双参数构造函数被调用" << endl;
    }
    
    void display() {
        cout << "姓名: " << name << ", 年龄: " << age << endl;
    }

private:
    string name;
    int age;
};

int main() {
    Student s1("小明");
    s1.display();

    Student s2("小红", 18);
    s2.display();

    return 0;
}

3.2 继承构造函数

==可以直接调用父类的构造函数进行构造父类==

继承构造函数是 C++11 引入的一个特性,允许派生类自动继承基类的构造函数。这种机制的引入,使得我们无需在派生类中手动定义调用基类的构造函数,从而简化了代码编写,特别是在基类构造函数较多的情况下。

当派生类需要和基类具有相同的构造方式时,可以使用继承构造函数,而无需重复书写代码。这样,派生类可以直接继承并使用基类的构造函数,减少了重复代码。

语法

class 派生类名 : public 基类名 {
public:
    using 基类名::基类构造函数;
    // 其他派生类的内容
};

示例程序

#include <iostream>
using namespace std;

class Person {
public:
    Person(string name) : name(name) {
        cout << "Person(string name) 构造函数被调用" << endl;
    }
    
    Person(string name, int age) : name(name), age(age) {
        cout << "Person(string name, int age) 构造函数被调用" << endl;
    }

    void display() {
        cout << "姓名: " << name << ", 年龄: " << age << endl;
    }

private:
    string name;
    int age = 0;
};

class Student : public Person {
public:
    using Person::Person; // 继承基类的构造函数
};

int main() {
    Student s1("小明");
    s1.display();

    Student s2("小红", 18);
    s2.display();

    return 0;
}

第四章 智能指针

在C++ 没有垃圾回收机制,所以我们在操作动态空间的时候必须自己进行内存空间的释放。

所以在C++11 中引进了更加高级的办法,==智能指针==:

智能指针是 C++ 标准库提供的一种用于自动管理内存的工具,能够在合适的时机自动释放内存,从而避免手动管理内存时可能出现的内存泄漏和悬空指针问题。智能指针使用模板类封装了原始指针,通过 RAII(资源获取即初始化)的方式管理资源,确保资源在不再需要时自动释放。

在C++11中,提供了三种只能指针如下

头文件:`<memory>`
    
std::shared_ptr	      	共享所有权智能指针

std::unique_ptr	      	独占所有权智能指针

std::weak_ptr			弱引用智能指针 不能共享 不能操作数据 用来监视 std::shared_ptr

4.1 共享智能指针

共享智能指针(std::shared_ptr)是一种智能指针,主要通过引用计数管理资源的生命周期,允许多个指针共享同一个资源,当所有 shared_ptr 都不再指向该资源时,资源会自动释放。

引用计数

use_count 返回当前共享资源的引用计数,表示有多少个 shared_ptr 实例指向同一资源。

std::cout << "引用计数: " << p4.use_count() << std::endl;

4.1.1 初始化

1、构造函数初始化

通过直接调用 std::shared_ptr 的构造函数,可以将一个原始指针或另一个 shared_ptr 初始化为一个新的 shared_ptr

std::shared_ptr<int> p1(new int(10));  // 使用原始指针初始化
std::shared_ptr<int> p2(p1);           // 使用拷贝构造函数初始化

2、移动构造初始化

移动构造会将一个现有的 shared_ptr 的所有权转移到新的 shared_ptr,而原有的指针会变为空指针。

std::shared_ptr<int> p3(std::move(p1)); // p1 变为空指针

3、拷贝构造初始化

拷贝构造将创建一个新的 shared_ptr,并与原始 shared_ptr 共享同一个资源的所有权,引用计数增加。

std::shared_ptr<int> p4(p2); // p2 和 p4 共享同一个资源

4、make_shared 函数初始化

make_shared 是一种推荐的初始化方式,因为它更安全且高效。它在单一的内存分配中同时创建对象和 shared_ptr 管理的控制块,减少了内存分配的次数。

std::shared_ptr<int> p5 = std::make_shared<int>(20); // 推荐的初始化方式

5、reset 函数初始化

reset 函数可以重新绑定一个 shared_ptr 到一个新的资源上,并释放原来的资源。如果没有指定参数,则该 shared_ptr 将被重置为空指针。

p5.reset(new int(30)); // 重新绑定到新的 int(30)
p5.reset();            // 释放 p5 持有的资源并将其置为空指针

6、获取原始指针

通过 get() 获取 shared_ptr 管理的原始指针,注意不要手动释放它。

int* rawPtr = p4.get(); // 获取原始指针

4.1.2 删除器

1、内置删除器

在默认情况下,std::shared_ptr 使用 std::default_delete 作为删除器,它会自动调用 delete 来释放所管理的资源。std::default_delete 是一个模板类,可以通过 std::default_delete<T>() 来调用默认删除行为。

#include <iostream>
#include <memory>

using namespace std;

int main() {
    // 使用内置删除器 std::default_delete
    std::shared_ptr<int> p1(new int(42), std::default_delete<int>());
    cout << "值: " << *p1 << endl;

    // 当 p1 离开作用域时,会调用 std::default_delete<int>() 自动释放资源
    return 0;
}

2、自定义删除器

有时我们需要在释放资源时执行额外的操作,比如关闭文件、释放自定义资源等。在这种情况下,可以为 std::shared_ptr 指定一个自定义删除器。自定义删除器可以是一个函数、函数对象,或是 lambda 表达式。

#include <iostream>
#include <memory>
using namespace std;

void customDeleter(int* ptr) {
    cout << "自定义删除器正在删除资源" << endl;
    delete ptr;
}

int main() {
    // 使用函数作为自定义删除器
    std::shared_ptr<int> p1(new int(42), customDeleter);
    cout << "值: " << *p1 << endl;

    // 使用 lambda 表达式作为自定义删除器
    std::shared_ptr<int> p2(new int(100), [](int* ptr) {
        cout << "Lambda 自定义删除器正在删除资源" << endl;
        delete ptr;
    });

    cout << "值: " << *p2 << endl;

    // 当 p1 和 p2 离开作用域时,会调用各自的自定义删除器
    return 0;
}

6.1.3 使用示例

4.2 独占智能指针

独占智能指针(std::unique_ptr)是 C++11 引入的一种智能指针类型,它确保资源的独占所有权,不能被多个指针共享,因此也被称为“独占所有权智能指针”。当 unique_ptr 被销毁或重置时,所管理的资源会被自动释放。

4.2.1 初始化

1、构造函数初始化

可以通过直接调用 unique_ptr 的构造函数来初始化,构造时接受一个原始指针作为参数。

#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> p1(new int(42)); // 使用构造函数初始化
    cout << "值: " << *p1 << endl;
    
    // 不允许进行二次赋值
    unique_ptr<int> p2 = p1;

    return 0;
}

2、移动构造初始化

unique_ptr 不能被拷贝,但可以通过“移动”方式转移所有权。使用 std::move 可以将 unique_ptr 的所有权转移给另一个 unique_ptr

#include <iostream>
#include <memory>
using namespace std;

int main() {
    std::unique_ptr<int> p1(new int(42));
    std::unique_ptr<int> p2 = std::move(p1); // 转移所有权

    cout << "p2 的值: " << *p2 << endl;
    cout << "p1 是否为空: " << (p1 ? "否" : "是") << endl;

    return 0;
}

3、reset 函数初始化

reset 函数可以重置 unique_ptr 指向的新资源,或将其置为空,释放旧的资源。

#include <iostream>
#include <memory>
using namespace std;

int main() {
    std::unique_ptr<int> p1(new int(42));
    p1.reset(new int(100)); // 重新分配资源
    cout << "新的值: " << *p1 << endl;

    p1.reset(); // 释放资源并置为空
    cout << "p1 是否为空: " << (p1 ? "否" : "是") << endl;

    return 0;
}

4、获取原始指针

可以通过 get() 方法获取 unique_ptr 管理的原始指针。使用时需谨慎,不要手动删除该指针。

#include <iostream>
#include <memory>
using namespace std;

int main() {
    std::unique_ptr<int> p1(new int(42));
    int* rawPtr = p1.get(); // 获取原始指针
    cout << "原始指针指向的值: " << *rawPtr << endl;

    return 0;
}

4.2.2 删除器

1、内置删除器

2、自定义删除器

4.2.3 使用示例

4.3 弱引用智能指针<暂时不讲>

std::weak_ptr 是一种智能指针,用于对 std::shared_ptr 所管理的对象进行非拥有(弱引用)访问。弱引用不会影响共享对象的引用计数,因此不会导致对象的延迟析构。它主要用于解决 std::shared_ptr 之间的循环引用问题。


4.3.1 初始化

std::weak_ptr 的初始化通常依赖于已有的 std::shared_ptr,用于创建一个指向相同对象的弱引用。

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
    std::weak_ptr<int> weakPtr(sharedPtr); // 使用 shared_ptr 初始化 weak_ptr

    // 输出共享对象的引用计数
    std::cout << "use_count: " << weakPtr.use_count() << std::endl;
    return 0;
}

4.3.2 常用 API

1. use_count

use_count 返回与 std::weak_ptr 所指向的共享对象相关联的 std::shared_ptr 数量。注意,std::weak_ptr 自身不会增加对象的引用计数。

std::cout << "use_count: " << weakPtr.use_count() << std::endl;

2. expired

expired 检查 std::weak_ptr 所指向的共享对象是否已经被销毁。如果返回 true,则表示对象已经销毁,弱指针为空。

if (weakPtr.expired()) {
    std::cout << "对象已被销毁" << std::endl;
}

3. lock

lock 返回一个指向共享对象的 std::shared_ptr。如果对象已销毁,则返回一个空的 std::shared_ptr

std::shared_ptr<int> sharedPtrFromWeak = weakPtr.lock();
if (sharedPtrFromWeak) {
    std::cout << "获取共享对象的值: " << *sharedPtrFromWeak << std::endl;
} else {
    std::cout << "对象已被销毁" << std::endl;
}

4. reset

resetstd::weak_ptr 指向的对象置空。

weakPtr.reset();
if (weakPtr.expired()) {
    std::cout << "weakPtr 已被重置" << std::endl;
}

4.3.3 重复指向

多个 std::weak_ptr 可以指向同一个共享对象。这种重复指向不会影响共享对象的生命周期,因为弱引用不会增加对象的引用计数。

std::shared_ptr<int> sharedPtr = std::make_shared<int>(20);
std::weak_ptr<int> weakPtr1(sharedPtr);
std::weak_ptr<int> weakPtr2(sharedPtr);

std::cout << "use_count: " << weakPtr1.use_count() << std::endl; // 输出共享对象的引用计数

4.3.4 循环指向

std::weak_ptr 主要用于打破 std::shared_ptr 之间的循环引用。若两个对象互相拥有对方的 std::shared_ptr,将会导致循环引用,最终造成内存泄漏。通过使用 std::weak_ptr,可以避免循环引用,从而正常释放对象。

示例

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 使用 weak_ptr 打破循环引用

    ~Node() {
        std::cout << "Node 被销毁" << std::endl;
    }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->prev = node1; // 使用 weak_ptr,避免循环引用

    return 0; // node1 和 node2 都会被正常销毁
}

在此示例中,Node 结构中的 prev 成员使用 std::weak_ptr,从而打破了循环引用,确保对象在超出作用域时被正确释放。