C 语言本身非常小:类型、运算符、控制流、函数、指针。它不能打印,不能读文件,不能比较字符串,也不能分配内存。这些全都由标准库 提供:一组每个符合标准的实现都会附带的头文件。
本页是一张地图。每一节说明一个头文件的用途,给出一个简短的可运行示例,并在有详细页面时附上链接。
stdio.h:输入和输出
几乎每个程序第一个包含的头文件。它包含 printf 和 scanf、FILE 类型和文件函数,以及更安全的按行输入函数。
#include <stdio.h>
int main(void) {
printf("Formatted: %d %s %.2f %c\n", 42, "text", 3.14159, 'X');
/* fputs writes without interpreting format specifiers */
fputs("Plain output, no formatting\n", stdout);
/* stderr is a separate stream for diagnostics */
fprintf(stderr, "This goes to standard error\n");
/* putchar writes a single character */
for (char c = 'a'; c <= 'e'; c++) {
putchar(c);
}
putchar('\n');
return 0;
}Master C the hands-on way
Interactive lessons, real projects, and instant feedback.
START NOW
核心函数:输出用 printf、fprintf、sprintf、snprintf;输入用 scanf、fscanf、sscanf、fgets、getchar;文件操作用 fopen、fclose、fread、fwrite、fseek。更深入的内容见 printf 、scanf 和文件操作 。
有一条规则值得牢记:永远不要用 gets()。它无法知道缓冲区的大小,已在 C11 中从标准里移除。替代写法是 fgets(buf, sizeof buf, stdin)。
stdlib.h:通用工具
什么都有:内存、转换、随机数、进程控制、排序。
#include <stdio.h>
#include <stdlib.h>
int compare_ints(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int main(void) {
/* string to number */
printf("atoi(\"123\") = %d\n", atoi("123"));
printf("strtol(\"ff\", 16) = %ld\n", strtol("ff", NULL, 16));
printf("atof(\"3.5\") = %.2f\n", atof("3.5"));
/* dynamic memory */
int *nums = malloc(5 * sizeof *nums);
if (nums == NULL) {
return 1;
}
int seed_values[] = {50, 10, 40, 20, 30};
for (int i = 0; i < 5; i++) {
nums[i] = seed_values[i];
}
/* qsort sorts anything given a comparison function */
qsort(nums, 5, sizeof *nums, compare_ints);
for (int i = 0; i < 5; i++) {
printf("%d ", nums[i]);
}
printf("\n");
free(nums);
return 0;
}
重点:malloc、calloc、realloc、free(见动态内存 );用于转换的 atoi、atof、strtol、strtod,优先用 strto* 系列,它能报告失败,而 atoi 失败时只会悄悄返回 0;rand 和 srand(见随机数 );exit、abort、getenv、system;以及 qsort/bsearch。
string.h:字符串和内存块
C 字符串是以 '\0' 结尾的 char 数组,对它们的每个操作都是一次函数调用。
#include <stdio.h>
#include <string.h>
int main(void) {
char dest[64] = "Hello";
printf("strlen : %zu\n", strlen(dest));
strcat(dest, ", world");
printf("strcat : %s\n", dest);
printf("strcmp : %d\n", strcmp("apple", "banana"));
printf("strchr : %s\n", strchr(dest, 'w'));
printf("strstr : %s\n", strstr(dest, "world"));
char copy[64];
strcpy(copy, dest);
printf("strcpy : %s\n", copy);
/* the mem* functions work on raw bytes, not just text */
char block[10];
memset(block, '-', 9);
block[9] = '\0';
printf("memset : %s\n", block);
return 0;
}
有两个习惯能避免这个头文件变成缓冲区溢出的源头:始终清楚目标缓冲区的大小;记住 strcmp 在相等时返回 0,也就是说 if (strcmp(a, b)) 在两个字符串不同时才为真。字符串函数 详细介绍了更安全的 strncpy/snprintf 写法。
math.h:数学
sqrt、pow、fabs、floor、ceil、round、fmod,三角函数和对数函数系列,以及 INFINITY 和 NAN。
#include <stdio.h>
#include <math.h>
int main(void) {
printf("sqrt(2) = %.6f\n", sqrt(2.0));
printf("pow(2, 10) = %.0f\n", pow(2.0, 10.0));
printf("ceil(4.1) = %.0f\n", ceil(4.1));
printf("fabs(-9.5) = %.1f\n", fabs(-9.5));
return 0;
}
这是唯一可能需要链接选项的头文件:在 Linux 上,要在编译命令末尾加 -lm,否则链接器会报“undefined reference to sqrt”。完整介绍见数学函数 。
time.h:时钟和日期
#include <stdio.h>
#include <time.h>
int main(void) {
time_t now = time(NULL);
printf("Seconds since 1970: %ld\n", (long) now);
printf("Local time: %s", ctime(&now));
struct tm *lt = localtime(&now);
printf("Year: %d, month: %d, day: %d\n",
lt->tm_year + 1900, lt->tm_mon + 1, lt->tm_mday);
char buf[64];
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", lt);
printf("Formatted: %s\n", buf);
/* clock() measures processor time used by this program */
clock_t start = clock();
volatile long sum = 0;
for (long i = 0; i < 3000000L; i++) {
sum += i;
}
double secs = (double)(clock() - start) / CLOCKS_PER_SEC;
printf("Loop took %.4f CPU seconds\n", secs);
return 0;
}
struct tm 中的两个偏移量谁都会踩坑:tm_year 从 1900 开始计数,tm_mon 从 0 开始。time(NULL) 也是 srand 最常用的种子。
ctype.h:字符分类
很小,但比看上去有用。每个函数接收一个字符,回答一个是或否的问题,或者转换大小写。
#include <stdio.h>
#include <ctype.h>
int main(void) {
const char *text = "Coddy C17, launched!";
int letters = 0, digits = 0, spaces = 0, punct = 0;
for (const char *p = text; *p != '\0'; p++) {
unsigned char c = (unsigned char) *p;
if (isalpha(c)) letters++;
else if (isdigit(c)) digits++;
else if (isspace(c)) spaces++;
else if (ispunct(c)) punct++;
}
printf("letters=%d digits=%d spaces=%d punctuation=%d\n",
letters, digits, spaces, punct);
printf("Upper: ");
for (const char *p = text; *p != '\0'; p++) {
putchar(toupper((unsigned char) *p));
}
printf("\n");
return 0;
}
完整列表:isalpha、isdigit、isalnum、isspace、isupper、islower、ispunct、isxdigit、isprint,还有 toupper 和 tolower。
注意那个 (unsigned char) 转换。这些函数只对能用 unsigned char 表示的值和 EOF 有定义;在大多数系统上普通 char 是有符号的,所以大于 127 的字节会变成负数,调用就成了未定义行为。这个转换没有任何开销,却能消除这一整类问题。
另外,优先用这些函数,而不是自己写判断。c >= '0' && c <= '9' 碰巧能用,但 isdigit(c) 表达的意图更清楚,而且在哪里都正确。
limits.h 和 float.h:类型的边界
在这台机器上 int 最大能有多大?这两个头文件给出答案,而正因为答案因机器而异,可移植的代码才要去查询而不是假设。
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("char : %d to %d (%zu byte)\n", CHAR_MIN, CHAR_MAX, sizeof(char));
printf("int : %d to %d (%zu bytes)\n", INT_MIN, INT_MAX, sizeof(int));
printf("long : %ld to %ld (%zu bytes)\n", LONG_MIN, LONG_MAX, sizeof(long));
printf("uint : 0 to %u\n", UINT_MAX);
printf("float : %g to %g (%zu bytes)\n", FLT_MIN, FLT_MAX, sizeof(float));
printf("double: %g to %g (%zu bytes)\n", DBL_MIN, DBL_MAX, sizeof(double));
printf("double has %d decimal digits of precision\n", DBL_DIG);
return 0;
}
INT_MAX 让你能在溢出发生之前就检查它。这一点很重要,因为有符号整数溢出是未定义行为 ,而不是能事后检测的回绕。
stdbool.h 和 stdint.h:更好的类型
C99 之前,C 没有布尔类型。<stdbool.h> 提供了一个:
#include <stdio.h>
#include <stdbool.h>
bool is_even(int n) {
return n % 2 == 0;
}
int main(void) {
bool ready = true;
bool done = false;
printf("ready=%d done=%d\n", ready, done);
printf("is_even(10) = %s\n", is_even(10) ? "true" : "false");
return 0;
}
bool、true 和 false 是 _Bool、1 和 0 的宏,但它们让意图一目了然。(在 C23 中它们成了关键字,不再需要这个头文件。)
<stdint.h> 提供宽度有保证的类型。在文件格式、网络协议和嵌入式开发中,这是必不可少的,因为“int 大概是 32 位”远远不够:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
int8_t small = -128;
uint16_t port = 8080;
int32_t count = 2000000000;
uint64_t big = 18000000000000000000ULL;
printf("int8_t : %" PRId8 "\n", small);
printf("uint16_t: %" PRIu16 "\n", port);
printf("int32_t : %" PRId32 "\n", count);
printf("uint64_t: %" PRIu64 "\n", big);
printf("sizes: %zu %zu %zu %zu\n",
sizeof small, sizeof port, sizeof count, sizeof big);
return 0;
}
<inttypes.h> 中 PRId32 这类宏会展开成当前平台上对应定宽类型的正确 printf 格式说明符。这是打印它们的可移植写法,因为 %d 对 int32_t 在某些系统上是对的,在另一些系统上就是错的。
assert.h:检查你的假设
assert(expr) 在表达式为真时什么也不做,为假时打印一条消息并终止程序。
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); /* a contract: callers must not pass 0 */
return a / b;
}
int main(void) {
printf("20 / 4 = %d\n", divide(20, 4));
printf("20 / 5 = %d\n", divide(20, 5));
/* divide(20, 0) here would abort with:
Assertion `b != 0' failed. */
return 0;
}
用 -DNDEBUG 编译会去掉所有断言,所以在发布版本中它们没有任何开销。这也是要警惕的地方:永远不要在 assert 里放有副作用的代码 。assert(read_next() > 0) 在发布版本中会完全不再读取,由此产生的 bug 只会在生产环境中出现。
断言是用来抓你自己的错误的:如果代码正确,这些条件就不可能发生。用户输入错误不属于断言的范畴,应该用 if 加一条错误消息来处理。
其余头文件,简要介绍
<stddef.h>:size_t、ptrdiff_t、NULL、offsetof
<stdarg.h>:可变参数函数,printf 背后的机制
<errno.h>:errno,以及用于报告调用失败原因的 perror 和 strerror
<signal.h>:处理 Ctrl-C 等信号
<setjmp.h>:非局部跳转,很少是正确的选择
<locale.h>、<wchar.h>、<wctype.h>:区域设置和宽字符
<stdnoreturn.h>、<stdalign.h>、<threads.h>、<stdatomic.h>:C11 新增的头文件
两个习惯能让标准库用起来更顺手。先读手册页,不要去猜函数签名:在 macOS 和 Linux 上,man 3 strtol 既权威又快捷。另外要检查返回值:malloc、fopen 和 strtol 都会报告失败,忽略它们的程序会在很久以后才出错,而且错得莫名其妙。