# C 语法详细总结


# C语言语法详细总结（基于C11标准，工业通用版）
本文全面覆盖C语言核心语法，从基础入门到进阶核心，兼顾**语法规则、用法示例、标准规范与常见坑点**，适配C99/C11/C17主流标准。

---

## 一、C语言程序基础结构与规范
### 1. 最小程序框架
C语言程序由函数、变量、预处理指令组成，**main函数是程序唯一执行入口**。
```c
#include <stdio.h>  // 预处理：头文件包含
int main(void) {    // 主函数，程序入口
    printf("Hello World!\n");  // 库函数：标准输出
    return 0;       // 函数返回值，0表示程序正常结束
}
```

### 2. 核心规范
1.  **语句规则**：所有执行语句必须以英文分号`;`结尾，代码块用`{}`包裹，支持嵌套。
2.  **大小写敏感**：`A`和`a`是完全不同的标识符。
3.  **标识符命名规则**：
    - 只能由字母、数字、下划线`_`组成，不能以数字开头；
    - 不能与C语言关键字重名；
    - 见名知意，避免使用下划线开头的标识符（系统保留）。
4.  **注释规范**
    - 单行注释：`// 注释内容`（C99起标准支持）
    - 多行注释：`/* 注释内容 */`，**不可嵌套使用**
    - 注释不参与编译，仅用于代码说明。

---

## 二、关键字与数据类型
### 1. C11标准关键字（37个）
| 分类 | 关键字 |
| :--- | :--- |
| 数据类型 | `char` `short` `int` `long` `float` `double` `signed` `unsigned` `_Bool` `_Complex` `_Imaginary` `void` `enum` `struct` `union` `typedef` |
| 存储类 | `auto` `register` `static` `extern` `typedef` |
| 流程控制 | `if` `else` `switch` `case` `default` `for` `while` `do` `break` `continue` `goto` `return` |
| 类型限定 | `const` `volatile` `restrict` `_Atomic` |
| 其他 | `sizeof` `inline` |

### 2. 数据类型总览
C语言数据类型分为4大类，决定了变量的内存占用、取值范围与操作规则。
```
数据类型
├─ 基本类型（内置类型）
│  ├─ 整型：char、short、int、long、long long、_Bool
│  ├─ 浮点型：float、double、long double
│  └─ 枚举类型：enum
├─ 构造类型（自定义类型）
│  ├─ 结构体：struct
│  ├─ 联合体：union
│  └─ 数组类型
├─ 指针类型：type *
└─ 空类型：void
```

### 3. 基本数据类型
#### （1）整型
| 类型 | 标准最小字节 | 典型32/64位系统字节 | 取值范围（signed） |
| :--- | :--- | :--- | :--- |
| `char` | 1 | 1 | -128 ~ 127 |
| `short` | 2 | 2 | -32768 ~ 32767 |
| `int` | 2 | 4 | -2147483648 ~ 2147483647 |
| `long` | 4 | 32位4/64位8 | 与系统位数匹配 |
| `long long` | 8 | 8 | -9223372036854775808 ~ 9223372036854775807 |

- 修饰符：`signed`（有符号，默认）、`unsigned`（无符号，最小值为0，最大值翻倍）；
- `char`默认是否有符号由编译器决定，跨平台场景需显式指定`signed/unsigned`；
- 布尔类型：`_Bool`（C99起），仅能存0（假）/1（真），引入`<stdbool.h>`可使用`bool`、`true`、`false`别名。

#### （2）浮点型
| 类型 | 字节 | 有效精度 | 适用场景 |
| :--- | :--- | :--- | :--- |
| `float` | 4 | 6~7位十进制 | 单精度，对精度要求不高的场景 |
| `double` | 8 | 15~16位十进制 | 双精度，默认浮点类型，通用场景 |
| `long double` | 8/16 | 18~19位十进制 | 高精度计算，编译器相关 |

- 浮点常量默认是`double`类型，加`f/F`后缀指定为`float`，加`l/L`指定为`long double`；
- 浮点运算存在精度误差，禁止直接用`==`判断两个浮点数相等。

#### （3）空类型`void`
- 用于函数返回值：表示函数无返回值；
- 用于函数参数：`int func(void)`表示函数无参数；
- 用于通用指针：`void*`可指向任意类型数据，不可直接解引用。

### 4. 类型转换
1.  **隐式转换（自动类型提升）**：编译器自动完成，规则：
    - 低精度类型向高精度类型转换（避免数据丢失）：`char/short` → `int` → `long` → `long long` → `float` → `double` → `long double`；
    - 无符号类型与有符号类型运算，有符号类型转为无符号类型（极易踩坑）。
2.  **显式转换（强制类型转换）**：手动指定转换类型，语法：`(目标类型)表达式`
    ```c
    double pi = 3.14159;
    int int_pi = (int)pi; // 强制转为int，结果为3，丢失小数部分
    ```
    - 强制转换仅临时改变表达式的类型，不改变原变量的类型与值；
    - 高精度转低精度会丢失数据，需谨慎使用。

---

## 三、运算符与表达式
C语言运算符按功能分为10大类，核心规则：**优先级决定运算顺序，结合性决定同优先级运算方向**。

### 1. 核心运算符分类
| 运算符类型 | 运算符 | 核心说明与注意事项 |
| :--- | :--- | :--- |
| 算术运算符 | `+` `-` `*` `/` `%` `++` `--` | 1. `/`整数除法：两个整数相除，结果舍弃小数（如5/2=2）；<br>2. `%`取模：仅支持整型，结果符号与被除数一致；<br>3. `++/--`前置：先自增/减，再取值；后置：先取值，再自增/减。 |
| 关系运算符 | `>` `<` `>=` `<=` `==` `!=` | 结果为**0（假）**或**非0（真）**；<br>禁止将`==`写成`=`（赋值运算符），极易引发bug。 |
| 逻辑运算符 | `&&` 逻辑与 `||` 逻辑或 `!` 逻辑非 | 1. `&&`：两侧都为真，结果才为真；<br>2. `||`：一侧为真，结果就为真；<br>3. **短路特性**：`&&`左侧为假，右侧不执行；`||`左侧为真，右侧不执行。 |
| 位运算符 | `&` 按位与 `|` 按位或 `^` 按位异或 `~` 按位取反 `<<` 左移 `>>` 右移 | 1. 仅支持整型，对二进制位直接操作；<br>2. 左移`<<`：高位丢弃，低位补0，等价于×2^n；<br>3. 右移`>>`：无符号数补0，有符号数补符号位（编译器相关）。 |
| 赋值运算符 | `=` `+=` `-=` `*=` `/=` `%=` `&=` `|=` `^=` `<<=` `>>=` | 1. 结合性为**从右到左**；<br>2. 复合赋值：`a += 3` 等价于 `a = a + 3`，代码更简洁，编译效率更高。 |
| 条件运算符 | `表达式1 ? 表达式2 : 表达式3` | C语言唯一三目运算符；<br>表达式1为真，执行表达式2；为假执行表达式3。 |
| 逗号运算符 | `,` | 优先级最低；<br>从左到右依次执行，结果为最后一个表达式的值。 |
| 特殊运算符 | `sizeof` `&` `*` `.` `->` `[]` `()` | 1. `sizeof`：**运算符，不是函数**，获取类型/变量占用的字节数；<br>2. `&`：取地址，获取变量的内存地址；<br>3. `*`：解引用，访问指针指向的内存；<br>4. `.`/`->`：结构体成员访问，`->`用于结构体指针。 |

### 2. 优先级与结合性核心口诀
优先级从高到低核心顺序：
**单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值 > 逗号**

- 绝大多数运算符结合性为**从左到右**；
- 例外（从右到左）：单目运算符、三目运算符、赋值运算符。

---

## 四、流程控制语句
C语言支持3种基本程序结构：**顺序结构、选择结构、循环结构**，通过跳转语句实现流程控制。

### 1. 选择结构（分支语句）
#### （1）if-else 语句
```c
// 1. 单分支
if (条件表达式) {
    // 条件为真执行
}

// 2. 双分支
if (条件表达式) {
    // 条件为真执行
} else {
    // 条件为假执行
}

// 3. 多分支
if (条件1) {
    // 条件1为真执行
} else if (条件2) {
    // 条件2为真执行
} else {
    // 所有条件都为假执行
}
```
- 注意：**悬空else问题**：else始终与最近的、未匹配的if绑定，建议所有分支都用`{}`包裹，避免歧义。

#### （2）switch-case 语句
用于多分支等值判断，替代冗长的if-else链。
```c
switch (整型/枚举表达式) {
    case 常量表达式1:
        // 执行语句
        break; // 跳出switch，否则会发生case穿透
    case 常量表达式2:
        // 执行语句
        break;
    default: // 可选，所有case都不匹配时执行
        // 执行语句
        break;
}
```
- 核心规则：
  1.  switch括号内必须是**整型/枚举类型表达式**，不支持浮点型、字符串；
  2.  case后必须是**整型/枚举常量**，不能是变量，且不能重复；
  3.  break用于终止switch，省略会发生**case穿透**（继续执行后续case语句），可利用穿透实现多值匹配同一逻辑。

### 2. 循环结构
#### （1）while 循环
先判断条件，后执行循环体，条件为假时直接跳出，循环体可能一次都不执行。
```c
while (条件表达式) {
    // 循环体
}
```

#### （2）do-while 循环
先执行循环体，后判断条件，**循环体至少执行一次**，适合必须先执行一次的场景。
```c
do {
    // 循环体
} while (条件表达式); // 结尾必须加分号
```

#### （3）for 循环
最常用的循环，适合已知循环次数的场景，结构紧凑。
```c
// 语法：for(初始化表达式; 条件表达式; 更新表达式)
for (int i = 0; i < 10; i++) { // C99支持循环内定义变量
    printf("%d ", i);
}
```
- 三个表达式均可省略：`for(;;)`等价于无限循环（同`while(1)`）；
- 初始化表达式仅在循环开始时执行一次。

### 3. 跳转语句
| 语句 | 作用 | 限制 |
| :--- | :--- | :--- |
| `break` | 跳出**当前一层**循环/switch语句 | 不能跳出多层循环，不能用于if语句（非循环/switch内） |
| `continue` | 结束**本次循环**，跳过后续循环体，直接进入下一次循环条件判断 | 仅能用于循环语句，不能用于switch |
| `return` | 终止当前函数，返回指定值给调用者 | 函数内执行return后，后续代码不再执行 |
| `goto` | 无条件跳转到函数内指定标签位置 | 仅限当前函数内跳转，禁止跨函数；不建议滥用，仅推荐用于多层循环错误处理跳出 |

---

## 五、函数
函数是C语言的核心执行单元，是实现代码封装、复用、模块化的基础。

### 1. 函数的定义与声明
#### （1）函数定义（函数的实现）
语法：
```c
返回值类型 函数名(参数类型1 形参1, 参数类型2 形参2, ...) {
    // 函数体：执行逻辑
    return 返回值; // 无返回值(void)可省略return
}
```
- 示例：两数相加函数
```c
// 定义：返回值类型int，两个int类型参数
int add(int a, int b) {
    return a + b;
}
```
- 规则：
  1.  无返回值时，返回值类型写`void`；
  2.  无参数时，参数列表写`void`（标准写法，避免歧义）；
  3.  函数不能嵌套定义，C语言不支持函数嵌套。

#### （2）函数声明（函数原型）
告诉编译器函数的名称、返回值类型、参数类型，函数定义在调用之后时，必须先声明。
```c
// 函数声明，形参名可省略，仅保留类型即可
int add(int a, int b);
// 等价于 int add(int, int);

int main(void) {
    int res = add(1, 2); // 调用函数
    return 0;
}

// 函数定义
int add(int a, int b) {
    return a + b;
}
```
- 规范：函数声明通常放在头文件中，定义放在源文件中，通过#include引入头文件使用。

### 2. 函数的调用与参数传递
#### （1）函数调用
语法：`函数名(实参1, 实参2, ...);`
- 实参必须与形参的**个数、类型、顺序**匹配；
- 函数调用可以作为表达式、参数、语句使用。

#### （2）参数传递规则
C语言函数参数传递**只有值传递一种方式**，分为两种场景：
1.  **值传递（传值）**：实参将值拷贝给形参，形参是实参的副本，函数内修改形参不会影响原实参。
    ```c
    void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp; // 仅修改副本，原实参不变
    }
    int main() {
        int x=1, y=2;
        swap(x, y); // x、y的值不会交换
        return 0;
    }
    ```
2.  **地址传递（传地址）**：实参将变量的内存地址传递给形参（形参为指针），函数内可通过地址直接修改原实参的值。
    ```c
    void swap(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp; // 通过地址修改原变量
    }
    int main() {
        int x=1, y=2;
        swap(&x, &y); // x、y成功交换
        return 0;
    }
    ```

### 3. 核心函数特性
#### （1）递归函数
函数直接或间接调用自身，称为递归，适合解决具有递推规律、有明确终止条件的问题（如阶乘、斐波那契、树遍历）。
```c
// 递归计算n的阶乘
int factorial(int n) {
    if (n == 0 || n == 1) return 1; // 递归出口：终止条件
    return n * factorial(n-1); // 递推公式
}
```
- 递归必须有**明确的出口**，否则会导致栈溢出；
- 递归优点：代码简洁、逻辑清晰；缺点：函数调用有开销，层级过深会栈溢出。

#### （2）main函数的参数
main函数支持带参数，用于接收命令行传入的参数，标准写法：
```c
int main(int argc, char *argv[]) {
    // argc：命令行参数的个数（包括程序名本身）
    // argv：字符串数组，存储每个参数的内容
    for (int i = 0; i < argc; i++) {
        printf("参数%d: %s\n", i, argv[i]);
    }
    return 0;
}
```

#### （3）函数的作用域
- `extern`：默认属性，函数可被项目内其他源文件调用，需在调用处声明；
- `static`：限制函数仅能在**当前定义的源文件**内调用，无法被其他文件引用，避免命名冲突。

#### （4）内联函数`inline`
C99起支持，用于小型高频调用函数，编译器会将函数体直接嵌入到调用处，减少函数调用的开销。
```c
inline int max(int a, int b) {
    return a > b ? a : b;
}
```
- 仅适用于代码量极小、无循环、无递归的函数；
- inline只是给编译器的建议，编译器可选择忽略。

---

## 六、数组与字符串
### 1. 数组基础
数组是**相同类型数据的有序集合**，在内存中连续存储，通过下标访问元素。

#### （1）一维数组
1.  定义语法：`元素类型 数组名[元素个数];`
    - 元素个数必须是**整型常量表达式**（C99支持变长数组VLA，可用变量指定长度）；
    - 数组名是常量，代表数组首元素的内存地址，不可被赋值。
2.  初始化：
    ```c
    // 1. 完全初始化
    int arr[5] = {1, 2, 3, 4, 5};
    // 2. 部分初始化：未初始化的元素自动赋值为0
    int arr[5] = {1, 2}; // 等价于 {1,2,0,0,0}
    // 3. 省略长度：根据初始化元素个数自动确定数组长度
    int arr[] = {1,2,3,4,5}; // 数组长度为5
    ```
3.  元素访问：`数组名[下标]`
    - 下标从**0**开始，最大下标为`数组长度-1`；
    - 数组越界访问属于**未定义行为**，会导致程序崩溃、数据篡改，必须严格避免。

#### （2）二维数组与多维数组
二维数组本质是“数组的数组”，常用于矩阵、表格类数据。
1.  定义语法：`元素类型 数组名[行数][列数];`
2.  初始化：
    ```c
    // 1. 按行初始化（推荐，可读性高）
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    // 2. 连续初始化
    int arr[2][3] = {1,2,3,4,5,6};
    // 3. 行数可省略，列数必须指定
    int arr[][3] = {{1,2,3}, {4,5,6}};
    ```
3.  访问：`数组名[行下标][列下标]`，内存中按**行优先**连续存储。

#### （3）数组与函数
数组名作为函数参数传递时，会**退化为指向首元素的指针**，函数内无法通过sizeof获取数组总长度，通常需要额外传递数组长度参数。
```c
// 数组传参，等价于 void print_arr(int *arr, int len)
void print_arr(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[5] = {1,2,3,4,5};
    print_arr(arr, 5); // 传递数组名和长度
    return 0;
}
```

### 2. 字符串
C语言没有专门的字符串类型，**字符串本质是以'\0'（空字符，ASCII值0）结尾的字符数组**。

#### （1）字符串的定义与初始化
```c
// 1. 推荐写法：自动在末尾补'\0'
char str1[] = "hello"; // 数组长度为6，包含'\0'
// 等价于 char str1[] = {'h','e','l','l','o','\0'};

// 2. 错误写法：无空间存储'\0'，会导致字符串操作越界
char str2[5] = "hello";

// 3. 字符指针方式：指向字符串常量（只读，不可修改）
char *str3 = "hello";
```
- 核心规则：所有字符串处理函数都依赖`'\0'`判断字符串结束，必须保证字符串以`'\0'`结尾。

#### （2）常用字符串处理函数（`<string.h>`）
| 函数 | 原型 | 功能 | 注意事项 |
| :--- | :--- | :--- | :--- |
| `strlen` | `size_t strlen(const char *s);` | 计算字符串长度，不含末尾的`'\0'` | 与sizeof不同，不计算数组总大小 |
| `strcpy` | `char *strcpy(char *dest, const char *src);` | 将src字符串拷贝到dest，包括`'\0'` | 必须保证dest缓冲区足够大，否则溢出 |
| `strncpy` | `char *strncpy(char *dest, const char *src, size_t n);` | 最多拷贝n个字符，安全版本 | 不会自动补`'\0'`，需手动处理 |
| `strcat` | `char *strcat(char *dest, const char *src);` | 将src字符串拼接到dest末尾 | dest必须有足够的剩余空间 |
| `strcmp` | `int strcmp(const char *s1, const char *s2);` | 逐字符比较两个字符串：<br>s1>s2返回正数，相等返回0，s1<s2返回负数 | 比较的是ASCII值，不是字符串长度 |
| `strchr` | `char *strchr(const char *s, int c);` | 查找字符c在字符串s中第一次出现的位置 | 找到返回地址，找不到返回NULL |
| `strstr` | `char *strstr(const char *haystack, const char *needle);` | 查找子串needle在haystack中第一次出现的位置 | 找到返回地址，找不到返回NULL |

---

## 七、指针（C语言核心灵魂）
指针是存储**内存地址**的变量，是C语言直接操作内存的核心，也是C语言高效的关键。

### 1. 指针基础
#### （1）指针的定义与初始化
- 定义语法：`指向的类型 *指针变量名;`
- 核心规则：指针的类型决定了指针解引用时访问的内存大小，以及指针算术运算的偏移步长。
```c
int a = 10;
// 定义int类型指针p，指向变量a的地址
int *p = &a; // &：取地址运算符，获取变量a的内存地址
```

#### （2）核心操作：取地址& 与 解引用*
```c
int a = 10;
int *p = &a;

*p = 20; // *：解引用运算符，访问指针指向的内存，等价于 a=20
printf("%d", a); // 输出20
```

#### （3）空指针与野指针
1.  **空指针`NULL`**：定义在`<stddef.h>`等头文件，本质是`(void*)0`，指向无效的内存地址0。
    - 作用：初始化指针、判断指针是否有效，避免野指针；
    - 规则：空指针不可解引用，否则会导致程序崩溃。
    ```c
    int *p = NULL; // 初始化空指针
    if (p != NULL) { // 使用前判空，安全规范
        *p = 10;
    }
    ```
2.  **野指针**：指向非法、无效内存的指针，是C语言最常见的bug来源。
    - 产生原因：指针未初始化、free释放后未置空、指针越界、指向已销毁的局部变量；
    - 危害：未定义行为，可能导致程序崩溃、数据篡改、系统异常；
    - 规避：指针必须初始化，释放后立即置空，使用前判空，避免越界访问。

### 2. 指针的算术运算
指针仅支持有限的算术运算，核心是**按指向类型的大小进行地址偏移**。
1.  **指针 ± 整数**：地址偏移 `整数 × sizeof(指向类型)` 字节
    ```c
    int arr[5] = {1,2,3,4,5};
    int *p = arr; // p指向数组首元素arr[0]
    printf("%d", *(p+2)); // 偏移2个int，访问arr[2]，输出3
    ```
2.  **指针++/--**：向前/向后偏移一个指向类型的大小，常用于数组遍历。
3.  **指针 - 指针**：两个同类型指针指向同一块连续内存时，差值为两个指针之间的**元素个数**。
4.  **关系运算**：同类型指针可比较地址大小，仅同一块连续内存内有效。

### 3. 指针与数组的深度关联
- 数组名是数组首元素的常量地址，`arr == &arr[0]`；
- 数组元素访问的本质：`arr[i] == *(arr + i) == *(p + i) == p[i]`，四种写法完全等价。
```c
int arr[5] = {1,2,3,4,5};
int *p = arr;
// 四种方式遍历数组，效果完全一致
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
    printf("%d ", *(arr+i));
    printf("%d ", p[i]);
    printf("%d ", *(p+i));
}
```

### 4. 指针进阶用法
#### （1）const与指针的组合（3种核心场景）
| 写法 | 含义 | 可修改项 | 不可修改项 |
| :--- | :--- | :--- | :--- |
| `const int *p` / `int const *p` | 指向常量的指针 | 指针p本身的指向 | 指针指向的内容 |
| `int *const p` | 指针常量 | 指针指向的内容 | 指针p本身的指向 |
| `const int *const p` | 指向常量的指针常量 | 无 | 指向和内容都不可修改 |

#### （2）二级指针（指向指针的指针）
存储一级指针的地址，用于修改一级指针的指向，常用于指针数组、函数内修改入参指针。
```c
int a = 10;
int *p = &a;  // 一级指针
int **pp = &p; // 二级指针，存储一级指针p的地址

**pp = 20; // 等价于 *p=20，等价于 a=20
```

#### （3）指针数组 vs 数组指针
1.  **指针数组**：数组，每个元素都是同类型的指针，常用于字符串数组。
    ```c
    // 定义：存储3个char*类型指针的数组
    char *str_arr[3] = {"apple", "banana", "orange"};
    ```
2.  **数组指针**：指针，指向一个完整的数组，常用于二维数组。
    ```c
    // 定义：指向包含3个int元素的一维数组的指针
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    int (*p)[3] = arr; // p指向二维数组的第一行
    ```

#### （4）函数指针
指向函数的指针，存储函数的入口地址（函数名就是函数的入口地址），常用于回调函数、函数跳转表。
1.  定义语法：`返回值类型 (*指针名)(参数类型列表);`
2.  示例：
    ```c
    // 定义一个加法函数
    int add(int a, int b) {
        return a + b;
    }

    int main() {
        // 定义函数指针p，指向参数为(int,int)、返回值为int的函数
        int (*p)(int, int) = add;
        // 通过函数指针调用函数，两种写法等价
        int res1 = p(1, 2);
        int res2 = (*p)(3, 4);
        return 0;
    }
    ```

#### （5）void* 通用指针
- 可指向任意类型的内存地址，称为“无类型指针”；
- 不可直接解引用，必须强制转换为具体类型后才能访问内存；
- 常用于函数通用参数、动态内存分配（malloc返回值为void*）。

---

## 八、构造数据类型
### 1. 结构体`struct`
结构体用于将多个不同类型的变量组合成一个自定义类型，适合描述复杂对象（如学生、员工）。

#### （1）结构体的定义与变量声明
```c
// 1. 定义结构体类型
struct Student {
    // 结构体成员
    int id;
    char name[20];
    int age;
    float score;
}; // 结尾必须加分号

// 2. 声明结构体变量
struct Student stu1;
// 3. 定义类型同时声明变量
struct Student {
    int id;
    char name[20];
} stu2, stu3;
```

#### （2）结构体变量的初始化
```c
// 1. 顺序初始化
struct Student stu1 = {1001, "张三", 18, 90.5};
// 2. 指定初始化（C99起，推荐，可读性高）
struct Student stu2 = {
    .id = 1002,
    .name = "李四",
    .age = 19,
    .score = 88.0
};
```

#### （3）结构体成员访问
- 普通结构体变量：用`.`运算符
- 结构体指针：用`->`运算符
```c
struct Student stu = {1001, "张三", 18, 90.5};
struct Student *p = &stu;

// 访问成员，两种写法等价
stu.age = 20;
p->score = 95.0;
```

#### （4）结构体的内存对齐
结构体的内存大小不是成员大小的简单相加，而是遵循**内存对齐规则**，目的是提高CPU内存访问效率。
核心对齐规则：
1.  结构体成员的偏移量，必须是成员自身大小的整数倍；
2.  结构体总大小，必须是最大基本成员大小的整数倍；
3.  可通过`#pragma pack(n)`修改默认对齐系数。
```c
struct Test {
    char a;  // 1字节
    int b;   // 4字节
    short c; // 2字节
};
// 该结构体大小为12字节，而非1+4+2=7字节，因内存对齐填充了5字节
```

#### （5）位段（位域）
结构体中可按二进制位定义成员，用于节省内存，常用于硬件编程、协议封装。
```c
struct Flag {
    unsigned int enable:1;  // 占1位，取值0/1
    unsigned int status:2;  // 占2位，取值0~3
    unsigned int reserve:5; // 占5位
};
// 该结构体总大小为4字节（1个int），仅用了8位
```

### 2. 联合体`union`
联合体也叫共用体，所有成员共享同一块内存空间，同一时间只有一个成员有效。
```c
// 定义联合体
union Data {
    int a;
    char b;
    float c;
};
```
- 核心特性：
  1.  联合体的大小等于**最大成员的大小**；
  2.  修改一个成员，会覆盖其他成员的值；
  3.  典型用途：内存复用、判断系统大小端模式。

### 3. 枚举`enum`
枚举用于定义一组命名的整型常量，提高代码可读性，限定变量的取值范围。
```c
// 定义枚举类型
enum Week {
    Mon, // 默认0，后续依次+1
    Tue, // 1
    Wed, // 2
    Thu=10, // 手动赋值为10
    Fri, // 11
    Sat, // 12
    Sun  // 13
};

// 声明枚举变量
enum Week today = Mon;
```
- 核心规则：
  1.  枚举常量本质是`int`类型，默认从0开始，手动赋值后后续常量依次+1；
  2.  枚举变量可赋值为任意整型值，但不推荐，失去枚举的意义。

### 4. `typedef` 类型别名
用于给已有的数据类型起别名，简化复杂类型，提高代码可移植性。
```c
// 1. 给基本类型起别名
typedef unsigned int uint;
uint a = 10; // 等价于 unsigned int a=10;

// 2. 给结构体起别名（最常用）
typedef struct {
    int id;
    char name[20];
} Student;
Student stu; // 无需写struct关键字

// 3. 给复杂指针类型起别名
typedef int (*FuncPtr)(int, int);
FuncPtr p; // 等价于 int (*p)(int,int);
```
- 与`#define`的核心区别：`typedef`是编译器处理的类型别名，支持类型检查；`#define`是预处理器的文本替换，无类型检查。

---

## 九、预处理指令
预处理是编译前的处理步骤，所有预处理指令都以`#`开头，独占一行，无需分号结尾。

### 1. 宏定义`#define`
#### （1）无参宏
用于定义常量、代码别名，预编译时直接文本替换。
```c
#define PI 3.1415926  // 定义圆周率常量
#define MAX_SIZE 1024  // 定义数组最大长度
```

#### （2）带参宏
类似函数，预编译时进行参数替换，无函数调用开销。
```c
// 定义求最大值的带参宏，必须给每个参数和整体加括号，避免优先级问题
#define MAX(a,b) ((a) > (b) ? (a) : (b))

// 使用
int res = MAX(1, 2); // 预编译替换为 ((1)>(2)?(1):(2))
```
- 核心注意事项：
  1.  宏参数和整体必须加括号，避免运算符优先级导致的bug；
  2.  宏无类型检查，不安全；
  3.  带参宏会产生代码膨胀，且有副作用（如`MAX(a++, b++)`会导致变量自增两次）；
  4.  宏定义末尾不能加分号，否则会被一起替换。

#### （3）宏的特殊运算符
- `#`：字符串化，将宏参数转为字符串常量；
- `##`：标记连接，将两个标记拼接为一个标记。
```c
#define STR(s) #s
#define CAT(a,b) a##b

printf("%s", STR(hello)); // 替换为 printf("%s", "hello");
int num123 = 10;
printf("%d", CAT(num, 123)); // 替换为 printf("%d", num123);
```

#### （4）宏的取消`#undef`
用于取消已定义的宏，限定宏的作用范围。
```c
#define PI 3.14
#undef PI // 取消PI的定义，后续无法使用
```

### 2. 头文件包含`#include`
用于将指定头文件的内容插入到当前位置，分为两种格式：
1.  `#include <头文件.h>`：用于系统标准头文件，直接搜索系统库路径；
2.  `#include "头文件.h"`：用于自定义头文件，先搜索当前项目目录，再搜索系统路径。

#### 头文件重复包含防护
防止头文件被多次包含导致重复定义，两种实现方式：
1.  条件编译防护（跨编译器兼容，推荐）
    ```c
    #ifndef __STUDENT_H__
    #define __STUDENT_H__

    // 头文件核心内容

    #endif
    ```
2.  `#pragma once`：编译器特定指令，代码简洁，主流编译器均支持。

### 3. 条件编译
根据指定条件，决定是否编译某段代码，常用于跨平台兼容、调试开关、功能裁剪。
```c
// 1. 判断宏是否定义
#ifdef DEBUG
    printf("调试模式\n"); // 定义了DEBUG宏才会编译
#endif

// 2. 判断宏是否未定义
#ifndef RELEASE
    // 未定义RELEASE宏才会编译
#endif

// 3. 多条件判断
#if OS == WINDOWS
    #include <windows.h>
#elif OS == LINUX
    #include <linux.h>
#else
    #error 不支持的操作系统
#endif
```

### 4. 其他预处理指令
- `#error`：编译时输出错误信息，终止编译，用于条件判断不满足的场景；
- `#line`：修改编译器的行号和文件名，用于调试；
- `#pragma`：编译器特定指令，如`#pragma pack`设置内存对齐、`#pragma once`防止头文件重复包含。

### 5. 标准预定义宏
C标准内置的宏，可直接使用，常用于调试、日志：
- `__FILE__`：当前源文件的文件名（字符串）
- `__LINE__`：当前代码的行号（整型）
- `__DATE__`：程序编译日期（字符串）
- `__TIME__`：程序编译时间（字符串）
- `__STDC__`：编译器符合C标准时，值为1

---

## 十、存储类、作用域与生命周期
### 1. 核心概念
- **作用域**：标识符（变量、函数）可被访问的代码范围；
- **生命周期**：变量占用内存的时间周期，从分配到释放；
- **存储类**：决定变量的存储位置、生命周期、作用域。

### 2. 四大存储类说明符
| 存储类 | 存储位置 | 生命周期 | 作用域 | 用法与特性 |
| :--- | :--- | :--- | :--- | :--- |
| `auto` | 栈区 | 自动存储期：进入代码块分配，离开释放 | 代码块作用域 | 局部变量默认存储类，可省略，仅能用于局部变量 |
| `register` | 寄存器（建议） | 同auto | 代码块作用域 | 建议编译器将变量存入寄存器，提高访问速度，不可取地址，现代编译器自动优化，极少手动使用 |
| `static` | 全局/静态区 | 静态存储期：程序启动分配，程序结束释放 | 1. 局部静态变量：代码块作用域<br>2. 全局静态变量/函数：文件作用域 | 1. 局部静态变量：仅初始化一次，函数调用结束后值保留；<br>2. 全局静态变量/函数：仅当前源文件可访问，无法被其他文件extern引用 |
| `extern` | 全局/静态区 | 静态存储期 | 文件作用域 | 声明外部变量/函数，用于跨文件引用，变量/函数定义在其他源文件中 |

### 3. 作用域分类
1.  **代码块作用域**：局部变量、for循环内定义的变量，仅在`{}`代码块内可访问；
2.  **函数作用域**：goto标签，在整个函数内可访问；
3.  **文件作用域**：全局变量、函数，在整个源文件内可访问；
4.  **函数原型作用域**：函数声明的参数列表，仅在声明内有效。

### 4. 变量分类对比
| 变量类型 | 定义位置 | 存储类 | 生命周期 | 作用域 |
| :--- | :--- | :--- | :--- | :--- |
| 局部变量 | 函数/代码块内 | auto（默认） | 进入代码块创建，离开销毁 | 代码块内 |
| 局部静态变量 | 函数/代码块内 | static | 程序启动创建，程序结束销毁 | 代码块内 |
| 全局变量 | 函数外 | 无（默认extern） | 程序启动创建，程序结束销毁 | 整个项目所有源文件 |
| 全局静态变量 | 函数外 | static | 程序启动创建，程序结束销毁 | 当前源文件内 |

---

## 十一、动态内存管理
C语言提供了手动管理堆内存的能力，可灵活分配/释放内存，避免栈内存大小固定的限制。

### 1. C语言内存分区
| 内存分区 | 存储内容 | 生命周期 | 管理方式 |
| :--- | :--- | :--- | :--- |
| 代码区（Text） | 程序的二进制可执行代码 | 程序运行期间 | 系统自动管理，只读，共享 |
| 常量区（Rodata） | 字符串常量、const全局常量 | 程序运行期间 | 系统自动管理，只读 |
| 全局/静态区 | 初始化的全局/静态变量（Data段）、未初始化的全局/静态变量（BSS段） | 程序启动分配，程序结束释放 | 系统自动管理 |
| 栈区（Stack） | 局部变量、函数参数、返回地址 | 进入代码块分配，离开释放 | 系统自动管理，大小固定，溢出会栈溢出 |
| 堆区（Heap） | 动态分配的内存 | 手动分配，手动释放，否则程序结束才回收 | 程序员手动管理，大小灵活，易产生内存泄漏、内存碎片 |

### 2. 动态内存管理函数（`<stdlib.h>`）
#### （1）`malloc`：内存分配
```c
void *malloc(size_t size);
```
- 功能：分配`size`字节的连续堆内存，内存内容未初始化（随机值）；
- 返回值：成功返回内存首地址，失败返回`NULL`；
- 示例：
```c
// 分配10个int类型的内存空间
int *p = (int *)malloc(10 * sizeof(int));
// 必须判空，避免空指针解引用
if (p == NULL) {
    perror("malloc failed");
    return -1;
}
```

#### （2）`calloc`：初始化内存分配
```c
void *calloc(size_t num, size_t size);
```
- 功能：分配`num`个`size`字节的连续堆内存，**所有字节自动初始化为0**；
- 返回值：成功返回内存首地址，失败返回`NULL`；
- 适合需要初始化的数组场景。

#### （3）`realloc`：内存大小调整
```c
void *realloc(void *ptr, size_t new_size);
```
- 功能：调整已分配内存的大小，可扩容/缩容；
- 规则：
  1.  若原内存后有足够空间，直接扩容，返回原地址；
  2.  若空间不足，重新分配新内存，拷贝原数据，释放原内存，返回新地址；
  3.  ptr为NULL时，等价于malloc；new_size为0时，等价于free；
- 返回值：成功返回新地址，失败返回`NULL`，原内存保持不变。

#### （4）`free`：内存释放
```c
void free(void *ptr);
```
- 功能：释放ptr指向的动态分配的堆内存；
- 核心规则：
  1.  ptr为`NULL`时，函数无任何操作；
  2.  只能释放malloc/calloc/realloc分配的堆内存，不能释放栈内存、常量区地址；
  3.  同一块内存不能重复释放，否则会导致未定义行为；
  4.  内存释放后，ptr变为野指针，建议立即置为`NULL`。
```c
free(p);
p = NULL; // 释放后置空，避免野指针
```

### 3. 常见问题与规避方案
| 问题 | 产生原因 | 规避方案 |
| :--- | :--- | :--- |
| 内存泄漏 | malloc后未free，堆内存无法回收 | 谁分配谁释放，配对使用malloc/free，避免指针丢失 |
| 野指针 | 指针未初始化、free后未置空、指向已销毁的局部变量 | 指针必须初始化，free后立即置空，使用前判空 |
| 内存越界 | 访问超出分配的内存范围 | 严格控制访问边界，避免数组/指针越界 |
| 重复释放 | 同一块堆内存free多次 | free后立即置空，free前可判空 |
| 空指针解引用 | malloc失败返回NULL，未判空直接使用 | 动态内存分配后，必须检查返回值是否为NULL |

---

## 十二、文件操作
C语言将文件视为**字节流**，通过`FILE`指针实现文件的打开、读写、关闭等操作，相关函数定义在`<stdio.h>`。

### 1. 文件的打开与关闭
#### （1）`fopen`：打开文件
```c
FILE *fopen(const char *filename, const char *mode);
```
- 功能：打开指定文件，创建文件流；
- 参数：
  - `filename`：文件路径+文件名；
  - `mode`：文件打开模式，决定文件的读写权限；
- 返回值：成功返回`FILE*`类型的文件指针，失败返回`NULL`，必须判空。

#### （2）核心打开模式
| 模式 | 含义 | 文件不存在 | 文件存在 | 读写限制 |
| :--- | :--- | :--- | :--- | :--- |
| `r` | 只读（文本模式） | 打开失败 | 正常打开，从头读 | 只能读，不能写 |
| `w` | 只写（文本模式） | 创建新文件 | 清空原有内容，从头写 | 只能写，不能读 |
| `a` | 追加（文本模式） | 创建新文件 | 末尾追加内容，不覆盖原有内容 | 只能写，不能读 |
| `r+` | 读写（文本模式） | 打开失败 | 正常打开，可读写 | 可读可写，不清空文件 |
| `w+` | 读写（文本模式） | 创建新文件 | 清空原有内容 | 可读可写 |
| `a+` | 读写追加（文本模式） | 创建新文件 | 末尾追加，读从开头 | 可读可写，写只能追加 |
| `rb/wb/ab/rb+/wb+/ab+` | 二进制模式 | 与对应文本模式一致 | 与对应文本模式一致 | 不处理换行符转换，直接操作二进制字节 |

#### （3）`fclose`：关闭文件
```c
int fclose(FILE *stream);
```
- 功能：关闭文件，刷新缓冲区，释放文件资源；
- 返回值：成功返回0，失败返回`EOF`；
- 规范：文件打开后，使用完毕必须关闭，否则会导致资源泄漏、数据丢失。

### 2. 文件读写函数
#### （1）字符读写（单字节）
- `int fgetc(FILE *stream)`：从文件中读取一个字符，成功返回读取的字符，到达文件末尾/出错返回`EOF`；
- `int fputc(int c, FILE *stream)`：向文件写入一个字符，成功返回写入的字符，失败返回`EOF`。

#### （2）字符串读写
- `char *fgets(char *str, int n, FILE *stream)`：从文件读取最多`n-1`个字符到str，遇到换行符/EOF结束，自动在末尾补`'\0'`，成功返回str，失败/EOF返回`NULL`；
- `int fputs(const char *str, FILE *stream)`：向文件写入字符串，成功返回非负数，失败返回`EOF`。

#### （3）格式化读写
与`printf/scanf`用法一致，只是读写目标是文件，常用于文本文件的格式化读写。
- `int fscanf(FILE *stream, const char *format, ...)`：从文件格式化读取数据；
- `int fprintf(FILE *stream, const char *format, ...)`：向文件格式化写入数据。

#### （4）块读写（二进制文件专用）
直接操作内存块，读写效率高，无格式转换，常用于二进制文件（图片、视频、结构体数据）。
```c
// 读文件
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
// 写文件
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
```
- 参数：
  - `ptr`：内存缓冲区地址，存储读取/待写入的数据；
  - `size`：每个数据块的字节大小；
  - `count`：要读写的数据块个数；
- 返回值：成功读写的完整数据块个数，小于count表示到达文件末尾/出错。

### 3. 文件指针定位
控制文件读写指针的位置，实现随机读写。
1.  `rewind(FILE *stream)`：将文件指针移动到文件开头；
2.  `ftell(FILE *stream)`：返回当前文件指针的位置，失败返回`-1L`；
3.  `fseek(FILE *stream, long offset, int origin)`：移动文件指针到指定位置
    - `offset`：偏移字节数，正数向后偏移，负数向前偏移；
    - `origin`：偏移基准点，可选：
      - `SEEK_SET`：文件开头；
      - `SEEK_CUR`：当前位置；
      - `SEEK_END`：文件结尾。

### 4. 文件状态判断
1.  `int feof(FILE *stream)`：判断是否到达文件末尾，到达返回非0，否则返回0；
    - 注意：必须执行读操作失败后，EOF标志才会被设置，不能提前用feof判断文件结束。
2.  `int ferror(FILE *stream)`：判断文件操作是否出错，出错返回非0，否则返回0；
3.  `void clearerr(FILE *stream)`：清除文件的EOF标志和错误标志。

---

## 十三、错误处理与常用标准库
### 1. 错误处理机制
#### （1）`errno`全局变量
定义在`<errno.h>`，用于记录系统调用/库函数的错误码，函数执行出错时会设置该值，执行成功不会修改，使用前需先清零。

#### （2）`perror`函数
定义在`<stdio.h>`，输出自定义字符串 + 冒号 + 当前`errno`对应的错误描述。
```c
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
    perror("fopen failed"); // 输出：fopen failed: No such file or directory
    return -1;
}
```

#### （3）`strerror`函数
定义在`<string.h>`，返回错误码对应的错误描述字符串。
```c
#include <string.h>
#include <errno.h>

FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
    printf("fopen failed: %s\n", strerror(errno));
    return -1;
}
```

#### （4）`assert`断言
定义在`<assert.h>`，用于调试阶段的错误检查，表达式为假时，终止程序并输出错误信息。
```c
#include <assert.h>

int divide(int a, int b) {
    assert(b != 0); // 断言b不为0，为0时程序崩溃
    return a / b;
}
```
- 发布版本中，定义`NDEBUG`宏可禁用所有断言，不影响程序性能。

### 2. 常用标准库头文件
| 头文件 | 核心功能 | 常用函数/宏 |
| :--- | :--- | :--- |
| `<stdio.h>` | 标准输入输出 | `printf` `scanf` `fopen` `fclose` `fgets` `fputs` `perror` |
| `<stdlib.h>` | 通用工具函数 | `malloc` `calloc` `realloc` `free` `atoi` `rand` `srand` `exit` |
| `<string.h>` | 字符串与内存操作 | `strlen` `strcpy` `strcmp` `strcat` `memset` `memcpy` `strerror` |
| `<math.h>` | 数学函数 | `sqrt` `pow` `fabs` `sin` `cos` `tan` `log`，gcc编译需加`-lm` |
| `<time.h>` | 时间日期处理 | `time` `localtime` `strftime` `difftime` |
| `<ctype.h>` | 字符判断与转换 | `isdigit` `isalpha` `isspace` `toupper` `tolower` |
| `<stdbool.h>` | 布尔类型支持 | `bool` `true` `false` |
| `<stdarg.h>` | 可变参数处理 | `va_list` `va_start` `va_arg` `va_end` |
| `<errno.h>` | 错误码处理 | `errno` |
| `<assert.h>` | 断言调试 | `assert` |


