相关文章链接 :
1.【嵌入式开发】C语言 指针数组 多维数组
2.【嵌入式开发】C语言 命令行参数 函数指针 gdb调试
3.【嵌入式开发】C语言 结构体相关 的 函数 指针 数组
4.【嵌入式开发】gcc 学习笔记(一) - 编译C程序 及 编译过程
5.【C语言】 C 语言 关键字分析 ( 属性关键字 | 常量关键字 | 结构体关键字 | 联合体关键字 | 枚举关键字 | 命名关键字 | 杂项关键字)
6.【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 )
7.【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)
动态内存分配 :
动态内存分配方法 :
1.申请内存 : 使用 malloc 或 calloc 或 realloc 申请内存;
2.归还内存 : 使用 free 归还 申请的内存 ;
3.内存来源 : 系统专门预留一块内存, 用来响应程序的动态内存分配请求 ;
4.内存分配相关函数 :
#include
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
malloc 函数简介 :
void *malloc(size_t size);
free 函数简介 :
void free(void *ptr);
calloc 函数简介 :
void *calloc(size_t nmemb, size_t size);
realloc 函数简介 :
void *realloc(void *ptr, size_t size);
代码示例 :
#include
#include
int main()
{
//1. 使用 malloc 分配 20 个字节的内存, 这些内存中的数据保持原样
int* p1 = (int*)malloc(sizeof(int) * 5);
//2. 使用 calloc 分配 5 个 int 类型元素的 内存, 初始化 5 个元素的值为 0
int* p2 = (int*)calloc(5, sizeof(int));
//3. 以 int 类型 打印 p1 和 p2 指向的内存中的数据值
int i = 0;
for(i = 0; i < 5; i ++)
{
printf("p1[%d] = %d, p2[%d] = %d\n", i, p1[i], i, p2[i]);
}
//4. 重新分配 p1 指向的内存, 在多分配 10 个数据;
p1 = (int*) realloc(p1, 15);
for(i = 0; i < 15; i ++)
{
printf("p1[%d] = %d\n", i, p1[i]);
}
return 0;
}
栈 简介 :
栈对函数的作用 :
函数 栈内存 的几个相关概念 :
函数入栈流程 :
函数出栈流程 :
代码示例 :
#include
void fun1(int i)
{
}
int fun2(int i)
{
fun1();
return i;
}
/*
分析栈内存 入栈 出栈 esp ebp 指针操作;
程序开始执行, 目前 栈 中是空的, 栈底没有数据 ;
注意点 :
1. esp 指针 : esp 指针变量所在的地址不重要, 讲解的全程没有涉及到过, 重要的是 esp 指向的值, 这个值随着 函数 入栈 出栈 一直的变 ;
( 1 ) 入栈 : esp 上次指向的地址 放入 返回地址 中, 然后 esp 指向新的栈顶 ;
( 2 ) 出栈 : 获取 返回地址 中的地址, esp 指向 该获取的地址 (获取方式 通过 ebp 指针获取);
2. ebp 指针 : ebp 指针变量所在的地址不重要, 讲解全过程中没有涉及到, 重要的是 ebp 指向的值, 这个是随着 函数 入栈 出栈 一直在变 ;
( 1 ) 入栈 : 将 ebp 指针指向的地址 入栈, 并且 ebp 指向新的栈内存地址 ;
( 2 ) 出栈 : ebp 回退一个指针即可获取 返回地址 (这个返回地址供 esp 指针使用), 然后 ebp 获取内存中的地址, 然后ebp 直接指向这个地址, 即回退到上一个函数的ebp地址;
3. 返回地址作用 : 指引 esp 指针回退到上一个函数的栈顶 ;
4. ebp 地址作用 : 指引 ebp 指针会退到上一个函数的 ebp 地址, 获取 esp 的返回地址 ;
5. 初始地址 : 最初的 返回地址 和 old ebp 地址值 是 栈底地址 ;
1. main 函数执行
( 1 ) 参数入栈 : 将 参数 放入栈中, 此时 main 函数 参数 在栈底 ;
( 2 ) 返回地址入栈 : 然后将 返回地址 放入栈中, 返回地址是 栈底地址 ;
( 3 ) ebp 指针入栈 : 将 old ebp 指针入栈, ebp 指针指向 old ebp 存放的地址 address1 , 这个 address1 是 栈底地址;
( 3 ) 数据入栈 : ( 局部变量, 寄存器的值 等 ) ;
( 4 ) esp 指向栈顶 : esp 指针指向 栈顶 (即数据后面的内存首地址), 此时栈顶数据 address2;
( 5 ) 数据总结 : main 的栈中 栈底 到 栈顶 的数据 : main 参数 -> 返回地址 -> old ebp -> 数据
( 6 ) 执行函数体 : 开始执行 main 函数的函数体, 执行 fun1 函数, 下面是 栈 中内存变化 :
2. 调用 fun1 函数, 继续将 fun1 函数内容入栈 :
( 1 ) 参数入栈 : 将 fun1 参数 入栈
( 2 ) 返回地址入栈 : 存放一个返回地址, 此时存放的是栈顶的值 address2 地址, 返回的时候通过 ebp 指针回退一个读取 ;
( 3 ) ebp 指针入栈 : old ebp (上次 ebp 指针指向的地址) 指针指向的地址值入栈, 该指针指向 address1 地址, 即 ebp 指针上一次指向的位置,
该栈内存中存放 ebp 指针上次指向的地址 address1, 这段存放 address1 的内存首地址为 address3,
ebp 指针指向 address3 , 即 ebp 指针变量存储 address3 的地址值, 栈内存中的 address3 存放 address1 地址 ;
( 3 ) 数据入栈 : 存放数据 (局部变量)
( 4 ) esp 指向栈顶 : esp 指向 栈顶
( 5 ) 执行函数体 : 开始执行 fun1 函数体内容, 执行结束后需要出栈 返回 ;
3. fun1 函数执行完毕, 开始 退栈 返回 操作 :
( 1 ) 获取返回地址 : 返回地址存放在 ebp 的上一个指针地址, ebp 指向 返回地址的尾地址,
ebp 回退一个指针位置即可获取返回地址 , 此时的返回地址是 address2 上面已经描述过了 ;
( 2 ) esp 指针指向 : esp 指向 address2, 即将 esp 指针变量的值 设置为 address2 即可 ;
( 3 ) ebp 指针指向 :
获取上一个 ebp 指向的地址 : 当前 ebp 指向的内存中存储了上一个 ebp 指向的内存地址, 获取这个地址;
ebp 指向这个刚获取的地址 ;
( 4 ) 释放栈空间 : 将 esp 指针指向的当前地址 和 之后的地址 都释放掉 ;
( 5 ) 执行 main 函数体 : 继续执行 main 函数 函数体 , 然后执行 fun2 函数;
4. 执行 fun2 函数
( 1 ) 参数入栈 : fun2 函数参数入栈;
( 2 ) 返回地址 入栈 : esp 指向的地址 存放到 返回地址中 ;
( 3 ) ebp 地址入栈 : 将 ebp 指向的地址存放到栈内存中, ebp 指向 该段内存的首地址 (即返回地址的尾地址);
( 4 ) 数据入栈 : 将数据 入栈
( 5 ) esp 指向栈顶 : esp 指向 数据 的末尾地址 ;
( 6 ) 执行函数体 : 执行 fun2 函数体时, 发现 fun2 中居然调用了 fun1, 此时又要开始将 fun1 函数入栈 ;
5. fun1 函数入栈
( 1 ) 参数入栈 : 将 fun1 参数入栈
( 2 ) 返回地址入栈 : esp 指向的 返回地址 存入栈内存 ;
( 3 ) ebp 地址入栈 : 将 old ebp 地址 入栈, 并且 ebp 指针指向 该段 栈内存首地址 (即 返回地址 的尾地址);
( 4 ) 数据入栈 : 局部变量, 寄存器值 入栈 ;
( 5 ) esp 指针指向 : esp 指针指向栈顶 ;
( 6 ) 执行函数体 : 继续执行函数体, 执行完 fun1 函数之后, 函数执行完毕, 开始出栈操作 ;
6. fun1 函数 出栈
( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ;
( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值;
( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ;
( 4 ) 执行函数体 : 执行完 fun1 出栈后, 继续执行 fun2 中的函数体, 发现 fun2 函数体也执行完了, 开始 fun2 出栈 ;
7. fun2 函数 出栈
( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ;
( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值;
( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ;
( 4 ) 执行函数体 : 执行完 fun2 出栈后, 继续执行 main 中的函数体, 如果 main 函数执行完毕, esp 和 ebp 都指向 栈底 ;
*/
int main()
{
fun1(1);
fun2(1);
return 0;
}
分析的代码内容 :
#include
void fun1(int i)
{
}
int fun2(int i)
{
fun1();
return i;
}
int main()
{
fun1(1);
fun2(1);
return 0;
}
代码 栈内存 行为操作 图示分析 :
int main()
{
fun1(1);
fun2(1);
return 0;
}
void fun1(int i)
{
}
int main()
{
fun1(1);
fun2(1);
return 0;
}
int fun2(int i)
{
fun1();
return i;
}
void fun1(int i)
{
}
int fun2(int i)
{
fun1();
return i;
}
堆 相关 概念 :
堆 管理 方法 :
空闲链表法方案 :
2.程序申请堆内存 : int* p = (int*)malloc(sizeof(int)) ; 申请一个 4 字节的堆空间, 从空闲链表中查找能满足要求的空间, 发现一个 5 字节的空间, 满足要求, 这里直接将 5 字节的空间, 分配给了程序 , 不一定要分配正好的内存给程序, 可能分配的内存比申请的要大一些 ;
3.程序释放堆内存 : 将 p 指向的内存插入到空闲链表中 ;
静态存储区 相关概念 :
总结 :
1.栈内存 : 主要存储函数调用相关信息 ;
2.堆内存 : 用于程序申请动态内存, 归还动态内存使用 ;
3.静态存储区 : 用于保存程序中的 全局变量 和 静态局部变量 ;
可执行程序文件的内容 : 三个段 是程序文件的信息, 编译后确定 ;
分析简单程序的 程序文件布局 :
#include
//1. 全局的 int 类型变量, 并且进行了初始化, 存放在 数据段
int global_int = 666;
//2. 全局 char 类型变量, 没有进行初始化, 存放在 bss段
char global_char;
//3. fun1 和 fun2 函数存放在文本段
void fun1(int i)
{
}
int fun2(int i)
{
fun1();
return i;
}
int main()
{
//4. 静态局部变量, 并且已经初始化过, 存放在 数据段;
static int static_part_int = 888;
//5. 静态局部变量, 没有进行初始化, 存放在 bss段;
static char static_part_char;
//6. 局部变量存放到文本段
int part_int = 999;
char part_char;
//7. 函数语句等内容存放在文本段
fun1(1);
fun2(1);
return 0;
}
程序运行后的内存布局 : 从高地址 到 低地址 介绍, 顺序为 栈 -> 堆 -> bss段 -> data 段 -> text段 ;
程序内存总结 :
函数调用过程 :
野指针相关概念 :
野指针来源 :
#include
#include
//1. 定义一个结构体, 其中包含 字符串 和 int 类型元素
struct Student
{
char* name;
int age;
};
int main()
{
//2. 声明一个 Student 结构体变量但是没有进行初始化,
// 结构体中的两个元素都是随机值
// 需要 malloc 初始化该局部变量
struct Student stu;
//3. 向 stu.name 指针指向的地址 写入 "Bill Gates" 字符串,
// 要出事, stu.name 没有进行初始化, 其地址是随机值,
// 向一个随机地址中写入数据, 会出现任意情况, 严重会直接让系统故障
//4. 此时 stu.name 就是一个野指针
strcpy(stu.name, "Bill Gates");
stu.age = 63;
return 0;
}
#include
#include
#include
int main()
{
//1. 创建一个字符串, 并为其分配空间
char* str = (char *)malloc(3);
//2. 给字符串赋值, 申请了 3 个字节, 但是放入了 11 个字符
// 有内存越界的风险
strcpy(str, "HanShuliang");
//3. 打印字符串
printf("%s\n", str);
//4. 释放字符串空间
free(str);
//5. 再次打印, 为空
printf("%s\n", str);
}
#include
//从函数中返回的局部变量要注意一定要是值传递, 不能有地址传递
//局部变量在函数执行完就释放掉了
char * fun()
{
//注意该变量是局部变量,
//函数执行完毕后该变量所在的栈空间就会被销毁
char* str = "Hanshuliang";
return str;
}
int main()
{
//从 fun() 函数中返回的 str 的值是栈空间的值,
//该值在函数返回后就释放掉了,
//当前这个值是被已经销毁了
char * str = fun();
//打印出来的值可能是正确的
printf("%s\n", str);
}
非法内存操作 : 主要是**结构体的指针成员出现的问题, 如结 ① 构体指针未进行初始化(分配动态内存, 或者分配一个变量地址), 或者***② 进行了初始化, 但是超出范围使用***;
#include
//在结构体中定义指针成员, 当结构体为局部变量时, 该指针成员 int* ages 需要手动初始化操作
struct Students
{
int* ages;
};
int main()
{
//1. 在函数中声明一个结构体局部变量, 结构体成员不会自动初始化, 此时其中是随机值
struct Students stu1;
//2. 遍历结构体的指针成员, 并为其赋值, 但是该指针未进行初始化, 对一个随机空间进行操作会造成未知错误
int i = 0;
for(i = 0; i < 10; i ++)
{
stu1.ages[i] = 0;
}
return 0;
}
#include
#include
//在结构体中定义指针成员, 当结构体为局部变量时, 该指针成员 int* ages 需要手动初始化操作
struct Students
{
int* ages;
};
int main()
{
//1. 在函数中声明一个结构体局部变量, 结构体成员不会自动初始化, 此时其中是随机值
struct Students stu1;
//2. 为结构体变量中的 ages 指针分配内存空间, 并进行初始化;
stu1.ages = (int *)calloc(2, sizeof(int));
//3. 遍历结构体的指针成员, 并为其赋值, 此处超出了其 2 * 4 字节的范围, 8 ~ 11 字节可能分配给了其他应用
int i = 0;
for(i = 0; i < 3; i ++)
{
stu1.ages[i] = 0;
}
free(stu1.ages);
return 0;
}
内存分配成功, 没有进行初始化 : 内存中的是随机值, 如果对这个随机值进行操作, 也会产生未知后果;
#include
#include
//内存分配成功, 需要先进行初始化, 在使用这块内存
int main()
{
//1. 定义一个字符串, 为其分配一个 20 字节空间
char* str = (char*)malloc(20);
//2. 打印字符串, 这里可能会出现错误, 因为内存没有初始化
// 此时其中的数据都是随机值, 不确定在哪个地方有 '\0' 即字符串结尾
// 打印一个位置长度的 str, 显然不符合我们的需求
printf(str);
//3. 释放内存
free(str);
return 0;
}
内存越界分析 :
#include
//数组退化 : 方法中的数组参数会退化为指针, 即这个方法可以传入任意 int* 类型的数据
//不能确定数组大小 : 只有一个 int* 指针变量, 无法确定这个数组的大小
//可能出错 : 这里按照10个字节处理数组, 如果传入一个 小于 10字节的数组, 可能出现错误
void fun(int array[10])
{
int i = 0;
for(i = 0; i < 10; i ++)
{
array[i] = i;
printf("%d\n", array[i]);
}
}
int main()
{
//1. 定义一个大小为 5 的int 类型数组, 稍后将该数组传入fun方法中
int array[5];
//2. 将大小为5的int类型数组传入fun函数, 此时fun函数按照int[10]类型超出范围为数组赋值
// 如果为一个未知地址赋值会出现无法估计的后果
fun(array);
return 0;
}
内存泄露 :
#include
/*
内存问题 : 该函数有一个入口, 两个出口
正常出口 : 处理的比较完善, 内存会释放;
异常出口 : 临时机制, 出现某种状态, 没有处理完善, 出现了内存泄露
*/
void fun(unsigned int size)
{
//申请一块内存空间
int* p = (int*)malloc(size * sizeof(int));
int i = 0;
//如果size小于5, 就不处理, 直接返回
//注意 : 在这个位置, 善后没有处理好, 没有释放内存
// 如果size小于5, 临时退出函数, 而 p 指针指向的内存没有释放
// p 指针是一个局部变量, 函数执行完之后, 该局部变量就消失了, 之后就无法释放该内存了
if(size < 5)
{
return;
}
//内存大于等于5以后才处理
for(int i = 0; i < size; i ++)
{
p[i] = i;
printf("%d\n", p[i]);
}
//释放内存
free(p);
}
int main()
{
fun(4);
return 0;
}
#include
/*
内存问题 : 该函数有一个入口, 两个出口
正常出口 : 处理的比较完善, 内存会释放;
异常出口 : 临时机制, 出现某种状态, 没有处理完善, 出现了内存泄露
*/
void fun(unsigned int size)
{
//申请一块内存空间
int* p = (int*)malloc(size * sizeof(int));
int i = 0;
//将错误示例中的此处的出口取消即可解决内存泄露的问题
if(size >= 5)
{
//内存大于等于5以后才处理
for(int i = 0; i < size; i ++)
{
p[i] = i;
printf("%d\n", p[i]);
}
}
//释放内存
free(p);
}
int main()
{
fun(4);
return 0;
}
指针被多次释放 :
#include
#include
/*
内存问题 : 多次释放指针
如果规避这种问题 : 动态内存 谁申请 谁释放
*/
void fun(int* p, int size)
{
int i = 0;
for(i = 0; i < size; i ++)
{
p[i] = i;
printf("%d\n", p[i]);
}
//释放内存
// 注意这里 p 不是在本函数中申请的内存
// 如果在其它位置再次释放内存, 就可能会出错
free(p);
}
int main()
{
//申请内存
int* p = (int*)malloc(3 * sizeof(int));
//使用内存, 并在函数中释放内存
fun(p, 3);
//如果在此处释放一个已经释放的内存, 就会报错
free(p);
return 0;
}
使用已经释放的指针 :
#include
#include
/*
内存问题 : 使用已经释放的指针
如果规避这种问题 : 动态内存 谁申请 谁释放
*/
void fun(int* p, int size)
{
int i = 0;
for(i = 0; i < size; i ++)
{
p[i] = i;
printf("%d\n", p[i]);
}
//释放内存
// 注意这里 p 不是在本函数中申请的内存
// 如果在其它位置再次释放内存, 就可能会出错
free(p);
}
int main()
{
//申请内存
int* p = (int*)malloc(3 * sizeof(int));
int i = 0;
//使用内存, 并在函数中释放内存
fun(p, 3);
//使用已经释放的指针
//产生的后果无法估计
for(i = 0; i <= 2; i ++)
{
p[i] = i;
}
return 0;
}
申请空间后先判断 : 使用 malloc 申请内存之后, 先检查返回值是否为 NULL, 防止使用 NULL 指针, 防止对 0 地址进行操作, 这样会破坏操作系统的内存区; 操作系统检测到程序使用 0 地址, 就会杀死本程序;
#include
#include
int main()
{
//申请内存
int* p = (int*)malloc(3 * sizeof(int));
//申请完内存后, 先判断是否申请成功, 在使用这段内存
if(p != NULL){
//执行相关操作
}
//释放内存
free(p);
return 0;
}
避免数组越界 : 数组创建后, 一定要记住数组的长度, 防止数组越界, 推荐使用柔性数组;
动态内存申请规范 : 动态内存的***申请操作*** 和 释放操作 一一对应匹配, 防止内存泄露和多次释放; 谁申请 谁 释放, 在哪个方法中申请, 就在哪个方法中释放 ;
指针释放后立即设置NULL : 在一个指针被 free() 掉以后, 马上将该指针设置为 NULL, 防止重复使用该指针;