Skip to content

C 速查表

驱动系统、嵌入式和操作系统开发的低级语言。

01

入门

Hello World

每个 C 程序都从 main() 开始。#include <stdio.h> 引入标准 I/O 库(printf、scanf)。main 成功返回 0,失败返回非零值。void 关键字显式声明 main 不接受参数。

c
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

变量与类型

C 是静态类型的。常见类型:int、float、double、char。float 需要 f 后缀。char[] 是字符串(以 null 结尾的数组)。long 和 short 是大小修饰符。unsigned 表示非负。大小因平台而异;使用 <stdint.h> 获取固定宽度。

c
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);

输入与输出

scanf 需要变量的地址(&)来存储输入。始终限制字符串输入长度(50 字符缓冲区用 %49s)以防止缓冲区溢出。scanf 在空白处停止读取字符串;使用 fgets 读取整行。

c
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);

char name[50];
printf("Enter name: ");
scanf("%49s", name);  // limit to prevent overflow
printf("Hi, %s!\n", name);

printf 格式说明符

格式说明符控制输出:%d 整数、%f 浮点数、%c 字符、%s 字符串、%x 十六进制、%p 指针。宽度和精度(例如 %5.2f)控制对齐和小数位数。说明符与类型不匹配会导致未定义行为。

c
printf("%d\n", 42);        // integer
printf("%f\n", 3.14);       // float/double
printf("%.2f\n", 3.14159);  // 3.14 (2 decimals)
printf("%c\n", 'A');        // char
printf("%s\n", "hello");    // string
printf("%x\n", 255);        // ff (hex)
printf("%5d\n", 42);        // right-aligned, width 5
printf("%-5d|\n", 42);      // left-aligned

预处理器与头文件

预处理器在编译前运行。#include 粘贴头文件,#define 创建宏和常量。在头文件中始终使用包含保护(#ifndef/#define/#endif)以防止重复包含。宏是文本替换——在参数周围使用括号。

c
#include <stdio.h>   // system header
#include "myheader.h"  // local header

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

#ifndef GUARD_H
#define GUARD_H
// header content
#endif
02

字符串与 String.h

字符串基础

C 字符串是以 null 结尾的 char 数组。strlen 计算 '\0' 之前的字符数;sizeof 返回缓冲区大小。strcpy 复制直到 null 终止符——始终确保目标足够大以避免溢出。

c
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s));   // 5
printf("Size: %zu\n", sizeof(s));     // 20

char dest[20];
strcpy(dest, s);   // copy
printf("%s\n", dest);  // Hello

连接与比较

strcat 追加(目标必须有空间)。strcmp 按字典序比较:相等返回 0,第一个 < 第二个返回负值,第一个 > 第二个返回正值。切勿使用 == 比较字符串(那比较的是指针,而非内容)。

c
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!");   // s = "Hello, World!"
printf("%s\n", s);

int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
    printf("Equal!\n");
}

sprintf 与 sscanf

sprintf 格式化为字符串缓冲区(类似 printf 但到字符串)。sscanf 将字符串解析为变量(类似 scanf 但从字符串)。使用 snprintf 代替 sprintf,通过指定最大大小来防止缓冲区溢出。

c
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);

int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b);  // a=10, b=20

strchr、strstr 与 strtok

strchr 查找字符,strstr 查找子字符串。strtok 按分隔符分割字符串但会修改原始字符串(插入 null 终止符)且不是线程安全的——在后续调用中传递 NULL 以继续分词。

c
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W');   // find first 'W'
printf("%s\n", p);          // World!

char *sub = strstr(s, "World");
printf("%s\n", sub);        // World!

char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
    printf("%s\n", tok);
    tok = strtok(NULL, ",");
}

fgets 与安全输入

fgets 是读取字符串的安全方式——它接受大小限制以防止溢出。与 scanf 不同,它读取空格。换行符包含在结果中;strcspn 查找并移除它。始终优先使用 fgets 而非 gets(已从 C11 中移除)。

c
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);
03

数字与数学

整数类型与限制

当确切大小重要时,使用 <stdint.h> 获取固定宽度类型(int32_t、int64_t)。<limits.h> 提供 INT_MAX、INT_MIN 等平台特定边界。LL 后缀标记 long long 字面量。int/long 的大小因平台而异。

c
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;

printf("INT_MAX = %d\n", INT_MAX);     // 2147483647
printf("INT_MIN = %d\n", INT_MIN);     // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX);   // 4294967295

浮点数

float 是 4 字节(6-7 位精度),double 是 8 字节(15-16 位)。由于舍入误差,切勿用 == 比较浮点数——使用 fabs(a - b) < epsilon。<float.h> 提供 DBL_MAX、DBL_EPSILON 用于边界和精度。

c
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;

printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

if (d == 0.1 + 0.2) {
    // likely false! floating point imprecision
}

数学函数

<math.h> 提供标准数学函数。pow 和 sqrt 返回 double。fabs 是 abs 的浮点版本(abs 用于 int)。在某些系统上用 -lm 链接。对于财务计算,避免浮点——改用整数分。

c
#include <math.h>
double x = 2.5;
pow(x, 3);      // 15.625
sqrt(x);        // 1.581
fabs(-5.0);     // 5.0
floor(3.7);     // 3.0
ceil(3.2);      // 4.0
fmod(10.5, 3);  // 1.5
exp(1);         // 2.718 (e^1)
log(2.718);     // 1.0 (natural log)

随机数

rand() 返回 0 到 RAND_MAX 之间的伪随机整数。在程序开始时用 srand() 播种一次(使用 time(NULL))。rand() % N 有模偏差且质量低;对于严肃用途,读取 /dev/urandom 或使用第三方 PRNG 库。

c
#include <stdlib.h>
#include <time.h>

srand(time(NULL));  // seed once at start
int r = rand() % 100;       // 0-99
int dice = rand() % 6 + 1;  // 1-6

float fr = (float)rand() / RAND_MAX;  // 0.0 - 1.0

类型转换与强制转换

强制转换是 (type)value。整数除法截断——使用浮点操作数获得浮点结果。atoi/atof 将字符串转换为数字但不做错误检查;优先使用 strtol/strtod,它们通过 errno 报告解析错误。

c
int i = 65;
char c = (char)i;        // 'A'
double d = 3.99;
int truncated = (int)d;  // 3

// Implicit promotion
int a = 5;
double result = a / 2.0;  // 2.5 (promoted to double)
int bad = a / 2;          // 2 (integer division)

// String to number
int n = atoi("42");
double f = atof("3.14");
04

控制流

If / Else

if/else if/else 是标准条件语句。C 将 0 视为 false,任何非零值视为 true。即使单条语句也使用大括号以防止稍后添加行时的错误。C89 中没有布尔类型;C99 添加了 _Bool 和 <stdbool.h>。

c
int score = 85;
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}

Switch

switch 跳转到匹配的 case 标签。始终使用 break 防止穿透(case 6 和 7 有意共享代码)。default 处理未匹配的值。switch 仅适用于整数和 char 类型,不适用于字符串或浮点数。

c
int day = 3;
switch (day) {
    case 1: printf("Mon\n"); break;
    case 2: printf("Tue\n"); break;
    case 3: printf("Wed\n"); break;
    case 6:
    case 7: printf("Weekend\n"); break;
    default: printf("Invalid\n");
}

For 循环

for 循环有 init; condition; update。sizeof(nums)/sizeof(nums[0]) 在编译时计算数组长度。在 for 循环内声明 i 需要 C99 或更高版本。如果条件最初为 false,循环体执行零次。

c
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
    printf("%d\n", nums[i]);
}

While 与 Do-While

while 在执行前检查(可能运行零次)。do-while 先执行循环体,然后检查(至少运行一次)。do-while 非常适合输入验证和菜单循环,其中提示必须在条件可检查之前出现。

c
int count = 0;
while (count < 3) {
    printf("%d\n", count++);
}

int x;
do {
    printf("Enter positive: ");
    scanf("%d", &x);
} while (x <= 0);  // runs at least once

Break、Continue 与 goto

break 退出最近的循环/switch;continue 跳到下一次迭代。C 没有带标签的 break,因此 goto 是跳出深度嵌套循环的惯用方式。其他情况下不鼓励使用 goto,但对于清理模式和嵌套循环退出是可接受的。

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    printf("%d ", i);      // 0 1 2 4 5 6
}

// goto for breaking nested loops
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (found) goto done;
    }
}
done: printf("exited\n");
05

函数

定义与调用

函数必须在使用前声明(原型)或定义。void 返回类型表示无返回值。const char *name 表示函数不会修改字符串。C 按值传递参数;使用指针模拟按引用传递。

c
int add(int a, int b) {
    return a + b;
}

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    int sum = add(3, 4);
    greet("Alice");
    return 0;
}

递归

递归以更小的输入调用自身。每个递归函数都需要基本情况来停止。上面的朴素 fib 是 O(2^n)——指数级。使用记忆化或迭代提高效率。深度递归可能溢出调用栈。

c
int factorial(int n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1); // recursive case
}

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120

函数指针

函数指针存储函数的地址,启用回调和动态分派。语法 int (*op)(int, int) 声明一个指向接受两个 int 并返回 int 的函数的指针。用于 qsort、事件处理程序和插件系统。

c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// As a parameter
int apply(int (*f)(int, int), int a, int b) {
    return f(a, b);
}

可变参数函数

可变参数函数使用 <stdarg.h> 接受可变数量的参数。va_start 初始化,va_arg 检索下一个参数,va_end 清理。您需要一种方法知道计数(例如,计数参数或哨兵值)。printf 就是这样工作的。

c
#include <stdarg.h>
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;
}
// sum(3, 10, 20, 30) == 60

static 与 inline

static 作用于函数/变量将其限制在当前翻译单元(文件)内。static 作用于局部变量使其跨调用持久(类似全局但作用域受限)。inline 建议编译器嵌入函数体;现代编译器忽略它并自行决定。

c
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }

// inline: hint to expand inline
static inline int square(int x) { return x * x; }

// static local: persists across calls
int call_count(void) {
    static int n = 0;
    return ++n;
}
06

数组与指针

数组

数组是固定大小、零索引且在内存中连续存储的。sizeof(arr)/sizeof(arr[0]) 计算长度,但仅适用于实际数组,不适用于指针(数组传递给函数时退化为指针,丢失大小信息)。

c
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]);     // 1
printf("%d\n", nums[4]);     // 5

int len = sizeof(nums) / sizeof(nums[0]);  // 5

// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]);   // banana

指针

指针存储内存地址。& 获取地址,* 解引用。始终初始化指针(如果尚未赋值则使用 NULL)。解引用 NULL 或未初始化的指针是未定义行为(通常是崩溃)。解引用前检查 NULL。

c
int x = 10;
int *ptr = &x;     // ptr holds address of x
printf("%p\n", (void*)ptr);  // address
printf("%d\n", *ptr);        // 10 (dereference)

*ptr = 20;         // modify x through pointer
printf("%d\n", x);  // 20

int *p = NULL;     // null pointer (points to nothing)
if (p) { /* safe to dereference */ }

指针算术

指针算术按元素大小缩放:p+1 移动到下一个元素,而非下一个字节。这使 p[i] 等同于 *(p+i)。减去指向同一数组的两个指针给出元素计数。指针算术仅在数组内有效。

c
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;       // points to arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30

p += 3;              // now points to arr[3]
printf("%d\n", *p);  // 40

int diff = (p - arr); // 3 (number of elements)

数组与指针

数组名在传递给函数或在表达式中使用时退化为指针,丢失大小信息。这就是为什么您必须单独传递数组长度。sizeof(arr) 仅当 arr 是真正的数组而非指针时给出完整数组大小。

c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr decays to &arr[0]

// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));

// But sizeof differs
printf("%zu\n", sizeof(arr));   // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr));   // 8 (pointer size)

多维数组

二维数组是数组的数组,按行主序存储。grid[i][j] 访问第 i 行第 j 列。传递给函数时,必须指定列数:void foo(int arr[][3], int rows)。对于动态二维数组,使用指针数组。

c
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("%d\n", grid[0][1]);  // 2
printf("%d\n", grid[1][2]);  // 6

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", grid[i][j]);
    }
    printf("\n");
}
07

结构体与联合

结构体

结构体将不同类型的相关变量分组。成员用点运算符(.)访问。用大括号表示法初始化。结构体按值传递(复制);按指针传递(struct Point *)以避免复制并修改原始值。

c
struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);  // (3, 4)

p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y);  // (10, 20)

typedef

typedef 为类型创建别名,因此您可以写 Student 而非 struct Student。它通常与结构体一起使用以简化语法。typedef 还可以为函数指针类型起别名,使回调更可读。

c
typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);

// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);

指向结构体的指针

当您有指向结构体的指针时,使用箭头运算符(->)访问成员。ptr->x 是 (*ptr).x 的简写。将结构体指针传递给函数以提高效率(避免复制大型结构体)并允许修改。

c
typedef struct {
    int x, y;
} Point;

Point p = {3, 4};
Point *ptr = &p;

// Arrow operator (->) for pointer members
printf("%d\n", ptr->x);   // 3
ptr->y = 10;
printf("%d\n", p.y);      // 10

// Equivalent: (*ptr).x

联合

联合在相同内存上叠加多种类型——一次只有一个成员有效。设置一个成员会覆盖其他成员。适用于类型双关(将位重新解释为不同类型)和在只需要几种类型之一时节省内存。

c
union Value {
    int i;
    float f;
    char bytes[4];
};

union Value v;
v.i = 65;
printf("%d\n", v.i);          // 65
printf("%c\n", v.bytes[0]);   // 'A' (same memory)

v.f = 3.14f;
printf("%d\n", v.i);  // reinterpreted bits!

位域与枚举

位域将多个小字段打包到单个 int 中,节省内存(常见于协议和硬件寄存器)。枚举定义命名的整数常量(默认 0、1、2...)。使用枚举代替 #define 以获得更好的调试和类型安全。

c
struct Flags {
    unsigned int bold : 1;
    unsigned int italic : 1;
    unsigned int size : 6;  // 0-63
};

struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);

enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c);  // 1
08

内存管理

malloc 与 free

malloc 分配堆内存并返回 void 指针(或失败时 NULL)。始终检查 NULL。每个 malloc 必须配对 free 以避免内存泄漏。free 后将指针设置为 NULL 可防止 use-after-free 错误。

c
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr);  // release memory
arr = NULL; // avoid dangling pointer

calloc 与 realloc

calloc 分配并清零内存(比有垃圾的 malloc 更安全)。realloc 调整大小:它可能移动块,返回新指针。如果 realloc 失败,它返回 NULL 但原始块仍有效——使用临时指针以避免泄漏。

c
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int));  // all zeros

// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }

free(arr);

栈与堆

栈内存是自动的(随函数调用分配/释放)且快速但有限(通常 1-8 MB)。堆内存通过 malloc/free 手动管理,大得多,但更慢且容易泄漏。小型、短生命周期数据用栈;大型或长生命周期数据用堆。

c
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100];  // on the stack

// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));

// Stack frame is freed when function returns
// Heap memory persists until explicitly freed

内存泄漏与悬空指针

当您丢失指向已分配内存的唯一指针时发生内存泄漏(无法释放)。悬空指针指向已释放的内存——解引用是未定义行为。双重释放也是未定义的。Valgrind 和 AddressSanitizer 等工具检测这些错误。

c
// Memory leak: lost the pointer, can't free
void leak(void) {
    int *p = malloc(100 * sizeof(int));
    // function returns without free -> leaked!
}

// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42;  // UNDEFINED BEHAVIOR!

// Double free: also undefined
free(p);  // crash likely

动态数组与字符串

动态分配让您创建大小在运行时确定的字符串/数组。调用者负责释放内存。始终为字符串分配 strlen+1(null 终止符)。这种模式(分配、返回、调用者释放)在 C API 中很常见。

c
#include <stdlib.h>
#include <string.h>

// Dynamic string copy
char *dup_str(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // +1 for null
    if (copy) strcpy(copy, s);
    return copy;  // caller must free
}

char *name = dup_str("Alice");
printf("%s\n", name);
free(name);
09

文件 I/O

fopen 与 fclose

fopen 打开文件并返回 FILE 指针(或失败时 NULL)。模式:r(读)、w(写/截断)、a(追加)、r+(读/写)、b(二进制)。始终检查 NULL。fclose 刷新缓冲区并关闭文件。fgets 安全读取一行。

c
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
    perror("fopen failed");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), f)) {
    printf("%s", line);
}

fclose(f);

fprintf 与 fscanf

fprintf 和 fscanf 的工作方式类似 printf/scanf,但作用于文件。fscanf 脆弱——格式不匹配会导致问题。对于稳健的解析,用 fgets 读取行然后用 sscanf 解析。完成后始终关闭文件以刷新缓冲区并释放资源。

c
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);

FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);

fread 与 fwrite(二进制)

fread/fwrite 读/写原始字节——适用于二进制数据和结构体。参数是:缓冲区、元素大小、计数、文件。二进制文件紧凑但不可跨架构移植(字节序、结构体填充)。始终用 'b' 模式打开二进制文件。

c
typedef struct { int id; float score; } Record;

Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);

Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);

fseek、ftell 与 rewind

fseek 移动文件位置:SEEK_SET(从开始)、SEEK_CUR(相对)、SEEK_END(从末尾)。ftell 返回当前位置。rewind 是 fseek(f, 0, SEEK_SET) 的简写。这些启用文件中的随机访问,适用于数据库和索引查找。

c
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END);   // jump to end
long size = ftell(f);     // get position = file size
printf("Size: %ld bytes\n", size);

rewind(f);                // back to start
// or: fseek(f, 0, SEEK_SET);

fseek(f, 10, SEEK_SET);   // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);

stderr 与标准流

每个 C 程序有三个流:stdin(键盘)、stdout(屏幕)、stderr(屏幕,无缓冲)。将错误写入 stderr 将它们与正常输出分离,启用重定向:program 2> errors.log。stderr 是无缓冲的,因此消息在崩溃前出现。

c
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");

int c;
while ((c = fgetc(stdin)) != EOF) {
    fputc(c, stdout);  // echo input
}

// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)
10

预处理器与宏

#define 常量与宏

#define 创建文本替换宏。像 PI 这样的常量提高可读性和可维护性。类函数宏必须将参数括在括号中以避免优先级错误:不带括号的 SQUARE(2+3) 会是 2+3*2+3=11。优先使用 const 变量和 inline 函数而非宏。

c
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"

// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = SQUARE(5);    // 25
int big = MAX(3, 7);     // 7

条件编译

条件编译(#if、#ifdef、#ifndef)在编译时包含/排除代码。这用于平台特定代码、调试构建和功能标志。#ifdef 检查宏是否已定义;#if 评估其值。#elif 和 #else 提供替代方案。

c
#define DEBUG 1

#if DEBUG
    printf("Debug: x=%d\n", x);
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

包含保护

包含保护防止头文件重复包含,否则会导致重定义错误。#ifndef/#define/#endif 模式是标准 C。#pragma once 是更简单、广泛支持的替代方案(非标准但适用于所有主要编译器)。

c
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point { int x, y; };
void init_point(struct Point *p);

#endif // MYHEADER_H

// Alternative (non-standard but widely supported):
#pragma once

#pragma 与编译器提示

#pragma 提供编译器特定指令。#pragma once 是更简单的包含保护。#pragma pack 控制结构体内存布局(对二进制协议很重要)。__attribute__(GCC/Clang)为函数注解优化、弃用和警告。

c
#pragma once              // include guard
#pragma pack(1)            // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Common pragmas
#pragma message("Compiling " __FILE__)

// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);

字符串化与标记粘贴

#(字符串化)将宏参数转换为字符串字面量。##(标记粘贴)将标记连接成新标识符。两级 STR/XSTR 模式确保宏在字符串化前展开。这些用于代码生成和日志宏。

c
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b

printf("%s\n", STR(Hello World));  // "Hello World"
printf("%s\n", XSTR(VERSION));      // expands VERSION first

int CONCAT(foo, bar) = 42;  // creates variable foobar
printf("%d\n", foobar);     // 42
11

位运算

基本位运算符

位运算符操作单个位。AND(&)屏蔽位(仅保留设置的位),OR(|)设置位,XOR(^)切换位,NOT(~)反转所有位。左移(<<)乘以 2 的幂,右移(>>)除以(对于无符号)。位操作始终使用无符号类型——有符号右移是实现定义的(可能符号扩展)。位运算极快(单 CPU 周期),用于标志、硬件寄存器、压缩和加密。二进制字面量(0b 前缀)是 C23/C++14;在旧 C 中使用十六进制(0x)或十进制。

c
#include <stdio.h>

int main() {
    unsigned int a = 0b1100;  // 12
    unsigned int b = 0b1010;  // 10

    // AND: both bits must be 1
    printf("%u\n", a & b);   // 8  (0b1000)

    // OR: either bit is 1
    printf("%u\n", a | b);   // 14 (0b1110)

    // XOR: bits differ (exclusive or)
    printf("%u\n", a ^ b);   // 6  (0b0110)

    // NOT: flip all bits
    printf("%u\n", ~a);      // 4294967283 (on 32-bit)

    // Left shift: multiply by 2^n
    printf("%u\n", a << 2);  // 48 (12 * 4)

    // Right shift: divide by 2^n (unsigned)
    printf("%u\n", a >> 1);  // 6  (12 / 2)

    return 0;
}

设置、清除与切换位

位标志将多个布尔选项打包到单个整数中,节省内存。三个核心操作:SET(|= mask)、CLEAR(&= ~mask)、TOGGLE(^= mask)、CHECK(& mask)。使用 #define 和 (1 << n) 获得可读的标志名。这种模式在系统编程中无处不在(文件权限、设备控制、配置选项)。例如,Unix 文件权限(rwxr-xr-x = 0755)使用位标志。标志始终使用无符号整数以避免符号扩展问题。这比 bool 数组更节省内存(每个标志 1 位 vs 8 位)。

c
#include <stdio.h>

// Flag definitions (powers of 2)
#define FLAG_READ    (1 << 0)  // 0b0001
#define FLAG_WRITE   (1 << 1)  // 0b0010
#define FLAG_EXECUTE (1 << 2)  // 0b0100
#define FLAG_ADMIN   (1 << 3)  // 0b1000

int main() {
    unsigned int permissions = 0;

    // SET a bit (OR with mask)
    permissions |= FLAG_READ | FLAG_WRITE;  // 0b0011

    // CHECK if a bit is set (AND, compare to 0)
    if (permissions & FLAG_READ) {
        printf("Read permission granted\n");
    }

    // CLEAR a bit (AND with inverted mask)
    permissions &= ~FLAG_WRITE;  // 0b0001

    // TOGGLE a bit (XOR with mask)
    permissions ^= FLAG_EXECUTE;  // 0b0101 (execute now on)
    permissions ^= FLAG_EXECUTE;  // 0b0001 (execute now off)

    // SET multiple bits at once
    permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;

    printf("Permissions: 0x%X\n", permissions);  // 0xD
    return 0;
}

位操作技巧

位技巧利用二进制表示提高速度。x & 1 测试奇偶(比取模快)。x & (x-1) 清除最低设置位——适用于检查 2 的幂和计数位。__builtin_popcount(GCC/Clang)或 __popcnt(MSVC)在现代 CPU 上用一条指令计数设置位。XOR 交换(a^=b; b^=a; a^=b)避免临时变量但在现代 CPU 上更慢且可读性差——避免使用。'向上取整到 2 的幂' 技巧将最高设置位传播到所有低位,然后加 1。这些技巧在嵌入式系统、游戏引擎和性能关键代码中很有用。

c
#include <stdio.h>

int main() {
    int x = 42;

    // Check if odd/even (faster than x % 2)
    if (x & 1) printf("odd\n"); else printf("even\n");

    // Check if power of 2 (only one bit set)
    // x & (x-1) clears the lowest set bit
    if (x && !(x & (x - 1))) printf("power of 2\n");

    // Count set bits (popcount / Hamming weight)
    unsigned int n = 0b10110110;
    int count = 0;
    while (n) { count += n & 1; n >>= 1; }
    printf("Set bits: %d\n", count);  // 6
    // Or use __builtin_popcount(n) (GCC/Clang)

    // Swap two values without temp (XOR swap)
    int a = 5, b = 10;
    a ^= b; b ^= a; a ^= b;
    // a=10, b=5 (avoid in practice — less readable)

    // Get lowest set bit
    unsigned int lowest = x & (-x);  // isolates lowest 1-bit

    // Round up to next power of 2
    unsigned int v = 5;
    v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;

    return 0;
}

结构体中的位域

位域将多个小值打包到单个结构体中,节省内存。冒号语法(unsigned int field : N)指定位宽。编译器自动处理位提取/插入。这适用于内存受限系统、网络协议和硬件寄存器映射。但是,位域布局是实现定义的(字节顺序、对齐、填充)——不要将位域用于跨平台二进制兼容性。对于可移植二进制格式,使用显式位屏蔽(#define + & |)。未命名字段(: 5)添加填充。总大小向上舍入到结构体的对齐。

c
#include <stdio.h>

// Bit fields: pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31  (5 bits)
    unsigned int month : 4;   // 0-15  (4 bits)
    unsigned int year  : 12;  // 0-4095 (12 bits)
    unsigned int is_leap : 1; // 0 or 1 (1 bit)
};  // Total: 22 bits (padded to 32)

struct Flags {
    unsigned int visible  : 1;
    unsigned int editable : 1;
    unsigned int locked   : 1;
    unsigned int          : 5;  // unnamed padding (5 bits)
    unsigned int priority : 4;  // 0-15
};

int main() {
    struct Date d = { 15, 6, 2024, 0 };
    printf("Size: %zu bytes\n", sizeof(d));  // 4 bytes
    printf("Date: %u/%u/%u\n", d.day, d.month, d.year);

    struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
    printf("Size: %zu bytes\n", sizeof(f));  // 4 bytes

    return 0;
}

实用位操作(RGB 颜色)

将多个值打包到一个整数中在图形、网络和嵌入式系统中很常见。RGB 颜色将三个 8 位通道打包到 24 位(0xRRGGBB)。左移(<<)定位每个通道,OR(|)组合它们。右移(>>)和屏蔽(& 0xFF)提取单个通道。这节省内存(1 个 int vs 3 个字节)并启用原子操作。相同模式适用于网络字节排序、硬件寄存器访问和数据压缩。始终使用固定宽度类型(uint8_t、uint32_t)以获得可移植性——int 大小因平台而异。

c
#include <stdio.h>
#include <stdint.h>

// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Extract components
uint8_t get_red(uint32_t color)   { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8)  & 0xFF; }
uint8_t get_blue(uint32_t color)  { return color & 0xFF; }

// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
    uint8_t r = (get_red(c1) + get_red(c2)) / 2;
    uint8_t g = (get_green(c1) + get_green(c2)) / 2;
    uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
    return make_color(r, g, b);
}

int main() {
    uint32_t red   = make_color(255, 0, 0);    // 0xFF0000
    uint32_t blue  = make_color(0, 0, 255);    // 0x0000FF
    uint32_t purple = blend(red, blue);          // 0x7F007F

    printf("Red:   0x%06X\n", red);
    printf("Blue:  0x%06X\n", blue);
    printf("Mix:   0x%06X (R=%d G=%d B=%d)\n",
           purple, get_red(purple), get_green(purple), get_blue(purple));

    return 0;
}
12

信号处理

基本信号处理

信号是发送给进程的软件中断(例如,Ctrl+C 发送 SIGINT,除零发送 SIGFPE)。signal() 注册处理函数。在处理函数内部,仅允许异步信号安全函数——printf、malloc 和大多数 stdlib 函数不安全,因为主程序可能在调用中途被中断。使用 write() 输出。常见信号:SIGINT(Ctrl+C)、SIGTERM(终止请求)、SIGKILL(强制杀死,无法捕获)、SIGSEGV(段错误)、SIGALRM(定时器)。出于可移植性和控制,优先使用 sigaction() 而非 signal()。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function (must match signature)
void handler(int sig) {
    // WARNING: only async-signal-safe functions allowed here!
    // printf is NOT safe — use write() instead
    const char *msg = "Caught SIGINT\n";
    write(STDOUT_FILENO, msg, 14);
}

int main() {
    // Register handler for Ctrl+C (SIGINT)
    signal(SIGINT, handler);

    // Ignore SIGINT entirely
    // signal(SIGINT, SIG_IGN);

    // Reset to default behavior (terminate)
    // signal(SIGINT, SIG_DFL);

    printf("PID %d waiting. Press Ctrl+C...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

sigaction(可移植信号处理)

sigaction() 是处理信号的现代、可移植方式(signal() 的行为因平台而异)。sa_sigaction 处理函数接收带详细信息的 siginfo_t:si_pid(发送者 PID)、si_uid(发送者 UID)、si_signo(信号编号)、si_code(原因)。SA_SIGINFO 标志启用三参数处理函数。sa_mask 在处理函数执行期间阻塞指定信号(防止嵌套中断)。其他标志:SA_RESTART(自动重启中断的系统调用)、SA_NOCLDWAIT(无僵尸子进程)。在生产代码中始终使用 sigaction()——signal() 在某些平台上不可靠。

c
#include <stdio.h>
#include <signal.h>
#include <string.h>

void handler(int sig, siginfo_t *info, void *context) {
    // siginfo_t provides details about the signal
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = handler;  // use sa_sigaction (not sa_handler)
    sa.sa_flags = SA_SIGINFO;   // enable siginfo_t parameter

    // Block other signals during handler execution
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);  // block SIGQUIT during handler

    // Register (more portable than signal())
    sigaction(SIGINT, &sa, NULL);

    // Send a signal to self
    raise(SIGINT);  // like kill(getpid(), SIGINT)

    printf("Done\n");
    return 0;
}

发送信号与闹钟

alarm(seconds) 在指定时间后调度 SIGALRM——适用于超时。pause() 阻塞直到任何信号到达。volatile sig_atomic_t 是在信号处理函数和主代码之间共享数据的唯一安全方式——volatile 防止编译器优化,sig_atomic_t 保证原子访问。kill(pid, signal) 向另一个进程发送信号。raise(sig) 向自己发送信号。SIGKILL(9)和 SIGSTOP 无法捕获或忽略——它们始终有效。SIGTERM(15)是礼貌的终止请求(程序可以捕获它进行清理)。使用 alarm() 实现简单超时;使用 setitimer()/timer_create() 获得更多控制。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_alarm = 0;

void alarm_handler(int sig) {
    got_alarm = 1;  // safe: sig_atomic_t is atomic
}

int main() {
    signal(SIGALRM, alarm_handler);

    // Set a timer: deliver SIGALRM after 3 seconds
    alarm(3);
    printf("Waiting for alarm...\n");

    // Wait for the alarm
    while (!got_alarm) {
        pause();  // sleep until any signal arrives
    }
    printf("Alarm fired!\n");

    // Send signal to another process
    // kill(pid, SIGTERM);  // request termination
    // kill(pid, SIGKILL);  // force kill (can't be caught)

    // Send signal to self
    raise(SIGUSR1);

    return 0;
}

常见信号参考

理解信号对 Unix 编程至关重要。SIGKILL(9)和 SIGSTOP 无法捕获——它们是最后手段。SIGTERM 是标准的优雅关闭信号(捕获它以保存状态)。SIGINT 是 Ctrl+C(交互式中断)。SIGCHLD 在子进程退出时触发——如果不 wait() 它,子进程变为僵尸。将 SIGCHLD 设置为 SIG_IGN 自动回收子进程(或使用 SA_NOCLDWAIT)。SIGPIPE 在写入关闭的管道/套接字时触发——大多数服务器忽略它(signal(SIGPIPE, SIG_IGN))并改为检查 write() 返回值。在信号处理函数中使用 _exit()(而非 exit())——exit() 运行可能不是信号安全的 atexit 处理函数。

c
#include <signal.h>

// Common POSIX signals:
// SIGINT  (2)  - Ctrl+C interrupt (terminate)
// SIGQUIT (3)  - Ctrl+\ quit (core dump)
// SIGKILL (9)  - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE  (8)  - Arithmetic error (divide by zero)
// SIGBUS  (7)  - Bus error (misaligned access)

// Graceful shutdown pattern:
void cleanup_handler(int sig) {
    // Save state, close files, release resources
    // Then exit cleanly
    _exit(0);  // use _exit in signal handlers (not exit)
}

// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);

// Prevent zombie children:
signal(SIGCHLD, SIG_IGN);  // auto-reap children

自管道技巧(信号安全唤醒)

自管道技巧解决了一个基本问题:信号处理函数无法安全地做复杂工作,但您需要在主循环中响应信号。解决方案:处理函数向管道写入一个字节,主循环使用 select()/poll() 检测它。这安全地将信号与事件循环集成。处理函数仅调用 write()(异步信号安全)。现代替代方案:signalfd()(Linux 特定,将信号直接转换为文件描述符)或 pselect()(在 select 期间原子地阻塞信号)。此模式用于事件驱动服务器(nginx、Redis)以无竞争条件地处理信号。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int pipe_fd[2];  // [0]=read, [1]=write

void handler(int sig) {
    // Write one byte to the pipe — wakes up select()/poll()
    write(pipe_fd[1], &sig, sizeof(sig));
}

int main() {
    pipe(pipe_fd);
    // Make read end non-blocking
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);

    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("Waiting (select-based)...\n");

    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(pipe_fd[0], &readfds);

        // select() blocks until pipe is writable (signal received)
        int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
        if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
            int sig;
            read(pipe_fd[0], &sig, sizeof(sig));
            printf("Handled signal %d in main loop\n", sig);
            if (sig == SIGTERM) break;
        }
    }
    return 0;
}
13

进程 Fork 与 Exec

fork() 基础

fork() 创建当前进程的精确副本——唯一区别是返回值:子进程中为 0,父进程中为子进程的 PID。两个进程从 fork() 调用继续。子进程获得父进程内存的副本(写时复制优化此操作)。始终检查所有三种情况:pid < 0(错误)、pid == 0(子进程)、pid > 0(父进程)。waitpid() 阻塞直到子进程退出并检索其状态。WIFEXITED 检查是否正常退出,WEXITSTATUS 获取退出代码。如果不 wait(),子进程变为僵尸直到被回收。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // create a child process

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // CHILD process (fork returned 0)
        printf("Child: PID=%d, Parent PID=%d\n",
               getpid(), getppid());
        sleep(2);
        printf("Child exiting\n");
        return 42;  // child exit code
    } else {
        // PARENT process (fork returned child's PID)
        printf("Parent: PID=%d, Child PID=%d\n",
               getpid(), pid);

        int status;
        waitpid(pid, &status, 0);  // wait for child

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

exec 家族(替换进程映像)

exec 用新程序替换当前进程映像——PID 保持不变,但代码、数据和栈被替换。exec 仅在失败时返回。命名约定:'l' = 列表参数(可变,NULL 终止),'v' = 参数向量/数组,'p' = 搜索 PATH 查找可执行文件,'e' = 自定义环境。第一个参数按约定是程序名(argv[0])。fork()+exec() 是 Unix 启动程序的方式——fork 创建进程,exec 加载新程序。这种分离允许在 fork 和 exec 之间设置文件描述符、环境和信号。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: replace self with a new program
        // exec never returns on success (only on failure)

        // execlp: search PATH, list arguments
        execlp("ls", "ls", "-la", "/tmp", NULL);

        // execvp: search PATH, array of arguments
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);

        // execl: full path, list arguments
        execl("/bin/ls", "ls", "-la", NULL);

        // Only reached if exec failed
        perror("exec failed");
        _exit(1);
    } else {
        wait(NULL);  // parent waits for child
        printf("Child finished\n");
    }
    return 0;
}

// exec variants:
// execl  (path, arg1, arg2, ..., NULL)        — list args, full path
// execlp (file, arg1, arg2, ..., NULL)        — list args, search PATH
// execv  (path, argv[])                        — array args, full path
// execvp (file, argv[])                        — array args, search PATH
// execve (path, argv[], envp[])                — array args, custom env

僵尸与孤儿进程

当子进程退出但父进程尚未调用 wait() 时产生僵尸——内核保留进程表条目(PID、退出状态)直到被回收。僵尸浪费 PID 并可能耗尽进程表。修复:始终为子进程 wait(),或将 SIGCHLD 设置为 SIG_IGN(内核自动回收)。当父进程在子进程之前退出时产生孤儿——init/systemd(PID 1)收养孤儿并在它退出时回收。双重 fork 模式(fork,子进程再 fork,第一个子进程退出)创建自动重新父化到 init 的守护进程,脱离终端。用 'ps aux | grep Z' 或 'top' 监控僵尸。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child PID=%d\n", getpid());
        _exit(0);  // child exits immediately
    }

    // If parent doesn't wait(), child becomes a ZOMBIE
    // (process table entry remains until reaped)
    sleep(5);  // parent sleeps — child is now a zombie
    // Run 'ps' during this window to see the zombie (state 'Z')

    // Reap the zombie:
    int status;
    waitpid(pid, &status, 0);
    printf("Zombie reaped\n");

    // Orphan: if parent exits before child
    pid_t pid2 = fork();
    if (pid2 == 0) {
        sleep(3);  // parent will exit first
        printf("Orphan adopted by init (PID 1), new parent=%d\n",
               getppid());
        _exit(0);
    }
    // Parent exits immediately — child becomes orphan
    // init/systemd (PID 1) adopts and reaps it

    // Prevent zombies: ignore SIGCHLD
    // signal(SIGCHLD, SIG_IGN);  // kernel auto-reaps children
    // Or use SA_NOCLDWAIT with sigaction

    return 0;
}

守护进程创建

守护进程是在没有终端的情况下运行的后台进程(例如,Web 服务器、数据库)。守护进程化步骤:fork+exit 脱离 shell,setsid() 创建新会话(无控制终端),再次 fork 以确保安全,chdir('/') 避免持有文件系统,设置 umask 以获得可预测的文件权限,关闭/重定向 stdio 到 /dev/null。双重 fork 是 Unix 约定,防止守护进程通过 open() 重新获取终端。现代系统提供 systemd 服务文件用于守护进程管理,但理解手动守护进程化对嵌入式系统和可移植代码仍然重要。记录到文件(而非 stdout),因为 stdout 是 /dev/null。

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize() {
    // 1. Fork and exit parent (child continues in background)
    pid_t pid = fork();
    if (pid > 0) exit(0);  // parent exits
    if (pid < 0) exit(1);

    // 2. Create new session (detach from controlling terminal)
    setsid();

    // 3. Fork again (prevent reacquiring a terminal)
    pid = fork();
    if (pid > 0) exit(0);
    if (pid < 0) exit(1);

    // 4. Change working directory to / (don't hold filesystem)
    chdir("/");

    // 5. Set umask to 0 (full control over file permissions)
    umask(0);

    // 6. Close standard file descriptors (detach from terminal)
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 7. Redirect them to /dev/null (in case code writes to them)
    open("/dev/null", O_RDWR);  // fd 0 = stdin
    dup(0);                      // fd 1 = stdout
    dup(0);                      // fd 2 = stderr
}

int main() {
    daemonize();
    // Now running as a daemon (background, no terminal)
    while (1) {
        // Daemon work here (e.g., log to file, listen on socket)
        sleep(60);
    }
    return 0;
}

进程间通信(IPC)概述

IPC 让进程通信。管道最简单(父子,单向)。命名管道(FIFO)通过文件系统路径在不相关进程之间工作。共享内存最快(零拷贝)但需要同步(信号量/互斥锁)。套接字最灵活(双向,支持网络)。消息队列提供结构化、有消息边界的通信。信号最简(仅一个数字)。根据需要选择:简单父子用管道,高性能数据共享用共享内存,网络通信用套接字。System V IPC(shmget、semget)较旧;POSIX IPC(shm_open、sem_open)更清晰但可用性较低。

c
#include <stdio.h>
// C provides several IPC mechanisms:

// 1. PIPES: unidirectional byte stream between parent/child
//    pipe(fd) creates fd[0]=read, fd[1]=write
//    Only works between related processes (fork)

// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
//    mkfifo("/tmp/myfifo", 0666);
//    Works between unrelated processes

// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
//    shmget/shmat (System V) or shm_open/mmap (POSIX)

// 4. MESSAGE QUEUES: structured messages (not byte stream)
//    msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)

// 5. SEMAPHORES: synchronization (not data transfer)
//    semget/semop (System V) or sem_open (POSIX)

// 6. SOCKETS: bidirectional, works across machines (network)
//    socket/bind/listen/accept/connect

// 7. SIGNALS: minimal data (just signal number)
//    kill(pid, SIGUSR1)

// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queues
14

管道与 IPC

匿名管道(父子)

管道在相关进程(由 fork 创建)之间提供单向通信。pipe(fd) 创建两个文件描述符:fd[0] 用于读取,fd[1] 用于写入。关键:在每个进程中关闭未使用的一端——父进程关闭读端,子进程关闭写端。如果不关闭写端,子进程的 read() 永远阻塞(等待更多数据)。read() 仅在所有写端关闭时返回 0(EOF)。管道有固定缓冲区(通常 64KB)——缓冲区满时 write() 阻塞。管道非常适合父子通信和管道 shell 命令(ls | grep)。

c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];  // fd[0]=read end, fd[1]=write end
    pipe(fd);   // create pipe

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: read from pipe
        close(fd[1]);  // close unused write end

        char buf[256];
        int n = read(fd[0], buf, sizeof(buf));
        buf[n] = '\0';
        printf("Child received: %s", buf);

        close(fd[0]);
    } else {
        // PARENT: write to pipe
        close(fd[0]);  // close unused read end

        const char *msg = "Hello from parent!\n";
        write(fd[1], msg, strlen(msg));

        close(fd[1]);  // close write end → child's read returns 0 (EOF)
        wait(NULL);
    }
    return 0;
}

命名管道(FIFO)

命名管道(FIFO)是带文件系统名称的管道——它们在不相关进程之间工作。mkfifo() 创建管道文件;open() 阻塞直到读端和写端都连接(内置同步)。FIFO 持续存在直到 unlink()'d(不同于进程退出时消失的匿名管道)。它们适用于独立程序之间的简单 IPC。open() 上的阻塞行为确保写端在读者就绪前不开始。使用 O_NONBLOCK 进行非阻塞打开。FIFO 是单向的——对于双向通信,使用两个 FIFO 或套接字。命名管道常用于 shell 脚本和系统服务。

c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

// Process A (writer):
int writer_main() {
    mkfifo("/tmp/myfifo", 0666);  // create named pipe

    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello via FIFO!", 15);
    close(fd);
    return 0;
}

// Process B (reader) — can be a completely separate program:
int reader_main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[256];
    int n = read(fd, buf, sizeof(buf));
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");

// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)

共享内存(最快 IPC)

共享内存是最快的 IPC——两个进程映射相同的物理 RAM,因此数据传输是零拷贝。shmget() 创建段,shmat() 将其附加到进程地址空间,shmdt() 分离,shmctl(IPC_RMID) 销毁。关键警告:共享内存不提供同步——如果两个进程同时访问,会产生数据竞争。您必须使用信号量、互斥锁或其他同步来协调访问。ftok() 从文件路径生成键(两个进程必须就键达成一致)。完成后始终销毁共享内存(它在进程退出后持续存在,泄漏内存)。POSIX 共享内存(shm_open/mmap)是更清晰的替代方案。

c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp/shmfile", 65);  // generate unique key

    // Create shared memory segment
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: attach and read
        char *shared = (char *)shmat(shmid, NULL, 0);
        sleep(1);  // wait for parent to write
        printf("Child reads: %s\n", shared);
        shmdt(shared);  // detach
    } else {
        // PARENT: attach and write
        char *shared = (char *)shmat(shmid, NULL, 0);
        strcpy(shared, "Hello from shared memory!");
        printf("Parent wrote to shared memory\n");
        shmdt(shared);
        wait(NULL);

        // Destroy shared memory after use
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.

dup2 与重定向

dup2(oldfd, newfd) 使 newfd 成为 oldfd 的副本——这是 shell 重定向的工作方式。要将 stdout 重定向到管道:dup2(pipe_write, STDOUT_FILENO)——现在 printf/write 到 stdout 进入管道。要将 stdin 从管道重定向:dup2(pipe_read, STDIN_FILENO)——现在 scanf/read 从 stdin 来自管道。这正是 shell 实现管道(ls | sort)、重定向(ls > file)和输入(sort < file)的方式。dup2 后,关闭原始 fd(它已被复制)。此模式是以编程方式构建 Unix 管道的基础,被 shell、popen() 和进程管理库使用。

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// Implement shell-like pipeline: ls | sort
int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // First child: ls (writes to pipe instead of stdout)
        close(pipefd[0]);                    // close read end
        dup2(pipefd[1], STDOUT_FILENO);      // stdout → pipe write
        close(pipefd[1]);                    // close original (dup'd)
        execlp("ls", "ls", NULL);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // Second child: sort (reads from pipe instead of stdin)
        close(pipefd[1]);                    // close write end
        dup2(pipefd[0], STDIN_FILENO);       // stdin → pipe read
        close(pipefd[0]);                    // close original (dup'd)
        execlp("sort", "sort", NULL);
    }

    // Parent: close both ends and wait
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

popen(高级管道)

popen() 是 fork+pipe+exec+shell 的高级包装器——它通过 /bin/sh 运行命令并返回 FILE* 用于读取输出('r')或写入输入('w')。它比手动 fork/pipe/exec 简单得多,但通过 shell 运行,因此切勿传递不受信任的输入(shell 注入风险)。在返回的 FILE* 上使用 fgets/fprintf 像常规文件一样。pclose() 关闭管道并等待子进程退出(返回其状态)。对于不受信任的输入,直接使用 fork+execvp(无 shell)。popen 非常适合快速脚本、系统管理工具和读取命令输出。对于双向通信,使用 socketpair() 或两个管道。

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    // popen opens a process with a pipe (like shell command | ...)
    FILE *fp = popen("ls -la /tmp", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // Read command output line by line
    char buf[256];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf(">> %s", buf);
    }

    pclose(fp);  // closes pipe and waits for child

    // Writing to a process (like ... | command):
    FILE *wp = popen("grep hello", "w");
    fprintf(wp, "hello world\n");
    fprintf(wp, "goodbye\n");
    pclose(wp);  // grep outputs "hello world"

    return 0;
}

// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted input
15

Makefile 与构建工具

基本 Makefile 结构

Make 自动化编译。Makefile 有规则:target(要构建的文件)、prerequisites(依赖)和 recipe(shell 命令,TAB 缩进)。变量(CC、CFLAGS)集中配置。自动变量:$@(目标名)、$<(第一个先决条件)、$^(所有先决条件)。模式规则(%.o: %.c)为所有源文件概括编译。.PHONY 声明不是文件的目标(clean、all、install)。第一条规则是默认的(不带参数的 make 构建 'all')。Make 跟踪文件时间戳——仅在先决条件比目标新时重新构建。这种增量构建节省大型项目的时间。

c
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
#               recipe (must start with TAB, not spaces)

# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # substitute .c with .o

# Default target (first rule)
all: $(TARGET)

# Link object files into executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# Compile each .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build artifacts
clean:
	rm -f $(OBJS) $(TARGET)

# Run the program
run: $(TARGET)
	./$(TARGET)

# Phony targets (not files)
.PHONY: all clean run

# Usage:
# $ make        — builds 'all' (the default)
# $ make clean  — removes build files
# $ make run    — builds and runs
# $ make -j4    — parallel build (4 jobs)

自动变量与模式规则

自动变量使 Makefile 简洁且可维护。$@(目标)、$<(第一个先决条件)和 $^(所有先决条件)最常见。模式规则(%.o: %.c)让您为所有源文件编写一条规则——% 匹配任何字符串。$(wildcard) 查找匹配 glob 的文件,$(patsubst) 转换字符串——它们一起自动发现源。@ 前缀抑制命令回显。静态模式规则(target: %.o: %.c)应用于特定列表。理解这些功能消除了重复规则,使 Makefile 扩展到大型项目。recipe 缩进始终使用 TAB(而非空格)——Make 对此很严格。

c
# Automatic variables in recipes:
# $@   — the target filename
# $<   — the first prerequisite
# $^   — all prerequisites (no duplicates)
# $+   — all prerequisites (with duplicates)
# $?   — prerequisites newer than the target
# $*   — the stem (matching % part)

# Example showing all automatic variables:
program: main.o utils.o
	@echo "Target: $@"        # program
	@echo "First dep: $<"     # main.o
	@echo "All deps: $^"      # main.o utils.o
	@echo "Newer deps: $?"    # (whichever changed)
	gcc -o $@ $^

# Pattern rule: compile any .c to .o
%.o: %.c
	gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)

# Static pattern rule (specific files):
$(OBJS): %.o: %.c
	gcc -c $< -o $@

# Built-in functions:
SRCS = $(wildcard src/*.c)           # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))  # path substitution
DIRS = $(sort $(dir $(SRCS)))         # unique directories

依赖与头文件

跟踪头文件依赖至关重要——没有它,更改 .h 文件不会触发包含它的 .c 文件重新编译,导致陈旧构建。解决方案:gcc -MMD -MP 生成列出所有依赖(包括头文件)的 .d 文件。-include 将这些拉入 Makefile。-MP 为头文件添加伪目标(如果头文件被删除则防止错误)。这是 C/C++ 项目的标准方法。没有它,您必须手动列出每个头文件依赖——对大型项目不可管理。第一次构建不会有 .d 文件(-include 中的 '-' 抑制错误);它们在编译期间创建并在后续构建中使用。

c
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD

CC = gcc
CFLAGS = -Wall -MMD -MP  # generate .d dependency files

SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)    # dependency files

all: myapp

myapp: $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Include auto-generated dependency files
-include $(DEPS)  # '-' means don't error if missing

clean:
	rm -f $(OBJS) $(DEPS) myapp

.PHONY: all clean

# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automatically

多目录项目 Makefile

真实项目跨多个目录。此 Makefile 自动发现源(wildcard),将它们映射到构建目录(patsubst),并根据需要创建目录。| 语法创建仅顺序先决条件——$(BUILDDIR) 在编译前创建,但其时间戳更改不会触发重新构建(没有 |,创建目录会使每次都重新构建所有内容)。-Iinclude 告诉 gcc 在哪里查找头文件。-MMD 在构建目录中生成依赖文件。此结构保持源、构建和二进制目录分离——易于清理(rm -rf build)且不污染源树。对于非常大的项目,考虑 CMake 或 Meson。

c
# Project structure:
# project/
#   src/       — source files
#   include/   — header files
#   build/     — object files (created by make)
#   bin/       — final executable

CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin

TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) -o $@ $^

# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(BUILDDIR):
	mkdir -p $(BUILDDIR)

$(BINDIR):
	mkdir -p $(BINDIR)

-include $(DEPS)

clean:
	rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changes

CMake 基础(Make 的替代)

CMake 是元构建系统——它从 CMakeLists.txt 文件生成 Makefile(或 Ninja、Visual Studio、Xcode 项目)。它是 C/C++ 项目的事实标准,因为它处理跨平台编译、依赖检测和 IDE 集成。关键命令:project() 设置项目名,add_executable() 定义构建目标,target_include_directories() 添加头文件路径,target_link_libraries() 链接库。源外构建(mkdir build && cd build && cmake ..)保持源树清洁。CMake 按平台自动检测编译器和标志。对于新的 C/C++ 项目,优先使用 CMake 而非原始 Makefile——它更可维护且可移植。

c
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)

cmake_minimum_required(VERSION 3.10)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)

# Include directory
target_include_directories(myapp PRIVATE include)

# Link a library
target_link_libraries(myapp m)  # math library (-lm)

# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")

# Usage:
# $ mkdir build && cd build
# $ cmake ..            # generate Makefiles
# $ make                # build
# $ make install        # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug  # debug build

# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projects
16

函数指针深入

函数指针语法与用法

函数指针存储函数的地址,启用运行时分派。语法 int (*fp)(int, int) 臭名昭著地令人困惑——将其读作 'fp 是指向接受 (int, int) 返回 int 的函数的指针'。typedef 简化此操作:typedef int (*math_func)(int, int) 创建可读别名。函数名退化为指针(类似数组名),因此 'add' 和 '&add' 等价。函数指针启用回调、事件处理程序、策略模式和分派表(函数指针数组用于类似 switch 的分派)。它们是 qsort 比较器和 GUI 事件系统的基础。

c
#include <stdio.h>

// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare a function pointer
    int (*operation)(int, int);

    // Assign (function name decays to pointer)
    operation = add;          // or &add
    printf("5 + 3 = %d\n", operation(5, 3));  // 8

    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    // typedef for readability
    typedef int (*math_func)(int, int);
    math_func fn = add;
    printf("Result: %d\n", fn(10, 20));

    // Array of function pointers (dispatch table)
    math_func ops[] = {add, subtract};
    printf("op[0](4,2)=%d  op[1](4,2)=%d\n",
           ops[0](4, 2), ops[1](4, 2));

    return 0;
}

回调(qsort 示例)

qsort 是函数指针作为回调的经典示例。比较器接收 const void* 指针(通用)并返回指示顺序的整数。qsort 调用您的比较器决定元素排序——您通过传递不同函数控制排序行为。这是 C 中的策略模式:算法(qsort)固定,但比较逻辑被注入。void* 启用通用编程(排序任何类型)。比较器必须是纯函数(无副作用)且一致(如果 a<b 且 b<c 则 a<c)。此模式贯穿 C 标准库(bsearch、atexit、signal)。

c
#include <stdio.h>
#include <stdlib.h>

// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
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 main() {
    int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer as the comparator
    qsort(arr, n, sizeof(int), compare_asc);
    printf("Ascending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // Same array, different comparator (descending)
    qsort(arr, n, sizeof(int), compare_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

带函数指针的结构体(C 中的 OOP)

C 可以使用带函数指针的结构体模拟 OOP——这就是 C++ vtable 内部的工作方式。结构体包含指向 'vtable'(虚函数表)的指针,其中包含函数指针。每个 '子类'(Circle、Square)有自己的 vtable 及其实现。将 Circle* 转换为 Shape* 启用多态——print_shape() 通过 vtable 调用正确的 area() 函数。此模式用于真实 C 代码:Linux 内核(设备驱动程序)、GObject(GTK)和 SQLite。它提供封装、继承(通过结构体嵌入)和多态。虽然比 C++ 冗长,但它提供对内存布局和虚分派的完全控制。

c
#include <stdio.h>

// Simulating OOP with structs + function pointers
typedef struct Shape Shape;

// Virtual function table (vtable)
typedef struct {
    double (*area)(Shape *);
    double (*perimeter)(Shape *);
    void (*describe)(Shape *);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vtable;  // pointer to virtual functions
    char name[32];
};

// Circle implementation
typedef struct {
    Shape base;      // inherit from Shape
    double radius;
} Circle;

double circle_area(Shape *s) {
    return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}

double circle_perimeter(Shape *s) {
    return 2 * 3.14159 * ((Circle *)s)->radius;
}

static const ShapeVTable circle_vtable = {
    circle_area, circle_perimeter, NULL
};

Circle *circle_create(double r) {
    Circle *c = malloc(sizeof(Circle));
    c->base.vtable = &circle_vtable;
    strcpy(c->base.name, "Circle");
    c->radius = r;
    return c;
}

// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
    printf("%s: area=%.2f, perimeter=%.2f\n",
           s->name, s->vtable->area(s), s->vtable->perimeter(s));
}

带回调的事件驱动编程

函数指针在 C 中启用事件驱动架构——发布/订阅模式。处理程序通过 on_event() 注册(订阅),emit_event() 调用所有注册的处理程序(发布)。这将事件生产者与消费者解耦——发射器不知道处理程序做什么。此模式是 GUI 框架(按钮点击 -> 处理程序)、游戏引擎(碰撞 -> 回调)和异步 I/O(数据就绪 -> 读取处理程序)的基础。处理程序签名(事件名 + void* data)对任何事件类型都足够通用。在生产环境中,添加错误处理(如果处理程序崩溃怎么办?)、优先级排序和取消订阅的能力。这是 libuv、libevent 和 Node.js 底层的工作方式。

c
#include <stdio.h>

// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);

// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;

void on_event(EventHandler handler) {
    if (handler_count < MAX_HANDLERS) {
        handlers[handler_count++] = handler;
    }
}

void emit_event(const char *event, void *data) {
    for (int i = 0; i < handler_count; i++) {
        handlers[i](event, data);  // call each registered handler
    }
}

// Concrete handlers
void log_handler(const char *event, void *data) {
    printf("[LOG] Event: %s\n", event);
}

void alert_handler(const char *event, void *data) {
    if (strcmp(event, "error") == 0) {
        printf("[ALERT] Error occurred!\n");
    }
}

int main() {
    // Register handlers (subscribe)
    on_event(log_handler);
    on_event(alert_handler);

    // Emit events (publish)
    emit_event("click", NULL);
    emit_event("error", NULL);
    emit_event("scroll", NULL);

    return 0;
}

函数指针陷阱

函数指针有几个陷阱。调用 NULL 函数指针会崩溃(段错误)——调用前始终检查 NULL。转换为错误签名是未定义行为(调用约定可能不同)。比较函数指针相等性是有效的(相同函数),但排序(<、>)是未定义的。始终一致使用 typedef——函数指针语法容易出错,typedef 使声明可读且可维护。在 C 中,函数指针是实现运行时多态和回调的唯一方式,因此掌握它们至关重要。C++ 添加了 std::function、lambda 和虚函数作为更安全的替代方案。

c
#include <stdio.h>

// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
    void (*fp)(void) = NULL;
    fp();  // SEGFAULT — always check for NULL
    if (fp) fp();  // safe
}

// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
    void (*fp)(void) = (void (*)(void))takes_int;  // WRONG cast
    fp();  // UB: missing argument, garbage value
}

// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
    // Returning pointer to local function is OK (functions aren't local)
    // But returning a pointer to a local VARIABLE is not
    return NULL;  // functions have static storage, safe to return
}

// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
    int (*p1)(int) = f1;
    int (*p2)(int) = f1;
    if (p1 == p2) printf("Same function\n");  // OK
    // Comparing p1 == f2 is valid but they're different functions
}

// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistent
17

可变参数(varargs)

基本可变参数函数(stdarg)

可变参数函数使用 stdarg.h 接受可变数量的参数。va_list 保存参数列表,va_start 初始化它(需要 ... 之前的最后一个命名参数),va_arg 用指定类型检索下一个参数,va_end 清理。函数必须知道要读取多少参数——通过计数参数(如 printf 的格式字符串)或哨兵值(NULL 终止符)。'...' 必须始终是最后一个参数。va_arg 不进行类型检查——传递错误类型是未定义行为。这就是 printf、fprintf 和 execl 的工作方式。

c
#include <stdio.h>
#include <stdarg.h>

// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
    va_list args;           // argument list type
    va_start(args, count);  // initialize (needs last named param)

    int total = 0;
    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int);  // get next argument (as int)
        total += val;
    }

    va_end(args);  // cleanup
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30));      // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));   // 15
    printf("%d\n", sum(0));                    // 0
    return 0;
}

// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.

实现自定义 printf

vprintf/vfprintf/vsprintf 是接受 va_list 而非 ... 的可变参数辅助函数——它们让您构建自定义的类 printf 函数。log_msg 示例用日志级别前缀包装 printf。print_values 示例展示如何处理混合类型:在每个值前传递类型标签,然后根据标签切换调用正确类型的 va_arg。这是必要的,因为 va_arg 需要确切类型——没有运行时类型信息。类型标签模式用于多态 C API(例如,SQLite 的绑定函数)。始终精确匹配 va_arg 类型——int vs long,float vs double(float 在 varargs 中提升为 double)。

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, format);

    // vprintf: like printf but takes va_list instead of ...
    vprintf(format, args);

    va_end(args);
    printf("\n");
}

// Variadic function with mixed types
void print_values(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        int type = va_arg(args, int);  // type tag
        switch (type) {
            case 0:  // int
                printf("int: %d\n", va_arg(args, int));
                break;
            case 1:  // double
                printf("double: %f\n", va_arg(args, double));
                break;
            case 2:  // string
                printf("string: %s\n", va_arg(args, char *));
                break;
        }
    }
    va_end(args);
}

int main() {
    log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
    log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);

    print_values(2, 0, 42, 2, "hello", 1, 3.14);
    return 0;
}

哨兵终止的可变参数函数

哨兵终止的可变参数函数使用特殊值(通常 NULL)标记参数结束,而非计数。这对于字符串密集的 API 更清晰——调用者不需要计数参数。exec 家族(execl、execlp)使用 NULL 作为哨兵。缺点:如果调用者忘记 NULL,函数读取垃圾内存(未定义行为)。某些编译器(GCC)支持 __attribute__((sentinel)) 警告缺少哨兵。始终记录需要 NULL。缓冲区大小参数防止缓冲区溢出——始终传递目标大小并在 strcat 前检查边界。

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
    va_list args;
    va_start(args, size);

    dest[0] = '\0';  // start with empty string
    size_t used = 0;

    while (1) {
        char *s = va_arg(args, char *);
        if (s == NULL) break;  // sentinel — stop reading

        size_t len = strlen(s);
        if (used + len < size) {
            strcat(dest, s);
            used += len;
        }
    }

    va_end(args);
}

int main() {
    char result[256];
    concat_strings(result, sizeof(result),
                   "Hello, ", "world", "! ", "How are you?", NULL);
    printf("%s\n", result);
    // Hello, world! How are you?

    // Common C APIs using sentinels:
    // execl("/bin/ls", "ls", "-l", NULL);  // exec family
    // sqlite3_exec(db, sql, callback, NULL, NULL);
    return 0;
}

转发可变参数

转发可变参数需要 va_copy(而非赋值)——va_list 可能是无法用 = 复制的不透明类型。va_copy 让您多次遍历参数列表(例如,先测量,然后打印)。LOG 宏使用 __VA_ARGS__ 将所有参数转发给 fprintf。##__VA_ARGS__ GCC 扩展在没有可变参数时删除前面的逗号(因此 LOG("msg") 无尾随逗号工作)。此模式在 C 日志宏中无处不在。C99 要求 ... 前至少一个参数;C11/C23 和 GCC 允许零。对于 C++ 中的类型安全替代方案,使用可变参数模板或 std::format。

c
#include <stdio.h>
#include <stdarg.h>

// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);  // copy for second use

    // First pass: count characters that would be printed
    int len = vsnprintf(NULL, 0, format, args1);
    printf("[len=%d] ", len);

    // Second pass: actually print
    vprintf(format, args2);

    va_end(args1);
    va_end(args2);
}

// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)

int main() {
    custom_printf("Value: %d, Name: %s\n", 42, "test");
    LOG("Simple message");           // no extra args
    LOG("With value: %d", 100);      // with args
    return 0;
}

可变参数宏(C99)

C99 可变参数宏使用 __VA_ARGS__ 捕获匹配宏定义中 ... 的所有参数。##__VA_ARGS__(GCC 扩展,现在在 C20 中标准化)在没有可变参数传递时删除逗号。COUNT 宏使用巧妙技巧:将 N 个参数映射到 N、5、4、3、2、1,第 N 个位置给出计数。ASSERT 中的 do { ... } while (0) 惯用法使宏表现得像单条语句(在 if/else 中无大括号也安全)。# 字符串化宏参数。可变参数宏对 C 中的日志、调试和通用编程至关重要。它们是许多库 API 和 Linux 内核日志系统的基础。

c
#include <stdio.h>

// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
    printf("DEBUG: " fmt "\n", ##__VA_ARGS__)

#define MAX(...) (max_of(__VA_ARGS__))

// Assert macro with message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
                    #cond, ##__VA_ARGS__); \
            exit(1); \
        } \
    } while (0)

// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N

// Stringification of all args
#define STR(...) #__VA_ARGS__

int main() {
    DEBUG_PRINT("x = %d", 42);
    DEBUG_PRINT("no args");  // ## removes comma

    ASSERT(x > 0, "x was %d", x);

    printf("Count: %d\n", COUNT(a, b, c));  // 3
    printf("Stringified: %s\n", STR(hello, world));  // "hello, world"

    return 0;
}
18

位操作技巧

常见位技巧

位操作直接作用于二进制表示。n & 1 检查最低有效位的奇偶。左移(<<)乘以 2;右移(>>)除以。XOR 交换避免临时变量但可读性差。n & (n-1) 清除最低设置位,适用于 2 的幂检查和 popcount。这些技巧快速但在应用程序代码中优先考虑可读性。

c
// Check if odd
int is_odd(int n) { return n & 1; }

// Multiply/divide by powers of 2
int doubled = x << 1;    // x * 2
int halved = x >> 1;     // x / 2
int times8 = x << 3;     // x * 8

// Swap without temp variable
void swap(int *a, int *b) {
    *a ^= *b; *b ^= *a; *a ^= *b;
}

// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }

// Count set bits (population count)
int popcount(unsigned int n) {
    int count = 0;
    while (n) { n &= (n-1); count++; }
    return count;
}

位标志与掩码

位标志将多个布尔选项打包到单个整数中,节省内存。每个标志是 2 的幂(一位)。OR(|)设置标志,AND(&)检查标志,XOR(^)切换,AND NOT(&= ~)清除。此模式在系统编程中无处不在:文件权限(O_RDONLY、O_CREAT)、套接字选项和 GPU 状态。使用命名常量提高可读性。

c
#define FLAG_READ    (1 << 0)  // 0x01
#define FLAG_WRITE   (1 << 1)  // 0x02
#define FLAG_EXECUTE (1 << 2)  // 0x04

// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;

// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }

// Toggle a flag
perms ^= FLAG_EXECUTE;

// Clear a flag
perms &= ~FLAG_APPEND;

// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
            == (FLAG_READ|FLAG_WRITE);

结构体中的位域

位域将小值打包到结构体中的最少位数。冒号语法指定位宽。这为有许多小字段的数据结构节省内存(日期、标志、硬件寄存器)。但是,位域布局是实现相关的:字节顺序、填充和对齐因编译器而异。避免将位域用于可移植数据格式;改用显式位掩码。

c
// Pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31 (5 bits)
    unsigned int month : 4;   // 0-15 (4 bits)
    unsigned int year  : 23;  // 0-8M (23 bits)
};  // Total: 32 bits = 4 bytes

struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today));  // 4

today.day = 15;
if (today.month == 12) {
    today.year++;
    today.month = 1;
}

字节序转换

字节序决定字节顺序:小端(x86、ARM 默认)先存储 LSB;大端(网络、某些 MIPS)先存储 MSB。网络协议使用大端(网络字节序)。使用 htonl/ntohl 进行可移植网络代码。用移位和掩码手动字节交换适用于任何平台。用 __BYTE_ORDER__ 在编译时检测字节序以获得优化代码路径。

c
#include <arpa/inet.h>  // htonl, ntohl

// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);

// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
    return ((v & 0xFF000000) >> 24) |
           ((v & 0x00FF0000) >> 8)  |
           ((v & 0x0000FF00) << 8)  |
           ((v & 0x000000FF) << 24);
}

// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    printf("Little-endian system\n");
#endif

位运算技巧

无分支位技巧避免条件跳转以提高紧密循环的性能。abs 技巧使用算术右移创建掩码。next_pow2 填充最高设置位以下的所有位,然后加 1。位反转使用分治:交换半字节,然后对,然后单个位。这些适用于加密、哈希和 DSP。现代 CPU 通常有更快的内置指令(POPCNT、LZCNT)。

c
// Absolute value without branching
int abs_val(int n) {
    int mask = n >> (sizeof(int)*8 - 1);
    return (n ^ mask) - mask;
}

// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
    n--;
    n |= n >> 1;  n |= n >> 2;
    n |= n >> 4;  n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
    b = (b >> 4) | (b << 4);
    b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
    b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
    return b;
}
19

信号处理高级

信号集与阻塞

sigprocmask 阻塞信号使其排队(不丢失)并稍后传递。这保护关键部分免受中断。SIG_BLOCK 添加到掩码,SIG_UNBLOCK 移除,SIG_SETMASK 替换。使用 sigpending 检查排队信号。仅短暂阻塞信号;长时间阻塞可能错过重要事件。信号掩码是每进程的,跨 fork 继承。

c
#include <signal.h>

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - signals are deferred
do_critical_work();

// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);

// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
    printf("SIGINT is pending\n");
}

安全信号处理函数

信号处理函数必须是异步信号安全的:仅使用可重入函数(write、_exit、signal)。避免 printf、malloc 和大多数库函数——它们可能在操作中途被中断并损坏状态。对处理函数设置的标志使用 volatile sig_atomic_t。SA_RESTART 自动重启中断的系统调用。sigaction 优于 signal 以获得可移植、良好定义的行为。

c
#include <signal.h>
#include <unistd.h>

// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // ONLY use async-signal-safe functions!
    // write() is safe; printf() is NOT
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg)-1);
    got_signal = 1;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigaction(SIGINT, &sa, NULL);

    while (!got_signal) pause();
    return 0;
}

自管道与 signalfd

signalfd(Linux)将信号转换为文件描述符,将它们集成到事件循环(epoll、select)中。首先阻塞信号,然后创建 signalfd。自管道技巧可移植:处理函数向管道写入一个字节,主循环读取它。两种方法都将信号处理从受限的处理函数上下文移到正常代码中,在那里您可以安全调用任何函数。

c
#include <sys/signalfd.h>

// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);  // Must block first

    int fd = signalfd(-1, &mask, SFD_CLOEXEC);
    return fd;
}

// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);

// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
    write(pipefd[1], &sig, sizeof(sig));
}

定时器与 SIGALRM

setitimer 按间隔传递 SIGALRM。ITIMER_REAL 使用实时;ITIMER_VIRTUAL 使用 CPU 时间;ITIMER_PROF 使用 CPU + 系统时间。定时器重复直到取消。对于现代代码,优先使用带 SIGEV_THREAD 的 timer_create 实现每线程定时器,或使用专用定时器 fd(Linux 上的 timerfd_create)与事件循环集成。始终处理信号以避免默认终止。

c
#include <sys/time.h>

volatile sig_atomic_t timer_fired = 0;

void timer_handler(int sig) { timer_fired = 1; }

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    // Set interval timer: 2 sec initial, 1 sec repeat
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_interval.tv_sec = 1;
    setitimer(ITIMER_REAL, &timer, NULL);

    int count = 0;
    while (count < 5) {
        pause();
        if (timer_fired) {
            printf("Timer %d\n", ++count);
            timer_fired = 0;
        }
    }
    return 0;
}

发送信号

kill 按 PID 向进程发送信号。kill(0, sig) 向整个进程组发送。raise 向调用进程发送信号。sigqueue 发送带附加数据的信号(siginfo)。始终使用 waitpid 回收子进程并检查退出状态。WIFSIGNALED 区分信号死亡和正常退出。发送 SIGTERM(而非 SIGKILL)允许优雅关闭。

c
#include <signal.h>
#include <sys/wait.h>

pid_t child = fork();
if (child == 0) {
    while (1) { sleep(1); }
} else {
    sleep(3);
    kill(child, SIGTERM);  // Send SIGTERM to child

    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status))
        printf("Killed by signal %d\n", WTERMSIG(status));
}

// Send signal to self
raise(SIGSTOP);  // Stop (resume with SIGCONT)

// Send to process group
kill(0, SIGUSR1);  // 0 = own process group

// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);
20

进程管理高级

fork 与 exec 模式

fork-exec 模式创建子进程(fork)并用新程序替换其映像(exec)。fork 复制进程;exec 加载新程序。子进程在 exec 失败时必须调用 _exit(而非 exit)以避免刷新父缓冲区。waitpid 阻塞直到子进程退出。WEXITSTATUS 提取退出代码。这就是 shell 运行命令的方式。

c
#include <unistd.h>
#include <sys/wait.h>

int run_program(const char *path, char *const args[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return -1; }

    if (pid == 0) {
        // Child: exec replaces process image
        execvp(path, args);
        perror("execvp");  // Only on failure
        _exit(127);
    }

    // Parent: wait for child
    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);

守护进程化

setsid 创建新会话和进程组,脱离控制终端。双重 fork 模式防止守护进程重新获取终端。守护进程化后,关闭标准文件描述符并重定向到 /dev/null 或日志文件。chdir 到 / 防止阻塞卸载。umask(0) 确保可预测的文件权限。这是标准守护进程模式。

c
#include <unistd.h>

int main() {
    pid_t child = fork();
    if (child > 0) _exit(0);  // Parent exits
    if (child < 0) return 1;

    // First child: create new session
    setsid();

    // Second fork (prevent reacquiring terminal)
    pid_t grandchild = fork();
    if (grandchild > 0) _exit(0);

    // Daemon process
    chdir("/");
    umask(0);
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    while (1) {
        sleep(60);
        // Do daemon work
    }
    return 0;
}

posix_spawn 替代方案

posix_spawn 是 fork+exec 在无 MMU 系统(嵌入式)或大内存占用(fork 复制页表)系统上的更高效替代方案。它在一次调用中组合进程创建和 exec,原子地应用文件操作(重定向、关闭)。当您不需要在 fork 和 exec 之间修改子进程状态时使用 posix_spawn。它是 POSIX 标准,可在 Linux、macOS 和大多数 Unix 系统上使用。

c
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int spawn_child(const char *cmd, char *const argv[]) {
    pid_t pid;
    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);

    posix_spawn_file_actions_destroy(&actions);
    posix_spawnattr_destroy(&attr);

    if (ret != 0) return -1;

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

僵尸预防

当子进程在父进程调用 wait 之前退出时产生僵尸进程。通过以下方式预防:(1) 在循环中用 waitpid 处理 SIGCHLD(WNOHANG 避免阻塞),(2) 将 SIGCHLD 设置为 SIG_IGN(内核自动回收),或 (3) 双重 fork 使孙进程成为孤儿并被 init(PID 1)收养,init 自动回收它。始终在信号处理函数中保存和恢复 errno。

c
#include <sys/wait.h>
#include <signal.h>

// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // Reap all available zombies
    }
    errno = saved_errno;
}

// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);

// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
    if (fork() == 0) {
        // Grandchild does the work
        execlp("sleep", "sleep", "10", NULL);
        _exit(1);
    }
    _exit(0);  // First child exits immediately
}
waitpid(inter, NULL, 0);

资源限制

setrlimit 对进程施加资源限制:CPU 时间(RLIMIT_CPU)、虚拟内存(RLIMIT_AS)、文件大小(RLIMIT_FSIZE)、打开文件数(RLIMIT_NOFILE)、栈大小和核心转储大小。软限制被强制执行;硬限制是上限。超过 CPU 时间发送 SIGXCPU;超过内存导致 malloc 失败。在子进程中使用限制以防止因错误或攻击导致的资源耗尽。

c
#include <sys/resource.h>

void set_limits() {
    struct rlimit lim;

    // Limit CPU time to 10 seconds
    lim.rlim_cur = 10; lim.rlim_max = 10;
    setrlimit(RLIMIT_CPU, &lim);

    // Limit memory to 256 MB
    lim.rlim_cur = 256*1024*1024;
    lim.rlim_max = 256*1024*1024;
    setrlimit(RLIMIT_AS, &lim);

    // Limit open files
    lim.rlim_cur = 32; lim.rlim_max = 64;
    setrlimit(RLIMIT_NOFILE, &lim);
}

void show_limits() {
    struct rlimit lim;
    getrlimit(RLIMIT_NOFILE, &lim);
    printf("Max files: soft=%lu, hard=%lu\n",
           lim.rlim_cur, lim.rlim_max);
}
21

管道与 IPC 高级

匿名管道

匿名管道在父子进程之间提供单向通信。始终关闭未使用的一端:写端必须关闭读端,反之亦然。关闭写端向读者发出 EOF 信号(read 返回 0)。管道有固定缓冲区(通常 64KB);满时写入阻塞。管道仅用于相关进程(父子)。对于不相关进程,使用命名管道(FIFO)或套接字。

c
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // Child: read from pipe
        close(pipefd[1]);  // Close unused write end
        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        buf[n] = '\0';
        printf("Child received: %s", buf);
        close(pipefd[0]);
    } else {
        // Parent: write to pipe
        close(pipefd[0]);  // Close unused read end
        const char *msg = "Hello from parent!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);  // EOF signal to reader
        wait(NULL);
    }
    return 0;
}

命名管道(FIFO)

命名管道(FIFO)是作为不相关进程之间管道的特殊文件。mkfifo 创建文件;open 阻塞直到读端和写端都存在。使用 O_NONBLOCK 进行非阻塞打开。FIFO 在文件系统中持续存在直到被 unlink。它们适用于独立程序之间的简单 IPC,但对于复杂通信,考虑 Unix 域套接字或消息队列。

c
#include <sys/stat.h>
#include <fcntl.h>

// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);

// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);

// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);

// Clean up
unlink("/tmp/myfifo");

共享内存

共享内存是最快的 IPC:进程将相同物理内存映射到其地址空间。shm_open 创建 POSIX 共享内存对象;mmap 映射它。更改立即可见给所有映射者。使用信号量或互斥锁(带 PTHREAD_PROCESS_SHARED)进行同步。始终 unmap 和 unlink 以避免泄漏。共享内存适用于大数据;开销仅是初始映射。

c
#include <sys/mman.h>
#include <fcntl.h>

// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);  // Set size

// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);
close(fd);  // Can close after mmap

// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);

// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);

// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");

Unix 域套接字

Unix 域套接字在同一机器上提供双向、面向流的 IPC。它们比 TCP 更快(无网络开销)并支持通过 SCM_RIGHTS 在进程之间传递文件描述符。使用 SOCK_STREAM 获得可靠流,SOCK_DGRAM 获得数据报。套接字路径是文件系统条目;bind 前 unlink 以避免地址已使用错误。Unix 套接字是 Docker、X11 和 systemd 通信的基础。

c
#include <sys/socket.h>
#include <sys/un.h>

// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);

unlink("/tmp/mysocket");

消息队列

POSIX 消息队列提供按优先级排序的、基于消息的 IPC。每条消息有优先级;高优先级消息先被接收。mq_send 和 mq_receive 对单条消息是原子的。使用 O_NONBLOCK 进行非阻塞操作或 mq_timedreceive 进行超时。消息队列持续存在直到被 unlink,不同于随进程消亡的管道。它们非常适合任务分派和事件通知。

c
#include <mqueue.h>

// Create/open a message queue
struct mq_attr attr = {
    .mq_maxmsg = 10,
    .mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);

// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1);  // priority 1

// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);

// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);

// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);

mq_close(mq);
mq_unlink("/my_queue");
22

Makefile 与构建高级

自动变量与模式

自动变量使 Makefile 简洁且可维护。模式规则(%.o: %.c)定义如何构建匹配模式的任何文件。-MM 生成依赖文件(.d),跟踪头文件依赖,因此编辑头文件触发依赖 .c 文件的重新编译。-include 指令在依赖文件存在时静默包含它们。这是稳健 C/C++ 构建系统的基础。

c
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target

# Pattern rule: compiles any .c to .o
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

OBJS = main.o utils.o parser.o

program: $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)

# Automatic dependency generation
%.d: %.c
	@$(CC) -MM $< > $@

-include $(OBJS:.o=.d)

clean:
	rm -f $(OBJS) program

变量与条件

使用 := 进行立即求值(更快、可预测),使用 = 进行惰性求值(允许前向引用)。?= 仅在未设置时设置变量,允许从命令行用户覆盖。条件(ifeq、ifdef)启用调试/发布构建。Q 技巧在未设置 VERBOSE 时静默命令回显。MAKECMDGOALS 包含来自命令行的目标。

c
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)

CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))

ifeq ($(DEBUG),debug)
    CFLAGS += -g -DDEBUG -O0
else
    CFLAGS += -DNDEBUG
endif

ifdef VERBOSE
    Q =
else
    Q = @
endif

build:
	$(Q)$(CC) $(CFLAGS) -c main.c

函数与文本处理

Make 函数启用文本转换:patsubst 进行模式替换,filter 进行文件选择,wildcard 进行 glob,foreach 进行迭代。shell 函数在解析时执行命令——适用于嵌入版本信息。替换引用($(VAR:.c=.o))是简单后缀更改的简洁替代方案。

c
SRCS = main.c utils.c parser.c

# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS))  # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)

# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))

# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))

# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)

# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"

子目录与递归 Make

递归 make(子目录 Makefile)是传统的,但在并行构建时可能慢且容易出错。非递归方法(带 vpath 的单个 Makefile)在正确性和速度方面更受青睐。如果使用递归 make,显式传递变量并使用 MAKECMDGOALS 传播目标。对于大型项目,考虑 CMake 或 Meson 代替原始 Make 以获得更好的依赖跟踪和 IDE 支持。

c
SUBDIRS = lib src tests

.PHONY: all $(SUBDIRS) clean test

all: $(SUBDIRS)

# Pass variables to sub-makes
$(SUBDIRS):
	$(MAKE) -C $@ $(MAKECMDGOALS)

# Parallel build: make -j4

clean: $(SUBDIRS)
	rm -f *.o program

# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include

CFLAGS += -Iinclude -Ilib

program: main.o lib/utils.o
	$(CC) $^ -o $@

CMake 集成

CMake 从声明式 CMakeLists.txt 生成 Makefile(或 Ninja、VS、Xcode 项目)。现代 CMake 使用基于目标的命令(target_include_directories、target_link_libraries)而非全局变量。生成器表达式($<$<CONFIG:Debug>:...)启用每配置标志。CMake 是 C/C++ 项目的事实标准,比原始 Makefile 有更好的 IDE 集成和跨平台支持。

c
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp src/main.c src/utils.c)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# Per-configuration flags
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-g -O0 -DDEBUG>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)

install(TARGETS myapp DESTINATION bin)
23

使用 GDB 调试

启动与断点

用 -g 编译嵌入调试符号,用 -O0 禁用优化(否则变量可能被优化掉)。break 在函数、行或条件处设置断点。watch(数据断点)在变量更改时触发——对于查找内存损坏很强大。条件断点(break func if cond)仅在条件为 true 时触发,适用于循环。tbreak 是一次性断点。

c
# Compile with debug symbols
gcc -g -O0 -o program program.c

# Start GDB
gdb ./program
gdb --args ./program arg1 arg2

# Breakpoints
(gdb) break main           # Function
(gdb) break 42             # Line 42
(gdb) break file.c:50      # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main          # Temporary (removed after hit)
(gdb) watch x              # Break when x changes

# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2             # Delete breakpoint #2
(gdb) disable 1            # Temporarily disable
(gdb) enable 1

单步执行与检查

next 跳过函数调用;step 进入它们。finish 运行到当前函数末尾。print 格式:/x(十六进制)、/c(字符)、/s(字符串)、/t(二进制)。@ 运算符打印数组切片:arr@5 显示 5 个元素。display 在每次停止时自动打印变量。backtrace 显示调用栈;frame N 切换上下文以检查该帧。

c
(gdb) run                  # Start program
(gdb) continue             # Continue to next breakpoint
(gdb) next                 # Step over (no function entry)
(gdb) step                 # Step into (enters functions)
(gdb) finish               # Run until function returns
(gdb) until 50             # Run until line 50

# Inspect variables
(gdb) print x              # Print variable
(gdb) print *ptr           # Dereference pointer
(gdb) print arr[0]@5       # First 5 elements
(gdb) print/x x            # Print in hex
(gdb) display x            # Auto-print at each stop
(gdb) info locals          # All local variables

# Backtrace
(gdb) backtrace            # Call stack
(gdb) frame 2              # Switch to frame #2
(gdb) up / down            # Navigate stack

检查内存

examine(x)命令检查原始内存。格式指定计数、显示格式和单位大小。x/10i $pc 从程序计数器反汇编 10 条指令。x/s 将内存视为以 null 结尾的字符串。info proc mappings 显示虚拟内存布局(文本、数据、堆、栈、共享库)。这对于调试缓冲区溢出和内存损坏至关重要。

c
# x/[count][format][size] address
(gdb) x/10x &arr           # 10 words in hex
(gdb) x/20cb &str          # 20 bytes as chars
(gdb) x/4xw ptr            # 4 words in hex
(gdb) x/s str              # String

# Format: x(hex) d(decimal) u(unsigned) o(octal)
#         t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)

# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main  # With raw bytes

# Memory map
(gdb) info proc mappings   # Process memory layout

核心转储

核心转储捕获崩溃时的进程状态用于事后调试。用 ulimit -c unlimited 启用它们。用 gdb program core 加载核心文件。backtrace 显示崩溃位置;info locals 显示变量值。对于多线程程序,thread apply all bt 显示所有线程状态——对于死锁分析至关重要。

c
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern

# Run program until it crashes
./program                  # Produces core.12345

# Analyze core dump
gdb ./program core.12345

(gdb) bt                   # Backtrace at crash
(gdb) bt full              # With local variables
(gdb) frame 0              # Top frame (crash location)
(gdb) info locals          # Variables at crash
(gdb) print *ptr           # What was the pointer?

# Multithreaded
(gdb) info threads         # List all threads
(gdb) thread 3             # Switch to thread 3
(gdb) thread apply all bt  # Backtraces for ALL threads

GDB 脚本与自动化

.gdbinit 在启动时自动化常见设置。define 为重复任务创建自定义命令。commands 将操作附加到断点(例如,记录变量并继续)。GDB 支持用于复杂分析的 Python 脚本:自动化测试运行、可视化数据结构或提取统计信息。Python 脚本可以通过 gdb 模块访问 GDB 内部。使用脚本在团队之间标准化调试工作流。

c
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0        # No limit on string display

# Define custom commands
define print_array
    set $i = 0
    while $i < $arg0
        printf "[%d] = %d\n", $i, $arg1[$i]
        set $i = $i + 1
    end
end
# Usage: print_array 10 my_array

# Commands attached to breakpoints
break main
commands 1
  silent
  printf "x = %d\n", x
  continue
end

# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
end
24

内存对齐与位域

结构体对齐与填充

编译器插入填充使每个成员自然对齐(通常到其大小:char=1、short=2、int=4、double=8)。从大到小重新排序成员最小化填充。使用 offsetof 检查布局。在 64 位系统上,指针需要 8 字节对齐。过多填充浪费内存并损害缓存性能。始终按大小递减排序结构体成员。

c
#include <stddef.h>

// Without alignment consideration (padded)
struct Bad {
    char a;     // 1 byte + 7 padding
    double b;   // 8 bytes
    char c;     // 1 byte + 7 padding
};  // sizeof = 24

// Reordered for efficiency
struct Good {
    double b;   // 8 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 6 padding
};  // sizeof = 16

printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
    offsetof(struct Bad, a),
    offsetof(struct Bad, b),
    offsetof(struct Bad, c),
    sizeof(struct Bad));

控制对齐

C11 alignas 为类型或变量指定最小对齐——适用于 SIMD(16/32 字节对齐)和 DMA。打包结构体(__attribute__((packed)) 或 #pragma pack)移除所有填充,节省空间但可能减慢访问(未对齐内存访问在某些架构上可能出错)。对于确切布局重要的网络协议和文件格式使用 packed。切勿打包需要快速访问的结构体。

c
#include <stdalign.h>

// C11 explicit alignment
struct alignas(16) Aligned16 {
    int data[4];  // 16 bytes, 16-byte aligned
};

// Check alignment
printf("Alignment of int: %zu\n", alignof(int));     // 4
printf("Alignment of struct: %zu\n",
       alignof(struct Aligned16));  // 16

// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
    char a;
    int b;    // No padding after a
    char c;
};  // sizeof = 6

// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)

灵活数组成员

灵活数组成员(C99)允许结构体将可变长度数组作为最后一个成员。用 malloc(sizeof(struct) + desired_length) 分配。数组共享单个分配,因此一次 free 释放所有内容。这比单独的指针 + malloc 更高效、更清晰。常见于动态数组、字符串和网络包头。sizeof(struct) 不包括灵活数组。

c
#include <stdlib.h>

// C99 flexible array member (last member with no size)
struct Buffer {
    size_t size;
    char data[];  // Flexible array
};

// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);

// Use data
strcpy(buf->data, "Hello");

// Single free (no separate allocation for data)
free(buf);

联合类型双关

联合在相同内存中叠加成员,启用类型双关(将位重新解释为不同类型)。读取非最后写入的联合成员在 C 中是允许的(实现定义)。联合类型双关在严格别名下是合法的,不同于指针转换。匿名联合(C11)直接暴露成员而无需成员名。使用联合用于标记变体和访问 IEEE 754 浮点内部。

c
#include <stdio.h>

// Union: members share the same memory
union Data {
    int i;
    float f;
    char bytes[4];
};

int main() {
    union Data d;
    d.i = 0x41424344;

    printf("As int: %d\n", d.i);
    printf("As float: %f\n", d.f);
    printf("As bytes: %02x %02x %02x %02x\n",
           d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);

    // Safe type punning (legal in C)
    union { float f; int i; } u;
    u.f = 3.14f;
    printf("Float bits: 0x%08x\n", u.i);

    // Anonymous unions (C11)
    struct Variant {
        int type;
        union { int i; float f; char *s; };
    };
}

内存布局与字节序

字节序决定内存中的字节顺序:小端(x86、ARM 默认)先存储 LSB;大端先存储 MSB。编写可移植二进制格式时,用显式移位而非 memcpy 序列化。使用 dump_hex 在调试期间检查原始内存。网络协议使用大端(网络字节序);使用 htonl/ntohl 进行可移植代码。编写可移植二进制 I/O 时始终在两种字节序上测试。

c
#include <stdint.h>

void dump_hex(void *ptr, size_t len) {
    unsigned char *bytes = ptr;
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", bytes[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
}

int main() {
    uint32_t val = 0x12345678;
    printf("Value: 0x%08x\n", val);
    printf("Bytes: ");
    dump_hex(&val, sizeof(val));

    // Little-endian: 78 56 34 12 (LSB first)
    // Big-endian:    12 34 56 78 (MSB first)

    // Serialize to big-endian (network order)
    unsigned char be[4];
    be[0] = (val >> 24) & 0xFF;
    be[1] = (val >> 16) & 0xFF;
    be[2] = (val >> 8)  & 0xFF;
    be[3] = val & 0xFF;

    // Deserialize from big-endian
    uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
    printf("Recovered: 0x%08x\n", recovered);
    return 0;
}
25

可变参数函数高级

va_list 基础

可变参数函数使用 va_list 访问可变参数。va_start 用最后一个命名参数初始化列表。va_arg 用指定类型检索下一个参数。va_end 清理。调用者必须传达计数和类型(例如,printf 使用格式说明符)。可变参数函数缺乏类型安全——类型不匹配导致未定义行为。

c
#include <stdarg.h>

// Variadic function: variable number of arguments
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize with last named param

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  // Get next argument
    }

    va_end(args);  // Cleanup
    return total;
}

// Usage
int result = sum(3, 10, 20, 30);  // 60
int result2 = sum(5, 1, 2, 3, 4, 5);  // 15

哨兵终止的可变参数

哨兵值(通常 NULL)标记参数列表结束,消除对计数参数的需求。这在 execl 等 C API 中很常见。GCC __attribute__((sentinel)) 在最后一个参数不是 NULL 时发出警告。始终记录预期的哨兵。缺点是哨兵不能作为有效数据值出现。

c
#include <stdarg.h>

// Sentinel value marks the end
void log_messages(const char *first, ...) {
    va_list args;
    va_start(args, first);

    const char *msg = first;
    while (msg != NULL) {  // NULL is the sentinel
        printf("%s\n", msg);
        msg = va_arg(args, const char *);
    }

    va_end(args);
}

// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);

// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
    __attribute__((sentinel));

vfprintf 与格式字符串

vprintf/vfprintf/vsnprintf 接受 va_list 而非 ...,启用自定义类 printf 函数。始终使用 vsnprintf(有界)而非 vsprintf 以防止缓冲区溢出。直接转发 va_list。此模式用于日志库、错误报告和自定义格式化器。格式字符串漏洞(用户控制格式)是安全风险——切勿将用户输入作为格式传递。

c
#include <stdarg.h>
#include <stdio.h>

// Custom printf-like function
void logf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // Add timestamp prefix
    printf("[LOG] ");
    vprintf(format, args);  // Pass va_list to vprintf
    printf("\n");

    va_end(args);
}

// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, size, fmt, args);  // Safe: bounded
    va_end(args);
}

// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);

函数指针与回调

函数指针在 C 中启用回调和多态。语法 return_type (*name)(params) 声明指向函数的指针。qsort 使用比较回调进行通用排序。函数指针数组实现分派表(switch 的替代)。始终确保回调签名精确匹配。函数指针是事件处理程序、插件和 C 中策略模式的基础。

c
#include <stdlib.h>

// Function pointer type
typedef int (*compare_fn)(const void *, const void *);

// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer
    qsort(arr, n, sizeof(int), cmp_int);

    // Array of function pointers
    double (*ops[])(double, double) = {
        add, subtract, multiply, divide
    };
    double result = ops[2](10.0, 3.0);  // multiply

    // Function pointer as parameter
    void apply(int *arr, size_t n, int (*fn)(int)) {
        for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
    }
    apply(arr, n, square);
    return 0;
}

可变参数宏

可变参数宏(__VA_ARGS__)接受可变参数,适用于日志和调试。__VA_OPT__(C2x)通过条件包含逗号处理零参数情况。##__VA_ARGS__ GCC 扩展在没有参数传递时删除前面的逗号。在发布构建中编译为无操作的调试宏无需代码更改即可消除开销。始终保护格式字符串以防止格式字符串攻击。

c
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);

// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...)     printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)

// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N

// Debug macro that compiles out in release
#ifdef NDEBUG
    #define DEBUG(fmt, ...) ((void)0)
#else
    #define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif

// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。