C++ 内存管理
一、智能指针
记忆口诀:智能指针三兄弟,unique独占share共享,weak观测防循环,RAII思想记心上
智能指针是一种自动管理动态内存的工具类,用于防止内存泄漏。C++提供了三种常用的智能指针:
unique_ptr(独占智能指针):
- 独占对象所有权,同一时间只能有一个指针指向一个对象
- 禁止拷贝构造和拷贝赋值,支持移动语义
- 适合独占资源的场景
shared_ptr(共享智能指针):
- 共享对象所有权,允许多个指针指向同一个对象
- 使用引用计数,当引用计数为0时释放资源
- 可以通过
use_count()
查看引用计数
weak_ptr(弱引用指针):
- 不拥有资源,不增加引用计数
- 用于解决shared_ptr的循环引用问题
- 需要通过
lock()
方法提升为shared_ptr才能访问资源
RAII机制(资源获取即初始化):
- 当创建智能指针对象时,它立即接管资源
- 当智能指针生命周期结束时,自动调用析构函数释放资源
- 无需手动调用delete,有效防止内存泄漏
代码示例
1 | // unique_ptr示例 |
二、C++内存分区
记忆口诀:内存分区五大块,栈堆全局常量代码,各司其职不混乱,生命周期要明白
C++程序运行时,内存被分为五个不同的区域:
栈区(Stack):
- 存储函数的局部变量、函数参数和函数调用信息
- 自动分配和释放,速度快
- 生命周期与函数执行期相同
堆区(Heap):
- 存储动态分配的内存
- 需要手动分配(new/malloc)和释放(delete/free)
- 生命周期由程序员控制
全局/静态区(Global/Static):
- 存储全局变量和静态变量
- 程序启动时分配,结束时释放
- 生命周期贯穿整个程序运行期间
常量区(Const):
- 也称为只读区
- 存储常量数据,如字符串常量
- 不可修改
代码区(Code):
- 存储程序的可执行代码
- 只读
三、内存泄漏
记忆口诀:内存泄漏三类型,堆内存系统资源虚析构,防止泄漏有妙招,智能指针RAII不可少
内存泄漏是指程序未能释放不再使用的内存,导致内存浪费的情况。
分类:
- 堆内存泄漏(Heap leak):通过malloc/realloc/new分配的内存未通过free/delete释放
- 系统资源泄露(Resource Leak):未释放系统分配的资源如Bitmap、handle、SOCKET等
- 基类析构函数未定义为虚函数:当基类指针指向子类对象时,子类析构函数不会被调用
避免方法:
- 使用智能指针自动管理内存
- 遵循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
七、内存对齐
记忆口诀:内存对齐提效率,数据存放按边界,硬件要求是根本,访问速度大提升
内存对齐是指数据在内存中的存储起始地址是某个值(通常是其大小)的倍数。
原因:
- CPU访问效率:大多数CPU要求数据对齐到特定边界,对齐数据可一次读取
- 缓存优化:对齐数据能提高缓存命中率
- 硬件限制:某些硬件架构要求特定类型数据必须对齐
- 原子操作支持:某些原子操作要求数据对齐
八、进程地址空间分布
记忆口诀:地址空间分七段,高到低来记清楚,命令行栈映射堆,BSS数据代码段
从高地址到低地址,进程地址空间分布为:
- 命令行参数和环境变量:程序启动时传入的参数和环境信息
- 栈区:存储局部变量、函数参数,从高地址向低地址增长
- 文件映射区:位于堆和栈之间
- 堆区:动态内存分配区域,从低地址向高地址增长
- BSS段:存储未初始化的全局变量和静态变量
- 数据段:存储已初始化的全局变量和静态变量
- 代码段:存储程序执行代码,只读
九、C与C++的内存分配方式
记忆口诀:内存分配有三种,静态存储栈和堆,静态编译时分配,栈上自动释放,堆区手动管理
静态存储区域分配:
- 编译时已分配好内存
- 程序运行期间一直存在
- 如全局变量、static变量
栈上分配:
- 函数执行时自动分配
- 函数结束时自动释放
- 效率高,但空间有限
- 如局部变量
堆上分配(动态内存分配):
- 程序运行时通过malloc/new申请
- 需要手动通过free/delete释放
- 灵活,但需注意内存管理
十、计算机中的乱序执行
记忆口诀:乱序执行提效率,单线程没问题,多线程需注意,内存模型来规范
乱序执行是指CPU或编译器为了提高性能,可能会改变指令执行的顺序。
一定会按正常顺序执行的情况:
- 对同一块内存进行访问时
- 新定义的变量值依赖于之前定义的变量时
C++11中的六种内存模型:
memory_order_relaxed
:最宽松的模型memory_order_consume
:搭配release使用,保证相关变量的顺序memory_order_acquire
:用于获取资源memory_order_release
:用于释放资源,设置内存屏障memory_order_acq_rel
:同时具有acquire和release语义memory_order_seq_cst
:最严格的顺序一致性模型
十一、信号量
记忆口诀:信号量分两种,binary和counting,前者单线程,后者多线程控数量
binary_semaphore(二元信号量):
- 只有有信号和无信号两种状态
- 一次只能被一个线程持有
- 可作为事件通知机制
counting_semaphore(计数信号量):
- 可以指定同时访问的线程数量
- 通过计数器控制并发访问
使用示例
1 | // binary_semaphore示例 |
十二、future库
记忆口诀:future库任务链,promise承诺future取,async异步更方便,线程同步不用烦
future库用于处理异步任务和任务依赖关系,特别适用于任务链场景(任务A依赖任务B的返回值)。
主要组件:
- promise:生产者用来设置值或异常
- future:消费者用来获取promise设置的值或异常
- async:异步执行函数,返回future对象
生产者-消费者示例
1 | // 生产者-消费者模式 |
十三、常用字符串操作函数
记忆口诀:字符串函数要掌握,复制连接比较长度,实现细节要注意,安全高效是关键
strcpy():字符串复制
1
2
3
4
5
6char* strcpy(char *dst, const char *src) {
assert(dst != NULL && src != NULL);
char *ret = dst;
while ((*dst++ = *src++) != '\0');
return ret;
}strlen():计算字符串长度
1
2
3
4
5
6size_t strlen(const char *str) {
assert(str != NULL);
size_t len = 0;
while (*str++ != '\0') len++;
return len;
}strcat():字符串连接
1
2
3
4
5
6
7char* strcat(char *dest, const char *src) {
assert(dest != NULL && src != NULL);
char *ret = dest;
while (*dest != '\0') dest++;
while ((*dest++ = *src++) != '\0');
return ret;
}strcmp():字符串比较
1
2
3
4
5
6
7
8int strcmp(const char *str1, const char *str2) {
assert(str1 != NULL && str2 != NULL);
while (*str1 && *str2 && (*str1 == *str2)) {
str1++;
str2++;
}
return *str1 - *str2;
}
十四、内存拷贝函数实现(考虑重叠)
记忆口诀:内存拷贝要小心,重叠情况需处理,低地址开始正常,高地址开始防覆盖
1 | char *my_memcpy(char *dst, const char* src, int cnt) { |
十五、String类的实现
记忆口诀:String类四函数,构造析构拷贝赋值,深拷贝是关键,自赋值要检查
1 | class String { |
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.