相对于其他语言,C、C++的一大利器便是可以非常灵活的控制内存。与此同时,另一方面灵活的带来的要求也是十分严格,否则会出现令人头疼的分配错误、内存越界、内存泄漏等众多内存问题。
C程序的内存结构分为两种,一种是存储在磁盘时的结构,一种是程序运行时的结构。两者的区别在与运行时,系统会为其多分配堆栈空间。
图:C程序内存结构
下面通过一个例子看看具体的分配
#include <stdio.h>
int bss_var;
int data_var = 1;
int main(int argc,char** argv)
{
int stack_var = 2;
printf("栈 stack \n");
printf("--------------------\n");
printf("\t stack_var地址:%p\n",&stack_var);
int stack_var2 = 3;
printf("\t stack_var2地址:%p\n",&stack_var2);
printf("堆 heap\n");
printf("--------------------\n");
char *b = (char *)malloc(sizeof(char));
printf("\t b地址 :%p\n",b);
printf("未初始化数据段 .bss \n");
printf("--------------------\n");
printf(" \t bss_var地址:%p \n",&bss_var);
printf("已初始化数据段 .data \n");
printf("--------------------\n");
printf(" \t data_var地址:%p \n",&data_var);
printf("代码段 text \n");
printf("--------------------\n");
printf("\t main函数地址:%p\n",main);
return 1;
}
输出的结果。
栈 stack
--------------------
stack_var地址:0x7fff5fbff71c
stack_var2地址:0x7fff5fbff718
堆 heap
--------------------
b地址 :0x100203850
未初始化数据段 .bss
--------------------
bss_var地址:\314p
已初始化数据段 .data
--------------------
data_var地址:0x100001028
代码段 text
--------------------
main函数地址:0x100000cf0
堆与栈
栈是一种的“先进后出”的存储结构。
堆是一种完全二叉树。节点从左到右填满,最后一层的树叶都在最左边。(即如果一个节点没有左边儿子,那么它一定没有右边儿子),每个节点的值都小于(或者大于)其子节点的值(大顶堆、小顶堆)。它的特点是可以使用一维数组来表示。堆的操作也可通过数据元素交换的形式解决,非常适合内存空间线性的特点。
C语言中,内存分配有三种
先从汇编的角度来看堆与栈在分配空间的区别。
下面这个例子非常简单,给栈变量a赋值0xA,给堆分配的变量赋值0xB
int a = 0xA;
char *b = (char *)malloc(sizeof(char));
*b = 0xB;
int c = a+*b;
下面是汇编代码
Test`main:
0x100000f10 <+0>: pushq %rbp
0x100000f11 <+1>: movq %rsp, %rbp
0x100000f14 <+4>: subq $0x20, %rsp
0x100000f18 <+8>: movl $0x1, %eax
0x100000f1d <+13>: movl %eax, %ecx
0x100000f1f <+15>: movl $0x0, -0x4(%rbp)
0x100000f26 <+22>: movl %edi, -0x8(%rbp)
0x100000f29 <+25>: movq %rsi, -0x10(%rbp)
0x100000f2d <+29>: movl $0xa, -0x14(%rbp)
0x100000f34 <+36>: movq %rcx, %rdi
0x100000f37 <+39>: callq 0x100000f62 ; symbol stub for: malloc
0x100000f3c <+44>: movq %rax, -0x20(%rbp)
0x100000f40 <+48>: movq -0x20(%rbp), %rax
0x100000f44 <+52>: movb $0xb, (%rax)
0x100000f47 <+55>: movq -0x20(%rbp), %rdi
0x100000f4b <+59>: callq 0x100000f5c ; symbol stub for: free
0x100000f50 <+64>: movl $0x1, %eax
0x100000f55 <+69>: addq $0x20, %rsp
0x100000f59 <+73>: popq %rbp
0x100000f5a <+74>: retq
对于栈变量a的内存分配与赋值,这里的做法非常简单,主要的两个指令便可以完成分配与赋值操作。
1.栈顶指针寄存器向低地址移动0x30个字节空间(subq $0x30, %rsp),也可以理解为分配了0x30个字节空间给当前堆栈,而此时栈中已经包含变量a的空间。 2.将直接将立即数0xA赋值给变量a movl $0xa, -0x14(%rbp)。此时我们可以知道,相对于栈基指针寄存器向低地址偏移0x14的地址便是a的内存区域。 3.当函数执行完毕后,栈顶指针寄存器rsp与栈基地址寄存器rbp,回退到上一函数,步骤1中分配的的空间也同时被释放。
而堆变量b的内存分配与赋值,则可以看到其是通过调用callq 0x100000f68实现的(此处0x100000f68指的是malloc函数的地址)。也就是变量b内存的分配是由malloc函数内部实现,并没有像栈变量分配一样通过简单的两个指令便可以完成。
void *malloc(size_t size)
从堆中分配内存,分配大小为size。若分配成功,返回内存首地址,如果分配失败,返回NULL。
void *calloc(size_t count, size_t size)
从堆中分配内存,分配count个相邻的内存单元,每个单元大小为size。若分配成功,返回内存首地址,如果分配失败,返回NULL。 从功能上看,该函数与malloc差不不大,不同的是calloc函数会将内存初始化为0。
有人会问既然calloc已经覆盖malloc所做的事情,而且还非常方便的将内存初始化为0,那malloc不就不太有用了吗?其实在调用方看来malloc而不需要初始化为0的情况,可能分配内存后马上赋值了有用的数据,不需要初始化为0.
void realloc(void ptr, size_t size)
用于更改已经配置的内存空间,其同样是从堆中分配内存。 当程序需要扩大空间时,函数试图从堆上当前内存段后的字节中获取更多的内存空间,如有足够的存储空间,则扩大内存后返回原地址。如果当前内存段后的字节不够,则使用内存堆上满足要求的其他内存块,并将原有的数据拷贝至新分配的区域,然后释放原有区域,返回新区域的指针。
void *alloca(size_t)
不同于malloc、calloc、realloc是从堆中分配内存,alloca是从栈中分配空间。正因其从栈中分配的内存,因此无需手动释放内存。
使用malloc、calloc、realloc从堆中分配内存时,需要及时释放
使用内存分配函数获取指针变量时,需堆分配函数的返回值进行判空处理。 因内存分配函数可能会因为其他的一些不可预知的情况导致分配失败。
char * chp = (char *)malloc(100);
if(NULL == chp){
//处理内存分配失败
}else{
//正常逻辑
}
因内存分配函数返回值都为void (也称无类型),而且void 无法对该一段内存区域进行移位访问操作,所以在使用分配函数必须对其转换成其他类型,以便进行操作。
char * chp = (char *)malloc(sizeof(char)*100);
在使用malloc进行分配时,因该内存函数为进行初始化,若此时对内存进行访问,很可能会造成程序崩溃
char * chp = (char *)malloc(sizeof(char)*100);
if(NULL == chp){
//处理内存分配失败
}else{
memset(chp,sizeof(char)*100),0);
}
C99规定,程序尝试分配长度为0的内存时,该行为是由具体编译器所决定的。可能会到时程序崩溃,可能返回一个NULL指针。所以需要避免此行为。
一块内存区域使用free释放后,需要养成将其设置为NULL的习惯,以避免在程序错误的再次访问指针时造成野指针访问错误。
char *b = (char *)malloc(sizeof(char)*4);
memset(b,sizeof(char)*4,0);
strcpy(b,"abc");
free(b);
//此处应该加上 b = NULL;
if (b) {
//发生错误,非法访问野指针
strcpy(b,"def");
}
对于不是通过内存分配函数获取的空间,禁止非法访问。
char *b = (char *)malloc(sizeof(char)*4);
b[5] = '1'; //错误,指针越界!
b = b--; //错误,不能直接操作内存
b[0] = 'a'
C语言中,只要是一个指针变量,那就需要确保其指向是一段合法有效的值。
struct student{
char *name;
int id;
};
struct student std;
strcpy(std.name, "Jack"); //非法赋值,std.name指针指向一个非法的值
如上例子中,需要给指针变量分配一段合法的内存
struct student std;
std.name = malloc(sizeof(char)*20);
strcpy(std.name, "Jack");
在释放c语言中的结构体时,需要确保其成员属性中的所有内存都释放,以免出现内存泄漏。 延续上面的例子
struct student{
char *name;
int id;
};
struct student * std = malloc(sizeof(struct student));
std->name = malloc(sizeof(char)*20);
strcpy(std->name, "Jack");
free(std);
仅仅释放std指针是不够的,需要释放其name成员,而且释放的顺序也需要注意,是先成员后对象。
free(std->name);
free(std);
各个内存分配函数中对于大小的参数都是size_t,在分配内存时需要确保避免申请过大的内存空间。
C语言中对于内存使用是十分灵活与方便的,正是由于其过于灵活,我们在使用它时,需要对于分配出来的内存块的大小,初始化,生命周期,释放时机,释放方法,有个非常清楚的了解,要清楚的了解分配的每一块内存的去向,做到胸有成竹才能用好这一利器。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。