T0FV404 / C语言语法速记

Created Sun, 07 Dec 2025 16:54:20 +0800 Modified Mon, 08 Dec 2025 13:44:50 +0800
11959 Words

碎碎念

最近搬迁博客,就把这些文章都过了一遍,修改了许多东西。

为了写出来的 C 语言能和 Cpp(C++20 标准)兼容,我写的一般是 C99 标准,高于 C99 标准的我会说明。

这个只是速记,不能代替正常的学习过程。

好习惯

stdint.h 代替原本大小不明确的类型。

所有控制语句都加上大括号,即使只有一句话。

错误处理约定好,0 为成功,非 0 为错误码。对任何可能调用失败的函数都做好检查。不要随意 exit,尽可能把错误交给上层。

比起宏,多用静态内联函数代替,避免出错。

语言编写风格

Google C 风格(Google C Style)

Google 的公开风格指南主要是 C++,但在写 C 代码时通常沿用类似约定。核心特点:小写 + 下划线,为主;驼峰只在类型名等少数地方用

典型约定(简略版):

  • 变量名(局部、全局、参数):
    使用小写 + 下划线:total_count, file_size, index_i
  • 函数名:
    同样小写 + 下划线:read_file(), compute_sum()
  • 类型名(struct/enum/typedef):
    一般首字母大写 + 下划线或驼峰:MyStruct, ErrorCode,或写成:my_struct_t
  • 宏 / 常量宏:
    全大写 + 下划线:MAX_BUFFER_SIZE, DEFAULT_TIMEOUT_MS
  • 枚举常量:
    全大写 + 下划线:COLOR_RED, COLOR_GREEN
  • 全局变量:
    不鼓励大量使用;若用也写成普通的 g_sizeg_config 或直接 global_config,不会特意用匈牙利前缀。

一个符合 Google 风味的 C 例子:

typedef struct {
    int width;
    int height;
} Rectangle;

int compute_area(const Rectangle *rect) {
    return rect->width * rect->height;
}

static int max_int(int a, int b) {
    return a > b ? a : b;
}

特点:整体偏“UNIX/C 社区”风格,不使用匈牙利命名,不用成员名前缀类型信息,简单直接。

微软风格(Microsoft C/C++ 命名习惯)

微软内部风格历史上深受 Windows API 和早期 MFC 影响,特点是:喜欢匈牙利命名 + 大小写混合(驼峰/帕斯卡)
不过需要注意:现在官方也在弱化“匈牙利记号法”,但 Windows SDK/MFC 里仍然能广泛看到这种风格。

典型传统习惯:

  • 变量名前缀(“匈牙利命名”):
    • n/i/cnCount(int),iIndexcChar
    • sz:以 '\0' 结尾的字符串:szName
    • psz:指向字符串的指针:pszBuffer
    • dwDWORDdwFlags
    • lp/p:指针:lpBuffer, pNext
  • 函数名:
    • 常用 帕斯卡命名法(首字母大写驼峰):CreateWindowEx, GetModuleHandle
  • 结构体 / 类型名:
    • typedef struct _FOO { ... } FOO, *PFOO;
      成员常驼峰:dwSize, lpBuffer
  • 宏常量:
    • 全大写 + 下划线:MAX_PATH, ERROR_ACCESS_DENIED

一个典型带微软味道的片段:

typedef struct _RECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT, *PRECT;

int nCount;
char szName[64];
DWORD dwFlags;

编译预处理

宏定义

define 的宏定义只是替换(一般来说, 宏定义用大写字母, 与普通变量区分), 下例:

#include<stdio.h>
#define a 10
#define b 20+a
b*2=20+10*2 //要达到先加后乘的效果应该是把b定义为(20+a)

注意: 由于宏定义在预编译中,编译之前,没有检查语法。用#undef 可以终止宏定义的作用域.

宏定义只是机械的替换,所以会出现这样的情况:

#define s(x) x*x 
int main(){
	int x = 5;
	printf("s(x+1)");
}
// 输出x+1*x+1

所以最好每个值加上小括号避免这种情况。

#define STR(s) #s

STR(hello)       "hello"
STR(123 + 456)   "123 + 456"
  
#define CAT(x, y) x##y

CAT(hello, world)   helloworld
CAT(var, 123)       var123

还可以用 \ 换行。

常用指令

#include
#if / #ifdef / #ifndef / #elif / #else / #endif
#error
#warning
#pragma once
#pragma pack
_Static_assert(sizeof(int) == 4, "int must be 4 bytes"); // C11

数据类型

数据二进制底层形式

原码,反码和补码

负数补码: “取反 + 1”(对整个数取反再加一,符号位也一起参与)

浮点数

以 32 位浮点数为例子,其底层为:

[S][ EEEEEEEE ][ FFFFFFFFFFFFFFFFFFFFFFF ]
 ^    8 位指数          23 位尾数字段
符号位 exponent          fraction

$$ \text{值} = (-1)^S \times 1.\text{F} \times 2^{E_{\text{实际}}} $$

一些技巧

sizeof 可以获得相应类型或者变量的内存大小。

C 语言可以是用科学记数法,e 和 E 都可以用。

类型转化

分为隐式类型转化和显示类型转化,很好理解,不写了。

注意截断和溢出。

基础类型

布尔型

实际上 C 语言有布尔类型,内建为 _Bool,可以用 stdbool.h 引入(烂尾了)。

整形

因为 C 语言历史发展的缘故,它的整形数据结构大小不同平台不一致,下表:

类型 标准最小宽度 / 最小范围 32 位 ILP32(典型 x86 Linux/Win32) 64 位 LP64(Linux/macOS x86_64, aarch64) 64 位 LLP64(Windows x64)
char 至少 8 位;范围为 signed charunsigned char 1 字节;-128~1270~255 1 字节;-128~1270~255 1 字节;-128~1270~255
signed char ≥ 8 位;至少 -127 ~ +127 1 字节;通常 -128~127 1 字节;通常 -128~127 1 字节;通常 -128~127
unsigned char ≥ 8 位;至少 0 ~ 255 1 字节;0~255 1 字节;0~255 1 字节;0~255
short ≥ 16 位;至少 -32767 ~ +32767 2 字节;-32768~32767 2 字节;-32768~32767 2 字节;-32768~32767
unsigned short ≥ 16 位;至少 0 ~ 65535 2 字节;0~65535 2 字节;0~65535 2 字节;0~65535
int ≥ 16 位;至少 -32767 ~ +32767 4 字节;-2 147 483 648~2 147 483 647 4 字节;-2 147 483 648~2 147 483 647 4 字节;-2 147 483 648~2 147 483 647
unsigned int ≥ 16 位;至少 0 ~ 65 535 4 字节;0~4 294 967 295 4 字节;0~4 294 967 295 4 字节;0~4 294 967 295
long ≥ 32 位;至少 -2 147 483 647 ~ +2 147 483 647 4 字节;-2 147 483 648~2 147 483 647 8 字节;-9 223 372 036 854 775 808~9 223…807 4 字节;-2 147 483 648~2 147 483 647
unsigned long ≥ 32 位;至少 0 ~ 4 294 967 295 4 字节;0~4 294 967 295 8 字节;0~18 446 744 073 709 551 615 4 字节;0~4 294 967 295
long long ≥ 64 位;至少 -9 223 372 036 854 775 807 ~ +…807 8 字节;-9 223 372 036 854 775 808~9 223…807 8 字节;-9 223 372 036 854 775 808~9 223…807 8 字节;-9 223 372 036 854 775 808~9 223…807
unsigned long long ≥ 64 位;至少 0 ~ 18 446 744 073 709 551 615 8 字节;0~18 446 744 073 709 551 615 8 字节;0~18 446 744 073 709 551 615 8 字节;0~18 446 744 073 709 551 615

在标准库 limits.h 中可以看到相应类型的最小值和最大值,是宏定义,我就不一一列出来了。

故而写 C 语言我更推荐引用 <stdint.h> 库,使用里面的数据类型,里面的格式都是 uintx_t、intx_t,非常简洁。

C99 起同样拥有 long longLL/ULL 后缀

字符类型

本来字符类型属于整数类型是不用再写的,但是 C 语言存在转义字符和规定符。

转义字符

写法 含义 说明
\\ 反斜杠字符 \ 本来 \ 是转义起始,所以要写成 \\
\' 单引号 ' 用在字符/字符串中
\" 双引号 " 字符串字面量里写 " 必须转义
\n 换行(LF,line feed) 在 Unix/Linux/macOS 里就是换行
\r 回车(CR,carriage return) Windows 文本换行常是 \r\n
\t 水平制表(Tab) Tab
\v 垂直制表 现在很少用
\b 退格(backspace) 光标向左退一格
\f 换页(form feed) 古老打印机/终端控制符
\a 响铃(bell) 老终端会“滴”一声,现代一般没反应
\? 问号 ? 避免被老式“三字母序列”影响(几乎不用)
\ooo 八进制数字 反斜杠 + 1~3 位八进制数字(0–7),最多三位
\xhh... 十六位进制数字 直到遇到非十六进制字符为止的那串数字组成的值
\uXXXX 16 位 Unicode 码点(4 位十六进制) 基本用不上
\UXXXXXXXX 32 位 Unicode 码点(8 位十六进制) 基本用不上

规定符

格式/用法 适用函数 含义 / 说明 对应类型(参数或指针类型)
%d / %i printf/scanf 十进制有符号整数 int / int *
%u printf/scanf 十进制无符号整数 unsigned int / unsigned int *
%ld printf/scanf 十进制有符号长整型(“32 位”只是常见实现,实际位数由 long 决定) long int / long int *
%lu printf/scanf 十进制无符号长整型 unsigned long int / unsigned long int *
%lld printf/scanf 十进制有符号长长整型(常见为 64 位) long long int / long long int *
%llu printf/scanf 十进制无符号长长整型 unsigned long long int / unsigned long long int *
%x / %X printf/scanf 无符号整数的十六进制表示(小写/大写) unsigned int / unsigned int *
%o printf/scanf 无符号整数的八进制表示 unsigned int / unsigned int *
%f printf 小数形式的浮点数(printf 中参数类型为 double double
%lf printf printf 中与 %f 等价(同样读取 double double
%f scanf 读入 float float *
%lf scanf 读入 double double *
%e / %E printf/scanf 指数形式的浮点数(如 1.23e+10 double / double *
%g / %G printf/scanf %e%f 中输出“更短”的形式 double / double *
%c printf/scanf 单个字符 int / char *
%s printf/scanf 字符串(以 '\0' 结尾) char * / char *
%p printf 指针的值(地址) void *(自动提升)
%p scanf 读入指针值 void ** 或具体类型的 T **
%% printf 输出一个 % 字符本身

补充要点:

  • printf 中所有浮点实参都会默认提升为 double,因此 %f / %lf 实际一样,都读 double
  • scanf%f%lf 有区别:分别是 float *double *

printf 中的宽度/精度:

  • %nd: 在 printf 中表示:

    宽度至少为 n 的十进制整数,右对齐,不足时左侧补空格。 示例:printf("%5d", x); 表示宽度至少 5。

  • %-ns: 在 printf 中表示:

    宽度至少为 n 的字符串,左对齐,不足时右侧补空格。 示例:printf("%-10s", s); 表示宽度至少 10。

  • %m.nf: 在 printf 中表示:

    总宽度至少为 m,小数部分保留 n 位,右对齐; 若实际位数不足 m,则左侧补空格。 示例:printf("%5.2f", y); 即 m = 5, n = 2。

  • %0nd: 在 printf 中表示:

    宽度至少为 n 的整数,右对齐,不足时左侧用 0 填充。 示例:printf("%05d", x); 即 n = 5。

  • %#x: 在 printf 中表示:

    以十六进制输出无符号整数,并带有 0x 前缀(%#X0X 前缀)。

  • %*d: 在 printf 中表示:

    宽度由 额外的一个 int 实参 指定。 例如: printf("%*d “, w, x); // 等价于 printf("%nd “, x); 其中 n = w

scanf 中的宽度/抑制赋值:

  • %nd: 在 scanf 中表示:

    最多读取 n 位数字 构成一个整数,存入对应变量,多余的字符留在输入缓冲区。 示例:scanf("%4d", &a); 即 n = 4。

  • %*d: 在 scanf 中表示:

    读取一个整数,但 不赋值、不保存(抑制赋值),常用于跳过不需要的数据。

  • %d%*c%d: 在 scanf 中表示:

    先读一个整数给第一个参数,然后读一个字符并丢弃(%*c),再读一个整数给第二个参数。 例如输入 1+2 时:a=1+ 被丢弃,b=2

单引号当成字符,双引号当成字符串。

宽字符

<wchar.h><stddef.h> 中引入

wchar_t 宽字符

L"" 宽字符字面量

浮点型

类型 存储大小 值范围 精度
float 4 字节 1.2E-38 到 3.4E+38 6 位有效数字
double 8 字节 2.3E-308 到 1.7E+308 15 位有效数字
long double 16 字节 * 3.4E-4932 到 1.1E+4932* 19 位有效数字*

正常的浮点数字默认被当成 double 类型的,带上 f 或者 F,比如 2.3f,可以修改字面量为 foalt。同理 l 和 L 可以修改字面量为 long double

void

void 类型指定没有可用的值。它通常用于以下三种情况下:

函数返回为空:C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);

函数参数为空:C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);

指针指向 void:类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 * void *malloc( size_t size );* 返回指向 void 的指针,可以转换为任何数据类型。

指针

指针,就是一个专门指向内存一个地方的东西,形式为 type*,这里的 type 同样可以是指针,因为指针的值也是存放在内存里的。

实际上在 C 语言中几乎所有东西都能是指针,甚至函数也是!

数组

一维数组

定义: type name[length]

存储在连续内存中, 根据个数和类型占空间, 内存字节数 = 元素个数*sizeof(类型)。

注意: 引用范围为 [0, 长度-1]

int a[4]={1,2,3,4};
int a[4]={1,2,3}; // 没给初始值的默认为0
int a[]={1,2,3,4} // 长度为初始化的长度
char fuck[] = "fjskljf" // 结尾默认加0,因为这是字符串
int a[12]; // 初始化:如果没有初始化,那么内存中是什么是不确定的

多维数组

那二维数组举例

顾名思义, 两个维度.形式:

int score[3][4](先行后列)

其实仍是一维排序, 在内存里就是

[0][0],[0][1],[0][2],[0][3],[1][0],[1][1],[1][2],[1][3](如此依序排列)

初始化:

int a[2][3]={1,2,3,4,5,6};
int a[2][3]={{1,2,3},{4,5,6}};
int a[2][3]={1,2,3,4}; // 未初始化的为0即{1,2,3,4,0,0}
int a[2][3]={{1,2},{3,4}}; // 为{1,2,0,3,4,0}

可省略行数, 但不可省略列数, 以上所有把行数去掉依旧等价.

多类型结构

结构体

定义

结构体就是把 若干字段打包成一个整体类型,格式如下:

struct Point { // 定义
    int x;
    int y;
};

struct Person { // 定义同时声明变量
    char name[20];
    int age;
} p1, p2;

struct Point p; // 定义变量
struct Point *pp = &p;
p.x = 10;
pp->y = 4;     // 等价于 (*pp).y

初始化

顺序初始化

struct Person p = {"Alice", 20};  // 按成员声明顺序

指定初始化

struct Person p = {
    .age = 20,
    .name = "Alice",
};

复合字面量

可以直接在表达式里构造一个“临时结构体对象”:

func((struct Point){3, 4});  // 作为参数

内存布局

struct S {
    char c;   // 1 字节
    int  i;   // 通常 4 字节
};

常见 32/64 位 ABI 中,sizeof(struct S) 通常是 8,而不是 5,因为中间会插入填充字节,使 int 按 4 字节对齐。

在单片机这类非常缺空间的机子上这是不能容忍的,所以这里有许多优化手段。

位域让你用“位”为单位定义整数成员(常用于协议、寄存器等):

struct Flags {
    unsigned int a : 1;   // 1 位
    unsigned int b : 3;   // 3 位
    unsigned int c : 4;   // 4 位
};

注意:

  • 具体布局(高位/低位顺序、是否跨字节)是 实现相关 的。
  • 跨平台时尽量不用位域刻画精确二进制协议,改用位运算 &<< 等自己控制。

有些编译器还支持 #pragma pack 或属性来改变对齐方式:

#pragma pack(push, 1)
struct Packed {
    char c;
    int  i;
};
#pragma pack(pop)
  • 可减少空间,但可能导致访问变慢,甚至在某些硬件上非法(未对齐访问)。
  • 标准 C 不定义这些行为,属于编译器扩展;跨平台时要谨慎。

结构体与 typedef

下面的写法十分繁琐:

struct Point {
    int x, y;
};

struct Point p;

typedef struct Point {
    int x, y;
} Point;

Point p;   // 不用再写 struct

typedef struct { // 使用匿名函数
    int x, y;
} Point; 

嵌套结构体与自引用

struct Date { int y, m, d; };

struct Person {
    char name[20];
    int age;
    struct Date birthday;
};

struct Person p = {
    .name = "Bob",
    .age = 30,
    .birthday = {1990, 1, 1}
};

struct Node {
    int value;
    struct Node *next;  // OK,指针大小固定
    // struct Node next; // 错误,类型不完整
};

struct S {
    int tag;
    union {
        int i;
        float f;
    };  // 匿名 union C11标准
};

struct S s;
s.i = 10;   // 不需要 s.u.i

联合体

其实等于只有一个地址的结构体,以最后赋值的值为内容, 最大占其中最大成员的内存。语法与结构体一致。

union Data d = {10};    // 初始化 .i = 10
union Data d = {.f = 3.14f};

经典用法:

typedef enum {
    TYPE_INT,
    TYPE_FLOAT,
    TYPE_PTR
} ValueType;

typedef struct {
    ValueType type;
    union {
        int   i;
        float f;
        void *p;
    } data;
} Value;

Value v;
v.type = TYPE_INT;
v.data.i = 42;

// 也可匿名

枚举类型

enum Color {
    RED, // 默认为0,逐次+1
    GREEN,
    BLUE
};

enum ErrorCode {
    ERR_OK      = 0,
    ERR_IO      = 1,
    ERR_TIMEOUT = 2,
    ERR_FATAL   = 100
};

enum Weekday {
    MON = 1,
    TUE,        // 2
    WED,        // 3
    THU = 10,
    FRI,        // 11
    SAT,        // 12
    SUN         // 13
};

enum Color color;

color = RED;

if (color == GREEN) {
    /* ... */
}

enum Color color = BLUE; // C99

运算符与表达式

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。

算术运算符

下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母,如果是整数向下取整 B / A 将得到 2
% 取模运算符,整除后的余数.ps: 浮点型不能用, 要用 math.h 的 fmod()和 fmodl() B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
自减运算符,整数值减少 1 A– 将得到 9

关系运算符

下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 为假。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 为假。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 为假。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

逻辑运算符

下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 (A && B) 为假。
|| 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 (A|| B) 为真。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 !(A && B) 为真。

位运算符

位运算符作用于位,并逐位执行操作。

运算符 描述 实例
& 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。按位与操作,按二进制位进行 “与” 运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; (A & B) 将得到 12,即为 0000 1100
| 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。按位或运算符,按二进制位进行 “或” 运算。运算规则:`0 0 = 0; 0
^ 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。异或运算符,按二进制位进行 “异或” 运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; (A ^ B) 将得到 49,即为 0011 0001
~ 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。取反运算符,按二进制位进行 “取反” 运算。运算规则:~1=-2; ~0=-1; (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
« 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补 0)。 A « 2 将得到 240,即为 1111 0000
» 将操作数的所有位向右移动指定的位数。右移 n 位相当于除以 2 的 n 次方。二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 A » 2 将得到 15,即为 0000 1111

赋值运算符

下表列出了 C 语言支持的赋值运算符:

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C * = A 相当于 C = C * A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
«= 左移且赋值运算符 C «= 2 等同于 C = C « 2
»= 右移且赋值运算符 C »= 2 等同于 C = C » 2
&= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 C|= 2 等同于 C = C | 2

杂项运算符 ↦ sizeof & 三元

下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof? :

运算符 描述 实例
sizeof() 返回变量的大小。 sizeof(a) 将返回 4,其中 a 是整数。
& 返回变量的地址。 &a; 将给出变量的实际地址。
* 指向一个变量。 * a; 将指向一个变量。
? : 条件表达式 如果条件为真 ? 则值为 X : 否则值为 Y

C 中的运算符优先级

下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。

类别 运算符 结合性
后缀 () [] -> . ++ - - 从左到右
一元 + - ! ~ ++ - - (type)* & sizeof 从右到左
乘除 * / % 从左到右
加减 + -s 从左到右
移位 « > > 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 AND & 从左到右
位异或 XOR ^ 从左到右
位或 OR | 从左到右
逻辑与 AND && 从左到右
逻辑或 OR || 从左到右
条件 ?: 从右到左
赋值 = += -= *= /= %=»= «= &= ^=|= 从右到左
逗号 , 从左到右

变量和函数

命名规则

语法层面的规则(编译器强制):

  • 可以包含:字母(a–z / A–Z)、数字(0–9)、下划线 _
  • 第一个字符:必须是字母或 _,不能是数字
  • 区分大小写:myVarmyvar 是两个不同标识符
  • 不能包含空格或特殊符号:& * $ @ - + 等都不允许
  • 不能是关键字:ifelseintstaticreturn

作用域(scope)

作用域 = 这个标识符“在哪些地方可见”。

代码块作用域(块作用域)

  • 出现于:花括号 { ... } 内,如函数体、if、while、for 块
  • 典型:局部变量、形参
void f(int x) {        // x 也是块作用域
    int a = 10;        // a 只在 f 的花括号内可见
    if (x > 0) {
        int b = 20;    // b 只在 if 块内可见
    }                  // 这里 b 已经“不可见”
}

文件作用域(全局/静态作用域)

  • 出现于:所有函数之外、全局区域
  • 从定义点开始,到 该源文件末尾 都可见
int g = 0;         // 文件作用域

void foo(void) {
    g++;
}

函数作用域(只针对标签)

  • goto 标签名的作用域是所在函数内部,这里一般不深究。

链接属性(linkage)

链接属性 = 跨源文件时,这个名字是否“指向同一个实体”

C 里有三种链接属性:

  • 外部链接(external linkage)
  • 内部链接(internal linkage)
  • 无链接(no linkage)

外部链接:多个文件共享同一个标识符

  • 默认:文件作用域的非 static 变量 / 函数
// a.c
int g = 0;        // g 有外部链接

void foo(void) {} // foo 也有外部链接

// b.c
extern int g;     // 和 a.c 里的 g 是同一个
void foo(void);   // 声明 a.c 中的 foo

内部链接:仅在当前源文件内部可见

  • 通过 static 修饰的、处在文件作用域的变量 / 函数
// a.c
static int counter = 0;     // 只能在 a.c 内使用

static void helper(void) {  // 只能在 a.c 内调用
    counter++;
}
  • 其他源文件即使声明 extern int counter; 也无法访问到这里的 counter,因为链接属性不同。

无链接:每个定义独立,文件间不共享

  • 块作用域的变量、形参、typedef 名等都属于“无链接”:
    • 它们在每个函数/块里都是各自独立的标识符,“名字相同”也不是同一个实体。
void f(void) {
    int x;       // 无链接
}

void g(void) {
    int x;       // 另一个完全无关的 x
}

生存期(lifetime / storage duration)

生存期 = 变量在内存中实际存在的时间段,和作用域(可见范围)不是一回事。

主要有两类:

  • 静态存储期(static storage duration)
  • 自动存储期(automatic storage duration)

静态存储期

  • 在程序开始运行时分配,在程序结束时释放
  • 包括:
    • 所有文件作用域的变量(不管有没有 static
    • static 修饰的局部变量
    • 所有字符串常量、常量区之类的实现细节对象(你可以先记住“全局 + static 局部”)
int g;                 // 静态存储期

void f(void) {
    static int s = 0;  // 也是静态存储期,只是作用域在块内
    s++;
}

自动存储期

  • 典型:普通局部变量、函数形参
  • 在进入块/函数时分配,在离开块/函数时销毁
void f(int x) {  // x 自动存储期
    int y = 10;  // y 自动存储期
}                // 运行到这里 x 和 y 就“消亡”
  • 自动变量的初始内容 如果你没显式初始化,就是未定义的垃圾值

(动态分配内存 malloc 那块,属于“动态存储期”,由你手动控制,这里不归类进关键字存储类型里。)

存储类关键字与常见关键字

这里把常见的相关关键字一块讲清楚:autoregisterstaticexternconstinline

auto(自动存储,几乎不用显式写)

  • 默认就是 auto,你写不写都一样:
void f(void) {
    int x;        // 等价于 auto int x;
    auto int y;   // 很少有人这样写
}
  • 作用:

    • 块作用域
    • 自动存储期
    • 无链接
  • 现代 C 基本不用显式写 auto,只要知道“普通局部变量就是 auto 存储期”即可。

register(建议放在寄存器)

  • 语法:
void f(void) {
    register int x = 0;
}
  • 本意:建议编译器把这个变量放在 CPU 寄存器里,加快访问(现在优化器通常更聪明,register 多数被忽略)。
  • 限制:
    • 理论上不能对 register 变量取地址(&x 不一定允许)。
  • 作用:
    • 块作用域
    • 自动存储期
    • 无链接

在现代 C 里,普遍建议:不用写 register,交给编译器自动优化就好。

static

分几种情况:

文件作用域的 static(改变链接属性 → 内部链接)

static int g = 0;   // 只在本源文件内可见(内部链接)

static void helper(void) {
    /* 只在本文件内可调用 */
}
  • 作用域:文件作用域
  • 存储期:静态存储期
  • 链接属性:内部链接(仅本文件共享)

这是一种“隐藏实现细节”的手段,非常推荐用于库/模块内部的全局量和函数。

块作用域的 static(改变存储期 → 静态存储期)

void counter(void) {
    static int c = 0;   // 只初始化一次,整个程序期间都存在
    c++;
    printf("%d\n", c);
}
  • 作用域:块作用域(只在函数内部可见)
  • 存储期:静态存储期(整个程序运行期一直存在)
  • 链接属性:无链接

典型用途:

  • 在函数内做“静态局部变量”,用来记忆调用状态(如计数器、缓存等)。
  • 注意:初始值默认是 0(全局/static 变量默认会清零)。

extern

用于 声明一个已经在别处定义的全局变量或函数,不改变其本身属性,只是告诉编译器“这个名在别的翻译单元有定义”。

// a.c
int g = 42;

// b.c
extern int g;     // 声明:g 在别处定义

适用场景:

  • 在头文件中声明全局变量/函数接口,在某个 .c 中给出定义:
// mylib.h
extern int my_global;
void my_func(void);

// mylib.c
#include "mylib.h"
int my_global = 0;
void my_func(void) { ... }

特性:

  • 只能用在文件作用域(不能在函数里面用 extern 定义一个新变量,还想有外部链接)。
  • 不会改变变量本身的生存期/链接属性,只是一个声明。

const

const = 只读 语义限定符,和存储期/作用域无直接关系,但用在变量定义和声明时极其常用。

const int x = 10;         // x 不能被修改
int *p;
const int *pc;            // 指向 const int 的指针:*pc 不能改
int * const cp = &x;      // const 指针:cp 本身不能改,*cp 可以(这里示例不严谨,只看语法)

几点要注意:

  • const 不等于“放在只读存储区”,只是 语义上的不可改,具体放哪是实现细节。
  • 作用域、生存期、公/私有都由“位置和 static/extern”决定,const 只是增加“禁止修改”的约束。

常见模式:文件内只读全局常量

static const double PI = 3.141592653589793;
  • 作用域:文件作用域
  • 存储期:静态存储期
  • 链接属性:内部链接
  • 语义:不可修改

inline(内联函数)

  • 关键字:inline,表示“建议内联”,是否真正内联由编译器决定。
  • 一般配合 staticextern 一起用(否则可能出现多重定义问题),在 C99/C11 中需要注意用法:

常见安全写法(放在头文件):

static inline int max_int(int a, int b) {
    return a > b ? a : b;
}
  • static inline 函数:
    • 作用域:翻译单元内部(类似内部链接)
    • 通常会被内联,也可以生成本地函数体

或在 C11 里配合 extern inline 用于某些高级优化场景,这个比较复杂,入门阶段记住:

  • 把小工具函数写成 static inline 放头文件里,即可。

inline 不改变存储期,它只影响函数的链接属性和是否生成独立函数体,与“变量的生命周期”不同类概念,这里放一起只是顺带说明。


“就近原则”的变量遮蔽(shadowing)

当你在内层作用域定义了与外层同名的变量时,内层会“遮蔽”外层:

int x = 1;          // 全局 x

void f(void) {
    int x = 2;      // 局部 x,遮蔽全局 x
    printf("%d\n", x); // 打印 2,而不是 1
}

建议:

  • 尽量避免在函数内使用和全局变量相同的名字,免得混淆。
  • 若必须使用全局变量,给全局量加统一前缀或放在结构体里,减少直接裸露的全局。

程序控制流

控制流关键字

if(){
  
}else if(){
  
}else{
  
}

switch:
case x:
	xxx;
	break;
default:
	xxx;

for (a;b;c){ // 除了b都可省略
  
}

while(){
  continue; // 跳过一次循环
  break; // 跳出循环
}

do{
  
}while()
  
  
goto xxx
xxx:
	xxx;

main

int main(int argc, char* argv[]){
} // 函数入口强制定义argc表示参数个数,包括自己。argv表示每个命令字符串

同时 main 不一定是第一个函数, 在一些调试检测需求下, 以下是有可能会出现的:

  • 全局对象的构造函数会在 main 函数之前执行。(Cpp)

  • 一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行 main 函数之前,而 main 函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作。

  • 进程启动后,要执行一些初始化代码(CRT),然后跳转到 main 执行。全局对象的构造也在 main 之前。

  • 通过关键字 __attribute__,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。

其余关键字

volatile:禁止编译器优化

auto:表示“自动存储期的局部变量”(和默认一样,所以不用管)

nullptr:C23 加的,没绷住。

常用库函数

math.h

函数 函数原型(简化) 作用 示例
fabs double fabs(double x); 绝对值(浮点) fabs(-3.5) → 3.5
sqrt double sqrt(double x); 开平方 sqrt(9.0) → 3.0
sin / cos double sin(double x); 正弦 / 余弦(弧度) sin(3.14159/2)
pow double pow(double x, double y); 幂运算,x 的 y 次方 pow(2.0, 3.0) → 8.0
exp double exp(double x); e 的 x 次方 exp(1.0) ≈ 2.71828
log / log10 double log(double x); 自然对数 / 以 10 为底对数 log(2.71828) ≈ 1

ctype.h

函数 函数原型 作用 示例
isdigit int isdigit(int c); 是否数字字符 '0'..'9' isdigit('3') → 非0
isalpha int isalpha(int c); 是否字母字符 A..Z,a..z isalpha('A') → 非0
isupper int isupper(int c); 是否大写字母 isupper('A') → 非0
islower int islower(int c); 是否小写字母 islower('a') → 非0
isspace int isspace(int c); 是否空白(空格、换行等) isspace(' ') → 非0
tolower int tolower(int c); 转为小写(若是字母) tolower('A') → 'a'
toupper int toupper(int c); 转为大写(若是字母) toupper('a') → 'A'

stdio.h

标准输入输出

函数 原型 作用 简例
printf int printf(const char *format, ...); 格式化输出到标准输出 printf("x=%d\n", x);
fprintf int fprintf(FILE *stream, const char *format, ...); 格式化输出到指定文件流 fprintf(fp, "x=%d\n", x);
sprintf int sprintf(char *str, const char *format, ...); 格式化输出到字符串(不安全) sprintf(buf, "%d", x);
snprintf int snprintf(char *str, size_t size, const char *format, ...); 限长格式化到字符串(推荐) snprintf(buf, 16, "%.2f", f);
scanf int scanf(const char *format, ...); 从标准输入按格式读 scanf("%d", &x);
fscanf int fscanf(FILE *stream, const char *format, ...); 从文件流按格式读 fscanf(fp, "%s", s);
sscanf int sscanf(const char *str, const char *format, ...); 从字符串按格式解析 sscanf(buf, "%d", &x);
puts int puts(const char *s); 输出字符串并加换行 puts("Hello");
fputs int fputs(const char *s, FILE *stream); 输出字符串到文件 fputs("Hi", fp);
putchar int putchar(int c); 输出一个字符 putchar('A');
fputc int fputc(int c, FILE *stream); 向文件写一个字符 fputc('A', fp);
getchar int getchar(void); 从标准输入读一个字符 int c = getchar();
fgetc int fgetc(FILE *stream); 从文件读一个字符 int c = fgetc(fp);
ungetc int ungetc(int c, FILE *stream); 把字符退回输入流 ungetc(c, fp);

文件处理

函数 原型 作用 简例
fopen FILE *fopen(const char *path, const char *mode); 打开文件 FILE *fp = fopen("a.txt","r");
fclose int fclose(FILE *stream); 关闭文件 fclose(fp);
fflush int fflush(FILE *stream); 刷新输出缓冲 fflush(stdout);
fread size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 二进制读块 fread(buf,1,100,fp);
fwrite size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 二进制写块 fwrite(buf,1,n,fp);
fseek int fseek(FILE *stream, long offset, int whence); 移动文件位置 fseek(fp, 0, SEEK_SET);
ftell long ftell(FILE *stream); 当前文件位置 long pos = ftell(fp);
rewind void rewind(FILE *stream); 回到文件开头 rewind(fp);
remove int remove(const char *path); 删除文件 remove("a.txt");
rename int rename(const char *old, const char *new); 重命名文件 rename("a.txt","b.txt");
tmpfile FILE *tmpfile(void); 创建临时文件 FILE *fp = tmpfile();

错误检查

函数 原型 作用
perror void perror(const char *s); 打印最近错误原因(结合 errno
feof int feof(FILE *stream); 是否到达 EOF
ferror int ferror(FILE *stream); 是否发生 I/O 错误
clearerr void clearerr(FILE *stream); 清除 EOF/错误 标志

stdlib.h

动态内存

函数 原型 作用 示例
malloc void *malloc(size_t size); 申请未初始化内存 int *p = malloc(10*sizeof *p);
calloc void *calloc(size_t nmemb, size_t size); 申请并清零 nmemb 个元素 int *p = calloc(10,sizeof *p);
realloc void *realloc(void *ptr, size_t size); 调整已分配内存大小 p = realloc(p,20*sizeof *p);
free void free(void *ptr); 释放动态内存 free(p); p=NULL;

字符串到数字

函数 原型 作用 示例
atoi int atoi(const char *nptr); 字符串 → int int x = atoi("123");
atol long atol(const char *nptr); 字符串 → long
atof double atof(const char *nptr); 字符串 → double double d = atof("3.14");
strtol long strtol(const char *nptr, char **endptr, int base); 字符串 → long,支持进制,能返回错误位置 long x = strtol("0xff",NULL,0);
strtoul unsigned long strtoul(const char *nptr, char **endptr, int base); 字符串 → unsigned long
strtod double strtod(const char *nptr, char **endptr); 字符串 → double

随机数

函数 原型 作用 示例
rand int rand(void); 返回 [0, RAND_MAX] int x = rand()%(n-m+1)+m;
srand void srand(unsigned int seed); 设置随机种子 srand(time(NULL));

程序控制 / 环境

函数 原型 作用
exit void exit(int status); 正常结束程序
abort void abort(void); 异常终止
atexit int atexit(void (*func)(void)); 注册进程退出时调用的函数
system int system(const char *command); 调用系统命令

排序与查找

函数 原型 作用 简例
qsort void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); 通用快速排序 qsort(a,n,sizeof *a, cmp);
bsearch void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); 有序数组二分查找 bsearch(&key,a,n,sizeof *a,cmp);

string.h

字符串函数(以 '\0' 结尾)

函数 原型 作用
strlen size_t strlen(const char *s); 返回字符串长度(不含 '\0'
strcpy char *strcpy(char *dest, const char *src); 拷贝字符串到 dest
strncpy char *strncpy(char *dest, const char *src, size_t n); 最多拷贝 n 字符
strcat char *strcat(char *dest, const char *src); 把 src 接到 dest 末尾
strncat char *strncat(char *dest, const char *src, size_t n); 最多连接 n 字符
strcmp int strcmp(const char *s1, const char *s2); 比较两个字符串
strncmp int strncmp(const char *s1, const char *s2, size_t n); 比较前 n 个字符
strchr char *strchr(const char *s, int c); 查找首次出现的字符 c
strrchr char *strrchr(const char *s, int c); 查找最后一次出现的字符 c
strstr char *strstr(const char *haystack, const char *needle); 查找子串
strtok char *strtok(char *str, const char *delim); 分割字符串(非线程安全,慎用)

内存函数(任意字节块)

函数 原型 作用
memset void *memset(void *s, int c, size_t n); 用字节 c 填充前 n 字节
memcpy void *memcpy(void *dest, const void *src, size_t n); 拷贝 n 字节(不允许重叠)
memmove void *memmove(void *dest, const void *src, size_t n); 拷贝 n 字节(允许重叠)
memcmp int memcmp(const void *s1, const void *s2, size_t n); 比较前 n 字节
memchr void *memchr(const void *s, int c, size_t n); 在前 n 字节中查找字符 c

断言

assert 在 assert.h 中, 检测参数是否为否, 如果问否, 则使用 abort 退出程序。是运行时断言,_Static_assert 是编译时。