内存管理机制
在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等;
- 数据区包括:堆,栈,全局/静态存储区。
- 全局/静态存储区包括:常量区,全局区、静态区。
- 常量区包括:字符串常量区、常变量区。
- 代码区:存放程序编译后的二进制代码,不可寻址区。
可以说C/C++内存分区其实只有两个,即代码区和数据区。
1. 内存分区
1.1 运行之前
预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
gcc –E index.c –o index.i
编译:检查语法,将预处理后文件编译生成汇编文件
gcc –S index.i –o index.s
汇编:将汇编文件生成目标文件(二进制文件)
gcc –c index.s –o index.o .obj
链接:将目标文件链接为可执行程序
gcc index.o –o index
通过size命令可以查看一个可执行二进制文件基本情况:
程序没有加载到内存前,程序内部已经分好: 代码区(text)、数据区(data)和未初始化数据区(bss).程序源代码被编译之后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据域段和.bss段属于程序数据。
- 代码区
存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
- 全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
- 未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
那为什么把程序的指令和程序数据分开呢?
- 程序被加载到内存后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改;
- 当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer 7.0运行起来之后, 它需要占用112844KB的内存,它的私有部分数据有大概15944KB,也就是说有96900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。
1.2 运行之后
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。运行可执行程序,操作系统把物理硬盘程序加载到内存,除了代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
- 代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
- 未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
- 全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
- 栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为从申请到释放该段栈空间。
- 堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
局部变量(也可使用auto显式定义) | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数(只能用来修饰局部变量 register int x = 10;) | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
2. 分区模型
2.1 栈区
由系统进行内存的管理。主要存放函数的参数以及局部变量。
- 每个线程都有自己专属的栈;
- 栈的最大尺寸固定,超出则引起栈溢出;
- 变量离开作用域后栈上的内存会自动释放。
int jia(int a, int b) {
return a + b;
}
int a = 1;
int b = 1;
printf("%d", jia(a, b));
2.2 堆区
由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
C语言的标准内存分配函数:malloc,calloc,realloc,free等。
- 使用方式
- malloc调用形式为(类型 ) malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。
- calloc调用形式为(类型 ) calloc(n,size):在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。
- realloc调用形式为(类型) realloc(*ptr,size):将ptr内存大小增大到size。
- free的调用形式为(类型) free(void *ptr):释放ptr所指向的一块内存空间。
- 共同点
- 都为了分配存储空间,
- 它们返回的是
void *
类型,也就是说如果我们要为int或者其他类型的数据分配空间必须显式强制转换;
- 不同点
- malloc传入1个形参,因此如果是数组,必须由我们计算需要的字节总数作为形参传递,malloc只分配空间不初始化。
- calloc 2个形参 ,因此如果是数组,需要传递个数和数据类型,而calloc则进行了初始化,calloc分配的空间全部初始化为0,这样就避免了可能的一些数据错误。
#include <stdlib.h>
#include<stdio.h>
#include <string.h>
void print_array(char *p, char n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
}
int main(int argc, char* argv[])
{
char *p = (char *)malloc(1024 * 1024 * 1024); // 在堆中申请了内存
memset(p, 'a', sizeof(int)* 10); // 初始化内存
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 65;
}
print_array(p, 10);
free(p); // 释放申请的堆内存
getchar();
}
2.3 全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。
- 这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。
- 全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。
- 字符串常量存储在全局/静态存储区的常量区。
例1:
int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL
//那么全局static int 和 全局int变量有什么区别?
void test(){
static int v4 = 20; //全局/静态区
}
例2:
char* func(){
static char arr[] = "hello world!"; //在静态区存储 可读可写
arr[2] = 'c';
char* p = "hello world!"; //全局/静态区-字符串常量区
//p[2] = 'c'; //只读,不可修改
printf("%d\n",arr);
printf("%d\n",p);
printf("%s\n", arr);
return arr;
}
void test(){
char* p = func();
printf("%s\n",p);
}
全局static int 和 全局int变量有什么区别?
它们之间的主要区别在于作用域:
int v1 = 10;
(普通全局变量):普通全局变量的作用域是整个程序文件,即在声明该变量的文件内和其他文件(通过extern
声明)中都可以访问它。换句话说,其他文件可以通过extern
关键字引用并访问该全局变量。static int v3 = 20;
(静态全局变量):静态全局变量的作用域仅限于声明它的文件。即使其他文件使用extern
声明该变量名,它也无法访问v3
。静态全局变量只在本文件内部可见,无法被其他文件引用。
file1.c
#include <stdio.h>
int v1 = 10; // 普通全局变量
static int v3 = 20; // 静态全局变量
void print_v1_v3() {
printf("v1 = %d\n", v1);
printf("v3 = %d\n", v3);
}
file2.c
#include <stdio.h>
extern int v1; // 声明 file1.c 中的普通全局变量 v1
// extern int v3; // 错误!无法访问 file1.c 中的静态全局变量 v3
int main() {
printf("v1 = %d\n", v1);
// printf("v3 = %d\n", v3); // 错误!无法访问 file1.c 中的静态全局变量 v3
return 0;
}
总结
- 普通全局变量(
int v1
):可以被整个程序访问,包括其他文件。 - 静态全局变量(
static int v3
):仅限于当前文件,其他文件无法访问,即使使用extern
关键字。
2.4. 程序分析
- main函数和UpdateCounter为代码的一部分,故存放在代码区;
- 数组a默认为全局变量,故存放在静态区;
- main函数中的
char *b = NULL
定义了自动变量b(variable),故其存放在栈区,接着malloc向堆申请了部分内存空间,故这段空间在堆区;
需要注意以下几点:
- 栈是从高地址向低地址方向增长;高地址在栈顶,低地址在栈口,先入栈的变量地址大于后入栈的;堆相反:
- 在C语言中,函数参数的入栈顺序是从右到左,因此UpdateCounter函数的3个参数入栈顺序是a1、c、b;
2.5 内存管理的目的
学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:
- 如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
- 如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
- 如果需要动态创建数组,则用堆;
1
1
1
1
1
1
1
1
1
1