入门
Hello World
每个 C 程序都从 main() 开始。#include <stdio.h> 引入标准 I/O 库(printf、scanf)。main 成功返回 0,失败返回非零值。void 关键字显式声明 main 不接受参数。
#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> 获取固定宽度。
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 读取整行。
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)控制对齐和小数位数。说明符与类型不匹配会导致未定义行为。
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)以防止重复包含。宏是文本替换——在参数周围使用括号。
#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字符串与 String.h
字符串基础
C 字符串是以 null 结尾的 char 数组。strlen 计算 '\0' 之前的字符数;sizeof 返回缓冲区大小。strcpy 复 制直到 null 终止符——始终确保目标足够大以避免溢出。
#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,第一个 < 第二个返回负值,第一个 > 第二个返回正值。切勿使用 == 比较字符串(那比较的是指针,而非内容)。
#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,通过指定最大大小来防止缓冲区溢出。
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=20strchr、strstr 与 strtok
strchr 查找字符,strstr 查找子字符串。strtok 按分隔符分割字符串但会修改原始字符串(插入 null 终止符)且不是线程安全的——在后续调用中传递 NULL 以继续分词。
#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 中移除)。
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);数字与数学
整数类型与限制
当确切大小重要时,使用 <stdint.h> 获取固定宽度类型(int32_t、int64_t)。<limits.h> 提供 INT_MAX、INT_MIN 等平台特定边界。LL 后缀标记 long long 字面量。int/long 的大小因平台而异。
#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 用于边界和精度。
#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 链接。对于财务计算,避免浮点——改用整数分。
#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 库。
#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 报告解析错误。
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");控制流
If / Else
if/else if/else 是标准条件语句。C 将 0 视为 false,任何非零值视为 true。即使单条语句也使用大括号以防止稍后添加行时的错误。C89 中没有布尔类型;C99 添加了 _Bool 和 <stdbool.h>。
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 类型,不适用于字符串或浮点数。
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,循环体执行零次。
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 非常适合输入验证和菜单循环,其中提示必须在条件可检查之前出现。
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 onceBreak、Continue 与 goto
break 退出最近的循环/switch;continue 跳到下一次迭代。C 没有带标签的 break,因此 goto 是跳出深度嵌套循环的惯用方式。其他情况下不鼓励使用 goto,但对于清理模式和嵌套循环退出是可接受的。
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");函数
定义与调用
函数必须在使用前声明(原型)或定义。void 返回类型表示无返回值。const char *name 表示函数不会修改字符串。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)——指数级。使用记忆化或迭代提高效率。深度递归可能溢出调用栈。
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、事件处理程序和插件系统。
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 就是这样工作的。
#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) == 60static 与 inline
static 作用于函数/变量将其限制在当前翻译单元(文件)内。static 作用于局部变量使其跨调用持久(类似全局但作用域受限)。inline 建议编译器嵌入函数体;现代编译器忽略它并自行决定。
// 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;
}数组与指针
数组
数组是固定大小、零索引且在内存中连续存储的。sizeof(arr)/sizeof(arr[0]) 计算长度,但仅适用于实际数组,不适用于指针(数组传递给函数时退化为指针,丢失大小信息)。
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。
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)。减去指向同一数组的两个指针给出元素计数。指针算术仅在数组内有效。
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 是真正的数组而非指针时给出完整数组大小。
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)。对于动态二维数组,使用指针数组。
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");
}结构体与联合
结构体
结构体将不同类型的相关变量分组。成员用点运算符(.)访问。用大括号表示法初始化。结构体按值传递(复制);按指针传递(struct Point *)以避免复制并修改原始值。
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 还可以为函数指针类型起别名,使回调更可读。
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 的简写。将结构体指针传递给函数以提高效率(避免复制大型结构体)并允许修改。
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联合
联合在相同内存上叠加多种类型——一次只有一个成员有效。设置一个成员会覆盖其他成员。适用于类型双关(将位重新解释为不同类型)和在只需要几种类型之一时节省内存。
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 以获得更好的调试和类型安全。
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内存管理
malloc 与 free
malloc 分配堆内存并返回 void 指针(或失败时 NULL)。始终检查 NULL。每个 malloc 必须配对 free 以避免内存泄漏。free 后将指针设置为 NULL 可防止 use-after-free 错误。
#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 pointercalloc 与 realloc
calloc 分配并清零内存(比有垃圾的 malloc 更安全)。realloc 调整大小:它可能移动块,返回新指针。如果 realloc 失败,它返回 NULL 但原始块仍有效——使用临时指针以避免泄漏。
#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 手动管理,大得多,但更慢且容易泄漏。小型、短生命周期数据用栈;大型或长生命周期数据用堆。
// 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 等工具检测这些错误。
// 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 中很常见。
#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);文件 I/O
fopen 与 fclose
fopen 打开文件并返回 FILE 指针(或失败时 NULL)。模式:r(读)、w(写/截断)、a(追加)、r+(读/写)、b(二进制)。始终检查 NULL。fclose 刷新缓冲区并关闭文件。fgets 安全读取一行。
#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 解析。完成后始终关闭文件以刷新缓冲区并释放资源。
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' 模式打开二进制文件。
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) 的简写。这些启用文件中的随机访问,适用于数据库和索引查找。
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 是无缓冲的,因此消息在崩溃前出现。
#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)预处理器与宏
#define 常量与宏
#define 创建文本替换宏。像 PI 这样的常量提高可读性和可维护性。类函数宏必须将参数括在括号中以避免优先级错误:不带括号的 SQUARE(2+3) 会是 2+3*2+3=11。优先使用 const 变量和 inline 函数而非宏。
#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 提供替代方案。
#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 是更简单、广泛支持的替代方案(非标准但适用于所有主要编译器)。
// 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)为函数注解优化、弃用和警告。
#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 模式确保宏在字符串化前展开。这些用于代码生成和日志宏。
#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位运算
基本位运算符
位运算符操作单个位。AND(&)屏蔽位(仅保留设置的位),OR(|)设置位,XOR(^)切换位,NOT(~)反转所有位。左移(<<)乘以 2 的幂,右移(>>)除以(对于无符号)。位操作始终使用无符号类型——有符号右移是实现定义的(可能符号扩展)。位运算极快(单 CPU 周期),用于标志、硬件寄存器、压缩和加密。二进制字面量(0b 前缀)是 C23/C++14;在旧 C 中使用十六进制(0x)或十进制。
#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 位)。
#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。这些技巧在嵌入式系统、游戏引擎和性能关键代码中很有用。
#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)添加填充。总大小向上舍入到结构体的对齐。
#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 大小因平台而异。
#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;
}信号处理
基本信号处理
信号是发送给进程的软件中断(例如,Ctrl+C 发送 SIGINT,除零发送 SIGFPE)。signal() 注册处理函数。在处理函数内部,仅允许异步信号安全函数——printf、malloc 和大多数 stdlib 函数不安全,因为主程序可能在调用中途被中断。使用 write() 输出。常见信号:SIGINT(Ctrl+C)、SIGTERM(终止请求)、SIGKILL(强制杀死,无法捕获)、SIGSEGV(段错误)、SIGALRM(定时器)。出于可移植性和控制,优先使用 sigaction() 而非 signal()。
#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() 在某些平台上不可靠。
#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() 获得更多控制。
#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 处理函数。
#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)以无竞争条件地处理信号。
#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;
}进程 Fork 与 Exec
fork() 基础
fork() 创建当前进程的精确副本——唯一区别是返回值:子进程中为 0,父进程中为子进程的 PID。两个进程从 fork() 调用继续。子进程获得父进程内存的副本(写时复制优化此操作)。始终检查所有三种情况:pid < 0(错误)、pid == 0(子进程)、pid > 0(父进程)。waitpid() 阻塞直到子进程退出并检索其状态。WIFEXITED 检查是否正常退出,WEXITSTATUS 获取退出代码。如果不 wait(),子进程变为僵尸直到被回收。
#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 之间设置文件描述符、环境和信号。
#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' 监控僵尸。
#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。
#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)更清晰但可用性较低。
#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管道与 IPC
匿名管道(父子)
管道在相关进程(由 fork 创建)之间提供单向通信。pipe(fd) 创建两个文件描述符:fd[0] 用于读取,fd[1] 用于写入。关键:在每个进程中关闭未使用的一端——父进程关闭读端,子进程关闭写端。如果不关闭写端,子进程的 read() 永远阻塞(等待更多数据)。read() 仅在所有写端关闭时返回 0(EOF)。管道有固定缓冲区(通常 64KB)——缓冲区满时 write() 阻塞。管道非常适合父子通信和管道 shell 命令(ls | grep)。
#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 脚本和系统服务。
#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)是更清晰的替代方案。
#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() 和进程管理库使用。
#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() 或两个管道。
#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 inputMakefile 与构建工具
基本 Makefile 结构
Make 自动化编译。Makefile 有规则:target(要构建的文件)、prerequisites(依赖)和 recipe(shell 命令,TAB 缩进)。变量(CC、CFLAGS)集中配置。自动变量:$@(目标名)、$<(第一个先决条件)、$^(所有先决条件)。模式规则(%.o: %.c)为所有源文件概括编译。.PHONY 声明不是文件的目标(clean、all、install)。第一条规则是默认的(不带参数的 make 构建 'all')。Make 跟踪文件时间戳——仅在先决条件比目标新时重新构建。这种增量构建节省大型项目的时间。
# 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 对此很严格。
# 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 中的 '-' 抑制错误);它们在编译期间创建并在后续构建中使用。
# 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。
# 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 changesCMake 基础(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——它更可维护且可移植。
# 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函数指针深入
函数指针语法与用法
函数指针存储函数的地址,启用运行时分派。语法 int (*fp)(int, int) 臭名昭著地令人困惑——将其读作 'fp 是指向接受 (int, int) 返回 int 的函数的指针'。typedef 简化此操作:typedef int (*math_func)(int, int) 创建可读别名。函数名退化为指针(类似数组名),因此 'add' 和 '&add' 等价。函数指针启用回调、事件处理程序、策略模式和分派表(函数指针数组用于类似 switch 的分派)。它们是 qsort 比较器和 GUI 事件系统的基础。
#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)。
#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++ 冗长,但它提供对内存布局和虚分派的完全控制。
#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 底层的工作方式。
#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 和虚函数作为更安全的替代方案。
#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可变参数(varargs)
基本可变参数函数(stdarg)
可变参数函数使用 stdarg.h 接受可变数量的参数。va_list 保存参数列表,va_start 初始化它(需要 ... 之前的最后一个命名参数),va_arg 用指定类型检索下一个参数,va_end 清理。函数必须知道要读取多少参数——通过计数参数(如 printf 的格式字符串)或哨兵值(NULL 终止符)。'...' 必须始终是最后一个参数。va_arg 不进行类型检查——传递错误类型是未定义行为。这就是 printf、fprintf 和 execl 的工作方式。
#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)。
#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 前检查边界。
#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。
#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 内核日志系统的基础。
#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;
}位操作技巧
常见位技巧
位操作直接作用于二进制表示。n & 1 检查最低有效位的奇偶。左移(<<)乘以 2;右移(>>)除以。XOR 交换避免临时变量但可读性差。n & (n-1) 清除最低设置位,适用于 2 的幂检查和 popcount。这些技巧快速但在应用程序代码中优先考虑可读性。
// 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 状态。使用命名常量提高可读性。
#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);结构体中的位域
位域将小值打包到结构体中的最少位数。冒号语法指定位宽。这为有许多小字段的数据结构节省内存(日期、标志、硬件寄存器)。但是,位域布局是实现相关的:字节顺序、填充和对齐因编译器而异。避免将位域用于可移植数据格式;改用显式位掩码。
// 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__ 在编译时检测字节序以获得优化代码路径。
#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)。
// 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;
}信号处理高级
信号集与阻塞
sigprocmask 阻塞信号使其排队(不丢失)并稍后传递。这保护关键部分免受中断。SIG_BLOCK 添加到掩码,SIG_UNBLOCK 移除,SIG_SETMASK 替换。使用 sigpending 检查排队信号。仅短暂阻塞信号;长时间阻塞可能错过重要事件。信号掩码是每进程的,跨 fork 继承。
#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 以获得可移植、良好定义的行为。
#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。自管道技巧可移植:处理函数向管道写入一个字节,主循环读取它。两种方法都将信号处理从受限的处理函数上下文移到正常代码中,在那里您可以安全调用任何函数。
#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)与事件循环集成。始终处理信号以避免默认终止。
#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)允许优雅关闭。
#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);进程管理高级
fork 与 exec 模式
fork-exec 模式创建子进程(fork)并用新程序替换其映像(exec)。fork 复制进程;exec 加载新程序。子进程在 exec 失败时必须调用 _exit(而非 exit)以避免刷新父缓冲区。waitpid 阻塞直到子进程退出。WEXITSTATUS 提取退出代码。这就是 shell 运行命令的方式。
#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) 确保可预测的文件权限。这是标准守护进程模式。
#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 系统上使用。
#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。
#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 失败。在子进程中使用限制以防止因错误或攻击导致的资源耗尽。
#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);
}管道与 IPC 高级
匿名管道
匿名管道在父子进程之间提供单向通信。始终关闭未使用的一端:写端必须关闭读端,反之亦然。关闭写端向读者发出 EOF 信号(read 返回 0)。管道有固定缓冲区(通常 64KB);满时写入阻塞。管道仅用于相关进程(父子)。对于不相关进程,使用命名管道(FIFO)或套接字。
#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 域套接字或消息队列。
#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 以避免泄漏。共享内存适用于大数据;开销仅是初始映射。
#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 通信的基础。
#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,不同于随进程消亡的管道。它们非常适合任务分派和事件通知。
#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");Makefile 与构建高级
自动变量与模式
自动变量使 Makefile 简洁且可维护。模式规则(%.o: %.c)定义如何构建匹配模式的任何文件。-MM 生成依赖文件(.d),跟踪头文件依赖,因此编辑头文件触发依赖 .c 文件的重新编译。-include 指令在依赖文件存在时静默包含它们。这是稳健 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 包含来自命令行的目标。
# = 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))是简单后缀更改的简洁替代方案。
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 支持。
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 集成和跨平台支持。
# 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)使用 GDB 调试
启动与断点
用 -g 编译嵌入调试符号,用 -O0 禁用优化(否则变量可能被优化掉)。break 在函数、行或条件处设置断点。watch(数据断点)在变量更 改时触发——对于查找内存损坏很强大。条件断点(break func if cond)仅在条件为 true 时触发,适用于循环。tbreak 是一次性断点。
# 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 切换上下文以检查该帧。
(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 显示虚拟内存布局(文本、数据、堆、栈、共享库)。这对于调试缓冲区溢出和内存损坏至关重要。
# 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 显示所有线程状态——对于死锁分析至关重要。
# 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 threadsGDB 脚本与自动化
.gdbinit 在启动时自动化常见设置。define 为重复任务创建自定义命令。commands 将操作附加到断点(例如,记录变量并继续)。GDB 支持用于复杂分析的 Python 脚本:自动化测试运行、可视化数据结构或提取统计信息。Python 脚本可 以通过 gdb 模块访问 GDB 内部。使用脚本在团队之间标准化调试工作流。
# .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内存对齐与位域
结构体对齐与填充
编译器插入填充使每个成员自然对齐(通常到其大小:char=1、short=2、int=4、double=8)。从大到小重新排序成员最小化填充。使用 offsetof 检查布局。在 64 位系统上,指针需要 8 字节对齐。过多填充浪费内存并损害缓存性能。始终按大小递减排序结构体成员。
#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。切勿打包需要快速访问的结构体。
#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) 不包括灵活数组。
#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 浮点内部。
#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 时始终在两种字节序上测试。
#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;
}可变参数函数高级
va_list 基础
可变参数函数使用 va_list 访问可变参数。va_start 用最后一个命名参数初始化列表。va_arg 用指定类型检索下一个参数。va_end 清理。调用者必须传达计数和类型(例如,printf 使用格式说明符)。可变参数函数缺乏类型安全——类型不匹配导致未定义行为。
#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 时发出警告。始终记录预期的哨兵。缺点是哨兵不能作为有效数据值出现。
#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。此模式用于日志库、错误报告和自定义格式化器。格式字符串漏洞(用户控制格式)是安全风险——切勿将用户输入作为格式传递。
#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 中策略模式的基础。
#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 扩展在没有参数传递时删除前面的逗号。在发布构建中编译为无操作的调试宏无需代码更改即可消除开销。始终保护格式字符串以防止格式字符串攻击。
// 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);相关 C 代码片段
Copy-paste ready code for common tasks.
Pointer Basics
Declare pointers, dereference, and walk an array with pointer arithmetic.
Memory Management
Allocate, resize, and free heap memory with malloc, realloc, and free.
String Operations
Use string.h helpers for length, copy, concat, compare, and tokenize.
File I/O
Open, read, write, and close files using the stdio FILE API.
Structs
Group related fields with typedef and pass by pointer for mutation.
Function Pointers
Store function addresses for callbacks and dispatch tables.
Preprocessor Macros
Define object-like and function-like macros with conditional compilation.
Bit Operations
Set, clear, toggle, and test bits with bitwise operators and flags.
这篇内容对您有帮助吗?