C语言程序在内存中的分布:
bss段:该段用来存放没有被初始化或者初始化为0的全局变量,以及被static修饰的未初始化的局部变量。在程序运行的整个生命周期内都存在于内存中。这个段中的变量只占用程序运行时的内存空间,而不占用程序文件的储存空间。
举个例子:定义一个1MB的未初始化的全局变量 (char类型只占一个字节 定义1024*1024个char类型说明有1MB)
#include <stdio.h>
char bss[1024*1024];
int main()
{ return 0;}
现在来看看程序的可执行文件大小
定义 char bss[1024*1024]={0};也是这个结果。
可以看到 bss的大小并没有1MB 说明未初始化的全局变量不占程序文件的存储空间。
data段(数据段):存放初始化过的全局变量和static修饰的初始化过的变量,程序运行的整个生命周期内都存在于内存中。这个段中的变量不仅占用程序运行时的内存空间,也占用程序文件的储存空间。
这里我们也举个例子: 定义一个1MB的初始化过的全局变量
#include<stdio.h>
char data[1024*1024]={1};
int main()
{return 0;}
运行结果:
可以看到可执行文件data的大小有1MB 因为数组分配的内存是连续的 所以就算只初始化了一个元素 后续内存依然会被开辟出来
rodata段(只读段):存储的是一些只能读取不能修改的数,一般是程序里面的只读变量(如const修饰的变量)和字符串字面量。ro就是Read Only的意思。
char* p = "12345"
字符串"12345"可能在只读段,也有可能在代码段,看编译器而定。
text段(代码段):程序的二进制数据,这段内存只能读,不能修改,这部分区域的大小在程序运行之前就已经确定。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。程序段为程序代码在内存中的映射,一个程序可以在内存中有多个副本。
stack(栈区):保存函数的局部变量,参数以及返回值(但不包括static声明的变量)。是一种“后进先出”(Last In First Out,LIFO)的数据结构,这意味着最后放到栈上的数据,将会是第一个从栈上移走的数据。对于哪些暂时存贮的信息,和不需要长时间保存的信息来说,LIFO这种数据结构非常理想。在调用函数或过程后,系统通常会清除栈上保存的局部变量、函数调用信息及其它的信息。栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。
heap(堆区):堆是用于存放进程运行中被动态分配的内存段,更准确的说是保存程序的动态变量。它完全由程序来负责内存的管理,包括什么时候申请,什么时候释放,而且对它的使用也没有什么大小的限制。
如何使用堆内存:
C语言是没有操作内存的语句的,只能使用标准库提供的函数stdlib.h 使用堆内存要指针配置使用。
内存申请函数 size表示要申请的字节数 void*返回的是申请到的内存的首地址。
void *malloc(size_t size);
内存释放函数 ptr是要释放的内存的首地址 内存释放后ptr要及时的设置为空。
void free(void *ptr);
举个例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("%d\n",getpid()); //得到进程id
char* p = malloc(sizeof(char)*10);
char str1[7] = "123avbd";
char* str2 = "101";
printf("&p:%p\np:%p\n",&p,p);
printf("str1:%p\nstr2:%p\n",str1,str2);
while(1){}; //死循环不让程序结束
free(p);
return 0;
}
让我们看一下结果:
然后新开一个终端 输入 vim/proc/3149/maps 查看内存分配情况
可以看到 在堆区的只有p 而&p则是在栈区
str1存放在栈区
str2存放在只读区
注意:在堆区申请完内存使用完毕后一定要记得释放,即free,否则会造成内存泄漏
其他的有关内存的操作函数:
1.申请适合数组使用的内存,size指的是一次申请多少个字节的内存,nmemb指的是申请多少次size,申请到的内存会被设置为0
void *calloc(size_t nmemb, size_t size);
2.调整内存的大小,可以把ptr指向的内存,变大或变小。如果内存被调小,数据不会立即删除,会一直存在,直到被别人覆盖;如果内存调大,如果后面没有被使用,则在原来的基础上调大,如有人使用,会重新开辟一块内存,再把原来的数据复制过去。
void *realloc(void *ptr, size_t size);
内存操作辅助函数:
malloc函数申请到的内存内容是随机的以下函数可以把内存清理了。
s为要清理的内存的首地址,n为内存的字节数 (头文件是strings.h)
void bzero(void *s, size_t n);
s为要设置数据的内存的首地址,c为要设置的值(以字节为单位),n内存的字节数 (头文件是 string.h)
void *memset(void *s, int c, size_t n);
碎片问题:对于堆来讲,频繁的malloc()势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低,也就是说当如果没有足够大小的空间,malloc()申请内存可能会失败。
虚拟内存:
每个程序启动后,就有了0~4G的内存空间地址,但不能直接使用,因为它们是虚拟的。
相当于操作系统给的一张空头支票,如果需要使用这些内存,需要让操作系统把这些内存与物理内存联系起来。
内存映射:
把虚拟内存与物理内存对应起来的过程叫内存映射,此时内存归malloc函数所有。
当第一次向malloc申请内存时,操作系统会一次给程序映射33页内存(1页=4096byte)。
内存分配:
把内存使用权从malloc手里要过来,内存的释放也只是把内存使用权限交还给malloc。
在使用malloc管理内存的过程,会有一些内存用来记录malloc的分配情况。
内存越界:
1、超过映射的范围,会出现段错误。
2、在映射范围内,会出现脏数据。
3、当把malloc分配情况的信息修改了,会造成申请和释放内存的错误。
我们看个例子:
#include<stdio.h>
#include<string.h>
int main()
{
char str[10]={};
char arr[10]="0123456789";
printf("str:%p\narr:%p\n",str,arr);
puts(str); //打印str
puts(arr); //打印arr
strcpy(str,"abcdef123456");
puts(str);
puts(arr);
printf("len=%d\n",strlen(str));
printf("size=%d\n",sizeof(str)/sizeof(str[0]));
return 0;
}
这里定义了两个数组一个为空,一个初始化了,然后将字符串通过strcpy(字符串拷贝函数)赋给了str
此时这串字符串的长度已经大于str定义的长度10了,可是编译结果却没有报错 看下编译结果:
str甚至能够完整的输出 长度通过strlen(求字符串长度函数)来看也变成了12 但是通过sizeof可以看到str的长度还是10
而且这里arr的值却变成了56 这是为什么呢?
看下输出的前两行,打印的是两个数组的首地址 ,str的地址+10之后就是arr的首地址了,当使用strcpy函数时,发现str本身的大小不够拷贝了,于是它就会往后找,正好后面开辟了一段内存可以用,于是strcpy就把arr的内存给占用了,导致arr本来的数据被覆盖 ,这就是脏数据,也是所谓的内存越界的形式之一。
内存越界访问造成的后果非常严重,是程序稳定性的致命威胁之一。更麻烦的是,它造成的后果是随机的,表现出来的症状和时机也是随机的,让BUG的现象和本质看似没有什么联系,这给BUG的定位带来极大的困难。
对于内存越界,比较保险的方法还是在编程时就小心,特别是对于外部传入的参数要仔细检查(特别是外来的指针)。