C 语言语法速查表 - C 语言常用语法与指针编程大全
这套速查给用 C 写嵌入式、操作系统或高性能代码的开发者,这里不像托管语言,内存和指针生命周期都得自己盯。内容覆盖数据类型与常量、控制流、最容易埋坑的指针与数组关系、函数、手动 malloc/free、文件 I/O 和预处理器。条目按你在写哪一类构造块来分,并标记出容易写错的地方。读完应能分清何时用指针、何时用数组,把 malloc/free 管好不漏不悬垂,并用 #ifdef 把编译期逻辑写进代码。
典型使用场景
面向写系统层、嵌入式或性能敏感代码的 C 程序员。本表把语言最容易出事的几块集中钉死:指针与数组的等价关系(数组名在多数表达式中退化为首地址指针,但 sizeof 仍是整数组大小)、动态内存的四个函数(malloc/calloc/realloc/free)与不检查返回值的风险、结构体与指针互访(p.x 与 pp->x)、以及预处理器里的宏与头文件保护。工程上真正每天用的是这几组组合:malloc 后判空、free 后置 NULL、memcpy 与可重叠时的 memmove 选择、宏参数要加括号、带副作用的宏参数要小心,以及 #pragma once 之外的 #ifndef 双保险。文件读写用 fopen/fscanf/fprintf 配 fclose,调试段错误时按排障路径走。
数据类型与变量 8
int x = 10;float pi = 3.14f;double e = 2.718;char c = 'A';char str[] = "Hello";const int MAX = 100;enum {RED, GREEN, BLUE};typedef int Integer;控制流 8
if (x > 0) { } else if (x < 0) { } else { }switch (x) { case 1: break; default: break; }for (int i = 0; i < n; i++) { }while (condition) { }do { } while (condition);break;continue;goto label;指针与数组 8
int *p;int x = 10; int *p = &x;*p = 20;int arr[5] = {1, 2, 3, 4, 5};int *p = arr;*(p + i)int **pp;void *vp;函数 6
int add(int a, int b) { return a + b; }void swap(int *a, int *b);int (*fp)(int, int) = add;static int count = 0;extern int globalVar;inline int square(int x) { return x * x; }内存管理 7
int *p = (int *)malloc(sizeof(int) * 10);int *p = (int *)calloc(10, sizeof(int));p = (int *)realloc(p, sizeof(int) * 20);free(p);memset(p, 0, size);memcpy(dest, src, size);memmove(dest, src, size);结构体与联合体 6
struct Point { int x; int y; };struct Point p = {1, 2};p.x = 10;struct Point *pp = &p; pp->x = 10;union Data { int i; float f; };typedef struct { int x; int y; } Point;文件操作 8
FILE *fp = fopen("file.txt", "r");fclose(fp);fscanf(fp, "%d", &x);fprintf(fp, "%d\n", x);fread(buf, size, count, fp);fwrite(buf, size, count, fp);fgets(buf, size, fp);fseek(fp, offset, SEEK_SET);预处理器 7
#include <stdio.h>#include "myheader.h"#define MAX 100#define SQUARE(x) ((x) * (x))#ifdef DEBUG ... #endif#ifndef HEADER_H ... #define HEADER_H ... #endif#pragma once参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
int x = 10; | 声明有符号整型变量 | int x = 10; |
float pi = 3.14f; | 单精度浮点(约 7 位有效数字) | float pi = 3.14f; |
double e = 2.718; | 双精度浮点(约 15 位有效数字) | double e = 2.718; |
char c = 'A'; | 字符变量,值为其 ASCII 编码 | char c = 'A'; |
char str[] = "Hello"; | 以 \0 结尾的字符数组 | char str[] = "Hello"; |
const int MAX = 100; | 只读常量,赋值即编译报错 | const int MAX = 100; |
enum {RED, GREEN, BLUE}; | 批量命名整型常量,默认从 0 递增 | enum {RED, GREEN, BLUE}; |
typedef int Integer; | 给已有类型起别名 | typedef int Integer; |
for (int i = 0; i < n; i++) | 计数型循环 | for (int i = 0; i < n; i++) { } |
while (condition) | 先判后执行 | while (condition) { } |
do { } while (condition); | 先执行后判断,至少跑一次 | do { } while (condition); |
switch (x) { case 1: break; default: break; } | 多路分支,break 防止贯穿 | switch (x) { case 1: break; default: break; } |
int *p = &x; | 取地址赋给指针 | int x = 10; int *p = &x; |
*p = 20; | 解引用写值 | *p = 20; |
int arr[5] = {1,2,3,4,5}; | 数组初始化 | int arr[5] = {1, 2, 3, 4, 5}; |
int *p = arr; | 数组名即首地址 | int *p = arr; |
*(p + i) | 指针算术,等价于 arr[i] | *(p + i) |
int **pp; | 二级指针 | int **pp; |
void *vp; | 通用指针 | void *vp; |
void swap(int *a, int *b); | 函数声明(原型) | void swap(int *a, int *b); |
int (*fp)(int, int) = add; | 函数指针赋值 | int (*fp)(int, int) = add; |
static int count = 0; | 静态局部变量,跨调用保留 | static int count = 0; |
extern int globalVar; | 引用外部已定义变量 | extern int globalVar; |
inline int square(int x) | 内联函数建议 | inline int square(int x) { return x * x; } |
(int *)malloc(sizeof(int) * 10) | 动态分配内存,返回 NULL 表示失败 | int *p = (int *)malloc(sizeof(int) * 10); |
(int *)calloc(10, sizeof(int)) | 分配并初始化为 0 | int *p = (int *)calloc(10, sizeof(int)); |
(int *)realloc(p, size) | 重新分配,可能返回新地址 | p = (int *)realloc(p, sizeof(int) * 20); |
free(p); | 释放内存,之后置 NULL | free(p); p = NULL; |
memset(p, 0, size) | 按字节填充内存 | memset(p, 0, size); |
memcpy(dest, src, size) | 内存拷贝,源与目标不可重叠 | memcpy(dest, src, size); |
memmove(dest, src, size) | 内存拷贝,允许区域重叠 | memmove(dest, src, size); |
struct Point { int x; int y; }; | 结构体定义 | struct Point { int x; int y; }; |
struct Point p = {1, 2}; | 结构体初始化 | struct Point p = {1, 2}; |
pp->x = 10; | 结构体指针访问成员 | struct Point *pp = &p; pp->x = 10; |
union Data { int i; float f; }; | 联合体,成员共享内存 | union Data { int i; float f; }; |
typedef struct { int x; int y; } Point; | 结构体别名 | typedef struct { int x; int y; } Point; |
FILE *fp = fopen("file.txt", "r"); | 打开文件(r/w/a/rb/wb) | FILE *fp = fopen("file.txt", "r"); |
fclose(fp); | 关闭文件并刷新缓冲 | fclose(fp); |
fscanf(fp, "%d", &x); | 格式化读取 | fscanf(fp, "%d", &x); |
fprintf(fp, "%d\n", x); | 格式化写入 | fprintf(fp, "%d\n", x); |
fread(buf, size, count, fp); | 二进制读取 | fread(buf, size, count, fp); |
fwrite(buf, size, count, fp); | 二进制写入 | fwrite(buf, size, count, fp); |
fgets(buf, size, fp); | 读取一行到缓冲区 | fgets(buf, size, fp); |
fseek(fp, offset, SEEK_SET); | 文件定位到指定位置 | fseek(fp, 0, SEEK_SET); |
#include <stdio.h> | 包含系统头文件 | #include <stdio.h> |
#define MAX 100 | 宏定义常量 | #define MAX 100 |
#define SQUARE(x) ((x) * (x)) | 宏函数,参数必须加括号 | #define SQUARE(x) ((x) * (x)) |
#ifdef DEBUG ... #endif | 条件编译 | #ifdef DEBUG ... #endif |
#ifndef H ... #endif / #pragma once | 头文件保护 | #ifndef HEADER_H ... #define HEADER_H ... #endif |
易错点与避坑指南
现象sizeof 得到的数组大小比元素个数大好几倍,或远大于预期
原因数组名在多数表达式中退化为首元素指针,但在 sizeof 这一处不做退化,仍计算整个数组的字节数;而对指针做 sizeof 得到的是指针大小。
处置需要数组长度时用 sizeof(arr) / sizeof(arr[0]);若数组已退化为指针则必须另行记录长度,不能依赖 sizeof。
现象程序跑到一半崩溃或打印出乱码的地址值
原因malloc 返回值未判空就解引用,或 realloc 失败后仍用旧指针(realloc 失败时原内存不变但会返回 NULL,覆盖后泄漏)。
处置malloc/calloc/realloc 后一律判 NULL;realloc 时用临时指针接收,成功后再赋回原变量,失败则保留原指针。
现象free 之后同一个指针又被使用,偶发段错误
原因释放后指针仍保留原地址,成为悬垂指针;再次读写或再次 free 都属未定义行为。
处置free(p) 之后立刻 p = NULL;,把释放和置空写成固定搭配。
现象宏在特定入参下算出错误结果,例如 1+1 被算成 3 而不是 4
原因宏是文本替换,参数未加括号时展开后的运算符优先级由调用处的上下文决定。
处置宏定义里每个参数都加括号,整体也加括号:#define SQUARE(x) ((x) * (x))。
现象结构体指针用 p->x 访问报编译错误,或误写成 p.x 后拿到垃圾值
原因p 是指针时必须用 -> 解引用运算符;用 . 只适用于结构体变量本身。
处置统一约定:指针用 pp->x,变量用 p.x,赋值前确认左侧到底是什么类型。
现象自己写的头文件被多个源文件包含后出现大量重复定义报错
原因头文件缺少包含保护,同一份定义被展开多次。
处置在头文件顶部加 #ifndef HEADER_H ... #define HEADER_H ... #endif,或用 #pragma once。
现象fwrite 写完文件后立刻被别的程序读取,内容缺失末尾一段
原因文件仍有数据留在 FILE 缓冲中未落盘,没有执行 fclose 刷新。
处置写入结束必须调用 fclose(fp),它会先刷新缓冲再关闭;需要提前落盘可先 fflush。
现象switch 分支里漏写 break,某个 case 执行后继续往下贯穿执行
原因C 的 switch 默认贯穿(fall-through),除非显式 break 或 return。
处置每个 case 末尾补 break,并在 default 后也补上;若确实是刻意贯穿,就在分支内写注释说明。
排障路径
1确认动态分配是否成功并检查容量
int *p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { /* 失败分支 */ } int *q = (int *)calloc(10, sizeof(int));先判 NULL 再使用;calloc 版本额外保证初始全为 0,适合需要清零的缓冲区。p 为 NULL 说明分配失败(内存不足或 size 计算溢出),应中止流程而不是继续解引用。
2厘清数组与指针的退化关系
int arr[5] = {1,2,3,4,5}; int *p = arr; sizeof(arr); sizeof(p); *(p + 2);sizeof(arr) 返回整个数组字节数,sizeof(p) 返回指针大小,两者不同即说明退化已发生。此时 *(p+2) 能正确取值,但 arr 的长度信息已丢失,越界检查必须另行处理。
3安全地调整缓冲区大小
int *tmp = (int *)realloc(p, sizeof(int) * 20); if (tmp != NULL) { p = tmp; }realloc 失败时返回 NULL 且原内存不变,因此必须用临时指针接收并在成功后回写;直接 p = realloc(p, ...) 会在失败时丢掉原指针造成泄漏。
4区分 memcpy 与 memmove
memcpy(dest, src, size); memmove(dest, src, size);memcpy 假定两块区域不重叠,重叠时行为未定义;memmove 会先拷贝到临时区再写回,代价略高但重叠安全。数组内部前移/后移一律用 memmove。
5结构体在值与指针间安全互访
struct Point p = {1, 2}; p.x = 10; struct Point *pp = &p; pp->x = 20;先确认 pp 与 p 指向同一对象,再分别用 -> 与 . 访问:若 pp 改完后 p.x 同步变了,说明指针正确;若没变则初始化或指向有误。
6文件写入的完整闭环
FILE *fp = fopen("file.txt", "w"); fprintf(fp, "%d\n", 42); fclose(fp);打开、写入、关闭三步缺一不可,fclose 同时负责刷新缓冲。写完立刻用另一段代码重新读取校验,就能区分「没写进去」与「缓冲未落盘」。
7排查宏展开是否符合预期
#define SQUARE(x) ((x) * (x)) SQUARE(a + b)预处理器做纯文本替换,SQUARE(a + b) 应展开为 ((a + b) * (a + b))。逐项检查每个参数是否被括号包住、整体是否被括号包住,任一处缺失都会在特定入参下改变优先级。
命令示例
源区间与目标区间重叠时必须用 memmove
memmove(dest, src, size);
memcpy(dest, src, size);把一段缓冲区里的子串前移或后移时,源与目标天然重叠,行为未定义。memcpy 不保证方向,memmove 内部按重叠情况选择拷贝方向,是这类搬移的唯一正确选择。
realloc 失败不丢原指针
int *p = (int *)malloc(sizeof(int) * 10);
int *tmp = (int *)realloc(p, sizeof(int) * 20);
if (tmp == NULL) { /* p 仍然有效 */ }
else { p = tmp; }写成 p = realloc(p, ...) 时,分配失败返回 NULL 会把唯一还活着的指针覆盖掉,原内存泄漏且再也找不回来。先存到临时变量、成功后再赋回,是防泄漏的基本写法。
读一行用 fgets 而不是无长度限制的输入
char buf[128];
if (fgets(buf, sizeof(buf), fp) == NULL) { /* EOF 或出错 */ }
buf[strcspn(buf, "\n")] = '\0';缓冲区按 sizeof 计算传入,天然带长度上限,超长行被截断而不会写穿栈。fgets 会保留换行符,去掉它才能让 atoi/strcmp 之类按整行语义处理。
宏里的参数必须加括号,否则多次求值
#define SQUARE(x) ((x) * (x))
int i = 1;
int r = SQUARE(i++);这是 C 预处理阶段最隐蔽的一类 bug:i++ 出现在宏体里两处,运行时被算了两遍,代码看起来完全正常,值却不对。把宏换成 static inline 函数就没有这个问题。
输出
// r = 2 而非预期的 4:i++ 被展开两次
静态局部变量做调用计数,但只在单文件内有效
static int count = 0;
count++;static 让变量拥有整个程序生命周期、零初始化,但名字只在本文件可见。想给整个程序做计数必须配 extern 声明,否则多文件项目里会出现两份互不相干的计数。
常见坑与注意事项
- free(p) 之后继续读写 p,或忘记把 p 置 NULL:use-after-free 是最难查的一类崩溃,且不会稳定复现。
- p = (int *)realloc(p, ...) 失败时返回 NULL 会覆盖原指针,原内存直接泄漏,必须先存临时变量再赋回。
- memcpy(dest, src, sizeof(arr)) 常把结尾的 \0 一起拷进目标,导致字符串后面多出垃圾字符;按 strlen 或 sizeof-1 计算长度。
- 用数组或 strcpy 处理输入却不给长度上限,是栈缓冲区溢出的主要来源;至少用 fgets(buf, sizeof(buf), fp)。
- 头文件漏写 #ifndef HEADER_H ... #endif 保护,单文件能编过,一旦被两个 .c 包含就重复定义报错。
- malloc/calloc/realloc 的返回值不判 NULL 就解引用,内存紧张时会直接段错误且现场极难复现。
提示
- malloc 分配后务必检查返回值是否为 NULL。
- free 后建议将指针置为 NULL,避免野指针。
- 数组名在大多数表达式中会退化为指针,但 sizeof(arr) 仍是整个数组大小。
常见问题
malloc 之后不检查 NULL 会不会出问题,出问题长什么样?
会。malloc 返回 NULL 表示分配失败(内存不足或 size 计算溢出),若不判断就直接写入,行为是未定义,常见表现是运行时段错误或静默写坏别处数据。稳妥写法是 p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { /* 处理失败 */ };需要清零时用 calloc(10, sizeof(int)) 一步到位,扩容用 realloc,且 realloc 失败返回 NULL 时不能覆盖原指针,否则会泄漏。
为什么说 free 完要把指针置成 NULL?
free(p) 只释放 p 指向的内存块,本身不会改动 p 的值,于是 p 变成悬垂指针:它仍指向已归还给系统的地址。再 free(p) 一次是重复释放,再 *p = 1 就是往已释放内存写,两者都是未定义行为。free(p) 之后紧跟一句 p = NULL;,让"这块内存已经没了"这件事在指针本身可见,误用时会立刻崩在 NULL 上而不是悄悄破坏数据。
数组名传给函数后就变成指针了,sizeof 还能用吗?
取决于在哪里用。int *p = arr; 之后 p 确实指向首元素,只能用 *(p + i) 间接访问;但在原作用域里 sizeof(arr) 仍返回整个数组的字节数,因为数组名只有在大多数表达式中才退化为指针,传参时它作为实参退化了。把数组交给函数时应额外传入长度(如 void swap(int *a, int *b)),因为函数内部无法从指针反推元素个数。
头文件为什么要写 include guard,什么时候用 pragma once?
同一头文件被多个 .c 间接包含时,其内容会被重复展开,轻则重复定义报错,重则浪费空间。传统写法是 #ifndef HEADER_H ... #define HEADER_H ... #endif:靠宏判断是否已展开,现代编译器支持更好;#pragma once 只需一行、意图更清楚,是现代头文件保护的推荐方式。新写的头文件直接用 #pragma once,与旧文件混用时注意别把宏保护删掉而造成重复包含。
char str[] = "Hello" 到底占了多少内存,为什么说它不是字符串类型?
C 里没有真正的字符串类型,"Hello" 只是 6 个字符字面量加一个隐式结束符:H e l l o \0,所以 char str[] = "Hello"; 的 sizeof(str) 是 6,占 6 个字节。最后那个 \0 是唯一的终止标志,任何以 \0 结尾的字符数组才能被 fgets、fprintf("%s") 之类按字符串处理;数组多留一字节时它就是空的终止符,遍历时也必须检查是否碰到 \0,否则会读越界。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。