目录

C 语法详细总结

目录

C语言语法详细总结(基于C11标准,工业通用版)

本文全面覆盖C语言核心语法,从基础入门到进阶核心,兼顾语法规则、用法示例、标准规范与常见坑点,适配C99/C11/C17主流标准。


一、C语言程序基础结构与规范

1. 最小程序框架

C语言程序由函数、变量、预处理指令组成,main函数是程序唯一执行入口

#include <stdio.h>  // 预处理:头文件包含
int main(void) {    // 主函数,程序入口
    printf("Hello World!\n");  // 库函数:标准输出
    return 0;       // 函数返回值,0表示程序正常结束
}

2. 核心规范

  1. 语句规则:所有执行语句必须以英文分号;结尾,代码块用{}包裹,支持嵌套。
  2. 大小写敏感Aa是完全不同的标识符。
  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>可使用booltruefalse别名。

(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/shortintlonglong longfloatdoublelong double
    • 无符号类型与有符号类型运算,有符号类型转为无符号类型(极易踩坑)。
  2. 显式转换(强制类型转换):手动指定转换类型,语法:(目标类型)表达式
    double pi = 3.14159;
    int int_pi = (int)pi; // 强制转为int,结果为3,丢失小数部分
    
    • 强制转换仅临时改变表达式的类型,不改变原变量的类型与值;
    • 高精度转低精度会丢失数据,需谨慎使用。

三、运算符与表达式

C语言运算符按功能分为10大类,核心规则:优先级决定运算顺序,结合性决定同优先级运算方向

1. 核心运算符分类

运算符类型 运算符 核心说明与注意事项
算术运算符 + - * / % ++ -- 1. /整数除法:两个整数相除,结果舍弃小数(如5/2=2);
2. %取模:仅支持整型,结果符号与被除数一致;
3. ++/--前置:先自增/减,再取值;后置:先取值,再自增/减。
关系运算符 > < >= <= == != 结果为0(假)非0(真)
禁止将==写成=(赋值运算符),极易引发bug。
逻辑运算符 && 逻辑与 `
位运算符 & 按位与 ` 按位或^按位异或~按位取反«左移»` 右移
赋值运算符 = += -= *= /= %= &= ` = ^= «= »=`
条件运算符 表达式1 ? 表达式2 : 表达式3 C语言唯一三目运算符;
表达式1为真,执行表达式2;为假执行表达式3。
逗号运算符 , 优先级最低;
从左到右依次执行,结果为最后一个表达式的值。
特殊运算符 sizeof & * . -> [] () 1. sizeof运算符,不是函数,获取类型/变量占用的字节数;
2. &:取地址,获取变量的内存地址;
3. *:解引用,访问指针指向的内存;
4. ./->:结构体成员访问,->用于结构体指针。

2. 优先级与结合性核心口诀

优先级从高到低核心顺序: 单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值 > 逗号

  • 绝大多数运算符结合性为从左到右
  • 例外(从右到左):单目运算符、三目运算符、赋值运算符。

四、流程控制语句

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

1. 选择结构(分支语句)

(1)if-else 语句

// 1. 单分支
if (条件表达式) {
    // 条件为真执行
}

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

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

(2)switch-case 语句

用于多分支等值判断,替代冗长的if-else链。

switch (整型/枚举表达式) {
    case 常量表达式1:
        // 执行语句
        break; // 跳出switch,否则会发生case穿透
    case 常量表达式2:
        // 执行语句
        break;
    default: // 可选,所有case都不匹配时执行
        // 执行语句
        break;
}
  • 核心规则:
    1. switch括号内必须是整型/枚举类型表达式,不支持浮点型、字符串;
    2. case后必须是整型/枚举常量,不能是变量,且不能重复;
    3. break用于终止switch,省略会发生case穿透(继续执行后续case语句),可利用穿透实现多值匹配同一逻辑。

2. 循环结构

(1)while 循环

先判断条件,后执行循环体,条件为假时直接跳出,循环体可能一次都不执行。

while (条件表达式) {
    // 循环体
}

(2)do-while 循环

先执行循环体,后判断条件,循环体至少执行一次,适合必须先执行一次的场景。

do {
    // 循环体
} while (条件表达式); // 结尾必须加分号

(3)for 循环

最常用的循环,适合已知循环次数的场景,结构紧凑。

// 语法: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)函数定义(函数的实现)

语法:

返回值类型 函数名(参数类型1 形参1, 参数类型2 形参2, ...) {
    // 函数体:执行逻辑
    return 返回值; // 无返回值(void)可省略return
}
  • 示例:两数相加函数
// 定义:返回值类型int,两个int类型参数
int add(int a, int b) {
    return a + b;
}
  • 规则:
    1. 无返回值时,返回值类型写void
    2. 无参数时,参数列表写void(标准写法,避免歧义);
    3. 函数不能嵌套定义,C语言不支持函数嵌套。

(2)函数声明(函数原型)

告诉编译器函数的名称、返回值类型、参数类型,函数定义在调用之后时,必须先声明。

// 函数声明,形参名可省略,仅保留类型即可
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. 值传递(传值):实参将值拷贝给形参,形参是实参的副本,函数内修改形参不会影响原实参。
    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. 地址传递(传地址):实参将变量的内存地址传递给形参(形参为指针),函数内可通过地址直接修改原实参的值。
    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)递归函数

函数直接或间接调用自身,称为递归,适合解决具有递推规律、有明确终止条件的问题(如阶乘、斐波那契、树遍历)。

// 递归计算n的阶乘
int factorial(int n) {
    if (n == 0 || n == 1) return 1; // 递归出口:终止条件
    return n * factorial(n-1); // 递推公式
}
  • 递归必须有明确的出口,否则会导致栈溢出;
  • 递归优点:代码简洁、逻辑清晰;缺点:函数调用有开销,层级过深会栈溢出。

(2)main函数的参数

main函数支持带参数,用于接收命令行传入的参数,标准写法:

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起支持,用于小型高频调用函数,编译器会将函数体直接嵌入到调用处,减少函数调用的开销。

inline int max(int a, int b) {
    return a > b ? a : b;
}
  • 仅适用于代码量极小、无循环、无递归的函数;
  • inline只是给编译器的建议,编译器可选择忽略。

六、数组与字符串

1. 数组基础

数组是相同类型数据的有序集合,在内存中连续存储,通过下标访问元素。

(1)一维数组

  1. 定义语法:元素类型 数组名[元素个数];
    • 元素个数必须是整型常量表达式(C99支持变长数组VLA,可用变量指定长度);
    • 数组名是常量,代表数组首元素的内存地址,不可被赋值。
  2. 初始化:
    // 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. 初始化:
    // 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获取数组总长度,通常需要额外传递数组长度参数。

// 数组传参,等价于 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)字符串的定义与初始化

// 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); 逐字符比较两个字符串:
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)指针的定义与初始化

  • 定义语法:指向的类型 *指针变量名;
  • 核心规则:指针的类型决定了指针解引用时访问的内存大小,以及指针算术运算的偏移步长。
int a = 10;
// 定义int类型指针p,指向变量a的地址
int *p = &a; // &:取地址运算符,获取变量a的内存地址

(2)核心操作:取地址& 与 解引用*

int a = 10;
int *p = &a;

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

(3)空指针与野指针

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

2. 指针的算术运算

指针仅支持有限的算术运算,核心是按指向类型的大小进行地址偏移

  1. 指针 ± 整数:地址偏移 整数 × sizeof(指向类型) 字节
    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],四种写法完全等价。
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)二级指针(指向指针的指针)

存储一级指针的地址,用于修改一级指针的指向,常用于指针数组、函数内修改入参指针。

int a = 10;
int *p = &a;  // 一级指针
int **pp = &p; // 二级指针,存储一级指针p的地址

**pp = 20; // 等价于 *p=20,等价于 a=20

(3)指针数组 vs 数组指针

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

(4)函数指针

指向函数的指针,存储函数的入口地址(函数名就是函数的入口地址),常用于回调函数、函数跳转表。

  1. 定义语法:返回值类型 (*指针名)(参数类型列表);
  2. 示例:
    // 定义一个加法函数
    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)结构体的定义与变量声明

// 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)结构体变量的初始化

// 1. 顺序初始化
struct Student stu1 = {1001, "张三", 18, 90.5};
// 2. 指定初始化(C99起,推荐,可读性高)
struct Student stu2 = {
    .id = 1002,
    .name = "李四",
    .age = 19,
    .score = 88.0
};

(3)结构体成员访问

  • 普通结构体变量:用.运算符
  • 结构体指针:用->运算符
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)修改默认对齐系数。
struct Test {
    char a;  // 1字节
    int b;   // 4字节
    short c; // 2字节
};
// 该结构体大小为12字节,而非1+4+2=7字节,因内存对齐填充了5字节

(5)位段(位域)

结构体中可按二进制位定义成员,用于节省内存,常用于硬件编程、协议封装。

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

联合体也叫共用体,所有成员共享同一块内存空间,同一时间只有一个成员有效。

// 定义联合体
union Data {
    int a;
    char b;
    float c;
};
  • 核心特性:
    1. 联合体的大小等于最大成员的大小
    2. 修改一个成员,会覆盖其他成员的值;
    3. 典型用途:内存复用、判断系统大小端模式。

3. 枚举enum

枚举用于定义一组命名的整型常量,提高代码可读性,限定变量的取值范围。

// 定义枚举类型
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 类型别名

用于给已有的数据类型起别名,简化复杂类型,提高代码可移植性。

// 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)无参宏

用于定义常量、代码别名,预编译时直接文本替换。

#define PI 3.1415926  // 定义圆周率常量
#define MAX_SIZE 1024  // 定义数组最大长度

(2)带参宏

类似函数,预编译时进行参数替换,无函数调用开销。

// 定义求最大值的带参宏,必须给每个参数和整体加括号,避免优先级问题
#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)宏的特殊运算符

  • #:字符串化,将宏参数转为字符串常量;
  • ##:标记连接,将两个标记拼接为一个标记。
#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

用于取消已定义的宏,限定宏的作用范围。

#define PI 3.14
#undef PI // 取消PI的定义,后续无法使用

2. 头文件包含#include

用于将指定头文件的内容插入到当前位置,分为两种格式:

  1. #include <头文件.h>:用于系统标准头文件,直接搜索系统库路径;
  2. #include "头文件.h":用于自定义头文件,先搜索当前项目目录,再搜索系统路径。

头文件重复包含防护

防止头文件被多次包含导致重复定义,两种实现方式:

  1. 条件编译防护(跨编译器兼容,推荐)
    #ifndef __STUDENT_H__
    #define __STUDENT_H__
    
    // 头文件核心内容
    
    #endif
  2. #pragma once:编译器特定指令,代码简洁,主流编译器均支持。

3. 条件编译

根据指定条件,决定是否编译某段代码,常用于跨平台兼容、调试开关、功能裁剪。

// 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. 局部静态变量:代码块作用域
2. 全局静态变量/函数:文件作用域
1. 局部静态变量:仅初始化一次,函数调用结束后值保留;
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:内存分配

void *malloc(size_t size);
  • 功能:分配size字节的连续堆内存,内存内容未初始化(随机值);
  • 返回值:成功返回内存首地址,失败返回NULL
  • 示例:
// 分配10个int类型的内存空间
int *p = (int *)malloc(10 * sizeof(int));
// 必须判空,避免空指针解引用
if (p == NULL) {
    perror("malloc failed");
    return -1;
}

(2)calloc:初始化内存分配

void *calloc(size_t num, size_t size);
  • 功能:分配numsize字节的连续堆内存,所有字节自动初始化为0
  • 返回值:成功返回内存首地址,失败返回NULL
  • 适合需要初始化的数组场景。

(3)realloc:内存大小调整

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

(4)free:内存释放

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

3. 常见问题与规避方案

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

十二、文件操作

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

1. 文件的打开与关闭

(1)fopen:打开文件

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:关闭文件

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)块读写(二进制文件专用)

直接操作内存块,读写效率高,无格式转换,常用于二进制文件(图片、视频、结构体数据)。

// 读文件
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对应的错误描述。

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>,返回错误码对应的错误描述字符串。

#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>,用于调试阶段的错误检查,表达式为假时,终止程序并输出错误信息。

#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