对内存分配的思考

一、预备知识—程序的内存分配

一个由C/C++编译的程序占用的内存分为以下几个部分

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其
操作方式类似于数据结构中的栈。

2、堆区(heap)一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放。

4、文字常量区。常量字符串就是放在这里的,程序结束后由系统释放。

5、程序代码区。存放函数体的二进制代码。

二、堆和栈的理论知识

1.申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = new char[10];
但是注意p1、p2本身是在栈中的。

2.申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3.申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储
的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小
受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

4.申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

5.堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

6.存取效率的比较

char s1[] = “aaaaaaaaaaaaaaa”;
char *s2 = “bbbbbbbbbbbbbbbbb”;
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = “1234567890”;
char *p =“1234567890”;
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。

下面,我们通过代码段来看看对这样的三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。

①静态存储区与栈区

char* p = “Hello World1”;
char a[] = “Hello World2”;
p[2] = ‘A’;
a[2] = ‘A’;

char* p1 = “Hello World1;”

这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据“Hello World1”和数据“Hello World2”是存储于不同的区域的。

因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据

②栈区与堆区
char* f1()
{
char* p = NULL;
char a;
p = &a;
return p;
}
char* f2()
{
char* p = NULL:
p =(char*) new char[4];
return p;
}

这两个函数都是将某个存储空间的地址返回,二者有何区别呢?f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。所以,当调用f1()函数时,如果程序中有下面的语句:
char* p ;
p = f1();
*p = ‘a’;

此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。但是,相比之下,f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:

void f()
{

char * p;
p = (char*)new char[100];

}

这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏。这是我们C++程序员的大忌!!请大家一定要避免这件事情的发生。

三、举例说明

//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = “abc”; 栈
char *p2; 栈
char *p3 = “123456”; 123456\0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, “123456”); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"
优化成一个地方。
}

四、总结

对于堆区、栈区和静态存储区它们之间最大的不同在于,栈的生命周期很短暂。但是堆区和静态存储区的生命周期相当于与程序的生命同时存在(如果您不在程序运行中间将堆内存delete的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 (经典!)

字节对齐规则

第一条:第一个成员的首地址为0
第二条:每个成员的首地址是自身大小的整数倍。补充:以4字节对齐为例,如果自身大小大于4字节,都以4字节整数倍为基准对齐。
第三条:最后以结构总体对齐。补充:以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。(其中这一条还有个名字叫:“补齐”,补齐的目的就是多个结构变量挨着摆放的时候也满足对齐的要求。)


typedef struct Demo_B
{
    char name[5];   // 0 - 4 ;
    int id;         // 8 - 11 ;
    double score;   // 16 - 23 ;
    float ui;       // 24 - 27 ;  
    short grade;    // 28 - 29 ;
    char weight;    // 30 ;
}BB;

BB结构体内存0-30 长度为31 ,根据最后一条, sizeof(BB) = 32

内存泄漏

动态内存忘记释放;延迟(时间比较久)释放,都可称为内存泄漏。
造成原因:
忘记释放
释放动态内存的代码没有被执行到
内存泄漏的检测
valgrind
gdb
内存泄露的问题:
程序运行时间越长,占用的内存越多,最后可能导致内存耗尽,导致程序崩溃。

动态内存分配原理(32bit操作系统为例)

在程序中,第一次申请动态内存,malloc至少分配33页的内存

1页 = 4096Byte = 4KBls

#include <unistd.h>    //不是标准C语言头文件   而是linux操作系统文件  windows下也有对应的
int getpagesize(void);    //获得一页内存的大小    字节数   4096Byte(字节) = 4KB
1
2
int main(){
    char *s = malloc(1); //申请1byte
    int i;
    //打印了33页的数据
    for(i=-8;i<33*4096-8;i++){//[s-8,s+33*4096-8)   这片内存打印
        printf("%x ",s[i]);
    }
    printf("\n");
    free(s);
}

即使所有的动态内存都free,最开始分配的33页动态内存一定会被保留

32bit处理器下,申请的动态内存自动以4字节对齐,即使申请1个字节,但其实也有8byte的空间

对于每一块动态内存,都有额外的一块空间用于保存该动态内存块的信息,一般是8-12byte

把这8-12byte称为动态内存块控制信息 (是否空闲,大小,下一个动态内存块)
动态内存块控制信息一旦修改,在free时基本上都会导致程序崩溃
每两块动态内存之间,至少相隔16byte(64bit处理器 32byte) 至少8byte的动态内存控制块信息 至少4byte的数据(malloc给用户的空间, 4byte缓冲 realloc只是增加4字节,并会不使用新的内存)

使用动态内存,遵循申请几个字节,使用几个字节的原则,不要越界访问

如果越界访问,可能导致各种问题
如果在申请动态内存中,之前分配的33页没有用完之前,后续的malloc都是在33页内存里划分一块动态内存

如果最初的33页使用完了,后续分配是以页为单位 如果发现不够,则映射一页内存,如果发现有内存页存在空闲,则回收页内存

申请动态内存的过程

如果第一次申请 分配33页的动态内存 然后划出一块内存(以及8字节动态内存块) 返回用户一块可以使用的内存(一般都在控制信息块后面)
如果不是第一次,则从已经动态内存块中查找空闲(被free)动态内存块且大小合适(动态内存块的大小不小于用户申请内存的大小)的内存块返回给用户,且设置为忙碌(非空闲状态)
如果目前没有合适的,则从后面的动态内存中划分一块合适的内存
如果后面内存不足,则进行页映射(分配新的一页)
如果在free过程中,发现多块动态内存处于空闲状态,则合并动态内存块
如果发现页空闲(除最开始33页外),则回收页(取消页映射)
以上的目的是为了提高分配申请的效率

内存碎片

动态内存在不断地申请和释放的过程中,导致内存块之间的一些内存永远也无法被使用到,称为内存碎片
如何减少内存碎片,小内存使用栈,大内存使用堆 及时释放没必要的动态内存
在重复申请和释放的过程中,有一些大的内存块分配给小于该内存块字节数据的用户申请使用,导致部分内存无法被使用,----内存碎片
为什么小块内存不要用堆内存
分配耗时
每一块动态内存都需要额外至少8byte的动态内存块控制信息,内存的利用率不高
容易造成内存碎片
什么时候使用动态内存
不清楚需要使用多少内存,动态变化的内存 栈内存大小是固定的

内存比较大时 小内存建议使用栈

项目中,存储数据往往都是使用动态内存

用,----内存碎片

为什么小块内存不要用堆内存
分配耗时
每一块动态内存都需要额外至少8byte的动态内存块控制信息,内存的利用率不高
容易造成内存碎片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值