1.stdint.h标准头文件
stdint.h 是C99标准引入的标准整数类型头文件,C++中也可用 cstdint 。具有以下特点:
-
直接定义固定字节长度的整数类型,解决原生整数类型在不同平台(32/64位、不同编译器)下的字节长度不固定、跨平台兼容性的问题,开发者可精准控制内存占用;可按需选择适合长度的类型,从源头上减少溢出风险; 操作硬件寄存器/外设时,寄存器的位宽固定,8位寄存器用 ``uint8_t``` ,精准匹配。
-
stdint定义的类名命名规律:[u]int[N]_t,其中u代表无符号,N代表位数/比特位,_t代表这是一个类型定义。- ``uint8_t``` :无符号8位整数,8位1字节;
- ``int64_t
:有符号64位整数,64位8字节。 打印输出时,直接使用普通格式符%d %u```会有兼容性问题或格式错误,这是因为底层映射的不确定性:
-
在多数系统中,
uint8_t=unsigned char,int32_t=int,int6_t=longlong,但并不绝对,在某些嵌入式系统中,int32_t可能映射为long而非int。- 例如:对于 ``int64_t
,若直接用%d``` 格式(4字节)打印,会导致内存越界,打印结果错误等问题; - 跨平台时,32位系统
int64_t```是long long(格式符 ``%lld),64位系统可能int64_t```是long(格式符 ``%ld),写死格式符会导致代码在不同平台打印异常。
- 例如:对于 ``int64_t
使用 stdint.h 配套的格式宏-->根据当前平台自动替换为正确的格式符:
PRIu8-->无符号8位整数格式符;PRId32-->有符号32位整数格式符;PRId64-->有符号64位整数格式符.
!使用前需包含头文件**<inttypes.h>**,格式宏本身是字符串,需要双引号包裹。
uint8_t age = 18;
printf("年龄:%"PRIu8"", age);2.C语言从源码到可执行文件经历的四个阶段
四个阶段分别是:预处理、编译、汇编、链接
(1)预处理器预处理阶段 hello.c -->hello.i
该阶段的主要任务是处理带 # 的语句,包括加载头文件、宏替换、条件编译等。
- 将所有
include包含的内容原封不动地拷贝到当前文件中,但会导致重复包含的问题。
(2)编译器编译阶段 hello.i -->hello.s
- 该阶段核心任务是把预处理后的
hello.i文件,通过语法分析、语义分析、优化和代码生成,转换成汇编代码hello.s。 - 汇编代码是比c语言更接近硬件的代码,但此文件仍可用文本编辑器打开查看。
- 语法分析:检查代码是否符合语法规则;
- 语义分析:检查逻辑合理性,如变量未定义、函数参数类型不匹配;
- 优化:删除空语句、简化表达式等,无副作用;
- 代码生成:将C语言代码翻译成对应架构的汇编指令。
(3)汇编器汇编阶段 hello.s -->hello.o
汇编器将汇编语言翻译成机器认识的机器代码,得到 .o 为后缀的二进制文件,该文件也称目标文件。
(4)链接器链接阶段 多个目标文件 -->一个可执行文件
- 链接器的工作:将多个目标文件,包括
hello.o目标文件和printf函数所在的目标文件拼在一起,解决函数/变量的地址引用问题,最终生成一个能直接运行的可执行文件。
汇编得到的hello.o 是一个不完整的目标文件,里面用到了printf 函数,但printf 函数的实现不在hello.o 中,它只存了一个待填充的跳转地址。
链接器会根据代码里的外部引用,去标准库中找到printf 函数所在的目标文件(以及其他依赖的目标文件),将它们合并为一个整体:
- 给每个目标文件里的函数、变量分配最终的内存地址;
- 用对应的内存地址填充跳转地址;
- 把所有目标文件的符号整理成一个全局符号表,确保没有冲突;
- 生成最终的可执行文件,
windows对应.exe后缀,linux对应.out后缀。
3.四个阶段的内存地址分配问题
相对地址:相对于当前目标文件的偏移地址,汇编阶段生成;
绝对地址:程序运行时在内存中的真实地址,链接阶段生成;
存储段:目标文件、可执行文件的内存分区,包括.text代码段、.data数据段、.rodata只读数据段,堆区、栈区。
- 地址确定阶段
- 汇编阶段:确定全局变量/函数的
相对地址+存储段,局部变量的栈偏移; - 链接阶段:确定全局变量/函数的
最终绝对地址; - 运行阶段:确定局部变量的
栈绝对地址。
- 汇编阶段:确定全局变量/函数的
- 存储位置
.data数据段:可读写,存储普通全局变量;.rodata数据段:只读,只存储const全局变量/字符串字面量(“hello”),修改会触发内存错误;.text文本段:代码、函数放在这里;- 栈:运行时(栈帧建立之后)动态分配,函数调用时创建,结束时销毁。
- 为什么函数/全局变量的绝对地址要等到链接阶段?
一个程序可能由多个.c文件编译成多个.o文件,汇编阶段只能处理单个.o文件的相对地址,链接阶段才能合并所有.o的段,统一分配整个程序的绝对地址。 - 局部变量为什么运行阶段才确定地址?
局部变量存在栈上,运行时动态增长/收缩的,编译和汇编阶段无法预知程序运行时的栈指针位置,只能记录栈偏移,运行时根据当前栈帧计算绝对地址。 - const全局和const局部的只读有什么区别?
const全局:内存级只读,.rodata数据段由操作系统标记已读,修改会报错;
const局部:语法级只读,编译器禁止直接修改,但栈内存本身可读写,能通过指针强制修改。
4.变量的存储区域和初始化
| 内存区域 | 存储内容 | 初始化规则 | 生命周期 |
|---|---|---|---|
.data |
已初始化的全局或者静态变量 | 显示赋初始值 | 程序运行全程 |
.bss |
未初始化的全局或者静态变量 | 默认初始化为0 | 程序运行全程 |
| 栈 | 局部变量 | 无默认初始化值(残留值) | 函数调用时创建,结束销毁 |
.bss段:只记录变量大小,不存储初始值。程序启动时操作系统将.bss段的所有变量置0,不占用可执行文件保存在硬盘上的大小;而.data段会占用,存储了初始化值。
5.声明函数的作用
- 对于调用在前、定义在后的函数,解决其编译问题(报错:找不到该函数的定义
implict declaration) - 跨文件调用函数:函数声明加多文件编译
main.c中调用a.c文件中的func函数:- 必须在
main.c中声明func函数; - 多文件编译:
gcc main.c a.c -o test。
- 必须在
6.头文件
头文件的作用
为源文件提供接口信息,让编译器知道有哪些函数/类型/变量可以用,但不用关心具体实现,让接口和实现分离。
头文件中可以写什么
核心原则:只写“声明”和“不占内存的定义”,不要写“会分配内存的定义”。
- 函数声明
- 类型定义:结构体定义、枚举定义、为类型取的别名等
//结构体定义
typedef struct{
int id;
char name[20];
}student;
//枚举定义
enum Color {RED, GREEN, BLUE};- 宏定义和const常量
const全局常量在只读段,不占用源文件内存 - 全局变量声明,需加extern
extern int g_count;7.头文件重复包含问题
假设你的代码结构如下:
- b.h:声明void func_b();
- a.h:#include "b.h"
- hello.c:#include "a.h" && #include "b.h"
此时 void func_b(); 会被导入两次,重复包含可能带来以下后果:
- 预处理之后的
.i文件内容重复,编译效率低下; - 变量/函数重定义问题,C/C++不允许同一个作用域中存在同名的变量或完全相同的函数,意味着同一内存地址被多次分配,这是不被允许的。
头文件重复包含问题如何解决?
pragma once:极简高效
#pragma once- 写在头文件第一行,告诉编译器当前头文件仅处理一次;
- 兼容性:主流编译器(GCC/Clang/MSVC)全支持,工业项目首选
- 头文件保护法
// b.h
#ifndef B_H_
#define B_H_
void fun_c();
#endif用 #ifndef/#define/#endif 包裹头文件内容,通过唯一宏名标记是否已处理。
- 宏命名规范:项目前缀_文件名大写_后缀,注意避免宏名冲突。
8.C内存布局笔记
(1).文本段:.text ,存放机器码,编译链接阶段已经确定大小,只读权限。
objdump -h 可执行文件(2).静态数据区:分为未初始化的.bss 和已初始化的.data 、.rodata
未初始化数据区 .bss :大小在编译时确定,运行时可读可写
- 只在内存中预留位置,但不在文件中保存实体。
- 即在编译器生成可执行文件时,并不将这些变量的大量0值写入,而仅仅记录下他们的名称、大小等元信息。
- 操作系统加载程序时,读取这些元信息,在内存中预留出相应的内存空间,全部置0;
- 为大型未初始化的数据节省磁盘空间,eg:未初始化或初始化为0的大型数组。
#反汇编
objdump -h 可执行文件已初始化数据区.data 、.rodata
- 大小在构建阶段就被完全确定下来,从可执行文件直接复制到内存
- 根据能否在运行时修改分为
.data、.rodata
(3).动态数据区:运行时确定,分为堆和栈
栈:编译器自动化管理区域,用于局部变量存储和函数调用,遵循先进后出原则。
- 函数调用时,对应的栈帧(包含该函数所有局部变量、参数和返回地址的内存块)被压入栈顶;
- 函数返回时,栈帧被整体弹出,所占内存被瞬间回收,使得栈的内存分配和释放速度极快。
- 缺点:大小在编译时已经确定,通常较小;
- 其上的所有数据的生命周期严格限制在函数作用域内。
堆:适用于动态分配的内存区,灵活,开发者可手工申请+管理
(4). 连续内存和分散内存在数据读取速度方面的差异
CPU在读取数据时,会一次性将目标数据及其邻居一同加载到高速缓存中,因此,
- 在遍历连续内存时,下一个元素大概率已在缓存中,称为缓存命中,速度极快,对应的数据结构有
vector; - 而在遍历分散内存时,访问下一节点需要重新去主内存读取,称为缓存缺失,速度较慢,对应的数据结构有
链表。
(5).地址空间布局随机化ASLR
- 操作系统在每次运行程序之前后随机化内存区域的起始地址;
- 程序的内存地址,只在单次运行时固定,防止黑客依赖固定内存地址攻击,提高了程序的安全性。
(6)现代操作系统(空间较足,堆和栈没有相撞的可能)内存布局