9 内存分区模型
作用:C++ 程序在执行时,将内存大方向划分为 4 个区域,分别是:代码区、全局区、栈区、堆区。==不同区域存放的数据,赋予不同的生命周期==,给我们带来更大的灵活编程空间。
- 代码区:存放函数体的二进制代码,由操作系统进行管理
- 全局区:存放全局变量、静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
提示:内存分区模型是 C++ 核心编程的地基,后面学习的引用、类和对象等内容,都建立在对内存四区的理解之上,请务必把这一章学扎实。
9.1 程序运行流程
作用:弄清楚程序从源代码到运行,各阶段分别使用了哪些内存区域,是学习内存分区模型的第一步。一个 C++ 程序从编写到运行,大致经过下面的流程:
- 编写源代码,保存为
.cpp源文件; - 编译器将源代码翻译成机器指令,链接器把目标文件和用到的库链接成可执行程序;
- 运行可执行程序前:操作系统把程序加载进内存,划分出代码区和全局区;
- 运行可执行程序后:程序开始执行,又划分出栈区和堆区。
作用:四个区域各司其职,存放的数据和由谁管理都不相同:
- 代码区:存放 CPU 执行的机器指令,由操作系统进行管理;
- 全局区:存放全局变量、静态变量、常量,数据在程序结束后由操作系统释放;
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等;
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
示例:下面的程序把函数、全局变量、局部变量和 new 出来的数据所在的地址全部打印出来,先对四个区域在内存中的位置有个直观印象:
1 |
|
注意:不同编译器、不同系统下,地址的具体数值和显示格式都不一样,重点观察==哪些地址落在相近的区间==,而不是记住具体数字。
9.2 内存四区
作用:把数据放进合适的区域,本质上是在为数据选择合适的生命周期。下面逐个介绍四个区域里存放什么数据、由谁管理。
9.2.1 代码区
作用:存放 CPU 执行的机器指令。我们写下的每一个函数体,经过编译后都变成一条条二进制指令,存放在代码区。代码区有两个特点:
- 共享的:对于频繁被执行的程序,只需要在内存中有一份代码即可,比如同时打开同一个程序的两个窗口,运行的数据有两份,但代码在内存中只有一份;
- 只读的:把代码区设置为只读,是为了防止程序意外地修改了它的指令。
示例:函数名就是函数的入口地址,把函数的地址打印出来,就能观察到代码区在内存中的位置:
1 |
|
注意:代码区由操作系统管理,程序员不需要、也不能对代码区进行读写操作。
9.2.2 全局区
作用:存放全局变量、静态变量和常量,全局区的数据在程序结束后由操作系统释放。全局区中常见的成员有:
- 全局变量:定义在函数外部的变量;
- 静态变量:用
static关键字修饰的变量; - 字符串常量:用双引号括起来的一串字符,例如
"hello world"; - const 修饰的全局变量。
示例:把局部变量、全局变量、静态变量、常量的地址都打印出来,验证它们到底存放在哪个区域:
1 |
|
注意:观察运行结果:局部变量 a、b 和局部常量 c_l_a 的地址与其他地址相差很远,说明它们不在全局区;而==全局变量、静态变量、字符串常量、const 修饰的全局变量==的地址都集中在相近的区间,说明它们都存放在全局区。
总结:全局区存放全局变量、静态变量和常量(字符串常量、const 修饰的全局变量);const 修饰的局部变量不在全局区,仍然存放在栈区。
9.2.3 栈区
作用:存放函数的参数值(形参)和局部变量等,由编译器自动分配和释放,==函数执行完后,栈区的数据自动释放==。栈区还有两个特点:
- 栈区的开辟和释放由编译器自动完成,不需要程序员操心;
- 栈区空间有限(通常只有几 MB),不适合存放特别大的数据。
注意:不要返回局部变量的地址!栈区的数据由编译器自动释放,函数执行完后,局部变量所占的内存就被回收了,再拿着返回的地址去访问,得到的就是垃圾值。
示例:下面这段代码是一个反面教材,专门演示返回局部变量地址会发生什么:
1 |
|
注意:第一次能打印出 10,是编译器”仁慈”地保留了一次数据,纯属碰运气;第二次访问打印的就是乱码,因为 a 的内存已经被释放。这种写法是错误的,不要在程序中返回局部变量的地址。
9.2.4 堆区
作用:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。C++ 中主要利用 new 关键字在堆区开辟内存。
示例:利用 new 把数据开辟到堆区,函数结束后,堆区的数据依然存在:
1 |
|
注意:对比上一小节的反面教材:同样是”在函数里创建数据、到函数外使用”,存放在栈区的数据函数一结束就消失了,而利用 new 存放到==堆区==的数据,只要程序员不释放,就一直存在。
总结:栈区和堆区的核心区别是==栈区由编译器自动管理,堆区由程序员手动管理==。堆区的内存用完要记得释放,否则会造成内存泄漏。
9.3 new 操作符
作用:在堆区开辟内存。C++ 中利用 new 在堆区开辟数据,利用 delete 释放堆区数据。
语法:
- 开辟单个变量:
new 数据类型(初始值);,new 返回的是==该数据类型的指针==; - 释放单个变量:
delete 指针;; - 开辟数组:
new 数据类型[元素个数];; - 释放数组:
delete[] 指针;,释放数组时必须加上方括号。
示例:分别演示利用 new 开辟单个变量和数组,再用 delete 释放:
1 |
|
注意:new 和 delete 必须成对出现:
new int(10)开辟的单个变量用delete p;释放,new int[10]开辟的数组用delete[] arr;释放;形式用错了,程序可能出现不可预期的错误。另外,内存被 delete 释放后就不要再访问了,像示例中那样把不再使用的指针置为nullptr,可以有效防止野指针。
总结:堆区内存的使用分三步:new 开辟 → 使用 → delete 释放。new 的返回值是该数据类型的指针;释放单个变量用 delete,释放数组时 delete 后面要加方括号。