C 语言高频基础题(整理版 · 含参考答案)
一、运算符
1. 运算符优先级
从高到低大致:
括号 () → 单目(! ~ ++ -- * & 强转)→ 算术( / % 高于 + -)→ 移位(<< >>)→ 关系(< <= > >= 高于 == !=)→ & → ^ → | → && → || → ?: → 赋值 = → 逗号 ,*
2. 逗号运算符
从左到右依次求值,整个表达式的值是最后一个表达式的值。
x = (a = 2, b = 3, a + b); // 结果是 5
3. 自增自减与左值
a++ = b++; // 不可以(后置自增结果是右值) ++a = ++b; // 原稿标"可以"------注意:这是 C++ 语义(C++ 中 ++a 是左值); // 标准 C 中 ++ 结果都不是左值,++a = ++b 同样非法4. 后置自增求值
a = 5, b = 10; a = b++; printf("%d,%d", a, b); // 结果是 10, 115. a++ 与 ++a 的实现过程
// a++:先保存原值再自增,返回原值 int temp = a; a = a + 1; return temp; // ++a:先自增再返回新值 a = a + 1; return a;结论:后置自增需把原值复制到临时空间,前置效率更高(不需要旧值时优先
++i)。
二、输入输出
1. scanf 和 gets 的区别?
- gets:读一整行,能读含空格字符串,不保留换行;不检查缓冲区边界、易溢出,C11 已废弃,改用 fgets。
- scanf:按格式读,
%s遇空白即停,不能读含空格字符串;可指定宽度如%9s。
2. 怎么用 scanf 接收 i 个数据?
for (int k = 0; k < i; k++) scanf("%d", &arr[k]); // 或一条格式串:scanf("%d %d %d", &a, &b, &c);
3. 抑制符 '*' 的作用?
%*d表示跳过该输入项不赋值。例scanf("%d %*d %d", &a, &b);输入1 2 3→ a=1,跳过 2,b=3。
4. printf 参数求值顺序(从右向左)
int arr[] = {11, 12, 13, 14, 15}; int *ptr = arr; *(ptr++) += 100; // arr[0]=111, ptr 指向 arr[1] printf("%d %d", *ptr, *(++ptr)); // 输出 13 13printf 从右向左求值:先算
*(++ptr)使 ptr 移到 arr2 取 13;再算*ptr仍为 13。
5. printf 特殊用法
char str[] = "hello"; printf(str); // 等价于 printf("%s", str),但字符串含 % 会被当格式符,有安全风险正确写法是
printf("%s", str)。
三、原码、反码、补码
- 正数:原码 = 反码 = 补码;
- 负数:原码(符号位 1 + 绝对值)→ 反码(符号位不变,数值位按位取反)→ 补码(反码 + 1);
- 补码求原码:再"取反加一"。
- 例 -5(8 位):原码 1000 0101 → 反码 1111 1010 → 补码 1111 1011。
- 意义:加减统一为加法、符号位参与运算、0 表示唯一。
四、预处理
1. 编译过程分几个部分?
预处理 → 编译 → 汇编 → 链接 四个阶段。
2. 宏定义在哪个阶段处理?
预处理阶段,编译前做纯文本替换。
3. 用 #define 声明 1 年有多少秒
#define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL)整个表达式加括号;用 UL 后缀防止溢出。
4. #error 的目的?
在预处理阶段产生编译错误并停止编译,常配合条件编译检查条件:
#if !defined(__cplusplus) #error "This file must be compiled as C++" #endif
五、关键字
1. static 的作用?
- 修饰局部变量:静态局部变量,只初始化一次,生命周期到程序结束,作用域仍局部;
- 修饰全局变量:作用域限制为本文件;
- 修饰函数:作用域限制为本文件(内部链接)。
2. const
const int *p; // 指向整型常量的指针(指针可改、所指值不可改) int * const p; // 指向整型变量的指针常量(指针不可改、所指值可改)
- const 与 #define 区别:const 有类型、编译器做类型检查、可调试、遵循作用域;#define 是纯文本替换、无类型检查。
3. volatile 作用?何时用?
- 作用:告诉编译器该变量可能被外部机制意外修改,禁止优化、每次从内存读取。
- 场景:①硬件寄存器;②中断服务程序修改的变量;③多线程共享变量。
4. extern 作用?extern "C" 作用?
- extern:声明变量/函数定义在其它文件中(外部链接),扩展作用域,不分配空间。
- extern "C":C++ 中按 C 链接方式处理(不做名字修饰),用于 C/C++ 混合编程。
5. volatile 和 extern 分别在哪个阶段起作用?
都是编译阶段:extern 决定外部链接属性,volatile 决定生成的访问代码。
6. sizeof 作用?不用 sizeof 求字节数?
作用:返回类型/变量占用字节数,编译期求值。
不用 sizeof(指针差值法):
int a[10];
size_t n = (char *)(a + 1) - (char *)a; // 即 sizeof(int)
7. sizeof 和 strlen 的区别?
sizeof strlen 时机 编译期 运行期 对象 类型/变量/数组 字符串 结果 占用字节(含 '\0') 字符个数(不含 '\0')
char s[] = "hello"; sizeof(s) // 6 strlen(s) // 5
8. typedef:dPS/tPS 哪种更好?
#define dPS struct s * typedef struct s * tPS;typedef 更好:
dPS p1, p2;宏替换后是struct s *p1, p2;,p2 是 struct s 不是指针;tPS p1, p2;两个都是指针。
9. typedef 和 #define 区别?
- typedef:编译期、类型别名、有类型检查;
- #define:预处理文本替换、无类型检查、可替换任意文本。
10. auto 的作用?
默认存储类别(局部变量默认即 auto),一般省略。C++11 中 auto 改为自动类型推导,含义不同。
11. register 作用?注意什么?
- 建议编译器把变量放寄存器加快访问(建议非强制)。
- 注意:不能取地址;寄存器数量有限,多了会被忽略;现代编译器已自动优化。
六、堆栈
1. 堆和栈的区别?
对比项 栈 Stack 堆 Heap 分配/释放 系统自动 程序员手动(malloc/free) 空间 小 大 连续性 连续 不连续(碎片化) 速度 快(硬件支持) 慢 生命周期 函数调用期间 手动控制
2. 哪个更快?
栈更快(只需移动栈指针一条指令;堆需查空闲链表、处理碎片)。
3. 堆栈溢出的原因?
递归过深;函数内声明过大局部变量/数组;死循环中不断压栈;嵌套调用过深。
4. 队列和栈的区别?
栈:LIFO 后进先出,单端操作;队列:FIFO 先进先出,一端入一端出。
5. 为什么堆空间不连续?
堆用空闲块链表管理,多次分配释放产生内存碎片,分配地址散落各处。
6. 为什么局部变量在栈不在堆?
局部变量生命周期短,栈的自动分配/释放与函数调用机制天然匹配、效率高;堆适合生命周期长、大小运行时才确定的数据。
7. 函数参数压栈顺序?
常见实现(x86)从右到左(C 标准未强制,但为事实标准)。
8. 未初始化内存的经典现象
VC 调试版:栈填 0xCC → "烫烫烫";堆填 0xCD → "屯屯屯"。
七、指针
1. 指针大小?
与类型无关,取决于平台:32 位 4 字节,64 位 8 字节。
2. 指针与数组名的区别?
- 数组名是常量地址不可自增/赋值;指针是变量可改变;
sizeof(数组名)= 整个数组;sizeof(指针)= 指针大小;- 数组名传参/表达式中退化为首元素指针。
3. 指针数组 vs 数组指针?
int *p[10]; // 指针数组:数组,元素是指针([] 优先级高) int (*p)[10]; // 数组指针:指针,指向含 10 个 int 的数组
4. 用指针数组表示二维数组?
int a[2][3]; int *p[2] = {a[0], a[1]}; // 每行指针 p[i][j]
5. 用数组指针表示二维数组?
int a[2][3]; int (*p)[3] = a; // p 指向一行 p[i][j]
6. 可变长数组怎么实现?
C99 VLA:
int a[n];(注意栈溢出);通用:int *a = malloc(n * sizeof(int));
7. 什么是函数指针?作用?
int (*fp)(int);指向函数的指针。作用:①回调函数;②表驱动(命令表/状态机/跳转表);③多态/策略;④作参数和返回值。
8. 什么是指针函数?
返回指针的函数:
int *func(void);
9. 什么是野指针?如何避免?
指向无效内存(未初始化、已释放、越界)的指针。
避免:定义时初始化置 NULL;free 后置 NULL;不返回局部变量地址;使用前判空。
10. 引用与指针的区别?
引用必须初始化、不能为空、不能改绑定、操作引用即操作原对象;
指针可空、可改、需解引用、可多级。
11. 经典题:不能返回局部变量的地址
int *bad(void) { int x = 10; return &x; } // 错误!x 返回后已失效
八、函数
1. 函数返回过程?
返回值放约定寄存器 → 恢复调用者现场(栈指针/基址/寄存器)→ 弹出返回地址 → 跳回调用点。
2. A 调用 B 的过程?
实参从右到左压栈 → 压返回地址 → 跳转 B → B 保存旧栈帧建新栈帧、分配局部变量空间 → 执行 → 返回值放寄存器 → 恢复栈帧、弹出返回地址、跳回 A。
3. 什么是回调函数?为什么用?
通过函数指针、由别人在特定时机调用的函数。
为什么用:①解耦(调用方不需知道实现细节);②通用可扩展(同一逻辑传入不同行为);③典型场景:事件处理、定时器、qsort 比较函数、状态机。
4. 什么是可重入函数?特点及注意?
可被中断打断后再次进入而结果仍正确的函数。
特点:不用全局/静态变量(或用关中断保护);不调用不可重入函数(printf/malloc 等);不改自身代码。
注意:共享资源要么不访问,要么互斥/关中断保护,否则数据竞争。
5. 如何实现函数在 main 之前执行?
C++:全局对象的构造函数;GCC:
__attribute__((constructor));也可用全局变量初始化时调用。6. C 中函数未指定返回值类型默认?
C89/C90 默认 int;C99 起为语法错误。
九、字符串
1. strcmp 复现
int my_strcmp(const char *s1, const char *s2) { while (*s1 && *s1 == *s2) { s1++; s2++; } return (unsigned char)*s1 - (unsigned char)*s2; }
2. strstr 复现
char *my_strstr(const char *h, const char *n) { if (*n == '\0') return (char *)h; for (; *h != '\0'; h++) { const char *a = h, *b = n; while (*b && *a && *a == *b) { a++; b++; } if (*b == '\0') return (char *)h; } return NULL; }3. strcpy 复现
char *my_strcpy(char *d, const char *s) { char *p = d; while ((*p++ = *s++) != '\0'); return d; }
十、结构体、联合体
1. 怎么计算结构体大小?
按字节对齐:
①每成员对齐到 min(自身大小, 默认对齐) 的整数倍偏移;
②总大小是最大对齐数整数倍;③末尾可能填充。
struct S { char a; int b; char c; }; // 32位默认4字节对齐 → sizeof = 12
2. 根据成员地址反推结构体首地址?
container_of 思想:
首地址 = 成员地址 - 成员偏移。
#define offsetof(type, member) ((size_t)&(((type *)0)->member)) #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member)))
3. 结构体和联合体的区别?
- struct:成员独立占用内存,大小≈各成员之和(含对齐),可同时访问所有成员;
- union:成员共享同一块内存,大小取最大成员,同一时刻只能用其中一个。
4. 16 位机器不考虑字节对齐
结构体大小 = 各成员大小直接相加。
十一、程序内存分配
1. 程序运行时内存分几个区?
通常 5 个:栈(局部变量/函数调用)、堆(动态分配)、全局/静态区(数据段 .data/.bss)、常量区(只读)、代码段(文本段)。
2. 什么是内存泄漏?
动态分配的内存没有释放且失去引用,无法回收。长期运行可用内存越来越少,最终分配失败或性能下降。
十二、malloc
1. new 和 malloc 的区别?
new malloc 本质 C++ 运算符 C 库函数 构造/析构 自动调用 不调用 返回类型 类型安全 void* 需强转 失败 抛异常 返回 NULL 释放 delete free
2. calloc 和 malloc 的区别?
calloc(n, size):分配 n 个 size 并初始化为 0;malloc(size):只分配不初始化。
3. 1G 内存能否 malloc(1.2G)?
取决于系统。Linux 支持虚拟内存/overcommit 时 malloc 可能成功(只申请虚拟地址),但真正访问时若物理内存+交换不足,可能被 OOM 杀死。malloc 成功 ≠ 真的拿到 1.2G 物理内存。
十三、大小端
1. 什么是大端/小端?
- 大端:高字节存低地址;
- 小端:低字节存低地址。
- 例 x = 0x12345678:大端 12 34 56 78;小端 78 56 34 12(x86 小端)。
2. 判断大小端
// 指针法 int x = 0x12345678; if (*(unsigned char *)&x == 0x78) puts("little-endian"); else puts("big-endian"); // 联合体法 union { int i; char c; } u; u.i = 1; if (u.c == 1) puts("little-endian");
3. 大小端转换
unsigned int swap_endian(unsigned int x) { return ((x & 0x000000FF) << 24) | ((x & 0x0000FF00) << 8) | ((x & 0x00FF0000) >> 8) | ((x & 0xFF000000) >> 24); }
十四、其他
1. 不定义新变量交换两个数
// 加减法(可能溢出) a = a + b; b = a - b; a = a - b; // 异或法(无溢出) a ^= b; b ^= a; a ^= b;
2. 计算二进制中 1 的个数(平行算法)
int count_bits(unsigned int x) { x = x - ((x >> 1) & 0x55555555); x = (x & 0x33333333) + ((x >> 2) & 0x33333333); x = (x + (x >> 4)) & 0x0F0F0F0F; return (x * 0x01010101) >> 24; } // 简单版:while (n) { n &= (n-1); count++; }
3. 各数据类型大小
32 位:char=1, short=2, int=4, long=4, float=4, double=8, 指针=4;
64 位:long=8, 指针=8。
4. 定义以下变量
int a; // 整型数 int *p; // 指向整型数的指针 int **pp; // 指向指针的指针 int arr[10]; // 10 个整型的数组 int *parr[10]; // 10 个指针的数组(指向整型) int (*p_arr)[10]; // 指向含 10 个整型数组的指针 int (*fp)(int); // 指向"参数 int 返回 int"函数的指针 int (*fp_arr[10])(int);// 10 个指向"参数 int 返回 int"函数的指针的数组
5. 变量作用域有哪些?
局部(块内)、全局(定义处到文件末,跨文件需 extern)、静态局部(生命周期全局、作用域局部)、静态全局(本文件)、文件作用域。
6. 浮点数怎么判断相等?
if (fabs(a - b) < 1e-6) // 认为相等
7. 用 C 实现读写寄存器
#define REG (*(volatile unsigned int *)0x40000000UL) REG = 0x01; // 写 unsigned int v = REG; // 读
8. 数组下标可以为负数吗?
语法上允许(
a[-1]=*(a-1)),但属于越界/未定义行为,一般不用;仅当确实指向同一数组内合法元素时可。
9. 什么是字节对齐,为什么要对齐?
数据起始地址按一定规则对齐。
原因:CPU 访问对齐数据高效、部分硬件强制要求、避免跨缓存行/跨页访问。
10. 用宏求数组元素个数
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
11. 带参宏和函数的区别?
宏:预处理文本替换、无类型检查、无调用开销、但可能多次求值副作用、代码膨胀、不易调试;
函数:有类型检查、有调用开销、代码只存一份、易调试。
12. 内联函数优缺点和适用场景?
优点:减少调用开销(压栈/跳转/返回),保留类型检查;
缺点:代码膨胀;适用:函数体短小、被频繁调用。不适合递归、循环大、函数体过大的函数。
13. 全局变量和局部变量的区别?
生命周期(程序期 vs 调用期)、作用域(全局/文件 vs 局部)、存储位置(全局数据区 vs 栈)不同。
14. 全局变量可不可以定义在头文件?
不可以。多文件包含会出现重复定义。
正确:头文件用
extern声明,在某个 .c 中定义;或定义为static(每个文件各一份副本)。
15. float 型数据怎么存储?
IEEE 754 单精度,32 位 = 1 符号位 + 8 指数位(偏置 127)+ 23 尾数位。
16. 什么是左值、右值?
- 左值:可出现在赋值号左边、有内存地址的对象(变量、数组元素、解引用结果),可取地址;
- 右值:只能出现在右边,代表临时值/立即数,一般无独立地址、不可赋值。
第二部分 · 进阶补充篇
一、指针与 const 的四种组合
const int *p; // ① 指向 const int 的指针:*p 不可改,p 可改 int const *p; // ② 同① int *const p; // ③ 指向 int 的 const 指针:p 不可改,*p 可改 const int *const p; // ④ 都不可改读法技巧:从右往左读,const 修饰离它最近的东西。
const int *p→ p 是一个指针,指向const int;int * const p→ p 是一个 const 指针,指向int。追问:为什么 const 指针常用于函数形参?------防止函数内误改实参所指内容,同时传指针避免大对象拷贝,兼顾效率与安全(如
const char *str)。二级指针的作用(
int **pp)
在函数内修改调用者的指针:传指针的地址,函数内改
*pp即改调用者的指针指向;表示指针数组 / 二维字符串数组(
char **argv可表示char *argv[]);动态分配二维数组(指针数组模拟)。
void alloc(int **pp) {
*pp = (int *)malloc(sizeof(int)); // 修改调用者的指针
}
int *p = NULL;
alloc(&p);
二、数组与指针的经典细节
1. 二维数组传参的三种方式
void f1(int a[][3], int n); // 方式一:数组形式(行数可省,列数必给) void f2(int (*a)[3], int n); // 方式二:数组指针(等价于方式一) void f3(int n, int a[n][3]); // 方式三:C99 变长数组
2.
a和&a的区别
int a[10]; a // int*,首元素地址,a+1 移动 sizeof(int) &a // int(*)[10],整个数组地址,&a+1 移动 10*sizeof(int)数值相同,但类型和步长不同。
3. sizeof 二维数组
int a[2][3]; sizeof(a) // 24(整个数组) sizeof(a[0]) // 12(一行) sizeof(a[0][0]) // 4(一个元素)
4. 指针加减的跨度
int *p的p+1移动sizeof(int)字节;指针相减结果是元素个数而非字节数。
三、位操作(嵌入式高频)
1. 置位 / 清零 / 翻转 / 测试某一位
#define SET_BIT(x, n) ((x) |= (1U << (n))) #define CLR_BIT(x, n) ((x) &= ~(1U << (n))) #define TOGGLE_BIT(x, n) ((x) ^= (1U << (n))) #define GET_BIT(x, n) (((x) >> (n)) & 1U)
2. 常见位技巧
(x & 1) // 判断奇偶 x &= (x - 1) // 清除最低位的 1 (x & -x) // 取出最低位的 1
3. 左移 / 右移细节
- 左移
<<:低位补 0;- 右移
>>:无符号数高位补 0(逻辑右移);有符号数高位补符号位(算术右移,常见实现)。
4. 用位操作交换 / 求绝对值
// 交换 a ^= b; b ^= a; a ^= b; // 绝对值(32 位 int) int abs_v(int x) { return (x ^ (x >> 31)) - (x >> 31); }
四、宏的陷阱与高级用法
1.
#和##的作用
#define STR(x) #x // #:字符串化,STR(123) → "123" #define CAT(a,b) a##b // ##:连接 token,CAT(x,1) → x1
2. 宏的副作用(经典坑)
#define MAX(a,b) ((a) > (b) ? (a) : (b)) MAX(i++, j); // i++ 可能被求值多次 → 结果不可预期原则:宏参数应避免带副作用的表达式。
3. 为什么用
do { } while(0)封装多语句宏
#define SWAP(a,b) do { typeof(a) t = (a); (a)=(b); (b)=t; } while(0)多语句宏若不用 do-while 包裹,在
if后使用会出现悬空 else/语法错误;do-while(0) 把多条语句封装成单个语句。
4. 防止头文件重复包含
#ifndef HEADER_H #define HEADER_H /* 内容 */ #endif // 或:#pragma once
5. #pragma pack 的作用
改变结构体对齐方式。通信协议、文件格式中常用
#pragma pack(1)按 1 字节对齐(无填充),保证结构体与字节流一一对应。用后#pragma pack()恢复默认。
五、程序输出与易错题
1. 短路求值
int i = 0; i && (i = 5); // i 仍为 0(&& 左边为假,右边不求值) i = 1; i || (i = 5); // i 仍为 1(|| 左边为真,右边不求值)
2. 隐式类型转换 / 整型提升
char/short 参与运算时提升为 int;
有符号与无符号混用时转成无符号(负数会变成很大的数)。
if (-1 < 0U) // 假!-1 转成 unsigned 变成很大的正数
unsigned int x = 10;
if (x > -1) // 假!-1 提升为 unsigned
3. char 是有符号还是无符号?
标准未规定,由实现决定(x86 默认 signed char)。跨平台要显式写
signed char/unsigned char。
4. 整数溢出
int溢出是未定义行为;unsigned溢出是定义行为(回绕)。大数运算用
long long或检查范围。
5. 逗号表达式陷阱
int x = (1, 2, 3); // x = 3
6.
sizeof不执行表达式
int i = 0; sizeof(i++); // 不改变 i,sizeof 是编译期运算
六、内存与安全
1. 段错误(Segmentation Fault)的原因
①解引用空指针/野指针;②越界访问数组;
③访问只读内存(如写字符串常量);④栈溢出;⑤访问已释放内存。
2. 缓冲区溢出
向超出缓冲区大小的位置写入,覆盖相邻内存,可能被利用(栈溢出攻击)。
避免:用
fgets/snprintf/strncpy并显式检查长度;strcpy/sprintf无边界检查,慎用。
3. 内存泄漏如何检测
- Linux:valgrind(
valgrind --leak-check=full ./a.out);- VS:
_CrtDumpMemoryLeaks()等 CRT 调试机制;- 规范:谁分配谁释放、malloc/new 与 free/delete 严格配对、释放后置 NULL。
4. 悬挂指针 vs 野指针
- 悬挂指针:指向已 free/delete 的内存(曾经有效,现在失效);
- 野指针:未初始化或指向无效地址的指针。
- 都是解引用即未定义行为;free 后置 NULL 可避免悬挂。
七、结构体进阶(位域 / 柔性数组)
1. 位域(bit-field)
struct { unsigned int a : 3; unsigned int b : 5; };
- 按位分配,节省内存;位域不能取地址;跨平台时位域的内存布局(大小端、对齐)不确定,慎用于跨平台通信。
2. 柔性数组(C99)
struct S { int len; char data[]; }; // 最后一个成员是不定长数组
sizeof(S)不包含柔性数组;需 malloc 额外分配:struct S *s = malloc(sizeof(struct S) + 100);
常用于变长数据缓冲区(如协议报文)。
3. 结构体为什么不能直接
==比较?①成员间可能有填充字节(内容不确定);②浮点成员比较本身有误差;③指针成员比较的是地址。正确做法:逐成员比较。
4. 结构体自引用
链表/树节点用指针自引用(
struct Node *next;),不能用值自引用(会无限递归大小)。
八、字符串与格式化安全
1. strcpy 为什么不安全?
无目标缓冲区长度检查,源串过长就越界写。用
strncpy或更安全的snprintf。
2. strncpy 的坑
char buf[5]; strncpy(buf, "hello world", 5); // 不保证以 '\0' 结尾! buf[4] = '\0'; // 需手动补结尾
strncpy当源长度 ≥ n 时不写 '\0';strncat则总是补 '\0'。
3. sprintf vs snprintf
sprintf(buf, "%s", s); // 无边界检查,可能溢出 → 危险 snprintf(buf, sizeof(buf), "%s", s); // 安全一律用 snprintf。
4. 手写字符串反转
void reverse(char *s) { if (!s) return; for (int i = 0, j = strlen(s) - 1; i < j; i++, j--) { char t = s[i]; s[i] = s[j]; s[j] = t; } }
九、经典算法手写题(笔试高频)
1. 二分查找
int binary_search(int a[], int n, int key) { int lo = 0, hi = n - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; // 防溢出 if (a[mid] == key) return mid; else if (a[mid] < key) lo = mid + 1; else hi = mid - 1; } return -1; }
2. 判断回文
int is_palindrome(const char *s) { int i = 0, j = strlen(s) - 1; while (i < j) if (s[i++] != s[j--]) return 0; return 1; }
3. 单链表反转(三指针)
struct Node *reverse(struct Node *head) { struct Node *prev = NULL, *cur = head; while (cur) { struct Node *next = cur->next; cur->next = prev; prev = cur; cur = next; } return prev; }
4. 判断链表是否有环(快慢指针)
int has_cycle(struct Node *head) { struct Node *slow = head, *fast = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; if (slow == fast) return 1; } return 0; }
5. 求最大公约数(辗转相除)
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
6. 斐波那契(迭代,避免重复递归)
int fib(int n) { if (n <= 1) return n; int a = 0, b = 1; for (int i = 2; i <= n; i++) { int t = a + b; a = b; b = t; } return b; }
7. 判断素数
int is_prime(int n) { if (n < 2) return 0; for (int i = 2; i * i <= n; i++) if (n % i == 0) return 0; return 1; }
十、编译与链接
1. 静态链接 vs 动态链接
- 静态链接:库代码复制进可执行文件,独立运行、体积大、库更新需重编译;
- 动态链接:运行时加载共享库(.so/.dll),体积小、共享内存、库可单独升级,但依赖运行环境。
2. 头文件的作用 / 防止重复包含
头文件放声明(函数原型、extern 变量、宏、类型),源文件放定义;
用 include guard 防止重复包含。
3. 编译单元
一个 .c 文件 + 它包含的头文件构成一个编译单元;
各编译单元独立编译,最后链接。static 限制符号在编译单元内可见。
十一、函数进阶
1. 变参函数(printf 的原理)
#include <stdarg.h> int my_sum(int n, ...) { va_list ap; int sum = 0; va_start(ap, n); for (int i = 0; i < n; i++) sum += va_arg(ap, int); va_end(ap); return sum; }关键:变参函数必须有固定参数(如 printf 的格式串)来确定参数个数与类型;类型不匹配是未定义行为。
2. main 函数参数
int main(int argc, char *argv[]) // argc 参数个数,argv 参数数组
3. 递归的优缺点
- 优点:代码简洁、逻辑清晰、天然契合分治/树结构问题;
- 缺点:栈开销大、可能栈溢出、重复计算效率低(如朴素斐波那契)。
- 深递归场景改迭代 / 尾递归 / 动态规划。
4. 宏 vs 内联函数 vs 普通函数
宏 内联函数 普通函数 展开时机 预处理 编译期 运行期调用 类型检查 无 有 有 调用开销 无 无 有 代码膨胀 严重 中等 无 副作用 易产生 无 无
十二、杂项高频
1. 为什么
0.1 + 0.2 != 0.3?二进制浮点数(IEEE 754)无法精确表示 0.1、0.2,存在舍入误差。比较浮点用差值阈值。
2. 枚举 vs 宏(#define 常量)
枚举有类型检查、调试器可见、遵循作用域、可自动递增;
**宏是纯文本替换无类型。**需要一组相关常量用 enum。
3. void* 的作用
通用指针,可指向任意类型;但不能直接解引用、不能做指针运算,需强制转换为具体类型后再用。
malloc返回 void* 的原因。
4. 什么是可移植性问题?
同一代码在不同平台结果不同:
int位数、char有无符号、大小端、字节对齐、结构体布局。写跨平台代码要显式指定类型宽度(如 int32_t)并考虑对齐/字节序。

