[C.C++] C++进阶(5万字解析,超详细!!!)

615 0
Honkers 2026-1-23 08:07:48 来自手机 | 显示全部楼层 |阅读模式

目录

1 语言基础

1.1 基础数据类型范围与精度

2.2 作用域与生命周期

作用域

生命周期

2.3 const的全面用法

2 指针与引用

2.1 内存布局

2.2 指针与引用的本质区别及使用场景

2.3 理解空指针与野指针

3 面向对象与设计模式

3.1 OOP:封装、继承、多态(虚函数表原理)

虚函数表

3.2 设计模式:单例、工厂、观察者

单例模式

工厂模式

观察者模式

4 资源管理与RALL

4.1 智能指针

shared_ptr(共享)

unique_ptr(独占)

weak_ptr(防循环引用)

循环引用问题

4.2 RALL原则

5 STL与泛型编程

5.1 容器

vector

map/unordered_map

list

5.2 算法

sort

find

remove_if

5.3 lambda:结合STL算法使用

6 现代c++常用特性

6.1 auto类型推导

6.2 范围for循环

6.3 constexpr编译期计算

7 拷贝/移动语义

7.1 深拷贝 vs 浅拷贝

7.2 右值引用与move

7.3 移动构造函数

8 异常安全与设计模式

8.1 try_catch正确用法(不滥用)

8.2 noexcept关键字

8.3 接口设计原则(Pimpl模式隐藏细节)

9 并发与内存模型

9.1 thread与async

9.2 互斥锁

9.3 死锁预防

9.4 原子操作

9.4.1 内存顺序

9.4.2 比较交换(CAS)


1 语言基础

1.1 基础数据类型范围与精度

整数类型

有符号整数

类型字节大小范围说明
char1-128 ~ 127可能等价于signed char
signed char1-128 ~ 127
short2-32,768 ~ 32,767
int4(通常)-2,147,483,648 ~ 2,147,483,647至少2字节,平台相关
long4或8-2³¹ ~ 2³¹-1 或 -2⁶³ ~ 2⁶³-1平台相关
long long8-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807C++11引入

无符号整数

类型字节大小范围说明
unsigned char10 ~ 255
unsigned short20 ~ 65,535
unsigned int4(通常)0 ~ 4,294,967,295
unsigned long4或80 ~ 2³²-1 或 0 ~ 2⁶⁴-1
unsigned long long80 ~ 18,446,744,073,709,551,615

浮点类型

类型字节大小精度(小数位)范围(绝对值)说明
float46-7位1.2e-38 ~ 3.4e38单精度
double815-16位2.3e-308 ~ 1.7e308双精度
long double8,12,1618-19位以上更大范围平台相关

布尔与字符类型

类型字节大小范围/值说明
bool1(通常)true 或 false理论上1位,但占用1字节
char1-128~127 或 0~255可能是有符号或无符号
wchar_t2或40~65,535 或 更大宽字符
char16_t20~65,535UTF-16字符(C++11)
char32_t40~4,294,967,295UTF-32字符(C++11)

2.2 作用域与生命周期

作用域

作用域是指程序中标识符(如变量、函数、类型等)的可见区域。在C++中,主要有以下几种作用域:

  1. 局部作用域(块作用域)

  • 在函数内部或一个代码块(由花括号{}包围)内声明的标识符具有局部作用域。

  • 它们只在声明它们的块内可见,包括该块内的嵌套块。

  • 例如,在函数内部定义的变量,只能在函数内部访问。

  1. void func()
  2. {
  3. int x = 10; // 局部变量,作用域开始
  4. {
  5. int y = 20; // 在这个内层块中声明,只在这个内层块中可见
  6. cout << x << endl; // 可以访问x
  7. }
  8. // cout << y << endl; // 错误,y在这里不可见
  9. } // x的作用域结束
复制代码

2. 函数作用域

  • 实际上,C++中只有标号(如goto语句使用的标号)具有函数作用域。标号在函数内部声明,可以在函数内的任何位置使用(即使在声明之前),但只能在同一函数内使用。

  1. void func()
  2. {
  3. start: // 标签在整个函数内可见
  4. // ...
  5. goto start;
  6. }
复制代码

3. 函数原型作用域

  • 在函数原型中声明的参数名具有函数原型作用域。这些名字只在函数原型中可见,通常只是为了可读性,编译器会忽略它们。

  1. void func(int x, int y); // x和y的作用域仅限于此原型,在函数定义中可以使用不同的名字
复制代码

4. 文件作用域(全局作用域)

  • 在所有函数和类之外声明的标识符具有文件作用域,也称为全局作用域。

  • 它们从声明点开始,直到文件末尾都可见,并且可以被同一文件中的其他函数访问。

  • 如果使用extern关键字进行声明,则可以在多个文件之间共享。

  1. int globalVar = 100; // 全局变量,文件作用域
  2. void func1()
  3. {
  4. globalVar = 200; // 可以访问全局变量
  5. }
  6. int main()
  7. {
  8. cout << globalVar << endl; // 输出200(如果func1被调用过)
  9. return 0;
  10. }
复制代码

5. 类作用域

  • 在类中定义的成员(数据成员和成员函数)具有类作用域。

  • 类的成员在类的所有成员函数中都是可见的,但是通过对象、引用、指针或作用域解析运算符来访问

  1. class MyClass
  2. {
  3. private:
  4. int data; // 类作用域
  5. public:
  6. void setData(int d)
  7. {
  8. data = d; // 在成员函数中可以直接访问类的成员
  9. }
  10. };
  11. int main()
  12. {
  13. MyClass obj;
  14. // cout << data; // 错误,不能直接访问类成员
  15. obj.setData(10); // 通过对象调用成员函数
  16. return 0;
  17. }
复制代码

6. 命名空间作用域

  • 定义在命名空间内的标识符具有命名空间作用域。

  • 它们从声明点开始,直到命名空间结束都可见。

  • 可以通过using声明或using指令引入到当前作用域,或者通过命名空间名和作用域解析运算符来访问。

  1. namespace MyNamespace
  2. {
  3. int value = 42;
  4. }
  5. int main()
  6. {
  7. cout << MyNamespace::value << endl; // 通过命名空间访问
  8. using MyNamespace::value;
  9. cout << value << endl; // 可以直接访问
  10. return 0;
  11. }
复制代码

生命周期

生命周期是指标识符(特别是变量和对象)存在的时间段,即从创建到销毁的时间。生命周期与存储类别密切相关。

1. 自动生命周期(局部变量)

  • 在函数内部或块内声明的局部变量(包括函数参数)具有自动生命周期。

  • 它们在声明时创建(进入块时),在块结束时销毁。

  • 通常存储在栈上。

  1. void func()
  2. {
  3. int x = 5; // 生命周期开始
  4. // 使用x
  5. } // 生命周期结束,x被销毁
复制代码

2. 静态生命周期

  • 全局变量、静态局部变量和静态成员变量具有静态生命周期。

  • 它们在程序开始执行时创建(或第一次使用时,取决于具体实现),在程序结束时销毁。

  • 存储在静态存储区。

  1. int globalVar; // 静态生命周期,程序开始时创建,程序结束时销毁
  2. void func()
  3. {
  4. static int count = 0; // 静态局部变量,第一次进入函数时创建,程序结束时销毁
  5. count++;
  6. }
  7. class MyClass
  8. {
  9. static int staticVar; // 静态成员变量,具有静态生命周期
  10. };
复制代码

3. 动态生命周期

  • 通过new运算符动态分配的内存具有动态生命周期。

  • 它们在new时创建,直到通过delete显式释放时才销毁。

  • 存储在堆上。

  1. int main()
  2. {
  3. int* p = new int(10); // 动态生命周期开始
  4. // 使用p
  5. delete p; // 动态生命周期结束,释放内存
  6. return 0;
  7. }
复制代码

4. 线程局部生命周期

  • 使用thread_local关键字声明的变量具有线程局部生命周期。

  • 每个线程都有该变量的独立实例,线程开始时创建,线程结束时销毁。

  1. thread_local int tlsVar = 0; // 每个线程都有自己的tlsVar
复制代码

2.3 const的全面用法

定义常量变量

使用 const 定义常量变量,必须在定义时初始化,且之后不能修改。

  1. const int a = 10;
  2. a = 20; // 错误:不能修改常量
复制代码

指针与const

指向常量的指针:指针指向的内容是常量,不能通过指针修改内容,但指针本身可以指向其他地址。

  1. const int* p; // 或者 int const* p;
  2. int b = 5;
  3. p = &b;
  4. *p = 10; // 错误:不能通过p修改b的值
复制代码

常量指针:指针本身是常量,初始化后不能指向其他地址,但可以通过指针修改所指向的内容(除非指向的是常量)。

  1. int* const p = &b;
  2. *p = 20; // 正确:可以修改b的值
  3. int c = 30;
  4. p = &c; // 错误:不能修改指针的指向
复制代码

指向常量的常量指针:指针本身和指向的内容都是常量。

  1. const int* const p = &b;
  2. // *p = 30; // 错误:不能修改指向的内容
  3. // p = &c; // 错误:不能修改指针的指向
复制代码

函数与const

常量参数:函数参数使用 const,表示在函数内部不会修改该参数。通常用于指针或引用参数,以保护原始数据。

  1. void func(const int* p)
  2. {
  3. // *p = 10; // 错误:不能修改
  4. }
复制代码

常量引用:常量引用常用于函数参数,避免拷贝,同时不允许修改原始数据。

  1. void func2(const int& x)
  2. {
  3. // x = 10; // 错误:不能修改
  4. }
复制代码

常量成员函数:在成员函数后加 const,表示该函数不会修改类的成员变量(除非成员变量被声明为 mutable)。

  1. class MyClass
  2. {
  3. int value;
  4. public:
  5. int getValue() const
  6. {
  7. // value = 10; // 错误:不能修改成员变量
  8. return value;
  9. }
  10. };
复制代码

常量返回值:函数返回常量值,通常用于返回指针或引用,防止返回值被修改。

  1. const int& getRef(const int& a)
  2. {
  3. return a;
  4. }
  5. // 调用
  6. int x = 5;
  7. const int& y = getRef(x);
  8. // y = 10; // 错误:不能通过y修改
复制代码

类与const

常量成员变量:类的常量成员变量必须在构造函数的初始化列表中初始化。

  1. class MyClass
  2. {
  3. const int constMember;
  4. public:
  5. MyClass(int val) : constMember(val) {}
  6. };
复制代码

常量对象:常量对象只能调用常量成员函数。

  1. const MyClass obj(10);
  2. // obj.getValue(); // 正确,getValue是常量成员函数
  3. // 如果有一个非常量成员函数,则常量对象不能调用
复制代码

修饰函数参数中的指针或引用:如上所述,可以防止函数内部修改指针或引用所指向的数据。

在类型转换中的使用:const_cast 可以用于去除常量性,但必须谨慎使用,因为修改原本是常量的对象会导致未定义行为。

  1. const int a = 10;
  2. int* p = const_cast<int*>(&a);
  3. *p = 20; // 未定义行为,因为a原本是常量
复制代码

与#define宏常量的区别

  • const 有类型检查,更安全。

  • const 常量在编译时确定,可以调试。

  • #define 是文本替换,无类型,容易出错。

在模板中的应用:在模板中,const 可以用于模板参数,表示常量类型。

  1. template<typename T>
  2. const T& max(const T& a, const T& b)
  3. {
  4. return a > b ? a : b;
  5. }
复制代码

修饰lambda表达式:C++11 以后,lambda表达式可以指定为常量(默认情况下,即使捕获列表为值捕获,lambda的函数调用运算符也是可变的)。但是,如果lambda被声明为const,则不能修改捕获的变量(即使是以值方式捕获的非mutable lambda)。

实际上,在C++11中,lambda表达式默认是不可修改捕获的变量的,除非使用mutable关键字。而C++17引入了constexpr lambda

  1. int x = 10;
  2. auto lambda = [x]() const
  3. { // 注意:这个语法在C++11/14中并不直接支持,实际上lambda的常量性是通过mutable来控制的
  4. // x = 5; // 错误:不能修改捕获的变量
  5. return x;
  6. };
复制代码

修饰类类型的对象:如果类类型的对象被声明为const,那么只能调用其const成员函数。

2 指针与引用

2.1 内存布局

C++的内存布局通常可以分为以下几个部分:

  1. 栈(Stack):用于存储局部变量、函数参数和函数调用的上下文。栈由编译器自动管理,通过压栈和出栈操作进行分配和释放。栈内存的分配是连续的,通常有限,但访问速度快。

  • 特点:自动分配和释放,由编译器管理
  • 存储内容

    • 局部变量

    • 函数参数

    • 函数返回地址

  • 大小限制:通常较小(Linux下默认8MB,Windows下1MB)

  • 分配方式:LIFO(后进先出)

  • 访问速度:快

  1. void func()
  2. {
  3. int x = 10; // x在栈上
  4. char buffer[100]; // buffer在栈上
  5. // 函数结束时自动释放
  6. }
复制代码

堆(Heap):用于动态内存分配。堆内存由程序员手动管理(在C++中,使用new和delete或malloc和free)。堆内存的分配不是连续的,需要更复杂的管理,因此访问速度相对较慢,但大小只受系统内存限制。

  • 特点:手动分配和释放,由程序员管理

  • 存储内容

    • 动态分配的内存(new/malloc)

  • 大小限制:受系统内存限制

  • 访问速度:相对较慢

  • 注意事项:容易产生内存泄漏

  1. int* p = new int(10); // 在堆上分配
  2. delete p; // 必须手动释放
复制代码

全局/静态存储区:用于存储全局变量和静态变量(包括静态局部变量和静态成员变量)。这部分内存在程序启动时分配,在程序结束时释放。它又可以分为:

  • 已初始化数据段(data segment):存储已初始化的全局变量和静态变量。

  • 未初始化数据段(bss segment):存储未初始化的全局变量和静态变量(在程序启动时会被初始化为0或nullptr)。

  • 存储内容

    • 全局变量

    • 静态变量(static)

    • 常量数据(常量字符串等)

  • 生命周期:整个程序运行期间

  1. int globalVar = 100; // 全局变量 - 数据段
  2. static int staticVar = 200; // 静态变量 - 数据段
  3. const char* str = "Hello"; // 常量字符串 - 只读数据段
复制代码
  1. 常量存储区:用于存储常量,如字符串常量和其他常量。这部分内存通常是只读的。

  2. 代码区:用于存储程序的执行代码(机器指令)。

    • 存储内容

      • 可执行代码

      • 常量(const变量,取决于定义位置)

    • 特点:只读,共享内存

  1. const int MAX_SIZE = 100; // 可能存储在代码区或只读数据段
复制代码
  1. 高地址
  2. ┌─────────────────┐
  3. │ 栈区 │ ← 向下增长
  4. ├─────────────────┤
  5. │ 堆区 │ ← 向上增长
  6. ├─────────────────┤
  7. │ BSS段(未初始化) │
  8. ├─────────────────┤
  9. │ 数据段(已初始化) │
  10. ├─────────────────┤
  11. │ 代码区 │
  12. └─────────────────┘
  13. 低地址
复制代码

2.2 指针与引用的本质区别及使用场景

本质区别:

  • 指针是一个变量,存储的是另一个变量的内存地址。指针本身有地址,并且可以改变其指向。

  • 引用是一个别名,一旦被初始化为一个对象,就不能再改变为另一个对象的别名。引用没有自己的地址(不能获取引用的地址,它和它引用的对象使用相同的地址)

使用上的区别:

  • 指针可以重新赋值指向不同的对象,也可以为nullptr

  • 引用必须在定义时初始化,并且一旦绑定到一个对象上,就不能再绑定到其他对象。因此,引用没有空引用。

指针的使用场景:

  • 需要能够指向空(无对象)的情况。

  • 需要能够在不同时刻指向不同对象的情况

  • 需要存储和操作内存地址,比如动态内存分配(new返回指针)

  • 需要遍历数组或数据结构(如链表、树)时,使用指针移动

  • 多态:通过基类指针指向派生类对象

  • 需要传递函数指针或回调函数时

引用的使用场景:

  • 函数参数:希望函数内修改传入的变量,并且不希望传递空值。引用传递避免了拷贝,同时保证了绑定对象的存在。

  • 函数返回值:返回引用可以避免返回时的拷贝,但必须注意不能返回局部变量的引用(因为局部变量在函数结束后被销毁)。通常用于返回类成员、静态变量或传入的引用参数等。

  • 操作符重载:比如流操作符<<和>>,赋值操作符=等,通常返回引用以支持链式调用

  • 别名:为一个复杂的表达式或变量起一个别名,使代码更清晰

注意事项:

  • 引用在底层通常是通过指针实现的,但语言层面隐藏了这一细节。

  • 指针可以有多级(如指向指针的指针),而引用只有一级。

  • 指针可以参与算术运算(如指针加减),而引用不能。

  • 使用指针时需要注意内存管理,避免内存泄漏和野指针。

  • 引用更安全,但灵活性不如指针。

2.3 理解空指针与野指针

空指针是指不指向任何有效对象或函数的指针。在C++11之前,我们通常用NULL(实际上是0)或者0来表示空指针。从C++11开始,引入了nullptr关键字,用来表示空指针,它是空指针常量,具有明确的类型(std::nullptr_t)。(如果是c++11标准之后,不建议使用NULL,因为0是整数,不是指针类型)

野指针是指向无效内存地址的指针。这种指针不是空指针,但它指向的内存可能已经被释放或者从未分配。使用野指针会导致未定义行为,通常是程序崩溃或数据损坏。

野指针的产生原因:

  • 指针未初始化:声明指针变量时没有初始化,它的值是随机的

  • 指针指向的内存被释放后未置空

  • 指针指向局部变量,但该变量已经离开作用域

  • 指针运算超出数组边界

如何避免野指针:

  • 初始化指针:声明指针时应该立即初始化(为nullptr或者有效地址)

  • 释放内存后立即置空

  • 避免返回局部变量的地址

  • 使用智能指针

  • 使用引用代替指针:引用必须初始化,并且不能改变指向,所以不会成为野引用

3 面向对象与设计模式

3.1 OOP:封装、继承、多态(虚函数表原理)

封装是将数据(变量)和操作数据的函数(方法)组合在一起,形成一个类(class)。同时,通过访问修饰符(public、private、protected)来控制对类内部数据和函数的访问权限,从而隐藏内部实现细节,只暴露必要的接口。

封装的优点:

  • 数据隐藏:保护类的内部状态,防止外部代码随意修改

  • 接口与实现分离:用户只需知道如何使用类提供的接口,而不必了解内部实现

  • 提高代码可维护性:内部实现可以自由更改,只要接口保持不变,就不会影响使用该类的代码

继承允许我们定义一个类(称为派生类或者子类)基于另一个类(称为基类或父类)。派生类继承基类的成员(属性和方法),并可以添加自己的新成员或重写基类的方法。继承实现了代码的重用和层次关系。

继承的类型:

  • 公有继承:基类的公有成员在派生类中保持公有,保护成员保持保护

  • 保护继承:基类的公有和保护成员在派生类中都变为保护成员

  • 私有继承:基类的公有和保护成员在派生类中变为私有成员

private:完全私有,只能在本类中访问

protected:可在本类和派生类中访问

public:完全公开,任何地方都可访问

继承中的访问控制:

基类成员访问权限公有继承保护继承私有继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可访问不可访问不可访问

用虚继承的方式解决菱形继承问题:三层继承,中层使用虚继承

  1. class Animal
  2. {
  3. public:
  4. void breathe() { cout << "Breathing\n"; }
  5. };
  6. // 虚继承
  7. class Mammal : virtual public Animal
  8. {
  9. };
  10. class WingedAnimal : virtual public Animal
  11. {
  12. };
  13. // Bat只包含一份Animal的副本
  14. class Bat : public Mammal, public WingedAnimal
  15. {
  16. };
复制代码

多态通常通过虚函数实现,允许使用基类的指针或引用来调用派生类中重写的方法。同一个函数调用会根据实际对象的类型执行不同的操作。

多态的条件:

  • 必须有继承关系

  • 基类中必须有虚函数

  • 派生类必须重写基类的虚函数

  • 通过基类的指针或引用调用虚函数

静态多态(编译时多态):

  • 函数重载

  • 运算符重载

动态多态(运行时多态):

  • 虚函数机制

虚函数表

虚函数表是在编译时创建的静态表,存储了类中所有虚函数的地址。每个包含虚函数的类都有自己的虚函数表,而每个该类的对象都包含一个指向该虚函数表的指针。

没有虚函数的情况:函数调用是在编译时确定的(静态绑定),因此调用的是Base::func。

  1. class Base
  2. {
  3. public:
  4. void func() { cout << "Base::func" << endl; }
  5. };
  6. class Derived : public Base
  7. {
  8. public:
  9. void func() { cout << "Derived::func" << endl; }
  10. };
  11. int main()
  12. {
  13. Base* obj = new Derived();
  14. obj->func(); // 输出: Base::func(静态绑定)
  15. delete obj;
  16. }
复制代码

有虚函数的情况:函数调用是在运行时根据对象的实际类型确定的(动态绑定)

  1. class Base
  2. {
  3. public:
  4. virtual void func1() { cout << "Base::func1" << endl; }
  5. virtual void func2() { cout << "Base::func2" << endl; }
  6. void func3() { cout << "Base::func3" << endl; }
  7. };
  8. class Derived : public Base
  9. {
  10. public:
  11. void func1() override { cout << "Derived::func1" << endl; }
  12. void func2() override { cout << "Derived::func2" << endl; }
  13. };
  14. int main()
  15. {
  16. Base* obj = new Derived();
  17. obj->func1(); // 输出: Derived::func1(动态绑定)
  18. obj->func2(); // 输出: Derived::func2(动态绑定)
  19. obj->func3(); // 输出: Base::func3(静态绑定)
  20. delete obj;
  21. }
复制代码

单继承情况的虚函数表内存布局:

  1. class Base
  2. {
  3. public:
  4. virtual void func1() { }
  5. virtual void func2() { }
  6. int base_data;
  7. };
  8. class Derived : public Base
  9. {
  10. public:
  11. void func1() override { }
  12. virtual void func3() { }
  13. int derived_data;
  14. };
复制代码
  1. Base对象:
  2. +----------------+
  3. | vptr | --> 指向Base的虚函数表
  4. +----------------+
  5. | base_data |
  6. +----------------+
  7. Derived对象:
  8. +----------------+
  9. | vptr | --> 指向Derived的虚函数表
  10. +----------------+
  11. | base_data | (继承自Base)
  12. +----------------+
  13. | derived_data |
  14. +----------------+
  15. Base的虚函数表:
  16. +----------------+
  17. | &Base::func1 |
  18. +----------------+
  19. | &Base::func2 |
  20. +----------------+
  21. Derived的虚函数表:
  22. +----------------+
  23. | &Derived::func1| // 重写了Base::func1
  24. +----------------+
  25. | &Base::func2 | // 没有重写,所以还是Base::func2
  26. +----------------+
  27. | &Derived::func3| // 新增的虚函数
  28. +----------------+
复制代码

多继承情况的虚函数表内存布局:

  1. class Base1
  2. {
  3. public:
  4. virtual void func1() { }
  5. int data1;
  6. };
  7. class Base2
  8. {
  9. public:
  10. virtual void func2() { }
  11. int data2;
  12. };
  13. class Derived : public Base1, public Base2
  14. {
  15. public:
  16. void func1() override { }
  17. void func2() override { }
  18. virtual void func3() { }
  19. int data3;
  20. };
复制代码
  1. Derived对象:
  2. +----------------+
  3. | vptr1 | --> 指向Derived的Base1部分虚函数表
  4. +----------------+
  5. | Base1::data1 |
  6. +----------------+
  7. | vptr2 | --> 指向Derived的Base2部分虚函数表
  8. +----------------+
  9. | Base2::data2 |
  10. +----------------+
  11. | Derived::data3 |
  12. +----------------+
  13. Derived的Base1部分虚函数表:
  14. +----------------+
  15. | &Derived::func1| // 重写了Base1::func1
  16. +----------------+
  17. | &Derived::func3| // 新增的虚函数
  18. +----------------+
  19. Derived的Base2部分虚函数表:
  20. +----------------+
  21. | &Derived::func2| // 重写了Base2::func2
  22. +----------------+
  23. | 偏移量/thunk | // 可能包含this指针调整信息
  24. +----------------+
复制代码

虚函数表指针: 虚函数表指针的位置:

  • 通常位于对象的起始位置(但这不是C++标准要求的,只是大多数编译器的实现方式)

  • 每个有虚函数的对象至少有一个vptr

  • 多重继承中,对象可能有多个vptr

虚函数表指针的初始化:

  • 在构造函数中初始化

  • 初始化顺序:先基类后派生类

  • 在对象构造过程中,vptr可能被多次修改

虚函数调用的实现:

  1. Base* obj = new Derived();
  2. obj->func1(); // 虚函数调用
复制代码

实际过程:

  1. 通过对象的vptr找到虚函数表

  2. 在虚函数表中通过固定的偏移量找到函数地址

  3. 通过函数地址调用函数,并传递正确的this指针

this指针调整:在多继承中,当通过第二个基类的指针调用派生类的虚函数时,需要调整this指针

  1. Derived* d = new Derived();
  2. Base2* b2 = d; // 这里b2指向Derived对象中的Base2子对象
  3. b2->func2(); // 调用时,this指针需要调整到Derived对象的起始位置
复制代码

虚函数表的构造过程:

编译时:

  1. 编译器为每个包含虚函数的类生成一个虚函数表

  2. 表中按声明顺序存放虚函数地址

  3. 对于被重写的函数,使用派生类的函数地址

  4. 对于未被重写的函数,使用基类的函数地址

运行时:

  1. 在对象构造时设置vptr

  2. 在构造函数初始化列表中,vptr被设置为当前类的虚函数表

  3. 如果有基类,先调用基类构造函数(设置基类的vptr)

  4. 然后调用派生类构造函数(重新设置vptr)

3.2 设计模式:单例、工厂、观察者

单例模式

确保一个类只有一个实例,并提供全局访问点。

实现要点:

  1. 构造函数私有化:确保外部不能直接创建对象。

  2. 禁用拷贝和赋值:避免通过拷贝或赋值创建新实例。

  3. 提供全局访问点:通过静态成员函数获取唯一实例。

  4. 线程安全:多线程环境下确保只创建一个实例。

  5. 延迟初始化(可选):有些场景下希望第一次使用时才创建实例(懒汉式),而有些场景下希望程序启动时就创建(饿汉式)。

  6. 资源释放:一般情况下,单例对象在程序运行期间一直存在,不需要手动释放。如果使用new创建,可以考虑使用智能指针,或者使用静态成员变量(在程序结束时自动销毁)。使用局部静态变量方式可以自动销毁。

懒汉式(线程不安全)和饿汉式(线程安全),现代C++中推荐线程安全懒汉式(使用局部静态变量)。

  1. class Singleton
  2. {
  3. private:
  4. Singleton() {}
  5. Singleton(const Singleton&) = delete;
  6. Singleton& operator=(const Singleton&) = delete;
  7. public:
  8. static Singleton& getInstance()
  9. {
  10. static Singleton instance; // 线程安全
  11. return instance;
  12. }
  13. };
复制代码

工厂模式

简单工厂模式:一个工厂类,根据参数创建不同的产品。适用于产品类型较少的情况。

要点:

  1. 一个工厂类,一个产品接口(或抽象类),多个具体产品类。

  2. 工厂类中有一个静态方法(或非静态),根据参数创建具体产品。

  1. #include <iostream>
  2. using namespace std;
  3. // 产品接口
  4. class Shape
  5. {
  6. public:
  7. virtual void draw() = 0;
  8. virtual ~Shape() {}
  9. };
  10. // 具体产品:圆形
  11. class Circle : public Shape
  12. {
  13. public:
  14. void draw() override
  15. {
  16. cout << "Draw a circle." << endl;
  17. }
  18. };
  19. // 具体产品:正方形
  20. class Square : public Shape
  21. {
  22. public:
  23. void draw() override
  24. {
  25. cout << "Draw a square." << endl;
  26. }
  27. };
  28. // 简单工厂
  29. class ShapeFactory
  30. {
  31. public:
  32. // 根据类型创建形状
  33. static Shape* createShape(const string& type)
  34. {
  35. if (type == "circle")
  36. {
  37. return new Circle();
  38. } else if (type == "square")
  39. {
  40. return new Square();
  41. } else
  42. {
  43. return nullptr;
  44. }
  45. }
  46. };
  47. int main()
  48. {
  49. Shape* circle = ShapeFactory::createShape("circle");
  50. Shape* square = ShapeFactory::createShape("square");
  51. if (circle) circle->draw();
  52. if (square) square->draw();
  53. delete circle;
  54. delete square;
  55. return 0;
  56. }
复制代码

优点:

  • 客户端不需要知道具体产品类的类名,只需要知道参数。

  • 通过配置文件,可以不用修改客户端代码,而更换和增加新的具体产品。

缺点:

  • 工厂类集中了所有产品的创建逻辑,一旦不能正常工作,整个系统都受影响。

  • 系统扩展困难,一旦增加新产品就不得不修改工厂逻辑。

工厂方法模式:定义一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

要点:

  1. 一个产品接口,多个具体产品。

  2. 一个工厂接口(或抽象类),多个具体工厂,每个具体工厂对应一个具体产品。

  1. #include <iostream>
  2. using namespace std;
  3. // 产品接口
  4. class Shape
  5. {
  6. public:
  7. virtual void draw() = 0;
  8. virtual ~Shape() {}
  9. };
  10. // 具体产品:圆形
  11. class Circle : public Shape
  12. {
  13. public:
  14. void draw() override
  15. {
  16. cout << "Draw a circle." << endl;
  17. }
  18. };
  19. // 具体产品:正方形
  20. class Square : public Shape
  21. {
  22. public:
  23. void draw() override
  24. {
  25. cout << "Draw a square." << endl;
  26. }
  27. };
  28. // 工厂接口
  29. class ShapeFactory
  30. {
  31. public:
  32. virtual Shape* createShape() = 0;
  33. virtual ~ShapeFactory() {}
  34. };
  35. // 具体工厂:创建圆形
  36. class CircleFactory : public ShapeFactory
  37. {
  38. public:
  39. Shape* createShape() override
  40. {
  41. return new Circle();
  42. }
  43. };
  44. // 具体工厂:创建正方形
  45. class SquareFactory : public ShapeFactory
  46. {
  47. public:
  48. Shape* createShape() override
  49. {
  50. return new Square();
  51. }
  52. };
  53. int main()
  54. {
  55. ShapeFactory* circleFactory = new CircleFactory();
  56. ShapeFactory* squareFactory = new SquareFactory();
  57. Shape* circle = circleFactory->createShape();
  58. Shape* square = squareFactory->createShape();
  59. circle->draw();
  60. square->draw();
  61. delete circle;
  62. delete square;
  63. delete circleFactory;
  64. delete squareFactory;
  65. return 0;
  66. }
复制代码

优点:

  • 客户端不需要知道具体产品类的类名,只需要知道对应的工厂。

  • 在系统增加新的产品时,只需要添加具体产品类和对应的具体工厂类,无需修改现有代码,符合开闭原则。

缺点:

  • 每增加一个产品,就需要增加一个具体工厂类,增加了系统的复杂度。

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

要点:

  1. 多个产品接口,每个产品接口有多个具体产品。

  2. 一个工厂接口,可以创建多个产品,每个具体工厂对应一个产品族。

示例代码:假设有两种产品:颜色和形状。颜色有红色和蓝色,形状有圆形和正方形。抽象工厂可以创建一组相关的产品,比如红色圆形、蓝色正方形等。

  1. #include <iostream>
  2. using namespace std;
  3. // 抽象产品:颜色
  4. class Color
  5. {
  6. public:
  7. virtual void fill() = 0;
  8. virtual ~Color() {}
  9. };
  10. // 具体产品:红色
  11. class Red : public Color
  12. {
  13. public:
  14. void fill() override
  15. {
  16. cout << "Fill with red color." << endl;
  17. }
  18. };
  19. // 具体产品:蓝色
  20. class Blue : public Color
  21. {
  22. public:
  23. void fill() override
  24. {
  25. cout << "Fill with blue color." << endl;
  26. }
  27. };
  28. // 抽象产品:形状
  29. class Shape {
  30. public:
  31. virtual void draw() = 0;
  32. virtual ~Shape() {}
  33. };
  34. // 具体产品:圆形
  35. class Circle : public Shape
  36. {
  37. public:
  38. void draw() override
  39. {
  40. cout << "Draw a circle." << endl;
  41. }
  42. };
  43. // 具体产品:正方形
  44. class Square : public Shape
  45. {
  46. public:
  47. void draw() override
  48. {
  49. cout << "Draw a square." << endl;
  50. }
  51. };
  52. // 抽象工厂
  53. class AbstractFactory
  54. {
  55. public:
  56. virtual Color* createColor() = 0;
  57. virtual Shape* createShape() = 0;
  58. virtual ~AbstractFactory() {}
  59. };
  60. // 具体工厂:创建红色和圆形
  61. class RedCircleFactory : public AbstractFactory
  62. {
  63. public:
  64. Color* createColor() override
  65. {
  66. return new Red();
  67. }
  68. Shape* createShape() override
  69. {
  70. return new Circle();
  71. }
  72. };
  73. // 具体工厂:创建蓝色和正方形
  74. class BlueSquareFactory : public AbstractFactory
  75. {
  76. public:
  77. Color* createColor() override
  78. {
  79. return new Blue();
  80. }
  81. Shape* createShape() override
  82. {
  83. return new Square();
  84. }
  85. };
  86. int main()
  87. {
  88. AbstractFactory* factory1 = new RedCircleFactory();
  89. Color* red = factory1->createColor();
  90. Shape* circle = factory1->createShape();
  91. red->fill();
  92. circle->draw();
  93. AbstractFactory* factory2 = new BlueSquareFactory();
  94. Color* blue = factory2->createColor();
  95. Shape* square = factory2->createShape();
  96. blue->fill();
  97. square->draw();
  98. delete red; //因为类是在堆上创建的,所以必须要手动delete
  99. delete circle;
  100. delete factory1;
  101. delete blue;
  102. delete square;
  103. delete factory2;
  104. return 0;
  105. }
复制代码

优点:

  • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

  • 增加新的产品族很方便,无需修改现有系统。

缺点:

  • 增加新的产品等级结构(即增加新的产品类型)很麻烦,需要修改抽象工厂和所有的具体工厂。

观察者模式

定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。

要点:

  • Subject(主题):维护观察者列表,提供添加、删除和通知观察者的接口

  • Observer(观察者):定义更新接口,用于在主题状态改变时接收通知

  • ConcreteSubject(具体主题):存储具体状态,状态改变时通知所有观察者

  • ConcreteObserver(具体观察者):实现更新接口,保持与主题状态的一致性

  1. #include <iostream>
  2. #include <vector>
  3. #include <memory>
  4. #include <functional>
  5. #include <mutex>
  6. #include <algorithm>
  7. // 现代观察者模式最佳实践
  8. template<typename... Args>
  9. class Observable //观察者回调函数的参数类型
  10. {
  11. private:
  12. using Callback = std::function<void(Args...)>;
  13. struct ObserverEntry //存储每个观察者的信息
  14. {
  15. std::weak_ptr<void> weakKey; // 用于取消订阅的键
  16. Callback callback;//回调函数
  17. int priority{0};//优先级,用于排序
  18. bool operator<(const ObserverEntry& other) const
  19. {
  20. return priority < other.priority;
  21. }
  22. };
  23. std::vector<ObserverEntry> observers;//存储所有观察者
  24. mutable std::shared_mutex mutex;//用于多线程保护的共享互斥锁
  25. std::atomic<bool> notifying{false};//原子布尔变量,防止重入通知
  26. public:
  27. // 订阅(返回可用于取消订阅的token)
  28. template<typename T>
  29. std::shared_ptr<T> subscribe(Callback callback, int priority = 0)
  30. {
  31. auto key = std::make_shared<T>();
  32. std::unique_lock lock(mutex);
  33. observers.push_back({key, callback, priority});
  34. std::sort(observers.begin(), observers.end());
  35. return key;
  36. }
  37. // 取消订阅
  38. template<typename T>
  39. void unsubscribe(const std::shared_ptr<T>& key)
  40. {
  41. std::unique_lock lock(mutex);
  42. observers.erase(
  43. std::remove_if(observers.begin(), observers.end(),
  44. [&key](const ObserverEntry& entry)
  45. {
  46. return entry.weakKey.lock() == key;
  47. }),
  48. observers.end()
  49. );
  50. }
  51. // 通知所有观察者(线程安全,异常安全)
  52. void notify(Args... args)
  53. {
  54. if (notifying.exchange(true))
  55. {
  56. return; // 防止重入
  57. }
  58. std::vector<ObserverEntry> observersCopy;
  59. {
  60. std::shared_lock lock(mutex);
  61. observersCopy = observers;
  62. }
  63. try
  64. {
  65. // 按优先级顺序通知
  66. for (const auto& entry : observersCopy)
  67. {
  68. if (auto key = entry.weakKey.lock())
  69. {
  70. try
  71. {
  72. entry.callback(args...);
  73. }
  74. catch (const std::exception& e)
  75. {
  76. std::cerr << "观察者回调异常: " << e.what() << std::endl;
  77. // 继续执行其他观察者
  78. }
  79. }
  80. }
  81. } catch (...)
  82. {
  83. notifying.store(false);
  84. throw;
  85. }
  86. notifying.store(false);
  87. // 清理已销毁的观察者
  88. cleanup();
  89. }
  90. private:
  91. void cleanup() //清理方法:移除所有weakKey已经过期的观察者
  92. {
  93. std::unique_lock lock(mutex);
  94. observers.erase(
  95. std::remove_if(observers.begin(), observers.end(),
  96. [](const ObserverEntry& entry)
  97. {
  98. return entry.weakKey.expired();
  99. }),
  100. observers.end()
  101. );
  102. }
  103. };
  104. // 使用示例
  105. class Button
  106. {
  107. private:
  108. Observable<int, int> onClick; // x, y坐标
  109. public:
  110. auto connectClick(std::function<void(int, int)> handler) //连接点击事件
  111. {
  112. return onClick.subscribe<int>(handler);
  113. }
  114. void click(int x, int y) //模拟按钮点击
  115. {
  116. std::cout << "按钮被点击: (" << x << ", " << y << ")" << std::endl;
  117. onClick.notify(x, y);
  118. }
  119. };
  120. int main()
  121. {
  122. Button button;//创建Button对象,连接两个点击事件处理其,然后触发点击事件,最后再连接一个处理器
  123. // 订阅点击事件
  124. auto token1 = button.connectClick([](int x, int y)
  125. {
  126. std::cout << "处理器1: 点击位置 (" << x << ", " << y << ")" << std::endl;
  127. });
  128. auto token2 = button.connectClick([](int x, int y)
  129. {
  130. std::cout << "处理器2: 按钮点击事件" << std::endl;
  131. });
  132. // 触发事件
  133. button.click(100, 200);
  134. // 取消订阅
  135. button.connectClick([](int, int)
  136. {
  137. // 这个不会执行,因为token2被释放了
  138. });
  139. return 0;
  140. }
复制代码

4 资源管理与RALL

4.1 智能指针

shared_ptr(共享)

特点:共享所有权,引用计数

共享所有权的智能指针。多个shared_ptr可以指向同一个对象,并通过引用计数来管理对象的生命周期。当最后一个shared_ptr被销毁时,对象才会被释放。

  1. #include <memory>
  2. #include <iostream>
  3. void shared_ptr_demo()
  4. {
  5. // 创建 shared_ptr
  6. std::shared_ptr<int> p1 = std::make_shared<int>(100); // 推荐
  7. std::shared_ptr<int> p2(p1); // 复制,引用计数+1
  8. std::cout << "p1.use_count(): " << p1.use_count() << std::endl; // 2
  9. std::cout << "p2.use_count(): " << p2.use_count() << std::endl; // 2
  10. // 独立指针
  11. std::shared_ptr<int> p3 = std::make_shared<int>(200);
  12. std::cout << "p3.use_count(): " << p3.use_count() << std::endl; // 1
  13. // 重置指针
  14. p2.reset(); // 释放所有权
  15. std::cout << "p1.use_count(): " << p1.use_count() << std::endl; // 1
  16. // 自定义删除器
  17. auto custom_deleter = [](int* p)
  18. {
  19. std::cout << "Custom delete: " << *p << std::endl;
  20. delete p;
  21. };
  22. std::shared_ptr<int> p4(new int(300), custom_deleter);
  23. // 数组支持(C++17起)
  24. std::shared_ptr<int[]> p5(new int[5]{10, 20, 30, 40, 50});
  25. std::cout << p5[2] << std::endl; // 30
  26. }
复制代码

注意:使用shared_ptr时要注意循环引用问题,如果两个对象相互使用shared_ptr指向对方,那么引用计数永远不会降到0,导致内存泄漏。这时可以使用weak_ptr来解决。

引用计数原理:

  1. p1 ──┐
  2. │───> 对象 (ref_count=2)
  3. p2 ──┘
复制代码

unique_ptr(独占)

特点:独占所有权,不可复制

独占所有权的智能指针。同一时间只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也会被自动销毁。 它不能复制,只能移动。这意味着,所有权可以从一个unique_ptr转移到另一个unique_ptr,但不会有两个unique_ptr同时拥有同一个对象。

  1. #include <memory>
  2. #include <iostream>
  3. void unique_ptr_demo()
  4. {
  5. // 创建 unique_ptr
  6. std::unique_ptr<int> p1(new int(10));
  7. auto p2 = std::make_unique<int>(20); // 推荐方式(C++14)
  8. // 访问数据
  9. std::cout << *p1 << std::endl; // 10
  10. std::cout << *p2 << std::endl; // 20
  11. // 转移所有权
  12. std::unique_ptr<int> p3 = std::move(p1);
  13. std::cout << (p1 == nullptr) << std::endl; // 1 (true)
  14. std::cout << *p3 << std::endl; // 10
  15. // 自定义删除器
  16. auto deleter = [](int* p)
  17. {
  18. std::cout << "Deleting int: " << *p << std::endl;
  19. delete p;
  20. };
  21. std::unique_ptr<int, decltype(deleter)> p4(new int(30), deleter);
  22. // 数组支持
  23. std::unique_ptr<int[]> p5(new int[5]{1, 2, 3, 4, 5});
  24. std::cout << p5[2] << std::endl; // 3
  25. }
复制代码

常用方法:

  1. std::unique_ptr<T> up;
  2. up.reset(ptr); // 接管新指针,释放原指针
  3. up.release(); // 释放所有权,返回原始指针,不删除对象
  4. up.get(); // 获取原始指针
  5. up.swap(other); // 交换指针
复制代码

也可以使用std::make_unique(C++14引入)来创建unique_ptr,这样更安全,因为如果内存分配失败,它会抛出异常,而且使用它时不需要直接使用new。

  1. auto p = std::make_unique<int>(10);
复制代码
weak_ptr(防循环引用)

特点:弱引用,不增加应用计数,解决循环引用问题

弱引用的智能指针,它不控制对象的生命周期,而是指向一个由shared_ptr管理的对象。weak_ptr的构造和析构不会影响引用计数。它主要用于解决shared_ptr的循环引用问题。

  1. #include <memory>
  2. #include <iostream>
  3. class Node
  4. {
  5. public:
  6. std::string name;
  7. std::shared_ptr<Node> next;
  8. std::weak_ptr<Node> prev; // 使用 weak_ptr 避免循环引用
  9. Node(const std::string& n) : name(n)
  10. {
  11. std::cout << "Node " << name << " created" << std::endl;
  12. }
  13. ~Node()
  14. {
  15. std::cout << "Node " << name << " destroyed" << std::endl;
  16. }
  17. };
  18. void weak_ptr_demo()
  19. {
  20. auto node1 = std::make_shared<Node>("Node1");
  21. auto node2 = std::make_shared<Node>("Node2");
  22. // 建立双向链接(使用 weak_ptr 避免循环引用)
  23. node1->next = node2;
  24. node2->prev = node1;
  25. // 使用 weak_ptr
  26. std::weak_ptr<Node> weak_node = node1;
  27. // 检查对象是否还存在
  28. if (auto shared_node = weak_node.lock())
  29. {
  30. std::cout << "Node exists: " << shared_node->name << std::endl;
  31. } else {
  32. std::cout << "Node has been destroyed" << std::endl;
  33. }
  34. // 重置 shared_ptr
  35. node1.reset();
  36. if (auto shared_node = weak_node.lock())
  37. {
  38. std::cout << "Node still exists" << std::endl;
  39. } else {
  40. std::cout << "Node has been destroyed" << std::endl;
  41. }
  42. }
复制代码

常用方法:

  1. std::weak_ptr<T> wp;
  2. wp.lock(); // 尝试获取 shared_ptr,失败返回空
  3. wp.expired(); // 检查对象是否已被删除
  4. wp.use_count(); // 获取引用计数
复制代码

注意:weak_ptr不能直接访问对象,必须通过lock()函数获取一个shared_ptr,然后使用。如果对象已经被释放,lock()会返回一个空的shared_ptr。

  1. std::shared_ptr<int> sp = std::make_shared<int>(30);
  2. std::weak_ptr<int> wp = sp;
  3. // 使用weak_ptr访问对象
  4. if (std::shared_ptr<int> np = wp.lock())
  5. {
  6. std::cout << *np << std::endl;
  7. }
  8. else
  9. {
  10. std::cout << "对象已释放" << std::endl;
  11. }
复制代码

循环引用问题
  1. class BadNode
  2. {
  3. public:
  4. std::shared_ptr<BadNode> next;
  5. std::shared_ptr<BadNode> prev; // 错误:相互持有 shared_ptr
  6. BadNode(const std::string& n) : name(n)
  7. {
  8. std::cout << "BadNode " << n << " created" << std::endl;
  9. }
  10. ~BadNode()
  11. {
  12. std::cout << "BadNode " << name << " destroyed" << std::endl; // 永远不会执行
  13. }
  14. private:
  15. std::string name;
  16. };
  17. void circular_reference_demo()
  18. {
  19. auto node1 = std::make_shared<BadNode>("Node1");
  20. auto node2 = std::make_shared<BadNode>("Node2");
  21. // 循环引用:两者都不会被销毁
  22. node1->next = node2;
  23. node2->prev = node1;
  24. std::cout << "node1.use_count(): " << node1.use_count() << std::endl; // 2
  25. std::cout << "node2.use_count(): " << node2.use_count() << std::endl; // 2
  26. // 退出时,引用计数从2减到1,内存泄漏!
  27. }
复制代码

4.2 RALL原则

构造函数获取资源,析构函数释放资源

RAII的核心思想是将资源(如动态内存、文件句柄、网络连接、锁等)的生命周期与对象的生命周期绑定,在对象构造时获取资源,在对象析构时释放资源。这样,只要对象能正确析构,资源就能被正确释放,从而避免资源泄漏。

RALL的工作原理:

  • 在类的构造函数中获取资源(分配内存、打开文件、加锁等)

  • 在类的析构函数中释放资源(释放内存、关闭文件、解锁等)

  • 这样,当对象离开作用域时(无论是正常离开还是因为异常),析构函数都会被调用,从而释放资源。

管理动态内存(类似智能指针):

  1. template<typename T>
  2. class SimpleUniquePtr
  3. {
  4. private:
  5. T* ptr;
  6. public:
  7. // 构造函数获取资源
  8. explicit SimpleUniquePtr(T* p = nullptr) : ptr(p) {}
  9. // 析构函数释放资源
  10. ~SimpleUniquePtr()
  11. {
  12. delete ptr;
  13. }
  14. // 禁止拷贝
  15. SimpleUniquePtr(const SimpleUniquePtr&) = delete;
  16. SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;
  17. // 允许移动
  18. SimpleUniquePtr(SimpleUniquePtr&& other) noexcept : ptr(other.ptr)
  19. {
  20. other.ptr = nullptr;
  21. }
  22. SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept
  23. {
  24. if (this != &other)
  25. {
  26. delete ptr;
  27. ptr = other.ptr;
  28. other.ptr = nullptr;
  29. }
  30. return *this;
  31. }
  32. // 访问操作
  33. T& operator*() const { return *ptr; }
  34. T* operator->() const { return ptr; }
  35. T* get() const { return ptr; }
  36. // 资源释放
  37. void reset(T* p = nullptr)
  38. {
  39. delete ptr;
  40. ptr = p;
  41. }
  42. };
复制代码

RALL与异常安全:

  1. void badFunction()
  2. {
  3. int* ptr = new int(100);
  4. someFunctionThatMayThrow(); // 如果抛出异常,delete不会执行,内存泄漏
  5. delete ptr;
  6. }
  7. void goodFunction()
  8. {
  9. std::unique_ptr<int> ptr = std::make_unique<int>(100); // 使用RAII
  10. someFunctionThatMayThrow(); // 即使抛出异常,ptr的析构函数也会被调用,内存被释放
  11. }
复制代码

C++标准库中的智能指针(unique_ptr、shared_ptr、weak_ptr)是RAII的典型应用。它们管理动态内存,确保在适当的时候释放内存。

作用域绑定资源生命周期:

  1. void scope_based_management()
  2. {
  3. {
  4. LockRAII lock(mutex); // 进入作用域时加锁
  5. // 临界区代码
  6. // 如果这里抛出异常,锁仍然会自动释放
  7. } // 离开作用域时自动解锁
  8. // 不同作用域不同资源
  9. {
  10. FileRAII file1("a.txt", "r");
  11. // 使用file1
  12. } // file1自动关闭
  13. {
  14. FileRAII file2("b.txt", "w");
  15. // 使用file2
  16. } // file2自动关闭
  17. }
复制代码

RALL的优点:

  1. 自动释放资源:不再需要手动释放资源,减少错误。

  2. 异常安全:即使发生异常,资源也能被正确释放。

  3. 代码简洁:将资源管理逻辑封装在类中,使用代码更清晰。

  4. 资源所有权明确:通过对象的生命周期明确资源的所有权。

RALL的注意事项:

  1. 资源的所有权:RAII对象应独占资源的所有权,避免多个RAII对象管理同一资源(除非使用引用计数如shared_ptr)。

  2. 拷贝控制:如果RAII类需要拷贝,需要仔细考虑拷贝语义(深拷贝或转移所有权)。通常,如果资源不可共享,应禁止拷贝(如unique_ptr),或使用引用计数(如shared_ptr)。

  3. 资源泄露:确保在构造函数中获取资源时没有泄漏,如果构造函数抛出异常,析构函数不会被调用,因此需要确保构造函数是异常安全的。

c++标准库中的RALL类

  1. #include <memory>
  2. #include <fstream>
  3. #include <mutex>
  4. void standard_library_raii()
  5. {
  6. // 1. 内存管理
  7. std::unique_ptr<int> ptr = std::make_unique<int>(42);
  8. std::shared_ptr<int> shared = std::make_shared<int>(42);
  9. // 2. 文件管理
  10. std::ifstream file("data.txt"); // RAII:自动关闭文件
  11. std::ofstream output("output.txt");
  12. // 3. 互斥锁管理
  13. std::mutex mtx;
  14. {
  15. std::lock_guard<std::mutex> lock(mtx); // RAII锁
  16. // 临界区
  17. } // 自动解锁
  18. // 4. 动态数组
  19. std::vector<int> vec; // RAII:自动管理内存
  20. vec.push_back(1);
  21. // 离开作用域时自动释放内存
  22. }
复制代码

RALL设计原则:

  1. 资源获取在构造函数中完成

  2. 资源释放在析构函数中完成

  3. 使用移动语义支持资源所有权的转移

  4. 通常禁用拷贝构造函数和拷贝赋值运算符

  5. 考虑异常安全性

5 STL与泛型编程

5.1 容器

vector

vector 是一个动态数组,它能够存储连续内存中的元素,并且可以动态地调整大小。

  • 元素在内存中是连续存储的,这意味着可以通过指针算术来访问元素。

  • 当当前容量不足以容纳新元素时,vector会重新分配一个更大的内存块(通常是当前容量的两倍),并将现有元素复制到新内存中。

常用操作:

  • 访问元素:使用[]运算符或at()方法(at()会进行边界检查)。

  • 尾部添加/删除:push_back()和pop_back(),平均时间复杂度为O(1)(不考虑重新分配的情况)。

  • 中间插入/删除:insert()和erase(),需要移动元素,时间复杂度为O(n)。

  • 大小和容量:size()返回元素数量,capacity()返回当前分配的内存可以容纳的元素数量。

优缺点:

  • 优点:连续内存使得访问速度快,支持随机访问,缓存友好。

  • 缺点:在中间插入或删除元素效率低,动态调整大小时可能需要昂贵的复制操作。

适用场景:

  • 需要频繁随机访问元素。

  • 主要是在尾部添加或删除元素。

  • 不需要在中间频繁插入或删除。

vector扩容机制:

vector的扩容机制是动态数组的核心特性。当vector的当前容量(capacity)不足以容纳新元素时,就需要扩容。 扩容的一般步骤:

  1. 分配一块更大的内存空间(具体大小取决于实现,通常是当前容量的两倍或1.5倍)。

  2. 将原有元素从旧内存移动到新内存(如果是非POD类型,移动过程中可能会调用元素的拷贝构造函数或移动构造函数)。

  3. 释放旧内存。

  4. 更新vector的内部指针,容量等状态。

vector 的扩容是自动的!这是 vector 内部机制,不需要手动实现。

当你使用 push_back、emplace_back、insert 等方法向 vector 中添加元素,而当前容量(capacity)不足以容纳新元素时,vector 会自动进行扩容。

注意事项:

  1. 性能考虑:自动扩容虽然方便,但频繁扩容会导致性能下降。如果你知道大概需要多少元素,最好使用 reserve 预分配。

  2. 迭代器失效:扩容后,所有迭代器、指针和引用都会失效,需要重新获取。

  3. 异常安全:vector 的扩容操作是异常安全的,如果扩容失败(如内存不足),会抛出 std::bad_alloc 异常,且原 vector 保持不变。

map/unordered_map
  • map 是一个关联容器,存储键值对,并且按照键的顺序排序。它通常使用红黑树实现。

  • unordered_map 也是一个关联容器,但它不保证顺序,通常使用哈希表实现。

map:

  • 优点:有序,便于范围查询,操作的时间复杂度稳定为O(log n)。

  • 缺点:相比unordered_map,访问速度较慢(因为树结构)。

unordered_map:

  • 优点:平均情况下插入、删除、查找的时间复杂度为O(1)。

  • 缺点:最坏情况下(哈希冲突严重)性能下降为O(n),无序。

适用场景:

  • map:需要有序的键值对,或者需要范围查询。

  • unordered_map:对顺序没有要求,需要快速的单个元素访问。

红黑树:

红黑树是一种自平衡的二叉搜索树,它在每个节点上增加了一个存储位表示节点的颜色(红色或黑色)。通过对任何一条从根到叶子的路径上各个节点颜色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似平衡的。

红黑树满足以下性质:

  1. 每个节点要么是红色,要么是黑色。

  2. 根节点是黑色的。

  3. 每个叶子节点(NIL节点,空节点)是黑色的。

  4. 如果一个节点是红色的,则它的两个子节点都是黑色的(即不能有两个连续的红色节点)。

  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(称为黑高)。

  1. 旋转:为了在插入和删除后重新平衡树,需要进行旋转操作。旋转有两种:左旋和右旋。

    • 左旋:将节点x的右子树y提升为新的父节点,x成为y的左子节点,y的左子节点成为x的右子节点。

    • 右旋:将节点x的左子树y提升为新的父节点,x成为y的右子节点,y的右子节点成为x的左子节点。

  2. 插入:新插入的节点通常被着色为红色,以避免违反黑高性质。然后,通过一系列的颜色更改和旋转来修复可能出现的连续红色节点问题。

  3. 删除:删除操作比插入更复杂。删除一个节点后,可能会破坏红黑树的性质,需要通过颜色更改和旋转来修复。

哈希表:

  1. // 哈希表(Hash Table)是一种:
  2. // 1. 键值对存储结构
  3. // 2. 通过哈希函数将键映射到数组索引
  4. // 3. 理想情况下提供O(1)的插入、删除和查找
  5. // 基本工作原理:
  6. // key → 哈希函数 → 哈希值 → 数组索引 → 存储value
  7. // 在C++标准库中,unordered_map通常实现为:
  8. // - 数组:存储桶(buckets)
  9. // - 每个桶:链表或红黑树(C++11后引入)
  10. // 解决哈希冲突的两种主要方法:
  11. // 1. 链地址法(Separate Chaining):每个桶是链表
  12. // 2. 开放地址法(Open Addressing):线性探测、二次探测等
  13. // C++ unordered_map使用链地址法
  14. /*
  15. buckets数组
  16. +---+ +-------+ +-------+ +-------+
  17. | 0 | -> | key1 | -> | key4 | -> | key7 | -> nullptr
  18. +---+ +-------+ +-------+ +-------+
  19. | 1 | -> nullptr
  20. +---+
  21. | 2 | -> | key2 | -> | key5 | -> nullptr
  22. +---+ +-------+ +-------+
  23. | 3 | -> | key3 | -> | key6 | -> nullptr
  24. +---+ +-------+ +-------+
  25. ...
  26. |n-1| -> nullptr
  27. +---+
  28. 哈希函数:h(key) = key % n
  29. */
复制代码

list

list 是一个双向链表,允许在常数时间内在任何位置进行插入和删除操作。

  • 每个元素(节点)包含指向前一个和后一个节点的指针。

  • 内存不是连续的,因此不能通过索引直接访问元素。

常用操作:

  • 插入:push_back()、push_front()、insert(),时间复杂度为O(1)(如果已知插入位置)。

  • 删除:pop_back()、pop_front()、erase(),时间复杂度为O(1)(如果已知删除位置)。

  • 访问:只能通过迭代器进行访问,不支持随机访问。

  • 优点:在任意位置插入和删除效率高,不需要移动其他元素。

  • 缺点:访问元素效率低,内存开销大(每个元素都需要额外的指针空间),缓存不友好。

适用场景:

  • 需要频繁在中间插入或删除元素。

  • 不需要随机访问,只需要顺序访问。

5.2 算法

以下三个算法函数都定义在头文件

sort

sort用于对容器(如vector、deque、数组等)中的元素进行排序,默认是升序排序。

  1. template< class RandomIt >
  2. void sort( RandomIt first, RandomIt last );
  3. template< class RandomIt, class Compare >
  4. void sort( RandomIt first, RandomIt last, Compare comp );
复制代码

参数:

  • first:指向待排序序列的第一个元素的迭代器。

  • last:指向待排序序列的最后一个元素之后位置的迭代器。

  • comp:比较函数,用于定义排序的顺序。它是一个二元谓词,接受两个参数,返回true如果第一个参数应该排在第二个参数之前。

使用:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> v = {5, 3, 1, 4, 2};
  7. // 默认升序排序
  8. std::sort(v.begin(), v.end());
  9. for (int i : v) std::cout << i << " "; // 输出: 1 2 3 4 5
  10. std::cout << std::endl;
  11. // 降序排序
  12. std::sort(v.begin(), v.end(), std::greater<int>());
  13. for (int i : v) std::cout << i << " "; // 输出: 5 4 3 2 1
  14. std::cout << std::endl;
  15. // 使用lambda表达式自定义排序
  16. std::sort(v.begin(), v.end(), [](int a, int b) {
  17. return a < b; // 升序
  18. });
  19. for (int i : v) std::cout << i << " "; // 输出: 1 2 3 4 5
  20. std::cout << std::endl;
  21. return 0;
  22. }
复制代码
  • sort要求迭代器是随机访问迭代器,所以它不能用于链表(如list)和关联容器(如set、map)。链表有自己特有的sort成员函数。

  • sort的平均时间复杂度为O(N log N),其中N是待排序元素的数量。

find

find用于在容器中查找特定值,返回指向第一个等于该值的元素的迭代器。如果未找到,则返回结束迭代器(即last)。

  1. template< class InputIt, class T >
  2. InputIt find( InputIt first, InputIt last, const T& value );
复制代码

参数:

  • first:指向查找范围的起始位置的迭代器。

  • last:指向查找范围的结束位置(最后一个元素之后)的迭代器。

  • value:要查找的值。

使用:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> v = {1, 2, 3, 4, 5};
  7. // 查找值为3的元素
  8. auto it = std::find(v.begin(), v.end(), 3);
  9. if (it != v.end())
  10. {
  11. std::cout << "Found: " << *it << std::endl; // 输出: Found: 3
  12. // 也可以计算索引
  13. std::cout << "Index: " << (it - v.begin()) << std::endl; // 输出: Index: 2
  14. }
  15. else
  16. {
  17. std::cout << "Not found" << std::endl;
  18. }
  19. // 查找值为10的元素(不存在)
  20. it = std::find(v.begin(), v.end(), 10);
  21. if (it == v.end())
  22. {
  23. std::cout << "10 not found" << std::endl; // 输出: 10 not found
  24. }
  25. return 0;
  26. }
复制代码

注意:

  • find是线性查找,时间复杂度为O(N)。

  • 对于已排序的序列,可以使用binary_search(二分查找)来获得更快的查找速度(O(log N)),但binary_search只返回是否存在,不返回位置。如果需要位置,可以使用lower_bound。

remove_if

remove_if用于从容器中移除满足特定条件的元素。注意,remove_if并不会真正删除元素,而是将不满足条件的元素移动到容器的前部,并返回一个指向新的逻辑结束位置的迭代器。之后,通常需要调用容器的erase方法来真正删除元素。

  1. template< class ForwardIt, class UnaryPredicate >
  2. ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );
复制代码

参数:

  • first、last:指定要处理的元素范围。

  • p:一元谓词,接受一个元素,返回true则将该元素移除(即移动到后面)。

使用:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  7. // 移除所有偶数
  8. auto new_end = std::remove_if(v.begin(), v.end(), [](int n)
  9. {
  10. return n % 2 == 0;
  11. });
  12. // 此时,v中的元素变为:1,3,5,7,9,?,?,?,?,? (?表示未指定值,通常为原值)
  13. // 注意:remove_if只是将不需要移除的元素移动到前面,并返回新的逻辑结束迭代器
  14. // 物理大小没有改变
  15. // 打印逻辑范围内的元素
  16. for (auto it = v.begin(); it != new_end; ++it)
  17. {
  18. std::cout << *it << " "; // 输出: 1 3 5 7 9
  19. }
  20. std::cout << std::endl;
  21. // 真正删除元素
  22. v.erase(new_end, v.end());
  23. // 现在v的物理大小也改变了
  24. for (int i : v) std::cout << i << " "; // 输出: 1 3 5 7 9
  25. std::cout << std::endl;
  26. return 0;
  27. }
复制代码

注意:

  • remove_if并不会改变容器的大小,只是将不被移除的元素移动到前面,并返回新的逻辑结束位置。要真正删除元素,需要结合erase使用(这种方法称为“erase-remove”惯用法)。

  • 对于链表(如list),有remove_if成员函数,它可以直接删除元素,不需要再调用erase。

  • 时间复杂度为O(N),其中N是容器中元素的数量。

5.3 lambda:结合STL算法使用

  1. [capture] (parameters) -> return_type
  2. {
  3. // 函数体
  4. }
复制代码
  • capture:捕获列表,用于捕获外部变量。可以是值捕获、引用捕获或混合捕获。

  • parameters:参数列表,类似于普通函数的参数列表。

  • return_type:返回类型,可以省略,编译器会根据函数体中的返回语句自动推导。

  • 函数体:与普通函数一样。

捕获列表的几种形式:

  • []:不捕获任何外部变量。

  • [=]:以值捕获的方式捕获所有外部变量。

  • [&]:以引用捕获的方式捕获所有外部变量。

  • [var]:以值捕获的方式捕获特定变量 var。

  • [&var]:以引用捕获的方式捕获特定变量 var。

  • [=, &var]:默认以值捕获所有外部变量,但是以引用捕获 var。

  • [&, var]:默认以引用捕获所有外部变量,但是以值捕获 var。

使用sort进行自定义排序:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6};
  7. // 使用 lambda 表达式按降序排序
  8. std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
  9. return a > b; // 降序
  10. });
  11. for (int num : numbers)
  12. {
  13. std::cout << num << " ";
  14. }
  15. std::cout << std::endl;
  16. return 0;
  17. }
复制代码

使用for_each遍历并修改元素:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> numbers = {1, 2, 3, 4, 5};
  7. // 使用 lambda 表达式将每个元素乘以2
  8. std::for_each(numbers.begin(), numbers.end(), [](int &n) {
  9. n *= 2;
  10. });
  11. for (int num : numbers)
  12. {
  13. std::cout << num << " ";
  14. }
  15. std::cout << std::endl;
  16. return 0;
  17. }
复制代码

for_each遍历容器中的每个元素,并对每个元素执行指定的操作。

  1. template< class InputIt, class UnaryFunction >
  2. UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );
复制代码

参数:

  • first, last:要遍历的范围

  • f:一元函数对象,接受容器中的元素(通过解引用迭代器得到)作为参数,返回值被忽略。

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> nums = {1, 2, 3, 4, 5};
  7. // 使用lambda打印每个元素
  8. std::for_each(nums.begin(), nums.end(), [](int n) {
  9. std::cout << n << " ";
  10. });
  11. // 输出:1 2 3 4 5
  12. // 修改每个元素(注意参数为引用)
  13. std::for_each(nums.begin(), nums.end(), [](int &n) {
  14. n *= 2;
  15. });
  16. // 再次打印
  17. std::for_each(nums.begin(), nums.end(), [](int n) {
  18. std::cout << n << " ";
  19. });
  20. // 输出:2 4 6 8 10
  21. return 0;
  22. }
复制代码
  • for_each可以修改元素,如果lambda参数是引用。

  • 可以使用for_each来执行任何不需要返回值的操作。

使用transform转换容器元素

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main() {
  5. std::vector<int> src = {1, 2, 3, 4, 5};
  6. std::vector<int> dst(src.size());
  7. // 将 src 中的每个元素加1,并存储到 dst 中
  8. std::transform(src.begin(), src.end(), dst.begin(), [](int n) {
  9. return n + 1;
  10. });
  11. for (int num : dst)
  12. {
  13. std::cout << num << " ";
  14. }
  15. std::cout << std::endl;
  16. return 0;
  17. }
复制代码

transform:将函数应用于范围中的每个元素,并将结果存储在另一个范围中(或原地转换)。

  1. // 一元操作版本
  2. template< class InputIt, class OutputIt, class UnaryOperation >
  3. OutputIt transform( InputIt first1, InputIt last1,
  4. OutputIt d_first, UnaryOperation unary_op );
  5. // 二元操作版本
  6. template< class InputIt1, class InputIt2, class OutputIt, class BinaryOperation >
  7. OutputIt transform( InputIt1 first1, InputIt1 last1, InputIt2 first2,
  8. OutputIt d_first, BinaryOperation binary_op );
复制代码

一元操作:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. #include <iterator>
  5. int main()
  6. {
  7. std::vector<int> src = {1, 2, 3, 4, 5};
  8. std::vector<int> dst;
  9. // 将src中每个元素平方,并插入到dst中
  10. std::transform(src.begin(), src.end(), std::back_inserter(dst),
  11. [](int n) { return n * n; });
  12. // 打印dst
  13. std::for_each(dst.begin(), dst.end(), [](int n) { std::cout << n << " "; });
  14. // 输出:1 4 9 16 25
  15. // 原地转换:将src中的每个元素加1
  16. std::transform(src.begin(), src.end(), src.begin(),
  17. [](int n) { return n + 1; });
  18. // 打印src
  19. std::for_each(src.begin(), src.end(), [](int n) { std::cout << n << " "; });
  20. // 输出:2 3 4 5 6
  21. return 0;
  22. }
复制代码

二元操作:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. #include <iterator>
  5. int main()
  6. {
  7. std::vector<int> a = {1, 2, 3, 4, 5};
  8. std::vector<int> b = {10, 20, 30, 40, 50};
  9. std::vector<int> result;
  10. // 将a和b中对应元素相加
  11. std::transform(a.begin(), a.end(), b.begin(),
  12. std::back_inserter(result),
  13. [](int x, int y) { return x + y; });
  14. // 打印result
  15. std::for_each(result.begin(), result.end(),
  16. [](int n) { std::cout << n << " "; });
  17. // 输出:11 22 33 44 55
  18. return 0;
  19. }
复制代码
  • transform需要指定目标位置,可以使用back_inserter来在容器尾部插入,或者确保目标容器有足够空间。

  • 一元操作和二元操作分别用于一个和两个输入序列。

使用find_if查找满足条件的元素

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
  7. // 查找第一个大于3的元素
  8. auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) {
  9. return n > 3;
  10. });
  11. if (it != numbers.end())
  12. {
  13. std::cout << "找到第一个大于3的元素: " << *it << std::endl;
  14. }
  15. else
  16. {
  17. std::cout << "未找到" << std::endl;
  18. }
  19. return 0;
  20. }
复制代码

find_if:在范围内查找第一个满足条件的元素,返回指向该元素的迭代器。

  1. template< class InputIt, class UnaryPredicate >
  2. InputIt find_if( InputIt first, InputIt last, UnaryPredicate p );
复制代码
  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> nums = {1, 2, 3, 4, 5, 6};
  7. // 查找第一个偶数
  8. auto it = std::find_if(nums.begin(), nums.end(),
  9. [](int n) { return n % 2 == 0; });
  10. if (it != nums.end())
  11. {
  12. std::cout << "找到偶数: " << *it << std::endl; // 输出:2
  13. }
  14. else
  15. {
  16. std::cout << "没有找到偶数" << std::endl;
  17. }
  18. // 查找第一个大于10的数
  19. auto it2 = std::find_if(nums.begin(), nums.end(),
  20. [](int n) { return n > 10; });
  21. if (it2 == nums.end())
  22. {
  23. std::cout << "没有找到大于10的数" << std::endl;
  24. }
  25. return 0;
  26. }
复制代码
  • 如果找不到,返回last(即结束迭代器)。

  • 与之类似的算法有find(查找特定值)、find_if_not(查找第一个不满足条件的)。

使用remove_if和erase删除满足条件的元素

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  7. // 删除所有偶数
  8. auto new_end = std::remove_if(numbers.begin(), numbers.end(), [](int n) {
  9. return n % 2 == 0;
  10. });
  11. numbers.erase(new_end, numbers.end());
  12. for (int num : numbers)
  13. {
  14. std::cout << num << " ";
  15. }
  16. std::cout << std::endl;
  17. return 0;
  18. }
复制代码

remove_if:将满足条件的元素移动到容器末尾,并返回一个指向新的逻辑末尾的迭代器。注意,它并不真正删除元素,也不改变容器的大小。 erase:真正从容器中删除元素。

  1. template< class ForwardIt, class UnaryPredicate >
  2. ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );
复制代码
  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int main()
  5. {
  6. std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  7. // 移除所有偶数
  8. auto new_end = std::remove_if(nums.begin(), nums.end(),
  9. [](int n) { return n % 2 == 0; });
  10. // 此时,nums中的元素可能是:1,3,5,7,9,?,?,?,? (?表示未指定,通常是被移除的元素)
  11. // 容器大小没有变,但[begin, new_end)是不包含被移除元素的序列
  12. // 打印[begin, new_end)范围内的元素
  13. std::for_each(nums.begin(), new_end,
  14. [](int n) { std::cout << n << " "; });
  15. std::cout << std::endl; // 输出:1 3 5 7 9
  16. // 真正删除元素,使用erase
  17. nums.erase(new_end, nums.end());
  18. // 现在容器中只有奇数
  19. std::for_each(nums.begin(), nums.end(),
  20. [](int n) { std::cout << n << " "; });
  21. // 输出:1 3 5 7 9
  22. return 0;
  23. }
复制代码
  • remove_if和erase结合使用来真正删除元素,这被称为“erase-remove”惯用法。

  • 对于顺序容器(如vector、deque、string),erase会真正减少容器的大小。

  • 对于list,有remove_if成员函数,它可以直接删除元素,效率更高。

使用accumulate进行累加(需要包含

  1. #include <iostream>
  2. #include <vector>
  3. #include <numeric>
  4. int main()
  5. {
  6. std::vector<int> numbers = {1, 2, 3, 4, 5};
  7. // 使用 lambda 表达式进行累加,初始值为0
  8. int sum = std::accumulate(numbers.begin(), numbers.end(), 0,
  9. [](int acc, int n) {
  10. return acc + n;
  11. });
  12. std::cout << "和为: " << sum << std::endl;
  13. return 0;
  14. }
复制代码

accumulate:计算给定范围内元素的累积值(求和、求积等),可以指定初始值和二元操作函数。

  1. template< class InputIt, class T >
  2. T accumulate( InputIt first, InputIt last, T init );
  3. template< class InputIt, class T, class BinaryOperation >
  4. T accumulate( InputIt first, InputIt last, T init,
  5. BinaryOperation op );
复制代码
  1. #include <iostream>
  2. #include <vector>
  3. #include <numeric> // accumulate在此头文件中
  4. int main()
  5. {
  6. std::vector<int> nums = {1, 2, 3, 4, 5};
  7. // 求和,初始值为0
  8. int sum = std::accumulate(nums.begin(), nums.end(), 0);
  9. std::cout << "和: " << sum << std::endl; // 输出:15
  10. // 求积,初始值为1
  11. int product = std::accumulate(nums.begin(), nums.end(), 1,
  12. [](int a, int b) { return a * b; });
  13. std::cout << "积: " << product << std::endl; // 输出:120
  14. // 拼接字符串
  15. std::vector<std::string> words = {"Hello", " ", "World", "!"};
  16. std::string concat = std::accumulate(words.begin(), words.end(),
  17. std::string(""));
  18. std::cout << concat << std::endl; // 输出:Hello World!
  19. // 使用自定义操作:将向量中的每个元素乘以2然后累加
  20. int double_sum = std::accumulate(nums.begin(), nums.end(), 0,
  21. [](int acc, int n) {
  22. return acc + n * 2;
  23. });
  24. std::cout << "每个元素乘以2后求和: " << double_sum << std::endl; // 输出:30
  25. return 0;
  26. }
复制代码
  • accumulate的初始值类型决定了返回类型,所以要注意选择合适的初始值类型(比如求和时初始值为0,求积时为1)。

  • 二元操作函数接受两个参数:第一个是当前的累积值,第二个是当前元素。

6 现代c++常用特性

6.1 auto类型推导

auto类型推导的基本规则:

  1. auto在推导类型时,会忽略顶层const(即变量本身是常量,但指向的对象可以是常量也可以不是),但会保留底层const(例如指针所指向的对象是常量)。

  2. 当使用auto声明引用时,auto会保留表达式的引用类型和const属性。

  3. 当使用auto声明指针时,auto会推导出指针类型,但也会保留底层const。

auto必须初始化:

  1. auto x; // 错误:必须初始化
  2. auto y = 10; // 正确
复制代码

何时使用auto的最佳实践:

  1. // 推荐使用auto
  2. auto result = calculateValue(); // 类型可能很复杂
  3. for (auto& item : container) // 范围循环
  4. auto ptr = std::make_unique<MyClass>(); // 智能指针
  5. // 可能需要明确类型
  6. int count = 10; // 基本类型更清晰
  7. float pi = 3.14f; // 明确使用float而不是double
复制代码

6.2 范围for循环

使用for(auto& x : vec)时,我们声明了一个引用类型,这意味着:

  1. 我们不会复制容器中的每一个元素,而是直接引用元素本身。

  2. 因此,如果修改x,就会修改容器中对应的元素(因为x是引用)。

  3. 同时,因为避免了拷贝,对于大型对象或容器,使用引用可以提高效率。

但是,如果只是读取元素而不修改,通常使用const引用,即for(const auto& x : vec),以避免不必要的拷贝并且保护元素不被修改。

  1. std::vector<int> vec = {1, 2, 3, 4, 5};
  2. // 1. 引用方式(可以修改元素)
  3. for (auto& x : vec)
  4. {
  5. x *= 2; // 修改原始元素
  6. }
  7. // 2. 常量引用方式(只读,高效)
  8. for (const auto& x : vec)
  9. {
  10. // x *= 2; // 错误:不能修改
  11. std::cout << x << " ";
  12. }
  13. // 3. 值拷贝方式(创建副本,不能修改原始元素)
  14. for (auto x : vec)
  15. {
  16. x = 100; // 只修改副本,不影响vec
  17. }
  18. // 4. 显式指定类型
  19. for (int& x : vec)
  20. { // 明确知道类型时
  21. x += 10;
  22. }
复制代码

6.3 constexpr编译期计算

C++中的constexpr关键字用于指示一个值或函数可以在编译时计算。

constexpr变量:必须在编译时初始化,并且其值必须是编译时常量。

  1. constexpr int size = 10; // 正确,编译时常量
  2. constexpr double pi = 3.1415926; // 正确
  3. int n = 20;
  4. constexpr int sz = n; // 错误,n不是编译时常量
复制代码

constexpr函数:是指能用于常量表达式的函数。在C++11中,constexpr函数只能包含一条return语句,而C++14放宽了限制。

  1. //c++11
  2. constexpr int factorial(int n)
  3. {
  4. return n <= 1 ? 1 : (n * factorial(n - 1));
  5. }
  6. // 使用
  7. constexpr int fact5 = factorial(5); // 编译时计算,结果为120
  8. //c++14
  9. constexpr int factorial(int n)
  10. {
  11. int result = 1;
  12. for (int i = 1; i <= n; ++i)
  13. {
  14. result *= i;
  15. }
  16. return result;
  17. }
  18. constexpr int fact5 = factorial(5); // 编译时计算
复制代码

constexpr函数的参数不一定必须是编译时常量。如果传入非常量参数,则函数在运行时计算。

constexpr指针:常量指针,即指针本身是常量,而不是所指向的对象是常量。

  1. int x = 10;
  2. constexpr int* p = &x; // p是一个常量指针,指向x
  3. // 注意:这里x的地址必须在编译时已知,但x不是constexpr变量,所以&x不是常量表达式,因此实际上这行代码会编译错误。
  4. constexpr int y = 20;
  5. constexpr const int* q = &y; // q是一个指向常量的常量指针,且&y是常量表达式
复制代码

constexpr构造函数:

  1. class Point
  2. {
  3. public:
  4. constexpr Point(double x = 0, double y = 0) : x_(x), y_(y) {}
  5. constexpr double x() const { return x_; }
  6. constexpr double y() const { return y_; }
  7. // C++14允许constexpr函数修改成员变量
  8. constexpr void setX(double x) { x_ = x; }
  9. constexpr void setY(double y) { y_ = y; }
  10. private:
  11. double x_, y_;
  12. };
  13. // 使用
  14. constexpr Point p(1.0, 2.0); // 编译时创建Point对象
  15. constexpr double x = p.x(); // 编译时调用成员函数
复制代码

编译期计算的优势:

  1. 性能提升:将计算从运行时移到编译时,减少运行时间。

  2. 类型安全:编译时计算可以捕获更多错误。

  3. 可用于模板元编程:constexpr函数可以替代部分模板元编程。

注意事项:

  • constexpr变量和函数必须非常简单,通常只能使用其他constexpr变量和函数。

  • constexpr函数可以同时用于编译时和运行时,但编译时必须传入常量表达式参数。

  • 在C++11中,constexpr函数体只能有一条语句,但可以是return语句和条件运算符组成的复杂表达式。

7 拷贝/移动语义

7.1 深拷贝 vs 浅拷贝

浅拷贝(Shallow Copy)

  • 浅拷贝只复制对象的成员变量的值,包括指针的值(即地址),而不复制指针所指向的内存。

  • 这意味着,原对象和拷贝对象中的指针成员将指向同一块内存。

  • 当其中一个对象修改了这块内存,另一个对象也会受到影响。

  • 如果其中一个对象销毁时释放了这块内存,另一个对象的指针就成了悬空指针(dangling pointer),再次访问或释放会导致未定义行为。

深拷贝(Deep Copy)

  • 深拷贝不仅复制对象的成员变量,还会为指针成员分配新的内存,并复制指针所指向的数据。

  • 这样,原对象和拷贝对象拥有各自独立的内存空间,互不影响。

  • 深拷贝通常需要在拷贝构造函数和赋值运算符中显式实现。

注意事项:

  1. 默认的拷贝构造函数和赋值运算符执行的是浅拷贝。

  2. 如果类中有指针成员,并且指向动态分配的内存,通常需要实现深拷贝以避免双重释放和悬空指针的问题。

  3. 在C++中,实现深拷贝时,通常需要同时实现拷贝构造函数、赋值运算符和析构函数(这被称为“三法则”)。在C++11以后,还有移动语义(移动构造函数和移动赋值运算符)需要考虑(“五法则”或“六法则”)。

三法则(Rule of Three)

如果一个类需要显式定义以下其中之一,那么它很可能需要显式定义所有三个:

  • 析构函数

  • 拷贝构造函数

  • 拷贝赋值运算符

五法则(Rule of Five)

在C++11中,由于移动语义的引入,可以扩展为五法则:

  • 析构函数

  • 拷贝构造函数

  • 拷贝赋值运算符

  • 移动构造函数

  • 移动赋值运算符

需要深拷贝的情况:

  1. 类包含原始指针,指向动态分配的内存

  2. 类管理文件句柄网络连接等资源

  3. 需要独立副本,修改副本不影响原对象

不需要深拷贝的情况:

  1. 所有成员都是基本类型(int, double等)

  2. 成员使用智能指针(shared_ptr, unique_ptr)

  3. 对象是不可变

  4. 使用移动语义可以避免拷贝

建议优先使用智能指针,避免手动内存管理,减少深拷贝的必要性;对于资源密集型对象,实现移动构造函数和移动赋值运算符。

7.2 右值引用与move

  • 左值(lvalue):指表达式结束后依然存在的对象,可以取地址。

  • 右值(rvalue):指表达式结束时就不再存在的临时对象,不能取地址。

右值引用使用两个&符号表示,例如int&&。右值引用只能绑定到右值(临时对象)。

  1. int a = 10; // a 是左值
  2. int&& rref = 10; // 10 是右值,rref 是右值引用
  3. int&& rref2 = a; // 错误!不能将左值绑定到右值引用
  4. // 正确:std::move 将左值转换为右值引用
  5. int&& rref3 = std::move(a);
复制代码

移动语义允许将资源(如堆内存)从一个对象转移到另一个对象,从而避免深拷贝。通常通过移动构造函数和移动赋值运算符实现。

移动构造函数接受一个右值引用参数,并“窃取”该参数的资源,然后将该参数置于有效但不确定的状态(通常为空)。

  1.  class MyString
  2.  {
  3.  private:
  4.   char* data;
  5.   size_t length;
  6.  ​
  7.  public:
  8.   // 移动构造函数
  9.   MyString(MyString&& other) noexcept
  10.   : data(other.data), length(other.length)
  11.   {
  12.   other.data = nullptr;
  13.   other.length = 0;
  14.   }
  15.  ​
  16.   // 移动赋值运算符
  17.   MyString& operator=(MyString&& other) noexcept
  18.   {
  19.   if (this != &other)
  20.   {
  21.   delete[] data; // 释放当前资源
  22.   data = other.data;
  23.   length = other.length;
  24.   other.data = nullptr;
  25.   other.length = 0;
  26.   }
  27.   return *this;
  28.   }
  29.  ​
  30.   // 其他成员函数...
  31.  };
复制代码

std::move是一个函数模板,它将左值转换为右值引用,从而允许使用移动语义。注意,使用std::move后,原对象的状态将被改变,因此通常不再使用原对象(除非重新赋值)。

  1.  #include <utility> // std::move
  2.  ​
  3.  std::string str1 = "Hello";
  4.  std::string str2 = std::move(str1); // 移动构造
  5.  ​
  6.  // 此时 str1 变为有效但未指定的状态
  7.  // 通常为空,但不能依赖其内容
复制代码
  • std::move 不移动任何东西

  • 只是将左值转换为右值引用,告诉编译器"这个对象可以被移动"

完美转发允许函数模板将其参数以原有的值类别(左值或右值)转发给其他函数。这通过使用右值引用和std::forward实现。

  1. template<typename T>
  2.  void wrapper(T&& arg)
  3.  {
  4.   // arg 是通用引用(既可以绑定左值也可以绑定右值)
  5.   // std::forward 保持参数原有的值类别
  6.   process(std::forward<T>(arg));
  7.  }
  8.  ​
  9.  void process(int& x)
  10.  {
  11.   std::cout << "左值: " << x << std::endl;
  12.  }
  13.  ​
  14.  void process(int&& x)
  15.  {
  16.   std::cout << "右值: " << x << std::endl;
  17.  }
  18.  ​
  19.  int main()
  20.  {
  21.   int a = 10;
  22.   wrapper(a); // 调用左值版本
  23.   wrapper(20); // 调用右值版本
  24.   wrapper(std::move(a)); // 调用右值版本
  25.  }
复制代码
  • 移动后的对象处于有效但不确定的状态,通常可以重新赋值或析构。

  • 对于内置类型,移动和拷贝相同,没有性能提升。

  • 移动操作通常应标记为noexcept,以允许标准库容器在重新分配内存时使用移动而非拷贝(例如std::vector)。

注意:

  • 使用noexcept标记移动操作,确保不会抛出异常

  • 移动后对象应处于有效状态:通常为空或者默认状态

  • 不要过度使用move

  • 在需要资源转移时使用移动语义

  • 实现移动构造函数后,应考虑删除拷贝操作(如果需要)

7.3 移动构造函数

移动构造函数是C++11引入的一种特殊构造函数,用于将资源从一个对象移动到另一个对象,而不是进行深拷贝。移动构造函数通常接受一个右值引用作为参数,并且会“窃取”参数中的资源,然后将参数置于有效但不可用的状态(通常为空)。

  1. class MyClass
  2. {
  3. public:
  4. // 移动构造函数
  5. MyClass(MyClass&& other) noexcept
  6. {
  7. // 转移资源
  8. data = other.data;
  9. size = other.size;
  10. // 将原对象置于有效状态(通常为空)
  11. other.data = nullptr;
  12. other.size = 0;
  13. }
  14. };
复制代码

移动构造函数的几个要点:

  1. 参数类型:必须是同类型的右值引用,即 MyClass&&。

  2. 通常不会抛出异常,因此应该用 noexcept 修饰,这对于标准库容器在重新分配内存时使用移动而非拷贝很重要。

  3. 移动构造函数会“窃取”源对象的资源,然后将源对象的资源指针置为空(或其他合适的状态),这样源对象的析构函数就不会释放这些资源了。

移动构造函数的使用场景:

  • 当用一个临时对象初始化另一个对象时。

  • 当使用 std::move 将一个左值转换为右值,然后用于初始化时。

移动构造函数与拷贝构造函数的区别:

  • 拷贝构造函数会复制资源,两个对象独立。

  • 移动构造函数只是转移资源,移动后源对象不再拥有资源。

如果类没有定义移动构造函数,编译器可能会自动生成一个(在某些条件下),但自动生成的移动构造函数只是简单地移动每个成员变量(对于内置类型就是拷贝,对于类类型则调用其移动构造函数)。如果类有自定义的拷贝构造函数、拷贝赋值运算符或析构函数,编译器不会自动生成移动构造函数。这时,如果需要移动操作,就必须自己定义。

8 异常安全与设计模式

8.1 try_catch正确用法(不滥用)

避免滥用的准则:

  • 不要用异常替代正常控制流

  • 不要捕获所有异常而不处理

  • 最小化try块范围

应该使用异常的情况:

  • 构造函数失败

  • 资源获取失败(文件、网格、内存等)

  • 程序状态无法恢复的错误

  • 库/API的边界处

不应该使用异常的情况:

  • 正常的控制流程

  • 频繁发生的错误(使用返回值)

  • 性能关键的代码路径

  • 析构函数(通常)

  • 信号处理程序

8.2 noexcept关键字

  • noexcept:表示函数不会抛出任何异常。

  • noexcept(expression):如果expression为true,则函数不会抛出异常;如果为false,则可能抛出异常。

  1. void myFunction() noexcept; // 保证不抛出异常
  2. void anotherFunction() noexcept(true); // 同上,明确指定不抛出
  3. void yetAnotherFunction() noexcept(false); // 可能抛出异常
复制代码

为什么使用 noexcept:

  1. 性能优化:编译器知道函数不会抛出异常后,可以生成更高效的代码,因为不需要处理异常抛出的情况。

  1. // 编译器可以为noexcept函数生成更高效的代码
  2. void optimizedFunction() noexcept
  3. {
  4. // 编译器知道不会抛出异常,可以:
  5. // 1. 减少异常处理表的生成
  6. // 2. 进行更多内联优化
  7. // 3. 减少栈展开代码
  8. for (int i = 0; i < 1000; ++i)
  9. {
  10. // 密集计算
  11. }
  12. }
复制代码
  1. 移动操作和析构函数:标准库中的一些操作(如std::vector的重新分配)会使用移动操作(如果它们被标记为noexcept),否则会使用拷贝操作。因此,将移动构造函数和移动赋值运算符标记为noexcept可以提高性能。

  2. 代码清晰性:明确告诉其他开发者该函数不会抛出异常,简化错误处理。

注意事项:

  1. 违反noexcept说明:如果函数被标记为noexcept但抛出了异常,程序会调用std::terminate()终止。因此,确保noexcept函数真的不会抛出异常。

  2. 析构函数:析构函数默认是noexcept的,除非显式指定为noexcept(false)。通常,析构函数不应该抛出异常。

  3. 移动操作:如果移动操作可能抛出异常,那么它不应该被标记为noexcept,因为异常安全很难保证。

应该使用noexcept的情况:

  • 析构函数(默认就是)

  • 移动构造函数和移动赋值运算符

  • 交换(swap)操作

  • 简单访问器和修改器(getter/setter)

  • 数学计算和纯函数

  • 内存操作(如memcpy的包装)

不应该使用noexcept的情况:

  • 可能失败的资源获取(文件、网络、内存)

  • 可能抛出异常的算法

  • 用户提供的回调函数

  • 复杂业务逻辑函数

与throw()的区别: 在C++11之前,使用throw()来指定函数不抛出异常。但是,如果函数抛出了异常,会调用std::unexpected(),这通常会导致程序终止。C++11中,noexcept更高效,因为编译器不需要为noexcept函数准备异常传播的代码。throw()在C++11中被废弃,在C++17中被移除。为了兼容,C++11中noexcept等同于noexcept(true),而throw()等同于noexcept(false)。

8.3 接口设计原则(Pimpl模式隐藏细节)

在C++中,Pimpl(Pointer to Implementation,指向实现的指针)模式是一种用来隐藏类实现细节的技术。它通过将类的实现细节(私有成员)移动到一个单独的类中,并在原类中仅保留一个指向该实现类的指针来实现。这样,原类的头文件就只需要包含实现类的声明,而不需要包含实现类所需的任何头文件,从而减少了编译依赖,加快了编译速度,并且隐藏了实现细节。

Pimpl模式的基本步骤:

  1. 在头文件中声明一个实现类(通常是一个嵌套类,但也可以是非嵌套的)的前向声明,然后声明一个指向该实现类的指针(通常使用智能指针,如std::unique_ptr)。

  2. 在源文件中定义实现类,并实现原类的成员函数,这些成员函数通过操作实现类来完成功能。

不使用pimpl模式时:

  1. // widget.h
  2. #include <string>
  3. #include <vector>
  4. class Widget
  5. {
  6. public:
  7. Widget();
  8. ~Widget();
  9. void doSomething();
  10. private:
  11. std::string name;
  12. std::vector<int> data;
  13. // 其他私有成员,可能涉及更多的头文件包含
  14. };
复制代码

使用pimpl模式后:

  1. // widget.h
  2. #include <memory>
  3. class Widget
  4. {
  5. public:
  6. Widget();
  7. ~Widget(); // 需要声明,因为std::unique_ptr需要知道析构函数
  8. Widget(Widget&&) = default; // 移动构造
  9. Widget& operator=(Widget&&) = default; // 移动赋值
  10. // 注意:需要禁止拷贝构造和拷贝赋值,或者实现深拷贝(因为unique_ptr不可拷贝)
  11. Widget(const Widget&) = delete;
  12. Widget& operator=(const Widget&) = delete;
  13. void doSomething();
  14. private:
  15. class Impl; // 前向声明
  16. std::unique_ptr<Impl> pImpl;
  17. };
  18. // widget.cpp
  19. #include "widget.h"
  20. #include <string>
  21. #include <vector>
  22. //在源文件中定义Impl类并实现Widget的成员函数
  23. class Widget::Impl
  24. {
  25. public:
  26. std::string name;
  27. std::vector<int> data;
  28. // 其他私有成员
  29. };
  30. Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
  31. // 必须定义析构函数,因为Impl是不完整类型,而unique_ptr的默认析构需要完整类型
  32. // 但是,如果使用std::unique_ptr,我们需要在Widget的析构函数之前知道Impl的完整类型
  33. // 因此,在widget.cpp中,Impl已经定义,所以析构函数可以正常编译。
  34. Widget::~Widget() = default;
  35. void Widget::doSomething()
  36. {
  37. // 通过pImpl访问实现
  38. pImpl->name = "example";
  39. pImpl->data.push_back(42);
  40. }
复制代码

注意:由于使用了std::unique_ptr,我们需要在头文件中声明析构函数,并在源文件中实现(即使它是默认的)。这是因为std::unique_ptr的析构函数需要知道Impl的完整类型,而在头文件中,Impl只是一个前向声明,所以编译器无法生成默认的析构函数。因此,我们必须在Impl类定义之后(即在源文件中)定义析构函数。

另外,需要注意的是,由于使用了std::unique_ptr,Widget类默认是不支持拷贝的(因为std::unique_ptr不可拷贝)。如果需要支持拷贝,需要手动实现拷贝构造函数和拷贝赋值运算符,对Impl进行深拷贝。

Pimpl模式的优点:

  1. 隐藏实现细节,减少头文件暴露的信息。

  2. 减少编译依赖,提高编译速度。当头文件发生变化时,只有直接依赖它的文件需要重新编译。

  3. 接口与实现分离,提高封装性。

缺点:

  1. 代码变得复杂,需要额外的指针间接访问,可能带来性能上的微小开销。

  2. 内存分配和释放开销(可以使用自定义分配器来优化)。

  3. 调试可能变得更困难,因为实现细节被隐藏。

9 并发与内存模型

9.1 thread与async

std::thread是C++11引入的线程库的一部分,它代表一个单独的执行线程。创建std::thread对象时,会启动一个新的线程来执行给定的函数或可调用对象。

  1. #include <iostream>
  2. #include <thread>
  3. void hello()
  4. {
  5. std::cout << "Hello from thread!" << std::endl;
  6. }
  7. int main()
  8. {
  9. std::thread t(hello);
  10. t.join(); // 等待线程结束
  11. return 0;
  12. }
复制代码
  1. #include <iostream>
  2. #include <thread>
  3. void compute(int& result)
  4. {
  5. result = 42;
  6. }
  7. int main()
  8. {
  9. int result = 0;
  10. std::thread t(compute, std::ref(result));//通过引用传递来获取结果,并且需要手动同步(这里使用了join,所以没有问题)
  11. t.join();
  12. std::cout << "Result: " << result << std::endl;
  13. return 0;
  14. }
复制代码

特点:

  • 直接管理线程的生命周期。

  • 需要显式地决定是等待线程完成(join)还是分离线程(detach)。

  • 如果既不join也不detach,在析构时会调用std::terminate。

  • 线程间的数据共享需要手动同步(使用互斥锁等)

std::async是一个更高层次的抽象,它用于异步执行任务,并返回一个std::future对象,该对象最终将持有函数的返回值。std::async可以选择立即启动线程(异步执行)或者延迟执行(直到future被请求结果时才执行)

  1. #include <iostream>
  2. #include <future>
  3. int compute()
  4. {
  5. return 42;
  6. }
  7. int main()
  8. {
  9. std::future<int> result = std::async(std::launch::async, compute);//通过future直接获取返回值,更加简洁安全
  10. std::cout << "Result: " << result.get() << std::endl;
  11. return 0;
  12. }
复制代码

启动策略:

  • std::launch::async:立即异步执行。

  • std::launch::deferred:延迟执行,直到在future上调用get()或wait()。

  • 默认策略(不指定)可能是两者的组合,由实现决定。

总结:

  • 选择 std::thread 当需要直接控制线程,处理I/O密集型长时间运行的任务

  • 选择 std::async 当需要获取计算结果,处理计算密集型的短期任务,或想简化并发代码

注意:

  1. 使用std::thread时,要确保线程在结束前被正确join或detach,否则程序会终止。

  2. 使用std::async时,要注意默认启动策略可能不是立即异步执行,如果需要确保异步,显式指定std::launch::async。

  3. std::async返回的future在析构时会阻塞等待任务完成(如果任务是异步执行的),这有时会导致意外的阻塞。如果不需要结果,可以存储future对象以避免阻塞。

9.2 互斥锁

互斥锁用于保护共享数据免受多个线程同时访问,从而避免数据竞争和不一致性。

互斥锁类型:

  1. std::mutex:最基本的互斥锁,不可递归,即同一线程不可重复锁定。

  2. std::recursive_mutex:递归互斥锁,同一线程可以多次锁定,但需要相同次数的解锁。

  3. std::timed_mutex:带超时功能的互斥锁,允许尝试锁定一段时间或直到某个时间点。

  4. std::recursive_timed_mutex:带超时功能的递归互斥锁。

锁定方法:

  • lock():锁定互斥锁,如果已被锁定则阻塞。

  • try_lock():尝试锁定互斥锁,如果已被锁定则立即返回false,否则锁定并返回true。

  • unlock():解锁互斥锁。

互斥锁管理类(RAII):

为了避免忘记解锁互斥锁,C++提供了几种基于RAII的类,在构造时锁定互斥锁,在析构时自动解锁。

  1. std::lock_guard:简单的RAII互斥锁包装器,构造时锁定,析构时解锁。不提供手动锁定和解锁接口。

  2. std::unique_lock:更灵活的RAII互斥锁包装器,允许延迟锁定、提前解锁以及转移所有权。它还可以与条件变量一起使用。

  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. std::mutex mtx;
  5. int shared_data = 0;
  6. void increment()
  7. {
  8. for (int i = 0; i < 100000; ++i)
  9. {
  10. std::lock_guard<std::mutex> lock(mtx); // 构造时锁定,析构时解锁
  11. ++shared_data;
  12. }
  13. }
  14. int main()
  15. {
  16. std::thread t1(increment);
  17. std::thread t2(increment);
  18. t1.join();
  19. t2.join();
  20. std::cout << "shared_data = " << shared_data << std::endl;
  21. return 0;
  22. }
复制代码
  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. std::mutex mtx;
  5. int shared_data = 0;
  6. void increment()
  7. {
  8. for (int i = 0; i < 100000; ++i)
  9. {
  10. std::unique_lock<std::mutex> lock(mtx); // 类似于lock_guard,但更灵活
  11. ++shared_data;
  12. // 可以提前解锁
  13. lock.unlock();
  14. // 做一些不需要互斥锁的操作
  15. }
  16. }
  17. int main()
  18. {
  19. std::thread t1(increment);
  20. std::thread t2(increment);
  21. t1.join();
  22. t2.join();
  23. std::cout << "shared_data = " << shared_data << std::endl;
  24. return 0;
  25. }
复制代码
  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. std::mutex mtx;
  5. void try_lock_example(int id)
  6. {
  7. if (mtx.try_lock())
  8. {
  9. std::cout << "Thread " << id << " locked successfully.\n";
  10. // 模拟一些工作
  11. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  12. mtx.unlock();
  13. }
  14. else
  15. {
  16. std::cout << "Thread " << id << " failed to lock.\n";
  17. }
  18. }
  19. int main()
  20. {
  21. std::thread t1(try_lock_example, 1);
  22. std::thread t2(try_lock_example, 2);
  23. t1.join();
  24. t2.join();
  25. return 0;
  26. }
复制代码
  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. #include <chrono>
  5. std::timed_mutex mtx;
  6. void timed_lock_example(int id)
  7. {
  8. // 尝试锁定最多100毫秒
  9. if (mtx.try_lock_for(std::chrono::milliseconds(100)))
  10. {
  11. std::cout << "Thread " << id << " locked successfully.\n";
  12. std::this_thread::sleep_for(std::chrono::milliseconds(200));
  13. mtx.unlock();
  14. }
  15. else
  16. {
  17. std::cout << "Thread " << id << " failed to lock within 100ms.\n";
  18. }
  19. }
  20. int main()
  21. {
  22. std::thread t1(timed_lock_example, 1);
  23. std::thread t2(timed_lock_example, 2);
  24. t1.join();
  25. t2.join();
  26. return 0;
  27. }
复制代码

互斥锁与条件变量(std::condition_variable)一起使用,条件变量允许线程等待某个条件成立。

  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. #include <condition_variable>
  5. std::mutex mtx;
  6. std::condition_variable cv;
  7. bool ready = false;
  8. void worker_thread()
  9. {
  10. std::unique_lock<std::mutex> lock(mtx);
  11. cv.wait(lock, []{ return ready; }); // 等待ready为true
  12. std::cout << "Worker thread is processing...\n";
  13. }
  14. int main()
  15. {
  16. std::thread worker(worker_thread);
  17. // 模拟一些准备工作
  18. std::this_thread::sleep_for(std::chrono::seconds(1));
  19. {
  20. std::lock_guard<std::mutex> lock(mtx);
  21. ready = true;
  22. }
  23. cv.notify_one(); // 通知等待的线程
  24. worker.join();
  25. return 0;
  26. }
复制代码

9.3 死锁预防

要发生死锁,必须同时满足以下四个条件:

  1. 互斥条件:资源不能被共享,只能独占使用

  2. 持有并等待:线程持有资源并等待其他资源

  3. 不可抢占:资源不能被强制剥夺

  4. 循环等待:存在线程的循环等待链

死锁预防是并发编程中的重要课题。在实际编程中,最常用且有效的方法是:

  • 固定顺序加锁:简单有效,但有时难以维护顺序。

  • 使用std::lock同时锁定:C++标准库提供的工具,非常方便。

  • 使用超时锁:避免永久等待,但可能导致活锁(线程一直在重试)。

避免死锁的关键原则:关键原则

  1. 最少锁原则:只在必要时使用锁

  2. 最短时间原则:持有锁的时间尽可能短

  3. 固定顺序原则:始终按相同顺序获取锁

  4. 单层锁定原则:避免嵌套锁

  5. 超时退避原则:为锁操作设置超时

9.4 原子操作

原子操作是并发编程中的一个核心概念,它指的是一个或多个操作的执行是不可中断的,即这些操作要么全部执行完成,要么都不执行,并且不会出现部分执行的状态。原子操作在多线程环境中尤为重要,因为它们可以用于实现无锁数据结构、计数器等,避免使用互斥锁带来的开销和潜在的死锁问题。

创建原子对象:

  1. #include <atomic>
  2. // 基本整型原子类型
  3. std::atomic<int> atomic_int;
  4. std::atomic<long> atomic_long;
  5. std::atomic<bool> atomic_bool;
  6. std::atomic<char> atomic_char;
  7. // 浮点型原子类型(C++20)
  8. std::atomic<float> atomic_float;
  9. std::atomic<double> atomic_double;
  10. // 指针原子类型
  11. std::atomic<int*> atomic_ptr;
  12. // 用户自定义类型(需要满足一定条件)
  13. struct Point
  14. {
  15. int x, y;
  16. };
  17. std::atomic<Point> atomic_point;
复制代码

基本操作接口:

  1. std::atomic<int> value(0);
  2. // 基本操作
  3. value.store(42); // 原子存储
  4. int x = value.load(); // 原子加载
  5. int old = value.exchange(100); // 原子交换
  6. // 算术运算
  7. int prev = value.fetch_add(5); // 原子加
  8. prev = value.fetch_sub(3); // 原子减
  9. prev = value.fetch_and(0xFF); // 原子与
  10. prev = value.fetch_or(0x01); // 原子或
  11. prev = value.fetch_xor(0x0F); // 原子异或
  12. // 比较交换操作(CAS - Compare And Swap)
  13. int expected = 50;
  14. bool success = value.compare_exchange_weak(expected, 60);
  15. // 如果 value == expected,则设置为60,否则 expected 被更新为当前值
复制代码

9.4.1 内存顺序

宽松顺序:

在线程1中,操作A和操作B是两个独立的原子操作,它们之间没有依赖关系。在宽松内存顺序下,编译器和CPU可能会重排这两个操作,即先执行操作B,再执行操作A。

在线程2中,操作C和操作D也是两个独立的原子操作,但是操作C循环等待y变为1,所以操作D必须在操作C完成后执行。

  1. std::atomic<int> x(0), y(0);
  2. int r1, r2;
  3. // 线程1
  4. x.store(1, std::memory_order_relaxed);
  5. y.store(1, std::memory_order_relaxed);
  6. // 线程2
  7. while (y.load(std::memory_order_relaxed) != 1) {}
  8. r1 = x.load(std::memory_order_relaxed);
  9. // 问题:r1 可能为0吗?是的!因为宽松顺序不保证可见性顺序
复制代码

获取-释放顺序:

获取-释放顺序用于在同一个原子变量上建立同步关系。一个线程使用 release 语义存储一个值,另一个线程使用 acquire 语义加载该值,那么第一个线程中在 store 之前的所有写操作,在第二个线程中 load 之后都是可见的。

  1. std::atomic<int> flag(0);
  2. int data = 0;
  3. // 线程1:生产者
  4. data = 42; // 1
  5. flag.store(1, std::memory_order_release); // 2
  6. // 线程2:消费者
  7. while (flag.load(std::memory_order_acquire) == 0) {} // 3
  8. int result = data; // 4,这里保证看到 data = 42
  9. // 关键保证:1 happens-before 2 synchronizes-with 3 happens-before 4
复制代码

顺序一致性:

顺序一致性保证了所有线程看到的操作顺序是一致的,且与代码顺序一致。但是,它也是性能开销最大的一种。

  1. std::atomic<int> x(0), y(0);
  2. int r1, r2;
  3. // 线程1
  4. x.store(1, std::memory_order_seq_cst); // A
  5. r1 = y.load(std::memory_order_seq_cst); // B
  6. // 线程2
  7. y.store(1, std::memory_order_seq_cst); // C
  8. r2 = x.load(std::memory_order_seq_cst); // D
  9. // 保证:不可能出现 r1 == 0 && r2 == 0
  10. // 所有seq_cst操作构成一个全序
复制代码

9.4.2 比较交换(CAS)

比较并交换(Compare and Swap)是原子操作中非常重要的一种操作,它用于实现无锁数据结构。CAS 操作会检查原子对象的当前值是否等于期望值,如果相等,则将其设置为新值。

C++ 提供了两个版本的 CAS:compare_exchange_weak 和 compare_exchange_strong。

  • compare_exchange_weak:可能会虚假失败(即即使当前值等于期望值,也可能返回 false),但性能可能更好。通常用在循环中。

  • compare_exchange_strong:不会虚假失败,但可能性能稍差。

  1. std::atomic<int> value(10);
  2. int expected = 10;
  3. bool success = value.compare_exchange_strong(expected, 20);
  4. // 如果 value 当前为10,则将其设置为20,success 为 true,expected 不变(仍为10)
  5. // 否则,将 expected 更新为 value 的当前值,success 为 false
复制代码
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行