程序结构与基本语法

50 minBeginner

C 程序的组成部分、注释规范、标识符规则、关键字与编译过程,对标 MIT/Stanford/CMU 教学水准。

程序结构与基本语法

“The most important property of a program is whether it accomplishes the intention of its user.” — C.A.R. Hoare

0. 前言与导读

本文是 FANDEX C 模块的基础章节,系统讲解 C 程序的组成结构、语法元素与编译过程。掌握本章是后续学习数据类型、指针、函数、模块化编程的前提。本文对标 K&R《The C Programming Language》第 1 章与 ISO/IEC 9899:2024 (C23) 标准。

阅读本文后,读者应能够:

  • 用准确的术语描述 C 程序的语法元素(token、preprocessing directive、declaration、statement、expression);
  • 编写结构清晰、注释规范、命名一致的 C 程序;
  • 理解 C 程序从源代码到可执行文件的完整编译流水线;
  • 区分声明(declaration)与定义(definition);
  • 使用现代 C(C11/C23)的语法特性编写可移植代码。

1. 学习目标

1.1 Bloom 分类目标

层次目标关键问题
Remember列举 C 程序的基本组成元素C 程序由哪些部分组成?
Understand解释 token、声明、语句的区别什么是 translation unit?
Apply编写一个完整 C 程序如何编写并编译 hello.c?
Analyze比较声明与定义extern 声明与定义有何不同?
Evaluate评估不同命名规范的优劣snake_case vs camelCase?
Create设计一个多文件项目结构如何组织 src/include/tests 目录?

1.2 具体目标

  1. 命名 C 程序的七大组成部分:预处理指令、声明、定义、函数、语句、表达式、注释。
  2. 解释 C 编译的四个阶段:预处理、编译、汇编、链接。
  3. 使用 GCC/Clang 的 -E/-S/-c 选项观察各阶段输出。
  4. 区分 declaration 与 definition 的语法与语义。
  5. 设计 一个跨平台 C 项目的目录结构。
  6. 撰写 符合团队规范的 C 代码(命名、注释、文件组织)。

2. 历史动机与发展脉络

2.1 K&R 时代的程序结构

K&R C(1978)的程序结构相对松散:

  • 隐式 int:未声明类型的变量默认 int
  • 旧式函数定义:int add(a, b) int a, b; { ... }
  • 无函数原型:调用前无需声明参数类型。
  • 注释仅 /* ... */

K&R 时代的 hello.c:

main()
{
    printf("hello, world\n");
}

注意:main 默认返回 intprintf 隐式声明(C89 仍允许,C99 起禁止)。

2.2 C89 的标准化

C89 引入了:

  • 函数原型:int add(int a, int b);
  • void 关键字。
  • constvolatile 类型限定符。
  • 完整的标准库。

C89 风格的 hello.c:

#include <stdio.h>

int main(void)
{
    printf("hello, world\n");
    return 0;
}

2.3 C99 的现代化

C99 引入了:

  • // 单行注释。
  • long long 类型。
  • 变量可在使用处声明(C89 要求在块首)。
  • for 循环内的变量声明:for (int i = 0; ...).
  • 指定初始化器:struct Point p = {.x = 1, .y = 2};

2.4 C11/C17 的并发与安全

C11 引入了:

  • 多线程:<threads.h>_Thread_local_Atomic
  • _Generic 泛型选择。
  • _Static_assert 静态断言。
  • 匿名结构体/联合体成员。
  • 边界检查接口(Annex K,可选)。

2.5 C23 的现代化

C23 进一步现代化:

  • booltruefalse 成为关键字。
  • nullptr 替代 NULL
  • auto 类型推断。
  • typeoftypeof_unqual
  • 数字分隔符:1'000'000
  • 二进制字面量:0b1010'1100
  • [[nodiscard]][[maybe_unused]] 标准化属性。
  • 移除 K&R 函数定义、三字母词。
  • constexpr 对象。

C23 风格的 hello.c:

#include <stdio.h>

int main(void)
{
    constexpr auto greeting = "hello, world\n";
    printf("%s", greeting);
    return 0;
}

2.6 演进时间线

1978 K&R C     ──── 隐式 int、旧式函数定义、/* */ 注释

1989 C89       ──── 函数原型、void、const/volatile

1999 C99       ──── // 注释、long long、for 内声明

2011 C11       ──── _Generic、_Atomic、_Static_assert

2018 C17       ──── C11 bug-fix

2024 C23       ──── nullptr、bool、auto 推断、constexpr、属性

2025+ C2y      ──── 模块、协程(草案)

3. 形式化定义

3.1 翻译阶段(Phases of Translation)

ISO/IEC 9899:2024 §5.1.1.2 定义了 C 程序的八个翻译阶段:

阶段操作
1物理源文件字符映射到源字符集(含三字母词替换,C23 移除)
2行拼接(反斜杠 \ 后换行被删除)
3注释替换为单个空格;token 化;识别预处理指令
4执行预处理指令(#include#define#if);展开宏
5字符常量与字符串字面量中的转义序列解释
6相邻字符串字面量拼接
7编译为汇编代码,再汇编为目标文件
8链接所有目标文件与库,解析外部引用,生成可执行文件

3.2 Token(词法单元)

ISO/IEC 9899:2024 §6.4 定义 token 分类:

token::=keywordidentifierconstantstring-literalpunctuator\text{token} ::= \text{keyword} \mid \text{identifier} \mid \text{constant} \mid \text{string-literal} \mid \text{punctuator}

预处理 token 还包含:头文件名、###__VA_ARGS__ 等。

3.3 标识符形式化

ISO/IEC 9899:2024 §6.4.2 定义标识符:

identifier::=identifier-nondigitidentifier identifier-nondigitidentifier digit\text{identifier} ::= \text{identifier-nondigit} \mid \text{identifier identifier-nondigit} \mid \text{identifier digit} identifier-nondigit::=nondigituniversal-character-name\text{identifier-nondigit} ::= \text{nondigit} \mid \text{universal-character-name}

其中 nondigit[A-Za-z_]digit[0-9]universal-character-name 允许 Unicode 字符(如 \u00E9 等价于 é)。

C23 进一步允许标识符中直接使用 Unicode 字符(部分实现):

int café = 1;  // C23 允许

3.4 声明与定义

声明(declaration):引入一个名字,告知其类型与链接性。

定义(definition):实际创建对象、函数或类型实例。

形式化:

Declarationintroduces nametype\text{Declaration} \to \text{introduces name} \to \text{type} Definitioncreates instancestorage\text{Definition} \to \text{creates instance} \to \text{storage}

例如:

extern int x;       /* 声明:告知 x 存在,类型 int */
int x = 42;         /* 定义:分配存储并初始化 */

int add(int, int);  /* 函数声明(原型) */
int add(int a, int b) { return a + b; }  /* 函数定义 */

3.5 翻译单元

ISO/IEC 9899:2024 §5.1.1.1:

TranslationUnit=SourceFileiHeaderiSkipped\text{TranslationUnit} = \text{SourceFile} \oplus \bigcup_{i} \text{Header}_i \ominus \text{Skipped}

预处理后,所有 #include 展开为一个完整的翻译单元。

3.6 作用域(Scope)

ISO/IEC 9899:2024 §6.2.1 定义作用域:

作用域范围
Block scope{} 内,自声明点到块尾
File scope从声明点到翻译单元末尾
Function prototype scope函数原型参数列表内
Function scope仅 label,整个函数内

C99 新增:

作用域范围
Function scope (for loop)for (int i = 0; ...)i 仅在循环内

形式化:

Scope(n)=region where name n is visible\text{Scope}(n) = \text{region where name } n \text{ is visible}

3.7 链接(Linkage)

ISO/IEC 9899:2024 §6.2.2:

链接类型可见范围
External linkage整个程序
Internal linkage当前翻译单元
No linkage当前作用域
int x = 1;              /* external linkage */
static int y = 2;       /* internal linkage */
void f(void) {
    int z = 3;          /* no linkage */
}

3.8 存储期(Storage Duration)

ISO/IEC 9899:2024 §6.2.4:

存储期生命周期
Static程序整个生命周期
Thread (C11)线程整个生命周期
Automatic所在代码块执行期间
Allocatedmallocfree

3.9 类型(Type)

ISO/IEC 9899:2024 §6.2.5 定义类型分类:

  • object type:对象类型(intstruct S 等)。
  • function type:函数类型。
  • incomplete type:不完整类型(如 voidstruct S; 前向声明)。

类型由四部分组成:

  1. 基本类型(basic type)。
  2. 派生类型(derived type):pointer、array、function、struct、union。
  3. 类型限定符(type qualifier):constvolatilerestrict_Atomic
  4. 函数说明符(function specifier):inline_Noreturn

4. 理论推导与原理解析

4.1 文法与语法分析

C 语法用上下文无关文法(CFG)描述。例如声明语句的产生式:

declaration::=declaration-specifiers init-declarator-listopt  ;\text{declaration} ::= \text{declaration-specifiers init-declarator-list}_{opt} \; ; declaration-specifiers::=storage-class-specifiertype-specifiertype-qualifierfunction-specifieralignment-specifier\text{declaration-specifiers} ::= \text{storage-class-specifier} \mid \text{type-specifier} \mid \text{type-qualifier} \mid \text{function-specifier} \mid \text{alignment-specifier}

复杂的”右左法则”用于解析 C 声明:

  1. 从标识符开始。
  2. 向右看(数组 []、函数 ())。
  3. 遇到 ); 时向左看(指针 *)。
  4. 重复 2-3 直到解析完成。

例如:

int (*fp)(int);

解析:fp 是指针,指向一个函数,函数接受 int 返回 int

4.2 词法分析的歧义

C 词法分析遵循”最大 munch”(maximal munch)原则:每一步都匹配最长的可能 token。

例如:

a+++b;     /* 等价于 a++ + b,而非 a + ++b */

但注意:

a---b;     /* 等价于 a-- - b,而非 a - --b */

更微妙的例子:

/* C 风格注释 */
i = a/*b;  /* 这是注释开始,而非除法 */

为避免歧义,建议在除号后加空格:

i = a / *b;  /* 显式除法 */

4.3 字符集与编码

C 源代码使用以下字符集:

  • 基本源字符集(96 字符):ASCII 字母、数字、标点、空白。
  • 扩展源字符集:通过 universal-character-name(\uXXXX)。

C23 引入 UTF-8 字符串字面量:

const char *s = u8"你好";      /* C11/C17: const char[] */
const char *t = u8"Hello";    /* C23: const char[],与之前一致 */
char8_t *u = u8"你好";         /* C23 中 u8"" 在 char8_t 字符串 */

4.4 转义序列

转义含义
\n换行 LF
\r回车 CR
\t水平制表
\v垂直制表
\f换页
\a响铃
\b退格
\\反斜杠
\'单引号
\"双引号
\?问号
\0空字符
\xHH十六进制
\OOO八进制
\uXXXX通用字符名(4 位十六进制)
\UXXXXXXXX通用字符名(8 位十六进制)

4.5 标识符长度

C 标准规定:

  • 最小支持长度:31 个有效字符(C89),63 个(C99+)。
  • 外部链接标识符:31 个有效字符(C89),31 个(C99),4095 个(C23+)。

实际编译器支持长度更长(GCC、Clang 通常支持 4095 字符)。

4.6 注释语义

注释在翻译阶段 3 被替换为单个空格:

int/* comment */x;  /* 等价于 int x; */

注意”行尾反斜杠续行”与”注释”的交互:

// comment \
this is still comment
int x;

C23 引入 // 单行注释已 20 余年,C89 仍要求 /* */

4.7 字符串字面量

字符串字面量类型:

char *a = "hello";           /* C89/C99/C11/C17: char[] */
const char *b = "hello";      /* C23: const char[](breaking change) */

C23 起,字符串字面量类型为 const char[](去除历史 char[]),修改字符串字面量是 UB。

修改字符串字面量:

char *s = "hello";
s[0] = 'H';   /* UB:修改只读内存 */

5. 代码示例

5.1 完整 C 程序结构

/* main.c — 完整 C 程序结构示例(C23) */

/* 1. 预处理指令 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

/* 2. 宏定义 */
#define PI 3.14159
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))

/* 3. 类型定义 */
typedef struct {
    int32_t x;
    int32_t y;
} Point;

typedef enum {
    COLOR_RED,
    COLOR_GREEN,
    COLOR_BLUE
} Color;

/* 4. 全局变量(外部链接) */
int g_counter = 0;

/* 5. 内部链接变量(仅本文件可见) */
static int s_internal = 42;

/* 6. 函数原型 */
static int32_t compute_distance(Point a, Point b);
[[nodiscard]] Point *point_new(int32_t x, int32_t y);
void point_free(Point *p);

/* 7. 主函数 */
int main(int argc, char *argv[])
{
    (void)argc;
    (void)argv;

    Point p1 = {.x = 0, .y = 0};
    Point p2 = {.x = 3, .y = 4};

    int32_t dist = compute_distance(p1, p2);
    printf("Distance: %d\n", dist);

    Point *p = point_new(10, 20);
    if (p == nullptr) {
        fprintf(stderr, "Allocation failed\n");
        return EXIT_FAILURE;
    }
    printf("Point: (%d, %d)\n", p->x, p->y);
    point_free(p);

    return EXIT_SUCCESS;
}

/* 8. 函数定义 */
static int32_t compute_distance(Point a, Point b)
{
    int32_t dx = b.x - a.x;
    int32_t dy = b.y - a.y;
    return (int32_t)(SQUARE(dx) + SQUARE(dy));
}

[[nodiscard]] Point *point_new(int32_t x, int32_t y)
{
    Point *p = malloc(sizeof(Point));
    if (p != nullptr) {
        p->x = x;
        p->y = y;
    }
    return p;
}

void point_free(Point *p)
{
    free(p);
}

5.2 头文件结构

/* point.h — 头文件 */
#ifndef POINT_H
#define POINT_H

#include <stdint.h>

/* 类型定义 */
typedef struct Point Point;

/* 函数原型 */
[[nodiscard]] Point *point_create(int32_t x, int32_t y);
void point_destroy(Point *p);
int32_t point_get_x(const Point *p);
int32_t point_get_y(const Point *p);
void point_set_x(Point *p, int32_t x);
void point_set_y(Point *p, int32_t y);

#endif /* POINT_H */

5.3 注释风格

/**
 * @brief 计算两个整数的最大公约数
 * @param a 第一个整数(非负)
 * @param b 第二个整数(非负)
 * @return GCD(a, b)
 *
 * 使用欧几里得算法。
 * 时间复杂度:O(log(min(a, b)))
 */
int gcd(int a, int b)
{
    while (b != 0) {
        int t = b;
        b = a % b;
        a = t;
    }
    return a;
}

5.4 标识符命名

/* snake_case:变量与函数 */
int user_age = 25;
int total_count = 0;
double calculate_average(int *scores, int count);

/* UPPER_CASE:宏与常量 */
#define MAX_BUFFER_SIZE 1024
const int DEFAULT_PORT = 8080;

/* PascalCase:类型(typedef) */
typedef struct {
    int x, y;
} Point2D;

typedef enum {
    StatusOk,
    StatusError
} Status;

/* _t 后缀:标准类型风格 */
typedef int32_t my_int_t;

5.5 关键字分类

5.5.1 类型关键字

char c = 'A';
short s = 32767;
int i = 2147483647;
long l = 9223372036854775807L;
long long ll = 9223372036854775807LL;
float f = 3.14f;
double d = 3.141592653589793;
long double ld = 3.14159265358979323846L;
void *ptr = NULL;
_Bool b = 1;          /* C99,C23 起 bool 是关键字 */

5.5.2 控制流关键字

if (x > 0) { /* ... */ } else { /* ... */ }
switch (x) {
    case 1: /* ... */ break;
    case 2: /* ... */ break;
    default: /* ... */ break;
}
for (int i = 0; i < 10; ++i) { /* ... */ }
while (cond) { /* ... */ }
do { /* ... */ } while (cond);
goto end;
end:
return 0;
break;
continue;

5.5.3 存储类关键字

auto x = 42;           /* C23: 类型推断;C89: 默认 */
register int y = 0;    /* C17 已弃用,C23 移除 */
static int z = 0;      /* 静态存储期 + 内部链接 */
extern int w;          /* 声明外部定义 */

5.5.4 类型限定符

const int ci = 42;             /* 不可修改 */
volatile int vi;               /* 不优化 */
int *restrict p = &arr[0];    /* 别名提示 */
_Atomic int ai = 0;           /* 原子操作 */

5.6 编译过程演示

# 1. 预处理:输出 .i 文件
gcc -std=c23 -E main.c -o main.i

# 2. 编译为汇编:输出 .s 文件
gcc -std=c23 -S main.c -o main.s

# 3. 汇编为目标文件:输出 .o 文件
gcc -std=c23 -c main.c -o main.o

# 4. 链接为可执行文件
gcc main.o -o main

# 一步完成
gcc -std=c23 -Wall -Wextra -O2 main.c -o main

5.7 多文件编译

# 方式一:一次编译
gcc -std=c23 main.c point.c -o app

# 方式二:分别编译,最后链接
gcc -std=c23 -c main.c -o main.o
gcc -std=c23 -c point.c -o point.o
gcc main.o point.o -o app

5.8 main 函数的所有形式

/* C89 标准 */
int main(void) { /* ... */ return 0; }
int main(int argc, char *argv[]) { /* ... */ return 0; }

/* C99 允许 */
int main(void) { /* ... */ }  /* 隐式 return 0; */

/* C23 允许 */
int main(void) { /* ... */ }  /* 隐式 return 0; */

/* 非标准(但常见) */
void main(void) { /* ... */ }          /* 错误:违反标准 */
int main(int argc, char **argv) { /* ... */ }  /* 等价于 char *argv[] */
int main(int argc, char *argv[], char *envp[]) { /* ... */ }  /* 非标准扩展 */

5.9 程序退出码

#include <stdlib.h>

int main(void)
{
    /* 标准退出码 */
    return EXIT_SUCCESS;  /* 等价于 return 0; */
    return EXIT_FAILURE;  /* 等价于 return 1; */

    /* 自定义退出码(POSIX 限制为 0-255) */
    return 42;
}

5.10 命令行参数

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; ++i) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

运行:

./app hello world
# 输出:
# argc = 3
# argv[0] = ./app
# argv[1] = hello
# argv[2] = world

5.11 环境变量

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

int main(void)
{
    char *path = getenv("PATH");
    if (path != nullptr) {
        printf("PATH = %s\n", path);
    }

    /* 设置环境变量 */
    setenv("MY_VAR", "hello", 1);  /* POSIX */
    char *my = getenv("MY_VAR");
    printf("MY_VAR = %s\n", my);

    return 0;
}

6. 对比分析

6.1 C 与 C++ 程序结构对比

特性CC++
入口函数int main(void)int main(int argc, char *argv[])同 C,另有 int main()
头文件包含#include <stdio.h>#include <iostream>
标准库命名<stdio.h><stdlib.h><cstdio><cstdlib>
命名空间std::
无(用 struct)支持
引用支持
重载支持
模板支持
异常try/catch
RAII无(手动管理)析构函数

6.2 C 与其他语言对比

维度CRustGoPython
编译方式编译型编译型编译型解释型
入口函数mainmainmain无(脚本)
类型系统静态弱静态强静态强动态
内存管理手动所有权GCGC
错误处理返回码Resulterror异常
模块系统头文件modpackageimport
注释///* *////* *////* */#
命名约定snake_casesnake_caseMixedCasesnake_case

6.3 注释规范对比

6.3.1 C 风格

/* 块注释 */
// 单行注释

/* Doxygen */
/**
 * @brief 函数说明
 * @param arg 参数说明
 * @return 返回值
 */

6.3.2 Python 风格

# 单行注释
"""Docstring"""
def func(arg):
    """Docstring"""
    pass

6.3.3 Rust 风格

// 单行
/* 块 */
/// 文档注释

6.4 何时使用何种注释

场景推荐注释
简短说明// 单行
多行说明/* ... */
函数文档Doxygen /** */
临时禁用代码#if 0 ... #endif(C 风格,更安全)
TODO// TODO(author): description
FIXME// FIXME: description

7. 常见陷阱与最佳实践

7.1 陷阱一:忘记 #include

int main(void) {
    printf("hello\n");  /* C89: 隐式声明返回 int,UB */
    return 0;
}

修正:

#include <stdio.h>

int main(void) {
    printf("hello\n");
    return 0;
}

7.2 陷阱二:头文件循环包含

/* a.h */
#include "b.h"

/* b.h */
#include "a.h"   /* 循环! */

最佳实践:使用 include guard。

#ifndef A_H
#define A_H
/* ... */
#endif

或 C23 的 #pragma once(非标准但广泛支持):

#pragma once
/* ... */

7.3 陷阱三:忘记函数原型

/* 没有 int add(int, int); 的声明 */
int main(void) {
    int s = add(1, 2);  /* C89: 隐式声明为 int add(); */
    return 0;
}

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

C99 起禁止隐式函数声明。

7.4 陷阱四:变量未初始化

int x;       /* 未初始化,UB 读取 */
if (x) { /* ... */ }

修正:

int x = 0;

7.5 陷阱五:混淆声明与定义

/* 错误:在头文件中定义变量 */
int x = 42;   /* 多文件包含会重复定义 */

修正:

/* 头文件 */
extern int x;

/* 源文件 */
int x = 42;

7.6 陷阱六:缺少返回值

int f(int x) {
    if (x > 0) return 1;
    /* 忘记处理 x <= 0 的情况:UB */
}

修正:

int f(int x) {
    if (x > 0) return 1;
    return 0;
}

7.7 陷阱七:分号缺失

struct Point {
    int x;
    int y;
}            /* 缺少分号 */
int main(void) { /* ... */ }

修正:

struct Point {
    int x;
    int y;
};           /* 必须有分号 */

7.8 陷阱八:注释嵌套

C 不允许 /* */ 嵌套:

/* outer /* inner */ outer continues */

实际只注释了 outer /* inner */,之后的 outer continues */ 是语法错误。

7.9 陷阱九:宏的副作用

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 3;
int z = MAX(x++, y++);  /* x++ 被展开两次 */

修正:使用 inline 函数。

static inline int max(int a, int b) {
    return a > b ? a : b;
}

7.10 陷阱十:变长数组(VLA)滥用

int n = get_size();
int arr[n];   /* C99 VLA,栈分配,可能爆栈 */

VLA 在 C11 改为可选,C23 仍有争议。建议大数组使用 malloc

7.11 综合最佳实践清单

  1. 启用所有警告-Wall -Wextra -Wpedantic -Werror
  2. 使用现代 C-std=c23 或至少 -std=c11
  3. 包含头文件:所有库函数调用前包含对应头文件。
  4. 声明函数原型:所有函数在调用前声明。
  5. 初始化变量:声明时即赋初值。
  6. 使用 const:不修改的参数标记 const
  7. 使用 static:内部函数标记 static
  8. 使用 stdint.h:固定宽度类型 int32_t 等。
  9. 避免宏:使用 inline 函数替代。
  10. 错误处理:检查所有可能失败的函数返回值。

8. 工程实践

8.1 文件组织

推荐目录结构:

project/
├── CMakeLists.txt
├── Makefile
├── README.md
├── include/                # 公共头文件
│   └── module.h
├── src/                    # 源文件
│   ├── module.c
│   └── main.c
├── tests/                  # 测试
│   ├── test_module.c
│   └── CMakeLists.txt
├── docs/                   # 文档
├── scripts/                # 脚本
│   └── build.sh
└── .github/
    └── workflows/
        └── ci.yml

8.2 头文件保护

8.2.1 传统 include guard

#ifndef MODULE_H
#define MODULE_H

/* ... */

#endif /* MODULE_H */

8.2.2 #pragma once

#pragma once

/* ... */

#pragma once 非标准但 GCC、Clang、MSVC 均支持。优点:

  • 不需选唯一宏名。
  • 编译速度略快(编译器可跳过整个文件)。

缺点:

  • 非标准。
  • 在符号链接等边缘情况下可能误判。

8.3 命名规范

参考 Linux Kernel 风格:

  • 局部变量:snake_case,如 user_count
  • 全局变量:g_ 前缀,如 g_config
  • 静态变量:s_ 前缀,如 s_cache
  • 函数:snake_case,如 parse_command
  • 类型:PascalCasesnake_case_t
  • 宏:UPPER_CASE,如 MAX_SIZE
  • 头文件宏:MODULE_H 格式。

8.4 注释规范

参考 Linux Kernel:

/*
 * 多行注释:第一行空,每行以 * 开头
 * 用于函数说明、复杂逻辑解释
 */

/* 单行注释:简洁 */

/* TODO: 描述待办事项 */

/* FIXME: 描述已知问题 */

8.5 函数规范

参考 Linux Kernel:

  • 函数长度:建议 50 行内,最长不超过 100 行。
  • 参数数量:建议 4 个以内,最多 7 个。
  • 出口参数:使用指针输出,返回值用于错误码。
/* 良好示例 */
int parse_int(const char *str, int *result);

8.6 错误处理

/* POSIX 风格:返回 -1,errno 设置 */
int fd = open("file.txt", O_RDONLY);
if (fd == -1) {
    perror("open");
    return EXIT_FAILURE;
}

/* Linux Kernel 风格:返回错误码(负数) */
long ret = sys_call(...);
if (ret < 0) {
    return ret;
}

/* 错误码枚举 */
typedef enum {
    ERR_OK = 0,
    ERR_INVALID_ARG = -1,
    ERR_OUT_OF_MEM = -2,
    ERR_IO = -3
} ErrorCode;

8.7 头文件设计

头文件应:

  1. 自包含(包含自身依赖的所有头文件)。
  2. 最小化依赖(不暴露实现细节)。
  3. 提供清晰 API。
  4. 使用 [[nodiscard]] 标记不可忽略返回值。
  5. 使用 const 标记不修改参数。
/* vector.h */
#ifndef VECTOR_H
#define VECTOR_H

#include <stddef.h>
#include <stdint.h>

typedef struct Vector Vector;

[[nodiscard]] Vector *vector_create(size_t capacity);
void vector_destroy(Vector *v);
[[nodiscard]] int vector_push(Vector *v, int32_t value);
[[nodiscard]] int vector_get(const Vector *v, size_t index, int32_t *out);
size_t vector_size(const Vector *v);

#endif /* VECTOR_H */

8.8 跨平台宏

/* platform.h */
#ifndef PLATFORM_H
#define PLATFORM_H

#if defined(_WIN32) || defined(_WIN64)
    #define PLATFORM_WINDOWS 1
#elif defined(__linux__)
    #define PLATFORM_LINUX 1
#elif defined(__APPLE__) && defined(__MACH__)
    #define PLATFORM_MACOS 1
#elif defined(__FreeBSD__)
    #define PLATFORM_FREEBSD 1
#else
    #define PLATFORM_UNKNOWN 1
#endif

#if defined(_WIN32)
    #define EXPORT __declspec(dllexport)
    #define IMPORT __declspec(dllimport)
#else
    #define EXPORT __attribute__((visibility("default")))
    #define IMPORT
#endif

#endif /* PLATFORM_H */

8.9 编译选项

CFLAGS := -std=c23 -Wall -Wextra -Wpedantic -Werror \
          -Wconversion -Wshadow -Wformat=2 \
          -Wmissing-prototypes -Wstrict-prototypes \
          -O2 -g -gdwarf-5

# Debug build
CFLAGS_DEBUG := -O0 -g -fsanitize=address,undefined

# Release build
CFLAGS_RELEASE := -O3 -DNDEBUG -flto

8.10 CI/CD

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        compiler: [gcc, clang]
    steps:
      - uses: actions/checkout@v4
      - name: Configure
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release
      - name: Build
        run: cmake --build build
      - name: Test
        run: cd build && ctest --output-on-failure

9. 案例研究

9.1 Linux Kernel 程序结构

Linux 内核的代码风格(Documentation/process/coding-style.rst):

  • 缩进:Tab(8 字符宽)。
  • 行宽:80 字符。
  • 函数长度:通常 50 行内,最长不超过 100 行。
  • 注释:/* */,不用 //(兼容 K&R 风格)。
  • 命名:snake_case,避免 PascalCase
  • 全局变量:使用 g_ 前缀(部分子系统)。
/* Linux kernel 风格示例 */
int register_device(struct device *dev)
{
    int ret;

    if (!dev)
        return -EINVAL;

    ret = mutex_lock_interruptible(&dev->mutex);
    if (ret)
        return ret;

    /* ... */

    mutex_unlock(&dev->mutex);
    return 0;
}

9.2 Redis 程序结构

Redis 风格特点:

  • 函数定义:返回类型独占一行。
void *
zmalloc(size_t size)
{
    void *ptr = malloc(size);
    if (!ptr) zmalloc_oom_handler(size);
    return ptr;
}
  • 全局变量:servershared
  • 字符串:自定义 SDS 类型。
  • 错误处理:返回 NULL 表示失败。

9.3 SQLite 程序结构

SQLite 风格特点:

  • 单文件合并:所有源文件合并为 sqlite3.c
  • 命名前缀:sqlite3_
  • 错误码:SQLITE_OKSQLITE_ERROR 等。
SQLITE_API int sqlite3_open(
    const char *filename,
    sqlite3 **ppDb
);

9.4 musl libc 程序结构

musl libc 风格特点:

  • 简洁:函数短小,避免过度抽象。
  • 标准:严格遵循 ISO C 与 POSIX。
  • 头文件组织:每个头文件对应源文件。

9.5 curl 程序结构

curl 风格特点:

  • 跨平台:使用宏抽象平台差异。
  • 模块化:每个协议独立文件。
  • 错误码:CURLE_OKCURLE_URL_MALFORMAT 等。

10. 习题

10.1 选择题

题 1:以下哪个是合法的 C 标识符?

A. 2variable B. _my_var C. int D. my-var

答案:B。_my_var 以字母(下划线)开头,后跟字母、数字、下划线。

题 2:C23 引入的二进制字面量前缀是?

A. 0x B. 0o C. 0b D. 2x

答案:C。0b1010 是 C23 引入的二进制字面量。

题 3:以下代码输出?

#include <stdio.h>
int main(void) {
    int a = 5;
    int b = a+++a;
    printf("%d\n", b);
    return 0;
}

A. 10 B. 11 C. 12 D. 未定义行为

答案:B。a+++a 按 maximal munch 解析为 a++ + a,即 5 + 6 = 11(但 a 的求值顺序未指定,可能是 UB;实际多数编译器为 11)。

题 4#include <stdio.h>#include "stdio.h" 的区别?

A. 没有区别 B. <> 在系统目录搜索,"" 先在当前目录搜索 C. "" 只搜索当前目录 D. <> 只搜索系统目录

答案:B。"" 先在当前目录搜索,找不到再到系统目录。

题 5:以下哪个不是 C23 的新增关键字?

A. bool B. nullptr C. constexpr D. class

答案:D。class 是 C++ 关键字,C 中没有。

题 6static 修饰局部变量时改变的是?

A. 作用域 B. 链接性 C. 存储期 D. 类型

答案:C。static 局部变量改变存储期(从自动到静态),作用域仍是块作用域。

题 7:以下代码哪个是定义?

A. extern int x; B. int x; C. int f(int); D. typedef int MyInt;

答案:B。int x; 是定义(分配存储)。A 是声明,C 是函数原型声明,D 是类型定义(不是变量定义)。

题 8:C 程序从源代码到可执行文件的正确顺序是?

A. 预处理 → 编译 → 链接 → 汇编 B. 编译 → 预处理 → 汇编 → 链接 C. 预处理 → 编译 → 汇编 → 链接 D. 链接 → 预处理 → 编译 → 汇编

答案:C。预处理 → 编译 → 汇编 → 链接。

10.2 填空题

题 1:C 程序的入口函数是 ________,其完整原型为 ________。

答案mainint main(int argc, char *argv[])int main(void)

题 2:C 语言支持的两种注释风格是 ________ 与 ________。

答案/* ... */// ...(C99 起)。

题 3:C 标识符第一个字符必须是 ________ 或 ________。

答案:字母;下划线。

题 4static 修饰全局变量时改变其 ________;修饰局部变量时改变其 ________;修饰函数时改变其 ________。

答案:链接性(external → internal);存储期(automatic → static);链接性。

题 5:C 翻译过程的四个主要阶段是

答案:预处理;编译;汇编;链接。

题 6:C23 中 nullptr 的类型是 ________。

答案nullptr_t

题 7extern int x; 是 ________(声明/定义),int x = 42; 是 ________(声明/定义)。

答案:声明;定义。

题 8:C 中字符串字面量 "hello" 的类型在 C23 中是 ________。

答案const char[6](包含末尾 \0)。

10.3 编程题

题 1:编写一个完整的 C23 程序,包含:

  • 头文件包含
  • 类型定义
  • 函数原型
  • main 函数
  • 一个辅助函数定义

要求:计算并打印一个圆的面积。

参考答案

/* circle.c — 计算圆面积 */
#include <stdio.h>
#include <math.h>

constexpr double PI = 3.141592653589793;

typedef struct {
    double radius;
} Circle;

double circle_area(const Circle *c);
void circle_print(const Circle *c);

int main(void)
{
    Circle c = {.radius = 5.0};
    circle_print(&c);
    return 0;
}

double circle_area(const Circle *c)
{
    return PI * c->radius * c->radius;
}

void circle_print(const Circle *c)
{
    printf("Circle with radius %.2f has area %.4f\n",
           c->radius, circle_area(c));
}

编译:

gcc -std=c23 -Wall -Wextra -O2 circle.c -lm -o circle
./circle

题 2:编写一个程序,演示变量存储期的差异(自动 vs 静态)。

参考答案

#include <stdio.h>

void counter_auto(void)
{
    int count = 0;   /* 自动存储期 */
    ++count;
    printf("auto: %d\n", count);
}

void counter_static(void)
{
    static int count = 0;   /* 静态存储期 */
    ++count;
    printf("static: %d\n", count);
}

int main(void)
{
    for (int i = 0; i < 3; ++i) {
        counter_auto();
        counter_static();
    }
    return 0;
}

输出:

auto: 1
static: 1
auto: 1
static: 2
auto: 1
static: 3

题 3:编写一个跨平台程序,使用宏检测操作系统并输出对应消息。

参考答案

#include <stdio.h>

int main(void)
{
#if defined(_WIN32)
    puts("Running on Windows");
#elif defined(__APPLE__) && defined(__MACH__)
    puts("Running on macOS");
#elif defined(__linux__)
    puts("Running on Linux");
#elif defined(__FreeBSD__)
    puts("Running on FreeBSD");
#elif defined(__NetBSD__)
    puts("Running on NetBSD");
#elif defined(__OpenBSD__)
    puts("Running on OpenBSD");
#elif defined(__sun)
    puts("Running on Solaris");
#elif defined(__HAIKU__)
    puts("Running on Haiku");
#else
    puts("Running on unknown platform");
#endif
    return 0;
}

题 4:编写一个简单的命令行参数解析器,输出所有参数。

参考答案

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Number of arguments: %d\n", argc);
    for (int i = 0; i < argc; ++i) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }
    return 0;
}

题 5:编写一个程序,使用 _Static_assert 在编译期验证假设。

参考答案

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

static_assert(sizeof(int) * CHAR_BIT >= 32, "int must be at least 32 bits");
static_assert(sizeof(void *) == 8, "64-bit platform required");
static_assert(sizeof(int64_t) == 8, "int64_t must be 8 bytes");

int main(void)
{
    printf("sizeof(int) = %zu\n", sizeof(int));
    printf("sizeof(void *) = %zu\n", sizeof(void *));
    printf("sizeof(int64_t) = %zu\n", sizeof(int64_t));
    printf("All compile-time assertions passed.\n");
    return 0;
}

10.4 思考题

题 1:为什么 C 程序的入口函数必须是 main?是否可以自定义入口?

参考答案要点

  • 标准规定:ISO/IEC 9899 §5.1.2.2.1 明确规定程序入口为 main
  • 链接器约定:C 运行时(CRT)调用 main,CRT 由编译器提供。
  • 自定义入口:可通过链接器选项(如 -e)修改,但失去可移植性。
  • 嵌入式系统:部分嵌入式系统使用 _start 或自定义入口(无 OS)。
  • 操作系统:内核入口通常不是 main,而是 kmain 或汇编入口。

题 2:声明与定义的区别是什么?为什么头文件中只放声明不放定义?

参考答案要点

  • 声明:告知编译器名字的存在与类型,不分配存储。
  • 定义:实际分配存储或实现函数。
  • 头文件只放声明:因为头文件可能被多个源文件包含,若放定义会导致重复定义错误(ODR 违反)。
  • 例外:static inline 函数、模板(C++)、constexpr 对象(C23)可放头文件。

题 3:为什么 C 不允许嵌套函数?对比 Pascal 与 Python。

参考答案要点

  • C 标准:不允许嵌套函数定义。
  • GCC 扩展:支持嵌套函数(非标准)。
  • 原因:C 的栈帧模型不支持闭包,嵌套函数需非局部跳转。
  • Pascal/Python:支持嵌套函数,但 Python 实际是闭包。
  • 现代 C 替代方案:使用 callback + void* 上下文参数。

题 4:为什么 C 的字符串字面量在 C23 中改为 const char[]?这是否会破坏现有代码?

参考答案要点

  • 安全性:避免意外修改只读字符串字面量(UB)。
  • Breaking change:是,但实际多数代码已使用 const char *
  • 兼容性:编译器可能提供 -Wstring-literal-const 警告而非错误。
  • 迁移成本:修改 char *s = "..."const char *s = "..."

题 5:阅读以下代码,指出所有错误并修正。

#include <stdio.h>

extern int counter;

int main {
    printf("counter = %d\n", counter)
    return 0
}

int counter = 42;

参考答案

错误:

  1. int main { 应为 int main(void) {
  2. printf 语句后缺分号 ;
  3. return 0 后缺分号。
  4. counter 在 main 中使用前未声明(extern 在前可以,但应该确保链接)。

修正:

#include <stdio.h>

extern int counter;

int main(void)
{
    printf("counter = %d\n", counter);
    return 0;
}

int counter = 42;

11. 参考文献

[1] Kernighan, B. W. and Ritchie, D. M. 1988. The C Programming Language (2nd ed.). Prentice Hall, Englewood Cliffs, NJ.

[2] International Organization for Standardization. 2024. Information technology — Programming languages — C (ISO/IEC 9899:2024). ISO, Geneva, Switzerland.

[3] International Organization for Standardization. 2018. Information technology — Programming languages — C (ISO/IEC 9899:2018). ISO, Geneva, Switzerland.

[4] Prinz, P. and Crawford, T. 2022. C in a Nutshell (2nd ed.). O’Reilly Media, Sebastopol, CA.

[5] King, K. N. 2008. C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company, New York, NY.

[6] Seacord, R. C. 2013. Effective C: An Introduction to Professional C Programming. No Starch Press, San Francisco, CA.

[7] Gustedt, J. 2019. Modern C (2nd ed.). Manning Publications, Shelter Island, NY. Available at https://gustedt.gitlabpages.inria.fr/modern-c/

[8] Torvalds, L. et al. 2024. Linux Kernel Coding Style. Available at https://www.kernel.org/doc/html/latest/process/coding-style.html

[9] Saks, D. 2024. C23: What’s new in the latest C standard. Overload 32, 178 (April 2024), 14–27.

[10] Stroustrup, B. 2013. The C++ Programming Language (4th ed.). Addison-Wesley, Boston, MA.

[11] Bryant, R. E. and O’Hallaron, D. R. 2015. Computer Systems: A Programmer’s Perspective (3rd ed.). Pearson, Boston, MA.

[12] Aho, A. V., Lam, M. S., Sethi, R., and Ullman, J. D. 2006. Compilers: Principles, Techniques, and Tools (2nd ed.). Addison-Wesley, Boston, MA.

[13] CERT. 2023. SEI CERT C Coding Standard (4th ed.). Carnegie Mellon University, Software Engineering Institute. Available at https://wiki.sei.cmu.edu/confluence/display/c/

[14] Free Software Foundation. 2024. GCC Manual. Available at https://gcc.gnu.org/onlinedocs/

[15] LLVM Team. 2024. Clang Compiler User’s Manual. Available at https://clang.llvm.org/docs/

[16] Hollinbeck, S., Overton, C., and Seacord, R. C. 2024. C23 Standard: What’s New. Carnegie Mellon University, Software Engineering Institute, Pittsburgh, PA.

[17] Richards, M. 1967. BCPL: A tool for compiler writing and system programming. MIT Press, Cambridge, MA.

[18] Thompson, K. 1972. Users’ Reference to B. Bell Telephone Laboratories, Murray Hill, NJ.

[19] Ritchie, D. M. 1993. The development of the C language. In The Second ACM SIGPLAN Conference on History of Programming Languages (HOPL II), 201–208. DOI: https://doi.org/10.1145/154766.155580

[20] Regehr, J. 2010. A guide to undefined behavior in C and C++. Available at https://blog.regehr.org/archives/213

[21] GNU Project. 2024. Doxygen Manual. Available at https://www.doxygen.nl/manual/

[22] Apache Software Foundation. 2024. Apache C Standard Library Style Guide. Available at https://httpd.apache.org/dev/styleguide.html

[23] Google. 2024. Google C++ Style Guide. Available at https://google.github.io/styleguide/cppguide.html

[24] CMake Community. 2024. CMake Tutorial. Available at https://cmake.org/cmake/help/latest/guide/tutorial/

[25] MIT OpenCourseWare. 2024. 6.S081: Operating System Engineering. Available at https://pdos.csail.mit.edu/6.S081/

[26] Stanford Computer Science. 2024. CS107: Computer Organization & Systems. Available at https://web.stanford.edu/class/archive/cs/cs107/

[27] Carnegie Mellon University. 2024. 15-213: Introduction to Computer Systems. Available at https://www.cs.cmu.edu/~213/

[28] Ertl, M. A. and Gregg, D. 2003. The structure and performance of efficient interpreters. Journal of Instruction-Level Parallelism 5, 1, 1–25.

[29] Patterson, D. A. and Hennessy, J. L. 2020. Computer Organization and Design RISC-V Edition (2nd ed.). Morgan Kaufmann, Cambridge, MA.

[30] O’Donoghue, P. 2023. CPPCHECK Manual. Available at http://cppcheck.sourceforge.net/manual.pdf


12. 延伸阅读

12.1 经典书籍

  1. K&R《The C Programming Language》:C 圣经,第 1 章讲程序结构。
  2. King《C Programming: A Modern Approach》:现代教材。
  3. Prinz《C in a Nutshell》:参考手册。
  4. Seacord《Effective C》:现代 C 实践。
  5. Gustedt《Modern C》:覆盖 C17/C23,免费开放。

12.2 在线资源

  1. cppreference.comhttps://en.cppreference.com/w/c
  2. ISO/IEC 9899 草案https://www.open-std.org/jtc1/sc22/wg14/
  3. GCC Manualhttps://gcc.gnu.org/onlinedocs/
  4. Clang Manualhttps://clang.llvm.org/docs/
  5. Linux Kernel Coding Stylehttps://www.kernel.org/doc/html/latest/process/coding-style.html

12.3 课程

  1. MIT 6.S081 Operating System Engineeringhttps://pdos.csail.mit.edu/6.S081/
  2. Stanford CS107 Computer Organization & Systemshttps://web.stanford.edu/class/archive/cs/cs107/
  3. CMU 15-213 Introduction to Computer Systemshttps://www.cs.cmu.edu/~213/
  4. Berkeley CS61C Great Ideas in Computer Architecturehttps://cs61c.org/

12.4 论文

  1. Ritchie, D. M. (1993). The Development of the C Language. ACM HOPL II.
  2. Stroustrup, B. (1991). The C++ Programming Language (3rd ed.).
  3. Bryant, R. E. and O’Hallaron, D. R. (2015). Computer Systems: A Programmer’s Perspective.

12.5 开源项目

  1. Linux Kernelhttps://github.com/torvalds/linux
  2. musl libchttps://musl.libc.org/
  3. Redishttps://github.com/redis/redis
  4. SQLitehttps://www.sqlite.org/
  5. curlhttps://github.com/curl/curl

12.6 工具

  1. GCChttps://gcc.gnu.org/
  2. Clang/LLVMhttps://clang.llvm.org/
  3. CMakehttps://cmake.org/
  4. Makehttps://www.gnu.org/software/make/
  5. Doxygenhttps://www.doxygen.nl/
  6. clang-tidyhttps://clang.llvm.org/extra/clang-tidy/
  7. cppcheckhttp://cppcheck.sourceforge.net/

12.7 社区与博客

  1. Stack Overflow C taghttps://stackoverflow.com/questions/tagged/c
  2. Reddit r/C_Programminghttps://www.reddit.com/r/C_Programming/
  3. Lobsters C taghttps://lobste.rs/t/c
  4. Eli Bendersky’s Websitehttps://eli.thegreenplace.net/

12.8 编码规范

  1. GNU Coding Standardshttps://www.gnu.org/prep/standards/
  2. Linux Kernel Coding Stylehttps://www.kernel.org/doc/html/latest/process/coding-style.html
  3. Google C++ Style Guidehttps://google.github.io/styleguide/cppguide.html
  4. CERT C Secure Coding Standardhttps://wiki.sei.cmu.edu/confluence/display/c/
  5. MISRA C:2012https://www.misra.org.uk/

附录 A:C 关键字全表

A.1 C89 关键字(32 个)

auto        break       case        char
const       continue    default     do
double      else        enum        extern
float       for         goto        if
int         long        register    return
short       signed      sizeof      static
struct      switch      typedef     union
unsigned    void        volatile    while

A.2 C99 新增(5 个)

_Bool       _Complex    _Imaginary  inline    restrict

A.3 C11 新增(7 个)

_Alignas    _Alignof    _Atomic     _Generic
_Noreturn   _Static_assert  _Thread_local

A.4 C23 新增

alignas     alignof     bool        char8_t
char16_t    char32_t    constexpr   nullptr
static_assert   thread_local  typeof  typeof_unqual

A.5 C23 弃用

register    _Alignas/_Alignof (推荐 alignas/alignof)

附录 B:C 标点符号

符号用途
;语句结束、声明结束
,分隔符(参数、初始化器)
.结构体成员访问
->指针访问成员
[]数组下标、声明
()函数调用、声明、分组
{}块、初始化
*指针、乘法、解引用
&取地址、按位与
#预处理指令
##预处理 token 拼接
...可变参数
:case、label、位域
?三目运算符
=赋值
== != < > <= >=比较
+ - * / %算术
<< >>移位
^ ` ~`
&& `
++ --自增自减
+= -= *= /= %=复合赋值
<<= >>= &= ^= `=`

附录 C:C 程序结构总览

┌─────────────────────────────────────┐
│ 1. 预处理指令                         │
│    #include <stdio.h>                │
│    #define MACRO value               │
│    #if / #ifdef / #endif             │
├─────────────────────────────────────┤
│ 2. 类型定义                          │
│    typedef struct { ... } Type;       │
│    typedef enum { ... } Enum;         │
├─────────────────────────────────────┤
│ 3. 全局变量声明                      │
│    extern int g_var;                 │
│    static int s_var;                 │
├─────────────────────────────────────┤
│ 4. 函数原型                          │
│    int function(int, int);           │
├─────────────────────────────────────┤
│ 5. main 函数                         │
│    int main(int argc, char *argv[]){ │
│        /* ... */                     │
│        return 0;                     │
│    }                                 │
├─────────────────────────────────────┤
│ 6. 函数定义                          │
│    int function(int a, int b) {      │
│        /* ... */                     │
│    }                                 │
└─────────────────────────────────────┘

更新日志 (Changelog)

  • 2026-04-05: 拆分并细化基础语法。
  • 2026-04-05: 详细扩写内容,增加了头文件结构、注释最佳实践、命名约定、关键字分类、编译过程详解和程序执行流程。
  • 2026-07-20: 第二批金标准升级(对标 MIT/Stanford/CMU 教学水准)。重构为 12 章结构,新增 Bloom 学习目标、翻译阶段、token 形式化、声明 vs 定义、跨平台宏、Linux Kernel 风格案例、详细习题与 ACM 参考文献等。行数从 259 扩展至约 1500 行。

延伸阅读

返回入门指南