目录
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 基础数据类型范围与精度
整数类型
有符号整数
类型 字节大小 范围 说明 char 1 -128 ~ 127 可能等价于signed char signed char 1 -128 ~ 127 short 2 -32,768 ~ 32,767 int 4(通常) -2,147,483,648 ~ 2,147,483,647 至少2字节,平台相关 long 4或8 -2³¹ ~ 2³¹-1 或 -2⁶³ ~ 2⁶³-1 平台相关 long long 8 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 C++11引入
无符号整数
类型 字节大小 范围 说明 unsigned char 1 0 ~ 255 unsigned short 2 0 ~ 65,535 unsigned int 4(通常) 0 ~ 4,294,967,295 unsigned long 4或8 0 ~ 2³²-1 或 0 ~ 2⁶⁴-1 unsigned long long 8 0 ~ 18,446,744,073,709,551,615
浮点类型
类型 字节大小 精度(小数位) 范围(绝对值) 说明 float 4 6-7位 1.2e-38 ~ 3.4e38 单精度 double 8 15-16位 2.3e-308 ~ 1.7e308 双精度 long double 8,12,16 18-19位以上 更大范围 平台相关
布尔与字符类型
类型 字节大小 范围/值 说明 bool 1(通常) true 或 false 理论上1位,但占用1字节 char 1 -128~127 或 0~255 可能是有符号或无符号 wchar_t 2或4 0~65,535 或 更大 宽字符 char16_t 2 0~65,535 UTF-16字符(C++11) char32_t 4 0~4,294,967,295 UTF-32字符(C++11)
2.2 作用域与生命周期
作用域
作用域是指程序中标识符(如变量、函数、类型等)的可见区域。在C++中,主要有以下几种作用域:
局部作用域(块作用域)
void func() { int x = 10; // 局部变量,作用域开始 { int y = 20; // 在这个内层块中声明,只在这个内层块中可见 cout << x << endl; // 可以访问x } // cout << y << endl; // 错误,y在这里不可见 } // x的作用域结束 复制代码
2. 函数作用域
void func() { start: // 标签在整个函数内可见 // ... goto start; } 复制代码
3. 函数原型作用域
void func(int x, int y); // x和y的作用域仅限于此原型,在函数定义中可以使用不同的名字 复制代码
4. 文件作用域(全局作用域)
在所有函数和类之外声明的标识符具有文件作用域,也称为全局作用域。
它们从声明点开始,直到文件末尾都可见,并且可以被同一文件中的其他函数访问。
如果使用extern关键字进行声明,则可以在多个文件之间共享。
int globalVar = 100; // 全局变量,文件作用域 void func1() { globalVar = 200; // 可以访问全局变量 } int main() { cout << globalVar << endl; // 输出200(如果func1被调用过) return 0; } 复制代码
5. 类作用域
class MyClass { private: int data; // 类作用域 public: void setData(int d) { data = d; // 在成员函数中可以直接访问类的成员 } }; int main() { MyClass obj; // cout << data; // 错误,不能直接访问类成员 obj.setData(10); // 通过对象调用成员函数 return 0; } 复制代码
6. 命名空间作用域
namespace MyNamespace { int value = 42; } int main() { cout << MyNamespace::value << endl; // 通过命名空间访问 using MyNamespace::value; cout << value << endl; // 可以直接访问 return 0; } 复制代码
生命周期
生命周期是指标识符(特别是变量和对象)存在的时间段,即从创建到销毁的时间。生命周期与存储类别密切相关。
1. 自动生命周期(局部变量)
void func() { int x = 5; // 生命周期开始 // 使用x } // 生命周期结束,x被销毁 复制代码
2. 静态生命周期
int globalVar; // 静态生命周期,程序开始时创建,程序结束时销毁 void func() { static int count = 0; // 静态局部变量,第一次进入函数时创建,程序结束时销毁 count++; } class MyClass { static int staticVar; // 静态成员变量,具有静态生命周期 }; 复制代码
3. 动态生命周期
int main() { int* p = new int(10); // 动态生命周期开始 // 使用p delete p; // 动态生命周期结束,释放内存 return 0; } 复制代码
4. 线程局部生命周期
thread_local int tlsVar = 0; // 每个线程都有自己的tlsVar 复制代码
2.3 const的全面用法
定义常量变量
使用 const 定义常量变量,必须在定义时初始化,且之后不能修改。
const int a = 10; a = 20; // 错误:不能修改常量 复制代码
指针与const
指向常量的指针:指针指向的内容是常量,不能通过指针修改内容,但指针本身可以指向其他地址。
const int* p; // 或者 int const* p; int b = 5; p = &b; *p = 10; // 错误:不能通过p修改b的值 复制代码
常量指针:指针本身是常量,初始化后不能指向其他地址,但可以通过指针修改所指向的内容(除非指向的是常量)。
int* const p = &b; *p = 20; // 正确:可以修改b的值 int c = 30; p = &c; // 错误:不能修改指针的指向 复制代码
指向常量的常量指针:指针本身和指向的内容都是常量。
const int* const p = &b; // *p = 30; // 错误:不能修改指向的内容 // p = &c; // 错误:不能修改指针的指向 复制代码
函数与const
常量参数:函数参数使用 const,表示在函数内部不会修改该参数。通常用于指针或引用参数,以保护原始数据。
void func(const int* p) { // *p = 10; // 错误:不能修改 } 复制代码
常量引用:常量引用常用于函数参数,避免拷贝,同时不允许修改原始数据。
void func2(const int& x) { // x = 10; // 错误:不能修改 } 复制代码
常量成员函数:在成员函数后加 const,表示该函数不会修改类的成员变量(除非成员变量被声明为 mutable)。
class MyClass { int value; public: int getValue() const { // value = 10; // 错误:不能修改成员变量 return value; } }; 复制代码
常量返回值:函数返回常量值,通常用于返回指针或引用,防止返回值被修改。
const int& getRef(const int& a) { return a; } // 调用 int x = 5; const int& y = getRef(x); // y = 10; // 错误:不能通过y修改 复制代码
类与const
常量成员变量:类的常量成员变量必须在构造函数的初始化列表中初始化。
class MyClass { const int constMember; public: MyClass(int val) : constMember(val) {} }; 复制代码
常量对象:常量对象只能调用常量成员函数。
const MyClass obj(10); // obj.getValue(); // 正确,getValue是常量成员函数 // 如果有一个非常量成员函数,则常量对象不能调用 复制代码
修饰函数参数中的指针或引用:如上所述,可以防止函数内部修改指针或引用所指向的数据。
在类型转换中的使用:const_cast 可以用于去除常量性,但必须谨慎使用,因为修改原本是常量的对象会导致未定义行为。
const int a = 10; int* p = const_cast<int*>(&a); *p = 20; // 未定义行为,因为a原本是常量 复制代码
与#define宏常量的区别
const 有类型检查,更安全。
const 常量在编译时确定,可以调试。
#define 是文本替换,无类型,容易出错。
在模板中的应用:在模板中,const 可以用于模板参数,表示常量类型。
template<typename T> const T& max(const T& a, const T& b) { return a > b ? a : b; } 复制代码
修饰lambda表达式:C++11 以后,lambda表达式可以指定为常量(默认情况下,即使捕获列表为值捕获,lambda的函数调用运算符也是可变的)。但是,如果lambda被声明为const,则不能修改捕获的变量(即使是以值方式捕获的非mutable lambda)。
实际上,在C++11中,lambda表达式默认是不可修改捕获的变量的,除非使用mutable关键字。而C++17引入了constexpr lambda
int x = 10; auto lambda = [x]() const { // 注意:这个语法在C++11/14中并不直接支持,实际上lambda的常量性是通过mutable来控制的 // x = 5; // 错误:不能修改捕获的变量 return x; }; 复制代码
修饰类类型的对象:如果类类型的对象被声明为const,那么只能调用其const成员函数。
2 指针与引用
2.1 内存布局
C++的内存布局通常可以分为以下几个部分:
栈(Stack):用于存储局部变量、函数参数和函数调用的上下文。栈由编译器自动管理,通过压栈和出栈操作进行分配和释放。栈内存的分配是连续的,通常有限,但访问速度快。
void func() { int x = 10; // x在栈上 char buffer[100]; // buffer在栈上 // 函数结束时自动释放 } 复制代码
堆(Heap):用于动态内存分配。堆内存由程序员手动管理(在C++中,使用new和delete或malloc和free)。堆内存的分配不是连续的,需要更复杂的管理,因此访问速度相对较慢,但大小只受系统内存限制。
特点 :手动分配和释放,由程序员管理
存储内容 :
大小限制 :受系统内存限制
访问速度 :相对较慢
注意事项 :容易产生内存泄漏
int* p = new int(10); // 在堆上分配 delete p; // 必须手动释放 复制代码
全局/静态存储区:用于存储全局变量和静态变量(包括静态局部变量和静态成员变量)。这部分内存在程序启动时分配,在程序结束时释放。它又可以分为:
int globalVar = 100; // 全局变量 - 数据段 static int staticVar = 200; // 静态变量 - 数据段 const char* str = "Hello"; // 常量字符串 - 只读数据段 复制代码
常量存储区:用于存储常量,如字符串常量和其他常量。这部分内存通常是只读的。
代码区:用于存储程序的执行代码(机器指令)。
存储内容 :
可执行代码
常量(const变量,取决于定义位置)
特点 :只读,共享内存
const int MAX_SIZE = 100; // 可能存储在代码区或只读数据段 复制代码 高地址 ┌─────────────────┐ │ 栈区 │ ← 向下增长 ├─────────────────┤ │ 堆区 │ ← 向上增长 ├─────────────────┤ │ BSS段(未初始化) │ ├─────────────────┤ │ 数据段(已初始化) │ ├─────────────────┤ │ 代码区 │ └─────────────────┘ 低地址 复制代码
2.2 指针与引用的本质区别及使用场景
本质区别:
使用上的区别:
指针的使用场景:
引用的使用场景:
函数参数:希望函数内修改传入的变量,并且不希望传递空值。引用传递避免了拷贝,同时保证了绑定对象的存在。
函数返回值:返回引用可以避免返回时的拷贝,但必须注意不能返回局部变量的引用(因为局部变量在函数结束后被销毁)。通常用于返回类成员、静态变量或传入的引用参数等。
操作符重载:比如流操作符<<和>>,赋值操作符=等,通常返回引用以支持链式调用
别名:为一个复杂的表达式或变量起一个别名,使代码更清晰
注意事项:
引用在底层通常是通过指针实现的,但语言层面隐藏了这一细节。
指针可以有多级(如指向指针的指针),而引用只有一级。
指针可以参与算术运算(如指针加减),而引用不能。
使用指针时需要注意内存管理,避免内存泄漏和野指针。
引用更安全,但灵活性不如指针。
2.3 理解空指针与野指针
空指针是指不指向任何有效对象或函数的指针。在C++11之前,我们通常用NULL(实际上是0)或者0来表示空指针。从C++11开始,引入了nullptr关键字,用来表示空指针,它是空指针常量,具有明确的类型(std::nullptr_t)。(如果是c++11标准之后,不建议使用NULL,因为0是整数,不是指针类型)
野指针是指向无效内存地址的指针。这种指针不是空指针,但它指向的内存可能已经被释放或者从未分配。使用野指针会导致未定义行为,通常是程序崩溃或数据损坏。
野指针的产生原因:
如何避免野指针:
3 面向对象与设计模式
3.1 OOP:封装、继承、多态(虚函数表原理)
封装是将数据(变量)和操作数据的函数(方法)组合在一起,形成一个类(class)。同时,通过访问修饰符(public、private、protected)来控制对类内部数据和函数的访问权限,从而隐藏内部实现细节,只暴露必要的接口。
封装的优点:
继承允许我们定义一个类(称为派生类或者子类)基于另一个类(称为基类或父类)。派生类继承基类的成员(属性和方法),并可以添加自己的新成员或重写基类的方法。继承实现了代码的重用和层次关系。
继承的类型:
公有继承:基类的公有成员在派生类中保持公有,保护成员保持保护
保护继承:基类的公有和保护成员在派生类中都变为保护成员
私有继承:基类的公有和保护成员在派生类中变为私有成员
private:完全私有,只能在本类中访问
protected:可在本类和派生类中访问
public:完全公开,任何地方都可访问
继承中的访问控制:
基类成员访问权限 公有继承 保护继承 私有继承 public public protected private protected protected protected private private 不可访问 不可访问 不可访问
用虚继承的方式解决菱形继承问题:三层继承,中层使用虚继承
class Animal { public: void breathe() { cout << "Breathing\n"; } }; // 虚继承 class Mammal : virtual public Animal { }; class WingedAnimal : virtual public Animal { }; // Bat只包含一份Animal的副本 class Bat : public Mammal, public WingedAnimal { }; 复制代码
多态通常通过虚函数实现,允许使用基类的指针或引用来调用派生类中重写的方法。同一个函数调用会根据实际对象的类型执行不同的操作。
多态的条件:
必须有继承关系
基类中必须有虚函数
派生类必须重写基类的虚函数
通过基类的指针或引用调用虚函数
静态多态(编译时多态):
动态多态(运行时多态):
虚函数表
虚函数表是在编译时创建的静态表,存储了类中所有虚函数的地址。每个包含虚函数的类都有自己的虚函数表,而每个该类的对象都包含一个指向该虚函数表的指针。
没有虚函数的情况:函数调用是在编译时确定的(静态绑定),因此调用的是Base::func。
class Base { public: void func() { cout << "Base::func" << endl; } }; class Derived : public Base { public: void func() { cout << "Derived::func" << endl; } }; int main() { Base* obj = new Derived(); obj->func(); // 输出: Base::func(静态绑定) delete obj; } 复制代码
有虚函数的情况:函数调用是在运行时根据对象的实际类型确定的(动态绑定)
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } void func3() { cout << "Base::func3" << endl; } }; class Derived : public Base { public: void func1() override { cout << "Derived::func1" << endl; } void func2() override { cout << "Derived::func2" << endl; } }; int main() { Base* obj = new Derived(); obj->func1(); // 输出: Derived::func1(动态绑定) obj->func2(); // 输出: Derived::func2(动态绑定) obj->func3(); // 输出: Base::func3(静态绑定) delete obj; } 复制代码
单继承情况的虚函数表内存布局:
class Base { public: virtual void func1() { } virtual void func2() { } int base_data; }; class Derived : public Base { public: void func1() override { } virtual void func3() { } int derived_data; }; 复制代码 Base对象: +----------------+ | vptr | --> 指向Base的虚函数表 +----------------+ | base_data | +----------------+ Derived对象: +----------------+ | vptr | --> 指向Derived的虚函数表 +----------------+ | base_data | (继承自Base) +----------------+ | derived_data | +----------------+ Base的虚函数表: +----------------+ | &Base::func1 | +----------------+ | &Base::func2 | +----------------+ Derived的虚函数表: +----------------+ | &Derived::func1| // 重写了Base::func1 +----------------+ | &Base::func2 | // 没有重写,所以还是Base::func2 +----------------+ | &Derived::func3| // 新增的虚函数 +----------------+ 复制代码
多继承情况的虚函数表内存布局:
class Base1 { public: virtual void func1() { } int data1; }; class Base2 { public: virtual void func2() { } int data2; }; class Derived : public Base1, public Base2 { public: void func1() override { } void func2() override { } virtual void func3() { } int data3; }; 复制代码 Derived对象: +----------------+ | vptr1 | --> 指向Derived的Base1部分虚函数表 +----------------+ | Base1::data1 | +----------------+ | vptr2 | --> 指向Derived的Base2部分虚函数表 +----------------+ | Base2::data2 | +----------------+ | Derived::data3 | +----------------+ Derived的Base1部分虚函数表: +----------------+ | &Derived::func1| // 重写了Base1::func1 +----------------+ | &Derived::func3| // 新增的虚函数 +----------------+ Derived的Base2部分虚函数表: +----------------+ | &Derived::func2| // 重写了Base2::func2 +----------------+ | 偏移量/thunk | // 可能包含this指针调整信息 +----------------+ 复制代码
虚函数表指针: 虚函数表指针的位置:
虚函数表指针的初始化:
在构造函数中初始化
初始化顺序:先基类后派生类
在对象构造过程中,vptr可能被多次修改
虚函数调用的实现:
Base* obj = new Derived(); obj->func1(); // 虚函数调用 复制代码
实际过程:
通过对象的vptr找到虚函数表
在虚函数表中通过固定的偏移量找到函数地址
通过函数地址调用函数,并传递正确的this指针
this指针调整:在多继承中,当通过第二个基类的指针调用派生类的虚函数时,需要调整this指针
Derived* d = new Derived(); Base2* b2 = d; // 这里b2指向Derived对象中的Base2子对象 b2->func2(); // 调用时,this指针需要调整到Derived对象的起始位置 复制代码
虚函数表的构造过程:
编译时:
编译器为每个包含虚函数的类生成一个虚函数表
表中按声明顺序存放虚函数地址
对于被重写的函数,使用派生类的函数地址
对于未被重写的函数,使用基类的函数地址
运行时:
在对象构造时设置vptr
在构造函数初始化列表中,vptr被设置为当前类的虚函数表
如果有基类,先调用基类构造函数(设置基类的vptr)
然后调用派生类构造函数(重新设置vptr)
3.2 设计模式:单例、工厂、观察者
单例模式
确保一个类只有一个实例,并提供全局访问点。
实现要点:
构造函数私有化 :确保外部不能直接创建对象。
禁用拷贝和赋值 :避免通过拷贝或赋值创建新实例。
提供全局访问点 :通过静态成员函数获取唯一实例。
线程安全 :多线程环境下确保只创建一个实例。
延迟初始化 (可选):有些场景下希望第一次使用时才创建实例(懒汉式),而有些场景下希望程序启动时就创建(饿汉式)。
资源释放 :一般情况下,单例对象在程序运行期间一直存在,不需要手动释放。如果使用new创建,可以考虑使用智能指针,或者使用静态成员变量(在程序结束时自动销毁)。使用局部静态变量方式可以自动销毁。
懒汉式(线程不安全)和饿汉式(线程安全),现代C++中推荐线程安全懒汉式(使用局部静态变量)。
class Singleton { private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: static Singleton& getInstance() { static Singleton instance; // 线程安全 return instance; } }; 复制代码
工厂模式
简单工厂模式: 一个工厂类,根据参数创建不同的产品。适用于产品类型较少的情况。
要点:
一个工厂类,一个产品接口(或抽象类),多个具体产品类。
工厂类中有一个静态方法(或非静态),根据参数创建具体产品。
#include <iostream> using namespace std; // 产品接口 class Shape { public: virtual void draw() = 0; virtual ~Shape() {} }; // 具体产品:圆形 class Circle : public Shape { public: void draw() override { cout << "Draw a circle." << endl; } }; // 具体产品:正方形 class Square : public Shape { public: void draw() override { cout << "Draw a square." << endl; } }; // 简单工厂 class ShapeFactory { public: // 根据类型创建形状 static Shape* createShape(const string& type) { if (type == "circle") { return new Circle(); } else if (type == "square") { return new Square(); } else { return nullptr; } } }; int main() { Shape* circle = ShapeFactory::createShape("circle"); Shape* square = ShapeFactory::createShape("square"); if (circle) circle->draw(); if (square) square->draw(); delete circle; delete square; return 0; } 复制代码
优点:
缺点:
工厂方法模式: 定义一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
要点:
一个产品接口,多个具体产品。
一个工厂接口(或抽象类),多个具体工厂,每个具体工厂对应一个具体产品。
#include <iostream> using namespace std; // 产品接口 class Shape { public: virtual void draw() = 0; virtual ~Shape() {} }; // 具体产品:圆形 class Circle : public Shape { public: void draw() override { cout << "Draw a circle." << endl; } }; // 具体产品:正方形 class Square : public Shape { public: void draw() override { cout << "Draw a square." << endl; } }; // 工厂接口 class ShapeFactory { public: virtual Shape* createShape() = 0; virtual ~ShapeFactory() {} }; // 具体工厂:创建圆形 class CircleFactory : public ShapeFactory { public: Shape* createShape() override { return new Circle(); } }; // 具体工厂:创建正方形 class SquareFactory : public ShapeFactory { public: Shape* createShape() override { return new Square(); } }; int main() { ShapeFactory* circleFactory = new CircleFactory(); ShapeFactory* squareFactory = new SquareFactory(); Shape* circle = circleFactory->createShape(); Shape* square = squareFactory->createShape(); circle->draw(); square->draw(); delete circle; delete square; delete circleFactory; delete squareFactory; return 0; } 复制代码
优点:
缺点:
抽象工厂模式: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
要点:
多个产品接口,每个产品接口有多个具体产品。
一个工厂接口,可以创建多个产品,每个具体工厂对应一个产品族。
示例代码:假设有两种产品:颜色和形状。颜色有红色和蓝色,形状有圆形和正方形。抽象工厂可以创建一组相关的产品,比如红色圆形、蓝色正方形等。
优点:
缺点:
观察者模式
定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。
要点:
Subject(主题) :维护观察者列表,提供添加、删除和通知观察者的接口
Observer(观察者) :定义更新接口,用于在主题状态改变时接收通知
ConcreteSubject(具体主题) :存储具体状态,状态改变时通知所有观察者
ConcreteObserver(具体观察者) :实现更新接口,保持与主题状态的一致性
#include <iostream> #include <vector> #include <memory> #include <functional> #include <mutex> #include <algorithm> // 现代观察者模式最佳实践 template<typename... Args> class Observable //观察者回调函数的参数类型 { private: using Callback = std::function<void(Args...)>; struct ObserverEntry //存储每个观察者的信息 { std::weak_ptr<void> weakKey; // 用于取消订阅的键 Callback callback;//回调函数 int priority{0};//优先级,用于排序 bool operator<(const ObserverEntry& other) const { return priority < other.priority; } }; std::vector<ObserverEntry> observers;//存储所有观察者 mutable std::shared_mutex mutex;//用于多线程保护的共享互斥锁 std::atomic<bool> notifying{false};//原子布尔变量,防止重入通知 public: // 订阅(返回可用于取消订阅的token) template<typename T> std::shared_ptr<T> subscribe(Callback callback, int priority = 0) { auto key = std::make_shared<T>(); std::unique_lock lock(mutex); observers.push_back({key, callback, priority}); std::sort(observers.begin(), observers.end()); return key; } // 取消订阅 template<typename T> void unsubscribe(const std::shared_ptr<T>& key) { std::unique_lock lock(mutex); observers.erase( std::remove_if(observers.begin(), observers.end(), [&key](const ObserverEntry& entry) { return entry.weakKey.lock() == key; }), observers.end() ); } // 通知所有观察者(线程安全,异常安全) void notify(Args... args) { if (notifying.exchange(true)) { return; // 防止重入 } std::vector<ObserverEntry> observersCopy; { std::shared_lock lock(mutex); observersCopy = observers; } try { // 按优先级顺序通知 for (const auto& entry : observersCopy) { if (auto key = entry.weakKey.lock()) { try { entry.callback(args...); } catch (const std::exception& e) { std::cerr << "观察者回调异常: " << e.what() << std::endl; // 继续执行其他观察者 } } } } catch (...) { notifying.store(false); throw; } notifying.store(false); // 清理已销毁的观察者 cleanup(); } private: void cleanup() //清理方法:移除所有weakKey已经过期的观察者 { std::unique_lock lock(mutex); observers.erase( std::remove_if(observers.begin(), observers.end(), [](const ObserverEntry& entry) { return entry.weakKey.expired(); }), observers.end() ); } }; // 使用示例 class Button { private: Observable<int, int> onClick; // x, y坐标 public: auto connectClick(std::function<void(int, int)> handler) //连接点击事件 { return onClick.subscribe<int>(handler); } void click(int x, int y) //模拟按钮点击 { std::cout << "按钮被点击: (" << x << ", " << y << ")" << std::endl; onClick.notify(x, y); } }; int main() { Button button;//创建Button对象,连接两个点击事件处理其,然后触发点击事件,最后再连接一个处理器 // 订阅点击事件 auto token1 = button.connectClick([](int x, int y) { std::cout << "处理器1: 点击位置 (" << x << ", " << y << ")" << std::endl; }); auto token2 = button.connectClick([](int x, int y) { std::cout << "处理器2: 按钮点击事件" << std::endl; }); // 触发事件 button.click(100, 200); // 取消订阅 button.connectClick([](int, int) { // 这个不会执行,因为token2被释放了 }); return 0; } 复制代码
4 资源管理与RALL
4.1 智能指针
shared_ptr(共享)
特点:共享所有权,引用计数
共享所有权的智能指针。多个shared_ptr可以指向同一个对象,并通过引用计数来管理对象的生命周期。当最后一个shared_ptr被销毁时,对象才会被释放。
#include <memory> #include <iostream> void shared_ptr_demo() { // 创建 shared_ptr std::shared_ptr<int> p1 = std::make_shared<int>(100); // 推荐 std::shared_ptr<int> p2(p1); // 复制,引用计数+1 std::cout << "p1.use_count(): " << p1.use_count() << std::endl; // 2 std::cout << "p2.use_count(): " << p2.use_count() << std::endl; // 2 // 独立指针 std::shared_ptr<int> p3 = std::make_shared<int>(200); std::cout << "p3.use_count(): " << p3.use_count() << std::endl; // 1 // 重置指针 p2.reset(); // 释放所有权 std::cout << "p1.use_count(): " << p1.use_count() << std::endl; // 1 // 自定义删除器 auto custom_deleter = [](int* p) { std::cout << "Custom delete: " << *p << std::endl; delete p; }; std::shared_ptr<int> p4(new int(300), custom_deleter); // 数组支持(C++17起) std::shared_ptr<int[]> p5(new int[5]{10, 20, 30, 40, 50}); std::cout << p5[2] << std::endl; // 30 } 复制代码
注意:使用shared_ptr时要注意循环引用问题,如果两个对象相互使用shared_ptr指向对方,那么引用计数永远不会降到0,导致内存泄漏。这时可以使用weak_ptr来解决。
引用计数原理:
p1 ──┐ │───> 对象 (ref_count=2) p2 ──┘ 复制代码
unique_ptr(独占)
特点:独占所有权,不可复制
独占所有权的智能指针。同一时间只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也会被自动销毁。 它不能复制,只能移动。这意味着,所有权可以从一个unique_ptr转移到另一个unique_ptr,但不会有两个unique_ptr同时拥有同一个对象。
#include <memory> #include <iostream> void unique_ptr_demo() { // 创建 unique_ptr std::unique_ptr<int> p1(new int(10)); auto p2 = std::make_unique<int>(20); // 推荐方式(C++14) // 访问数据 std::cout << *p1 << std::endl; // 10 std::cout << *p2 << std::endl; // 20 // 转移所有权 std::unique_ptr<int> p3 = std::move(p1); std::cout << (p1 == nullptr) << std::endl; // 1 (true) std::cout << *p3 << std::endl; // 10 // 自定义删除器 auto deleter = [](int* p) { std::cout << "Deleting int: " << *p << std::endl; delete p; }; std::unique_ptr<int, decltype(deleter)> p4(new int(30), deleter); // 数组支持 std::unique_ptr<int[]> p5(new int[5]{1, 2, 3, 4, 5}); std::cout << p5[2] << std::endl; // 3 } 复制代码
常用方法:
std::unique_ptr<T> up; up.reset(ptr); // 接管新指针,释放原指针 up.release(); // 释放所有权,返回原始指针,不删除对象 up.get(); // 获取原始指针 up.swap(other); // 交换指针 复制代码
也可以使用std::make_unique(C++14引入)来创建unique_ptr,这样更安全,因为如果内存分配失败,它会抛出异常,而且使用它时不需要直接使用new。
auto p = std::make_unique<int>(10); 复制代码
weak_ptr(防循环引用)
特点:弱引用,不增加应用计数,解决循环引用问题
弱引用的智能指针,它不控制对象的生命周期,而是指向一个由shared_ptr管理的对象。weak_ptr的构造和析构不会影响引用计数。它主要用于解决shared_ptr的循环引用问题。
#include <memory> #include <iostream> class Node { public: std::string name; std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // 使用 weak_ptr 避免循环引用 Node(const std::string& n) : name(n) { std::cout << "Node " << name << " created" << std::endl; } ~Node() { std::cout << "Node " << name << " destroyed" << std::endl; } }; void weak_ptr_demo() { auto node1 = std::make_shared<Node>("Node1"); auto node2 = std::make_shared<Node>("Node2"); // 建立双向链接(使用 weak_ptr 避免循环引用) node1->next = node2; node2->prev = node1; // 使用 weak_ptr std::weak_ptr<Node> weak_node = node1; // 检查对象是否还存在 if (auto shared_node = weak_node.lock()) { std::cout << "Node exists: " << shared_node->name << std::endl; } else { std::cout << "Node has been destroyed" << std::endl; } // 重置 shared_ptr node1.reset(); if (auto shared_node = weak_node.lock()) { std::cout << "Node still exists" << std::endl; } else { std::cout << "Node has been destroyed" << std::endl; } } 复制代码
常用方法:
std::weak_ptr<T> wp; wp.lock(); // 尝试获取 shared_ptr,失败返回空 wp.expired(); // 检查对象是否已被删除 wp.use_count(); // 获取引用计数 复制代码
注意:weak_ptr不能直接访问对象,必须通过lock()函数获取一个shared_ptr,然后使用。如果对象已经被释放,lock()会返回一个空的shared_ptr。
std::shared_ptr<int> sp = std::make_shared<int>(30); std::weak_ptr<int> wp = sp; // 使用weak_ptr访问对象 if (std::shared_ptr<int> np = wp.lock()) { std::cout << *np << std::endl; } else { std::cout << "对象已释放" << std::endl; } 复制代码
循环引用问题 class BadNode { public: std::shared_ptr<BadNode> next; std::shared_ptr<BadNode> prev; // 错误:相互持有 shared_ptr BadNode(const std::string& n) : name(n) { std::cout << "BadNode " << n << " created" << std::endl; } ~BadNode() { std::cout << "BadNode " << name << " destroyed" << std::endl; // 永远不会执行 } private: std::string name; }; void circular_reference_demo() { auto node1 = std::make_shared<BadNode>("Node1"); auto node2 = std::make_shared<BadNode>("Node2"); // 循环引用:两者都不会被销毁 node1->next = node2; node2->prev = node1; std::cout << "node1.use_count(): " << node1.use_count() << std::endl; // 2 std::cout << "node2.use_count(): " << node2.use_count() << std::endl; // 2 // 退出时,引用计数从2减到1,内存泄漏! } 复制代码
4.2 RALL原则
构造函数获取资源,析构函数释放资源
RAII的核心思想是将资源(如动态内存、文件句柄、网络连接、锁等)的生命周期与对象的生命周期绑定,在对象构造时获取资源,在对象析构时释放资源。这样,只要对象能正确析构,资源就能被正确释放,从而避免资源泄漏。
RALL的工作原理:
在类的构造函数中获取资源(分配内存、打开文件、加锁等)
在类的析构函数中释放资源(释放内存、关闭文件、解锁等)
这样,当对象离开作用域时(无论是正常离开还是因为异常),析构函数都会被调用,从而释放资源。
管理动态内存(类似智能指针):
template<typename T> class SimpleUniquePtr { private: T* ptr; public: // 构造函数获取资源 explicit SimpleUniquePtr(T* p = nullptr) : ptr(p) {} // 析构函数释放资源 ~SimpleUniquePtr() { delete ptr; } // 禁止拷贝 SimpleUniquePtr(const SimpleUniquePtr&) = delete; SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete; // 允许移动 SimpleUniquePtr(SimpleUniquePtr&& other) noexcept : ptr(other.ptr) { other.ptr = nullptr; } SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept { if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr; } return *this; } // 访问操作 T& operator*() const { return *ptr; } T* operator->() const { return ptr; } T* get() const { return ptr; } // 资源释放 void reset(T* p = nullptr) { delete ptr; ptr = p; } }; 复制代码
RALL与异常安全:
void badFunction() { int* ptr = new int(100); someFunctionThatMayThrow(); // 如果抛出异常,delete不会执行,内存泄漏 delete ptr; } void goodFunction() { std::unique_ptr<int> ptr = std::make_unique<int>(100); // 使用RAII someFunctionThatMayThrow(); // 即使抛出异常,ptr的析构函数也会被调用,内存被释放 } 复制代码
C++标准库中的智能指针(unique_ptr、shared_ptr、weak_ptr)是RAII的典型应用。它们管理动态内存,确保在适当的时候释放内存。
作用域绑定资源生命周期:
void scope_based_management() { { LockRAII lock(mutex); // 进入作用域时加锁 // 临界区代码 // 如果这里抛出异常,锁仍然会自动释放 } // 离开作用域时自动解锁 // 不同作用域不同资源 { FileRAII file1("a.txt", "r"); // 使用file1 } // file1自动关闭 { FileRAII file2("b.txt", "w"); // 使用file2 } // file2自动关闭 } 复制代码
RALL的优点:
自动释放资源 :不再需要手动释放资源,减少错误。
异常安全 :即使发生异常,资源也能被正确释放。
代码简洁 :将资源管理逻辑封装在类中,使用代码更清晰。
资源所有权明确 :通过对象的生命周期明确资源的所有权。
RALL的注意事项:
资源的所有权 :RAII对象应独占资源的所有权,避免多个RAII对象管理同一资源(除非使用引用计数如shared_ptr)。
拷贝控制 :如果RAII类需要拷贝,需要仔细考虑拷贝语义(深拷贝或转移所有权)。通常,如果资源不可共享,应禁止拷贝(如unique_ptr),或使用引用计数(如shared_ptr)。
资源泄露 :确保在构造函数中获取资源时没有泄漏,如果构造函数抛出异常,析构函数不会被调用,因此需要确保构造函数是异常安全的。
c++标准库中的RALL类
#include <memory> #include <fstream> #include <mutex> void standard_library_raii() { // 1. 内存管理 std::unique_ptr<int> ptr = std::make_unique<int>(42); std::shared_ptr<int> shared = std::make_shared<int>(42); // 2. 文件管理 std::ifstream file("data.txt"); // RAII:自动关闭文件 std::ofstream output("output.txt"); // 3. 互斥锁管理 std::mutex mtx; { std::lock_guard<std::mutex> lock(mtx); // RAII锁 // 临界区 } // 自动解锁 // 4. 动态数组 std::vector<int> vec; // RAII:自动管理内存 vec.push_back(1); // 离开作用域时自动释放内存 } 复制代码
RALL设计原则:
资源获取在构造函数中完成
资源释放在析构函数中完成
使用移动语义支持资源所有权的转移
通常禁用拷贝构造函数和拷贝赋值运算符
考虑异常安全性
5 STL与泛型编程
5.1 容器
vector
vector 是一个动态数组,它能够存储连续内存中的元素,并且可以动态地调整大小。
常用操作:
访问元素 :使用[]运算符或at()方法(at()会进行边界检查)。
尾部添加/删除 :push_back()和pop_back(),平均时间复杂度为O(1)(不考虑重新分配的情况)。
中间插入/删除 :insert()和erase(),需要移动元素,时间复杂度为O(n)。
大小和容量 :size()返回元素数量,capacity()返回当前分配的内存可以容纳的元素数量。
优缺点:
适用场景:
需要频繁随机访问元素。
主要是在尾部添加或删除元素。
不需要在中间频繁插入或删除。
vector扩容机制:
vector的扩容机制是动态数组的核心特性。当vector的当前容量(capacity)不足以容纳新元素时,就需要扩容。 扩容的一般步骤:
分配一块更大的内存空间(具体大小取决于实现,通常是当前容量的两倍或1.5倍)。
将原有元素从旧内存移动到新内存(如果是非POD类型,移动过程中可能会调用元素的拷贝构造函数或移动构造函数)。
释放旧内存。
更新vector的内部指针,容量等状态。
vector 的扩容是自动的!这是 vector 内部机制,不需要手动实现。
当你使用 push_back、emplace_back、insert 等方法向 vector 中添加元素,而当前容量(capacity)不足以容纳新元素时,vector 会自动进行扩容。
注意事项:
性能考虑 :自动扩容虽然方便,但频繁扩容会导致性能下降。如果你知道大概需要多少元素,最好使用 reserve 预分配。
迭代器失效 :扩容后,所有迭代器、指针和引用都会失效,需要重新获取。
异常安全 :vector 的扩容操作是异常安全的,如果扩容失败(如内存不足),会抛出 std::bad_alloc 异常,且原 vector 保持不变。
map/unordered_map
map:
unordered_map:
适用场景:
红黑树:
红黑树是一种自平衡的二叉搜索树,它在每个节点上增加了一个存储位表示节点的颜色(红色或黑色)。通过对任何一条从根到叶子的路径上各个节点颜色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似平衡的。
红黑树满足以下性质:
每个节点要么是红色,要么是黑色。
根节点是黑色的。
每个叶子节点(NIL节点,空节点)是黑色的。
如果一个节点是红色的,则它的两个子节点都是黑色的(即不能有两个连续的红色节点)。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(称为黑高)。
旋转:为了在插入和删除后重新平衡树,需要进行旋转操作。旋转有两种:左旋和右旋。
插入:新插入的节点通常被着色为红色,以避免违反黑高性质。然后,通过一系列的颜色更改和旋转来修复可能出现的连续红色节点问题。
删除:删除操作比插入更复杂。删除一个节点后,可能会破坏红黑树的性质,需要通过颜色更改和旋转来修复。
哈希表:
// 哈希表(Hash Table)是一种: // 1. 键值对存储结构 // 2. 通过哈希函数将键映射到数组索引 // 3. 理想情况下提供O(1)的插入、删除和查找 // 基本工作原理: // key → 哈希函数 → 哈希值 → 数组索引 → 存储value // 在C++标准库中,unordered_map通常实现为: // - 数组:存储桶(buckets) // - 每个桶:链表或红黑树(C++11后引入) // 解决哈希冲突的两种主要方法: // 1. 链地址法(Separate Chaining):每个桶是链表 // 2. 开放地址法(Open Addressing):线性探测、二次探测等 // C++ unordered_map使用链地址法 /* buckets数组 +---+ +-------+ +-------+ +-------+ | 0 | -> | key1 | -> | key4 | -> | key7 | -> nullptr +---+ +-------+ +-------+ +-------+ | 1 | -> nullptr +---+ | 2 | -> | key2 | -> | key5 | -> nullptr +---+ +-------+ +-------+ | 3 | -> | key3 | -> | key6 | -> nullptr +---+ +-------+ +-------+ ... |n-1| -> nullptr +---+ 哈希函数:h(key) = key % n */ 复制代码
list
list 是一个双向链表,允许在常数时间内在任何位置进行插入和删除操作。
常用操作:
适用场景:
需要频繁在中间插入或删除元素。
不需要随机访问,只需要顺序访问。
5.2 算法
以下三个算法函数都定义在头文件中
sort
sort用于对容器(如vector、deque、数组等)中的元素进行排序,默认是升序排序。
template< class RandomIt > void sort( RandomIt first, RandomIt last ); template< class RandomIt, class Compare > void sort( RandomIt first, RandomIt last, Compare comp ); 复制代码
参数:
使用:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> v = {5, 3, 1, 4, 2}; // 默认升序排序 std::sort(v.begin(), v.end()); for (int i : v) std::cout << i << " "; // 输出: 1 2 3 4 5 std::cout << std::endl; // 降序排序 std::sort(v.begin(), v.end(), std::greater<int>()); for (int i : v) std::cout << i << " "; // 输出: 5 4 3 2 1 std::cout << std::endl; // 使用lambda表达式自定义排序 std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; // 升序 }); for (int i : v) std::cout << i << " "; // 输出: 1 2 3 4 5 std::cout << std::endl; return 0; } 复制代码
find
find用于在容器中查找特定值,返回指向第一个等于该值的元素的迭代器。如果未找到,则返回结束迭代器(即last)。
template< class InputIt, class T > InputIt find( InputIt first, InputIt last, const T& value ); 复制代码
参数:
使用:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> v = {1, 2, 3, 4, 5}; // 查找值为3的元素 auto it = std::find(v.begin(), v.end(), 3); if (it != v.end()) { std::cout << "Found: " << *it << std::endl; // 输出: Found: 3 // 也可以计算索引 std::cout << "Index: " << (it - v.begin()) << std::endl; // 输出: Index: 2 } else { std::cout << "Not found" << std::endl; } // 查找值为10的元素(不存在) it = std::find(v.begin(), v.end(), 10); if (it == v.end()) { std::cout << "10 not found" << std::endl; // 输出: 10 not found } return 0; } 复制代码
注意:
remove_if
remove_if用于从容器中移除满足特定条件的元素。注意,remove_if并不会真正删除元素,而是将不满足条件的元素移动到容器的前部,并返回一个指向新的逻辑结束位置的迭代器。之后,通常需要调用容器的erase方法来真正删除元素。
template< class ForwardIt, class UnaryPredicate > ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p ); 复制代码
参数:
使用:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 移除所有偶数 auto new_end = std::remove_if(v.begin(), v.end(), [](int n) { return n % 2 == 0; }); // 此时,v中的元素变为:1,3,5,7,9,?,?,?,?,? (?表示未指定值,通常为原值) // 注意:remove_if只是将不需要移除的元素移动到前面,并返回新的逻辑结束迭代器 // 物理大小没有改变 // 打印逻辑范围内的元素 for (auto it = v.begin(); it != new_end; ++it) { std::cout << *it << " "; // 输出: 1 3 5 7 9 } std::cout << std::endl; // 真正删除元素 v.erase(new_end, v.end()); // 现在v的物理大小也改变了 for (int i : v) std::cout << i << " "; // 输出: 1 3 5 7 9 std::cout << std::endl; return 0; } 复制代码
注意:
5.3 lambda:结合STL算法使用 [capture] (parameters) -> return_type { // 函数体 } 复制代码
capture :捕获列表,用于捕获外部变量。可以是值捕获、引用捕获或混合捕获。
parameters :参数列表,类似于普通函数的参数列表。
return_type :返回类型,可以省略,编译器会根据函数体中的返回语句自动推导。
函数体 :与普通函数一样。
捕获列表的几种形式:
[]:不捕获任何外部变量。
[=]:以值捕获的方式捕获所有外部变量。
[&]:以引用捕获的方式捕获所有外部变量。
[var]:以值捕获的方式捕获特定变量 var。
[&var]:以引用捕获的方式捕获特定变量 var。
[=, &var]:默认以值捕获所有外部变量,但是以引用捕获 var。
[&, var]:默认以引用捕获所有外部变量,但是以值捕获 var。
使用sort进行自定义排序:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6}; // 使用 lambda 表达式按降序排序 std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; // 降序 }); for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; } 复制代码
使用for_each遍历并修改元素:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; // 使用 lambda 表达式将每个元素乘以2 std::for_each(numbers.begin(), numbers.end(), [](int &n) { n *= 2; }); for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; } 复制代码
for_each遍历容器中的每个元素,并对每个元素执行指定的操作。
template< class InputIt, class UnaryFunction > UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f ); 复制代码
参数:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; // 使用lambda打印每个元素 std::for_each(nums.begin(), nums.end(), [](int n) { std::cout << n << " "; }); // 输出:1 2 3 4 5 // 修改每个元素(注意参数为引用) std::for_each(nums.begin(), nums.end(), [](int &n) { n *= 2; }); // 再次打印 std::for_each(nums.begin(), nums.end(), [](int n) { std::cout << n << " "; }); // 输出:2 4 6 8 10 return 0; } 复制代码
使用transform转换容器元素
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> src = {1, 2, 3, 4, 5}; std::vector<int> dst(src.size()); // 将 src 中的每个元素加1,并存储到 dst 中 std::transform(src.begin(), src.end(), dst.begin(), [](int n) { return n + 1; }); for (int num : dst) { std::cout << num << " "; } std::cout << std::endl; return 0; } 复制代码
transform:将函数应用于范围中的每个元素,并将结果存储在另一个范围中(或原地转换)。
// 一元操作版本 template< class InputIt, class OutputIt, class UnaryOperation > OutputIt transform( InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op ); // 二元操作版本 template< class InputIt1, class InputIt2, class OutputIt, class BinaryOperation > OutputIt transform( InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op ); 复制代码
一元操作:
#include <iostream> #include <vector> #include <algorithm> #include <iterator> int main() { std::vector<int> src = {1, 2, 3, 4, 5}; std::vector<int> dst; // 将src中每个元素平方,并插入到dst中 std::transform(src.begin(), src.end(), std::back_inserter(dst), [](int n) { return n * n; }); // 打印dst std::for_each(dst.begin(), dst.end(), [](int n) { std::cout << n << " "; }); // 输出:1 4 9 16 25 // 原地转换:将src中的每个元素加1 std::transform(src.begin(), src.end(), src.begin(), [](int n) { return n + 1; }); // 打印src std::for_each(src.begin(), src.end(), [](int n) { std::cout << n << " "; }); // 输出:2 3 4 5 6 return 0; } 复制代码
二元操作:
#include <iostream> #include <vector> #include <algorithm> #include <iterator> int main() { std::vector<int> a = {1, 2, 3, 4, 5}; std::vector<int> b = {10, 20, 30, 40, 50}; std::vector<int> result; // 将a和b中对应元素相加 std::transform(a.begin(), a.end(), b.begin(), std::back_inserter(result), [](int x, int y) { return x + y; }); // 打印result std::for_each(result.begin(), result.end(), [](int n) { std::cout << n << " "; }); // 输出:11 22 33 44 55 return 0; } 复制代码
使用find_if查找满足条件的元素
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; // 查找第一个大于3的元素 auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) { return n > 3; }); if (it != numbers.end()) { std::cout << "找到第一个大于3的元素: " << *it << std::endl; } else { std::cout << "未找到" << std::endl; } return 0; } 复制代码
find_if:在范围内查找第一个满足条件的元素,返回指向该元素的迭代器。
template< class InputIt, class UnaryPredicate > InputIt find_if( InputIt first, InputIt last, UnaryPredicate p ); 复制代码 #include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> nums = {1, 2, 3, 4, 5, 6}; // 查找第一个偶数 auto it = std::find_if(nums.begin(), nums.end(), [](int n) { return n % 2 == 0; }); if (it != nums.end()) { std::cout << "找到偶数: " << *it << std::endl; // 输出:2 } else { std::cout << "没有找到偶数" << std::endl; } // 查找第一个大于10的数 auto it2 = std::find_if(nums.begin(), nums.end(), [](int n) { return n > 10; }); if (it2 == nums.end()) { std::cout << "没有找到大于10的数" << std::endl; } return 0; } 复制代码
使用remove_if和erase删除满足条件的元素
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 删除所有偶数 auto new_end = std::remove_if(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; }); numbers.erase(new_end, numbers.end()); for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; } 复制代码
remove_if:将满足条件的元素移动到容器末尾,并返回一个指向新的逻辑末尾的迭代器。注意,它并不真正删除元素,也不改变容器的大小。 erase:真正从容器中删除元素。
template< class ForwardIt, class UnaryPredicate > ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p ); 复制代码 #include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 移除所有偶数 auto new_end = std::remove_if(nums.begin(), nums.end(), [](int n) { return n % 2 == 0; }); // 此时,nums中的元素可能是:1,3,5,7,9,?,?,?,? (?表示未指定,通常是被移除的元素) // 容器大小没有变,但[begin, new_end)是不包含被移除元素的序列 // 打印[begin, new_end)范围内的元素 std::for_each(nums.begin(), new_end, [](int n) { std::cout << n << " "; }); std::cout << std::endl; // 输出:1 3 5 7 9 // 真正删除元素,使用erase nums.erase(new_end, nums.end()); // 现在容器中只有奇数 std::for_each(nums.begin(), nums.end(), [](int n) { std::cout << n << " "; }); // 输出:1 3 5 7 9 return 0; } 复制代码
remove_if和erase结合使用来真正删除元素,这被称为“erase-remove”惯用法。
对于顺序容器(如vector、deque、string),erase会真正减少容器的大小。
对于list,有remove_if成员函数,它可以直接删除元素,效率更高。
使用accumulate进行累加(需要包含)
#include <iostream> #include <vector> #include <numeric> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; // 使用 lambda 表达式进行累加,初始值为0 int sum = std::accumulate(numbers.begin(), numbers.end(), 0, [](int acc, int n) { return acc + n; }); std::cout << "和为: " << sum << std::endl; return 0; } 复制代码
accumulate:计算给定范围内元素的累积值(求和、求积等),可以指定初始值和二元操作函数。
template< class InputIt, class T > T accumulate( InputIt first, InputIt last, T init ); template< class InputIt, class T, class BinaryOperation > T accumulate( InputIt first, InputIt last, T init, BinaryOperation op ); 复制代码 #include <iostream> #include <vector> #include <numeric> // accumulate在此头文件中 int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; // 求和,初始值为0 int sum = std::accumulate(nums.begin(), nums.end(), 0); std::cout << "和: " << sum << std::endl; // 输出:15 // 求积,初始值为1 int product = std::accumulate(nums.begin(), nums.end(), 1, [](int a, int b) { return a * b; }); std::cout << "积: " << product << std::endl; // 输出:120 // 拼接字符串 std::vector<std::string> words = {"Hello", " ", "World", "!"}; std::string concat = std::accumulate(words.begin(), words.end(), std::string("")); std::cout << concat << std::endl; // 输出:Hello World! // 使用自定义操作:将向量中的每个元素乘以2然后累加 int double_sum = std::accumulate(nums.begin(), nums.end(), 0, [](int acc, int n) { return acc + n * 2; }); std::cout << "每个元素乘以2后求和: " << double_sum << std::endl; // 输出:30 return 0; } 复制代码
6 现代c++常用特性
6.1 auto类型推导
auto类型推导的基本规则:
auto在推导类型时,会忽略顶层const(即变量本身是常量,但指向的对象可以是常量也可以不是),但会保留底层const(例如指针所指向的对象是常量)。
当使用auto声明引用时,auto会保留表达式的引用类型和const属性。
当使用auto声明指针时,auto会推导出指针类型,但也会保留底层const。
auto必须初始化:
auto x; // 错误:必须初始化 auto y = 10; // 正确 复制代码
何时使用auto的最佳实践:
// 推荐使用auto auto result = calculateValue(); // 类型可能很复杂 for (auto& item : container) // 范围循环 auto ptr = std::make_unique<MyClass>(); // 智能指针 // 可能需要明确类型 int count = 10; // 基本类型更清晰 float pi = 3.14f; // 明确使用float而不是double 复制代码
6.2 范围for循环
使用for(auto& x : vec)时,我们声明了一个引用类型,这意味着:
我们不会复制容器中的每一个元素,而是直接引用元素本身。
因此,如果修改x,就会修改容器中对应的元素(因为x是引用)。
同时,因为避免了拷贝,对于大型对象或容器,使用引用可以提高效率。
但是,如果只是读取元素而不修改,通常使用const引用,即for(const auto& x : vec),以避免不必要的拷贝并且保护元素不被修改。
std::vector<int> vec = {1, 2, 3, 4, 5}; // 1. 引用方式(可以修改元素) for (auto& x : vec) { x *= 2; // 修改原始元素 } // 2. 常量引用方式(只读,高效) for (const auto& x : vec) { // x *= 2; // 错误:不能修改 std::cout << x << " "; } // 3. 值拷贝方式(创建副本,不能修改原始元素) for (auto x : vec) { x = 100; // 只修改副本,不影响vec } // 4. 显式指定类型 for (int& x : vec) { // 明确知道类型时 x += 10; } 复制代码
6.3 constexpr编译期计算
C++中的constexpr关键字用于指示一个值或函数可以在编译时计算。
constexpr变量:必须在编译时初始化,并且其值必须是编译时常量。
constexpr int size = 10; // 正确,编译时常量 constexpr double pi = 3.1415926; // 正确 int n = 20; constexpr int sz = n; // 错误,n不是编译时常量 复制代码
constexpr函数:是指能用于常量表达式的函数。在C++11中,constexpr函数只能包含一条return语句,而C++14放宽了限制。
//c++11 constexpr int factorial(int n) { return n <= 1 ? 1 : (n * factorial(n - 1)); } // 使用 constexpr int fact5 = factorial(5); // 编译时计算,结果为120 //c++14 constexpr int factorial(int n) { int result = 1; for (int i = 1; i <= n; ++i) { result *= i; } return result; } constexpr int fact5 = factorial(5); // 编译时计算 复制代码
constexpr函数的参数不一定必须是编译时常量。如果传入非常量参数,则函数在运行时计算。
constexpr指针:常量指针,即指针本身是常量,而不是所指向的对象是常量。
int x = 10; constexpr int* p = &x; // p是一个常量指针,指向x // 注意:这里x的地址必须在编译时已知,但x不是constexpr变量,所以&x不是常量表达式,因此实际上这行代码会编译错误。 constexpr int y = 20; constexpr const int* q = &y; // q是一个指向常量的常量指针,且&y是常量表达式 复制代码
constexpr构造函数:
class Point { public: constexpr Point(double x = 0, double y = 0) : x_(x), y_(y) {} constexpr double x() const { return x_; } constexpr double y() const { return y_; } // C++14允许constexpr函数修改成员变量 constexpr void setX(double x) { x_ = x; } constexpr void setY(double y) { y_ = y; } private: double x_, y_; }; // 使用 constexpr Point p(1.0, 2.0); // 编译时创建Point对象 constexpr double x = p.x(); // 编译时调用成员函数 复制代码
编译期计算的优势:
性能提升 :将计算从运行时移到编译时,减少运行时间。
类型安全 :编译时计算可以捕获更多错误。
可用于模板元编程 :constexpr函数可以替代部分模板元编程。
注意事项:
constexpr变量和函数必须非常简单,通常只能使用其他constexpr变量和函数。
constexpr函数可以同时用于编译时和运行时,但编译时必须传入常量表达式参数。
在C++11中,constexpr函数体只能有一条语句,但可以是return语句和条件运算符组成的复杂表达式。
7 拷贝/移动语义
7.1 深拷贝 vs 浅拷贝
浅拷贝(Shallow Copy)
浅拷贝只复制对象的成员变量的值,包括指针的值(即地址),而不复制指针所指向的内存。
这意味着,原对象和拷贝对象中的指针成员将指向同一块内存。
当其中一个对象修改了这块内存,另一个对象也会受到影响。
如果其中一个对象销毁时释放了这块内存,另一个对象的指针就成了悬空指针(dangling pointer),再次访问或释放会导致未定义行为。
深拷贝(Deep Copy)
深拷贝不仅复制对象的成员变量,还会为指针成员分配新的内存,并复制指针所指向的数据。
这样,原对象和拷贝对象拥有各自独立的内存空间,互不影响。
深拷贝通常需要在拷贝构造函数和赋值运算符中显式实现。
注意事项:
默认的拷贝构造函数和赋值运算符执行的是浅拷贝。
如果类中有指针成员,并且指向动态分配的内存,通常需要实现深拷贝以避免双重释放和悬空指针的问题。
在C++中,实现深拷贝时,通常需要同时实现拷贝构造函数、赋值运算符和析构函数(这被称为“三法则”)。在C++11以后,还有移动语义(移动构造函数和移动赋值运算符)需要考虑(“五法则”或“六法则”)。
三法则(Rule of Three)
如果一个类需要显式定义以下其中之一,那么它很可能需要显式定义所有三个:
五法则(Rule of Five)
在C++11中,由于移动语义的引入,可以扩展为五法则:
析构函数
拷贝构造函数
拷贝赋值运算符
移动构造函数
移动赋值运算符
需要深拷贝的情况:
类包含原始指针 ,指向动态分配的内存
类管理文件句柄 、网络连接 等资源
需要独立副本 ,修改副本不影响原对象
不需要深拷贝的情况:
所有成员都是基本类型 (int, double等)
成员使用智能指针 (shared_ptr, unique_ptr)
对象是不可变 的
使用移动语义 可以避免拷贝
建议优先使用智能指针,避免手动内存管理,减少深拷贝的必要性;对于资源密集型对象,实现移动构造函数和移动赋值运算符。
7.2 右值引用与move
右值引用使用两个&符号表示,例如int&&。右值引用只能绑定到右值(临时对象)。
int a = 10; // a 是左值 int&& rref = 10; // 10 是右值,rref 是右值引用 int&& rref2 = a; // 错误!不能将左值绑定到右值引用 // 正确:std::move 将左值转换为右值引用 int&& rref3 = std::move(a); 复制代码
移动语义允许将资源(如堆内存)从一个对象转移到另一个对象,从而避免深拷贝。通常通过移动构造函数和移动赋值运算符实现。
移动构造函数接受一个右值引用参数,并“窃取”该参数的资源,然后将该参数置于有效但不确定的状态(通常为空)。
class MyString { private: char* data; size_t length; public: // 移动构造函数 MyString(MyString&& other) noexcept : data(other.data), length(other.length) { other.data = nullptr; other.length = 0; } // 移动赋值运算符 MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data; // 释放当前资源 data = other.data; length = other.length; other.data = nullptr; other.length = 0; } return *this; } // 其他成员函数... }; 复制代码
std::move是一个函数模板,它将左值转换为右值引用,从而允许使用移动语义。注意,使用std::move后,原对象的状态将被改变,因此通常不再使用原对象(除非重新赋值)。
#include <utility> // std::move std::string str1 = "Hello"; std::string str2 = std::move(str1); // 移动构造 // 此时 str1 变为有效但未指定的状态 // 通常为空,但不能依赖其内容 复制代码
完美转发允许函数模板将其参数以原有的值类别(左值或右值)转发给其他函数。这通过使用右值引用和std::forward实现。
template<typename T> void wrapper(T&& arg) { // arg 是通用引用(既可以绑定左值也可以绑定右值) // std::forward 保持参数原有的值类别 process(std::forward<T>(arg)); } void process(int& x) { std::cout << "左值: " << x << std::endl; } void process(int&& x) { std::cout << "右值: " << x << std::endl; } int main() { int a = 10; wrapper(a); // 调用左值版本 wrapper(20); // 调用右值版本 wrapper(std::move(a)); // 调用右值版本 } 复制代码
注意:
7.3 移动构造函数
移动构造函数是C++11引入的一种特殊构造函数,用于将资源从一个对象移动到另一个对象,而不是进行深拷贝。移动构造函数通常接受一个右值引用作为参数,并且会“窃取”参数中的资源,然后将参数置于有效但不可用的状态(通常为空)。
class MyClass { public: // 移动构造函数 MyClass(MyClass&& other) noexcept { // 转移资源 data = other.data; size = other.size; // 将原对象置于有效状态(通常为空) other.data = nullptr; other.size = 0; } }; 复制代码
移动构造函数的几个要点:
参数类型:必须是同类型的右值引用,即 MyClass&&。
通常不会抛出异常,因此应该用 noexcept 修饰,这对于标准库容器在重新分配内存时使用移动而非拷贝很重要。
移动构造函数会“窃取”源对象的资源,然后将源对象的资源指针置为空(或其他合适的状态),这样源对象的析构函数就不会释放这些资源了。
移动构造函数的使用场景:
移动构造函数与拷贝构造函数的区别:
如果类没有定义移动构造函数,编译器可能会自动生成一个(在某些条件下),但自动生成的移动构造函数只是简单地移动每个成员变量(对于内置类型就是拷贝,对于类类型则调用其移动构造函数)。如果类有自定义的拷贝构造函数、拷贝赋值运算符或析构函数,编译器不会自动生成移动构造函数。这时,如果需要移动操作,就必须自己定义。
8 异常安全与设计模式
8.1 try_catch正确用法(不滥用)
避免滥用的准则:
不要用异常替代正常控制流
不要捕获所有异常而不处理
最小化try块范围
应该使用异常的情况:
构造函数失败
资源获取失败(文件、网格、内存等)
程序状态无法恢复的错误
库/API的边界处
不应该使用异常的情况:
正常的控制流程
频繁发生的错误(使用返回值)
性能关键的代码路径
析构函数(通常)
信号处理程序
8.2 noexcept关键字
void myFunction() noexcept; // 保证不抛出异常 void anotherFunction() noexcept(true); // 同上,明确指定不抛出 void yetAnotherFunction() noexcept(false); // 可能抛出异常 复制代码
为什么使用 noexcept:
性能优化 :编译器知道函数不会抛出异常后,可以生成更高效的代码,因为不需要处理异常抛出的情况。
// 编译器可以为noexcept函数生成更高效的代码 void optimizedFunction() noexcept { // 编译器知道不会抛出异常,可以: // 1. 减少异常处理表的生成 // 2. 进行更多内联优化 // 3. 减少栈展开代码 for (int i = 0; i < 1000; ++i) { // 密集计算 } } 复制代码
移动操作和析构函数 :标准库中的一些操作(如std::vector的重新分配)会使用移动操作(如果它们被标记为noexcept),否则会使用拷贝操作。因此,将移动构造函数和移动赋值运算符标记为noexcept可以提高性能。
代码清晰性 :明确告诉其他开发者该函数不会抛出异常,简化错误处理。
注意事项:
违反noexcept说明 :如果函数被标记为noexcept但抛出了异常,程序会调用std::terminate()终止。因此,确保noexcept函数真的不会抛出异常。
析构函数 :析构函数默认是noexcept的,除非显式指定为noexcept(false)。通常,析构函数不应该抛出异常。
移动操作 :如果移动操作可能抛出异常,那么它不应该被标记为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模式的基本步骤:
在头文件中声明一个实现类(通常是一个嵌套类,但也可以是非嵌套的)的前向声明,然后声明一个指向该实现类的指针(通常使用智能指针,如std::unique_ptr)。
在源文件中定义实现类,并实现原类的成员函数,这些成员函数通过操作实现类来完成功能。
不使用pimpl模式时:
// widget.h #include <string> #include <vector> class Widget { public: Widget(); ~Widget(); void doSomething(); private: std::string name; std::vector<int> data; // 其他私有成员,可能涉及更多的头文件包含 }; 复制代码
使用pimpl模式后:
// widget.h #include <memory> class Widget { public: Widget(); ~Widget(); // 需要声明,因为std::unique_ptr需要知道析构函数 Widget(Widget&&) = default; // 移动构造 Widget& operator=(Widget&&) = default; // 移动赋值 // 注意:需要禁止拷贝构造和拷贝赋值,或者实现深拷贝(因为unique_ptr不可拷贝) Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete; void doSomething(); private: class Impl; // 前向声明 std::unique_ptr<Impl> pImpl; }; // widget.cpp #include "widget.h" #include <string> #include <vector> //在源文件中定义Impl类并实现Widget的成员函数 class Widget::Impl { public: std::string name; std::vector<int> data; // 其他私有成员 }; Widget::Widget() : pImpl(std::make_unique<Impl>()) {} // 必须定义析构函数,因为Impl是不完整类型,而unique_ptr的默认析构需要完整类型 // 但是,如果使用std::unique_ptr,我们需要在Widget的析构函数之前知道Impl的完整类型 // 因此,在widget.cpp中,Impl已经定义,所以析构函数可以正常编译。 Widget::~Widget() = default; void Widget::doSomething() { // 通过pImpl访问实现 pImpl->name = "example"; pImpl->data.push_back(42); } 复制代码
注意:由于使用了std::unique_ptr,我们需要在头文件中声明析构函数,并在源文件中实现(即使它是默认的)。这是因为std::unique_ptr的析构函数需要知道Impl的完整类型,而在头文件中,Impl只是一个前向声明,所以编译器无法生成默认的析构函数。因此,我们必须在Impl类定义之后(即在源文件中)定义析构函数。
另外,需要注意的是,由于使用了std::unique_ptr,Widget类默认是不支持拷贝的(因为std::unique_ptr不可拷贝)。如果需要支持拷贝,需要手动实现拷贝构造函数和拷贝赋值运算符,对Impl进行深拷贝。
Pimpl模式的优点:
隐藏实现细节,减少头文件暴露的信息。
减少编译依赖,提高编译速度。当头文件发生变化时,只有直接依赖它的文件需要重新编译。
接口与实现分离,提高封装性。
缺点:
代码变得复杂,需要额外的指针间接访问,可能带来性能上的微小开销。
内存分配和释放开销(可以使用自定义分配器来优化)。
调试可能变得更困难,因为实现细节被隐藏。
9 并发与内存模型
9.1 thread与async
std::thread是C++11引入的线程库的一部分,它代表一个单独的执行线程。创建std::thread对象时,会启动一个新的线程来执行给定的函数或可调用对象。
#include <iostream> #include <thread> void hello() { std::cout << "Hello from thread!" << std::endl; } int main() { std::thread t(hello); t.join(); // 等待线程结束 return 0; } 复制代码 #include <iostream> #include <thread> void compute(int& result) { result = 42; } int main() { int result = 0; std::thread t(compute, std::ref(result));//通过引用传递来获取结果,并且需要手动同步(这里使用了join,所以没有问题) t.join(); std::cout << "Result: " << result << std::endl; return 0; } 复制代码
特点:
std::async是一个更高层次的抽象,它用于异步执行任务,并返回一个std::future对象,该对象最终将持有函数的返回值。std::async可以选择立即启动线程(异步执行)或者延迟执行(直到future被请求结果时才执行)
#include <iostream> #include <future> int compute() { return 42; } int main() { std::future<int> result = std::async(std::launch::async, compute);//通过future直接获取返回值,更加简洁安全 std::cout << "Result: " << result.get() << std::endl; return 0; } 复制代码
启动策略:
总结:
注意:
使用std::thread时,要确保线程在结束前被正确join或detach,否则程序会终止。
使用std::async时,要注意默认启动策略可能不是立即异步执行,如果需要确保异步,显式指定std::launch::async。
std::async返回的future在析构时会阻塞等待任务完成(如果任务是异步执行的),这有时会导致意外的阻塞。如果不需要结果,可以存储future对象以避免阻塞。
9.2 互斥锁
互斥锁用于保护共享数据免受多个线程同时访问,从而避免数据竞争和不一致性。
互斥锁类型:
std::mutex :最基本的互斥锁,不可递归,即同一线程不可重复锁定。
std::recursive_mutex :递归互斥锁,同一线程可以多次锁定,但需要相同次数的解锁。
std::timed_mutex :带超时功能的互斥锁,允许尝试锁定一段时间或直到某个时间点。
std::recursive_timed_mutex :带超时功能的递归互斥锁。
锁定方法:
互斥锁管理类(RAII):
为了避免忘记解锁互斥锁,C++提供了几种基于RAII的类,在构造时锁定互斥锁,在析构时自动解锁。
std::lock_guard :简单的RAII互斥锁包装器,构造时锁定,析构时解锁。不提供手动锁定和解锁接口。
std::unique_lock :更灵活的RAII互斥锁包装器,允许延迟锁定、提前解锁以及转移所有权。它还可以与条件变量一起使用。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; int shared_data = 0; void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 构造时锁定,析构时解锁 ++shared_data; } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "shared_data = " << shared_data << std::endl; return 0; } 复制代码 #include <iostream> #include <thread> #include <mutex> std::mutex mtx; int shared_data = 0; void increment() { for (int i = 0; i < 100000; ++i) { std::unique_lock<std::mutex> lock(mtx); // 类似于lock_guard,但更灵活 ++shared_data; // 可以提前解锁 lock.unlock(); // 做一些不需要互斥锁的操作 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "shared_data = " << shared_data << std::endl; return 0; } 复制代码 #include <iostream> #include <thread> #include <mutex> std::mutex mtx; void try_lock_example(int id) { if (mtx.try_lock()) { std::cout << "Thread " << id << " locked successfully.\n"; // 模拟一些工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); mtx.unlock(); } else { std::cout << "Thread " << id << " failed to lock.\n"; } } int main() { std::thread t1(try_lock_example, 1); std::thread t2(try_lock_example, 2); t1.join(); t2.join(); return 0; } 复制代码 #include <iostream> #include <thread> #include <mutex> #include <chrono> std::timed_mutex mtx; void timed_lock_example(int id) { // 尝试锁定最多100毫秒 if (mtx.try_lock_for(std::chrono::milliseconds(100))) { std::cout << "Thread " << id << " locked successfully.\n"; std::this_thread::sleep_for(std::chrono::milliseconds(200)); mtx.unlock(); } else { std::cout << "Thread " << id << " failed to lock within 100ms.\n"; } } int main() { std::thread t1(timed_lock_example, 1); std::thread t2(timed_lock_example, 2); t1.join(); t2.join(); return 0; } 复制代码
互斥锁与条件变量(std::condition_variable)一起使用,条件变量允许线程等待某个条件成立。
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void worker_thread() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 等待ready为true std::cout << "Worker thread is processing...\n"; } int main() { std::thread worker(worker_thread); // 模拟一些准备工作 std::this_thread::sleep_for(std::chrono::seconds(1)); { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // 通知等待的线程 worker.join(); return 0; } 复制代码
9.3 死锁预防
要发生死锁,必须同时满足以下四个条件:
互斥条件 :资源不能被共享,只能独占使用
持有并等待 :线程持有资源并等待其他资源
不可抢占 :资源不能被强制剥夺
循环等待 :存在线程的循环等待链
死锁预防是并发编程中的重要课题。在实际编程中,最常用且有效的方法是:
避免死锁的关键原则:关键原则 :
最少锁原则 :只在必要时使用锁
最短时间原则 :持有锁的时间尽可能短
固定顺序原则 :始终按相同顺序获取锁
单层锁定原则 :避免嵌套锁
超时退避原则 :为锁操作设置超时
9.4 原子操作
原子操作是并发编程中的一个核心概念,它指的是一个或多个操作的执行是不可中断的,即这些操作要么全部执行完成,要么都不执行,并且不会出现部分执行的状态。原子操作在多线程环境中尤为重要,因为它们可以用于实现无锁数据结构、计数器等,避免使用互斥锁带来的开销和潜在的死锁问题。
创建原子对象:
#include <atomic> // 基本整型原子类型 std::atomic<int> atomic_int; std::atomic<long> atomic_long; std::atomic<bool> atomic_bool; std::atomic<char> atomic_char; // 浮点型原子类型(C++20) std::atomic<float> atomic_float; std::atomic<double> atomic_double; // 指针原子类型 std::atomic<int*> atomic_ptr; // 用户自定义类型(需要满足一定条件) struct Point { int x, y; }; std::atomic<Point> atomic_point; 复制代码
基本操作接口:
std::atomic<int> value(0); // 基本操作 value.store(42); // 原子存储 int x = value.load(); // 原子加载 int old = value.exchange(100); // 原子交换 // 算术运算 int prev = value.fetch_add(5); // 原子加 prev = value.fetch_sub(3); // 原子减 prev = value.fetch_and(0xFF); // 原子与 prev = value.fetch_or(0x01); // 原子或 prev = value.fetch_xor(0x0F); // 原子异或 // 比较交换操作(CAS - Compare And Swap) int expected = 50; bool success = value.compare_exchange_weak(expected, 60); // 如果 value == expected,则设置为60,否则 expected 被更新为当前值 复制代码
9.4.1 内存顺序
宽松顺序:
在线程1中,操作A和操作B是两个独立的原子操作,它们之间没有依赖关系。在宽松内存顺序下,编译器和CPU可能会重排这两个操作,即先执行操作B,再执行操作A。
在线程2中,操作C和操作D也是两个独立的原子操作,但是操作C循环等待y变为1,所以操作D必须在操作C完成后执行。
std::atomic<int> x(0), y(0); int r1, r2; // 线程1 x.store(1, std::memory_order_relaxed); y.store(1, std::memory_order_relaxed); // 线程2 while (y.load(std::memory_order_relaxed) != 1) {} r1 = x.load(std::memory_order_relaxed); // 问题:r1 可能为0吗?是的!因为宽松顺序不保证可见性顺序 复制代码
获取-释放顺序:
获取-释放顺序用于在同一个原子变量上建立同步关系。一个线程使用 release 语义存储一个值,另一个线程使用 acquire 语义加载该值,那么第一个线程中在 store 之前的所有写操作,在第二个线程中 load 之后都是可见的。
std::atomic<int> flag(0); int data = 0; // 线程1:生产者 data = 42; // 1 flag.store(1, std::memory_order_release); // 2 // 线程2:消费者 while (flag.load(std::memory_order_acquire) == 0) {} // 3 int result = data; // 4,这里保证看到 data = 42 // 关键保证:1 happens-before 2 synchronizes-with 3 happens-before 4 复制代码
顺序一致性:
顺序一致性保证了所有线程看到的操作顺序是一致的,且与代码顺序一致。但是,它也是性能开销最大的一种。
std::atomic<int> x(0), y(0); int r1, r2; // 线程1 x.store(1, std::memory_order_seq_cst); // A r1 = y.load(std::memory_order_seq_cst); // B // 线程2 y.store(1, std::memory_order_seq_cst); // C r2 = x.load(std::memory_order_seq_cst); // D // 保证:不可能出现 r1 == 0 && r2 == 0 // 所有seq_cst操作构成一个全序 复制代码
9.4.2 比较交换(CAS)
比较并交换(Compare and Swap)是原子操作中非常重要的一种操作,它用于实现无锁数据结构。CAS 操作会检查原子对象的当前值是否等于期望值,如果相等,则将其设置为新值。
C++ 提供了两个版本的 CAS:compare_exchange_weak 和 compare_exchange_strong。
std::atomic<int> value(10); int expected = 10; bool success = value.compare_exchange_strong(expected, 20); // 如果 value 当前为10,则将其设置为20,success 为 true,expected 不变(仍为10) // 否则,将 expected 更新为 value 的当前值,success 为 false 复制代码