简介:这份 C 语言函数库手册 PDF 面向刚入门 C 语言、需要频繁查阅标准库接口的开发者与学生,解决函数名、参数、返回值记不牢、查文档效率低的问题。内容以函数分类为主线:ctype.h 中的字符分类与大小写转换函数逐一列出判断条件,如 isalpha、isdigit、isxdigit、isspace 与 tolower、toupper 的取值区间;math.h、stdlib.h、string.h、float.h 相关的数学运算与数值转换函数也一并收录,包括 abs、fabs、exp、log、pow、sqrt、三角函数与双曲函数,以及 srand、rand、atof、atoi、itoa 等常用工具函数,并标注返回值类型与弧度、基数等关键参数含义。整包共 1 个 pdf 文件,约 51KB,体积轻巧,便于随查随用、离线阅读,也可打印成速查卡片。目前已有 115 人学习,适合作为课堂练习、课后作业和项目开发时的案头参考,帮助读者减少翻书与搜索成本,把精力集中在代码逻辑本身。
1. C 语言函数库手册 PDF 到底该怎么用,才不是翻完就忘
很多人硬盘里都躺着一份 C 语言函数库手册 PDF,可能是标准库中文手册的扫描版,也可能是从系统 man page 导出后拼起来的合集。平时想不起来翻,真写代码时还是先去搜索引擎里找strcpy到底返回什么、snprintf截断了怎么判断。问题不在手册本身,而在于 PDF 这个载体天生不擅长"按函数查"——它擅长线性阅读,不擅长按函数名、按头文件、按 errno 做交叉定位。
把一份 C 语言函数库手册用出价值,关键在于换个角色:把它当原料而不是成品。先用 man 的分节结构把手册里每个字段的含义吃透,再动手生成一份属于自己项目、能跳转能搜索的 PDF,接着用 pdf 解析的手段把手册变成可查询的索引,最后把手册里的一行函数原型变成能编译、能跑、能断言的验证用例。下面按这条链路走一遍,每一步都给到可复现的命令和参数。
2. 把 C 函数库手册里的字段读懂,比背函数名更值钱
2.1 man 2 与 man 3 的分工,以及 SYNOPSIS 段怎么读
C 函数库手册的骨架来自 man 的分节约定:man 2是系统调用(open、read、write、mmap),man 3是库函数(printf、strcpy、qsort、malloc)。新手最容易混的是"同一个名字在两个节里都有",比如open在 2 节是文件描述符,在 3 节是fopen那一套的缓冲流接口。手册 PDF 如果不标节号,检索出来的结果就会互相打架,所以自己整理 PDF 时,务必在页眉或索引里保留节号。
SYNOPSIS 段是手册里信息密度最高的一块,它同时告诉四件事:依赖哪个头文件、函数原型、参数的可选性、返回值类型。看下面这个典型条目:
#include <string.h> char *strcpy(char *dest, const char *src);#include行决定了你必须引入哪个头文件,原型里的const决定了src不会被改写,而手册正文里那句 "the destination buffer must be large enough" 是约束而不是检查——编译器不会替你验证dest到底有多大。很多人读手册只扫一眼函数名就跳过去,恰恰漏掉了这类"约束型描述",而它们才是后面写测试用例的依据。
2.2 返回值、errno 与头文件宏:手册里最容易跳过的三段
RETURN VALUE 和 ERRORS 两段是排查问题的入口。以snprintf为例,手册明确写返回值是"假如缓冲区足够大时应当写入的字符数",不包含结尾的\0;一旦这个值大于等于你传入的size,就说明输出被截断了。这条规则如果只看函数名是绝对猜不到的。
errno 的使用有个常见误解:不是所有函数失败都会设置 errno,只有在 ERRORS 段列出具体错误码的函数才保证设置。正确的写法是在调用前把errno清零,调用后立即读取:
#include <errno.h> #include <stdio.h> #include <stdlib.h> errno = 0; /* 手册要求:先清零,避免读到上次残留值 */ char *end = NULL; long v = strtol("999999999999", &end, 10); if (errno == ERANGE) { /* 手册 ERRORS 段列出的溢出错误码 */ fprintf(stderr, "out of range, value=%ld\n", v); }strtol的 ERRORS 段只列出EINVAL和ERANGE两个值,写判断时就只判断这两个,多写的分支反而会掩盖真实问题。手册里还有一类容易忽略的内容是头文件里的宏约束,比如<limits.h>的LONG_MAX、<stdint.h>的INT32_MAX,它们决定了你写long long数组做累加时在什么范围内不会溢出。
2.3 用 grep 和 apropos 在本地手册里按头文件、按 errno 反查
PDF 不适合反查,但导出的纯文本手册非常适合。先把 man 手册批量转成去控制符的 txt,这一步是后面所有检索的基础:
# 把 2、3 节的手册页导出成纯文本,col -b 去掉退格控制字符 mkdir -p ~/c-manual/txt for p in 2 3; do man -k . 2>/dev/null | awk -v p="($p)" '$2 == p {print $1}' done | sort -u | while read -r f; do man 3 "$f" 2>/dev/null | col -b > ~/c-manual/txt/"$f".txt done ls ~/c-manual/txt | wc -l有了这批文本,就能做两种搜索引擎给不了的查询。第一种是按头文件反查,找出某个头文件下到底声明了哪些函数,这在排查"我到底该 include 谁"时很有用:
# 按头文件反查函数清单 grep -l "include <stdio.h>" ~/c-manual/txt/*.txt | xargs -n1 basename # 按错误码反查:哪些函数可能返回 ERANGE grep -l "ERANGE" ~/c-manual/txt/*.txt | xargs -n1 basename # 用 NAME 段做关键字搜索,比 man -k 更容易进脚本 grep -il "string copy" ~/c-manual/txt/*.txt手册 PDF 里的各个段落对应的检索价值,可以整理成一张对照表,整理手册索引时按这张表决定抽哪些字段:
| man 段名 | 内容 | 检索价值 |
|---|---|---|
| SYNOPSIS | 头文件与函数原型 | 生成函数签名索引、做原型比对 |
| DESCRIPTION | 行为描述与约束 | 提取"must be large enough"类约束 |
| RETURN VALUE | 成功/失败返回值语义 | 生成断言条件 |
| ERRORS | errno 取值列表 | 生成异常分支测试 |
| CONFORMING TO | 遵循的标准 | 判断可移植性边界 |
| SEE ALSO | 相关函数 | 构建函数关联图 |
注意:
man -k依赖 mandb 数据库,如果刚装完系统没建库,apropos会返回空。先执行一次mandb再导出。
3. 用 Doxygen 生成一份属于自己的 C 函数库手册 PDF
3.1 针对 C 项目的 Doxyfile 关键参数
现成的手册 PDF 只能读,不能反映你项目里的函数。常见做法是用 Doxygen 从源码注释里直接生成手册,再交给 LaTeX 输出 PDF。C 项目需要显式打开几个开关,否则 Doxygen 会按 C++ 的假设去解析,遇到函数指针和宏定义就出错。
# Doxyfile 片段:面向纯 C 库的输出配置 PROJECT_NAME = "MyCLib Manual" OPTIMIZE_OUTPUT_FOR_C = YES # 按 C 的语义解析,避免 C++ 假设 EXTRACT_ALL = YES # 没有 /** */ 注释的函数也进手册 EXTRACT_STATIC = YES # 静态函数一并输出,便于内部审阅 INPUT = ./src ./include RECURSIVE = YES FILE_PATTERNS = *.c *.h GENERATE_HTML = NO GENERATE_LATEX = YES LATEX_CMD_NAME = xelatex HAVE_DOT = YES CALL_GRAPH = YES CALLER_GRAPH = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = NO这批参数里,OPTIMIZE_OUTPUT_FOR_C直接影响交叉引用的准确性,它会让 Doxygen 把函数指针 typedef 当作类型引用而不是未知符号。MACRO_EXPANSION决定宏包装过的函数能不能出现在手册里,比如你用#define API __attribute__((visibility("default")))修饰过导出函数,不开这个开关,手册里就只剩一个宏名。CALL_GRAPH依赖 Graphviz 的dot,没装的话编译阶段会只给警告,图不会生成。
几个参数的取值倾向和影响范围:
| 参数 | 常见取值 | 影响 |
|---|---|---|
| OPTIMIZE_OUTPUT_FOR_C | YES | 按 C 语义生成索引,函数指针解析更准 |
| EXTRACT_STATIC | YES / NO | 决定内部函数是否出现在手册 |
| MACRO_EXPANSION | YES | 宏包装的声明能否被识别 |
| LATEX_CMD_NAME | xelatex | 输出中文 PDF 的前提 |
| CALL_GRAPH | YES | 生成调用图,依赖 dot |
3.2 让 LaTeX 输出的中文 PDF 不掉字
Doxygen 的 LaTeX 模板默认用 pdflatex 和西文字体,中文注释和文档标题会直接丢失或者报字体错误。改成xelatex之后还需要引入 ctex 宏包。稳妥的路径是准备一个自定义样式文件,通过LATEX_EXTRA_STYLESHEET注入,不去改 Doxygen 的模板文件,这样升级 Doxygen 时不会冲突:
# doxygen-zh.sty 放在项目根目录,供生成出的 latex 工程引用 cat > doxygen-zh.sty <<'EOF' \usepackage{ctex} \usepackage{fontspec} \setmonofont{DejaVu Sans Mono} EOF doxygen Doxyfile cd latex && makemake会在 latex 目录下生成refman.pdf。如果中途卡在字体上,先确认fc-list :lang=zh能列出中文字体;如果只有警告没有报错,通常是对\subsection级别的标题做了截断,检查日志里Missing character附近的行号即可。生成出的手册 PDF 目录结构和原版 C 手册是一致的:模块页、文件页、数据结构页、函数索引,可以直接替换掉硬盘里那份扫描版。
3.3 函数指针、指针函数与调用图的解析差异
int (*f)(int)是函数指针,int *f(int)是指针函数,这两者在源码里只差一对括号,Doxygen 的解析结果完全不同。前者会被识别为变量类型,后者会被识别为返回指针的函数。写接口注释时,用typedef先把函数指针类型命名出来,手册里的交叉引用才会正确指向类型定义页:
/** * @brief 排序比较函数类型 * @param a 指向第一个元素 * @param b 指向第二个元素 * @return 负数、零、正数分别表示 a<b、a==b、a>b */ typedef int (*cmp_fn)(const void *a, const void *b); /** * @brief 对数组做原地排序 * @param base 数组首地址 * @param nmemb 元素个数 * @param size 单个元素字节数 * @param cmp 比较函数,见 cmp_fn */ void sort_array(void *base, size_t nmemb, size_t size, cmp_fn cmp);这样一来,sort_array的文档页会出现指向cmp_fn的链接,CALL_GRAPH 也能顺着回调把调用关系画出来。相反,如果直接把int (*cmp)(const void*, const void*)写进参数列表,Doxygen 会把cmp解析成一个无名类型参数,手册里就只剩一行光秃秃的原型,读者还得回去翻源码。
注意:CALL_GRAPH 画的是静态调用关系,通过函数指针发起的调用它识别不了,除非调用点显式写出具体函数名。做性能分析时别把它当成完整调用链。
4. 用 Python 解析 C 函数库手册 PDF,建一个可检索索引
4.1 用 pdfplumber 按坐标还原 SYNOPSIS 代码块
拿到一份现成的手册 PDF 后,第一步是把 SYNOPSIS 段抽出来。extract_text()对分栏排版的 PDF 会丢列对齐,函数原型经常被压成一行,正则根本没法匹配。更稳的做法是按 y 坐标聚行、按 x 坐标排序,把视觉上的行还原出来:
import pdfplumber def page_lines(page, y_tol=3): """按 y 坐标聚类成行,按 x 坐标排序还原阅读顺序""" words = page.extract_words(use_text_flow=False) rows = {} for w in words: key = round(w['top'] / y_tol) rows.setdefault(key, []).append(w) for key in sorted(rows): row = sorted(rows[key], key=lambda w: w['x0']) yield ' '.join(w['text'] for w in row) with pdfplumber.open('c-library-manual.pdf') as pdf: for i, page in enumerate(pdf.pages, 1): head = '\n'.join(page_lines(page)) if 'SYNOPSIS' in head: print(f'--- page {i} ---') print(head)extract_words拿到的每个词都带x0、top坐标,y_tol=3是把垂直距离 3 点以内的词视为同一行。这个值对手册这类单栏排版够用,如果原 PDF 是双栏扫描件,先按页面宽度从中间切开再处理,否则左右两栏会被拼到同一行。
4.2 正则解析函数原型,把手册变成结构化字段
还原出行之后,用一条正则把返回值、函数名、参数列表拆开。手册里的原型大多数是ret name(args);这一形态,宏包装和函数指针会漏掉,属于可接受的取舍:
import re PROTO = re.compile( r'^\s*(?P<ret>(?:const\s+)?[A-Za-z_]\w*(?:\s*\*)?)\s+' r'(?P<name>[A-Za-z_]\w*)\s*\((?P<args>[^;]*)\)\s*;\s*$' ) def parse_prototype(line): m = PROTO.match(line) if not m: return None return { 'ret': m.group('ret').strip(), 'name': m.group('name'), 'args': ' '.join(m.group('args').split()), }(?:\s*\*)?用来吃掉落在这个位置的单个星号,比如char *strcpy的返回类型;[^;]*限定参数列表里不能出现分号,避免把多行声明误吞。' '.join(...split())是把参数之间的多余空格压缩掉,保证同一个函数在不同页面上抽出来的args一致,方便去重。手册字段到数据库列的对应关系:
| 手册字段 | 正则组 | 数据库列 | 用途 |
|---|---|---|---|
| 返回类型 | ret | ret | 查看返回指针还是值 |
| 函数名 | name | name | 主键查询 |
| 参数列表 | args | args | 去重与比对 |
| 所在页码 | 外部传入 | page | 回查原文 |
| 所在头文件 | 上一行 include | header | 按头文件分组 |
4.3 落进 SQLite,写一个命令行检索入口
结构化的下一步是存起来,SQLite 单文件、零配置,适合放在仓库里当工具用:
import sqlite3 conn = sqlite3.connect('cmanual.db') conn.execute('''CREATE TABLE IF NOT EXISTS funcs ( name TEXT NOT NULL, ret TEXT, args TEXT, header TEXT, page INTEGER, PRIMARY KEY (name, args) )''') conn.executemany( 'INSERT OR REPLACE INTO funcs VALUES (:name, :ret, :args, :header, :page)', rows, ) conn.commit()查询时直接用 sqlite3 命令行,比写脚本更快:
# 按函数名模糊查 sqlite3 -header -column cmanual.db \ "SELECT name, ret, header FROM funcs WHERE name LIKE '%cpy%';" # 找出所有返回指针的函数,排查内存管理相关接口 sqlite3 -header -column cmanual.db \ "SELECT name, ret FROM funcs WHERE ret LIKE '%*%' ORDER BY name;"有了这张表,还能做手册版本比对:把两份不同来源的 PDF 分别解析入库,用EXCEPT找出新增或删除的函数声明,接口变更一目了然。这比用 pdf 转 word 再肉眼比对靠谱得多。
注意:PDF 解析出来的页码是 PDF 物理页码,不是手册印刷页码,回查时要加上封面和目录的偏移量。
5. 把手册条目变成能跑的验证用例
5.1 边界值从手册的约束句里反推
手册里"必须足够大""可能被截断"这类句子,翻译成代码就是边界条件。snprintf的返回值语义是最典型的一个:
#include <assert.h> #include <stdio.h> #include <string.h> int main(void) { char dst[8]; /* 手册:返回"假如空间足够"应写入的长度,不含结尾 \0 */ int n = snprintf(dst, sizeof dst, "%s", "abcdefghij"); assert(n == 10); /* 说明需要 10 字节,被截断了 */ assert(strlen(dst) == 7); /* 实际只写入了 7 个字符 + \0 */ return 0; }编译时打开-D_FORTIFY_SOURCE=2,让strcpy、sprintf这类无长度限制的函数在编译期就报警:
gcc -std=c11 -Wall -Wextra -O2 -D_FORTIFY_SOURCE=2 snprintf_check.c -o snprintf_check-O2是_FORTIFY_SOURCE生效的前提,优化等级不够时它不会被激活;-Wall -Wextra负责把隐式截断、格式串不匹配这类警告暴露出来。手册不会告诉你这些编译开关,但它们和手册里的约束描述是配套的。
5.2 函数指针用法的验证:qsort 比较函数的返回值陷阱
手册里qsort的比较函数要求返回负数、零、正数,很多示例直接写return *(int*)a - *(int*)b;。换成long long数组做累加或排序时,这个减法会溢出:
typedef int (*cmp_fn)(const void *, const void *); static int cmp_ll(const void *a, const void *b) { long long x = *(const long long *)a; long long y = *(const long long *)b; return (x > y) - (x < y); /* 手册只要求符号,不要求具体差值 */ }(x > y) - (x < y)的写法把结果严格约束在 -1、0、1 三个值上,避开了减法溢出后又截断成 int 的风险。不同函数在手册里没写全的约束,以及对应的验证方式:
| 函数 | 手册未写全的约束 | 验证方式 |
|---|---|---|
| strcpy | dest 的容量下限 | 编译期_Static_assert检查缓冲区长度 |
| snprintf | 截断的判断依据 | 断言返回值 >= size |
| qsort 比较函数 | 返回值不得溢出 int | 用符号表达式替代减法 |
| malloc | 失败时返回 NULL 而非置 errno | 显式判空,不依赖 errno |
| strtol | 溢出时返回边界值并置 ERANGE | 清零 errno 后检查 ERANGE |
把这批断言整理成一个tests/目录,用make check串起来,每次改动手册注释或函数签名时跑一遍,手册和实现就不会各自漂移。前面几节生成的手册 PDF 加索引 SQLite,配合这套用例,才算把一份 C 语言函数库手册从"存着"变成"用着"。
本文还有配套的精品资源,点击获取