目录
[扩展:exit 库函数](#扩展:exit 库函数)
[1. 代码段(红色箭头)](#1. 代码段(红色箭头))
[2. 数据段(全局 / 静态区,紫色箭头)](#2. 数据段(全局 / 静态区,紫色箭头))
[3. 堆(黄色箭头,向上增长,低→高地址)](#3. 堆(黄色箭头,向上增长,低→高地址))
[4. 栈(绿色箭头,向下增长,高→低地址)](#4. 栈(绿色箭头,向下增长,高→低地址))
[5. 内存映射段](#5. 内存映射段)
[6. 内核空间](#6. 内核空间)
一、为什么要有动态内存分配
有些我们需要的空间大小,只要在程序运行的时侯才能确定,而且需要随时改变大小,如果只有数组,数组空间一旦确定,就无法被修改,无法达到我们需要的要求,所以此时,C语言就引入了动态内存开辟,让程序员可以自己申请和释放空间,程序边运行边扩大
这里要讲解一个误区:变长数组不也是能达到改变数组空间大小的能力吗?为啥还要动态内存?
变长数组确实是可以改变空间大小,但我们要注意一点,变长数组改变空间,需要让程序停止,重新输入大小才能改变空间,而我们需要的是在运行的时候就能随时改变大小,变长数组改变大小不是动态的,所以需要我们的动态内存开辟
动态内存的开辟、释放、调整都是在堆区上进行的,如果不free,生命周期为整个程序,结束时才会收回动态开辟的内存;数组是在栈区申请内存的,有作用域,生命周期为整个作用域,出了作用域就会被回收空间
二、动态内存操作函数
2.1malloc函数
malloc函数是用于在堆区上,向内存申请开辟空间的函数:
cpp
void* malloc (size_t size);
malloc → 全拼 memory allocation → 内存分配(不初始化)
malloc函数使用的注意事项:
- malloc向内存堆区上申请一块连续可用的空间,size是以字节为单位,并返回指向这块空间的指针
- 如果开辟成功,则返回指向这块空间的起始地址的指针
- 如果开辟失败,则返回一个空指针NULL,所以malloc在开辟内存后,要对返回值进行检查,才能知道是否开辟成功
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,需要使用者在使用的时候换成所需的类型
- 如果参数 size 为0,malloc的行为是标准是未定义的,具体取决于编译器,有些返回空指针,有些开辟一块极小的空间,返回一个地址
- malloc函数开辟空间后,不会进行初始化,空间存的是随机值
下面来看一个使用案例:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 申请10个int空间,自动全部置0
int* p = (int*)malloc(40);
if (NULL != p)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // 输出全部0
}
}
free(p); // 释放堆内存
p = NULL; // 置空防野指针
return 0;
}
我们使用malloc函数开了一个40个字节大小的空间,并进行判断是否开辟成功,如果成功,我们就打印出来看看里面存的是什么值,打印完后无需再使用这块内存,所以我们释放掉free(p),并将p置为空指针NULL
运行截图:

可以看到,malloc函数开辟的空间里存的是随机值,不会进行初始化
2.2free函数
free是用于主动释放由动态内存开辟函数开辟的内存的函数:
cpp
void free (void* ptr);
free函数使用的注意事项:
- 无需返回值,参数ptr为动态内存开辟空间的起始地址
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
- 如果参数 ptr 是NULL指针,则函数什么事都不做
- free释放空间后,这块空间里存的是随机值
下面看一个使用案例:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
//malloc模拟开辟一维数组
int* ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr)
{
int i = 0;
for (i = 0; i < num; i++)
{
*(ptr + i) = 0;
}
}
free(ptr); // 释放动态内存
ptr = NULL; // 必须置空,避免野指针
return 0;
}
我们使用malloc在堆区中开辟了一个num个整型大小的空间,相当于开辟了一个大小为num的整型一维数组,然后我们需要判断是否开辟成功,如果(NULL != ptr),我们就将这个空间的所有字节都初始化为0,最后要释放我们自主开辟的动态空间free(ptr),并将ptr置为空指针NULL,防止野指针的出现
2.3calloc函数
calloc函数也是用来在内存堆区申请空间的函数,但它的参数和实现效果和malloc略有不同:
cpp
void* calloc (size_t num, size_t size);
calloc → 全拼 contiguous allocation → 连续分配(自动清 0)
calloc函数使用的注意事项:
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节都初始化为0
- 如果开辟成功,则返回指向这块空间的起始地址的指针
- 如果开辟失败,则返回一个空指针NULL,所以calloc在开辟内存后,要对返回值进行检查,才能知道是否开辟成功
- 返回值的类型是 void* ,所以calloc函数并不知道开辟空间的类型,需要使用者在使用的时候换成所需的类型
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0,和参数不一样
下面我们来看看使用案例:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 申请10个int空间,自动全部置0
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // 输出全部0
}
}
free(p); // 释放堆内存
p = NULL; // 置空防野指针
return 0;
}
我们使用calloc函数开辟了一个大小为10个int类型的元素的空间大小,也就是10*4=40个字节的空间大小,判断一下是否成功开辟if (NULL != p),如果成功,则我们将里面的值都打印出来,打印完后无需再使用这块内存,所以我们释放掉free(p),并将p置为空指针NULL
运行截图:

可以看到,calloc函数在开辟空间时,确实将空间里面的值都自动初始化为0
2.4realloc函数
realloc 函数是用于调整我们动态开辟的内存的大小的函数:
cpp
void* realloc(void* ptr, size_t size);
realloc → re allocation→ 重新分配内存(动态调整大小)
realloc函数使用的注意事项:
- ptr 是要调整的内存的起始地址
- size 为调整之后的大小,单位为字节
- 调整成功:返回**调整后内存的起始地址,**原有空间数据会自动复制,且会自动释放原来的内存
- 调整失败:返回
NULL - 如果要调整的目标空间后面有足够多的没有被使用的空间,我们就会在这个空间的基础上直接扩展空间,原地扩容
- 如果要调整的目标空间后面的空间未被使用的空间不足以开辟出我们想要的调整的内存大小,那么realloc会重新在堆空间上另找⼀个合适大小的连续空间来使用,并且返回这个新的内存空间的起始地址,异地扩容
- 千万不要直接用原指针接收返回值,防止扩容失败导致内存泄漏,不仅没扩展好,原本的指针还没了(因为被置为了空指针),但它内存中的存的东西还在,可我们没有指针能指向它了,导致这块空间永远占着内存,无法被使用,也无法被释放,这就被称为内存泄露
- 如果
ptr填NULL,realloc 等价于malloc
下面我们来看看realloc的正确用法和错误用法
(1)错误用法:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
printf("malloc 开辟成功!地址:%p\n", ptr);
// 错误写法:直接赋值给原指针
ptr = (int*)realloc(ptr, 1000000000000); // 超大内存,必失败
// 失败判断
if (ptr == NULL)
{
printf("realloc 扩容失败!返回 NULL\n");
printf("原 malloc 的地址已经丢失!!!发生内存泄漏!\n");
}
free(ptr);
ptr = NULL;
return 0;
}
如果我们直接用源地址去接受我们realloc函数开辟的新地址,那么就可能会出现错误,如果realloc开辟失败了,那么不仅没有新开辟出空间,还把我们的ptr被置为了空指针,让原先的内存找不到了,但里面的内容依然存在,可我们没有指针能指向它了,导致这块空间永远占着内存,无法被使用,也无法被释放,导致内存泄露,上面代码中,直接在堆区要重新一次性开辟1000000000000个字节的空间,太大了,肯定会失败,这就是个错误代码的典例
运行截图:

(2)正确用法:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
// 正确写法
int* temp = realloc(ptr, 200);
if (temp != NULL)
{
ptr = temp;
printf("realloc 开辟成功!地址:%p\n", ptr);
}
free(ptr);
ptr = NULL;
return 0;
}
首先malloc在内存的堆区连续开辟了100个字节的空间,并返回了空间的起始地址给指针ptr,接着我们在使用realloc函数去扩展我们的空间时,用一个新的指针变量temp去接受,这要及时没有开辟成功,至少我们原本的内存空间不会丢失,可以找到,然后判断是否开辟成功,成功的话,realloc就会自动释放我们ptr原本指向的空间,ptr变为野指针,此时我们再把新地址赋给我们的ptr,就没有任何问题,这就是正确的用法
运行截图:

三、常见的动态内存的错误
3.1对NULL指针的解引用操作
错误案例:
cpp
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
我们在用动态内存函数开辟空间时,一定要进行判断,如果不进行判断,又没开辟成功,像上述代码这样,p就为空指针,就会直接对空指针进行解引用,导致程序运行崩溃
解决方法:
cpp
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
// 一定要判断是否开辟成功
if (p != NULL)
{
*p = 20;
}
free(p);
}
每次开辟完空间后都判断一下是否开辟成功,防止使用空指针
3.2对动态开辟空间的越界访问
错误示例:
cpp
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE); //正常退出
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
malloc如果开辟失败,p就等于NULL,我们用exit(EXIT_FAILURE)退出程序,表示正常退出,但若开辟成功,只开辟了十个int类型大小的空间,我们却访问了第十一个int的空间,导致越界访问,访问了不属于自己的空间,程序崩溃出错
运行截图:

扩展:exit 库函数
exit () 是 C 语言里用来直接结束整个程序的函数,不管你在哪个函数里调用它,整个程序立刻终止、退出、关闭
cpp
void exit(int status);
参数int status表示程序退出状态码,0表示正常退出,非0表示异常退出
EXIT_SUCCESS == 0:表示正常退出,exit(EXIT_SUCCESS);等价main里return 0;EXIT_FAILURE == 非0(一般是1):表示异常退出、出错退出- 使用exit函数需要头文件 #include <stdlib.h>
使用方法:
cpp
#include <stdio.h>
#include <stdlib.h>
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL != p)
{
free(p);
p = NULL;
exit(EXIT_SUCCESS); //正常退出 等价于exit(0);
}
else
exit(EXIT_FAILURE); //异常退出 等价于exit(1);
printf("hehe\n");
}
int main()
{
test();
}
如果malloc成功开辟出空间并返回地址给p,则p不为空指针,我们正常退出,否则异常退出,但不管哪种退出,都会直接立刻终止程序,下面的代码不会执行,即printf("hehe\n")不会执行
运行截图:

如图所示,什么都没有打印
3.3对非动态开辟内存使用free释放
cpp
void test()
{
int a = 10; // a在栈区,非堆
int* p = &a;
free(p); // 非法操作
p = NULL;
}
free()只能释放 malloc/calloc/realloc 在堆上开辟的内存,int a是在栈区上创建的,free栈区的空间这个行为是C语言未定义的,程序会崩溃报错
3.4使用free释放动态开辟内存的一部分
错误示例:
cpp
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
p = NULL;
}
当p++后,p已经不指向我们动态开辟内存的起始地址了,但free释放的地址必须传动态内存开辟空间的起始地址,free其它位置的地址属于未定义行为,程序会崩溃报错
解决方法:
cpp
void test()
{
int *p = (int *)malloc(100);
if(p == NULL)
return;
// 要移动就用临时指针
int *tmp = p;
tmp++;
free(p); // p 始终指向起始位置
p = NULL;
}
要移动起始指针指向的位置,我们就需创建个新的指针来移动,不能改动我们原来的指针指向的位置,原来的指针(p)坚决不动,永远保存 malloc 返回的起始地址,专门留给 free 使用
3.5对同一块动态内存多次释放
错误示例:
cpp
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
对于同一块堆区上开辟的空间,我们多次释放属于未定义行为,程序会崩溃报错
解决方法:
cpp
void test()
{
int* p = (int*)malloc(100);
free(p); // 释放
p = NULL; // 立刻把指针置空
free(p); // 安全!free(NULL) 是合法的,什么都不做
}
在每次free完后,我们紧接着都跟着将指针置为空指针,防止p成为野指针,此时free(p)就不会报错,而是什么都不错,这个代码豪无影响
3.6动态开辟内存忘记释放(内存泄漏)
错误示例:
cpp
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
p = NULL;
}
int main()
{
test();
while (1);
}
程序使用malloc开辟一块100个字节的空间后,使用完这块空间后并未对其释放,那么在我们整个程序的进程当中,这块空间就会一直被占用,我们p被置为了空指针,我们就无法对这块空间进行任何处理操作,这块空间没法使用也没法释放,造成内存泄露的问题
解决方法:每次使用完空间后都要记得释放空间
四、动态内存经典笔试题分析
4.1题目一
请问Test函数会有什么样的运行结果?
cpp
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
GetMemory(str)是传值调用,并没有传str这个指针变量的地址,所以p其实是对指针变量str的一份临时拷贝,地址和str不一样,我们使用malloc开辟了100个字节的空间的起始地址存入p中,却没有存入str中,str依然为空指针,所以当我们使用strcpy(str, "hello world")时,是对空指针 NULL 进行写操作,属于非法内存访问,程序直接崩溃,根本不会去执行后面的代码printf(str)
运行截图:

4.2题目二
请问Test函数会有什么样的运行结果?
cpp
#include <stdio.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
}
我们在GetMemory函数中创建的p字符数组是局部变量,在出函数后就会被销毁,此时我们return p的地址,相当于传了个野指针给str,因为p指向的空间在出函数后被自动释放,紧接着我们调用printf函数时,printf创建也需要压栈,使用栈空间,就把p指向的空间给覆盖掉了,打印出来的就是printf自己压栈后的乱码
运行截图:

4.3题目三
请问Test函数会有什么样的运行结果?
cpp
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
}
此代码我们将str这个一级指针的地址传给了函数GetMemory属于传址调用,用二级指针char** p去接收,将malloc申请的内存的起始地址传给了*p,也就是传给了str,str 现在指向有效的堆内存,不是野指针,可安全读写,所以strcpy(str, "hello")成功,会打印出hello
运行截图:

4.4题目四
请问Test函数会有什么样的运行结果?
cpp
#include <stdio.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
}
当我们将str指向的空间释放后,str虽然不是空指针,本身的值不变,但指向的空间不再属于它了,变为了野指针,使用strcpy(str, "world")属于非法访问野指针,printf会打印出一段乱码,如果打印出来的是world,只是它运气好,str所指向的空间的内容还未被覆盖,刚好能打印出world
五、柔性数组
5.1柔性数组的定义
在一个结构体中,最后一个成员是长度为 0 或者 未知 的数组,被称之为柔性数组
cpp
struct st_type
{
int i;
int a[0]; // 柔性数组,C99写法
};
如果编译器报错,就写成下面这种写法:
cpp
struct st_type
{
int i;
int a[]; // 柔性数组(标准写法)
};
5.2柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员
2.sizeof 计算结构体大小时,不包含柔性数组
3.使用 malloc 给结构体动态分配内存,分配大小 > 结构体大小 ,因为总大小 = sizeof(结构体) + 柔性数组需要的额外空间
cpp
#include <stdio.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
上述这串代码,sizeof计算结构体大小时,不包含柔性数组int a0,只计算int i的大小,为4
运行截图:

5.3柔性数组的使用
cpp
#include <stdio.h>
#include <stdlib.h>
// 先定义结构体 + 柔性数组
typedef struct type_a
{
int i; // 前面必须有成员
int a[]; // 柔性数组(最后一个)
} type_a;
int main()
{
int i = 0;
// 核心:一次性分配 结构体大小 + 100个int的柔性数组空间
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
p->i = 100; // 给结构体成员赋值
// 给柔性数组赋值
for (i = 0; i < 100; i++)
{
p->a[i] = i; // 柔性数组正常使用
}
// 打印看看
printf("%d\n", p->i);
printf("%d\n", p->a[50]);
free(p); // 一次释放全部!
p = NULL;
return 0;
}
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int))我们一次性创建了4个字节的空间 + 100个int的空间 给我们的结构体和柔性数组,并用结构体指针p来接收这个地址,然后我们用p->i = 100 和 for循环 给int i 和 int a\[\]赋值,并打印,最后一次性就能释放我们p的空间,并将p置为空指针
运行截图:

5.4柔性数组的优势
上述柔性数组的代码,用以下不使用柔性数组的代码也可以达到一模一样的效果:
例如:
cpp
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int* p_a; // 指针,不是柔性数组
} type_a;
int main()
{
int i = 0;
// 第一次 malloc:结构体
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;
// 第二次 malloc:数组
p->p_a = (int*)malloc(p->i * sizeof(int));
// 使用
for (i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
// 释放两次,顺序不能错
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
这个代码中,我们在结构体中将柔性数组替换成int* p_a,只不过这个是先给int i开辟了空间,然后再给int* p_a开辟空间,而不是一次性使用malloc开辟好,所以我们要释放两次空间,首先释放p->p_a,然后再释放p,顺序不能乱,不然如果先释放p,我们就找不到我们的p->p_a,导致内存泄露问题
此代码对比与5.3中的柔性数组的代码,都可以实现一样的功能,但柔性数组的代码有两个好处:
好处一:方便内存释放,柔性数组中只用释放一次内存,而使用指针模拟的需要释放两次内存,一次结构体的,还有一次结构体成员的,如果这个代码是给其它人用的,其他人在使用的时候,不能保证他也能想到结构体成员也需要释放,可能就会导致程序出小bug,而且很难找到这个bug,所以使用柔性数组更好
好处二:使用柔性数组有利于访问速度,因为创建的是连续的内存,有益于减少内存碎片,而使用指针模拟分两次开辟空间,创建的是两块分开的空间,就可能会浪费掉一些空间,导致内存碎片变多,效率变慢
六、总结C/C++中程序内存区域划分

从高地址→低地址依次:内核空间 → 栈 → 内存映射段 → 堆 → 数据段 → 代码段,用户程序无法操作内核空间。
1. 代码段(红色箭头)
只读区域,存放程序二进制指令、字符串字面常量
char *pChar3 = "abcd";:"abcd"常量字符串存在代码段,指针pChar3在栈;- 特性:只读,运行期间不可修改,多个进程共享同一份常量。
2. 数据段(全局 / 静态区,紫色箭头)
存放全局变量、static 静态变量,程序启动分配、进程结束释放
int globalVar=1;(全局变量)、static int staticGlobalVar=1;(静态全局)static int staticVar=1;(函数内静态局部,只初始化 1 次,生命周期同程序)- 细分:已初始化数据区、未初始化 BSS 区(默认 0 初始化)
3. 堆(黄色箭头,向上增长,低→高地址)
动态内存区,程序员手动
malloc/calloc/realloc/free管理
int* ptr1=malloc()/calloc()/realloc()开辟的内存全在堆;- 特点:空间大、生命周期自主控制,忘记 free 会内存泄漏;地址向上增长。
4. 栈(绿色箭头,向下增长,高→低地址)
函数调用自动分配释放,存放局部普通变量、数组、函数形参
int localVar=1;、int num1[10]={1,2,3,4};、char char2[]="abcd";、各类指针变量;- 函数
Test()执行完毕,栈空间自动回收,无需手动释放;地址向下增长,栈空间有限,过大数组会栈溢出。
5. 内存映射段
存放动态库、文件映射、匿名映射,介于堆和栈中间。
6. 内核空间
操作系统占用,用户代码无读写权限
感谢大家的观看,新人求互三,关注我必回关!下章见!
