文件 #
文本文件 #
(ASCII文件)
二进制文件 #
(二进制文件)
文件操作 #
| 操作 | 函数 |
|---|---|
| 打开文件 | fopen |
| 读取文件 | fscanf / fgets / fread |
| 写入文件 | fprintf / fputs / fwrite |
| 定位文件 | fseek / ftell / rewind |
| 关闭文件 | fclose |
注意事项 #
- fputs 函数不能写入换行符
- fscanf 读取时必须传地址
为什么 fclose 必须调用?
- 要刷新缓冲区
- 要释放文件资源
fgets 和 fscanf 的区别
| 对比项 | fgets() | fscanf() |
|---|---|---|
| 读取方式 | 按行读取 | 按格式读取 |
| 是否能读取空格 | 可以 | %s 不可以 |
| 遇到空格是否停止 | 不会 | %s 会停止 |
| 遇到换行是否停止 | 会 | 默认会跳过空白字符 |
是否保留 \n | 会保留 | 不会保留 |
是否自动补 \0 | 会 | %s 会 |
| 是否支持格式化读取 | 不支持 | 支持 |
| 是否适合读取整行文本 | 非常适合 | 一般 |
| 是否容易越界 | 不容易(可限制长度) | 容易 |
| 安全性 | 高 | 较低 |
| 常见用途 | 读取整行字符串 | 读取格式化数据 |
| 推荐场景 | 日志、文本行 | 数字、结构化数据 |
- fputs 专门写字符串
- fprintf 专门写格式化数据
文件定位操作 #
- fseek(fp, offset, whence)
| 宏 | 含义 |
|---|---|
| SEEK_SET | 文件开头 |
| SEEK_CUR | 当前位置 |
| SEEK_END | 文件末尾 |
| 参数 | 含义 |
|---|---|
| fp | 文件指针 |
| offset | 偏移量,单位是字节 |
| origin | 从哪里开始移动 |
fseek(fp,-3,SEEK_END); 表示移动到文件末尾的第3个字节
ftell(fp) 追加模式下,打开文件时指针处于文件末尾,ftell返回文件长度
计算文件的大小使用
fseek(fp,0,SEEK_END)
ftell(fp);文件mode列表 #
| 模式 | 作用 |
|---|---|
| r | 只读 |
| w | 只写(清空原文件) |
| a | 追加写 |
| r+ | 读写 |
| w+ | 读写(清空) |
| a+ | 读写追加 |
| rb | 二进制读 |
| wb | 二进制写 |
| ab | 二进制追加 |
位置指针 #
文件指针:文件指针是一个指向文件位置的指针,用于记录当前文件操作的位置。
判文件结束函数 feof()
文本文件可以使用EOF 作为文件结束标志;二进制文件则使用feof进行函数判断文件是否结束,如果返回值为非0,则文件结束,否则返回值为0。
读取输入返回值 #
| 函数家族 | 函数名 | 成功时的返回值 | 失败 / 结束时的返回值 |
|---|---|---|---|
| 单字符读写 | fgetc / getc | 读到的字符 (ASCII码) | EOF (-1) |
fputc / putc | 写入的字符 | EOF (-1) | |
| 字符串读写 | fgets | 缓冲区指针 (str) | NULL (空指针) |
fputs | 非负整数 | EOF (-1) | |
| 格式化读写 | fscanf | 成功读取的变量个数 (≥0) | EOF (-1) |
fprintf | 成功写入的字符数 (>0) | 负值 | |
| 二进制块读写 | fread | 实际读取的元素个数 | 小于要求的个数 (含 0) |
fwrite | 实际写入的元素个数 | 小于要求的个数 |
预处理 #
编译的流程:预处理 -> 编译 -> 汇编 -> 链接
防止重复包含
#ifndef TEST_H
#define TEST_H
// 内容
#endif宏函数 #
| 坑点 | 错误写法 | 问题示例 | 实际展开 | 问题原因 | 正确写法 |
|---|---|---|---|---|---|
| 参数没加括号 | #define SQUARE(x) x*x | SQUARE(1+2) | 1+2*1+2 | 运算优先级错误 | #define SQUARE(x) ((x)*(x)) |
| 整体没加括号 | #define ADD(a,b) a+b | 10*ADD(1,2) | 10*1+2 | 外部运算影响宏结果 | #define ADD(a,b) ((a)+(b)) |
| 参数被重复执行 | #define MAX(a,b) ((a)>(b)?(a):(b)) | MAX(i++,j++) | i++ 可能执行多次 | 宏本质是文本替换 | 尽量用函数替代 |
| 宏后加分号 | #define NUM 10; | if(x) NUM | 多出一个 ; | 预处理不是语句 | #define NUM 10 |
| 字符串相关问题 | #define STR hello | printf("%s", STR) | 不是字符串 | 少了双引号 | #define STR "hello" |
多行宏未加 \ | 宏换行直接写 | 编译报错 | 宏提前结束 | 预处理按行处理 | 使用 \ 连接 |
| 宏与变量同名 | #define NUM 10int NUM; | 编译异常 | 变量名被替换 | 宏全局替换 | 避免重名 |
| 宏不检查类型 | #define ADD(a,b) ((a)+(b)) | ADD("a","b") | 可能错误 | 宏没有类型检查 | 复杂逻辑用函数 |
| 优先级陷阱 | #define MUL(a,b) a*b | MUL(1+2,3+4) | 1+2*3+4 | 缺少括号 | #define MUL(a,b) ((a)*(b)) |
if 配合宏问题 | 多条语句宏 | if(x) TEST(); else ... | else 报错 | 宏展开导致语法混乱 | do{ }while(0) |
| 宏中定义多语句 | #define TEST a++;b++; | if(x) TEST | 逻辑混乱 | 宏不是代码块 | do{a++;b++;}while(0) |
| 宏递归定义 | #define A B#define B A | 使用 A | 展开异常 | 循环替换 | 避免循环宏 |
| 空格导致问题 | #define ADD(a, b) a + b | 某些复杂拼接 | 替换不符合预期 | 文本替换细节 | 规范书写 |
| 宏调试困难 | 宏内部复杂逻辑 | 打断点困难 | 看不到真实代码 | 编译前已替换 | 复杂逻辑改函数 |
| 宏不能取地址 | #define NUM 10 | &NUM | 编译错误 | 宏不是变量 | 使用 const |
| 宏没有作用域 | #define PI 3.14 | 全局污染 | 所有文件可见 | 预处理全局替换 | 减少全局宏 |
| 条件编译遗漏 | #ifdef DEBUG 未关闭 | 代码混乱 | 编译逻辑错误 | 条件不完整 | 配套 #endif |
| 头文件重复包含 | 没有 include guard | 重定义错误 | 重复展开 | 多次 include | 使用 #ifndef |
| 宏与函数名冲突 | #define getchar() | 调用库函数异常 | 被宏替换 | 名称污染 | 避免覆盖库名 |
| 位运算宏坑点 | #define BIT(x) 1<<x | BIT(1+2) | 1<<1+2 | 优先级问题 | #define BIT(x) (1<<(x)) |
#undef 有什么作用? #
取消宏定义
使宏失效
方便重新定义
避免宏污染库操作 #
静态库生成 #
gcc -c add.c sub.c
ar rcs libmylib.a add.o sub.o
# rcs 表示 r:插入/替换文件 c:创建库文件 s:生成索引(很重要)
gcc main.c -L. -lmylib -o app动态库生成 #
gcc -fPIC -c add.c -o add.o
gcc -shared -o libmylib.so add.o sub.o
gcc main.c -L. -lmylib -o app| 对比 | 静态库 (.a) | 动态库 (.so) |
|---|---|---|
| 链接时间 | 编译时 | 运行时 |
| 文件大小 | 大 | 小 |
| 是否依赖库 | 否 | 是 |
| 更新方式 | 重新编译 | 替换 .so |
| 内存占用 | 每个程序一份 | 多程序共享 |
- 注意:如果在程序编译/运行中链接了库文件,系统会到指定的目录下去查找库文件, 一般查找顺序如下:
- -L 指定的路径
- LD_LIBRARY_PATH
- /etc/ld.so.conf里的路径
- 默认系统路径: /lib /usr/lib
- 建议直接将库文件放置在 默认系统路径 ,可确保编译和运行时系统可以正确 加载库文件。
手动加载库文件 #
dlfcn 库函数, 使用时需要链接 -ldl
- dlopen() 返回值:*handle, 失败返回NULL,可以使用dlerror() 获取错误信息
- dlsym() 返回值:*func,失败返回NULL,可以使用dlerror() 获取错误信息
- dlclose()
- handle 句柄
位运算 #
- 位运算是对 二进制位(bit) 进行操作的运算
- 位运算符:& | ^ ~ << >>
| 运算符 | 名称 | 作用 |
|---|---|---|
& | 按位与 | 两位都为1才为1 |
| | | | 按位或,有1就为1 |
^ | 按位异或 | 相同为0,不同为1 |
~ | 按位取反 | 0变1,1变0 |
<< | 左移 | 二进制整体左移 |
>> | 右移 | 二进制整体右移 |
- 1)将某个数据二进制位(n1,n2…)清零
a &= ~(1<<n1) - 2)获取某个数据指定二进制位(n1,n2…)的数据
(a & (1 << n1)) >> n1(a >> n) & 1 - 3)将某个数据二进制位(n1,n2…)置位
a |= (1<<n1) - 4)将某个数据二进制位(n1,n2…)反转
a ^= (1<<n1)
| 目的 | 操作符 |
|---|---|
| 置1 | | |
| 清0 | & ~ |
| 取值 | >> &1 |
| 翻转 | ^ |
位端 #
- 位端 CPU存储多字节数据时:字节存放顺序不同
- 浮点型类型不能做位段;
- 位段不能跨越内存单元
- 位段二进制位数不能超过类型长度
- 位段是无法获取地址的。
- C语言允许定义无名位段,无名位段无法访问
- 无名位段设置长度为0, 强制下一个位段存储于下一个内存单元
- 不能定义位段数组。
- 位段数据要考虑主机字节序
内存地址与位权的高低 #
内存地址的高低:看十六进制数值的大小。比如 0x101 比 0x100 大,所以 0x101 是高地址,0x100 是低地址。 位权的高低:看二进制数的书写位置。左边是高位(权重最大),右边是低位(权重最小)。例如在 10000001 中,左边的 1 代表 128(高位),右边的 1 代表 1(低位)。
大小端模式 (Endianness) #
这是指多字节数据(如 int)在内存中的存储顺序: 小端模式 (Little-Endian):“低存低”。数据的低位字节存放在内存的低地址处。目前主流的 x86/x64 电脑都是小端模式。 大端模式 (Big-Endian):“高存低”。数据的高位字节存放在内存的低地址处。符合人类阅读习惯,常用于网络传输和部分嵌入式设备。 判断方法:利用联合体(union)或指针强转,将整数 1 存入内存,然后读取其最低地址的第一个字节。如果读到 1 就是小端,读到 0 就是大端。
联合体 (Union) #
核心特性:联合体内的所有成员共享同一块内存空间,并且都是从这块内存的起始位置(低地址)开始存放。 巧妙应用:正是因为它共享内存且从低地址对齐的特性,我们才能用一个单字节的 char 去“偷看”一个四字节 int 在低地址处到底存了什么,从而判断出系统的字节序。
