内存管理是c++变成中很重要的一环,这是编写高效可靠c++的必要条件,这也是c++与其他高级语言最大的区别,其他的语言都提供了高级的gc机制,而c++没有这就难住了很大一部分人了,对于高手来说这是高效程序的关键,对于菜鸟这就是噩梦啊,经常出现的野指针,忘记delete的内存严重影响了运行与开发效率。
内存分区
先让我们了解一些基本知识,比如内存分区,使用new会占用那里的内存,创建普通的变量会使用哪里的,理解内存分区是避免内存错误的基础。下面我们分析几种典型的内存问题及其解决方案,下面是c++的四个内存分区:
- 栈:存储局部变量、函数参数、函数返回地址等。这一部分是我们创建局部变量使用的内存,c++会自动管理这个区域,创建变量时自动创建内存,变量的生命周期解释就会自动释放内存
- 堆:这一部分就是通过new分配的内存,需要用户手动管理内存的生成会释放,如果我们没有释放分配的内存,程序结束运行后操作系统会收回内存
- 全局/静态存储区:存储全局变量、静态变量(包括类的静态成员变量)。
- 常量区:存储字符串常量和编译期确定的
const
常量,常量区的内存为只读,修改字符串常量或编译期const
变量会导致未定义行为(如程序崩溃)。
通过下面的代码,我们再对四个分区加深印象
int global_var = 1; // 全局变量 全局/静态存储区
int main(){
int a = 10; //局部变量 栈内存
int *p = new int(20); //这里占用了堆栈两个内存,p这个指针是栈,而p指向的内存是堆
const int b = 30; //常量 常量区
const char *str = "hello world"; //字符串常量 常量区
static int s_var = 15; // 静态局部变量 全局/静态存储区
delete p; //释放堆内存
return 0;
}
栈与堆是我们最常用的分区,我们再来详细的说明一下他们,栈是 LIFO(后进先出)的线性数据结构,在cpu寄存器中会有一个栈指针指向当前栈顶,这确保了栈极高的内存分配速度与非常快的访问速度,因为栈这种数据结构是先进后出的,保证不对出现之间一部分为空的情况,所有这避免了内存碎片的出现。
不过栈本身的空间是很小,Linux 默认线程栈大小通常为 8MB,Windows 为 1MB。,不过可以通过编译器的命令来修改,这就导致了我们需要特别注意,递归深度过大或局部变量占用过多栈空间(如大数组)导致的崩溃,这也叫做栈溢出。
堆是程序运行时管理的动态内存区域,通过操作系统的虚拟内存机制分配连续的虚拟地址空间(物理内存可能分散),可以理解为一个连续且超级大的数组,堆的虚拟地址连续,但物理内存可能分散(由 MMU 通过页表映射),程序仅操作虚拟地址,无需关心物理内存分布。
当我们想要分配内存时,操作系统会找到一块合适的内存区域进行分配,堆内存分配需要满足两个条件:内存区域必须是连续的,且大小必须大于等于请求的大小,由于每次分配内存都需要找到合适的区域,这大大降低了内存分配的速度,并且堆区的内存分配与释放是不规律的,这就导致了会出现内存碎片,影响内存的使用率,当堆空间不足时,可以通过系统调用(如 Linux 的 sbrk
或 Windows 的 VirtualAlloc
)向操作系统申请更多内存。
常见的内存问题与解决方案
内存泄漏
分配内存后未释放,导致程序持续占用内存,最终可能耗尽系统资源。
void func(){
int *p = new int(20);
}
int main(){
for (int i = 0; i < 10; i++) func();
return 0;
}
上面的代码,每次调用func函数会创建一个堆内存,这里没有进行delete,并且函数介绍后管理内存的指针被释放了,这就导致了分配的内存处于一种无管理的状态,我们无法使用它也无法释放,并且他会一种占用内存,严重影响了程序的效率与稳定性
我们可以使用智能指针来管理,它可以自动的获得资源并释放资源
//这样修改后,函数退出会自动释放int的内存
void func(){
auto p = std::make_shared<int>(10);
}
我们在使用new分配内存后,一定要仔细检查是否释放了内存
野指针
野指针不是nullptr指针,而是指向“垃圾”内存的指针。一般不会错用nullptr指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的成因主要有三种:
- 使用了没有初始化并且没有置为nullptr的指针,当创建一个没有初始化的指针时,并不会自动为nullptr
int main(){
int *a;
if (a) std::cout << "a is not null" << std::endl;
else std::cout << "a is null" << std::endl;
return 0;
}
# 输出
a is not null
- 指针配释放后没有被置为nullptr,被误以为是合法指针,当我们释放内存后并不会自动置为nullptr
int main(){
Abstract1* a = new Abstract1();
delete a;
if (a) std::cout << "a is not null" << std::endl;
else std::cout << "a is null" << std::endl;
std::cout << a->myName() << std::endl;
return 0;
}
- 指针的操作超越了变量的生命周期
int main(){
Abstract1* a;
{
Abstract1 A;
a = &A;
}
std::cout << a->myName() << std::endl;
return 0;
}
上面三种最好的预防办法,就是遵循RAII资源获取即初始化,使用智能指针通过析构函数自动释放,并且多加注意
越界访问
访问数组或内存块外的地址,导致数据损坏或崩溃。
int* arr = new int[3];
arr[3] = 10; // 越界写入(下标应为 0~2)
我们可以使用std::vector
或 std::array
替换普通的数组,也可以手动检查边界
通过上面的几点,我强烈希望大家使用智能指针,它真的可以解决很多不必要的问题
关于RAII思想可以看下这个RAII思想