C语言面试题问题+答案(claude生成)

C语言面试题库大全(含答案)

一、基础语法类

1. 数据类型

Q: charshortintlongfloatdouble 各占多少字节?

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: signedunsigned 的区别是什么?

A:

  • signed(有符号): 可以表示正数、负数和零,最高位是符号位
  • unsigned(无符号): 只能表示非负数,所有位都用来表示数值

例如:

  • signed char: -128 ~ 127
  • unsigned char: 0 ~ 255
  • signed int: -2^31 ~ 2^31-1
  • unsigned int: 0 ~ 2^32-1

Q: 什么是类型转换?隐式转换和显式转换的区别?

A:

  • 隐式转换: 编译器自动进行,如 int 转 float

    c 复制代码
    int a = 10;float b = a;  // 隐式转换
  • 显式转换: 程序员强制转换,使用强制类型转换运算符

    c 复制代码
    float f = 3.14;int i = (int)f;  // 显式转换,i = 3

注意:隐式转换可能导致数据丢失或精度损失。


Q: void\* 指针的作用是什么?

A: void* 是通用指针类型:

  • 可以指向任何类型的数据
  • 不能直接解引用,需要先转换为具体类型
  • 常用于 mallocmemcpy 等函数
  • 实现泛型编程的基础
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:

  1. 修饰局部变量: 延长生命周期到整个程序,但作用域不变

    c 复制代码
    void func() {
        static int count = 0;  // 只初始化一次
        count++;
    }
  2. 修饰全局变量/函数: 限制作用域为当前文件(内部链接)

    c 复制代码
    static int g_value = 100;  // 仅本文件可见
    static void helper() {}    // 仅本文件可见
  3. 作用:

    • 隐藏实现细节
    • 避免命名冲突
    • 保持状态

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 告诉编译器变量可能被意外改变,不要进行优化:

使用场景:

  1. 硬件寄存器 : volatile int* reg = (volatile int*)0x40000000;
  2. 中断服务程序: 共享变量
  3. 多线程: 信号量标志
  4. MMIO: 内存映射I/O
c 复制代码
volatile int flag = 0;

void interrupt_handler() {
    flag = 1;  // 中断中修改
}

while (!flag) {
    // 不加volatile,编译器可能优化掉循环
}

Q: register 关键字还有用吗?

A: 基本没用了:

  • 建议编译器将变量存储在寄存器中
  • 现代编译器的优化能力远超人工指定
  • 不能对 register 变量取地址
  • 编译器可能忽略这个建议

建议: 不要使用,让编译器自己优化。


3. 运算符

Q: ++ii++ 的区别?

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:

  1. 判断奇偶 : if (n & 1) // 奇数

  2. 交换变量

    :

    c 复制代码
    a ^= b;b ^= a;a ^= b;
  3. 清零特定位 : n &= ~(1 << k)

  4. 设置特定位 : n |= (1 << k)

  5. 乘除2的幂 : n << 1 (乘2), n >> 1 (除2)

  6. 取绝对值 : (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:

  1. 空指针(NULL): 不指向任何有效内存

    c 复制代码
    int* p = NULL;  // 安全的
  2. 野指针: 未初始化的指针,指向随机地址

    c 复制代码
    int* p;  // 危险!指向未知位置
    *p = 10; // 可能崩溃
  3. 悬空指针: 指向已释放的内存

    c 复制代码
    int* p = malloc(sizeof(int));
    free(p);
    // p现在是悬空指针
    p = NULL;  // 应该置NULL

防范措施:

  • 指针初始化为NULL
  • free后立即置NULL
  • 使用前检查指针是否为NULL

Q: 如何避免内存泄漏?

A:

  1. 配对使用 : 每个 malloc 对应一个 free

  2. 及时释放: 不再使用时立即释放

  3. 避免丢失指针

    :

    c 复制代码
    char* p = malloc(100);p = malloc(200);  // 错误!前面的100字节泄漏
  4. 使用工具: Valgrind、AddressSanitizer

  5. 良好习惯

    :

    • 谁分配谁释放
    • 释放后置NULL
    • 注意异常路径

2. 指针进阶

Q: 指向指针的指针(二级指针)的应用?

A: 二级指针的主要用途:

  1. 修改指针本身:

    c 复制代码
    void allocate(int** p) {
        *p = malloc(sizeof(int));
        **p = 100;
    }
    
    int* ptr = NULL;
    allocate(&ptr);  // 传递指针的地址
  2. 指针数组:

    c 复制代码
    char* names[] = {"Alice", "Bob", "Charlie"};
    char** p = names;  // 指向字符串数组
  3. 动态二维数组:

    c 复制代码
    int** 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: malloccallocrealloc 的区别?

A:

  1. malloc: 分配指定字节的内存,不初始化

    c 复制代码
    int* p = (int*)malloc(10 * sizeof(int));
    // 内容未初始化,可能是垃圾值
  2. calloc: 分配内存并初始化为0

    c 复制代码
    int* p = (int*)calloc(10, sizeof(int));
    // 所有元素都是0
  3. realloc: 调整已分配内存的大小

    c 复制代码
    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个 调整大小

注意事项:

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,原因:

  1. 防止悬空指针:

    c 复制代码
    int* p = malloc(sizeof(int));
    free(p);
    // p现在是悬空指针,指向已释放的内存
    p = NULL;  // 置NULL后,再次free不会出错
  2. 避免双重释放:

    c 复制代码
    free(p);
    free(p);  // 错误!导致程序崩溃
    
    // 如果置NULL
    free(p);
    p = NULL;
    free(p);  // 安全,free(NULL)什么都不做
  3. 便于判断:

    c 复制代码
    if (p != NULL) {
        // 安全使用p
    }

最佳实践:

c 复制代码
#define SAFE_FREE(p) do { free(p); (p) = NULL; } while(0)

Q: 内存对齐的原因和作用?

A:

原因:

  1. 硬件要求: 某些CPU访问未对齐数据会崩溃或效率低
  2. 性能优化: 对齐访问速度更快(一次读取)

规则:

  • 结构体成员按自身大小对齐
  • 结构体总大小是最大成员的整数倍
  • 编译器可能添加填充字节(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:

  1. Valgrind (Linux):

    bash 复制代码
    gcc -g program.c -o program
    valgrind --leak-check=full ./program
  2. AddressSanitizer (GCC/Clang):

    bash 复制代码
    gcc -fsanitize=address -g program.c -o program
    ./program
  3. Visual Studio (Windows):

    • 使用内置的内存泄漏检测
    • CRT调试库
  4. 手动跟踪:

    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
  5. 良好习惯:

    • 每个malloc对应一个free
    • 使用智能指针(C++11)
    • RAII模式
    • 资源获取即初始化

3. 内存越界

Q: 常见的内存越界场景?

A:

  1. 数组越界:

    c 复制代码
    int arr[10];
    arr[10] = 100;  // 越界!索引应该是0-9
  2. 字符串操作:

    c 复制代码
    char str[5] = "hello";  // 错误!需要6字节(包括'\0')
    strcpy(dest, src);      // dest空间不足
  3. 指针运算:

    c 复制代码
    int* p = malloc(10 * sizeof(int));
    p[20] = 100;  // 越界访问
  4. 栈溢出:

    c 复制代码
    void func() {
        int huge[1000000];  // 栈空间不足
    }
  5. 未初始化指针:

    c 复制代码
    int* p;  // 野指针
    *p = 10; // 访问未知内存

Q: 缓冲区溢出的原理和防范?

A:

原理: 写入的数据超过缓冲区大小,覆盖相邻内存

经典漏洞:

c 复制代码
void vulnerable(char* input) {
    char buffer[64];
    strcpy(buffer, input);  // 危险!input可能超过64字节
    // 可能覆盖返回地址,执行恶意代码
}

防范措施:

  1. 使用安全函数:

    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);
  2. 边界检查:

    c 复制代码
    if (strlen(src) < sizeof(dest)) {
        strcpy(dest, src);
    }
  3. 编译器保护:

    bash 复制代码
    gcc -fstack-protector-all program.c  # 栈保护
    gcc -D_FORTIFY_SOURCE=2 program.c    # 函数安全检查
  4. 静态分析工具: 使用 Coverity, Clang Static Analyzer

  5. 代码审查: 重点检查字符串和数组操作


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: strcpystrncpy 的区别?安全性如何?

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: strcatstrlenstrstr 等函数的使用?

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:

对齐规则:

  1. 第一个成员偏移量为0
  2. 每个成员按其自身大小对齐
  3. 结构体总大小是最大成员的整数倍
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 = &rect;
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内

括号规则:

  1. 每个参数都加括号 : (x)
  2. 整体表达式加括号 : ((x) + (y))
  3. 多条语句用do-while(0)包装
  4. 逗号表达式用小括号包围

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)freadfwrite 的使用

这两个函数常用于二进制文件的读写。

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)fseekftellrewind 的作用

函数 作用
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)fgetsgets 的区别

函数 特点 安全性
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
};

魔数的危害:

  1. 降低代码可读性
  2. 难以维护(需要修改所有出现的地方)
  3. 容易出错(数字容易记错)
  4. 缺乏语义(不知道数字含义)

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
相关推荐
CodeCraft Studio3 小时前
国产化Excel开发组件Spire.XLS教程:将Python列表转换为Excel表格(3种实用场景)
开发语言·python·excel·spire.xls·python列表转excel·国产化文档开发
金涛03193 小时前
QT-day1
开发语言·qt
曹牧3 小时前
C#:可选参数
开发语言·c#
磨十三3 小时前
C++ 容器详解:std::list 与 std::forward_list 深入解析
开发语言·c++·list
今麦郎xdu_3 小时前
【Linux系统】命令行参数和环境变量
linux·服务器·c语言·c++
Yeats_Liao3 小时前
Go语言技术与应用(二):分布式架构设计解析
开发语言·分布式·golang
脚踏实地的大梦想家4 小时前
【Go】P6 Golang 基础:流程控制
开发语言·golang
信息快讯4 小时前
“COMSOL+MATLAB光子学仿真:从入门到精通,掌握多物理场建模
开发语言·matlab·comsol·光学工程
LK_074 小时前
【Open3D】Ch.3:顶点法向量估计 | Python
开发语言·笔记·python