简介:《C语言标准函数库详解》PDF 面向C语言初学者及需要查阅标准库用法的开发者,围绕头文件与常用函数展开系统梳理,可作为课堂学习之外的案头查询手册。压缩包内仅含1个PDF文件,体积约967KB,轻量便携,便于在电脑或移动设备上随时翻阅。内容以头文件为纲,逐章讲解 assert.h、ctype.h、errno.h、limits.h、math.h、stdio.h、stdlib.h、time.h 等诊断、字符类别测试、错误处理、数学运算、输入输出与日期时间模块,并进一步细化到具体函数,如 clearerr、feof、ferror、fflush、fopen/fclose、fprintf、fread 等IO函数的原型、功能与典型用法,对错误标志复位、文件缓冲区刷新、格式化输入输出等细节也有说明,便于对照排查调用问题。目录层级清晰,先总览头文件再深入单个函数,既能按需检索,也能顺序通读。已有12394人学习下载,适合作为C语言编程基础阶段的常备参考。
1. 从一份能当字典翻的手册说起:C 语言 函数库查得到与查得准
接手别人写了三年的 C 工程,最耗时间的往往不是业务逻辑,而是fseek之后忘了rewind、fread的返回值当成字节数用、strncpy拷完发现末尾没有结束符。这些问题的答案都在 C语言 函数库里,只是标准只规定了行为,没规定怎么讲给人听。这份《C语言标准函数库详解》把条目按 IO、字符处理、字符串、数学、时间日期、内存与进程分成七章排开,每个函数给出原型、参数、返回值和示例,适合两类人:刚学完语法、写不出能跑的程序的新手,和写了几年、遇到某个函数只会复制粘贴的老手。下面按我自己查这本手册的顺序走一遍,从编译验证链讲到几个真正会翻车的边界。
2. 头文件地图:十三个头文件的边界与一条可编译的验证链
2.1 查函数之前先认清「谁规定的」
同样一个printf,ISO C 只规定格式串的语义和返回值含义,具体怎么把字符刷到设备上,由各个 C 库实现自己决定。这就意味着手册里的函数并不处在同一个「法律层级」上:一部分是 ISO C 明文规定的,换任何编译器都能用;一部分是某些平台自己加进去的扩展,只在特定工具链下成立。C89 定下十五个头文件,C99、C11 陆续补进stdint.h、stdbool.h、stdalign.h这些,而手册第四章里出现的strlwr、strupr、strrev、strset,以及第三章的isascii、toascii,从头到尾没进过 ISO C,它们分别来自某商业编译器的扩展和 POSIX 传统。
不分清这条线,代价很直接:本地能编过,换一个编译目标就报隐式声明,或者行为悄悄变了。我一般的习惯是,翻开手册查某个函数之前,先确认它声明在哪个头文件里,再确认这个头文件属于 ISO C 还是平台扩展。
还有一层结构要记住。头文件只放声明,真正的机器码在库里,链接期才做符号解析。这决定了「找不到声明」是编译期告警或错误,「找不到定义」是链接期错误,两者排查方向完全不同,第 5 章会分别给出现场。
2.2 十三个头文件的分工表
手册第一章按字母序列出了十三个头文件,这十三个是日常写代码出现频率最高的。把它们整理成一张表,查的时候先定位到行,再去翻具体条目,比从头翻页快得多。
| 头文件 | 主要声明 | 典型使用场景 | 最容易忽略的点 |
|---|---|---|---|
<assert.h> | assert宏 | 调试期断言、参数自检 | 定义NDEBUG后整个宏消失,断言里的副作用表达式也会一起消失 |
<ctype.h> | is*/to*系列 | 解析文本协议、词法分析 | 参数必须是unsigned char的值或EOF,直接传char在部分平台上越界 |
<errno.h> | errno、EDOM、ERANGE | 判断库函数失败的具体原因 | 成功的调用不会把errno清零,必须先置 0 再调用 |
<limits.h> | INT_MAX、CHAR_BIT、LONG_MAX | 定长选型、上界检查 | CHAR_BIT不一定是 8,写死 8 的位运算在特殊平台会错 |
<locale.h> | setlocale、struct lconv | 千分位、货币符号、小数点 | 改 locale 会影响printf输出的小数点字符 |
<math.h> | sin、sqrt、fmod、frexp | 数值计算、信号处理 | 需要显式链接数学库,且链接参数位置有讲究 |
<setjmp.h> | setjmp、longjmp | 深层嵌套里的统一错误跳出 | 跳转后非volatile的局部变量值不确定 |
<signal.h> | signal、raise | 处理外部中断、超时 | 处理函数里只能调用异步信号安全的函数 |
<stdarg.h> | va_list、va_start、va_end | 变参日志、格式化封装 | 必须成对va_end,va_list不能直接赋值拷贝,要用va_copy |
<stddef.h> | size_t、NULL、offsetof | 通用类型定义、结构体成员偏移 | offsetof只对标准布局结构体成立 |
<stdio.h> | FILE、printf、fopen、fread | 一切输入输出 | 返回值语义分「项数」「字节数」「字符数」三种,混用必错 |
<stdlib.h> | malloc、qsort、strtol、exit | 内存、排序、字符串转数值、进程退出 | exit会触发atexit注册的函数,_Exit不会 |
<string.h> | mem*、str*两大族 | 缓冲区操作、字符串处理 | 重叠区间只能用memmove,memcpy行为未定义 |
表格只是索引,具体参数和返回值仍然要回到手册对应条目去看。我通常会把这张表打印出来夹在手册前面,查的时候两步定位:先按场景找头文件,再按字母序找函数名。
2.3 一次性 include 全量头文件的探针程序
判断当前工具链到底提供了哪些声明,最省事的办法是写一个探针程序,把手册里列的头文件一次全包含进来,把关键宏和类型宽度打印出来。这样在换编译器、换目标板的时候,跑一遍就知道环境底细。
/* probe.c —— 一次性确认当前实现暴露了哪些标准声明与宏 */ #include <assert.h> #include <ctype.h> #include <errno.h> #include <limits.h> #include <locale.h> #include <math.h> #include <setjmp.h> #include <signal.h> #include <stdarg.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <float.h> struct node { int id; double score; }; /* 用于演示 offsetof 的成员偏移 */ int main(void) { printf("CHAR_BIT = %d\n", CHAR_BIT); printf("INT_MAX = %d\n", INT_MAX); printf("LONG_MAX = %ld\n", LONG_MAX); printf("sizeof(size_t) = %zu\n", sizeof(size_t)); printf("sizeof(long) = %zu\n", sizeof(long)); printf("offsetof(score) = %zu\n", offsetof(struct node, score)); printf("DBL_EPSILON = %.17g\n", DBL_EPSILON); printf("sqrt(2.0) = %.17g\n", sqrt(2.0)); printf("locale = %s\n", setlocale(LC_ALL, NULL)); return EXIT_SUCCESS; }编译时把语言标准锁死,并打开全部告警:
cc -std=c11 -Wall -Wextra -Wpedantic probe.c -o probe ./probe-std=c11把语言版本钉在 C11,避免编译器默认用扩展方言把非标准写法放过去。-Wall -Wextra打开常用告警集合,隐式声明、有符号无符号比较这类问题会在这里暴露。-Wpedantic会拒绝一切扩展语法,如果某个头文件里的声明只在扩展模式下存在,这一步就会报出来。sqrt在部分平台上需要额外链接数学库,如果链接阶段报未定义符号,把-lm加在源文件名后面:
cc -std=c11 -Wall -Wextra probe.c -o probe -lm顺序不能反。链接器从左到右扫描,-lm写在源文件前面时,那一刻还没有任何未解析的数学符号,库会被直接跳过,然后报一堆undefined reference to sqrt。这是我自己翻车过两次的地方。
跑出来的输出里,sizeof(long)在不同平台上是 4 或 8,CHAR_BIT绝大多数是 8 但不保证。这几个数字直接决定了第 3 章ftell的边界能不能用。
2.4 用预处理输出定位声明与宏的真实来源
有时候两个头文件里定义了同名宏,或者某个函数到底来自哪个头文件搞不清楚,靠翻手册不如直接看预处理结果。-E只做预处理不做编译,-H会打印整棵包含树。
# 定位某个宏是在哪个头文件里定义的 cc -std=c11 -E -dD probe.c | grep -n "define INT_MAX" # 打印完整的头文件包含树,看看到底引入了哪些文件 cc -std=c11 -H -c probe.c -o /dev/null 2>&1 | head -40-dD让预处理器在输出里保留所有宏定义,配合grep就能看到INT_MAX的具体展开形式,不同实现展开成2147483647还是__INT_MAX__一目了然。-H把每个被包含的头文件路径打到标准错误,2>&1把它并到标准输出才能管道给head。-c表示只编译不链接,-o /dev/null丢掉目标文件,我们只关心包含树。
提示:包含树里出现大量内部头文件是正常的,一个
<stdio.h>往往会带出十几个实现内部头。看树的时候只找自己 include 的那一层。
2.5 查一个函数时的固定顺序
手册每个条目的结构大致是:函数原型、功能描述、参数说明、返回值、示例。我查的时候按五步走,比随机翻页稳。
先看原型,确认每个参数的类型和个数,尤其是size_t和int的混用处;再看返回值的类型和特殊值,fread返回项数、fgetc返回int而不是char、snprintf返回「本来需要写入的长度」,这些是全篇最容易记错的地方;第三步看错误报告方式,是靠返回值、靠errno、还是靠流上的错误指示器;第四步扫一眼示例,重点看示例里有没有做返回值检查;最后把函数名和头文件记在便签上,下次直接定位。
按这个顺序过一遍,手册就不只是「查得到」,而是「查得准」。
3. IO 函数族:fopen 模式、返回值语义和缓冲区的三层结构
3.1 一个 FILE* 背后其实有三层缓冲
<stdio.h>里所有函数都围绕FILE *这个句柄转,但FILE结构体本身不保存数据,它管理的是一条流。写出去的字节通常要经过三层:你自己在应用层维护的缓冲区、FILE内部的库缓冲区、操作系统内核的页缓存。理解这三层的分工,很多「数据没落盘」的困惑就消失了。
fflush(fp)清的是第二层,也就是把库缓冲区里的内容交给内核;它不保证数据已经写到磁盘介质上,断电依然可能丢。要真正落盘,得在内核层面做同步,那是fflush之外的另一个动作。反过来,fclose(fp)会先做一次fflush再释放FILE,所以忘记fclose的程序崩在退出前,最后一段输出可能就没了。
FILE内部缓冲区默认大小是BUFSIZ,全缓冲模式下攒够才写。标准输出接终端时通常是行缓冲,攒到一个换行就刷;重定向到文件就变成全缓冲。这个差别导致很多「本地看着正常、跑批处理日志就断在半截」的现场。
3.2 fopen 的六种基础模式与 b 的位置
手册里fopen和fclose放在一起讲,模式的细节要单独记。模式串里b可以出现在末尾,也可以夹在中间,效果一样,只影响换行转换和文件结束符识别。
| 模式 | 可读 | 可写 | 文件不存在时 | 已有内容 |
|---|---|---|---|---|
"r" | 是 | 否 | 返回NULL | 保留,从头读 |
"w" | 否 | 是 | 新建 | 截断为 0 |
"a" | 否 | 是 | 新建 | 保留,写总在末尾 |
"r+" | 是 | 是 | 返回NULL | 保留,可读写 |
"w+" | 是 | 是 | 新建 | 截断为 0 |
"a+" | 是 | 是 | 新建 | 保留,写总在末尾,读的位置由fseek决定 |
加b的意义在文本模式会做转换的平台上尤其明显:读的时候把\r\n折叠成\n,写的时候反向展开,还会把某个控制字符当成文件结束。处理二进制格式、算校验和、做内存映射文件,一律加b,省掉一整类「字节数对不上」的问题。
fopen失败返回NULL,不设置流的错误指示器,所以要靠errno判断具体原因。手册的示例里通常会紧跟一句perror,这不是装饰,是唯一的诊断入口。
3.3 fread/fwrite 按项计数,短读必须循环
这是手册第二章里最容易被读快的一条。fread的签名是size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream),返回值是「成功读到的完整项数」,不是字节数。size传 1 时两者恰好相等,很多人就这么记,然后在size大于 1 的结构体数组上翻车。
/* 按块读入整个文件:用项数判断,并把出错与读到末尾分开处理 */ #include <stdio.h> #include <stdlib.h> static unsigned char *slurp(const char *path, size_t *out_len) { FILE *fp = fopen(path, "rb"); /* 二进制模式,屏蔽换行转换 */ if (fp == NULL) return NULL; if (fseek(fp, 0, SEEK_END) != 0) { /* 只有可寻址的流才能这样测长度 */ fclose(fp); return NULL; } long size = ftell(fp); /* 失败返回 -1L,必须判 */ if (size < 0) { fclose(fp); return NULL; } rewind(fp); /* 回到开头,同时清除错误指示器 */ unsigned char *buf = malloc((size_t)size + 1); if (buf == NULL) { fclose(fp); return NULL; } size_t got = fread(buf, 1, (size_t)size, fp); /* size=1 时返回值即字节数 */ if (got != (size_t)size && ferror(fp)) { /* 短读且出错,才算真失败 */ free(buf); fclose(fp); return NULL; } buf[got] = '\0'; /* 多留一字节,方便当文本处理 */ *out_len = got; fclose(fp); return buf; } int main(int argc, char **argv) { if (argc < 2) return 1; size_t len = 0; unsigned char *data = slurp(argv[1], &len); if (data == NULL) { perror("slurp"); return 1; } printf("read %zu bytes, first = 0x%02X\n", len, len ? data[0] : 0); free(data); return 0; }这段代码里有三个参数值得单独说。第二个参数传 1,让返回值直接等于字节数,读字节流时最省心;如果按结构体读,第二个参数传sizeof(struct X),返回值就是结构体个数,此时「短读」意味着末尾有半个结构体,必须用feof判断是正常结束还是数据损坏。fseek(fp, 0, SEEK_END)只对普通文件这类可寻址流有效,管道、终端、套接字上会失败返回非 0,所以必须判返回值。rewind除了把位置归零,还会清掉流的错误指示器,这一点手册里有写但容易漏看。
短读的处理原则:fread返回值小于请求项数时,先看ferror,再看feof。前者是真错误,后者是读到了末尾。两者都不成立的情况在普通文件上很少见,但在网络流和非阻塞流上完全可能,此时要循环继续读。
3.4 fseek/ftell 的 long 边界与 fgetpos 替代
ftell返回long。在long是 32 位的平台上,超过 2GB 的文件直接越界,ftell返回-1L并把errno设成EINVAL或EOVERFLOW。手册里fseek的偏移参数同样是long,这是同一套限制。
替代方案是fgetpos/fsetpos,用fpos_t记录位置,宽度由实现决定,能覆盖大文件。
/* 用 fgetpos/fsetpos 记位置,绕开 long 的宽度限制 */ #include <stdio.h> int main(void) { FILE *fp = fopen("data.bin", "rb"); if (fp == NULL) { perror("fopen"); return 1; } fpos_t mark; if (fgetpos(fp, &mark) != 0) { perror("fgetpos"); fclose(fp); return 1; } int ch = fgetc(fp); /* 往前读一个字节,位置随之移动 */ if (ch != EOF) printf("first byte = 0x%02X\n", ch); if (fsetpos(fp, &mark) != 0) { /* 精确回到刚才记录的位置 */ perror("fsetpos"); fclose(fp); return 1; } printf("after fsetpos, next = 0x%02X\n", fgetc(fp)); fclose(fp); return 0; }fpos_t是不透明类型,不要对它做算术,也不要假设它能转成整数,只能整体存取。fgetpos和fsetpos成功返回 0,失败返回非 0,失败时不会修改传入的fpos_t。这套接口在需要回退重读的场景里比ftell+fseek更稳,代价是不能做「往前跳 N 个字节」这类相对定位。
还有一个细节:fseek到文件末尾之外再写,中间会填零,形成稀疏文件;fseek到开头之前是未定义行为。手册的示例通常只演示SEEK_SET,另外两个宏SEEK_CUR、SEEK_END在二进制模式下才是可靠的,文本模式下SEEK_CUR的偏移语义由实现定义。
3.5 setvbuf 改变缓冲策略的三个约束
setvbuf是手册第二章里少数需要「提前布置」的函数。它必须在fopen之后、任何读写操作之前调用,否则行为未定义。
/* 给大文件读流换一块更大的缓冲,减少系统调用次数 */ #include <stdio.h> int main(void) { FILE *fp = fopen("big.log", "rb"); if (fp == NULL) { perror("fopen"); return 1; } static char io_buf[1 << 16]; /* 静态存储期,流关闭前一直有效 */ if (setvbuf(fp, io_buf, _IOFBF, sizeof io_buf) != 0) { /* 设置失败不影响流本身,退回默认缓冲继续用 */ } int c; long count = 0; while ((c = fgetc(fp)) != EOF) { /* 小缓冲下这个循环会明显变慢 */ if (c == '\n') ++count; } printf("lines = %ld\n", count); fclose(fp); return 0; }三个参数的含义:第二个参数是缓冲区地址,传NULL表示让实现自己分配;第三个是模式,_IOFBF全缓冲、_IOLBF行缓冲、_IONBF无缓冲;第四个是字节数,传 0 时由实现决定。
三个约束要记住。缓冲区必须是静态数组或堆上分配,不能是已经退栈的局部数组,也不能是malloc之后又free掉的指针,因为流在关闭前一直会往里写。setvbuf只能在流刚打开时调用一次。_IONBF模式虽然看起来「实时」,但每次fputc都触发一次系统调用,日志量大时性能掉得厉害,我一般只在调试期临时打开。
注意:
fflush对输入流的行为在标准里是未定义的,某些实现把它定义成「丢弃已缓冲的未读输入」。跨平台代码里不要靠fflush(stdin)清输入缓冲。
4. 字符串与字符函数:拷贝、切分、转换三组的分界线
4.1 strcpy 与 strncpy 是两份不同的契约
手册第四章把这两个函数排在相邻位置,看名字像是「安全版」和「普通版」的关系,实际不是。strcpy的契约是:把源串连同结束符一起拷到目标,目标必须足够大,否则越界。strncpy的契约是:最多拷 n 个字节,如果源串长度小于 n,剩余部分用'\0'填满;如果源串长度大于等于 n,不补结束符。
/* strncpy 不保证补 '\0':要么先清零,要么手动补一个 */ #include <string.h> void copy_name(char *dst, size_t cap, const char *src) { if (cap == 0) return; strncpy(dst, src, cap - 1); /* 留一个字节给结束符 */ dst[cap - 1] = '\0'; /* 保险丝,防止 src 过长时无终止符 */ } /* 另一种写法:先整体清零,再拷,利用 strncpy 的补零行为 */ void copy_name_zero(char *dst, size_t cap, const char *src) { if (cap == 0) return; memset(dst, 0, cap); strncpy(dst, src, cap - 1); }第一种写法适用面更广,因为memset整块清零在大缓冲区上有额外开销,而strncpy在源串短的时候本来就会补零,重复清零是浪费。第二种写法看着干净,但如果src恰好比cap长,strncpy一个结束符都不写,最后还是靠memset保住的。
strncpy真正的用途是填定长字段,比如写二进制文件头里的名字域,那种场景需要「不足部分补零、超出部分截断」的语义。拿它当「安全版 strcpy」用,是手册里最常被误读的一处。
4.2 memcpy 与 memmove:重叠判断与手写实现
两个函数的签名一模一样,区别只在源和目标区间重叠时的行为:memcpy不保证正确,memmove保证正确。重叠判断的规则是「目标起始地址是否落在源区间内」。
/* 重叠区拷贝:按方向决定从前往后还是从后往前 */ #include <stddef.h> void *my_memmove(void *dst, const void *src, size_t n) { unsigned char *d = (unsigned char *)dst; const unsigned char *s = (const unsigned char *)src; if (d == s || n == 0) return dst; if (d < s) { /* 目标在源前面,正向拷贝不会覆盖尚未读取的源字节 */ for (size_t i = 0; i < n; ++i) d[i] = s[i]; } else { /* 目标在源后面,必须倒序,否则尾部先被写坏 */ for (size_t i = n; i > 0; --i) d[i - 1] = s[i - 1]; } return dst; }这段实现的每个参数都有讲究。dst和src先转成unsigned char *,因为标准规定字符类型可以别名访问任何对象,用别的指针类型做逐字节操作是未定义行为。n的类型是size_t,循环变量也用size_t,避免有符号无符号比较告警。倒序分支用i > 0而不是i >= 0,因为size_t是无符号的,减到 0 再减一就回绕成极大值,循环直接失控。
需要注意,指针比较d < s只有在两个指针指向同一个数组(或数组末尾后一位置)时才有定义。实际使用中,重叠拷贝本来就发生在同一块缓冲区内,所以这个前提成立;如果两个指针来自不同对象,编译器完全可以把这段比较优化掉。
实际工程里我一般直接用memmove,让库去判断方向,自己写的版本只在面试题和嵌入式无库环境里出现。
4.3 strtok 的静态状态与可重入替代
strtok是手册里用得最多也最容易出事的函数。它把分隔符位置就地改成'\0',并在内部用一个静态变量记录下次从哪里继续。这个静态状态带来两个后果:不能嵌套切分,多线程下会互相踩。
/* 用 strtok_r 做可重入切分,把状态放在调用方自己的变量里 */ #include <stdio.h> #include <string.h> int main(void) { char line[] = "GET /index.html HTTP/1.1"; /* 必须是可写数组 */ char *save = NULL; const char *sep = " "; for (char *tok = strtok_r(line, sep, &save); tok != NULL; tok = strtok_r(NULL, sep, &save)) { printf("[%s]\n", tok); } return 0; }三个参数的分工:第一个传待切分的缓冲区,后续传NULL;第二个是分隔符集合,其中每个字符都算分隔符,连续多个分隔符会被合并跳过;第三个save是调用方提供的状态指针,每次调用由函数更新。
line必须是可写的字符数组,不能是字符串字面量,因为strtok和strtok_r都会往原缓冲区里写'\0'。这一点手册的示例里写了,但示例往往直接用char *s = "a b c",照着抄就崩。
strtok_r是 POSIX 里的名字,ISO C 标准里没有。要在严格标准模式下用它,得打开对应的特性宏,或者自己维护一个游标用strcspn/strspn手搓。手册第四章同时列了strcspn和strspn,这两个函数配合起来可以完全替代strtok,而且不改原缓冲区。
4.4 atoi 的信息丢失与 strtol 的完整判定
atoi、atof、atol这三个函数在手册里挨着排,共同点是「出错时返回 0」。这意味着"0"和"abc"经过atoi之后拿到的是同一个值,程序无从分辨。要写健壮的转换,得用strtol系列。
/* strtol 用 endptr 和 errno 给出完整判定:非法字符、溢出、成功 */ #include <errno.h> #include <limits.h> #include <stdlib.h> /* 返回 0 成功,-1 无数字,-2 溢出,-3 尾部有残留字符 */ int parse_long(const char *s, long *out) { char *end = NULL; errno = 0; /* 成功的调用不会清零 errno,必须自己置 0 */ long v = strtol(s, &end, 10); /* 第三个参数是进制,传 0 表示自动识别前缀 */ if (end == s) return -1; /* 一个字符都没吃掉,不是数字 */ if (errno == ERANGE) return -2; if (*end != '\0') return -3; /* 允许尾部空白的话,这里要先跳过空白 */ *out = v; return 0; }strtol的第二个参数是「第一个未被转换的字符位置」,这是整个函数的信息核心。end == s说明开头就不是数字;*end != '\0'说明数字后面还跟着别的东西;errno == ERANGE说明结果超出了long的范围,此时返回值被钳在LONG_MAX或LONG_MIN。
第三个参数是进制,传 10 表示十进制,传 16 表示十六进制,传 0 表示按前缀自动判断(0x十六进制、0八进制)。传 0 这个用法在解析配置文件时很方便,但也容易把"010"读成 8,我一般显式写进制。
strtoul转无符号长整型,strtod转双精度。strtod的溢出判定同样靠errno,下溢时返回一个接近 0 的值并可能置ERANGE,具体行为要看实现。
4.5 ctype 系列:参数必须先转 unsigned char
手册第三章列了十几个is*函数,用法看起来极其简单:传一个字符,返回真或假。问题出在参数类型上。这些函数接受int,但标准规定这个int必须是unsigned char能表示的值,或者EOF。
/* is* 的参数必须落在 unsigned char 范围或等于 EOF,先转再传 */ #include <ctype.h> int count_digits(const char *s) { int n = 0; for (; *s != '\0'; ++s) { if (isdigit((unsigned char)*s)) ++n; /* 强制转换是关键一步 */ } return n; }如果char在目标平台上是带符号的,一个高位为 1 的字节(比如某些编码下的扩展字符)转成int之后是负数,直接传进isdigit就越界访问了内部查找表,行为未定义。表现可能是返回随机值,也可能在特定平台上崩掉。加一个(unsigned char)转换,成本是零,把这类玄学问题一次性消掉。
tolower和toupper同理,参数先转unsigned char,返回值再按需转回char。
5. 常见问题排查:五次从链接失败到越界的现场复盘
5.1 编译与链接期的三处硬伤
现象一:链接阶段报undefined reference to 'sqrt',源文件里明明有#include <math.h>。
原因:头文件只提供声明,sqrt的机器码在数学库里,默认不参与链接。很多人只在编译命令末尾补-lm却发现还是报错,那是因为-lm写在了源文件名前面。链接器按命令行从左到右扫描,遇到源文件时产生未解析符号,此时还没看到-lm,等扫描到 `
本文还有配套的精品资源,点击获取