C语言面试题答案版(ai生成)
C语言面试题库大全(含答案)
一、基础语法类
1. 数据类型
Q: char
、short
、int
、long
、float
、double
各占多少字节?
A: 这取决于平台和编译器,但在大多数现代系统(32/64位)上:
char
: 1字节short
: 2字节int
: 4字节long
: 4字节(32位系统) 或 8字节(64位Linux/Unix)float
: 4字节double
: 8字节long long
: 8字节
可以用 sizeof()
运算符查看。
Q: signed
和 unsigned
的区别是什么?
A:
signed
(有符号): 可以表示正数、负数和零,最高位是符号位unsigned
(无符号): 只能表示非负数,所有位都用来表示数值
例如:
signed char
: -128 ~ 127unsigned char
: 0 ~ 255signed int
: -2^31 ~ 2^31-1unsigned int
: 0 ~ 2^32-1
Q: 什么是类型转换?隐式转换和显式转换的区别?
A:
-
隐式转换: 编译器自动进行,如 int 转 float
int a = 10;float b = a; // 隐式转换
-
显式转换: 程序员强制转换,使用强制类型转换运算符
float f = 3.14;int i = (int)f; // 显式转换,i = 3
注意:隐式转换可能导致数据丢失或精度损失。
Q: void\*
指针的作用是什么?
A: void*
是通用指针类型:
- 可以指向任何类型的数据
- 不能直接解引用,需要先转换为具体类型
- 常用于
malloc
、memcpy
等函数 - 实现泛型编程的基础
void* ptr = malloc(10);
int* iptr = (int*)ptr; // 需要转换后使用
2. 变量与常量
Q: const
和 #define
的区别?
A:
特性 | const | #define |
---|---|---|
类型检查 | 有类型,编译器检查 | 无类型,只是文本替换 |
作用域 | 有作用域限制 | 无作用域概念 |
内存分配 | 分配内存 | 不分配内存 |
调试 | 可以调试 | 难以调试 |
指针 | 可以用指针指向 | 不能 |
const int a = 10; // 推荐使用
#define MAX 100 // 宏定义
Q: static
关键字的作用?(修饰局部变量、全局变量、函数)
A:
-
修饰局部变量: 延长生命周期到整个程序,但作用域不变
void func() {static int count = 0; // 只初始化一次count++; }
-
修饰全局变量/函数: 限制作用域为当前文件(内部链接)
static int g_value = 100; // 仅本文件可见 static void helper() {} // 仅本文件可见
-
作用:
- 隐藏实现细节
- 避免命名冲突
- 保持状态
Q: extern
关键字的用法?
A: extern
声明外部变量或函数,表示定义在其他文件中:
// file1.c
int global_var = 100;// file2.c
extern int global_var; // 声明,不分配内存
void use() {printf("%d", global_var);
}
注意:
extern
只是声明,不是定义- 函数默认是
extern
的 - 不能用于初始化
Q: volatile
关键字的作用?
A: volatile
告诉编译器变量可能被意外改变,不要进行优化:
使用场景:
- 硬件寄存器:
volatile int* reg = (volatile int*)0x40000000;
- 中断服务程序: 共享变量
- 多线程: 信号量标志
- MMIO: 内存映射I/O
volatile int flag = 0;void interrupt_handler() {flag = 1; // 中断中修改
}while (!flag) {// 不加volatile,编译器可能优化掉循环
}
Q: register
关键字还有用吗?
A: 基本没用了:
- 建议编译器将变量存储在寄存器中
- 现代编译器的优化能力远超人工指定
- 不能对
register
变量取地址 - 编译器可能忽略这个建议
建议: 不要使用,让编译器自己优化。
3. 运算符
Q: ++i
和 i++
的区别?
A:
++i
(前置): 先自增,再返回值i++
(后置): 先返回值,再自增
int i = 5;
int a = ++i; // i=6, a=6
int b = i++; // i=7, b=6
效率: ++i
通常稍快,因为 i++
需要保存临时值。
Q: 位运算符的应用场景?
A:
-
判断奇偶:
if (n & 1) // 奇数
-
交换变量
:
a ^= b;b ^= a;a ^= b;
-
清零特定位:
n &= ~(1 << k)
-
设置特定位:
n |= (1 << k)
-
乘除2的幂:
n << 1
(乘2),n >> 1
(除2) -
取绝对值:
(n ^ (n >> 31)) - (n >> 31)
Q: 逗号运算符的优先级和用法?
A:
- 优先级: 最低
- 功能: 从左到右计算表达式,返回最后一个表达式的值
int a = (1, 2, 3); // a = 3
int b = 1, 2, 3; // 错误!
for (i = 0, j = 10; i < j; i++, j--) {} // 常见用法
Q: sizeof
是运算符还是函数?
A: 运算符,不是函数:
- 编译时计算,不是运行时
- 不需要括号(但习惯上加)
- 参数不会被执行
int i = 0;
sizeof(i++); // i不会自增
printf("%d", i); // 输出0
二、指针与数组
1. 指针基础
Q: 什么是指针?指针的本质是什么?
A:
- 指针: 存储变量地址的变量
- 本质: 指针就是一个整数,存储内存地址
- 大小: 在32位系统占4字节,64位系统占8字节
int a = 10;
int* p = &a; // p存储a的地址
*p = 20; // 通过指针修改a的值
Q: 野指针、悬空指针、空指针的区别?
A:
-
空指针(NULL): 不指向任何有效内存
int* p = NULL; // 安全的
-
野指针: 未初始化的指针,指向随机地址
int* p; // 危险!指向未知位置 *p = 10; // 可能崩溃
-
悬空指针: 指向已释放的内存
int* p = malloc(sizeof(int)); free(p); // p现在是悬空指针 p = NULL; // 应该置NULL
防范措施:
- 指针初始化为NULL
- free后立即置NULL
- 使用前检查指针是否为NULL
Q: 如何避免内存泄漏?
A:
-
配对使用: 每个
malloc
对应一个free
-
及时释放: 不再使用时立即释放
-
避免丢失指针
:
char* p = malloc(100);p = malloc(200); // 错误!前面的100字节泄漏
-
使用工具: Valgrind、AddressSanitizer
-
良好习惯
:
- 谁分配谁释放
- 释放后置NULL
- 注意异常路径
2. 指针进阶
Q: 指向指针的指针(二级指针)的应用?
A: 二级指针的主要用途:
-
修改指针本身:
void allocate(int** p) {*p = malloc(sizeof(int));**p = 100; }int* ptr = NULL; allocate(&ptr); // 传递指针的地址
-
指针数组:
char* names[] = {"Alice", "Bob", "Charlie"}; char** p = names; // 指向字符串数组
-
动态二维数组:
int** matrix = malloc(rows * sizeof(int*)); for (int i = 0; i < rows; i++) {matrix[i] = malloc(cols * sizeof(int)); }
Q: 函数指针的定义和使用?
A:
// 定义:返回类型 (*指针名)(参数列表)
int (*func_ptr)(int, int);// 使用示例
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }func_ptr = add;
int result = func_ptr(10, 5); // 15// 函数指针数组
int (*operations[])(int, int) = {add, sub};
result = operations[0](10, 5); // 调用add// 回调函数
void process(int* arr, int len, int (*compare)(int, int)) {// 使用compare函数处理数组
}
应用场景: 回调函数、状态机、插件系统
Q: 指针数组和数组指针的区别?
A:
// 指针数组:数组的每个元素都是指针
int* arr1[10]; // 10个int*指针的数组
char* strs[] = {"hello", "world"};// 数组指针:指向数组的指针
int (*arr2)[10]; // 指向包含10个int的数组的指针
int matrix[3][4];
arr2 = matrix; // arr2指向matrix的一行// 记忆技巧:看符号优先级
// int* arr[10] -> arr先和[]结合 -> 指针数组
// int (*arr)[10] -> arr先和*结合 -> 数组指针
Q: const int\*
、int const\*
、int\* const
的区别?
A:
// 1. const int* p = &a; (等同于 int const* p)
// 指针指向的内容不可改,指针本身可以改
const int* p1 = &a;
*p1 = 20; // 错误!
p1 = &b; // 正确// 2. int* const p = &a;
// 指针本身不可改,指向的内容可以改
int* const p2 = &a;
*p2 = 20; // 正确
p2 = &b; // 错误!// 3. const int* const p = &a;
// 指针和内容都不可改
const int* const p3 = &a;
*p3 = 20; // 错误!
p3 = &b; // 错误!// 记忆技巧:const修饰它右边的内容
// const在*左边 -> 修饰数据
// const在*右边 -> 修饰指针
3. 数组
Q: 数组名和指针的区别?
A: 虽然很相似,但有重要区别:
int arr[10];
int* p = arr;// 相同点
arr[0] == *arr == *p;
arr + 1 == p + 1;// 不同点
sizeof(arr); // 40 (整个数组大小)
sizeof(p); // 4或8 (指针大小)&arr; // 类型是 int(*)[10],指向整个数组
&p; // 类型是 int**arr = p; // 错误!数组名是常量,不能赋值
p = arr; // 正确
关键区别:
- 数组名是常量指针,不能修改
sizeof(数组名)
返回整个数组大小&数组名
的类型是指向整个数组的指针
Q: 多维数组在内存中的存储方式?
A: 按行优先(row-major)顺序连续存储:
int arr[2][3] = {{1, 2, 3},{4, 5, 6}
};// 内存布局:1 2 3 4 5 6 (连续存储)// 访问方式
arr[1][2] 等价于 *(*(arr + 1) + 2)等价于 *((int*)arr + 1*3 + 2)等价于 *((int*)arr + 5)// 地址计算
&arr[i][j] = 首地址 + (i * 列数 + j) * sizeof(元素类型)
Q: 数组作为函数参数时发生了什么?
A: 数组退化为指针:
void func1(int arr[10]) {// arr实际是 int* 类型sizeof(arr); // 指针大小(4或8),不是40!
}void func2(int* arr) {// 和func1完全等价
}// 多维数组
void func3(int arr[][3]) { // 第一维可以省略// arr是 int(*)[3] 类型
}void func4(int (*arr)[3]) {// 和func3等价
}// 调用
int a[10];
func1(a); // 传递的是首地址// 如果需要数组大小,必须额外传递
void func5(int* arr, int size) {}
注意: 数组作为参数时总是传递指针,无法知道原数组大小。
Q: 如何动态分配二维数组?
A: 三种方法:
// 方法1:指针数组(推荐)
int** matrix1 = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {matrix1[i] = malloc(cols * sizeof(int));
}
// 使用:matrix1[i][j]
// 释放:逐行释放,最后释放matrix1// 方法2:一维数组模拟
int* matrix2 = malloc(rows * cols * sizeof(int));
// 使用:matrix2[i * cols + j]
// 释放:一次free(matrix2)// 方法3:数组指针
int (*matrix3)[cols] = malloc(rows * sizeof(int[cols]));
// 使用:matrix3[i][j]
// 释放:一次free(matrix3)
// 注意:cols必须是常量(C99支持变长数组)
三、内存管理
1. 内存分区
Q: 程序在内存中的五大分区?
A:
高地址
│
├─ 栈区(Stack) ← 向下增长
│ - 局部变量、函数参数、返回地址
│ - 自动分配和释放
│ - 大小有限(通常几MB)
│
├─ ↓ (栈向下增长)
├─ ↑ (堆向上增长)
│
├─ 堆区(Heap) ← 向上增长
│ - malloc/calloc/realloc分配
│ - 手动分配和释放
│ - 大小较大
│
├─ 全局/静态区(Data Segment)
│ ├─ 已初始化(.data) - 已初始化的全局和静态变量
│ └─ 未初始化(.bss) - 未初始化的全局和静态变量(自动初始化为0)
│
├─ 常量区(.rodata)
│ - 字符串常量、const变量
│ - 只读,不可修改
│
├─ 代码区(.text)
│ - 可执行代码
│ - 只读,防止被意外修改
│
低地址
示例:
int g_init = 10; // .data段
int g_uninit; // .bss段
static int s_var = 20; // .data段
const int c_var = 30; // .rodata段void func() {int local = 40; // 栈static int s_local = 50; // .data段char* str = "hello"; // str在栈,"hello"在常量区char* p = malloc(100); // p在栈,分配的内存在堆
}
Q: 栈和堆的区别?
A:
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
分配方式 | 自动分配/释放 | 手动malloc/free |
分配速度 | 快(只需移动栈指针) | 慢(需要查找合适的块) |
大小限制 | 小(通常1-8MB) | 大(受系统内存限制) |
生命周期 | 函数调用期间 | 程序员控制 |
增长方向 | 向下(高地址→低地址) | 向上(低地址→高地址) |
碎片问题 | 无 | 可能产生碎片 |
访问速度 | 快(局部性好) | 相对慢 |
线程安全 | 每个线程独立 | 需要同步机制 |
Q: 全局变量和局部变量在内存中的存储位置?
A:
int global; // 全局变量 -> .bss段
int global_init = 10; // 全局变量 -> .data段
static int s_global; // 静态全局 -> .bss段void func() {int local; // 局部变量 -> 栈static int s_local; // 静态局部 -> .bss段static int s_local_init = 20; // 静态局部 -> .data段register int r_local; // 建议存储在寄存器
}
关键点:
- 全局变量: .data或.bss段,生命周期=程序运行期
- 局部变量: 栈,生命周期=函数调用期
- 静态变量: .data或.bss段,无论全局还是局部
2. 动态内存
Q: malloc
、calloc
、realloc
的区别?
A:
-
malloc: 分配指定字节的内存,不初始化
int* p = (int*)malloc(10 * sizeof(int)); // 内容未初始化,可能是垃圾值
-
calloc: 分配内存并初始化为0
int* p = (int*)calloc(10, sizeof(int)); // 所有元素都是0
-
realloc: 调整已分配内存的大小
int* p = malloc(10 * sizeof(int)); p = realloc(p, 20 * sizeof(int)); // 扩大到20个int // 如果扩展失败,返回NULL,原内存仍然有效
对比表:
函数 | 初始化 | 参数 | 典型用途 |
---|---|---|---|
malloc(size) | 否 | 1个 | 一般分配 |
calloc(n, size) | 是(清零) | 2个 | 需要初始化为0 |
realloc(ptr, size) | 否 | 2个 | 调整大小 |
注意事项:
// realloc使用要小心
int* p = malloc(10 * sizeof(int));
int* new_p = realloc(p, 20 * sizeof(int));
if (new_p == NULL) {// 扩展失败,p仍然有效,不要直接赋值给p// p = realloc(p, 20); // 错误!如果失败,p丢失
} else {p = new_p;
}
Q: free
之后指针需要置 NULL 吗?为什么?
A: 强烈建议置NULL,原因:
-
防止悬空指针:
int* p = malloc(sizeof(int)); free(p); // p现在是悬空指针,指向已释放的内存 p = NULL; // 置NULL后,再次free不会出错
-
避免双重释放:
free(p); free(p); // 错误!导致程序崩溃// 如果置NULL free(p); p = NULL; free(p); // 安全,free(NULL)什么都不做
-
便于判断:
if (p != NULL) {// 安全使用p }
最佳实践:
#define SAFE_FREE(p) do { free(p); (p) = NULL; } while(0)
Q: 内存对齐的原因和作用?
A:
原因:
- 硬件要求: 某些CPU访问未对齐数据会崩溃或效率低
- 性能优化: 对齐访问速度更快(一次读取)
规则:
- 结构体成员按自身大小对齐
- 结构体总大小是最大成员的整数倍
- 编译器可能添加填充字节(padding)
示例:
struct A {char c; // 1字节int i; // 4字节short s; // 2字节
};
// 实际布局:c + 3字节padding + i + s + 2字节padding
// 总大小:12字节(不是7字节)struct B {char c; // 1字节short s; // 2字节int i; // 4字节
};
// 实际布局:c + 1字节padding + s + i
// 总大小:8字节// 查看对齐
printf("%zu\n", offsetof(struct A, i)); // 4(不是1)
优化技巧:
- 按大小降序排列成员
- 使用
#pragma pack
指定对齐 - 使用
__attribute__((packed))
取消对齐
Q: 如何检测内存泄漏?
A:
-
Valgrind (Linux):
gcc -g program.c -o program valgrind --leak-check=full ./program
-
AddressSanitizer (GCC/Clang):
gcc -fsanitize=address -g program.c -o program ./program
-
Visual Studio (Windows):
- 使用内置的内存泄漏检测
- CRT调试库
-
手动跟踪:
#ifdef DEBUG static int alloc_count = 0;void* my_malloc(size_t size) {alloc_count++;return malloc(size); }void my_free(void* ptr) {if (ptr) alloc_count--;free(ptr); }void check_leaks() {if (alloc_count != 0) {printf("Memory leak! %d allocations not freed\n", alloc_count);} } #endif
-
良好习惯:
- 每个malloc对应一个free
- 使用智能指针(C++11)
- RAII模式
- 资源获取即初始化
3. 内存越界
Q: 常见的内存越界场景?
A:
-
数组越界:
int arr[10]; arr[10] = 100; // 越界!索引应该是0-9
-
字符串操作:
char str[5] = "hello"; // 错误!需要6字节(包括'\0') strcpy(dest, src); // dest空间不足
-
指针运算:
int* p = malloc(10 * sizeof(int)); p[20] = 100; // 越界访问
-
栈溢出:
void func() {int huge[1000000]; // 栈空间不足 }
-
未初始化指针:
int* p; // 野指针 *p = 10; // 访问未知内存
Q: 缓冲区溢出的原理和防范?
A:
原理: 写入的数据超过缓冲区大小,覆盖相邻内存
经典漏洞:
void vulnerable(char* input) {char buffer[64];strcpy(buffer, input); // 危险!input可能超过64字节// 可能覆盖返回地址,执行恶意代码
}
防范措施:
-
使用安全函数:
// 不安全 strcpy(dest, src); gets(buffer); sprintf(buffer, "%s", str);// 安全 strncpy(dest, src, sizeof(dest) - 1); dest[sizeof(dest) - 1] = '\0'; fgets(buffer, sizeof(buffer), stdin); snprintf(buffer, sizeof(buffer), "%s", str);
-
边界检查:
if (strlen(src) < sizeof(dest)) {strcpy(dest, src); }
-
编译器保护:
gcc -fstack-protector-all program.c # 栈保护 gcc -D_FORTIFY_SOURCE=2 program.c # 函数安全检查
-
静态分析工具: 使用 Coverity, Clang Static Analyzer
-
代码审查: 重点检查字符串和数组操作
Q: 如何安全地使用字符串函数?
A:
// 1. strcpy -> strncpy + 手动添加'\0'
char dest[20];
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保以'\0'结尾// 2. strcat -> strncat
strncat(dest, src, sizeof(dest) - strlen(dest) - 1);// 3. sprintf -> snprintf
snprintf(buffer, sizeof(buffer), "Value: %d", value);// 4. gets -> fgets
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = '\0'; // 移除换行符// 5. 自定义安全函数
size_t safe_strcpy(char* dest, const char* src, size_t dest_size) {if (dest_size == 0) return 0;size_t i;for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++) {dest[i] = src[i];}dest[i] = '\0';return i;
}
检查清单:
- ✓ 始终指定最大长度
- ✓ 确保目标缓冲区足够大
- ✓ 手动添加’\0’终止符
- ✓ 检查返回值
- ✓ 处理边界情况
四、函数
1. 函数基础
Q: 函数参数的传递方式?
A: C语言只有值传递(pass by value)
// 基本类型 - 传递副本
void func1(int x) {x = 100; // 不影响外部变量
}int a = 10;
func1(a);
printf("%d", a); // 输出10// 通过指针"模拟"引用传递
void func2(int* x) {*x = 100; // 修改指针指向的内容
}func2(&a);
printf("%d", a); // 输出100// 数组参数 - 实际传递指针
void func3(int arr[]) {// arr是指针,不是数组arr[0] = 100; // 修改原数组
}
关键点:
- 传递的是参数的副本
- 指针传递的是地址的副本
- 数组退化为指针
Q: 如何通过函数修改外部变量?
A:
// 1. 传递指针
void modify_int(int* p) {*p = 100;
}int value = 10;
modify_int(&value);// 2. 修改指针本身 - 需要二级指针
void allocate_memory(int** p) {*p = malloc(sizeof(int));**p = 100;
}int* ptr = NULL;
allocate_memory(&ptr);// 3. 修改数组
void modify_array(int arr[], int size) {for (int i = 0; i < size; i++) {arr[i] = i * 2;}
}// 4. 修改结构体
void modify_struct(struct Person* p) {p->age = 20;strcpy(p->name, "Alice");
}// 5. 使用全局变量(不推荐)
int global;
void modify_global() {global = 100;
}
Q: 函数返回局部变量的地址会有什么问题?
A: 严重错误! 返回的是悬空指针
// 错误示例
int* func() {int local = 100;return &local; // 危险!local在函数返回后被销毁
}int* p = func();
*p = 200; // 未定义行为,可能崩溃// 正确做法1:返回动态分配的内存
int* func1() {int* p = malloc(sizeof(int));*p = 100;return p; // 调用者负责释放
}// 正确做法2:使用静态变量
int* func2() {static int value = 100;return &value; // 静态变量生命周期是整个程序
}// 正确做法3:调用者提供缓冲区
void func3(int* result) {*result = 100;
}int value;
func3(&value);// 正确做法4:返回值而不是地址
int func4() {int local = 100;return local; // 返回值的副本,安全
}
原因: 局部变量在栈上,函数返回后栈空间被回收。
Q: 可变参数函数如何实现?
A: 使用 <stdarg.h>
中的宏
#include <stdarg.h>// 实现一个简单的printf
int my_printf(const char* format, ...) {va_list args;va_start(args, format); // 初始化,format是最后一个固定参数while (*format) {if (*format == '%') {format++;switch (*format) {case 'd': {int i = va_arg(args, int); // 获取int参数printf("%d", i);break;}case 's': {char* s = va_arg(args, char*); // 获取char*参数printf("%s", s);break;}case 'f': {double d = va_arg(args, double); // 获取double参数printf("%f", d);break;}}} else {putchar(*format);}format++;}va_end(args); // 清理return 0;
}// 使用
my_printf("Number: %d, String: %s\n", 42, "hello");// 求和函数
int sum(int count, ...) {va_list args;va_start(args, count);int total = 0;for (int i = 0; i < count; i++) {total += va_arg(args, int);}va_end(args);return total;
}int result = sum(4, 10, 20, 30, 40); // 100
关键宏:
va_list
: 参数列表类型va_start(ap, last)
: 初始化va_arg(ap, type)
: 获取下一个参数va_end(ap)
: 清理
注意:
- 至少需要一个固定参数
- 调用者必须知道参数个数和类型
- 类型不安全
2. 函数高级
Q: 回调函数的概念和应用?
A: 回调函数: 通过函数指针传递给另一个函数的函数
// 1. 排序回调
int compare_asc(const void* a, const void* b) {return *(int*)a - *(int*)b;
}int compare_desc(const void* a, const void* b) {return *(int*)b - *(int*)a;
}int arr[] = {5, 2, 8, 1, 9};
qsort(arr, 5, sizeof(int), compare_asc); // 升序
qsort(arr, 5, sizeof(int), compare_desc); // 降序// 2. 事件处理
typedef void (*EventHandler)(void);void button_click() {printf("Button clicked!\n");
}void register_event(EventHandler handler) {// 当事件发生时调用handler();
}register_event(button_click);// 3. 通用算法
void foreach(int* arr, int len, void (*func)(int)) {for (int i = 0; i < len; i++) {func(arr[i]);}
}void print_value(int x) {printf("%d ", x);
}foreach(arr, 5, print_value);// 4. 状态机
typedef enum { STATE_A, STATE_B, STATE_C } State;
typedef void (*StateFunc)(void);void state_a_handler() { printf("In State A\n"); }
void state_b_handler() { printf("In State B\n"); }
void state_c_handler() { printf("In State C\n"); }StateFunc state_table[] = {state_a_handler,state_b_handler,state_c_handler
};State current = STATE_A;
state_table[current](); // 调用对应状态的处理函数
应用场景:
- GUI事件处理
- 异步I/O回调
- 插件系统
- 算法定制
Q: 递归函数的优缺点?
A:
优点:
- 代码简洁,逻辑清晰
- 天然适合树、图等递归结构
- 某些问题用递归更自然
缺点:
- 栈空间消耗大
- 可能导致栈溢出
- 效率较低(重复计算)
// 1. 经典递归 - 阶乘
int factorial(int n) {if (n <= 1) return 1;return n * factorial(n - 1);
}// 2. 斐波那契(效率低,重复计算)
int fib(int n) {if (n <= 1) return n;return fib(n - 1) + fib(n - 2); // 指数级复杂度
}// 3. 尾递归优化
int factorial_tail(int n, int acc) {if (n <= 1) return acc;return factorial_tail(n - 1, n * acc);
}// 4. 递归转迭代(推荐)
int fib_iter(int n) {if (n <= 1) return n;int prev = 0, curr = 1;for (int i = 2; i <= n; i++) {int next = prev + curr;prev = curr;curr = next;}return curr;
}// 5. 递归深度限制
int safe_func(int n, int depth) {if (depth > 1000) {fprintf(stderr, "Recursion too deep!\n");return -1;}if (n <= 0) return 0;return safe_func(n - 1, depth + 1);
}
何时使用递归:
- ✓ 问题本身递归定义(树遍历、分治算法)
- ✓ 数据规模小,不会栈溢出
- ✗ 有重复子问题(用动态规划)
- ✗ 深度很大(改用迭代)
Q: 内联函数的作用(C99)?
A:
内联函数: 建议编译器在调用处展开函数体,减少函数调用开销
// 定义内联函数
inline int max(int a, int b) {return a > b ? a : b;
}// 相当于在调用处展开
int x = max(10, 20);
// 展开为: int x = 10 > 20 ? 10 : 20;// 对比宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 宏的问题:副作用
int y = MAX(i++, j++); // i++或j++可能执行多次!
优点 vs 宏:
- 类型安全
- 无副作用
- 可以调试
- 作用域明确
注意:
inline
只是建议,编译器可能忽略- 适合小函数(1-3行)
- 大函数内联可能增加代码体积
- 在头文件中需要配合
static inline
现代用法:
// C99及以后
static inline int square(int x) {return x * x;
}
Q: 函数指针数组的应用场景?
A:
// 1. 计算器实现
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div_op(int a, int b) { return b != 0 ? a / b : 0; }int (*operations[])(int, int) = {add, sub, mul, div_op};int calculate(int a, int b, char op) {int index;switch (op) {case '+': index = 0; break;case '-': index = 1; break;case '*': index = 2; break;case '/': index = 3; break;default: return 0;}return operations[index](a, b);
}// 2. 菜单系统
void menu_new() { printf("New file\n"); }
void menu_open() { printf("Open file\n"); }
void menu_save() { printf("Save file\n"); }
void menu_exit() { printf("Exit\n"); }typedef void (*MenuFunc)(void);typedef struct {const char* name;MenuFunc handler;
} MenuItem;MenuItem menu[] = {{"New", menu_new},{"Open", menu_open},{"Save", menu_save},{"Exit", menu_exit}
};void process_menu(int choice) {if (choice >= 0 && choice < 4) {menu[choice].handler();}
}// 3. 状态机
typedef enum { INIT, RUNNING, PAUSED, STOPPED } State;void handle_init() { printf("Initializing...\n"); }
void handle_running() { printf("Running...\n"); }
void handle_paused() { printf("Paused...\n"); }
void handle_stopped() { printf("Stopped...\n"); }void (*state_handlers[])(void) = {handle_init,handle_running,handle_paused,handle_stopped
};State current_state = INIT;
state_handlers[current_state]();// 4. 命令模式
typedef void (*Command)(const char*);void cmd_print(const char* arg) { printf("%s\n", arg); }
void cmd_upper(const char* arg) { /* 转大写 */ }
void cmd_lower(const char* arg) { /* 转小写 */ }typedef struct {const char* name;Command func;
} CommandEntry;CommandEntry commands[] = {{"print", cmd_print},{"upper", cmd_upper},{"lower", cmd_lower}
};void execute_command(const char* name, const char* arg) {for (int i = 0; i < 3; i++) {if (strcmp(commands[i].name, name) == 0) {commands[i].func(arg);return;}}
}
优势:
- 避免冗长的 if-else 或 switch
- 易于扩展(添加新功能)
- 代码更简洁
- 支持运行时选择
五、字符串
1. 字符串基础
Q: char[]
和 char\*
定义字符串的区别?
A:
// 1. 字符数组 - 可修改
char str1[] = "hello";
// 内存:栈上分配,内容可修改
// 大小:sizeof(str1) = 6 (包括'\0')
str1[0] = 'H'; // 正确// 2. 字符指针 - 指向常量区
char* str2 = "hello";
// 内存:str2在栈上,"hello"在常量区(只读)
// 大小:sizeof(str2) = 4或8 (指针大小)
str2[0] = 'H'; // 错误!段错误,修改只读内存// 3. 动态分配 - 可修改
char* str3 = malloc(6);
strcpy(str3, "hello");
str3[0] = 'H'; // 正确
free(str3);// 4. 总结
char arr[] = "abc"; // 数组,可修改,栈
char* ptr = "abc"; // 指针,不可修改,常量区
char* dyn = strdup("abc"); // 指针,可修改,堆
内存布局:
栈区: str1: ['h']['e']['l']['l']['o']['\0']str2: [地址指针] → 常量区
常量区: "hello"
堆区: str3 → malloc分配的内存
Q: 字符串结束符 \0
的作用?
A:
作用: 标记字符串结束,ASCII码为0
// 1. 正确的字符串
char str1[] = "hello"; // 自动添加'\0'
// 实际: 'h','e','l','l','o','\0'// 2. 没有'\0'的字符数组
char str2[] = {'h','e','l','l','o'}; // 不是字符串!
printf("%s", str2); // 未定义行为,会继续读取内存// 3. strlen依赖'\0'
int len1 = strlen(str1); // 5
int len2 = strlen(str2); // 未定义,可能很大// 4. 手动添加'\0'
char str3[6];
str3[0] = 'h';
str3[1] = 'e';
str3[2] = 'l';
str3[3] = 'l';
str3[4] = 'o';
str3[5] = '\0'; // 必须手动添加// 5. 常见错误
char str4[5] = "hello"; // 错误!需要6字节
// 正确:
char str5[6] = "hello"; // 或
char str6[] = "hello"; // 让编译器计算
注意:
- C字符串函数都依赖’\0’
- 缺少’\0’会导致越界访问
- sizeof包括’\0’,strlen不包括
Q: 如何计算字符串长度?
A:
#include <string.h>// 1. 使用strlen
char* str = "hello";
size_t len = strlen(str); // 5,不包括'\0'// 2. 手动实现strlen
size_t my_strlen(const char* str) {size_t count = 0;while (*str != '\0') {count++;str++;}return count;
}// 或者更简洁
size_t my_strlen2(const char* str) {const char* start = str;while (*str) str++;return str - start;
}// 3. sizeof vs strlen
char str1[] = "hello";
sizeof(str1); // 6 (编译时确定,包括'\0')
strlen(str1); // 5 (运行时计算,不包括'\0')char* str2 = "hello";
sizeof(str2); // 4或8 (指针大小)
strlen(str2); // 5// 4. 宽字符串
wchar_t wstr[] = L"hello";
wcslen(wstr); // 宽字符串长度// 5. 带长度的字符串(更安全)
typedef struct {size_t len;char* str;
} String;String create_string(const char* s) {String str;str.len = strlen(s);str.str = strdup(s);return str;
}
性能考虑:
strlen
是O(n)操作- 频繁调用应缓存结果
- 考虑使用带长度的字符串结构
2. 字符串函数
Q: strcpy
和 strncpy
的区别?安全性如何?
A:
// 1. strcpy - 不安全
char dest[10];
char* src = "hello world"; // 12字节
strcpy(dest, src); // 缓冲区溢出!// 2. strncpy - 较安全但有陷阱
strncpy(dest, src, sizeof(dest));
// 问题:如果src长度>=n,不会添加'\0'!
dest[sizeof(dest) - 1] = '\0'; // 必须手动添加// 3. 安全的strcpy实现
char* safe_strcpy(char* dest, const char* src, size_t dest_size) {if (dest_size == 0) return dest;size_t i;for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++) {dest[i] = src[i];}dest[i] = '\0';return dest;
}// 4. 使用snprintf (推荐)
snprintf(dest, sizeof(dest), "%s", src);// 5. C11的strcpy_s (如果支持)
#ifdef __STDC_LIB_EXT1__
strcpy_s(dest, sizeof(dest), src);
#endif// 6. strdup - 动态分配
char* copy = strdup(src); // 自动分配内存
if (copy) {// 使用copyfree(copy);
}// 对比表
/*
函数 安全性 自动添加'\0' 需要手动处理
strcpy 低 是 检查目标大小
strncpy 中 否(当截断时) 手动添加'\0'
snprintf 高 是 无
strcpy_s 高 是 需要C11支持
*/
最佳实践:
// 推荐方法
size_t len = strlen(src);
if (len < sizeof(dest)) {strcpy(dest, src);
} else {strncpy(dest, src, sizeof(dest) - 1);dest[sizeof(dest) - 1] = '\0';
}// 或直接使用snprintf
snprintf(dest, sizeof(dest), "%s", src);
Q: strcmp
的返回值含义?
A:
#include <string.h>int result = strcmp(str1, str2);// 返回值:
// < 0 : str1 < str2 (str1在字典序中排在前面)
// == 0 : str1 == str2 (两个字符串相同)
// > 0 : str1 > str2 (str1在字典序中排在后面)// 示例
strcmp("abc", "abc"); // 0
strcmp("abc", "abd"); // -1 (c < d)
strcmp("abd", "abc"); // 1 (d > c)
strcmp("ab", "abc"); // -1 (shorter < longer)// 常见用法
if (strcmp(str1, str2) == 0) {printf("字符串相同\n");
}// 排序
int compare(const void* a, const void* b) {return strcmp(*(char**)a, *(char**)b);
}
qsort(strings, count, sizeof(char*), compare);// 变体函数
strncmp(str1, str2, n); // 只比较前n个字符
strcasecmp(str1, str2); // 忽略大小写(非标准)
strncasecmp(str1, str2, n); // 组合// 手动实现strcmp
int my_strcmp(const char* s1, const char* s2) {while (*s1 && (*s1 == *s2)) {s1++;s2++;}return *(unsigned char*)s1 - *(unsigned char*)s2;
}
注意:
- 不要用
str1 == str2
比较字符串(比较的是指针) - 返回值不一定是-1、0、1,只保证符号
- 使用
unsigned char
比较避免符号问题
Q: strcat
、strlen
、strstr
等函数的使用?
A:
// 1. strcat - 字符串连接
char dest[20] = "hello";
strcat(dest, " world"); // "hello world"
// 危险:不检查dest空间是否足够// 安全版本:strncat
strncat(dest, src, sizeof(dest) - strlen(dest) - 1);// 2. strlen - 字符串长度
size_t len = strlen("hello"); // 5// 3. strstr - 查找子串
char* haystack = "hello world";
char* needle = "world";
char* pos = strstr(haystack, needle);
if (pos) {printf("Found at position: %ld\n", pos - haystack); // 6
}// 4. strchr - 查找字符
char* p = strchr("hello", 'l'); // 指向第一个'l'
char* q = strrchr("hello", 'l'); // 指向最后一个'l'// 5. strspn / strcspn - 跨度
char* str = "hello123world";
size_t len1 = strspn(str, "helo"); // 5 (前5个字符都在集合中)
size_t len2 = strcspn(str, "0123456789"); // 5 (前5个都不是数字)// 6. strtok - 分割字符串
char str[] = "hello,world,test";
char* token = strtok(str, ",");
while (token) {printf("%s\n", token);token = strtok(NULL, ","); // 后续调用传NULL
}
// 注意:strtok会修改原字符串!
// 7. strpbrk - 查找多个字符中的任意一个
char* str = "hello world";
char* p = strpbrk(str, "aeiou"); // 找到第一个元音字母 'e'
// 8. 字符串到数字转换
int num = atoi("123"); // 字符串转int
long ln = atol("123456"); // 字符串转long
double d = atof("3.14"); // 字符串转double
// 更安全的版本(C99)
char* endptr;
long val = strtol("123abc", &endptr, 10); // val=123, endptr指向"abc"
if (*endptr != '\0') {
printf("转换未完成\n");
}
// 9. 字符串格式化
char buffer[100];
sprintf(buffer, "Name: %s, Age: %d", "Alice", 25);
// 更安全:
snprintf(buffer, sizeof(buffer), "Name: %s, Age: %d", "Alice", 25);
// 10. 字符串复制到堆
char* copy = strdup("hello"); // 等价于 malloc + strcpy
if (copy) {
// 使用copy
free(copy);
}
// 7. strpbrk - 查找多个字符中的任意一个 char* str = “hello world”; char* p = strpbrk(str, “aeiou”); // 找到第一个元音字母 ‘e’
// 8. 字符串到数字转换 int num = atoi(“123”); // 字符串转int long ln = atol(“123456”); // 字符串转long double d = atof(“3.14”); // 字符串转double
// 更安全的版本(C99) char* endptr; long val = strtol(“123abc”, &endptr, 10); // val=123, endptr指向"abc" if (*endptr != ‘\0’) { printf(“转换未完成\n”); }
// 9. 字符串格式化 char buffer[100]; sprintf(buffer, “Name: %s, Age: %d”, “Alice”, 25); // 更安全: snprintf(buffer, sizeof(buffer), “Name: %s, Age: %d”, “Alice”, 25);
// 10. 字符串复制到堆 char* copy = strdup(“hello”); // 等价于 malloc + strcpy if (copy) { // 使用copy free(copy); }
---**Q: 如何实现自己的 `strcpy` 函数?**A:```c
// 1. 基础版本
char* my_strcpy(char* dest, const char* src) {char* ret = dest;while ((*dest++ = *src++) != '\0');return ret;
}// 2. 详细版本
char* my_strcpy_v2(char* dest, const char* src) {if (dest == NULL || src == NULL) {return NULL;}char* original = dest;while (*src != '\0') {*dest = *src;dest++;src++;}*dest = '\0'; // 添加结束符return original;
}// 3. 安全版本(带长度限制)
char* my_strncpy(char* dest, const char* src, size_t n) {if (dest == NULL || src == NULL || n == 0) {return dest;}char* original = dest;size_t i;// 复制最多n-1个字符for (i = 0; i < n - 1 && src[i] != '\0'; i++) {dest[i] = src[i];}// 确保以'\0'结尾dest[i] = '\0';return original;
}// 4. 带重叠检测
char* my_strcpy_safe(char* dest, const char* src, size_t dest_size) {if (!dest || !src || dest_size == 0) {return NULL;}// 检测内存重叠if ((src >= dest && src < dest + dest_size) ||(dest >= src && dest < src + strlen(src))) {fprintf(stderr, "Memory overlap detected!\n");return NULL;}size_t i;for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++) {dest[i] = src[i];}dest[i] = '\0';return dest;
}// 5. 性能优化版(按字处理)
char* my_strcpy_fast(char* dest, const char* src) {char* ret = dest;// 按机器字长度对齐后批量复制while ((uintptr_t)src & (sizeof(size_t) - 1)) {if ((*dest++ = *src++) == '\0')return ret;}// 批量复制size_t* dest_word = (size_t*)dest;const size_t* src_word = (const size_t*)src;while (1) {size_t word = *src_word++;// 检测是否包含'\0'if (((word - 0x0101010101010101ULL) & ~word & 0x8080808080808080ULL)) {break;}*dest_word++ = word;}// 复制剩余字节dest = (char*)dest_word;src = (const char*)src_word;while ((*dest++ = *src++) != '\0');return ret;
}// 测试
void test_strcpy() {char dest[20];const char* src = "Hello";my_strcpy(dest, src);printf("%s\n", dest); // Hello// 测试边界my_strncpy(dest, "Very long string", sizeof(dest));printf("%s\n", dest); // Very long strin (截断)
}
面试要点:
- 检查NULL指针
- 返回dest的原始指针
- 不要忘记复制’\0’
- 考虑缓冲区大小
- 处理重叠情况(可选)
六、结构体与联合体
1. 结构体
Q: 结构体的内存对齐规则?
A:
对齐规则:
- 第一个成员偏移量为0
- 每个成员按其自身大小对齐
- 结构体总大小是最大成员的整数倍
#include <stddef.h> // offsetof// 示例1:无优化
struct A {char c; // 1字节, offset=0int i; // 4字节, offset=4 (需要4字节对齐)short s; // 2字节, offset=8// 总大小:12字节 (需要是4的倍数)
};
// 内存:[c][pad][pad][pad][i][i][i][i][s][s][pad][pad]// 示例2:优化排列
struct B {int i; // 4字节, offset=0short s; // 2字节, offset=4char c; // 1字节, offset=6// 总大小:8字节
};
// 内存:[i][i][i][i][s][s][c][pad]// 示例3:嵌套结构体
struct Inner {char c;int i;
}; // 大小:8字节struct Outer {char c; // offset=0struct Inner in; // offset=4 (按Inner的对齐要求)short s; // offset=12
}; // 大小:16字节// 查看偏移量
printf("offset of i: %zu\n", offsetof(struct A, i));
printf("size of A: %zu\n", sizeof(struct A));// 示例4:复杂情况
struct Complex {double d; // 8字节, offset=0char c; // 1字节, offset=8int i; // 4字节, offset=12short s; // 2字节, offset=16char c2; // 1字节, offset=18
}; // 大小:24字节 (是8的倍数)// 优化版本
struct ComplexOpt {double d; // 8字节int i; // 4字节short s; // 2字节char c; // 1字节char c2; // 1字节
}; // 大小:16字节 (节省8字节!)
对齐系数:
char
: 1字节对齐short
: 2字节对齐int
: 4字节对齐long
: 4字节(32位) 或 8字节(64位)对齐double
: 8字节对齐- 指针: 4字节(32位) 或 8字节(64位)对齐
指定对齐方式:
// GCC
struct __attribute__((packed)) Packed {char c;int i;short s;
}; // 大小:7字节 (无填充)// MSVC
#pragma pack(push, 1)
struct Packed2 {char c;int i;
};
#pragma pack(pop)// 指定对齐值
struct __attribute__((aligned(16))) Aligned {int i;
}; // 大小:16字节
Q: 结构体中的位域是什么?
A: 位域: 以位为单位分配的结构体成员
// 1. 基本位域
struct Flags {unsigned int flag1 : 1; // 1位unsigned int flag2 : 1; // 1位unsigned int value : 4; // 4位unsigned int : 0; // 0宽度位域,强制下一个字段对齐unsigned int extra : 2; // 2位
};
// 总共只占用4字节(而不是16字节)// 使用
struct Flags f = {0};
f.flag1 = 1;
f.value = 15; // 最大值是 2^4-1 = 15// 2. 实际应用:寄存器映射
struct ControlRegister {unsigned int enable : 1; // bit 0unsigned int mode : 2; // bit 1-2unsigned int reserved : 5; // bit 3-7unsigned int priority : 4; // bit 8-11unsigned int : 0; // 对齐到下一个字
};// 3. 网络协议
struct IPv4Header {unsigned int version : 4;unsigned int ihl : 4;unsigned int dscp : 6;unsigned int ecn : 2;unsigned int total_length : 16;// ...
};// 4. 节省内存
struct Date {unsigned int year : 12; // 0-4095 (足够表示年份)unsigned int month : 4; // 0-15 (1-12)unsigned int day : 5; // 0-31 (1-31)
}; // 只占3字节// 传统方式需要12字节
struct DateNormal {int year;int month;int day;
};// 5. 状态标志
struct FileAttributes {unsigned int readable : 1;unsigned int writable : 1;unsigned int executable : 1;unsigned int hidden : 1;unsigned int system : 1;
};
注意事项:
- 不能取位域的地址
- 不能定义位域数组
- 位域的存储顺序依赖于实现
- 跨平台可能有问题
- 不能是浮点类型
何时使用位域:
- ✓ 硬件寄存器映射
- ✓ 网络协议解析
- ✓ 节省内存(大量标志位)
- ✗ 需要移植性
- ✗ 性能关键代码
Q: 结构体指针和结构体变量的访问方式?
A:
struct Point {int x;int y;
};// 1. 结构体变量 - 使用 '.'
struct Point p1;
p1.x = 10;
p1.y = 20;
printf("(%d, %d)\n", p1.x, p1.y);// 2. 结构体指针 - 使用 '->'
struct Point* p2 = &p1;
p2->x = 30; // 等价于 (*p2).x = 30
p2->y = 40; // 等价于 (*p2).y = 40
printf("(%d, %d)\n", p2->x, p2->y);// 3. 动态分配
struct Point* p3 = malloc(sizeof(struct Point));
if (p3) {p3->x = 50;p3->y = 60;free(p3);
}// 4. 数组访问
struct Point arr[3] = {{1,2}, {3,4}, {5,6}};
arr[0].x = 10; // 变量访问
(&arr[1])->y = 20; // 指针访问// 5. 嵌套结构体
struct Rectangle {struct Point top_left;struct Point bottom_right;
};struct Rectangle rect;
rect.top_left.x = 0; // 变量.变量
rect.top_left.y = 0;struct Rectangle* prect = ▭
prect->bottom_right.x = 100; // 指针->变量
prect->bottom_right.y = 100;// 6. 指针的指针
struct Point** pp = &p2;
(*pp)->x = 70; // 等价于 p2->x = 70// 7. 函数传递
void print_point_value(struct Point p) {printf("(%d, %d)\n", p.x, p.y); // 传值,复制整个结构体
}void print_point_pointer(const struct Point* p) {printf("(%d, %d)\n", p->x, p->y); // 传指针,高效
}print_point_value(p1); // 传值
print_point_pointer(&p1); // 传指针(推荐)// 8. 链表节点
struct Node {int data;struct Node* next;
};struct Node* head = malloc(sizeof(struct Node));
head->data = 100;
head->next = NULL;// 访问下一个节点
if (head->next != NULL) {int value = head->next->data;
}
效率对比:
// 大结构体
struct LargeStruct {char buffer[1024];int values[256];
};// 传值:慢(复制整个结构体)
void func1(struct LargeStruct s) {// s是副本
}// 传指针:快(只复制指针)
void func2(const struct LargeStruct* s) {// s指向原始数据// 使用const防止修改
}
Q: 结构体可以直接赋值吗?
A: 可以! C语言支持结构体直接赋值
struct Point {int x;int y;
};// 1. 直接赋值
struct Point p1 = {10, 20};
struct Point p2;
p2 = p1; // 正确!逐字节复制
printf("p2: (%d, %d)\n", p2.x, p2.y); // (10, 20)// 2. 初始化列表
struct Point p3 = {30, 40};
struct Point p4 = p3; // 初始化// 3. 返回结构体
struct Point create_point(int x, int y) {struct Point p = {x, y};return p; // 返回副本
}struct Point p5 = create_point(50, 60);// 4. 数组成员也会被复制
struct Data {int arr[100];char str[50];
};struct Data d1, d2;
// 初始化d1...
d2 = d1; // 整个数组都被复制!// 5. 包含指针的结构体 - 浅拷贝
struct Person {char* name;int age;
};struct Person p6;
p6.name = malloc(20);
strcpy(p6.name, "Alice");
p6.age = 25;struct Person p7 = p6; // 浅拷贝:name指针被复制,不是字符串内容
// p6.name和p7.name指向同一块内存!// 需要深拷贝
struct Person p8;
p8.name = strdup(p6.name); // 复制字符串内容
p8.age = p6.age;// 6. 比较结构体 - 不能直接比较!
if (p1 == p2) { // 错误!不能直接比较
}// 需要手动比较
int point_equal(struct Point* a, struct Point* b) {return a->x == b->x && a->y == b->y;
}// 或使用memcmp(注意填充字节)
if (memcmp(&p1, &p2, sizeof(struct Point)) == 0) {// 可能不可靠(填充字节未初始化)
}// 7. C99指定初始化
struct Point p9 = {.y = 100, .x = 50}; // 顺序任意// 8. 复合字面量(C99)
p2 = (struct Point){70, 80};// 函数调用
void process(struct Point p);
process((struct Point){90, 100}); // 临时对象
注意事项:
- ✓ 可以直接赋值(浅拷贝)
- ✓ 包括数组成员
- ✗ 不能直接比较(需要手动实现)
- ⚠️ 指针成员只复制指针,不复制内容
- ⚠️ 填充字节未初始化,memcmp可能不可靠
2. 联合体
Q: 联合体的内存布局?
A: 联合体(union): 所有成员共享同一块内存
// 1. 基本联合体
union Data {int i;float f;char str[20];
};sizeof(union Data); // 20 (最大成员的大小)// 内存布局:所有成员从同一地址开始
union Data d;
d.i = 10;
printf("%d\n", d.i); // 10
d.f = 3.14f;
printf("%f\n", d.f); // 3.14
printf("%d\n", d.i); // 不确定!被覆盖了// 2. 同一时间只能使用一个成员
union Number {int i_val;float f_val;double d_val;
};union Number num;
num.i_val = 42;
// 现在只有i_val有效
// f_val和d_val的值是未定义的// 3. 联合体的大小
union Size {char c; // 1字节short s; // 2字节int i; // 4字节double d; // 8字节
};
// sizeof(union Size) = 8 (最大成员的大小,可能有对齐)// 4. 对齐要求
union Aligned {char c; // 1字节对齐int i; // 4字节对齐
};
// 总大小:4字节 (按最严格的对齐要求)// 5. 访问不同类型的表示
union ByteView {int i;char bytes[4];
};union ByteView bv;
bv.i = 0x12345678;
printf("Bytes: %02x %02x %02x %02x\n",bv.bytes[0], bv.bytes[1], bv.bytes[2], bv.bytes[3]);
// 可以看到int的字节表示(大小端)// 6. 类型双关(type punning) - 查看浮点数的二进制
union FloatInt {float f;uint32_t i;
};union FloatInt fi;
fi.f = 3.14f;
printf("Float bits: 0x%08x\n", fi.i);
内存示意:
结构体:
struct S { [a][a][a][a][b][b][b][b][c][c][c][c]int a; ↑地址0 ↑地址4 ↑地址8int b;int c;
}; // 12字节联合体:
union U { [共享内存区域]int a; ↑地址0(所有成员都从这里开始)int b;int c;
}; // 4字节
Q: 联合体的应用场景?
A:
// 1. 节省内存(互斥数据)
struct Employee {char name[50];int id;char type; // 'H'=hourly, 'S'=salariedunion {float hourly_rate;float annual_salary;} payment;
};struct Employee emp;
strcpy(emp.name, "Alice");
emp.type = 'H';
emp.payment.hourly_rate = 25.50; // 只使用一个// 2. 类型转换和查看内存
union Converter {float f;int i;char bytes[4];
};// 查看浮点数的二进制表示
void print_float_bits(float f) {union Converter c;c.f = f;printf("Float: %f\n", f);printf("As int: 0x%08x\n", c.i);printf("Bytes: ");for (int i = 0; i < 4; i++) {printf("%02x ", (unsigned char)c.bytes[i]);}printf("\n");
}// 3. 网络协议(变长消息)
struct Message {int type;int length;union {char text[256];int numbers[64];struct {float x, y, z;} coordinates;} data;
};void send_message(struct Message* msg) {switch (msg->type) {case 1: /* 发送 text */ break;case 2: /* 发送 numbers */ break;case 3: /* 发送 coordinates */ break;}
}// 4. 实现变体类型(variant type)
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;struct Variant {ValueType type;union {int i;float f;char* s;} value;
};void print_variant(struct Variant* v) {switch (v->type) {case TYPE_INT:printf("%d\n", v->value.i);break;case TYPE_FLOAT:printf("%f\n", v->value.f);break;case TYPE_STRING:printf("%s\n", v->value.s);break;}
}// 使用
struct Variant v1 = {TYPE_INT, {.i = 42}};
struct Variant v2 = {TYPE_STRING, {.s = "hello"}};// 5. IP地址表示
union IPAddress {uint32_t addr;uint8_t bytes[4];
};union IPAddress ip;
ip.addr = 0xC0A80101; // 192.168.1.1
printf("%d.%d.%d.%d\n",ip.bytes[3], ip.bytes[2], ip.bytes[1], ip.bytes[0]);// 6. 大小端检测
int is_little_endian() {union {uint32_t i;uint8_t c[4];} test = {0x01020304};return test.c[0] == 0x04; // 小端:低字节在低地址
}// 7. 寄存器访问
union Register {uint32_t full;struct {uint16_t low;uint16_t high;} half;struct {uint8_t ll;uint8_t lh;uint8_t hl;uint8_t hh;} byte;
};union Register reg;
reg.full = 0x12345678;
printf("High word: 0x%04x\n", reg.half.high); // 0x1234
printf("Low byte: 0x%02x\n", reg.byte.ll); // 0x78
注意事项:
- 必须记住最后写入的是哪个成员
- 读取未写入的成员是未定义行为
- 通常与枚举或标志位配合使用
- 移植性问题(大小端、对齐)
Q: 枚举类型的作用?
A:
// 1. 基本枚举
enum Color {RED, // 0GREEN, // 1BLUE // 2
};enum Color c = RED;// 2. 指定值
enum Status {SUCCESS = 0,ERROR = -1,PENDING = 100
};// 3. 位标志
enum FileMode {READ = 1 << 0, // 0x01WRITE = 1 << 1, // 0x02EXECUTE = 1 << 2, // 0x04APPEND = 1 << 3 // 0x08
};int mode = READ | WRITE; // 0x03
if (mode & READ) {printf("可读\n");
}// 4. 替代魔数
// 不好的做法
if (status == 1) { /*...*/ }// 好的做法
enum HttpStatus {HTTP_OK = 200,HTTP_NOT_FOUND = 404,HTTP_SERVER_ERROR = 500
};if (status == HTTP_OK) { /*...*/ }// 5. 状态机
enum State {STATE_IDLE,STATE_RUNNING,STATE_PAUSED,STATE_STOPPED
};enum State current_state = STATE_IDLE;switch (current_state) {case STATE_IDLE:// 处理空闲状态break;case STATE_RUNNING:// 处理运行状态break;// ...
}// 6. 配合typedef
typedef enum {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
} Weekday;Weekday today = MONDAY;// 7. 枚举大小
enum Small { A, B, C };
sizeof(enum Small); // 通常是 sizeof(int) = 4// 8. 枚举转字符串
const char* color_to_string(enum Color c) {switch (c) {case RED: return "Red";case GREEN: return "Green";case BLUE: return "Blue";default: return "Unknown";}
}// 9. 使用宏自动生成
#define COLORS(X) \X(RED) \X(GREEN) \X(BLUE)#define ENUM_VALUE(name) name,
enum Color { COLORS(ENUM_VALUE) };#define STRING_VALUE(name) #name,
const char* color_strings[] = { COLORS(STRING_VALUE) };// 10. C23增强枚举(如果支持)
enum Color : uint8_t { // 指定底层类型RED = 1,GREEN,BLUE
};
优点:
- 提高代码可读性
- 编译时类型检查
- 易于维护
- 防止魔数
最佳实践:
- 用枚举替代魔数
- 命名清晰明确
- 位标志用移位定义
- 配合switch使用(编译器会检查遗漏)
七、预处理器
1. 宏定义
Q: 宏定义和函数的区别?
A:
特性 | 宏 | 函数 |
---|---|---|
处理时机 | 预处理(编译前) | 编译时 |
类型检查 | 无 | 有 |
调用开销 | 无(代码展开) | 有(函数调用) |
代码大小 | 可能增大 | 不变 |
调试 | 困难 | 容易 |
副作用 | 可能重复计算 | 无 |
作用域 | 全局(直到#undef) | 有作用域 |
// 1. 宏:文本替换
#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = MAX(3, 5);
// 预处理后:int x = ((3) > (5) ? (3) : (5));// 问题:副作用
int i = 1, j = 2;
int m = MAX(i++, j++); // i或j可能自增两次!
// 展开:int m = ((i++) > (j++) ? (i++) : (j++));// 2. 函数:正常调用
inline int max_func(int a, int b) {return a > b ? a : b;
}int m2 = max_func(i++, j++); // 安全,i和j各自增一次// 3. 宏的优势:泛型
#define SWAP(a, b, type) do { type temp = (a); (a) = (b); (b) = temp; } while(0)int x = 1, y = 2; SWAP(x, y, int);float f1 = 1.5, f2 = 2.5; SWAP(f1, f2, float);// 函数需要为每种类型定义 void swap_int(int* a, int* b) { int temp = *a; *a = *b; *b = temp; }// 4. 宏:可以操作不同类型 #define PRINT(x) printf("%d\n", (int)(x)) PRINT(10); // int PRINT(3.14); // double -> int// 5. 宏可以改变控制流 #define CHECK(x) if (!(x)) return -1int func() { CHECK(ptr != NULL); // 如果失败,直接返回 // ... }// 函数不能做到这点// 6. 性能对比 // 简单操作用宏(避免函数调用开销) #define SQUARE(x) ((x) * (x))// 复杂操作用函数(避免代码膨胀) int complex_calculation(int x) { // 100行代码... }
何时使用宏:
- ✓ 简单的常量和操作
- ✓ 需要泛型(C没有模板)
- ✓ 性能关键的小操作
- ✗ 复杂逻辑
- ✗ 有副作用的表达式
Q: 宏定义时为什么要加括号?
A: 避免运算符优先级问题
// 1. 错误示例:不加括号
#define SQUARE(x) x * xint a = SQUARE(1 + 2);
// 展开:int a = 1 + 2 * 1 + 2; // = 5,错误!
// 期望:int a = (1 + 2) * (1 + 2); // = 9// 正确:参数加括号
#define SQUARE(x) ((x) * (x))
int b = SQUARE(1 + 2);
// 展开:int b = ((1 + 2) * (1 + 2)); // = 9,正确!// 2. 整体加括号
#define ADD(a, b) a + bint c = ADD(1, 2) * 3;
// 展开:int c = 1 + 2 * 3; // = 7,错误!
// 期望:int c = (1 + 2) * 3; // = 9// 正确:整体加括号
#define ADD(a, b) ((a) + (b))
int d = ADD(1, 2) * 3;
// 展开:int d = ((1) + (2)) * 3; // = 9,正确!// 3. 复杂示例
#define MUL(a, b) a * bint e = 10 / MUL(2, 3);
// 展开:int e = 10 / 2 * 3; // = 15,错误!
// 期望:int e = 10 / (2 * 3); // = 1// 正确
#define MUL(a, b) ((a) * (b))
int f = 10 / MUL(2, 3);
// 展开:int f = 10 / ((2) * (3)); // = 1,正确!// 4. 指针和取地址
#define PTR(x) int* xPTR(a, b); // 展开:int* a, b; // a是指针,b是int!// 正确方式
#define PTR(x) int* (x)
// 或者用typedef
typedef int* IntPtr;
IntPtr a, b; // 都是指针// 5. 多条语句的宏
// 错误
#define SWAP_BAD(a, b, type) \type temp = a; \a = b; \b = temp;if (condition)SWAP_BAD(x, y, int);
// 展开后:
// if (condition)
// int temp = x; // 只有这一行在if内!
// x = y;
// y = temp;// 正确:使用do-while(0)
#define SWAP(a, b, type) do { \type temp = (a); \(a) = (b); \(b) = temp; \
} while(0)if (condition)SWAP(x, y, int); // 正确!整个语句块在if内
括号规则:
- 每个参数都加括号:
(x)
- 整体表达式加括号:
((x) + (y))
- 多条语句用do-while(0)包装
- 逗号表达式用小括号包围
Q: #
和 ##
运算符的作用?
A:
// 1. # 运算符:字符串化(Stringification)
#define TO_STRING(x) #xprintf("%s\n", TO_STRING(hello)); // 输出:hello
printf("%s\n", TO_STRING(123)); // 输出:123
printf("%s\n", TO_STRING(a + b)); // 输出:a + b// 应用:调试宏
#define DEBUG(x) printf(#x " = %d\n", x)int value = 42;
DEBUG(value); // 输出:value = 42
DEBUG(1+2); // 输出:1+2 = 3// 2. ## 运算符:标记连接(Token Pasting)
#define CONCAT(a, b) a##bint xy = 10;
int result = CONCAT(x, y); // 展开:int result = xy;
printf("%d\n", result); // 输出:10// 应用:生成变量名
#define DECLARE_VAR(type, name, suffix) \type name##suffixDECLARE_VAR(int, value, _1); // 展开:int value_1;
DECLARE_VAR(int, value, _2); // 展开:int value_2;// 3. 组合使用:生成getter/setter
#define PROPERTY(type, name) \type _##name; \type get_##name() { return _##name; } \void set_##name(type val) { _##name = val; }struct Point {PROPERTY(int, x)PROPERTY(int, y)
};
// 展开为:
// int _x;
// int get_x() { return _x; }
// void set_x(int val) { _x = val; }
// int _y;
// int get_y() { return _y; }
// void set_y(int val) { _y = val; }// 4. 生成函数名
#define MAKE_FUNC(name) \void func_##name() { \printf("Function: " #name "\n"); \}MAKE_FUNC(test) // 生成 func_test()
MAKE_FUNC(debug) // 生成 func_debug()func_test(); // 输出:Function: test
func_debug(); // 输出:Function: debug// 5. 生成枚举和字符串映射
#define ERROR_CODES(X) \X(SUCCESS, "Success") \X(ERROR_FILE, "File error") \X(ERROR_MEMORY, "Memory error") \X(ERROR_NETWORK, "Network error")// 生成枚举
#define ENUM_VALUE(name, str) name,
enum ErrorCode {ERROR_CODES(ENUM_VALUE)
};// 生成字符串数组
#define STRING_VALUE(name, str) str,
const char* error_strings[] = {ERROR_CODES(STRING_VALUE)
};printf("%s\n", error_strings[ERROR_FILE]); // File error// 6. 避免名称冲突
#define UNIQUE_NAME(base) base##__LINE__void func() {int UNIQUE_NAME(temp) = 10; // temp__123 (假设在第123行)int UNIQUE_NAME(temp) = 20; // temp__124
}// 7. 条件字符串化
#define LOG(level, msg) \printf("[" #level "] " msg "\n")LOG(INFO, "Program started"); // [INFO] Program started
LOG(ERROR, "Something wrong"); // [ERROR] Something wrong// 8. 类型安全的通用宏
#define MAX_TYPED(type) \type max_##type(type a, type b) { \return a > b ? a : b; \}MAX_TYPED(int) // 生成 int max_int(int a, int b)
MAX_TYPED(float) // 生成 float max_float(float a, float b)
MAX_TYPED(double) // 生成 double max_double(double a, double b)
注意事项:
#
会保留空格,##
会移除空格##
必须产生有效的标记- 字符串化会转义引号和反斜杠
- 宏参数不会展开(需要两层宏)
// 两层宏展开技巧
#define XSTR(x) #x
#define STR(x) XSTR(x)#define VALUE 100printf("%s\n", XSTR(VALUE)); // 输出:VALUE
printf("%s\n", STR(VALUE)); // 输出:100
Q: 常用的宏技巧?
A:
// 1. 数组大小
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))int numbers[] = {1, 2, 3, 4, 5};
int size = ARRAY_SIZE(numbers); // 5// 2. 结构体成员偏移
#define OFFSET_OF(type, member) \((size_t)&(((type*)0)->member))struct Point {int x;int y;
};size_t offset = OFFSET_OF(struct Point, y); // 4// 3. 容器获取
#define CONTAINER_OF(ptr, type, member) \((type*)((char*)(ptr) - OFFSET_OF(type, member)))// 4. MIN/MAX
#define MIN(a, b) ({ \typeof(a) _a = (a); \typeof(b) _b = (b); \_a < _b ? _a : _b; \
})#define MAX(a, b) ({ \typeof(a) _a = (a); \typeof(b) _b = (b); \_a > _b ? _a : _b; \
})// 避免参数重复计算
int x = 5;
int m = MIN(x++, 10); // x只自增一次// 5. 安全释放
#define SAFE_FREE(p) do { \if (p) { \free(p); \(p) = NULL; \} \
} while(0)char* str = malloc(100);
SAFE_FREE(str); // 释放并置NULL// 6. 错误检查
#define CHECK_NULL(p) do { \if (!(p)) { \fprintf(stderr, "NULL pointer at %s:%d\n", __FILE__, __LINE__); \return NULL; \} \
} while(0)void* func(void* ptr) {CHECK_NULL(ptr);// 使用ptr...
}// 7. 条件编译调试
#ifdef DEBUG#define DBG_PRINT(fmt, ...) \fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else#define DBG_PRINT(fmt, ...) do {} while(0)
#endifDBG_PRINT("Value: %d", 42); // DEBUG模式输出,否则不输出// 8. 静态断言(C11之前)
#define STATIC_ASSERT(expr, msg) \typedef char static_assert_##msg[(expr) ? 1 : -1]STATIC_ASSERT(sizeof(int) == 4, int_must_be_4_bytes);// 9. 位操作宏
#define SET_BIT(n, k) ((n) |= (1 << (k)))
#define CLEAR_BIT(n, k) ((n) &= ~(1 << (k)))
#define TOGGLE_BIT(n, k) ((n) ^= (1 << (k)))
#define CHECK_BIT(n, k) (((n) >> (k)) & 1)int flags = 0;
SET_BIT(flags, 3); // 设置第3位
CLEAR_BIT(flags, 2); // 清除第2位
TOGGLE_BIT(flags, 1); // 翻转第1位
if (CHECK_BIT(flags, 3)) {printf("Bit 3 is set\n");
}// 10. 循环展开
#define UNROLL_4(expr) \expr(0); expr(1); expr(2); expr(3);#define COPY_4(dest, src) UNROLL_4( \[&](int i) { dest[i] = src[i]; } \
)// 11. 类型安全的交换
#define SWAP(a, b) do { \typeof(a) temp = (a); \(a) = (b); \(b) = temp; \
} while(0)int x = 1, y = 2;
SWAP(x, y); // 自动使用int类型float f1 = 1.5, f2 = 2.5;
SWAP(f1, f2); // 自动使用float类型// 12. 编译时选择
#define IS_POWER_OF_2(x) (((x) != 0) && (((x) & ((x) - 1)) == 0))#if IS_POWER_OF_2(BUFFER_SIZE)// 优化的实现
#else// 通用的实现
#endif// 13. 字符串化连接
#define MAKE_NAME(prefix, name) prefix##_##name
#define MAKE_FUNC(name) MAKE_NAME(my, name)void MAKE_FUNC(test)() { // 生成 my_test()printf("Test function\n");
}// 14. 可变参数宏(C99)
#define LOG_ERROR(fmt, ...) \fprintf(stderr, "[ERROR] %s:%d: " fmt "\n", \__FILE__, __LINE__, ##__VA_ARGS__)LOG_ERROR("Failed to open file");
LOG_ERROR("Invalid value: %d", value);// 15. 作用域守卫
#define SCOPE_EXIT \__attribute__((cleanup(cleanup_func)))void cleanup_func(int* p) {printf("Cleanup called\n");// 清理资源
}void func() {SCOPE_EXIT int x = 0;// x超出作用域时自动调用cleanup_func
}
最佳实践:
- 宏名全大写
- 使用do-while(0)包装多语句宏
- 参数和整体都加括号
- 避免副作用
- 复杂逻辑用内联函数替代
- 添加注释说明宏的用途
2. 条件编译
Q: #ifdef
、#ifndef
、#if
的用法?
A:
// 1. #ifdef - 检查是否定义
#ifdef DEBUGprintf("Debug mode\n");
#endif// 等价于
#if defined(DEBUG)printf("Debug mode\n");
#endif// 2. #ifndef - 检查是否未定义
#ifndef MAX_SIZE#define MAX_SIZE 100
#endif// 3. #if - 条件表达式
#if MAX_SIZE > 1000#define USE_LARGE_BUFFER
#endif// 4. #else 和 #elif
#ifdef DEBUG#define LOG(x) printf(x)
#else#define LOG(x) do {} while(0)
#endif#if VERSION >= 2// V2代码
#elif VERSION == 1// V1代码
#else// 旧版本代码
#endif// 5. 多条件
#if defined(LINUX) && defined(X86_64)// Linux 64位特定代码
#endif#if defined(WINDOWS) || defined(MACOS)// Windows或Mac代码
#endif// 6. 取消定义
#define TEMP 100
#undef TEMP
// TEMP不再定义// 7. 平台检测
#ifdef _WIN32#include <windows.h>#define PATH_SEP '\\'
#elif defined(__linux__)#include <unistd.h>#define PATH_SEP '/'
#elif defined(__APPLE__)#include <TargetConditionals.h>#define PATH_SEP '/'
#else#error "Unsupported platform"
#endif// 8. 编译器检测
#ifdef __GNUC__#define COMPILER "GCC"
#elif defined(_MSC_VER)#define COMPILER "MSVC"
#elif defined(__clang__)#define COMPILER "Clang"
#endif// 9. 特性检测
#if __STDC_VERSION__ >= 199901L// C99或更新#include <stdbool.h>
#else// C89typedef enum { false, true } bool;
#endif// 10. 调试开关
#ifdef VERBOSE#define VLOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else#define VLOG(fmt, ...) do {} while(0)
#endifVLOG("Processing %d items\n", count); // 只在VERBOSE定义时输出// 11. 功能开关
#ifdef ENABLE_LOGGINGvoid log_message(const char* msg) {FILE* f = fopen("log.txt", "a");fprintf(f, "%s\n", msg);fclose(f);}
#elsevoid log_message(const char* msg) {// 什么也不做}
#endif// 12. 版本兼容
#if API_VERSION >= 2#define INIT_FUNC init_v2
#else#define INIT_FUNC init_v1
#endifvoid INIT_FUNC();// 13. 优化级别
#ifdef OPTIMIZE_SIZE#define INLINE static
#else#define INLINE static inline
#endif// 14. 断言开关
#ifdef NDEBUG#define assert(x) do {} while(0)
#else#define assert(x) \if (!(x)) { \fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \#x, __FILE__, __LINE__); \abort(); \}
#endif// 15. 内存调试
#ifdef DEBUG_MEMORY#define malloc(size) debug_malloc(size, __FILE__, __LINE__)#define free(ptr) debug_free(ptr, __FILE__, __LINE__)void* debug_malloc(size_t size, const char* file, int line);void debug_free(void* ptr, const char* file, int line);
#endif
常用预定义宏:
__FILE__ // 当前文件名
__LINE__ // 当前行号
__DATE__ // 编译日期
__TIME__ // 编译时间
__FUNCTION__ // 当前函数名(GCC)
__func__ // 当前函数名(C99)
__STDC__ // 是否符合ANSI C
__STDC_VERSION__ // C标准版本
__cplusplus // C++编译器
Q: 头文件防卫式声明(Header Guard)?
A:
// 1. 传统方法:#ifndef + #define
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H// 头文件内容
void my_function();struct MyStruct {int value;
};#endif // MYHEADER_H// 2. 为什么需要?
// file1.c
#include "myheader.h"
#include "otherheader.h" // 如果otherheader.h也包含myheader.h// 没有header guard会导致重复定义错误// 3. 命名约定
// utils.h
#ifndef UTILS_H
#define UTILS_H
// ...
#endif// project/module/config.h
#ifndef PROJECT_MODULE_CONFIG_H
#define PROJECT_MODULE_CONFIG_H
// ...
#endif// 4. #pragma once (现代替代方案)
// myheader.h
#pragma once // 更简洁,但不是标准C// 头文件内容
void my_function();// 5. 两者对比
/*
#ifndef + #define:
✓ 标准C,移植性好
✓ 完全控制
✗ 冗长
✗ 可能命名冲突#pragma once:
✓ 简洁
✓ 避免命名冲突
✗ 非标准(但广泛支持)
✗ 符号链接可能有问题
*/// 6. 最佳实践:两者结合
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H#pragma once // 优化编译速度// 头文件内容
void my_function();#endif // MYHEADER_H// 7. extern "C" (C/C++混合)
#ifndef MYLIB_H
#define MYLIB_H#ifdef __cplusplus
extern "C" {
#endif// C函数声明
void c_function();#ifdef __cplusplus
}
#endif#endif // MYLIB_H// 8. 嵌套包含问题
// a.h
#ifndef A_H
#define A_H
#include "b.h"
void func_a();
#endif// b.h
#ifndef B_H
#define B_H
#include "a.h" // 循环包含
void func_b();
#endif// 解决:前向声明
// a.h
#ifndef A_H
#define A_H
struct B; // 前向声明
void func_a(struct B* b);
#endif// 9. 条件包含
#ifndef CONFIG_H
#define CONFIG_H#ifdef ENABLE_FEATURE_A#include "feature_a.h"
#endif#ifdef ENABLE_FEATURE_B#include "feature_b.h"
#endif#endif // CONFIG_H// 10. 自动生成guard名
// 使用工具或IDE自动生成唯一名称
#ifndef UUID_A1B2C3D4_H
#define UUID_A1B2C3D4_H
// ...
#endif
常见错误:
// 错误1:忘记#endif
#ifndef HEADER_H
#define HEADER_H
// ...
// 缺少 #endif// 错误2:拼写错误
#ifndef HEADER_H
#define HEADRE_H // 拼写错误!
// ...
#endif// 错误3:guard名冲突
// file1.h
#ifndef COMMON_H // 太通用
#define COMMON_H
// ...
#endif// file2.h
#ifndef COMMON_H // 同名!
#define COMMON_H
// ...
#endif
最佳实践:
- 使用项目前缀避免冲突
- guard名与文件名对应
- 全大写,用下划线分隔
- 注释标明结尾
- 现代项目可以使用
#pragma once
Q: 预定义宏的使用?
A:
// 1. 文件和行信息
#define LOG(msg) \printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)LOG("Program started");
// 输出:[main.c:10] Program started// 2. 函数名
void my_function() {printf("In function: %s\n", __func__); // C99printf("In function: %s\n", __FUNCTION__); // GCC扩展
}// 3. 编译信息
printf("Compiled on %s at %s\n", __DATE__, __TIME__);
// 输出:Compiled on Oct 15 2025 at 14:30:22// 4. 断言宏
#define ASSERT(cond) do { \if (!(cond)) { \fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \#cond, __FILE__, __LINE__); \abort(); \} \
} while(0)ASSERT(ptr != NULL);// 5. 调试信息
#ifdef DEBUG#define DEBUG_PRINT(fmt, ...) \fprintf(stderr, "[%s:%s:%d] " fmt "\n", \__FILE__, __func__, __LINE__, ##__VA_ARGS__)
#else#define DEBUG_PRINT(fmt, ...) do {} while(0)
#endifDEBUG_PRINT("Value: %d", value);// 6. 版本检测
#if __STDC_VERSION__ >= 201112L// C11特性_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
#endif#if __STDC_VERSION__ >= 199901L// C99特性#include <stdbool.h>
#endif// 7. 平台检测
#ifdef _WIN32#define OS "Windows"
#elif defined(__linux__)#define OS "Linux"
#elif defined(__APPLE__)#define OS "macOS"
#else#define OS "Unknown"
#endifprintf("Operating System: %s\n", OS);// 8. 编译器检测
#ifdef __GNUC__printf("GCC version: %d.%d.%d\n",__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)printf("MSVC version: %d\n", _MSC_VER);
#endif// 9. 架构检测
#if defined(__x86_64__) || defined(_M_X64)#define ARCH "x86-64"
#elif defined(__i386__) || defined(_M_IX86)#define ARCH "x86"
#elif defined(__arm__) || defined(_M_ARM)#define ARCH "ARM"
#endif// 10. 优化提示
#ifdef __GNUC__#define LIKELY(x) __builtin_expect(!!(x), 1)#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else#define LIKELY(x) (x)#define UNLIKELY(x) (x)
#endifif (LIKELY(ptr != NULL)) {// 大概率执行
}if (UNLIKELY(error)) {// 小概率执行
}// 11. 计数器宏
#define UNIQUE_ID __COUNTER__ // GCC/Clangint array_##UNIQUE_ID; // 生成唯一名称// 12. 时间戳
printf("Build timestamp: %s %s\n", __DATE__, __TIME__);// 13. 标准合规
#ifdef __STDC__printf("ANSI C compliant\n");
#endif// 14. 编译环境信息
void print_build_info() {printf("Build Information:\n");printf(" File: %s\n", __FILE__);printf(" Date: %s\n", __DATE__);printf(" Time: %s\n", __TIME__);#ifdef __VERSION__printf(" Compiler: %s\n", __VERSION__);#endif#ifdef __STDC_VERSION__printf(" C Standard: %ld\n", __STDC_VERSION__);#endif
}// 15. 条件特性
#ifdef __STDC_NO_VLA__#error "Variable length arrays not supported"
#endif#ifdef __STDC_NO_ATOMICS__#warning "Atomic operations not supported"
#endif
常用预定义宏列表:
// 标准宏
__FILE__ // 文件名
__LINE__ // 行号
__DATE__ // 编译日期 "Oct 15 2025"
__TIME__ // 编译时间 "14:30:22"
__STDC__ // 1 (ANSI C)
__STDC_VERSION__ // C标准版本 (199901L=C99, 201112L=C11)
__STDC_HOSTED__ // 1=托管环境, 0=独立环境// GCC特定
__func__ // 函数名 (C99标准)
__FUNCTION__ // 函数名 (GCC扩展)
__PRETTY_FUNCTION__ // 带签名的函数名
__GNUC__ // GCC主版//
当然可以,以下是一份整理好的 “文件操作”知识总结文档(Markdown格式),内容涵盖你列出的所有问题:
八、文件操作
1. 文件基础
(1)文本文件与二进制文件的区别
对比项 | 文本文件(Text File) | 二进制文件(Binary File) |
---|---|---|
数据存储方式 | 以可读字符的形式存储(如ASCII码) | 以数据的原始二进制格式存储 |
可读性 | 可以用记事本等工具直接查看 | 通常不可直接阅读 |
文件大小 | 较大(因为换行符、编码转换) | 较小(数据按字节存储) |
读写效率 | 较低 | 较高 |
适用场景 | 存储文本内容(如配置文件、日志) | 存储结构化或大量数据(如图片、音频、程序数据) |
(2)fopen
的模式参数
模式 | 说明 |
---|---|
"r" | 以只读方式打开文件,文件必须存在。 |
"w" | 以写入方式打开文件,若文件存在则清空,不存在则创建。 |
"a" | 以追加方式打开文件,若文件不存在则创建。 |
"r+" | 以读写方式打开文件,文件必须存在。 |
"w+" | 以读写方式打开文件,若文件存在则清空,不存在则创建。 |
"a+" | 以读写追加方式打开文件,可读可写,不存在则创建。 |
"b" | 二进制模式(如 "rb" , "wb" ),可与上述模式组合使用。 |
💡 例如:
fopen("data.bin", "wb")
表示以二进制写模式打开文件。
(3)文件操作的基本流程
#include <stdio.h>int main() {FILE *fp; // 1. 定义文件指针fp = fopen("test.txt", "r"); // 2. 打开文件if (fp == NULL) { // 3. 判断是否打开成功perror("文件打开失败");return 1;}// 4. 进行读写操作char ch;while ((ch = fgetc(fp)) != EOF) {putchar(ch);}fclose(fp); // 5. 关闭文件return 0;
}
2. 文件函数
(1)fread
、fwrite
的使用
这两个函数常用于二进制文件的读写。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数 | 说明 |
---|---|
ptr | 存储读取数据的缓冲区指针(或写入数据的指针) |
size | 每个元素的字节数 |
nmemb | 元素个数 |
stream | 文件指针 |
示例:
struct Student {char name[20];int age;
};struct Student s = {"Tom", 18};
FILE *fp = fopen("stu.bin", "wb");
fwrite(&s, sizeof(struct Student), 1, fp);
fclose(fp);// 读取
fp = fopen("stu.bin", "rb");
fread(&s, sizeof(struct Student), 1, fp);
fclose(fp);
(2)fseek
、ftell
、rewind
的作用
函数 | 作用 |
---|---|
fseek(FILE *stream, long offset, int origin) | 移动文件指针位置。origin取值:SEEK_SET (文件开头)、SEEK_CUR (当前位置)、SEEK_END (文件末尾) |
ftell(FILE *stream) | 返回当前文件指针相对于文件开头的偏移量(单位:字节)。 |
rewind(FILE *stream) | 将文件指针重置到文件开头,相当于 fseek(stream, 0, SEEK_SET) 。 |
示例:
FILE *fp = fopen("test.txt", "r");
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
printf("文件大小: %ld 字节\n", size);
rewind(fp); // 回到文件开头
fclose(fp);
(3)fgets
和 gets
的区别
函数 | 特点 | 安全性 |
---|---|---|
gets(char *s) | 从标准输入读取一行字符,不检查缓冲区大小。 | ❌ 不安全,可能导致缓冲区溢出(C11已删除)。 |
fgets(char *s, int n, FILE *stream) | 从指定文件读取最多 n-1 个字符,自动加上 '\0' 。 | ✅ 安全,推荐使用。 |
示例:
char buf[50];
fgets(buf, sizeof(buf), stdin); // 从标准输入读取
printf("%s", buf);
(4)如何判断文件结束(EOF)
文件读取函数在到达文件末尾时,通常会返回特殊值 EOF
(End Of File)。
常用判断方式:
int ch;
FILE *fp = fopen("test.txt", "r");
while ((ch = fgetc(fp)) != EOF) {putchar(ch);
}
fclose(fp);
或使用 feof()
:
while (!feof(fp)) {ch = fgetc(fp);if (ch != EOF) putchar(ch);
}
⚠️ 注意:
feof()
只有在读取失败或到达文件末尾后才会返回真值。
📘 总结
操作 | 常用函数 |
---|---|
打开/关闭文件 | fopen() / fclose() |
读写文本 | fgetc() 、fgets() 、fputc() 、fputs() |
读写二进制 | fread() 、fwrite() |
文件定位 | fseek() 、ftell() 、rewind() |
文件结束检测 | feof() 、EOF |