C++基础复习笔记

发布于:2025-07-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、数组定义

在C++中,数组初始化有多种方式,以下是常见的几种方法:

默认初始化

数组元素未显式初始化时,内置类型(如intfloat)的元素值未定义(垃圾值),类类型调用默认构造函数。

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* ptrint 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;       // 偏移4
    double c;    // 偏移8
    short 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 5
    return 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 banana
    return 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:Bob
    return 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"); // 输出: 1
    return 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 4
    return 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() << " ";       // 输出栈顶: 20
    s.pop();                           // 弹栈
    std::cout << s.top();              // 输出: 10
    return 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() << " ";     // 输出队首: first
    q.pop();                           // 出队
    std::cout << q.front();            // 输出: second
    return 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 防止意外隐式转换 单参数构造的包装类
移动语义 优化临时对象资源转移 容器操作/大对象传递

最佳实践

  1. 优先使用=default/=delete显式控制特殊成员函数
  2. 移动构造函数应标记noexcept
  3. 资源管理类必须禁用复制语义或实现深拷贝### C++面向对象重点知识总结

八、类型转换

C++类型转换概述

C++提供四种类型转换运算符:static_castdynamic_castconst_castreinterpret_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); // 父子类指针转换(无运行时检查)

特点

  • 不进行运行时类型检查,不安全的上行转换(子类→父类)可能成功,但下行转换(父类→子类)需谨慎。
  • 不能移除constvolatile属性。

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

用于修改类型的constvolatile属性。

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)); // 安全用法:函数参数非修改场景

特点

  • 不能改变基础类型(如intdouble)。
  • 修改原为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  

风险

  • 缺少明确语义,易引入错误。

最佳实践

  1. 优先使用C++风格转换,明确意图。
  2. dynamic_cast仅用于多态类型,避免性能开销。
  3. const_cast慎用,确保逻辑正确性。
  4. 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; 
        });
        
        // 获取关联的future
        std::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; // 输出42
        t.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一次+1
	static int m_iMallocCount;  // 统计malloc次数,每malloc一次+1
private:
	MyMemoPool* next;
	static MyMemoPool* m_FreePos; // 总是指向一块可以分配出去的内存首地址
	static int m_sTrunkCount;     // 一次分配多少倍该类的内存
};
#include "MyMemoPool.h"
#include <memory>

#define MYMEMPOOL 1

void* MyMemoPool::operator new(size_t size) {
#ifndef MYMEMPOOL
	MyMemoPool* 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 MYMEMPOOL
	free(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;