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
cint a = 10;float b = a; // 隐式转换
-
显式转换: 程序员强制转换,使用强制类型转换运算符
cfloat f = 3.14;int i = (int)f; // 显式转换,i = 3
注意:隐式转换可能导致数据丢失或精度损失。
Q: void\*
指针的作用是什么?
A: void*
是通用指针类型:
- 可以指向任何类型的数据
- 不能直接解引用,需要先转换为具体类型
- 常用于
malloc
、memcpy
等函数 - 实现泛型编程的基础
c
void* ptr = malloc(10);
int* iptr = (int*)ptr; // 需要转换后使用
2. 变量与常量
Q: const
和 #define
的区别?
A:
特性 | const | #define |
---|---|---|
类型检查 | 有类型,编译器检查 | 无类型,只是文本替换 |
作用域 | 有作用域限制 | 无作用域概念 |
内存分配 | 分配内存 | 不分配内存 |
调试 | 可以调试 | 难以调试 |
指针 | 可以用指针指向 | 不能 |
c
const int a = 10; // 推荐使用
#define MAX 100 // 宏定义
Q: static
关键字的作用?(修饰局部变量、全局变量、函数)
A:
-
修饰局部变量: 延长生命周期到整个程序,但作用域不变
cvoid func() { static int count = 0; // 只初始化一次 count++; }
-
修饰全局变量/函数: 限制作用域为当前文件(内部链接)
cstatic int g_value = 100; // 仅本文件可见 static void helper() {} // 仅本文件可见
-
作用:
- 隐藏实现细节
- 避免命名冲突
- 保持状态
Q: extern
关键字的用法?
A: extern
声明外部变量或函数,表示定义在其他文件中:
c
// 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
c
volatile int flag = 0;
void interrupt_handler() {
flag = 1; // 中断中修改
}
while (!flag) {
// 不加volatile,编译器可能优化掉循环
}
Q: register
关键字还有用吗?
A: 基本没用了:
- 建议编译器将变量存储在寄存器中
- 现代编译器的优化能力远超人工指定
- 不能对
register
变量取地址 - 编译器可能忽略这个建议
建议: 不要使用,让编译器自己优化。
3. 运算符
Q: ++i
和 i++
的区别?
A:
++i
(前置): 先自增,再返回值i++
(后置): 先返回值,再自增
c
int i = 5;
int a = ++i; // i=6, a=6
int b = i++; // i=7, b=6
效率 : ++i
通常稍快,因为 i++
需要保存临时值。
Q: 位运算符的应用场景?
A:
-
判断奇偶 :
if (n & 1) // 奇数
-
交换变量
:
ca ^= 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:
- 优先级: 最低
- 功能: 从左到右计算表达式,返回最后一个表达式的值
c
int a = (1, 2, 3); // a = 3
int b = 1, 2, 3; // 错误!
for (i = 0, j = 10; i < j; i++, j--) {} // 常见用法
Q: sizeof
是运算符还是函数?
A: 运算符,不是函数:
- 编译时计算,不是运行时
- 不需要括号(但习惯上加)
- 参数不会被执行
c
int i = 0;
sizeof(i++); // i不会自增
printf("%d", i); // 输出0
二、指针与数组
1. 指针基础
Q: 什么是指针?指针的本质是什么?
A:
- 指针: 存储变量地址的变量
- 本质: 指针就是一个整数,存储内存地址
- 大小: 在32位系统占4字节,64位系统占8字节
c
int a = 10;
int* p = &a; // p存储a的地址
*p = 20; // 通过指针修改a的值
Q: 野指针、悬空指针、空指针的区别?
A:
-
空指针(NULL): 不指向任何有效内存
cint* p = NULL; // 安全的
-
野指针: 未初始化的指针,指向随机地址
cint* p; // 危险!指向未知位置 *p = 10; // 可能崩溃
-
悬空指针: 指向已释放的内存
cint* p = malloc(sizeof(int)); free(p); // p现在是悬空指针 p = NULL; // 应该置NULL
防范措施:
- 指针初始化为NULL
- free后立即置NULL
- 使用前检查指针是否为NULL
Q: 如何避免内存泄漏?
A:
-
配对使用 : 每个
malloc
对应一个free
-
及时释放: 不再使用时立即释放
-
避免丢失指针
:
cchar* p = malloc(100);p = malloc(200); // 错误!前面的100字节泄漏
-
使用工具: Valgrind、AddressSanitizer
-
良好习惯
:
- 谁分配谁释放
- 释放后置NULL
- 注意异常路径
2. 指针进阶
Q: 指向指针的指针(二级指针)的应用?
A: 二级指针的主要用途:
-
修改指针本身:
cvoid allocate(int** p) { *p = malloc(sizeof(int)); **p = 100; } int* ptr = NULL; allocate(&ptr); // 传递指针的地址
-
指针数组:
cchar* names[] = {"Alice", "Bob", "Charlie"}; char** p = names; // 指向字符串数组
-
动态二维数组:
cint** matrix = malloc(rows * sizeof(int*)); for (int i = 0; i < rows; i++) { matrix[i] = malloc(cols * sizeof(int)); }
Q: 函数指针的定义和使用?
A:
c
// 定义:返回类型 (*指针名)(参数列表)
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:
c
// 指针数组:数组的每个元素都是指针
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:
c
// 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: 虽然很相似,但有重要区别:
c
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)顺序连续存储:
c
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: 数组退化为指针:
c
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: 三种方法:
c
// 方法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)
│ - 可执行代码
│ - 只读,防止被意外修改
│
低地址
示例:
c
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:
c
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: 分配指定字节的内存,不初始化
cint* p = (int*)malloc(10 * sizeof(int)); // 内容未初始化,可能是垃圾值
-
calloc: 分配内存并初始化为0
cint* p = (int*)calloc(10, sizeof(int)); // 所有元素都是0
-
realloc: 调整已分配内存的大小
cint* 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个 | 调整大小 |
注意事项:
c
// 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,原因:
-
防止悬空指针:
cint* p = malloc(sizeof(int)); free(p); // p现在是悬空指针,指向已释放的内存 p = NULL; // 置NULL后,再次free不会出错
-
避免双重释放:
cfree(p); free(p); // 错误!导致程序崩溃 // 如果置NULL free(p); p = NULL; free(p); // 安全,free(NULL)什么都不做
-
便于判断:
cif (p != NULL) { // 安全使用p }
最佳实践:
c
#define SAFE_FREE(p) do { free(p); (p) = NULL; } while(0)
Q: 内存对齐的原因和作用?
A:
原因:
- 硬件要求: 某些CPU访问未对齐数据会崩溃或效率低
- 性能优化: 对齐访问速度更快(一次读取)
规则:
- 结构体成员按自身大小对齐
- 结构体总大小是最大成员的整数倍
- 编译器可能添加填充字节(padding)
示例:
c
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):
bashgcc -g program.c -o program valgrind --leak-check=full ./program
-
AddressSanitizer (GCC/Clang):
bashgcc -fsanitize=address -g program.c -o program ./program
-
Visual Studio (Windows):
- 使用内置的内存泄漏检测
- CRT调试库
-
手动跟踪:
c#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:
-
数组越界:
cint arr[10]; arr[10] = 100; // 越界!索引应该是0-9
-
字符串操作:
cchar str[5] = "hello"; // 错误!需要6字节(包括'\0') strcpy(dest, src); // dest空间不足
-
指针运算:
cint* p = malloc(10 * sizeof(int)); p[20] = 100; // 越界访问
-
栈溢出:
cvoid func() { int huge[1000000]; // 栈空间不足 }
-
未初始化指针:
cint* p; // 野指针 *p = 10; // 访问未知内存
Q: 缓冲区溢出的原理和防范?
A:
原理: 写入的数据超过缓冲区大小,覆盖相邻内存
经典漏洞:
c
void vulnerable(char* input) {
char buffer[64];
strcpy(buffer, input); // 危险!input可能超过64字节
// 可能覆盖返回地址,执行恶意代码
}
防范措施:
-
使用安全函数:
c// 不安全 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);
-
边界检查:
cif (strlen(src) < sizeof(dest)) { strcpy(dest, src); }
-
编译器保护:
bashgcc -fstack-protector-all program.c # 栈保护 gcc -D_FORTIFY_SOURCE=2 program.c # 函数安全检查
-
静态分析工具: 使用 Coverity, Clang Static Analyzer
-
代码审查: 重点检查字符串和数组操作
Q: 如何安全地使用字符串函数?
A:
c
// 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)
c
// 基本类型 - 传递副本
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:
c
// 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: 严重错误! 返回的是悬空指针
c
// 错误示例
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>
中的宏
c
#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: 回调函数: 通过函数指针传递给另一个函数的函数
c
// 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:
优点:
- 代码简洁,逻辑清晰
- 天然适合树、图等递归结构
- 某些问题用递归更自然
缺点:
- 栈空间消耗大
- 可能导致栈溢出
- 效率较低(重复计算)
c
// 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:
内联函数: 建议编译器在调用处展开函数体,减少函数调用开销
c
// 定义内联函数
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
现代用法:
c
// C99及以后
static inline int square(int x) {
return x * x;
}
Q: 函数指针数组的应用场景?
A:
c
// 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:
c
// 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
c
// 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:
c
#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:
c
// 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) {
// 使用copy
free(copy);
}
// 对比表
/*
函数 安全性 自动添加'\0' 需要手动处理
strcpy 低 是 检查目标大小
strncpy 中 否(当截断时) 手动添加'\0'
snprintf 高 是 无
strcpy_s 高 是 需要C11支持
*/
最佳实践:
c
// 推荐方法
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:
c
#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:
c
// 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
- 每个成员按其自身大小对齐
- 结构体总大小是最大成员的整数倍
c
#include <stddef.h> // offsetof
// 示例1:无优化
struct A {
char c; // 1字节, offset=0
int 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=0
short s; // 2字节, offset=4
char 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=0
struct 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=0
char c; // 1字节, offset=8
int i; // 4字节, offset=12
short s; // 2字节, offset=16
char 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位)对齐
指定对齐方式:
c
// 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: 位域: 以位为单位分配的结构体成员
c
// 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 0
unsigned int mode : 2; // bit 1-2
unsigned int reserved : 5; // bit 3-7
unsigned int priority : 4; // bit 8-11
unsigned 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:
c
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;
}
效率对比:
c
// 大结构体
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语言支持结构体直接赋值
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): 所有成员共享同一块内存
c
// 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 ↑地址8
int b;
int c;
}; // 12字节
联合体:
union U { [共享内存区域]
int a; ↑地址0(所有成员都从这里开始)
int b;
int c;
}; // 4字节
Q: 联合体的应用场景?
A:
c
// 1. 节省内存(互斥数据)
struct Employee {
char name[50];
int id;
char type; // 'H'=hourly, 'S'=salaried
union {
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:
c
// 1. 基本枚举
enum Color {
RED, // 0
GREEN, // 1
BLUE // 2
};
enum Color c = RED;
// 2. 指定值
enum Status {
SUCCESS = 0,
ERROR = -1,
PENDING = 100
};
// 3. 位标志
enum FileMode {
READ = 1 << 0, // 0x01
WRITE = 1 << 1, // 0x02
EXECUTE = 1 << 2, // 0x04
APPEND = 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) | 有作用域 |
c
// 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 -1
int func() { CHECK(ptr != NULL); // 如果失败,直接返回 // ... }
// 函数不能做到这点
// 6. 性能对比 // 简单操作用宏(避免函数调用开销) #define SQUARE(x) ((x) * (x))
// 复杂操作用函数(避免代码膨胀) int complex_calculation(int x) { // 100行代码... }
何时使用宏:
- ✓ 简单的常量和操作
- ✓ 需要泛型(C没有模板)
- ✓ 性能关键的小操作
- ✗ 复杂逻辑
- ✗ 有副作用的表达式
Q: 宏定义时为什么要加括号?
A: 避免运算符优先级问题
c
// 1. 错误示例:不加括号
#define SQUARE(x) x * x
int 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 + b
int 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 * b
int 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* x
PTR(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:
c
// 1. # 运算符:字符串化(Stringification)
#define TO_STRING(x) #x
printf("%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##b
int xy = 10;
int result = CONCAT(x, y); // 展开:int result = xy;
printf("%d\n", result); // 输出:10
// 应用:生成变量名
#define DECLARE_VAR(type, name, suffix) \
type name##suffix
DECLARE_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)
注意事项:
#
会保留空格,##
会移除空格##
必须产生有效的标记- 字符串化会转义引号和反斜杠
- 宏参数不会展开(需要两层宏)
c
// 两层宏展开技巧
#define XSTR(x) #x
#define STR(x) XSTR(x)
#define VALUE 100
printf("%s\n", XSTR(VALUE)); // 输出:VALUE
printf("%s\n", STR(VALUE)); // 输出:100
Q: 常用的宏技巧?
A:
c
// 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)
#endif
DBG_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:
c
// 1. #ifdef - 检查是否定义
#ifdef DEBUG
printf("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
// C89
typedef enum { false, true } bool;
#endif
// 10. 调试开关
#ifdef VERBOSE
#define VLOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define VLOG(fmt, ...) do {} while(0)
#endif
VLOG("Processing %d items\n", count); // 只在VERBOSE定义时输出
// 11. 功能开关
#ifdef ENABLE_LOGGING
void log_message(const char* msg) {
FILE* f = fopen("log.txt", "a");
fprintf(f, "%s\n", msg);
fclose(f);
}
#else
void log_message(const char* msg) {
// 什么也不做
}
#endif
// 12. 版本兼容
#if API_VERSION >= 2
#define INIT_FUNC init_v2
#else
#define INIT_FUNC init_v1
#endif
void 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
常用预定义宏:
c
__FILE__ // 当前文件名
__LINE__ // 当前行号
__DATE__ // 编译日期
__TIME__ // 编译时间
__FUNCTION__ // 当前函数名(GCC)
__func__ // 当前函数名(C99)
__STDC__ // 是否符合ANSI C
__STDC_VERSION__ // C标准版本
__cplusplus // C++编译器
Q: 头文件防卫式声明(Header Guard)?
A:
c
// 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
常见错误:
c
// 错误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:
c
// 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__); // C99
printf("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)
#endif
DEBUG_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"
#endif
printf("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)
#endif
if (LIKELY(ptr != NULL)) {
// 大概率执行
}
if (UNLIKELY(error)) {
// 小概率执行
}
// 11. 计数器宏
#define UNIQUE_ID __COUNTER__ // GCC/Clang
int 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
常用预定义宏列表:
c
// 标准宏
__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)文件操作的基本流程
c
#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
的使用
这两个函数常用于二进制文件的读写。
c
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 |
文件指针 |
示例:
c
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) 。 |
示例:
c
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' 。 |
✅ 安全,推荐使用。 |
示例:
c
char buf[50];
fgets(buf, sizeof(buf), stdin); // 从标准输入读取
printf("%s", buf);
(4)如何判断文件结束(EOF)
文件读取函数在到达文件末尾时,通常会返回特殊值 EOF
(End Of File)。
常用判断方式:
c
int ch;
FILE *fp = fopen("test.txt", "r");
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
或使用 feof()
:
c
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 |
九、常见算法题
1. 字符串处理
字符串反转
c
// 方法1: 使用额外空间
void reverseString1(char *str) {
if (str == NULL) return;
int len = strlen(str);
char *temp = (char*)malloc(len + 1);
for (int i = 0; i < len; i++) {
temp[i] = str[len - 1 - i];
}
temp[len] = '\0';
strcpy(str, temp);
free(temp);
}
// 方法2: 原地反转(推荐)
void reverseString2(char *str) {
if (str == NULL) return;
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
char temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
判断回文字符串
c
// 方法1: 双指针法
int isPalindrome(const char *str) {
if (str == NULL) return 0;
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
if (str[left] != str[right]) {
return 0;
}
left++;
right--;
}
return 1;
}
// 方法2: 忽略大小写和非字母字符
int isPalindromeAdvanced(const char *str) {
if (str == NULL) return 0;
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !isalnum(str[left])) left++;
while (left < right && !isalnum(str[right])) right--;
if (tolower(str[left]) != tolower(str[right])) {
return 0;
}
left++;
right--;
}
return 1;
}
字符串查找和替换
c
// 查找子串(KMP算法简化版)
char* findSubstring(const char *str, const char *substr) {
if (str == NULL || substr == NULL) return NULL;
return strstr(str, substr); // 使用库函数
}
// 手动实现查找
char* findSubstringManual(const char *str, const char *substr) {
if (str == NULL || substr == NULL) return NULL;
int len1 = strlen(str);
int len2 = strlen(substr);
for (int i = 0; i <= len1 - len2; i++) {
int j;
for (j = 0; j < len2; j++) {
if (str[i + j] != substr[j]) break;
}
if (j == len2) return (char*)(str + i);
}
return NULL;
}
// 字符串替换
char* replaceString(const char *str, const char *old, const char *new) {
if (str == NULL || old == NULL || new == NULL) return NULL;
int count = 0;
const char *temp = str;
// 计算需要替换的次数
while ((temp = strstr(temp, old)) != NULL) {
count++;
temp += strlen(old);
}
// 分配新内存
int newLen = strlen(str) + count * (strlen(new) - strlen(old));
char *result = (char*)malloc(newLen + 1);
char *current = result;
temp = str;
while (*temp) {
if (strstr(temp, old) == temp) {
strcpy(current, new);
current += strlen(new);
temp += strlen(old);
} else {
*current++ = *temp++;
}
}
*current = '\0';
return result;
}
字符串转整数(atoi)
c
int myAtoi(const char *str) {
if (str == NULL) return 0;
// 跳过前导空格
while (*str == ' ') str++;
// 处理符号
int sign = 1;
if (*str == '+' || *str == '-') {
sign = (*str == '-') ? -1 : 1;
str++;
}
// 转换数字
long result = 0;
while (*str >= '0' && *str <= '9') {
result = result * 10 + (*str - '0');
// 处理溢出
if (result * sign > INT_MAX) return INT_MAX;
if (result * sign < INT_MIN) return INT_MIN;
str++;
}
return (int)(result * sign);
}
2. 数组处理
数组去重
c
// 方法1: 对于已排序数组
int removeDuplicatesSorted(int arr[], int n) {
if (n == 0) return 0;
int uniqueIndex = 0;
for (int i = 1; i < n; i++) {
if (arr[i] != arr[uniqueIndex]) {
uniqueIndex++;
arr[uniqueIndex] = arr[i];
}
}
return uniqueIndex + 1;
}
// 方法2: 对于未排序数组(使用哈希)
int removeDuplicatesUnsorted(int arr[], int n) {
if (n == 0) return 0;
int uniqueIndex = 0;
for (int i = 0; i < n; i++) {
int j;
for (j = 0; j < uniqueIndex; j++) {
if (arr[i] == arr[j]) break;
}
if (j == uniqueIndex) {
arr[uniqueIndex++] = arr[i];
}
}
return uniqueIndex;
}
查找最大最小值
c
void findMinMax(int arr[], int n, int *min, int *max) {
if (arr == NULL || n <= 0) return;
*min = *max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
// 优化版本: 成对比较,减少比较次数
void findMinMaxOptimized(int arr[], int n, int *min, int *max) {
if (arr == NULL || n <= 0) return;
int i;
if (n % 2 == 0) {
*min = (arr[0] < arr[1]) ? arr[0] : arr[1];
*max = (arr[0] > arr[1]) ? arr[0] : arr[1];
i = 2;
} else {
*min = *max = arr[0];
i = 1;
}
for (; i < n - 1; i += 2) {
if (arr[i] < arr[i + 1]) {
if (arr[i] < *min) *min = arr[i];
if (arr[i + 1] > *max) *max = arr[i + 1];
} else {
if (arr[i + 1] < *min) *min = arr[i + 1];
if (arr[i] > *max) *max = arr[i];
}
}
}
数组排序
冒泡排序
c
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int swapped = 0;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
if (!swapped) break; // 优化: 已经有序
}
}
// 时间复杂度: O(n²), 空间复杂度: O(1)
// 稳定排序
选择排序
c
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
// 时间复杂度: O(n²), 空间复杂度: O(1)
// 不稳定排序
快速排序
c
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 时间复杂度: 平均O(n log n), 最坏O(n²)
// 空间复杂度: O(log n)
// 不稳定排序
二分查找
c
// 迭代版本
int binarySearch(int arr[], int n, int target) {
int left = 0;
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
// 递归版本
int binarySearchRecursive(int arr[], int left, int right, int target) {
if (left > right) return -1;
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) return binarySearchRecursive(arr, mid + 1, right, target);
return binarySearchRecursive(arr, left, mid - 1, target);
}
// 时间复杂度: O(log n)
// 前提: 数组必须有序
3. 链表操作
链表的创建、插入、删除
c
typedef struct Node {
int data;
struct Node *next;
} Node;
// 创建新节点
Node* createNode(int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
return NULL;
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 头部插入
void insertAtHead(Node **head, int data) {
Node *newNode = createNode(data);
if (newNode == NULL) return;
newNode->next = *head;
*head = newNode;
}
// 尾部插入
void insertAtTail(Node **head, int data) {
Node *newNode = createNode(data);
if (newNode == NULL) return;
if (*head == NULL) {
*head = newNode;
return;
}
Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
// 指定位置插入
void insertAtPosition(Node **head, int data, int position) {
if (position == 0) {
insertAtHead(head, data);
return;
}
Node *newNode = createNode(data);
if (newNode == NULL) return;
Node *temp = *head;
for (int i = 0; i < position - 1 && temp != NULL; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("位置超出链表长度\n");
free(newNode);
return;
}
newNode->next = temp->next;
temp->next = newNode;
}
// 删除指定值的节点
void deleteNode(Node **head, int data) {
if (*head == NULL) return;
// 删除头节点
if ((*head)->data == data) {
Node *temp = *head;
*head = (*head)->next;
free(temp);
return;
}
Node *current = *head;
while (current->next != NULL && current->next->data != data) {
current = current->next;
}
if (current->next == NULL) {
printf("未找到该节点\n");
return;
}
Node *temp = current->next;
current->next = current->next->next;
free(temp);
}
// 释放链表
void freeList(Node **head) {
Node *current = *head;
while (current != NULL) {
Node *next = current->next;
free(current);
current = next;
}
*head = NULL;
}
链表反转
c
// 迭代法
Node* reverseList(Node *head) {
Node *prev = NULL;
Node *current = head;
Node *next = NULL;
while (current != NULL) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转当前节点的指针
prev = current; // 移动prev
current = next; // 移动current
}
return prev;
}
// 递归法
Node* reverseListRecursive(Node *head) {
if (head == NULL || head->next == NULL) {
return head;
}
Node *newHead = reverseListRecursive(head->next);
head->next->next = head;
head->next = NULL;
return newHead;
}
判断链表是否有环
c
// 快慢指针法(Floyd判圈算法)
int hasCycle(Node *head) {
if (head == NULL) return 0;
Node *slow = head;
Node *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return 1; // 有环
}
}
return 0; // 无环
}
// 找到环的起始节点
Node* detectCycleStart(Node *head) {
if (head == NULL) return NULL;
Node *slow = head;
Node *fast = head;
// 判断是否有环
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
// 找到环的起始点
Node *start = head;
while (start != slow) {
start = start->next;
slow = slow->next;
}
return start;
}
}
return NULL;
}
合并两个有序链表
c
// 迭代法
Node* mergeTwoLists(Node *l1, Node *l2) {
Node dummy;
dummy.next = NULL;
Node *tail = &dummy;
while (l1 != NULL && l2 != NULL) {
if (l1->data <= l2->data) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
tail->next = (l1 != NULL) ? l1 : l2;
return dummy.next;
}
// 递归法
Node* mergeTwoListsRecursive(Node *l1, Node *l2) {
if (l1 == NULL) return l2;
if (l2 == NULL) return l1;
if (l1->data <= l2->data) {
l1->next = mergeTwoListsRecursive(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoListsRecursive(l1, l2->next);
return l2;
}
}
4. 位操作
判断奇偶数
c
int isOdd(int n) {
return n & 1; // 最低位为1则为奇数
}
int isEven(int n) {
return !(n & 1); // 最低位为0则为偶数
}
交换两个数(不用临时变量)
c
// 方法1: 异或法(推荐)
void swapXOR(int *a, int *b) {
if (a != b) { // 防止a和b指向同一地址
*a = *a ^ *b;
*b = *a ^ *b;
*c = *a ^ *b;
}
}
// 方法2: 加减法(可能溢出)
void swapAddSub(int *a, int *b) {
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
// 方法3: 乘除法(不能有0,可能溢出)
void swapMulDiv(int *a, int *b) {
if (*a != 0 && *b != 0) {
*a = *a * *b;
*b = *a / *b;
*a = *a / *b;
}
}
统计二进制中1的个数
c
// 方法1: 逐位检查
int countOnes1(unsigned int n) {
int count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
// 方法2: Brian Kernighan算法(更高效)
int countOnes2(unsigned int n) {
int count = 0;
while (n) {
n &= (n - 1); // 每次清除最右边的1
count++;
}
return count;
}
// 方法3: 查表法(最快)
int countOnes3(unsigned int n) {
int table[256];
// 预先计算0-255每个数的1的个数
for (int i = 0; i < 256; i++) {
table[i] = (i & 1) + table[i / 2];
}
int count = 0;
count += table[n & 0xFF];
count += table[(n >> 8) & 0xFF];
count += table[(n >> 16) & 0xFF];
count += table[(n >> 24) & 0xFF];
return count;
}
找出数组中唯一出现一次的数
c
// 其他数都出现两次,找出现一次的数
int singleNumber(int arr[], int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result ^= arr[i]; // 相同的数异或为0
}
return result;
}
// 其他数都出现三次,找出现一次的数
int singleNumberThree(int arr[], int n) {
int ones = 0, twos = 0;
for (int i = 0; i < n; i++) {
twos |= ones & arr[i];
ones ^= arr[i];
int threes = ones & twos;
ones &= ~threes;
twos &= ~threes;
}
return ones;
}
// 两个数只出现一次,其他都出现两次
void singleNumberTwo(int arr[], int n, int *num1, int *num2) {
int xorResult = 0;
for (int i = 0; i < n; i++) {
xorResult ^= arr[i];
}
// 找到xorResult中为1的最低位
int rightmostBit = xorResult & (-xorResult);
*num1 = 0;
*num2 = 0;
for (int i = 0; i < n; i++) {
if (arr[i] & rightmostBit) {
*num1 ^= arr[i];
} else {
*num2 ^= arr[i];
}
}
}
十、编程规范与调试
1. 编程规范
命名规范
变量命名
c
// 好的命名
int studentCount; // 驼峰命名
int student_count; // 下划线命名
int MAX_BUFFER_SIZE = 1024; // 常量大写
// 不好的命名
int a, b, c; // 无意义
int StudentCount; // 大写开头易混淆
int student_Count; // 混合风格
函数命名
c
// 好的命名
void calculateAverage(); // 动词开头,描述功能
int getUserInput();
void printErrorMessage();
// 不好的命名
void func1(); // 无意义
void PRINT(); // 全大写应该用于宏
宏命名
c
// 宏定义用全大写加下划线
#define MAX_SIZE 100
#define PI 3.14159
#define SQUARE(x) ((x) * (x)) // 注意加括号
// 避免
#define max_size 100 // 小写易混淆
#define Square(x) x*x // 缺少括号会出错
注释的重要性
c
/*
* 文件级注释
* 文件名: student.c
* 作者: Zhang San
* 创建日期: 2025-01-15
* 描述: 学生管理系统的核心功能实现
*/
/**
* @brief 计算数组的平均值
* @param arr 输入数组
* @param n 数组大小
* @return 平均值,如果n为0返回0
*/
double calculateAverage(int arr[], int n) {
if (n == 0) return 0.0; // 边界条件处理
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i]; // 累加求和
}
return (double)sum / n; // 注意类型转换
}
// 单行注释用于简单说明
int count = 0; // 计数器初始化
/*
* 多行注释用于复杂逻辑说明
* 这里使用快速排序算法
* 时间复杂度O(n log n)
*/
注释原则:
- 注释解释"为什么",代码说明"是什么"
- 复杂算法必须注释
- 公共接口必须有文档注释
- 避免无用注释(如:
i++; // i自增
) - 及时更新注释,避免注释与代码不一致
代码缩进和格式
c
// K&R风格(推荐)
int main() {
if (condition) {
// code
} else {
// code
}
return 0;
}
// Allman风格
int main()
{
if (condition)
{
// code
}
else
{
// code
}
return 0;
}
// 对齐和空格
int a = 10; // 赋值对齐
int count = 0;
int maxValue = 100;
// 运算符周围加空格
result = a + b * c; // 好
result=a+b*c; // 差
// 逗号后加空格
function(a, b, c); // 好
function(a,b,c); // 差
// 一行不要太长(80或120字符)
// 必要时换行
int result = veryLongFunctionName(parameter1, parameter2,
parameter3, parameter4);
魔数(Magic Number)的危害
c
// 不好的做法 - 使用魔数
void processData() {
char buffer[512]; // 512是什么?
for (int i = 0; i < 100; i++) { // 100是什么?
// ...
}
if (status == 3) { // 3代表什么?
// ...
}
}
// 好的做法 - 使用常量
#define BUFFER_SIZE 512
#define MAX_STUDENTS 100
#define STATUS_ERROR 3
void processData() {
char buffer[BUFFER_SIZE];
for (int i = 0; i < MAX_STUDENTS; i++) {
// ...
}
if (status == STATUS_ERROR) {
// ...
}
}
// 或使用枚举
enum Status {
STATUS_SUCCESS = 0,
STATUS_WARNING = 1,
STATUS_ERROR = 2,
STATUS_CRITICAL = 3
};
魔数的危害:
- 降低代码可读性
- 难以维护(需要修改所有出现的地方)
- 容易出错(数字容易记错)
- 缺乏语义(不知道数字含义)
2. 调试技巧
printf 调试法
c
void debugExample() {
int x = 10;
int y = 20;
// 基本调试输出
printf("Debug: x = %d, y = %d\n", x, y);
// 输出文件名和行号
printf("[%s:%d] x = %d\n", __FILE__, __LINE__, x);
// 输出函数名
printf("[%s] Entering function\n", __func__);
// 带时间戳的调试(需要time.h)
time_t now = time(NULL);
printf("[%s] Debug info\n", ctime(&now));
// 条件调试
#ifdef DEBUG
printf("Debug mode: x = %d\n", x);
#endif
}
// 调试宏
#define DEBUG_PRINT(fmt, ...) \
do { \
fprintf(stderr, "[%s:%d:%s] " fmt "\n", \
__FILE__, __LINE__, __func__, ##__VA_ARGS__); \
} while(0)
// 使用
DEBUG_PRINT("x = %d, y = %d", x, y);
assert 断言的使用
c
#include <assert.h>
void processArray(int *arr, int size) {
// 断言检查前置条件
assert(arr != NULL); // 确保指针非空
assert(size > 0); // 确保大小有效
for (int i = 0; i < size; i++) {
assert(i >= 0 && i < size); // 检查索引范围
arr[i] *= 2;
}
}
int divide(int a, int b) {
assert(b != 0); // 确保除数不为0
return a / b;
}
// 断言只在调试模式有效
// 发布版本可以用 #define NDEBUG 禁用
// 编译: gcc -DNDEBUG program.c
断言使用原则:
- 用于检查不应该发生的条件
- 不要用于正常的错误处理
- 不要有副作用(assert内不要修改变量)
- 发布版本会被禁用,不影响性能
GDB 调试器的基本命令
bash
# 编译时加上-g选项
gcc -g program.c -o program
# 启动GDB
gdb program
# 基本命令
(gdb) run [args] # 运行程序
(gdb) break main # 在main函数设置断点
(gdb) break file.c:10 # 在文件第10行设置断点
(gdb) info breakpoints # 查看所有断点
(gdb) delete 1 # 删除1号断点
# 执行控制
(gdb) step # 单步执行(进入函数)
(gdb) next # 单步执行(不进入函数)
(gdb) continue # 继续执行到下一个断点
(gdb) finish # 执行到当前函数返回
查看变量
(gdb) print x # 打印变量x的值
(gdb) print *ptr # 打印指针指向的值
(gdb) print arr[0]@10 # 打印数组前10个元素
(gdb) display x # 每次停止时自动显示x
(gdb) info locals # 显示所有局部变量
查看内存
(gdb) x/10x ptr # 以16进制显示10个内存单元
(gdb) x/s str # 以字符串格式显示
查看调用栈
(gdb) backtrace # 显示调用栈
(gdb) frame 2 # 切换到2号栈帧
(gdb) up # 上移一个栈帧
(gdb) down # 下移一个栈帧
查看代码
(gdb) list # 显示当前位置代码
(gdb) list 10 # 显示第10行附近代码
(gdb) list function # 显示函数代码
条件断点
(gdb) break 10 if x > 5 # 当x>5时在第10行中断
监视点
(gdb) watch x # 当x值改变时中断
其他
(gdb) help # 帮助
(gdb) quit # 退出GDB