整理自《C语言入门到精通》,偏重实用速查。C 的所有知识点最后都会归结到指针和内存,这是它与高级语言最本质的区别。
#include <stdio.h>
int main(void)
{
printf("Hello, C!\n");
return 0;
}编译流程(用 gcc):
gcc hello.c -o hello # 直接编译链接一步到位
./hello # 运行
# 分步走
gcc -E hello.c -o hello.i # 预处理
gcc -S hello.i -o hello.s # 编译(生成汇编)
gcc -c hello.s -o hello.o # 汇编(生成目标文件)
gcc hello.o -o hello # 链接几个基础常识:
| 类型 | 大小 | printf 格式 | 简介 |
| char | 1字节 | %c | 字符/小整数 |
| short | 2字节 | %hd | 短整型 |
| int | 通常是4字节 | %d | 默认整数 |
| long | 4或8字节 | %ld | 长整型 |
| long long | 8字节 | %lld | C99引入 |
| float | 4字节 | %f | 单精度浮点 |
| double | 8字节 | %lf | 双精度浮点 |
加上 unsigned 前缀后,范围从 0 到正数最大值(无符号数不会存在负数)。
unsigned int a = 100;
unsigned char b = 255;用 sizeof 看真实大小(结果单位是字节):
printf("%zu\n", sizeof(int)); // 4(大多数平台)
printf("%zu\n", sizeof(long)); // 可能是4或8int a = 42; // 十进制
int b = 052; // 八进制(以0开头)
int c = 0x2A; // 十六进制
long d = 100L; // long后缀
long long e = 100LL; // long long后缀
unsigned f = 100U; // unsigned后缀float f = 3.14f; // 不加f默认是double
double d = 2.71828;
double g = 6.022e23; // 科学计数法
// float 和 double 的 printf:
printf("%f\n", f); // float和double都用%f输出
printf("%lf\n", d); // scanf时double用%lfchar ch = 'A'; // 存的就是65(ASCII码值)
char ch2 = 65; // 等价,输出'A'
char ch3 = '\n'; // 转义字符:换行
char ch4 = '\t'; // 制表符
char ch5 = '\\'; // 反斜杠本身
// 大小写转换
char lower = upper + 32; // 大写→小写,利用ASCII码差// 隐式转换(自动提升):
// char/short → int → unsigned → long → float → double
int a = 10;
double b = a; // OK,自动提升
// 强制转换(截断):
double pi = 3.14159;
int n = (int)pi; // 3,小数部分丢弃
// 注意:有符号和无符号混用时,有符号会被转成无符号
int x = -1;
unsigned int y = 1;
if (x < y) // 结果是0(false),因为-1变成4294967295了int a, b, c; // C89必须在块开头声明所有变量
int a = 10, b = 20; // 声明时初始化
// C99后可以在任意位置声明
for (int i = 0; i < 10; i++) { ... }
// 常量(两种方式)
#define PI 3.14159 // 宏常量(预处理器,无类型)
const double PI = 3.14159; // const常量(有类型,占内存)#include <stdbool.h>
bool flag = true; // 本质是 _Bool
bool flag2 = false;
printf("%d\n", flag); // 输出1| 运算符 | 名称 | 示例 | 结果 | 说明 |
| + | 加 | 7 + 3 | 10 | 数字相加 |
| - | 减 | 7 - 3 | 4 | 数字相减 |
| * | 乘 | 7 * 3 | 21 | 数字相乘 |
| / | 除 | 7 / 2 | 3 | 整数除法结果截断为整数 |
| % | 取余 | 7 % 3 | 1 | 只能用于整数 |
7 / 2 // 3,整数除法直接截断小数
7 / 2.0 // 3.5,只要有一边是浮点就是浮点除法
7 % 3 // 1
-7 / 2 // -3(C99起向零截断,不是-4)
-7 % 3 // -1(C99起余数符号跟随被除数)
// 浮点取余要用 fmod
#include <math.h>
double r = fmod(7.5, 2.0); // 1.5注意:C 的 / 和 % 在负数时是"向零截断"(C99 起),和 Python 的"向下取整"不同,容易踩坑。整数除法结果还是整数,想得到小数至少让一边是浮点。
| 运算符 | 含义 | 示例 | 结果 |
| == | 等于 | 3 == 3 | 1(真) |
| != | 不等于 | 3 != 4 | 1(真) |
| > | 大于 | 5 > 3 | 1(真) |
| < | 小于 | 5 < 3 | 0(假) |
| >= | 大于等于 | 5 >= 5 | 1(真) |
| <= | 小于等于 | 5 <= 4 | 0(假) |
int a = 8;
a > 3 && a < 10 // 1,真
// 陷阱:连续比较不是数学里的链式比较
if (1 < 2 < 3) // (1<2)=1,再算 1<3=1,碰巧为真但逻辑是错的
if (1 < 2 && 2 < 3) // 正确写法
// 浮点数比较
if (0.1 + 0.2 == 0.3) // 可能是假!精度问题,推荐用差值判断
if (fabs(a - b) < 1e-9) // 正确姿势| 运算符 | 含义 | 示例 | 结果 |
| && | 逻辑与(短路) | 1 && 0 | 0 |
| || | 逻辑或(短路) | 1 || 0 | 1 |
| ! | 逻辑非 | !0 | 1 |
短路规则:
短路最常见的用法是"保护"后面的操作不越界:
// 保护:避免除以 0
if (b != 0 && a / b > 2) { ... }
// 保护:先判空指针再解引用
if (p != NULL && p->age > 18) { ... }
!0 // 1
!5 // 0
!"" // 0(字符串指针非空即为真)短路保证右边不执行,这个特性也叫"短路求值",是写出安全代码的基础。
| 运算符 | 等价写法 | 示例 |
| = | 直接赋值 | x = 10 |
| += | x = x + n | x += 5 |
| -= | x = x - n | x -= 5 |
| *= | x = x * n | x *= 5 |
| /= | x = x / n | x /= 5 |
| %= | x = x % n | x %= 5 |
| <<= | x = x << n | x <<= 1 |
| >>= | x = x >> n | x >>= 1 |
| &= | x = x & n | x &= 3 |
| |= | x = x | n | x |= 3 |
| ^= | x = x ^ n | x ^= 3 |
两个关键点:
int a, b, c;
a = b = c = 10; // 从右往左:c=10, b=10, a=10
if (x = 5) { } // 陷阱!等价于 x=5; if(5),永远为真
if (x == 5) { } // 正确写法
if (5 == x) { } // 防御式写法,少写一个=会编译报错int a = 5;
int b = a++; // b=5, a=6(先用后加)
int c = ++a; // a=7, c=7(先加后用)
a--; --a; // 同理两个警告:
i = i++; // 未定义行为!禁止
printf("%d %d\n", i, i++); // 未定义行为!禁止| 运算符 | 名称 | 示例(二进制) | 结果 |
| & | 按位与 | 5 & 3(101 & 011) | 1 |
| | | 按位或 | 5 | 3(101 | 011) | 7 |
| ^ | 按位异或 | 5 ^ 3(101 ^ 011) | 6 |
| ~ | 按位取反 | ~5 | -6(~x = -x - 1) |
| << | 左移 | 5 << 1 | 10(相当于乘 2) |
| >> | 右移 | 5 >> 1 | 2(逻辑/算术右移取决于类型) |
5 & 3 // 1 (101 & 011 = 001)
5 | 3 // 7 (101 | 011 = 111)
5 ^ 3 // 6 (101 ^ 011 = 110)
~5 // -6 (~x = -x - 1)
5 << 1 // 10 (左移1位 = 乘2)
5 >> 1 // 2 (右移1位 = 整除2,正数)
// 常见用途:开关标记(掩码)
int flags = 0;
flags |= (1 << 3); // 置第3位为1
flags &= ~(1 << 3); // 清第3位为0
flags ^= (1 << 3); // 翻转第3位
(flags >> 3) & 1 // 读第3位的值
// 判断奇偶(比 % 快)
if (n & 1) { ... } // 奇数右移细节:无符号数是逻辑右移(高位补 0);有符号负数通常是算术右移(高位补符号位)。位运算的优先级比比较运算符低,见 3.10。
max = a > b ? a : b; // 条件 ? 真值 : 假值
// 可以嵌套(不推荐,可读性差)
max = a > b ? (a > c ? a : c) : (b > c ? b : c);
// 三元是表达式,可以赋值、传参
printf("%s\n", score >= 60 ? "及格" : "不及格");// 从左到右依次求值,整个表达式的值是最右边那个
int a = (1, 2, 3); // a = 3
for (int i = 0, j = 10; i < j; i++, j--) { ... }注意区分:函数参数里的逗号不是逗号运算符,f(a, b) 只是分隔参数。
int x = (a, b, c); // 逗号运算符:x = c
f(a, b, c); // 三个参数,不是逗号运算符| 运算符 | 名称 | 示例 | 说明 |
| sizeof | 大小运算符 | sizeof(int) | 返回类型/变量的字节数(size_t) |
| & | 取地址 | &a | 获得变量的内存地址 |
| * | 解引用 | *p | 访问指针指向的内容 |
| [] | 下标 | arr[i] | 等价于 *(arr + i) |
| . | 成员访问 | s.age | 访问结构体成员 |
| -> | 箭头访问 | p->age | 通过指针访问结构体成员 |
| () | 函数调用/强转 | add(1,2)、(int)x | 函数调用、类型转换 |
| , | 逗号 | (a, b) | 依次求值,结果是最后一项 |
| 优先级 | 运算符 | 说明 |
| 1 | ()、[]、->、. | 括号、下标、成员访问 |
| 2 | !、~、++、--、+x、-x、p、&x、**(类型)* | 一元运算符 |
| 3 | *、/、% | 乘、除、取余 |
| 4 | +、- | 加、减 |
| 5 | <<**、**>> | 移位 |
| 6 | <、<=**、**>、>= | 关系比较 |
| 7 | ==、!= | 相等比较 |
| 8 | & | 按位与 |
| 9 | ^ | 按位异或 |
| 10 | | | 按位或 |
| 11 | && | 逻辑与 |
| 12 | || | 逻辑或 |
| 13 | ?: | 三元条件 |
| 14 | =、+=、-=、=、/=、%=、&=、|=**、*^=、<<=、**>>= | 赋值(右结合) |
| 15 | , | 逗号 |
不确定就加括号,别赌记忆力。容易踩坑的几个:
if (flags & MASK == 0) { ... } // 错!==优先级比&高→等价于flags & (MASK==0)
if ((flags & MASK) == 0) { ... } // 对
*p++ // 等价 *(p++),不是(*p)++
*p->field // 等价 *(p->field)if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
// 条件表达式必须用()括起来
// 花括号永远不要省,哪怕只有一行switch (day) {
case 1:
printf("Monday\n");
break; // 忘了break会穿透到下一个case!
case 2:
case 3: // 多个case共享代码是合法的
printf("Tuesday or Wednesday\n");
break;
default:
printf("Unknown\n");
break;
}
// switch 只能用于整数类型(char/short/int/long)和枚举
// C语言不支持在case中判断字符串// for(知道循环次数)
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
// while(条件满足就一直循环)
while (n > 0) {
n /= 10;
count++;
}
// do-while(至少执行一次,先斩后奏)
do {
printf("至少执行一次\n");
} while (0);
// 嵌套循环 + 带标签的break(用goto实现)
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) goto end_loop;
}
}
end_loop:break; // 跳出当前循环或switch
continue; // 跳过本次循环剩余语句,进入下一次迭代
// goto 不是洪水猛兽,清理多个资源时它是最清晰的方案:
if (open_resource1() < 0) goto out1;
if (open_resource2() < 0) goto out2;
if (open_resource3() < 0) goto out3;
// ... 正常逻辑 ...
out3: close_resource3();
out2: close_resource2();
out1: close_resource1();// 声明(告诉编译器函数存在,通常在.h里)
int add(int a, int b);
// 定义(函数体)
int add(int a, int b)
{
return a + b;
}
// C要求函数必须先声明后使用。如果没声明,编译器会默认返回intC 只有值传递:
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int x = 3, y = 5;
swap(&x, &y); // x=5, y=3int global = 10; // 全局变量:整个程序,程序结束才释放
void func(void)
{
static int count = 0; // 静态局部变量:只初始化一次,函数结束不释放
int local = 5; // 自动局部变量(栈):函数返回就释放
count++;
printf("count = %d\n", count);
}static 修饰全局变量/函数:限制为文件内部可见(其他 .c 文件用不了)。
#include <stdarg.h>
double average(int count, ...) // 至少有一个固定参数
{
va_list args;
va_start(args, count);
double sum = 0;
for (int i = 0; i < count; i++) {
sum += va_arg(args, double);
}
va_end(args); // 必须配对调用
return sum / count;
}
double avg = average(5, 1.0, 2.0, 3.0, 4.0, 5.0);long factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// C的递归深度受限于栈大小,太深就爆栈int arr[5]; // 声明,元素未初始化(垃圾值)
int arr2[5] = {1, 2, 3}; // 初始化为1,2,3,0,0
int arr3[] = {1, 2, 3, 4}; // 自动推断大小
// C不检查数组越界,下面能编译但不该写:
arr[100] = 42; // 写到别人家的内存了
// 数组名在大多数场合退化为指向首元素的指针
int size = sizeof(arr) / sizeof(arr[0]); // 数组大小(只在定义处有效)int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
matrix[0][2]; // 3
// 作为函数参数时,第二维必须指定
void print_matrix(int rows, int cols, int m[][4]) { ... } // 第二维写4int n = 10;
int arr[n]; // 数组大小由运行时变量决定
// 但VLA不能整体初始化,不能声明时赋初值
// VLA不是所有编译器都支持(MSVC不支持),用它要小心C 没有独立的字符串类型,字符串就是 char 数组,以 \0 结尾。
char s1[] = "hello"; // 6个字节:h e l l o \0
char s2[10] = "hello"; // 前6个是hello\0,后4个是\0
char *s3 = "hello"; // 指向常量区的字符串,不能修改!
// s3[0] = 'H'; // 运行时报错或未定义行为s1 和 s3 的本质区别:
| char s[] = "..." | *char s = "..." | |
| 内容 | 在栈上,可修改 | 在只读数据段,不可写 |
| sizeof 结果 | 整个数组大小 | 指针大小(4或8) |
#include <string.h>
strlen(s); // 长度(不含\0)
strcpy(dst, src); // 复制(危险!不检查边界)
strncpy(dst, src, sizeof(dst)-1); // 带边界复制,但记得手动补\0
strcat(dst, src); // 拼接(危险!)
strncat(dst, src, sizeof(dst) - strlen(dst) - 1); // 安全拼接
strcmp(a, b); // 比较:0=相等,负数=a<b,正数=a>b
strchr(s, 'a'); // 找字符,返回指针或NULL
strstr(haystack, needle); // 找子串
memcpy(dst, src, n); // 复制n字节(不能处理重叠)
memmove(dst, src, n); // 复制n字节(能处理重叠,更安全)
memset(ptr, 0, n); // 填充n字节为某个值安全字符串法则:
// 带 n 的版本永远手动补 \0
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';C 的核心,没有之一。指针就是存地址的变量。
int a = 42;
int *p = &a; // p存的是a的地址
*p = 100; // 通过指针修改a的值
printf("%d\n", a); // 100
// 解引用:*p 拿到p指向的值
// 取地址:&a 拿到a的地址const int *p; // p能变,*p不能变(指向的内容是常量)
int * const p; // p不能变,*p能变(指针本身是常量)
const int * const p; // 都不能变
// 记法:const在*左边 → 内容只读;const在*右边 → 指针只读int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 int *p = &arr[0];
printf("%d\n", *p); // 1
p++; // p指向arr[1](地址+sizeof(int))
printf("%d\n", *p); // 2
// arr[i] 完全等价于 *(arr + i)
arr[2] 和 *(arr + 2) 没有任何区别数组下标操作本质上就是指针运算的语法糖。写 a[i] 跟写 *(a + i) 一模一样,反过来 i[a] 也能工作(虽然没人这么写)。
int a = 42;
int *p = &a;
int **pp = &p; // 指向指针的指针
printf("%d\n", **pp); // 42(两次解引用)二维数组名不过是一个行指针:int (*ptr)[3]。
// 定义
int add(int a, int b) { return a + b; }
// 声明函数指针
int (*func_ptr)(int, int) = add;
// 调用
int result = func_ptr(3, 5); // 8
// 函数指针的实用场景:回调函数
void for_each(int *arr, int len, void (*callback)(int))
{
for (int i = 0; i < len; i++) {
callback(arr[i]);
}
}函数指针 typedef 更可读:
typedef int (*Operation)(int, int);
Operation op = add;int arr[5]; // 整型数组
int *pArr[5]; // 指针数组:5个元素的数组,每个元素是int*
int (*p)[5]; // 数组指针:指向长度为5的int数组的指针
int* (*f[5])(int); // 函数指针数组:5个函数指针,参数int返回int*// void* 是万能指针,可以指向任何类型
void *vp = &a;
// 但不能解引用,必须强转回来
int *ip = (int *)vp;
printf("%d\n", *ip);
// malloc返回的就是void*,正是此意struct Student {
char name[20];
int age;
double score;
}; // 注意分号不能少
// 使用
struct Student s1 = {"Alice", 20, 95.5};
struct Student s2;
s2.age = 21;
// typedef 简化
typedef struct {
char name[20];
int age;
} Student;
Student s3 = {"Bob", 22};
// 通过指针访问(两种等价方式)
Student *p = &s3;
p->age = 23; // 推荐,语义清晰
(*p).age = 23; // 等价但不推荐struct Demo {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
// sizeof(struct Demo) = 12,不是1+4+1=6
// 因为存在对齐填充:a后补3字节,c后补3字节控制对齐(非标准但主流编译器都支持):
#pragma pack(1) // 按1字节对齐
struct Packed {
char a;
int b;
char c;
};
#pragma pack() // 恢复默认
// sizeof = 6所有成员共享同一块内存,存一个覆盖另一个。
union Data {
int i;
float f;
char str[20];
};
union Data d;
d.i = 10;
d.f = 3.14; // 覆盖了i的值
// 典型用途1:判断大小端
union { int i; char c; } u;
u.i = 1;
if (u.c == 1) printf("小端\n");
// 典型用途2:多种数据类型复用同一内存enum Color { RED, GREEN, BLUE }; // RED=0, GREEN=1, BLUE=2
enum Weekday { MON = 1, TUE, WED, THU }; // MON=1, 依次递增
enum Color c = RED;
// enum 本质是int的别名,可以和int混用(不推荐)| 栈 | 堆 | |
| 分配方式 | 自动(函数调用) | 手动(malloc/free) |
| 大小 | 有限(默认几MB) | 取决于系统内存 |
| 生命周期 | 函数返回即释放 | 手动 free 才释放 |
| 速度 | 快 | 慢 |
#include <stdlib.h>
// 分配
int *arr = (int *)malloc(10 * sizeof(int));
if (arr == NULL) {
// 分配失败处理
exit(1);
}
// 初始化为0(calloc 会清零)
int *arr2 = (int *)calloc(10, sizeof(int));
// 重新调整大小
int *tmp = (int *)realloc(arr, 20 * sizeof(int));
if (tmp != NULL) arr = tmp; // realloc可能返回新地址
// 释放
free(arr);
arr = NULL; // 防止野指针// 好习惯模板
int *ptr = (int *)malloc(100 * sizeof(int));
if (ptr == NULL) {
perror("malloc");
exit(1);
}
// ... 使用 ...
free(ptr);
ptr = NULL;// 1. 返回局部变量地址
char *get_str(void) {
char buf[] = "hello";
return buf; // 返回后buf被销毁,悬空指针!
}
// 2. use-after-free
free(p);
printf("%d\n", *p); // 未定义
// 3. 内存越界
int *arr = malloc(10 * sizeof(int));
arr[10] = 42; // 越界!
// 4. 忘记free导致泄漏
void leak(void) {
char *buf = malloc(1024);
// 使用buf,但忘了free
} // 每次调用泄1024字节预处理器在编译之前执行纯文本替换,不是 C 语言的一部分,但没有它几乎写不了代码。
#include <stdio.h> // 尖括号:从系统头文件目录找
#include "myheader.h" // 双引号:先从当前目录找,再找系统目录// 对象式宏
#define PI 3.14159
#define MAX_SIZE 1024
// 函数式宏(参数和整体都必须加括号!)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
// 为什么必须加括号?
#define BAD_SQUARE(x) x * x
int result = BAD_SQUARE(3 + 2); // 展开为 3+2*3+2 = 11 !!!
// 副作用陷阱
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 3;
int m = BAD_MAX(x++, y++); // x和y可能被自增两次宏的替代方案 —— 内联函数(C99):
inline int max(int a, int b) {
return a > b ? a : b;
}
// 有类型检查,没有宏的副作用,性能一样好// 调试开关
#ifdef DEBUG
printf("x = %d\n", x);
#endif
// 平台适配
#if defined(__linux__)
// Linux代码
#elif defined(_WIN32)
// Windows代码
#else
#error "Unsupported platform"
#endif
// 防止头文件重复包含
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... 头文件内容 ...
#endif#define STR(x) #x // 字符串化
#define CAT(a, b) a ## b // 拼接
printf("%s\n", STR(hello)); // "hello"
int CAT(val, 123) = 10; // int val123 = 10;__FILE__ // 当前文件名
__LINE__ // 当前行号
__func__ // 当前函数名(C99)
__DATE__ // 编译日期
__TIME__ // 编译时间
// 实用日志宏
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)#include <stdio.h>
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("fopen"); // 打印系统错误信息
return 1;
}
// ... 读写操作 ...
fclose(fp);| 模式 | 含义 |
| "r" | 只读,文件必须存在 |
| "w" | 只写,存在则清空,不存在则创建 |
| "a" | 追加写,文件指针在末尾 |
| "r+" | 读写,文件必须存在 |
| "w+" | 读写,存在则清空 |
| "a+" | 读和追加写 |
| "rb"/"wb" | 二进制模式(Windows上b才有意义) |
// 逐字符
int ch;
while ((ch = fgetc(fp)) != EOF) { // ch必须是int!
fputc(ch, stdout);
}
// 逐行
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
// 去掉末尾换行符
line[strcspn(line, "\n")] = '\0';
printf("读取: [%s]\n", line);
}
// 格式化
fprintf(fp, "%s %d %.2f\n", "Alice", 25, 1.65); // 写入
fscanf(fp, "%s %d %f", name, &age, &height); // 读取// fread / fwrite
int arr[] = {1, 2, 3, 4, 5};
FILE *fp = fopen("data.bin", "wb");
fwrite(arr, sizeof(int), 5, fp); // 数组名, 每元素大小, 元素个数, 文件指针
fclose(fp);
int loaded[5];
FILE *fp2 = fopen("data.bin", "rb");
size_t n = fread(loaded, sizeof(int), 5, fp2); // 返回实际读取的元素个数
if (n != 5) printf("读取不完整\n");
fclose(fp2);警告:直接二进制读写结构体跨平台不可靠(对齐、大小端、指针成员都会出问题)。
fseek(fp, 0, SEEK_SET); // 移到开头
fseek(fp, 0, SEEK_END); // 移到末尾(配合ftell获取文件大小)
fseek(fp, -10, SEEK_CUR); // 从当前位置往前移10字节
long pos = ftell(fp); // 获取当前位置
rewind(fp); // 等价于fseek(fp, 0, SEEK_SET)fprintf(stdin, ...); // 标准输入
fprintf(stdout, ...); // 标准输出(行缓冲)
fprintf(stderr, ...); // 标准错误(不缓冲,即时输出)| 头文件 | 主要功能 |
| <stdio.h> | 输入输出:printf、scanf、fopen、fclose、fread、fwrite、fgets |
| <stdlib.h> | 通用工具:malloc、free、calloc、realloc、atoi、rand、qsort、bsearch、exit |
| <string.h> | 字符串:strlen、strcpy、strcmp、strcat、strchr、memcpy、memset |
| <ctype.h> | 字符判断:isalpha、isdigit、isspace、isupper、tolower、toupper |
| <math.h> | 数学:sqrt、pow、sin、cos、log、ceil、floor、fabs(编译加 -lm) |
| <time.h> | 时间:time、localtime、strftime、clock |
| <stdarg.h> | 可变参数:va_list、va_start、va_arg、va_end |
| <assert.h> | 断言:assert(debug用,NDEBUG宏可关闭) |
| <stdint.h> | 固定宽度整数:int32_t、uint64_t(C99) |
| <stdbool.h> | 布尔类型:bool、true、false(C99) |
| <errno.h> | 错误码:errno、perror、strerror |
// qsort 泛型排序(需要比较回调)
void qsort(void *base, size_t n, size_t size,
int (*cmp)(const void *, const void *));
// 使用示例
int cmp_int(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int arr[] = {5, 2, 9, 1};
qsort(arr, 4, sizeof(int), cmp_int);
// bsearch 二分搜索(需排序后)
void *bsearch(const void *key, const void *base,
size_t n, size_t size,
int (*cmp)(const void *, const void *));#include <assert.h>
// 调试时检查"绝不应该出现"的情况
void divide(int a, int b) {
assert(b != 0); // b为0时程序直接终止并报错
printf("%d\n", a / b);
}
// 发布版本编译加 -DNDEBUG,所有assert自动移除到这里,C语言基础语法概要就介绍完了。
C 的难度不在于语法多复杂,而在于它把计算机底层的所有细节都摊在你面前。指针、内存、位运算 —— 这些才是 C 真正的 "语法"。用 Valgrind、AddressSanitizer 这些工具辅助排查内存问题,比肉眼 debug 靠谱得多。