一 程序的内存分区
内存一般分为五个区:
1 堆区(stack)---由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
2 栈区(heap)---一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3 全局区(static)---全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放
4 文字常量区---常量字符串就是放在这里的。 程序结束后由系统释放
5 程序代码区---存放函数体的二进制代码。
//main.cpp
int a = 0; //全局初始化区
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"优化成一个地方。
}
内存模型:
堆:
寄存器(栈)只能存放少量的数据,大多数时候, CPU 还要指挥寄存器直接跟内存交换数据,所以除了寄存器,还必须了解内存怎么存储数据。
程序运行的时候,操作系统会给它分配一块内存,用来存储程序和运行产生的数据,比如从 0x1000 到 0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。
程序运行过程中,对于动态的内存占用请求(比如新建对象),系统就会从预先分配好的那段内存中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址 0x1000 开始给他分配,一直分配到 0x100A,如果在要求得到 22 个字节,那么就分配到 0x1020。
这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)
。它由起始位置开始,从低位(地址)向高位(地址)增长。 Heap
的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。
动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,需要一个保存在stack中的指针,因为要通过stack中的指针访问heap 中的数据。
可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。
栈:
除了 Heap
外,其他的内存占用叫做 Stack(栈)
。简单来说,Stack
是由于函数运行而临时占用的内存区域。
int main() {
int a = 2;
int b = 3;
}
上面代码中,系统开始执行 main
函数时,会为它在内存里面建立一个 帧(frame)
,所有的 main
的内部变量(比如 a
和 b
)都保存在这个 帧
里面。main
函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。
当主函数调用了其他函数:
int main() {
int a = 2;
int b = 3;
return add_a_and_b(a,b);
}
所有的帧都存放在 Stack ,由于帧是一层层叠加的,所以 Stack 叫做 栈。生成新的帧,叫 入栈,英文单词是 push;栈的回收叫 出栈,英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束执行),这种叫做 后进先出 的数据结构。每一次函数执行结束,就自动释放一个帧,所有的函数执行结束,整个 Stack 就都释放了。
Stack 是由内存区域的结束地址开始,从高位(地址)向地位(地址)分配。比如,内存区域的结束地址是 0x8000,第一帧假定是 16 字节,那么下一次分配的地址就会从 0x7FF0 开始;第二帧假定需要 64 字节,那么地址就会移到 0x7FB0。
二 代码段、数据段、bss段:
首先看一个例题,以下分配在BSS段的变量是?
char s1[100];
int s2 = 0;
static int s3 = 0;
int main() {
char s4[100];
}
答案是:
S1
-
代码段(Code Segment 或 Text Segment)对应于内存分区中的可执行代码区域,存储程序的指令和函数定义。
-
数据段(Data Segment)对应于内存分区中的已初始化全局变量和静态变量的区域。
-
BSS段对应于内存分区中的未初始化的全局变量和静态变量的区域。
三 动态内存函数
NEW和malloc
1.new内存分配失败时,会抛出bac alloc异常,它不会返回NULL; malloc分配内存失败时返回NULL;
2.使用new操作符申请内存分配时无须指定内存块的大小,而malloc则需要显式地指出所需内存的尺寸;
3.opeartor new /operator delete可以被重载,而malloc/free并不允许重载。;
4.new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会;
5.malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符;
6new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存.
四 柔性数组
C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少有一个其他成员。
struct S
{
int num;
double d;
int arr[];//柔性数组成员
};
4.1 柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员;
2.sizeof返回的这种结构大小不包括柔性数组的内存;
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S3
{
int num;
int arr[0];//柔性数组成员
};
int main()
{
printf("%d\n",sizeof(struct S3));//4
return 0;
}
4.2 柔性数组的使用
S3
{
int num;
int arr[0];//柔性数组成员
};
int main()
{
printf("%d\n",sizeof(struct S3));//4
//开辟空间
struct S3* ps = (struct S3*)malloc(sizeof(struct S3) + 40);//40用于柔性数组成员
//判空
if (ps == NULL)
{
perror("malloc");
return 1;
}
//使用
ps->num = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ",ps->arr[i]);
}
//扩容
struct S3* ptr = (struct S3*)realloc(ps, sizeof(struct S3) + 80);
//判空
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
{
ps = ptr;
}
//使用
for (i = 10; i < 20; i++)
{
ps->arr[i] = i;
}
//继续打印
for (i = 10; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
//释放
free(ps);
ps = NULL;
return 0;
}
4.3 柔性数组的优势
方便内存释放;
有利于访问速度。