一、智能指针

记忆口诀:智能指针三兄弟,unique独占share共享,weak观测防循环,RAII思想记心上

智能指针是一种自动管理动态内存的工具类,用于防止内存泄漏。C++提供了三种常用的智能指针:

  1. unique_ptr(独占智能指针)

    • 独占对象所有权,同一时间只能有一个指针指向一个对象
    • 禁止拷贝构造和拷贝赋值,支持移动语义
    • 适合独占资源的场景
  2. shared_ptr(共享智能指针)

    • 共享对象所有权,允许多个指针指向同一个对象
    • 使用引用计数,当引用计数为0时释放资源
    • 可以通过use_count()查看引用计数
  3. weak_ptr(弱引用指针)

    • 不拥有资源,不增加引用计数
    • 用于解决shared_ptr的循环引用问题
    • 需要通过lock()方法提升为shared_ptr才能访问资源

RAII机制(资源获取即初始化)

  • 当创建智能指针对象时,它立即接管资源
  • 当智能指针生命周期结束时,自动调用析构函数释放资源
  • 无需手动调用delete,有效防止内存泄漏

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// unique_ptr示例
std::unique_ptr<int> ptr1(new int(10));
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确,支持移动

// shared_ptr示例
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1; // 引用计数+1

// weak_ptr示例
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加引用计数
if (auto shared = wp.lock()) { // 安全访问
// 使用shared访问资源
}

二、C++内存分区

记忆口诀:内存分区五大块,栈堆全局常量代码,各司其职不混乱,生命周期要明白

C++程序运行时,内存被分为五个不同的区域:

  1. 栈区(Stack)

    • 存储函数的局部变量、函数参数和函数调用信息
    • 自动分配和释放,速度快
    • 生命周期与函数执行期相同
  2. 堆区(Heap)

    • 存储动态分配的内存
    • 需要手动分配(new/malloc)和释放(delete/free)
    • 生命周期由程序员控制
  3. 全局/静态区(Global/Static)

    • 存储全局变量和静态变量
    • 程序启动时分配,结束时释放
    • 生命周期贯穿整个程序运行期间
  4. 常量区(Const)

    • 也称为只读区
    • 存储常量数据,如字符串常量
    • 不可修改
  5. 代码区(Code)

    • 存储程序的可执行代码
    • 只读

三、内存泄漏

记忆口诀:内存泄漏三类型,堆内存系统资源虚析构,防止泄漏有妙招,智能指针RAII不可少

内存泄漏是指程序未能释放不再使用的内存,导致内存浪费的情况。

分类

  1. 堆内存泄漏(Heap leak):通过malloc/realloc/new分配的内存未通过free/delete释放
  2. 系统资源泄露(Resource Leak):未释放系统分配的资源如Bitmap、handle、SOCKET等
  3. 基类析构函数未定义为虚函数:当基类指针指向子类对象时,子类析构函数不会被调用

避免方法

  • 使用智能指针自动管理内存
  • 遵循RAII原则,将资源管理封装在类中
  • 基类析构函数应定义为虚函数
  • 使用内存泄漏检测工具如Valgrind、mtrace

四、new与malloc的区别

记忆口诀:new是运算符malloc是函数,类型安全异常处理各不同,内存分配与释放要匹配,构造析构调用记心中

特性 new malloc
类型 C++运算符 C语言库函数
构造函数 调用 不调用
返回类型 具体类型指针 void*(需类型转换)
失败处理 抛出std::bad_alloc异常 返回NULL
内存大小 编译器确定 需手动计算
重载 可重载 不可重载
数组分配 有专门的new[] 需手动计算大小

五、delete与free的区别

记忆口诀:delete调用析构函数,free简单释放内存,数组释放要匹配,类型安全很重要

特性 delete free
析构函数 调用 不调用
类型安全 类型感知 无类型概念
数组释放 支持delete[] 需手动处理
参数类型 具体类型指针 void*
重载 可重载 不可重载

六、野指针与悬空指针

记忆口诀:野指针未初始化,随机指向很危险,悬空指针曾有效,内存释放仍保留

野指针(Wild Pointer)

  • 定义:指向不可预测内存区域的指针
  • 原因:未初始化、越界访问、指针被非法修改
  • 特征:指针值是随机垃圾值,指向无效内存

悬空指针(Dangling Pointer)

  • 定义:指针原本指向有效内存,但该内存已被释放,指针仍保存原地址
  • 特征:指针值看似正常,但指向的内存已无效
  • 避免方法:释放内存后将指针置为nullptr

七、内存对齐

记忆口诀:内存对齐提效率,数据存放按边界,硬件要求是根本,访问速度大提升

内存对齐是指数据在内存中的存储起始地址是某个值(通常是其大小)的倍数。

原因

  1. CPU访问效率:大多数CPU要求数据对齐到特定边界,对齐数据可一次读取
  2. 缓存优化:对齐数据能提高缓存命中率
  3. 硬件限制:某些硬件架构要求特定类型数据必须对齐
  4. 原子操作支持:某些原子操作要求数据对齐

八、进程地址空间分布

记忆口诀:地址空间分七段,高到低来记清楚,命令行栈映射堆,BSS数据代码段

从高地址到低地址,进程地址空间分布为:

  1. 命令行参数和环境变量:程序启动时传入的参数和环境信息
  2. 栈区:存储局部变量、函数参数,从高地址向低地址增长
  3. 文件映射区:位于堆和栈之间
  4. 堆区:动态内存分配区域,从低地址向高地址增长
  5. BSS段:存储未初始化的全局变量和静态变量
  6. 数据段:存储已初始化的全局变量和静态变量
  7. 代码段:存储程序执行代码,只读

九、C与C++的内存分配方式

记忆口诀:内存分配有三种,静态存储栈和堆,静态编译时分配,栈上自动释放,堆区手动管理

  1. 静态存储区域分配

    • 编译时已分配好内存
    • 程序运行期间一直存在
    • 如全局变量、static变量
  2. 栈上分配

    • 函数执行时自动分配
    • 函数结束时自动释放
    • 效率高,但空间有限
    • 如局部变量
  3. 堆上分配(动态内存分配)

    • 程序运行时通过malloc/new申请
    • 需要手动通过free/delete释放
    • 灵活,但需注意内存管理

十、计算机中的乱序执行

记忆口诀:乱序执行提效率,单线程没问题,多线程需注意,内存模型来规范

乱序执行是指CPU或编译器为了提高性能,可能会改变指令执行的顺序。

一定会按正常顺序执行的情况

  1. 对同一块内存进行访问时
  2. 新定义的变量值依赖于之前定义的变量时

C++11中的六种内存模型

  1. memory_order_relaxed:最宽松的模型
  2. memory_order_consume:搭配release使用,保证相关变量的顺序
  3. memory_order_acquire:用于获取资源
  4. memory_order_release:用于释放资源,设置内存屏障
  5. memory_order_acq_rel:同时具有acquire和release语义
  6. memory_order_seq_cst:最严格的顺序一致性模型

十一、信号量

记忆口诀:信号量分两种,binary和counting,前者单线程,后者多线程控数量

  1. binary_semaphore(二元信号量)

    • 只有有信号和无信号两种状态
    • 一次只能被一个线程持有
    • 可作为事件通知机制
  2. counting_semaphore(计数信号量)

    • 可以指定同时访问的线程数量
    • 通过计数器控制并发访问

使用示例

1
2
3
4
5
6
7
8
9
10
11
// binary_semaphore示例
std::binary_semaphore sem(0); // 初始化为无信号状态
// 线程等待
sem.acquire(); // 阻塞直到收到信号
// 主线程发送信号
sem.release(); // 发送信号

// counting_semaphore示例
std::counting_semaphore<8> sem(0); // 最多8个线程同时访问
// 唤醒6个等待的线程
sem.release(6);

十二、future库

记忆口诀:future库任务链,promise承诺future取,async异步更方便,线程同步不用烦

future库用于处理异步任务和任务依赖关系,特别适用于任务链场景(任务A依赖任务B的返回值)。

主要组件

  1. promise:生产者用来设置值或异常
  2. future:消费者用来获取promise设置的值或异常
  3. async:异步执行函数,返回future对象

生产者-消费者示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 生产者-消费者模式
std::promise<int> prom; // 生产者的承诺
std::future<int> fut = prom.get_future(); // 消费者的future

// 生产者设置值
prom.set_value(42);

// 消费者获取值
int value = fut.get(); // 阻塞直到获取到值

// async异步执行
std::future<int> result = std::async(std::launch::async, []() {
return 42;
});
int value = result.get(); // 获取异步执行结果

十三、常用字符串操作函数

记忆口诀:字符串函数要掌握,复制连接比较长度,实现细节要注意,安全高效是关键

  1. strcpy():字符串复制

    1
    2
    3
    4
    5
    6
    char* strcpy(char *dst, const char *src) {
    assert(dst != NULL && src != NULL);
    char *ret = dst;
    while ((*dst++ = *src++) != '\0');
    return ret;
    }
  2. strlen():计算字符串长度

    1
    2
    3
    4
    5
    6
    size_t strlen(const char *str) {
    assert(str != NULL);
    size_t len = 0;
    while (*str++ != '\0') len++;
    return len;
    }
  3. strcat():字符串连接

    1
    2
    3
    4
    5
    6
    7
    char* strcat(char *dest, const char *src) {
    assert(dest != NULL && src != NULL);
    char *ret = dest;
    while (*dest != '\0') dest++;
    while ((*dest++ = *src++) != '\0');
    return ret;
    }
  4. strcmp():字符串比较

    1
    2
    3
    4
    5
    6
    7
    8
    int strcmp(const char *str1, const char *str2) {
    assert(str1 != NULL && str2 != NULL);
    while (*str1 && *str2 && (*str1 == *str2)) {
    str1++;
    str2++;
    }
    return *str1 - *str2;
    }

十四、内存拷贝函数实现(考虑重叠)

记忆口诀:内存拷贝要小心,重叠情况需处理,低地址开始正常,高地址开始防覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *my_memcpy(char *dst, const char* src, int cnt) {
assert(dst != NULL && src != NULL);
char *ret = dst;

// 处理内存重叠情况
if (dst >= src && dst <= src + cnt - 1) {
// 从高地址开始复制
dst = dst + cnt - 1;
src = src + cnt - 1;
while (cnt--) {
*dst-- = *src--;
}
} else {
// 正常情况,从低地址开始复制
while (cnt--) {
*dst++ = *src++;
}
}
return ret;
}

十五、String类的实现

记忆口诀:String类四函数,构造析构拷贝赋值,深拷贝是关键,自赋值要检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class String {
public:
String(const char *str = NULL); // 构造函数
String(const String &other); // 拷贝构造函数
~String(void); // 析构函数
String &operator=(const String &other); // 赋值运算符
private:
char *m_data; // 数据成员
};

// 构造函数实现
String::String(const char *str) {
if (str == NULL) {
m_data = new char[1];
*m_data = '\0';
} else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}

// 析构函数实现
String::~String(void) {
delete[] m_data;
}

// 拷贝构造函数实现
String::String(const String &other) {
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}

// 赋值运算符实现
String &String::operator=(const String &other) {
if (this == &other) { // 检查自赋值
return *this;
}
delete[] m_data; // 释放原有资源
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
return *this; // 返回本对象的引用
}