C 语言语法速查表 - C 语言常用语法与指针编程大全

这套速查给用 C 写嵌入式、操作系统或高性能代码的开发者,这里不像托管语言,内存和指针生命周期都得自己盯。内容覆盖数据类型与常量、控制流、最容易埋坑的指针与数组关系、函数、手动 malloc/free、文件 I/O 和预处理器。条目按你在写哪一类构造块来分,并标记出容易写错的地方。读完应能分清何时用指针、何时用数组,把 malloc/free 管好不漏不悬垂,并用 #ifdef 把编译期逻辑写进代码。

编程语言·共 58 条命令·最后更新 2026-07-21
c系统编程指针内存管理编译语法

典型使用场景

面向写系统层、嵌入式或性能敏感代码的 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;
声明有符号整型变量,范围通常 32 位,跨平台时注意位宽差异
float pi = 3.14f;
单精度浮点,约 7 位有效数字,末尾 f 后缀表明是 float
double e = 2.718;
双精度浮点,约 15 位有效数字,需要精度时默认用 double 而不是 float
char c = 'A';
字符变量占 1 字节,值为其 ASCII 编码(A=65),单引号括字符
char str[] = "Hello";
字符串实为以 \0 结尾的字符数组,洗它时牢记最后一个空闲字节是结束符
const int MAX = 100;
用 const 声明只读常量,赋值后再改会编译报错
enum {RED, GREEN, BLUE};
枚举批量命名一组整型常量,默认从 0 递增
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++) { }
for 循环
while (condition) { }
while 循环
do { } while (condition);
do-while 循环
break;
跳出循环/switch
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)
指针算术(等价 arr[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));
分配并初始化为 0
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");
打开文件(r/w/a/rb/wb)
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))分配并初始化为 0int *p = (int *)calloc(10, sizeof(int));
(int *)realloc(p, size)重新分配,可能返回新地址p = (int *)realloc(p, sizeof(int) * 20);
free(p);释放内存,之后置 NULLfree(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. 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. 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. 3安全地调整缓冲区大小

    int *tmp = (int *)realloc(p, sizeof(int) * 20); if (tmp != NULL) { p = tmp; }

    realloc 失败时返回 NULL 且原内存不变,因此必须用临时指针接收并在成功后回写;直接 p = realloc(p, ...) 会在失败时丢掉原指针造成泄漏。

  4. 4区分 memcpy 与 memmove

    memcpy(dest, src, size); memmove(dest, src, size);

    memcpy 假定两块区域不重叠,重叠时行为未定义;memmove 会先拷贝到临时区再写回,代价略高但重叠安全。数组内部前移/后移一律用 memmove。

  5. 5结构体在值与指针间安全互访

    struct Point p = {1, 2}; p.x = 10; struct Point *pp = &p; pp->x = 20;

    先确认 pp 与 p 指向同一对象,再分别用 -> 与 . 访问:若 pp 改完后 p.x 同步变了,说明指针正确;若没变则初始化或指向有误。

  6. 6文件写入的完整闭环

    FILE *fp = fopen("file.txt", "w"); fprintf(fp, "%d\n", 42); fclose(fp);

    打开、写入、关闭三步缺一不可,fclose 同时负责刷新缓冲。写完立刻用另一段代码重新读取校验,就能区分「没写进去」与「缓冲未落盘」。

  7. 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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们