变量和函数的生命周期
变量
- 全局变量
- 概念
- 定义在函数外部的变量
- 作用域
- 从定义位置开始,默认到本文件内部
- 其他文件如果想要使用,可以通过声明方式将作用域导出
- 概念
- 局部变量
- 概念
- 定义在函数内部的变量
- 作用域
- 从定义位置开始,到包裹该变量的第一个右大括号结束
- 概念
- static 全局变量
- 定义语法
- 在变量定义之前添加 static 关键字,例如:static int a = 10;
- 作用域
- 被限制在本文件内部,不允许通过声明导出到其他文件
- 定义语法
- static 局部变量
- 定义语法
- 在局部变量定义之前添加 static 关键字
- 作用域
- 从定义位置开始,到包裹该变量的第一个右大括号结束
- 特性
- 静态局部变量只定义一次,在全局位置
-
通常用来做计数器
#define _CRT_SECURE_NO_WARNINGS #include #include int b; void test(void) { static int b = 1; printf("%d\n", b++); } int main(void) { for (size_t i = 0; i < 10; i++) { test(); // 10 } printf("%d\n", b); // 0 system("pause"); return 0; }
- 定义语法
函数
- 全局函数
- 定义语法
- 函数原型 + 函数体
- 定义语法
- static 函数
- 定义语法
- static + 函数原型 + 函数体
- 作用域
- static 函数只能在本文件内部使用,其他文件即使声明也无效 (类似于 static 变量)
- 定义语法
变量和函数的生命周期
内存四区模型
代码段
- .text 段,程序源代码 (二进制形式)
数据段
- 只读数据段 .rodata
- 存放常量
- 初始化数据段 .data
- 存放初始化为非 0 的全局变量和静态变量
- 未初始化数据段 .bss
- 存放初始化为 0、未初始化的全局变量和静态变量
- 程序加载执行前,会将该段整体赋值为 0
栈 (stack)
- 在其上开辟栈帧
- Windows:1-10M
- Linux:8-16M
- 特性
- 存储特性:FILO (first in last out)
- 系统自动管理:自动分配、自动释放
- 比较小
堆 (heap)
- 给用户自定义数据提供空间
- 空间足够大,约 1.3 G+
示意图
内存分区
编译四步骤
- 预处理
- 宏定义展开、头文件展开、条件编译,这里不检查语法
- 编译
- 将预处理后的文件编译成汇编文件,这里检查语法
- 汇编
- 将汇编文件生成目标文件 (二进制文件)
- 链接
- 将目标文件链接为可执行文件
程序的内存分区模型
- 运行之前
- 代码区 (.text 段)
- 是共享的 (其它程序可调用它)、只读的 (防止程序以外修改它的指令)
- .data 和 .bss 合起来称为全局区/静态区
- 全局初始化数据区/静态数据区 (.data 段)
- 包括已初始化的全局变量、已初始化的静态变量和常量
- 全局静态存储区内的常量分为常变量和字符串常量。这里的常变量和局部变量不同。
- 局部变量存放于栈,实际可通过指针或引用进行修改
- 全局常变量存放于静态常量区,不可以间接修改
- 未初始化数据区 (.bss 区)
- 保存全局未初始化的变量和未初始化的静态变量
- .bss 区的数据在程序开始执行前被内核初始化为 0 或 NULL
- 全局初始化数据区/静态数据区 (.data 段)
- 代码区 (.text 段)
- 运行之后
程序在加载到内存之前,代码区和全局区的大小是固定的,程序运行期间不能改变。
程序运行后,操作系统把程序从硬盘加载到内存,除了根据可执行程序的信息分出代码区 (.text)、数据区 (.data) 和未初始化数据区 (.bss) 之外,还额外增加了栈区、堆区。- 代码区 (.text 段)
-
全局区/静态区
- 全局初始化数据区/静态数据区 (.data 段)
- 未初始化数据区 (.bss 区)
- 栈区 stack
- 特性
- 符合先进后出的数据结构
- 编译器自动管理 (分配和释放)
- 容量有限,会溢出
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include int* myFunc() { int a = 10; return &a; } char* getString() { char str[] = "hello world"; return str; } int main(void) { int *p = myFunc(); printf("%d\n", *p); // 返回 10 printf("%d\n", *p); // 返回 8460943 char* q = NULL; q = getString(); printf("%s\n", q); // 返回 烫烫烫烫烫烫橚? system("pause"); return 0; } `` - 局部变量 a 早已被释放,因此没有权限操作这块内存空间 - 第一次 printf,返回 10,是由于编译器自动优化导致的
- 特性
- 堆区 heap
- 特性
- 容量远远大于栈,不是无限
- 手动开辟 (malloc),手动释放 (free);若不手动释放,在程序结束时会隐式回收
- 注意事项
- 示例:可以正确显示,因为没有手动释放
#define _CRT_SECURE_NO_WARNINGS #include #include #include int* myFunc() { int a = 10; return &a; } char* getString() { char str[] = "hello world"; return str; } int* getSpace() { int* p = malloc(sizeof(int) * 5); if (p == NULL) { return; } for (int i = 0; i< 5; i++) { p[i] = i + 10; printf("%d\n", p[i]); } return p; } int main(void) { int *p = myFunc(); printf("%d\n", *p); // 返回 10 printf("%d\n", *p); // 返回 8460943 char* q = NULL; q = getString(); printf("%s\n", q); // 返回 烫烫烫烫烫烫橚? int *k = getSpace(); for (int i = 0; i < 5; i++) { p[i] = i + 10; printf("%d\n", k[i]); } free(k); k = NULL; system("pause"); return 0; }
- 给指针开辟内存时,传入的函数中不要用相同等级指针,需要传入指针的地址,函数中接收这个地址后再进行开辟内存
#define _CRT_SECURE_NO_WARNINGS #include #include #include // 错误示例 void allocateSpace1(char *pp) { char* temp = malloc(100); memset(temp, 0, 100); strcpy(temp, "hello world"); pp = temp; } void test0501() { char* p = NULL; allocateSpace1(p); printf("%s\n", p); } // 正确示例 void allocateSpace2(char **pp) { char* temp = malloc(100); memset(temp, 0, 100); strcpy(temp, "hello world"); *pp = temp; } void test0502() { char* p = NULL; allocateSpace2(&p); printf("%s\n", p); } int main(void) { //test0501(); // 返回 (null) test0502(); // 返回 hello world system("pause"); return 0; }
- 示意图
- 示意图
- 示例:可以正确显示,因为没有手动释放
- 特性
全局区/静态区
- 全局变量区
- extern 变量
- 告诉编译器,下面代码中出现某变量不要报错,是外部链接属性,在其他文件中
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { extern int g_a; // 在另一个文件中初始化 int g_a = 10;在本文件中使用 extern 链接过去 printf("%d\n", g_a); system("pause"); return 0; }
- 在其他文件中定义 g_a
- 链接阶段的错误:一个无法解析的外部命令
- extern 可以提高变量作用域
- C 语言下,全局变量前都隐式加了关键字 extern
- 告诉编译器,下面代码中出现某变量不要报错,是外部链接属性,在其他文件中
- extern 变量
- 静态变量区
- static 变量
- 在运行前分配,程序运行结束
- 在本文件内可以使用静态变量
- static 变量
- 常量区
- const 修饰的变量
- const 修饰全局变量
- 直接修改,失败
- 间接修改,语法通过,运行失败,受到常量区保护
- const 修饰局部变量
- 直接修改,失败;间接修改,成功,存放在栈上
- 伪常量
- 在 C 语言中,const 修饰的局部变量,称为伪常量
- 不可以初始化数组
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include const int a = 0; // 全局变量 // 修改全局变量 void test_0701() { //a = 100; // 直接修改,失败 printf("%d\n", a); int* p = &a; *p = 20; // 间接修改,失败 printf("%d\n", a); } // 修改局部变量 void test_0702() { const int b = 100; //b = 10; // 直接修改,失败 printf("%d\n", b); int* p = &b; *p = 20; // 间接修改,成功 printf("%d\n", b); } int main(void) { //test_0701(); test_0702(); system("pause"); return 0; }
- const 修饰全局变量
- 字符串常量
- 字符串地址是否相同
- tc2.0,同文件字符串常量不同
- VS,同文件和不同文件的多个相同字符串常量看成一个
- Dev c++、Qt,同文件相同,不同文件不同
- 不可以修改字符串常量
ANSI 并没有指定出字符串是否可以修改的标准,根据编译器的不同,可能最终结果也不相同。
-
示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include void test0801() { char* p1 = "hello world"; char* p2 = "hello world"; char* p3 = "hello world"; char* p4 = "hello world"; printf("%p\n", p1); printf("%p\n", p2); printf("%p\n", p3); printf("%p\n", p4); printf("%p\n", &"hello world"); p1[0] = 'c'; printf("%p\n", p1); // 也不报错,但是无法修改 } int main(void) { test0801(); // 返回值都是 00BB8B44 system("pause"); return 0; }
- 字符串地址是否相同
- const 修饰的变量
函数调用模型
- 宏函数
通常将一些频繁、短小的函数,写成宏函数。宏函数需要加小括号修饰,保证运算的完整性。
- 优点
- 以空间换时间。比普通函数在一定程度上,效率高,省去普通函数入栈、出栈时间上的开销
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include #define MYADD1(x, y) x + y // 计算会有歧义 #define MYADD2(x, y) ((x) + (y)) // 正确方法 int main(void) { printf("x + y = %d\n", MYADD1(10, 20)); // x + y = 30 printf("x + y = %d\n", MYADD1(10, 20) * 2); // x + y = 50 printf("x + y = %d\n", MYADD2(10, 20) * 2); // x + y = 60 system("pause"); return 0; }
- 优点
- 栈帧
函数调用所需的信息保存在栈上,通常叫栈帧 (stack frame) 或活动记录 (activate record)。
- 示意图:入栈 (push)、出栈 (pop)
- 经典操作系统中,栈总是向下增长。压栈操作使栈顶地址减小;出栈操作使栈顶地址增大。
- 示意图:入栈 (push)、出栈 (pop)
- 函数调用所需的信息包括
- 函数返回地址
- 函数形参
- 临时变量
- 保存的上下文 (包括函数调用前后需要保持不变的寄存器)
- 调用惯例
主调函数和被调函数必须有一致性约定,才能正确的调用函数,这个约定叫做调用惯例。
- 包含内容
- 出栈方、参数传递顺序和方式
-
栈的维护方式 (名字修饰)
为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。
-
常用的调用惯例
- cdecl
- _func
-
示例:函数 func 的完整写法
// _cdecl 不是标准的关键字,在不同的编译器中有不同的写法 int _cdecl fnc(int a, int b);
- stdcall
- _func@8
- cdecl
- 包含内容
- 示意图:函数变量传递分析
栈的生长方向和内存存放方向
-
栈的生长方向 (向下生长)
#define _CRT_SECURE_NO_WARNINGS #include #include #include void test0201() { int a = 10; // 栈底 -> 高地址 int b = 10; int c = 10; int d = 10; // 栈顶 -> 低地址 printf("%d\n", &a); printf("%d\n", &b); printf("%d\n", &c); printf("%d\n", &d); /* 13629540 // 高地址 13629528 13629516 13629504 // 低地址 */ } int main(void) { test0201(); system("pause"); return 0; }
- 栈底 -> 高地址
- 栈顶 -> 低地址
- 内存存放方向
- 高位字节数据 -> 放在高地址,低位字节数据 -> 放低地址
- 例外 (小端对齐)
开辟和释放 heap 空间
heap 空间是连续的,可以当作数组使用。
malloc 函数
- 函数描述
- 申请 size 大小的空间
- 函数原型
void *malloc(size_t size);
- 参数
- size
- 申请内存的大小 (字节)
- size
- 返回值
- 成功,返回分配空间的起始地址;失败,返回 NULL
- 因为返回值是 void,所以需要强转一下
- 参数
free 函数
- 函数描述
- 释放申请的空间
- 函数原型
void free(void *ptr)
- 参数
- malloc 返回的地址值
- 参数
- 注意事项
- free 后的空间不会立即失效,通常 free 后的地址值为 NULL
- free 的地址必须是 malloc 申请的地址,否则会出错
- 如果 malloc 之后的地址一定会变化,使用临时变量 tmp 保存一份
- 示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { //int str[10] = { 10, 20, 30 }; int* p = (int *)malloc(sizeof(int) * 10); // 申请的长度要与数组长度一致 char* temp = p; // 临时用来存储申请地址 if (p == NULL) // 固定用法 { printf("malloc error:\n"); return -1; } for (size_t i = 0; i<10; i++) { //p[i] = i + 10; // 写数据到 malloc 空间 *p = i + 10; //printf("%d\n", *(p+i)); // 读数据 printf("%d\n", *p); p++; // 如果这里改变指针地址,上面需要使用变量 temp 存储最初的申请地址 } //free(p); // 释放申请的内存 //p = NULL; // 手动置 null free(temp); temp = NULL; system("pause"); return 0; }
calloc 函数
- 函数描述
- 分配内存
在内存动态存储区中分配 nmemb 块长度为 size 字节的连续区域。calloc 自动将分配的内存置0。
-
calloc 会自动初始化数据为 0
- 分配内存
-
函数原型
void *calloc(size_t nmemb, size_t size);
- 参数
- nmemb
- 所需内存单元数量
- size
- 每个内存单元的大小 (单位:字节)
- nmemb
- 返回值
- 成功,分配空间的起始地址;失败,返回 NULL
- 参数
- 与 malloc 对比
- malloc(sizeof(int) * 10)
- 不会自动初始化数据
- calloc(10, sizeof(int))
- calloc 会自动初始化数据为 0
- malloc(sizeof(int) * 10)
realloc 函数
- 函数描述
- 在堆中重新分配内存
重新分配用 malloc 或 calloc 函数在堆中分配内存的大小。
realloc 不会自动清理增加的内存,需要手动清理。
如果指定的地址后面有连续的空间,那么就会在原有地址上增加内存;如果原地址后面没有空间,realloc 会重新分配新的连续内存,把旧的内存的值拷贝到新内存,同时释放旧内存。
- 在堆中重新分配内存
-
函数原型
void *realloc(void *ptr, size_t size);
- 参数
- ptr
- 之前用 malloc 或 calloc 分配的内存地址。如果置为 NULL,那么和 malloc 或 calloc 功能一致。
- size
- 重新分配内存的大小 (单位:字节)
- ptr
- 返回值
- 成功,返回新分配的内存地址;失败,返回 NULL
- 参数
- 重新分配的机制
- 示意图
-
如果重新分配的内存比原来大,不会初始化新空间为 0
- 先在原有空间后去查找后续的空闲空间是否足够大,如果够大,那么直接扩展
- 如果后续空间不足,会重新找一块足够大的内存,将原有空间内的数据拷贝到新空间下,释放掉原有空间,将新空间的首地址返回
- 如果重新分配的内存比原来小,那么会释放后续空间,只有权限操作申请空间
-
示例
#define _CRT_SECURE_NO_WARNINGS #include #include // calloc 自动初始化数据为 0 void calloc_mem() { printf("第一次申请内存 -> 5个\n"); int* p = malloc(sizeof(int) * 5); for (size_t i = 0; i 6个,但是只手动初始化5个值\n"); p = calloc(6, sizeof(int)); for (size_t i = 0; i 5个\n"); int* p = calloc(5, sizeof(int)); for (int i = 0; i 7个,后续空间足够,直接在后面扩展\n"); p = realloc(p, 7 * sizeof(int)); for (int i = 0; i 600个,后续空间不足,重新找一块足够大的空间,拷贝原有数据到新空间\n"); p = realloc(p, 600 * sizeof(int)); for (int i = 0; i 5个,重新分配的空间比原有空间小\n"); p = realloc(p, 5 * sizeof(int)); for (int i = 0; i < 6; i++) { printf("p[%d]: %d, %p\n", i, p[i], &p[i]); // } printf("无权操作未申请的内存 p[5]\n"); //p[5] = 0; // Debug Error! // 释放内存 if (p != NULL) { free(p); p = NULL; printf("释放内存\n"); } } int main(void) { printf("----------------calloc_mem() begin\n"); calloc_mem(); printf("----------------realloc_mem() begin\n"); realloc_mem(); system("pause"); return EXIT_SUCCESS; }
- 示意图
示例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
void test01()
{
int* p = calloc(10, sizeof(int));
if (p == NULL)
{
return;
}
for (int i = 0; i < 10; i++)
{
p[i] = 10 * i;
printf("%d ", p[i]); // 0 10 20 30 40 50 60 70 80 90
}
putchar('\n');
free(p);
}
void test02()
{
int* p1 = malloc(10 * sizeof(int));
if (p1 == NULL)
{
return;
}
for (int i = 0; i < 10; i++)
{
p1[i] = i * 10;
printf("%d ", p1[i]); // 0 10 20 30 40 50 60 70 80 90
}
putchar('\n');
int *p2 = realloc(p1, 15 * sizeof(int));
if (p2 == NULL)
{
return;
}
printf("p1: %d, p2: %d\n", p1, p2); // p1: 12699888, p2: 12675608
for (int i = 0; i < 15; i++)
{
printf("%d ", p2[i]); // 0 10 20 30 40 50 60 70 80 90 -842150451 -842150451 -842150451 -842150451 -842150451
}
putchar('\n');
//free(p1); // 旧的内存已经被释放,重复释放报错
free(p2);
}
int main()
{
test01();
test02();
return 0;
}
二级指针对应的 heap 空间
- 释放空间时,先释放内层空间,再释放外层空间
-
示意图
-
示例
#define _CRT_SECURE_NO_WARNINGS #include #include #include int main(void) { // 申请外层指针 int** p = (int **)malloc(sizeof(int *) * 3); if (p == NULL) { printf("malloc error:"); return -1; } // 申请内层指针 for (size_t i = 0; i < 3; i++) { p[i] = (int *)malloc(sizeof(int) * 5); if (p[i] == NULL) { printf("malloc error p+%d", i); return -2; } } // 写和读 for (size_t i = 0; i<3; i++) { for (size_t j = 0; j < 5; j++) { p[i][j] = i * 10 + j; //printf("%d\n", *(*(p + i) + j)); printf("%d\n", p[i][j]); } } // 释放内层指针 for (size_t i = 0; i < 3; i++) { free(p[i]); } // 释放外层指针 free(p); p = NULL; system("pause"); return 0; }