C++基础复习笔记
一、数组定义
在C++中,数组初始化有多种方式,以下是常见的几种方法:
默认初始化
数组元素未显式初始化时,内置类型(如int
、float
)的元素值未定义(垃圾值),类类型调用默认构造函数。
int arr1[5]; // 元素值未定义
聚合初始化(列表初始化)
使用花括号{}
直接初始化所有元素。若列表元素少于数组长度,剩余元素默认初始化(内置类型为0)。
int arr2[3] = {1, 2, 3}; // 完全初始化
int arr3[5] = {1, 2}; // 部分初始化,剩余为0
省略数组长度
编译器自动推断数组长度,适用于初始化列表完整的情况。
int arr4[] = {1, 2, 3, 4}; // 编译器推断长度为4
值初始化
使用空花括号{}
或={}
,所有元素初始化为0或默认值。
int arr5[3] = {}; // 全部初始化为0
int arr6[3]{}; // C++11统一初始化语法
字符数组的特殊初始化
字符数组可以用字符串字面量初始化,注意预留\0
的空间。
char str1[6] = "hello"; // 自动添加\0
char str2[] = "world"; // 编译器推断长度为6
C++11后的扩展初始化
支持省略等号、嵌套列表初始化等。
int arr7[2][3] { {1, 2, 3}, {4, 5, 6} }; // 多维数组初始化
每种方式适用于不同场景,需根据实际需求选择。列表初始化是推荐做法,因其清晰且能避免未定义行为。
二、常量指针和指针
常量指针(Pointer to Constant)
常量指针指向的内容不可修改,但指针本身可以重新指向其他地址。
语法形式:const int* ptr
或 int const* ptr
。
示例:
const int value = 10;
const int* ptr = &value;
// *ptr = 20; // 错误:不能修改指向的内容
int another = 30;
ptr = &another; // 正确:指针本身可以修改
指针常量(Constant Pointer)
指针本身不可修改(必须初始化且不能指向其他地址),但可以通过指针修改指向的内容。
语法形式:int* const ptr
。
示例:
int value = 10;
int* const ptr = &value;
*ptr = 20; // 正确:可以修改指向的内容
// ptr = &another; // 错误:指针本身不能修改
关键差异总结
- 常量指针:指向的数据是常量,指针可变。
- 指针常量:指针是常量,指向的数据可变。
- 双重常量(const int const ptr)*:指针和指向的数据均不可变。
记忆技巧
从右向左读声明:
const int* ptr
→ “ptr is a pointer to a const int”(指向常量的指针)。int* const ptr
→ “ptr is a const pointer to int”(常量指针)。
三、字符串初始化方式及基本操作
字符串初始化方式
在C++中,字符串可以通过多种方式初始化,以下是常见的几种方法:
使用双引号直接初始化
std::string str = "Hello, World!";
使用构造函数初始化
std::string str1; // 默认构造函数,空字符串
std::string str2(5, 'a'); // 重复字符构造,结果为 "aaaaa"
std::string str3(str2); // 拷贝构造函数
使用字符数组初始化
char charArray[] = {'H', 'e', 'l', 'l', 'o'};
std::string str4(charArray, 5); // 从字符数组构造
使用部分字符串初始化
std::string original = "Hello, World!";
std::string str5(original, 7, 5); // 从原字符串的第7个字符开始取5个字符,结果为 "World"
使用迭代器初始化
std::vector<char> vec = {'H', 'e', 'l', 'l', 'o'};
std::string str6(vec.begin(), vec.end());
字符串基本操作
访问字符
std::string str = "Hello";
char ch = str[0]; // 获取第一个字符 'H'
char ch_at = str.at(1); // 获取第二个字符 'e',会检查边界
修改字符串
str[0] = 'h'; // 修改第一个字符为 'h'
str.append(" World"); // 追加字符串,结果为 "hello World"
str.push_back('!'); // 追加单个字符,结果为 "hello World!"
str.insert(5, ", C++"); // 在位置5插入字符串,结果为 "hello, C++ World!"
str.erase(5, 5); // 从位置5删除5个字符,结果为 "hello World!"
str.replace(6, 5, "Universe"); // 从位置6替换5个字符,结果为 "hello Universe!"
字符串比较
在C++中,字符串的比较确实遵循字典序(lexicographical order)原则,即逐个字符进行对比。这种比较方式适用于标准库中的std::string
类以及C风格的字符串(如char*
)
std::string str1 = "apple";
std::string str2 = "banana";
bool isEqual = (str1 == str2); // false
bool isLess = (str1 < str2); // true
字符串长度和容量
size_t len = str.length(); // 或 str.size()
bool isEmpty = str.empty();
str.resize(10); // 调整字符串大小
size_t cap = str.capacity(); // 当前分配的存储空间
子串操作
std::string substr = str.substr(6, 8); // 从位置6开始取8个字符
size_t pos = str.find("Universe"); // 查找子串位置
输入输出操作
std::cout << str << std::endl;
std::cin >> str; // 输入字符串(遇到空格停止)
std::getline(std::cin, str); // 输入一行字符串
转换操作
const char* cstr = str.c_str(); // 转换为C风格字符串
int num = std::stoi("123"); // 字符串转整数
double d = std::stod("3.14"); // 字符串转浮点数
std::string numStr = std::to_string(123); // 数值转字符串
四、结构体初始化与内存对齐
结构体初始化方法
在C++中,结构体可以通过多种方式初始化。现代C++提供了灵活的初始化语法,包括聚合初始化、列表初始化以及构造函数初始化。
聚合初始化适用于没有用户定义构造函数、私有或保护非静态数据成员的结构体:
struct Point {int x;int y;
};Point p1 = {10, 20}; // C风格聚合初始化
Point p2{30, 40}; // C++11列表初始化
对于包含构造函数的复杂结构体:
struct Employee {std::string name;int id;Employee(std::string n, int i) : name(std::move(n)), id(i) {}
};Employee e{"John", 1001};
内存对齐原理
结构体内存对齐遵循以下基本原则:
- 每个成员的偏移量必须是其类型对齐值的整数倍
- 结构体总大小必须是最大成员对齐值的整数倍
- 编译器可能插入填充字节(padding)以满足对齐要求
典型对齐值示例:
- char: 1字节
- short: 2字节
- int: 4字节
- double: 8字节(32位系统可能为4字节)
对齐控制方法
使用alignas指定对齐要求:
struct alignas(16) AlignedStruct {char c;int i;double d;
};
通过#pragma pack修改默认对齐:
#pragma pack(push, 1)
struct PackedStruct {char c;int i;
};
#pragma pack(pop)
C++11标准布局类型要求:
- 所有非静态成员具有相同的访问控制
- 没有虚函数或虚基类
- 非静态成员都是标准布局类型
实际应用示例
计算结构体大小和对齐:
struct Example {char a; // 偏移0// 3字节填充int b; // 偏移4double c; // 偏移8short d; // 偏移16// 6字节填充(总大小需是8的倍数)
}; // 总大小24字节
跨平台注意事项:
- 不同平台可能有不同的默认对齐规则
- 网络传输或文件存储时应使用1字节对齐
- 关键数据结构建议显式指定对齐方式### 结构体初始化方法
五、常见容器类及适用场景
1. vector
(动态数组)
适用场景:
需要随机访问、尾部频繁插入/删除,元素连续存储的场景(如数据缓存、动态数据集)
#include <iostream>
#include <vector>int main() {std::vector<int> nums{7, 3, 5}; // 初始化nums.push_back(9); // 尾部插入nums.pop_back(); // 尾部删除std::cout << "元素: ";for (int n : nums) std::cout << n << " "; // 遍历输出: 7 3 5return 0;
}
2. Array(数组)
适用场景:
-
固定大小的数据存储
std::array 适用于已知编译时确定大小的场景,例如存储固定数量的配置参数、物理常数或预定义表。其大小在编译时确定,避免了动态内存分配的开销。constexpr std::array<double, 3> gravity = {9.7803, 9.832, 9.80665}; // 不同纬度重力加速度
-
需要 STL 接口兼容性
当算法需要接受 STL 兼容容器(如需要.begin()/.end()迭代器)时,std::array 可以直接替代 C 风格数组,无需额外适配。std::array<int, 5> arr = {1,2,3,4,5}; std::sort(arr.begin(), arr.end()); // 直接使用 STL 算法
-
性能敏感场景
std::array 数据存储在栈上,访问速度与 C 风格数组相当,适合实时系统或高频调用的代码段。其内存局部性优于动态容器(如 std::vector)。// 矩阵乘法中的小块操作 std::array<std::array<float, 4>, 4> matrix_multiply(const std::array<std::array<float, 4>, 4>& a, const std::array<std::array<float, 4>, 4>& b);
-
作为函数参数或返回值
std::array 支持值语义,可以安全地作为函数参数或返回值传递,避免指针和手动内存管理。std::array<char, 16> generate_id() {std::array<char, 16> id;// ...填充数据return id; // 返回值优化(RVO)确保高效 }
-
元编程与 constexpr 上下文
std::array 完全支持 constexpr,可在编译时计算中使用,配合模板元编程时尤其有用。template<std::size_t N> constexpr std::array<int, N> create_factorial_table() {std::array<int, N> res{};res[0] = 1;for (int i=1; i<N; ++i) res[i] = res[i-1] * i;return res; }
-
替代 C 风格数组
std::array 提供.at()边界检查、size()方法等安全特性,同时保持相同性能,是 C 风格数组的类型安全替代品。std::array<int, 10> safe_arr; // safe_arr[10] = 5; // 未定义行为 safe_arr.at(10) = 5; // 抛出 std::out_of_range 异常
3. list
(双向链表)
适用场景:
频繁在任意位置插入/删除,不需要随机访问(如任务队列、撤销操作栈)
#include <iostream>
#include <list>int main() {std::list<std::string> words{"apple", "banana"};auto it = words.begin();it++; // 指向第二个元素words.insert(it, "orange"); // 在中间插入words.erase(words.begin()); // 删除头部for (auto& w : words) std::cout << w << " "; // 输出: orange bananareturn 0;
}
4. map
(红黑树字典)
适用场景:
需要按键排序的键值对存储(如字典、配置参数)
#include <iostream>
#include <map>int main() {std::map<int, std::string> students;students[101] = "Alice"; // 插入键值对students[102] = "Bob";students.erase(101); // 按键删除for (auto& [id, name] : students) std::cout << id << ":" << name << " "; // 输出: 102:Bobreturn 0;
}
5. unordered_map
(哈希字典)
适用场景:
需要快速查找的键值对,不要求顺序(如缓存系统、词频统计)
#include <iostream>
#include <unordered_map>int main() {std::unordered_map<std::string, int> wordCount;wordCount["hello"] = 1; // 插入wordCount["world"]++;std::cout << wordCount.at("world"); // 输出: 1return 0;
}
6. deque
(双端队列)
适用场景:
频繁在头尾插入/删除(如滑动窗口、排队系统)
#include <iostream>
#include <deque>int main() {std::deque<int> dq = {2, 3};dq.push_front(1); // 头部插入dq.push_back(4); // 尾部插入dq.pop_front(); // 头部删除for (int n : dq) std::cout << n << " "; // 输出: 2 3 4return 0;
}
7. set
(红黑树集合)
适用场景:
需要自动排序的唯一元素集合(如黑名单、词典)
#include <iostream>
#include <set>int main() {std::set<int> uniqueNums = {5, 3, 5, 2}; // 自动去重uniqueNums.insert(1);if (uniqueNums.find(3) != uniqueNums.end()) std::cout << "3存在"; // 输出: 3存在return 0;
}
8. stack
(栈)
适用场景:
后进先出(LIFO)操作(如函数调用栈、表达式求值)
#include <iostream>
#include <stack>int main() {std::stack<int> s;s.push(10); s.push(20); // 压栈std::cout << s.top() << " "; // 输出栈顶: 20s.pop(); // 弹栈std::cout << s.top(); // 输出: 10return 0;
}
9. queue
(队列)
适用场景:
先进先出(FIFO)操作(如消息队列、BFS算法)
#include <iostream>
#include <queue>int main() {std::queue<std::string> q;q.push("first"); // 入队q.push("second");std::cout << q.front() << " "; // 输出队首: firstq.pop(); // 出队std::cout << q.front(); // 输出: secondreturn 0;
}
容器选择建议
操作需求 | 推荐容器 |
---|---|
快速随机访问 | vector , array |
头尾频繁插入/删除 | deque |
中间频繁插入/删除 | list |
按键快速查找(有序) | map , set |
按键快速查找(无序) | unordered_map |
LIFO操作 | stack |
FIFO操作 | queue |
六、左值、右值和移动语义
左值(Lvalue)
左值是指能够明确标识内存位置的表达式,通常可以取地址。左值具有持久性,例如变量、函数返回的左值引用等。
int x = 10; // x是左值
int* ptr = &x; // 可以取地址
右值(Rvalue)
右值通常是临时对象或字面量,没有明确的内存位置,不能取地址。右值包括纯右值(如临时对象、字面量)和将亡值(即将被移动的对象)。
int y = 20; // 20是右值
int z = x + y; // (x + y)的结果是右值
左值引用与右值引用
左值引用(T&
)只能绑定到左值,右值引用(T&&
)只能绑定到右值。
int a = 5;
int& lref = a; // 左值引用
int&& rref = 10; // 右值引用
移动语义(Move Semantics)
移动语义通过右值引用避免不必要的拷贝,提升性能。std::move
将左值强制转换为右值引用,触发移动构造函数或移动赋值运算符。
class MyClass {
public: MyClass() = default; MyClass(MyClass&& other) noexcept { // 移动构造函数 // 转移资源 } MyClass& operator=(MyClass&& other) noexcept { // 移动赋值运算符 if (this != &other) { // 释放当前资源并转移 } return *this; }
}; MyClass obj1;
MyClass obj2 = std::move(obj1); // 调用移动构造函数
完美转发(Perfect Forwarding)
std::forward
保持参数的值类别(左值或右值),用于模板函数中实现完美转发。
template<typename T>
void wrapper(T&& arg) { func(std::forward<T>(arg)); // 保持arg的原始类别
}
注意事项
- 移动后对象应处于有效但未定义状态,通常为空或可析构。
- 移动构造函数和移动赋值运算符应标记为
noexcept
,避免异常问题。 std::move
仅转换类型,不执行移动操作,实际移动由构造函数或赋值运算符完成。
七、面向对象
1. RAII(资源获取即初始化)
核心思想:资源生命周期与对象绑定。对象构造时获取资源,析构时自动释放资源。避免内存泄漏和资源未释放问题。
class FileHandler {
public:FileHandler(const std::string& path) : file_(fopen(path.c_str(), "r")) {}~FileHandler() { if(file_) fclose(file_); } // 析构时自动释放
private:FILE* file_;
};
2. RTTI(运行时类型识别)
动态类型识别机制:
dynamic_cast
:安全向下转型Base* b = new Derived(); if (Derived* d = dynamic_cast<Derived*>(b)) { /* 成功转换 */ }
typeid
:获取类型信息std::cout << typeid(*b).name(); // 输出实际类型名
限制:需启用RTTI编译选项(-frtti
),且类至少含一个虚函数。
3. 默认构造函数
- 无参或所有参数有默认值的构造函数
- 编译器自动生成条件:未显式定义任何构造函数时
- 重要场景:
MyClass obj; // 调用默认构造 std::vector<MyClass> v(10); // 元素默认构造
4. explicit
关键字
禁止隐式类型转换:
class Timer {
public:explicit Timer(int ms) {} // 阻止隐式转换
};
// Timer t = 1000; // 错误!必须显式:Timer t(1000);
适用场景:单参数构造函数,避免意外类型转换。
5. 复制语义
成员 | 签名格式 | 作用 |
---|---|---|
复制构造函数 | ClassName(const ClassName&) | 深拷贝对象 |
复制赋值运算符 | ClassName& operator=(const ClassName&) | 对象赋值时深拷贝 |
6. 移动语义(C++11)
成员 | 签名格式 | 作用 |
---|---|---|
移动构造函数 | ClassName(ClassName&&) | 转移资源所有权 |
移动赋值运算符 | ClassName& operator=(ClassName&&) | 赋值时转移资源 |
class Buffer {
public:Buffer(Buffer&& other) : data_(other.data_), size_(other.size_) {other.data_ = nullptr; // 转移后置空原指针}
private:int* data_;size_t size_;
};
关键对比表
特性 | 核心目的 | 典型应用场景 |
---|---|---|
RAII | 自动化资源管理 | 文件/锁/内存管理 |
RTTI | 运行时类型安全操作 | 多态类型检查 |
explicit | 防止意外隐式转换 | 单参数构造的包装类 |
移动语义 | 优化临时对象资源转移 | 容器操作/大对象传递 |
最佳实践:
- 优先使用
=default
/=delete
显式控制特殊成员函数- 移动构造函数应标记
noexcept
- 资源管理类必须禁用复制语义或实现深拷贝### C++面向对象重点知识总结
八、类型转换
C++类型转换概述
C++提供四种类型转换运算符:static_cast
、dynamic_cast
、const_cast
、reinterpret_cast
,用于替代C风格的强制转换,增强安全性和可读性。
static_cast
用于编译时已知的静态类型转换,适用于相关类型间的转换(如基本类型、父子类指针等)。
double d = 3.14;
int i = static_cast<int>(d); // 基本类型转换 class Base {};
class Derived : public Base {};
Base* b = new Derived();
Derived* dd = static_cast<Derived*>(b); // 父子类指针转换(无运行时检查)
特点:
- 不进行运行时类型检查,不安全的上行转换(子类→父类)可能成功,但下行转换(父类→子类)需谨慎。
- 不能移除
const
或volatile
属性。
dynamic_cast
主要用于多态类型(含虚函数)的安全转换,依赖RTTI(运行时类型信息)。
class Base { virtual void foo() {} };
class Derived : public Base {}; Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 成功
Base* bb = dynamic_cast<Base*>(d); // 上行转换总是安全 Base* invalid = new Base();
Derived* fail = dynamic_cast<Derived*>(invalid); // 返回nullptr(指针)或抛出异常(引用)
特点:
- 下行转换时检查类型安全性,失败返回
nullptr
(指针)或抛出std::bad_cast
(引用)。 - 仅适用于多态类型(类至少有一个虚函数)。
const_cast
用于修改类型的const
或volatile
属性。
const int x = 10;
int* y = const_cast<int*>(&x); // 移除const
*y = 20; // 未定义行为(原变量可能为常量存储区) void print(char* str) { cout << str; }
const char* msg = "hello";
print(const_cast<char*>(msg)); // 安全用法:函数参数非修改场景
特点:
- 不能改变基础类型(如
int
→double
)。 - 修改原为
const
的值可能导致未定义行为。
reinterpret_cast
低级别重新解释位模式,用于无关类型间的危险转换。
int* p = new int(65);
char* ch = reinterpret_cast<char*>(p); // int* → char*
cout << *ch; // 输出'A'(ASCII 65) uintptr_t addr = reinterpret_cast<uintptr_t>(p); // 指针转整数
特点:
- 高度依赖平台,可能引发安全问题。
- 常见用途:指针与整数互转、函数指针类型转换。
旧式C风格转换
C++中仍支持但 discouraged,行为相当于组合使用上述四种转换。
int a = (int)3.14; // 类似static_cast
Base* b = (Base*)new Derived(); // 可能类似static_cast或reinterpret_cast
const int* pc = &a;
int* pv = (int*)pc; // 类似const_cast
风险:
- 缺少明确语义,易引入错误。
最佳实践
- 优先使用C++风格转换,明确意图。
dynamic_cast
仅用于多态类型,避免性能开销。const_cast
慎用,确保逻辑正确性。reinterpret_cast
仅在底层编程(如硬件操作)中使用。
九、模板
1. 模板基本概念
- 核心思想:编写与类型无关的通用代码,实现代码复用。
- 工作原理:编译器在编译期根据具体类型生成特化代码。
- 优势:避免重复代码,提升类型安全性和灵活性。
2. 函数模板
- 定义:通过参数化类型实现通用函数。
- 语法:
template <typename T> T max(T a, T b) {return (a > b) ? a : b; }
- 使用:编译器自动推导类型或显式指定:
max(3, 5); // 推导为 int max<double>(3.1, 2); // 显式指定
3. 类模板
- 定义:参数化类成员的类型。
- 语法:
template <typename T> class Stack { private:T elements[100];int top; public:void push(T const&);T pop(); };
- 实例化:
Stack<int> intStack; // 存储 int 类型 Stack<std::string> strStack; // 存储 string 类型
4. 非类型模板参数
- 用途:传递常量值(如整数、枚举、指针)。
- 示例:
template <typename T, int size> class Array {T data[size]; }; Array<double, 10> arr; // 大小为 10 的 double 数组
5. 模板特化
- 全特化:为特定类型提供特殊实现。
template <> class Stack<bool> { // 针对 bool 类型的特化// 优化存储(如位向量) };
- 偏特化:对部分参数特化(仅类模板支持)。
template <typename T> class Stack<T*> { // 针对指针类型的偏特化// 特殊处理指针 };
6. 可变参数模板(C++11)
- 用途:处理任意数量、任意类型的参数。
- 语法:
template <typename... Args> void log(Args... args) {// 使用折叠表达式或递归展开参数包 }
- 示例:
log("Error:", 42, 3.14); // 接受多个参数
7. 模板元编程(TMP)
- 核心:在编译期执行计算,生成高效代码。
- 示例:编译期阶乘计算:
template <int N> struct Factorial {static const int value = N * Factorial<N-1>::value; }; template <> struct Factorial<0> {static const int value = 1; }; int x = Factorial<5>::value; // 编译期计算 120
8. 注意事项
- 编译分离问题:模板定义需在头文件中(链接器限制)。
- 类型约束:C++20 引入
concepts
明确类型要求。 - 性能:过度使用模板可能导致编译时间增加。
9. 典型应用场景
- 标准库容器(
vector<T>
,map<K, V>
) - 算法(
std::sort
,std::find
) - 智能指针(
shared_ptr<T>
) - 元编程库(如 Boost.MPL)
十、智能指针
核心概念
智能指针是管理动态内存的RAII(资源获取即初始化)对象,自动释放内存,防止内存泄漏。主要类型:
1. std::unique_ptr
- 独占所有权:同一时间只有一个指针可持有资源
- 轻量高效:零额外开销(无引用计数)
- 移动语义:可通过
std::move
转移所有权
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1变为nullptr
2. std::shared_ptr
- 共享所有权:多个指针共享同一资源
- 引用计数:通过控制块记录引用数,计数归零时释放资源
- 线程安全:引用计数原子操作(但资源访问需额外同步)
auto ptr3 = std::make_shared<int>(100);
auto ptr4 = ptr3; // 引用计数+1
3. std::weak_ptr
- 观察者模式:不增加引用计数,避免循环引用
- 需转换为
shared_ptr
访问资源
std::weak_ptr<int> weak = ptr3;
if (auto temp = weak.lock()) { // 尝试获取shared_ptr// 使用*temp
}
关键特性对比
特性 | unique_ptr | shared_ptr | weak_ptr |
---|---|---|---|
所有权 | 独占 | 共享 | 无所有权 |
复制语义 | ❌ | ✅ | ✅(不增计数) |
自定义删除器 | ✅ | ✅ | ❌ |
循环引用风险 | 无 | 有 | 解决方案 |
内存开销 | 极小 | 控制块开销 | 控制块开销 |
最佳实践
1. 优先使用make_xxx
auto p = std::make_unique<MyClass>(); // 避免显式new
- 异常安全:防止内存泄漏
- 性能优化:减少内存分配次数
2. 避免循环引用
class Node {std::shared_ptr<Node> next;std::weak_ptr<Node> prev; // 用weak_ptr打破循环
};
3. unique_ptr
作为工厂返回值
std::unique_ptr<Base> createObject(int type) {if (type == 1) return std::make_unique<Derived1>();else return std::make_unique<Derived2>();
}
4. weak_ptr
检查资源有效性
if (!weak.expired()) { // 检查资源是否存在auto res = weak.lock();
}
常见错误
1. 误用裸指针初始化
int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 错误!双重释放
2. 忽略自定义删除器
FILE* f = fopen("file.txt", "r");
std::unique_ptr<FILE, decltype(&fclose)> file(f, &fclose); // 需指定删除器
3. shared_ptr
循环引用
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; }; // 内存泄漏!
智能指针显著提升内存安全性,但需理解所有权语义。建议结合Valgrind/AddressSanitizer工具检测内存问题。
十一、并发与多线程
1. 线程基础
-
线程创建:使用
std::thread
类#include <thread> void task() { /* 任务逻辑 */ } std::thread t(task); // 创建线程 t.join(); // 等待线程结束
-
线程分离:
detach()
使线程在后台运行std::thread t(task); t.detach(); // 主线程不再管理此线程
2. 互斥锁(Mutex)
- 作用:防止数据竞争,确保临界区互斥访问
- 类型:
std::mutex
:基础互斥锁std::recursive_mutex
:可重入锁std::timed_mutex
:支持超时锁定
- 使用示例:
std::mutex mtx; void safe_increment(int& counter) {mtx.lock();++counter; // 临界区mtx.unlock(); }
3. 智能锁管理
lock_guard
:RAII风格自动锁管理std::mutex mtx; void safe_func() {std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁// 临界区操作 }
unique_lock
:更灵活的锁(支持延迟锁定、转移所有权)std::mutex mtx; void flexible_func() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock);lock.lock(); // 手动加锁// 操作...lock.unlock(); // 可手动解锁 }
4. 条件变量(Condition Variable)
- 作用:线程间同步,等待特定条件成立
- 典型生产者-消费者模式:
std::mutex mtx; std::condition_variable cv; std::queue<int> data_queue;void producer() {while (true) {std::unique_lock<std::mutex> lock(mtx);data_queue.push(42);cv.notify_one(); // 通知消费者} }void consumer() {while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, []{ return !data_queue.empty(); }); // 等待非空条件int data = data_queue.front();data_queue.pop();} }
5. 原子操作(Atomic)
- 作用:无锁线程安全操作
- 示例:
#include <atomic> std::atomic<int> counter(0);void increment() {counter.fetch_add(1, std::memory_order_relaxed); }
- 内存序:
memory_order_relaxed
:宽松顺序memory_order_seq_cst
:严格顺序(默认)
6. 异步任务(Async/Future)
-
std::async
:异步执行函数 -
std::future
:获取异步结果#include <future> int compute() { return 42; }std::future<int> fut = std::async(compute); int result = fut.get(); // 阻塞获取结果
-
std::packaged_task
:封装可调用对象(如函数、Lambda),执行结果与std::future绑定#include <future> #include <iostream> #include <thread>int main() {// 封装Lambda任务(计算平方)std::packaged_task<int(int)> task([](int x) { return x * x; });// 获取关联的futurestd::future<int> result = task.get_future();// 在独立线程执行任务std::thread t(std::move(task), 5);t.detach();// 获取结果(阻塞直到完成)std::cout << "Result: " << result.get() << std::endl; // 输出25 }
-
std::promise
:显式设置异步操作的结果值或异常#include <future> #include <thread>void compute(std::promise<int> prom) {try {int res = 42; // 复杂计算prom.set_value(res); // 设置结果} catch(...) {prom.set_exception(std::current_exception()); // 传递异常} }int main() {std::promise<int> prom;std::future<int> fut = prom.get_future();std::thread t(compute, std::move(prom));// 等待结果std::cout << "Result: " << fut.get() << std::endl; // 输出42t.join(); }
7. 线程安全设计原则
- 避免共享数据:使用线程局部存储(
thread_local
)thread_local int local_var = 0; // 每个线程独立副本
- 锁粒度最小化:减少临界区范围
- 预防死锁:
- 固定锁获取顺序
- 使用
std::lock()
同时锁定多个互斥量std::lock(mtx1, mtx2); // 原子化锁定 std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
8. 性能优化
- 无锁数据结构:如
std::atomic_flag
实现自旋锁 - 缓存对齐:避免伪共享(False Sharing)
struct alignas(64) CacheLineAligned { int data; // 单独缓存行 };
问题
怎么实现一个内存池?
#pragma once
class MyMemoPool {
public:static void* operator new(size_t size);static void operator delete(void* pHead);static int m_iCount; // 分配计数统计,每new一次+1static int m_iMallocCount; // 统计malloc次数,每malloc一次+1
private:MyMemoPool* next;static MyMemoPool* m_FreePos; // 总是指向一块可以分配出去的内存首地址static int m_sTrunkCount; // 一次分配多少倍该类的内存
};
#include "MyMemoPool.h"
#include <memory>#define MYMEMPOOL 1void* MyMemoPool::operator new(size_t size) {
#ifndef MYMEMPOOLMyMemoPool* pPoint = (MyMemoPool*)malloc(size);return pPoint;
#endif MyMemoPool* tmpLink;if (m_FreePos == nullptr) {// 为空,我们要申请内存,申请很大一块内存size_t realsize = m_sTrunkCount * size;m_FreePos = reinterpret_cast<MyMemoPool*>(new char[realsize]);tmpLink = m_FreePos;// 把分配的这一大块内存链接起来,供后续使用for (; tmpLink != &m_FreePos[m_sTrunkCount - 1]; ++tmpLink) {tmpLink->next = tmpLink + 1;}tmpLink->next = nullptr;++m_iMallocCount;}tmpLink = m_FreePos;m_FreePos = m_FreePos->next;++m_iCount;return tmpLink;
}void MyMemoPool::operator delete(void* pHead) {
#ifndef MYMEMPOOLfree(pHead);return;
#endif // MYMEMPOOL(static_cast<MyMemoPool*>(pHead))->next = m_FreePos;m_FreePos = static_cast<MyMemoPool*>(pHead);
}int MyMemoPool::m_iCount = 0;
int MyMemoPool::m_iMallocCount = 0;
MyMemoPool* MyMemoPool::m_FreePos = nullptr;
int MyMemoPool::m_sTrunkCount = 350;