【C语言】 初入嵌入式C语言复习(基础+进阶面试题)

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, 11

5. 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 13

printf 从右向左求值:先算 *(++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

  1. 在函数内修改调用者的指针:传指针的地址,函数内改 *pp 即改调用者的指针指向;

  2. 表示指针数组 / 二维字符串数组(char **argv 可表示 char *argv[]);

  3. 动态分配二维数组(指针数组模拟)。

    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 *pp+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)并考虑对齐/字节序。


相关推荐
熊野君35 分钟前
附录与 Codex 实操手册
开发语言·人工智能·产品经理
wuminyu37 分钟前
深入剖析 Panama Off-heap 的性能损耗与开销
java·linux·c语言·jvm·c++
rhythm-ring1 小时前
宏定义续行符 \ 的使用与踩坑
c语言·c++
XZ-0700012 小时前
week2-1-可视化
开发语言·python
Java后端的Ai之路2 小时前
14、Python - 责任链模式
服务器·开发语言·人工智能·python·责任链模式
吴声子夜歌2 小时前
Java——基本类型
java·开发语言
老当益壮梁奶奶2 小时前
Linux软件编程学习笔记(八):进程间通信详解(1)
linux·c语言·笔记·学习·算法
「QT(C++)开发工程师」2 小时前
C++11 std::unique_ptr — 独占所有权的智能指针
c语言·开发语言·c++·qt
不正经学生2 小时前
C语言动态内存管理(上):堆上的自由与责任
c语言·开发语言·c++·算法·面试