内存管理
1 、 C 程序结构
C 程序在没有调入内存之前(也就是在存储时),分为代码区( text )、数据区( data )和未初始化数据区( bss ) 3 个部分。
¨ 代码区 存放 CPU 执行的机器指令,即 函数体的二进制代码 。由于对于频繁被执行的程序,只需要在内存中有一份代码即可,所以代码区是可共享的 (可以被别的程序调用)。为了防止程序意外地修改代码区的机器指令,通常代码区是只读的 。
¨ 全局初始化数据区和静态数据区 包含已经被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
¨ 未初始化数据区 存储的是全局未初始化变量。
以上谈的是存储时 C 语言的程序结构,下面看看运行时 C 语言的程序结构。
¨ 代码区 该区的机器指令根据程序设计流程依次执行。代码区的指令包括操作码和要操作的对象(或对象地址引用,即寄存器间接寻址等)。如果操作对象是立即数,则将直接包含在代码中;如果操作对象是局部数据,则将在栈区分配空间,然后引用该数据地址;如果操作对象存在于 BSS 区和数据区,则在代码区中也将引用该数据地址。
¨ 全局初始化数据区和静态数据区 只做一次初始化
¨ 未初始化数据区 在运行时改变其值
¨ 栈区 由编译器自动分配释放,存放函数的参数值、局部变量的值等等, 其操作方式类似于数据结构中的栈。 这里简要谈一下 C 程序函数调用过程中栈框架的建立过程:
1) 第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址(目的是为了恢复现场 )
2) 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的
3) 然后是函数中的局部变量(注意静态变量是不入栈的)
4) 当本次函数调用结束后,局部变量先出栈,然后是参数 ,最后栈顶 指针 指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
¨ 堆区 用于动态分配内存 ,位于未初始化数据区和栈区之间, 一般由程序员分配和释放,若程序员不释放,程序结束时可能由 OS 回收。
问题:为什么要专门开辟代码区
一个进程在运行的过程中,代码是根据流程一次执行的,只需要执行一次 (当然跳转和递归也可使代码执行多次),然而程序可能会对数据进行多次访问(但没有必要为了对数据进行多次访问而多次访问代码 ),此时我们就有必要把代码区和数据区区分开来管理。
一例程序
//main.cpp
int a = 0; //a 在全局初始化数据区
char *p1; //p1 在全局未初始化数据区
int main(int argc, char *argv[ ])
{
int b; // b 为局部变量,所以存储于栈区
char s[] = "abc"; // s 为数组局部变量,存在于栈区
char *p2; // p2 为局部变量,所以存储于栈区
char *p3 = "123456"; // 123456/0 在常量区(已初始化数据区) , p3 在栈上。
static int c =0 ; // c 存储于静态数据区(静态数据区和全局初始化区同在一个区域)
p1 = (char *)malloc(10); // 系统动态分配得来的 10 和 20 字节的区域就在堆区
p2 = (char *)malloc(20);
free(p1);
free(p2);
return 0;
}
2 、内存的分配(内存的申请)
1) 申请方式
¨ Stack (静态分配):静态对象是有名字的变量,可以直接对其进行操作。由系统自动分配内存。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。
¨ Heap (动态分配):动态对象是没有名字的变量,需要通过指针间接地对它进行操作。需要程序员自己申请内存,并指明大小。
在 C 中 malloc 函数 ,如 p1 = (char *)malloc(10);
在 C++ 中用 new 运算符,如 p2 = new char[20]; //(char *)malloc(20);
分配堆空间之后, p1 、 p2 得到所分配堆空间首地址,将会指向堆空间。
2) 申请后系统的响应
¨ 栈:只要栈的剩余空间大于所申请空间, 系统将为程序提供内存,否则将报异常提示栈溢出
¨ 堆:首先 应该知道操作系统有一个记录空闲内存地址的链表 ,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点, 然后将该结点从空闲结点链表中删除, 并将该结点的空间分配给程序。
其次 ,大多数系统,会在这块内存空间中的首地址处记录本次分配的大小 ,这样,代码中的 delete 语句才能正确的释放本内存空间。
此外 ,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中 。
3 ) 申请大小的限制
栈:在 Windows 下,栈是高地址向低地址扩展的数据结构 , 是一块连续 的内存的区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的 。
在 WINDOWS 下,栈的大小是 2M (也有的说是 1M ,总之它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow 。因此,能从栈获得的空间较小。
堆:堆是低地址向高地址扩展的数据结构 ,是不连续的内存区域 。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。
堆的大小受限于计算机系统中有效的虚拟内存 ,由此可见,堆获得的空间很灵活。
但由程序员操作的过程中容易发生内存泄露,也极易产生内存空间的不连续,即内存碎片(频繁使用 malloc 和 free ( new 和 delete )的结果)。
4) 申请效率的比较
¨ 栈由系统自动分配,速度较快 ,但程序员是无法控制的。
¨ 堆是由 malloc 或者 new 分配的内存,一般速度比较慢 ,而且容易产生内存碎片 , 不过用起来最方便 .
5 ) 分配效率的比较
栈是机器提供的数据结构,机器会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。
堆则是 C 函数库提供的,它的机制很复杂。 首先 应该知道操作系统有一个记录空闲内存地址的链表 ,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点, 然后将该结点从空闲结点链表中删除, 并将该结点的空间分配给程序。
此外 ,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
可见,堆得存取效率和栈比较起来要低得多。
3 、下面再给出一个数据存储区域的实例
#include <stdio.h>
#include <malloc.h>
#include <unistd.h>
#include <alloca.h>
extern void afunc(void); // 声明 afunc() 函数
extern etext, edata, end; // 声明三个外部变量
int bss_var; // 未初始化全局数据存储在 BSS 区
int data_var = 42; // 初始化全局数据存储在数据区
// 定义了一个宏,用于打印地址
#define SHW_ADR(ID, I) printf("the %8s/t is at adr:%8x/n", ID, &I);
int main(int argc, char *argv[])
{
char *p, *b, *nb;
printf("Adr etext:%8x/t Adr edata%8x/t Adr end %8x/t/n", &etext, &edata,&end);
printf("/ntext Location:/n");
SHW_ADR("main", main); // 查看代码段 main 函数位置
SHW_ADR("afunc", afunc); // 查看代码段 afunc 函数位置
printf("/NBSS Location:/n");
SHW_ADR("bss_var", bss_var); // 查看 BSS 段变量位置
printf("/NDATA Location:/n");
SHW_ADR("data_var", data_var); // 查看数据段变量位置
printf("/nSTACK Location:/n");
afunc();
p = (char *)alloca(32); // 从栈中分配空间
if(p != NULL){
SHW_ADR("start", p); // 打印栈空间的起始位置
SHW_ADR("end", p+31); // 打印栈空间的结束位置
}
b = (char *)malloc(32*sizeof(char)); // 从堆中分配空间
nb = (char *)malloc(16*sizeof(char)); // 从堆中分配空间
printf("/NHEAP Location:/n");
printf("the heap start: %p/n", b); // 打印堆起始位置
printf("the heap end: %p/n", (nb+16*sizeof(char))); // 打印堆结束位置
printf("/nb and nb in Stack/n");
SHW_ADR("b", b); // 打印栈中字符指针变量 b 的存储位置
SHW_ADR("nb", nb); // 打印栈中字符指针变量 nb 的存储位置
free(b); // 释放申请的堆空间
free(nb); // 释放申请的堆空间
return 0;
}
void afunc(void)
{
static int long level = 0; // 静态数据存储在数据段中
int stack_var; // 局部变量,存储在栈区
if(++level == 5 ){
return;
}
printf("stack_var is at:%p/n", &stack_var); // 打印局部变量的地址
afunc(); // 递归执行 afunc 函数
}
4 、介绍几个内存管理函数
1) Malloc/free 函数
原型: extern void *malloc(size_t num_bytes);
头文件: include <stdlib.h>
功能:分配长度为 num_bytes 字节的内存块
返回值:如果分配成功则返回 指向被分配内存首地址的指针 ,否则返回空指针 NULL 。
说明:
¨ 该函数返回为 void 型指针,因此必要时要进行类型转换。
¨ 当内存不再使用时,应使用 free() 函数将内存块释放。
问题:为什么不再使用时,要用 free() 释放掉所申请的内存?
¨ 由于内存区域总是有限的,不能无限制地分配下去。
¨ 程序应该尽可能地去节省资源,当申请的堆空间不再使用时,应该释放掉,交由其它进程来使用。
注意:不能用 free() 来释放非 malloc(), calloc(), realloc() 函数所创建的堆空间 , 否则会发生错误。
2 ) new/delete (在 C++ 中)
使用 new/delete 运算符实现内存管理比 malloc/free 函数更有优越性。它们的定义如下:
Static void* operator new(size_t sz);
Static void* operator delete(void* p);
先看一段 C++ 代码:
void test(void)
{
// 申请一个 sizeof ( obj )大小的一块动态内存,并把头指针赋值给 obj 类型的指针变量 a
obj *a = new obj;
delete a; // 清除并且释放所申请的内存
}
下面通过一段代码具体介绍一下 new/delete 的用法:
Class A
{
Public:
A() { count << “A is here!” << endl; } // 构造函数
~A() { count << “A is here!” << endl; } // 析构函数
Private:
Int I;
};
A* pA = new A; // 调用 new 运算符申请空间
delete pA; // 删除 pA
其中,语句 new A 完成了一下两个功能:
A. 调用 new 运算符,在堆上分配一个 sizeof(A) 大小的内存空间。
B. 调用构造函数 A() ,在所分配的内存空间上初始化对象。
语句 delete pA 完成的是相反的两件事:
A. 调用析构函数 ~A() ,销毁对象。
B. 调用运算符 delete ,释放内存。
注意:
¨ 使用 new 比使用 malloc() 有以下优点
A. New 自动计算要分配给对象的内存空间大小,不使用 sizeof 运算符,这样一来简单,而来可以避免错误。
B. 自动地返回正确的指针类型,不用进行强制类型转换。
C. 用构造函数给分配的对象进行初始化。
¨ 使用 malloc 函数和 new 分配内存的时候,本身并没有对这块内存空间做清零等任何工作。因此,申请内存空间后,其返回的新分配的空间是没有用零填充的,程序员须使用 memset() 函数来初始化内存。
3) realloc 函数(更改已经配置的内存空间)
头文件: include <stdlib.h>
函数定义:
void *realloc(void *ptr, size_t size)
参数 ptr 为先前由 malloc 、 calloc 和 realloc 所返回的内存指针 ,而参数 size 为新配置的内存大小。
realloc 函数用来从堆上分配内存,当需要扩大一块内存空间时, realloc() 试图直接从堆上当前内存段后面的字节中 获得更多的内存空间:
¨ 如果能够分配成功,则返回指向这块新内存空间的首地址,而将原来的指针( realloc 函数的参数指针)指向的空间释放掉;
¨ 如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉;
¨ 如果内存不足,重新申请空间失败,则返回 NULL ,此时原来的指针( realloc 函数的参数指针)仍有效。
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[], char* envp[]) // 主函数
{
int input;
int n;
int *numbers1;
int *numbers2;
numbers1 = NULL;
if((numbers2 = (int *)malloc(5*sizeof(int))) == NULL) //numbers2 指针申请空间
{
printf("malloc memory unsuccessful");
//free(numbers2);
//numbers2=NULL;
exit(1);
}
for (n=0; n<5; n++) // 初始化 (0-4)
{
*(numbers2+n)=n;
printf("numbers2's data: %d/n", *(numbers2 +n)); // 把 0-4 打印出来
}
printf("Enter an integer value you want to remalloc ( enter 0 to stop)/n"); // 新申请空间大小
scanf ("%d", &input );
numbers1 = (int *)realloc(numbers2, (input +5)*sizeof(int)); // 重新申请空间
if (numbers1 == NULL)
{
printf("Error (re)allocating memory");
exit (1);
}
for(n=0;n<5;n++) // 这 5 个数是从 numbers2 (原指针空间)拷贝而来
{
printf("the numbers1s's data copy from numbers2: %d/n", *(numbers1 +n));
}
for(n=0; n<input; n++) // 新数据初始化( 0-input )
{
*(numbers1+5+n ) = n*2;
printf ("nummber1's new data: %d/n", *(numbers1+5+n )); // numbers1++;
}
printf("/n");
free(numbers1); // 释放 numbers1
numbers1 = NULL;
// free(numbers2); // 不能再释放 numbers2 ,因为 number2 早已被系统自动释放
return 0;
}