更新中。。。。。
一、c/c++内存分布
内存布局(展现了各个区的位置关系)
Code Segment(代码区)
代码段存放可执行指令和只读常量,在内存中,为了保证不会因为堆栈溢出被覆盖,将其放在了堆栈段下面(从上图可以看出)。通常来讲代码段是共享的,这样多次反复执行的指令只需要在内存中驻留一个副本即可。代码段一般是只读的,程序执行时不能随意更改指令,也是为了隔离保护。
Data Segment (数据区)
数据段存放已初始化的全局变量和静态变量。数据段是可以修改的,不然程序运行时变量无法修改了。
BSS(Block started by symbol)
BSS段属于静态内存分配,存放未初始化的全局和静态变量,由内核初始化为0。
Heap(堆)
堆是动态内存分配区,堆地址起始于BSS末端,然后从这里向高地址增长,容量大于栈。堆中内存分配管理由malloc, remalloc和free标准库函数完成。堆可以被进程的所有共享库以及动态加载模块共享。
Stack(栈)
程序中的局部变量、函数参数值、返回变量等存在栈区。
栈区和堆区一般相邻,但沿着相反方向增长。当栈指针和堆指针相等就说明堆栈内存耗尽。(现代大地址空间和虚拟内存技术可以将栈和堆放在任何地方,但是二者增长方向也是相反的)。栈区存放程序的栈,一种LIFO结构,一般都在内存的高地址段。栈指针寄存器记录栈顶地址,每次有值push进栈就会对栈指针进行修改。一个函数push进栈的一组值被称做堆栈帧,堆栈帧保存有返回地址的最小的返回地址。栈中存放有自动变量和每次函数调用时的信息。每次函数调用返回地址,一些调用者环境信息(比如寄存器)都被存放在栈中。然后新调用的函数就在栈中为他们的自动或者临时变量分配内存空间这就是C中递归函数调用的过程。每次递归函数调用自己,新的堆栈帧就被创建,这样新的变量集合就不会被其他函数实例的变量集合影响了。
来看一段示例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string.h>
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof (int)* 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)* 4);
free(ptr1);
free(ptr3);
std::cout << "sizeof(num1):" <<sizeof(num1) << std::endl;
std::cout << "sizeof(char2):"<<sizeof(char2) << std::endl;
std::cout << "strlen(char2):" << strlen(char2) << std::endl;
std::cout << "sizeof(pChar3):" << sizeof(pChar3) << std::endl;
std::cout << "strlen(pChar4):" << strlen(pChar3) << std::endl;
std::cout << "sizeof(ptr1):" << sizeof(ptr1) << std::endl;
}
int main()
{
Test();
return 0;
}
记住: sizeof是一个C语言中的一个单目运算符,而strlen是一个函数,用来计算字符串的长度。sizeof求的是数据类型所占空间的大小,而strlen是求字符串的长度,strlen(结束的标志是是否碰到\0)。
说明:
-
栈又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
-
内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)。
-
堆用于程序运行时动态内存分配,堆是可以上增长的。
-
数据段–存储全局数据和静态数据。
-
代码段–可执行的代码/只读常量。
解释栈为啥是向下生长,堆是向上生长。
栈向下生长是因为在栈区开辟的空间是先开辟的空间在高地址,后开辟的空间在低地址。
堆向上生长是因为在堆区开辟空间先开辟的空间在低地址,后开辟的空间在高地址。
注意:在堆区后一个开辟的空间的地址不一定比前面先开辟空间的地址大,因为可能后开辟空间的地址是在前面释放的空间上开辟的地址。
二、C语言中动态内存管理方式
realloc、malloc、以及calloc函数的区别
三者都是分配内存,都是stdlib.h库里的函数
malloc
void *malloc(unsigned int num_bytes) //原型
num_byte为要申请的空间大小,需要我们手动的去计算,
int *p = (int *)malloc(20*sizeof(int))
编译器默认int为4字节存储的话,那么计算结果是80Byte,一次申请一个80Byte的连续空间,并将空间基地址强制转换为int类型,赋值给指针p,此时申请的内存值是不确定的。
calloc
void *calloc(size_t n, size_t size);
从直观的看,其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,
int *p = (int *)calloc(20, sizeof(int))
这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0; 如下:
int *p = (int *)malloc(20*sizeof(int)); int *pp = (int *)calloc(20, sizeof(int)); printf("malloc申请的空间值:\n\n"); for (int i=0 ; i < 20; i++) { printf("%d ", *p++); } printf("\n\n"); printf("calloc申请的空间的值:\n\n"); for (int i=0 ; i < 20; i++) { printf("%d ", *pp++); }
结果为:malloc申请的空间值是未初始化的,而calloc申请的空间值初始化为0
既然calloc不需要计算空间并且可以直接初始化内存避免错误,那为什么不直接使用calloc函数,那要malloc要什么用呢?实际上,任何事物都有两面性,有好的一面,必然存在不好的地方。这就是效率。calloc函数由于给每一个空间都要初始化值,那必然效率较malloc要低,并且现实世界,很多情况的空间申请是不需要初始值的。 realloc:
void realloc(void *ptr, size_t new_Size)
realloc函数和上面两个有本质的区别,用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作)ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小
实际上:
-
如果size较小,原来申请的动态内存后面还有空余内存,系统将直接在原内存空间后面扩容,并返回原动态空间基地址;
-
如果size较大,原来申请的空间后面没有足够大的空间扩容,系统将重新申请一块(20+size)*sizeof(int)的内存,并把原来空间的内容拷贝过去,原来空间free;
-
如果size非常大,系统内存申请失败,返回NULL,原来的内存不会释放。
注意:如果扩容后的内存空间较原空间小,将会出现数据丢失,如果直接realloc(p, 0);相当于free(p)。
malloc/calloc/realloc和free
// 1.malloc/calloc/realloc的区别是什么
int main()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)calloc(4,sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
free(p1);
//free(p2); // 这里需要free(p2)吗?
free(p3);
return 0;
}
注意:如果此时你在p2的基础上扩容,则p2则不需要自己释放,否则会发生错误
原地扩容:需要扩容的空间后面有充足的空间可以扩容,realloc函数直接在原来的空间后方进行扩容,成功则返回该内存空间首地址(即原来的首地址)。
异地扩容:需要扩容的空间后方并没有足够的空间可供扩容,那么,realloc函数会在堆区中再找一块满足大小的内存空间然后将原来空间内的数据拷贝到新空间中,在主动将原空间内存释放(即还给操作系统),不需要自己手动free原有空间,否则会发生错误,最后返回新内存空间的首地址。
扩容失败:此时堆空间中没有足够的空间来扩容,此时就是扩容失败,返回NULL。