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. 核心规范
- 语句规则:所有执行语句必须以英文分号
;结尾,代码块用{}包裹,支持嵌套。 - 大小写敏感:
A和a是完全不同的标识符。 - 标识符命名规则:
- 只能由字母、数字、下划线
_组成,不能以数字开头; - 不能与C语言关键字重名;
- 见名知意,避免使用下划线开头的标识符(系统保留)。
- 只能由字母、数字、下划线
- 注释规范
- 单行注释:
// 注释内容(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 *
└─ 空类型:void3. 基本数据类型
(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. 类型转换
- 隐式转换(自动类型提升):编译器自动完成,规则:
- 低精度类型向高精度类型转换(避免数据丢失):
char/short→int→long→long long→float→double→long double; - 无符号类型与有符号类型运算,有符号类型转为无符号类型(极易踩坑)。
- 低精度类型向高精度类型转换(避免数据丢失):
- 显式转换(强制类型转换):手动指定转换类型,语法:
(目标类型)表达式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;
}- 核心规则:
- switch括号内必须是整型/枚举类型表达式,不支持浮点型、字符串;
- case后必须是整型/枚举常量,不能是变量,且不能重复;
- 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;
}- 规则:
- 无返回值时,返回值类型写
void; - 无参数时,参数列表写
void(标准写法,避免歧义); - 函数不能嵌套定义,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语言函数参数传递只有值传递一种方式,分为两种场景:
- 值传递(传值):实参将值拷贝给形参,形参是实参的副本,函数内修改形参不会影响原实参。
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; } - 地址传递(传地址):实参将变量的内存地址传递给形参(形参为指针),函数内可通过地址直接修改原实参的值。
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)一维数组
- 定义语法:
元素类型 数组名[元素个数];- 元素个数必须是整型常量表达式(C99支持变长数组VLA,可用变量指定长度);
- 数组名是常量,代表数组首元素的内存地址,不可被赋值。
- 初始化:
// 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 - 元素访问:
数组名[下标]- 下标从0开始,最大下标为
数组长度-1; - 数组越界访问属于未定义行为,会导致程序崩溃、数据篡改,必须严格避免。
- 下标从0开始,最大下标为
(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)数组与函数
数组名作为函数参数传递时,会退化为指向首元素的指针,函数内无法通过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)空指针与野指针
- 空指针
NULL:定义在<stddef.h>等头文件,本质是(void*)0,指向无效的内存地址0。- 作用:初始化指针、判断指针是否有效,避免野指针;
- 规则:空指针不可解引用,否则会导致程序崩溃。
int *p = NULL; // 初始化空指针 if (p != NULL) { // 使用前判空,安全规范 *p = 10; } - 野指针:指向非法、无效内存的指针,是C语言最常见的bug来源。
- 产生原因:指针未初始化、free释放后未置空、指针越界、指向已销毁的局部变量;
- 危害:未定义行为,可能导致程序崩溃、数据篡改、系统异常;
- 规避:指针必须初始化,释放后立即置空,使用前判空,避免越界访问。
2. 指针的算术运算
指针仅支持有限的算术运算,核心是按指向类型的大小进行地址偏移。
- 指针 ± 整数:地址偏移
整数 × sizeof(指向类型)字节int arr[5] = {1,2,3,4,5}; int *p = arr; // p指向数组首元素arr[0] printf("%d", *(p+2)); // 偏移2个int,访问arr[2],输出3 - 指针++/–:向前/向后偏移一个指向类型的大小,常用于数组遍历。
- 指针 - 指针:两个同类型指针指向同一块连续内存时,差值为两个指针之间的元素个数。
- 关系运算:同类型指针可比较地址大小,仅同一块连续内存内有效。
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 数组指针
- 指针数组:数组,每个元素都是同类型的指针,常用于字符串数组。
// 定义:存储3个char*类型指针的数组 char *str_arr[3] = {"apple", "banana", "orange"}; - 数组指针:指针,指向一个完整的数组,常用于二维数组。
// 定义:指向包含3个int元素的一维数组的指针 int arr[2][3] = {{1,2,3}, {4,5,6}}; int (*p)[3] = arr; // p指向二维数组的第一行
(4)函数指针
指向函数的指针,存储函数的入口地址(函数名就是函数的入口地址),常用于回调函数、函数跳转表。
- 定义语法:
返回值类型 (*指针名)(参数类型列表); - 示例:
// 定义一个加法函数 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内存访问效率。 核心对齐规则:
- 结构体成员的偏移量,必须是成员自身大小的整数倍;
- 结构体总大小,必须是最大基本成员大小的整数倍;
- 可通过
#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;
};- 核心特性:
- 联合体的大小等于最大成员的大小;
- 修改一个成员,会覆盖其他成员的值;
- 典型用途:内存复用、判断系统大小端模式。
3. 枚举enum
枚举用于定义一组命名的整型常量,提高代码可读性,限定变量的取值范围。
// 定义枚举类型
enum Week {
Mon, // 默认0,后续依次+1
Tue, // 1
Wed, // 2
Thu=10, // 手动赋值为10
Fri, // 11
Sat, // 12
Sun // 13
};
// 声明枚举变量
enum Week today = Mon;- 核心规则:
- 枚举常量本质是
int类型,默认从0开始,手动赋值后后续常量依次+1; - 枚举变量可赋值为任意整型值,但不推荐,失去枚举的意义。
- 枚举常量本质是
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))
- 核心注意事项:
- 宏参数和整体必须加括号,避免运算符优先级导致的bug;
- 宏无类型检查,不安全;
- 带参宏会产生代码膨胀,且有副作用(如
MAX(a++, b++)会导致变量自增两次); - 宏定义末尾不能加分号,否则会被一起替换。
(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
用于将指定头文件的内容插入到当前位置,分为两种格式:
#include <头文件.h>:用于系统标准头文件,直接搜索系统库路径;#include "头文件.h":用于自定义头文件,先搜索当前项目目录,再搜索系统路径。
头文件重复包含防护
防止头文件被多次包含导致重复定义,两种实现方式:
- 条件编译防护(跨编译器兼容,推荐)
#ifndef __STUDENT_H__ #define __STUDENT_H__ // 头文件核心内容 #endif #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 不支持的操作系统
#endif4. 其他预处理指令
#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. 作用域分类
- 代码块作用域:局部变量、for循环内定义的变量,仅在
{}代码块内可访问; - 函数作用域:goto标签,在整个函数内可访问;
- 文件作用域:全局变量、函数,在整个源文件内可访问;
- 函数原型作用域:函数声明的参数列表,仅在声明内有效。
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);- 功能:分配
num个size字节的连续堆内存,所有字节自动初始化为0; - 返回值:成功返回内存首地址,失败返回
NULL; - 适合需要初始化的数组场景。
(3)realloc:内存大小调整
void *realloc(void *ptr, size_t new_size);- 功能:调整已分配内存的大小,可扩容/缩容;
- 规则:
- 若原内存后有足够空间,直接扩容,返回原地址;
- 若空间不足,重新分配新内存,拷贝原数据,释放原内存,返回新地址;
- ptr为NULL时,等价于malloc;new_size为0时,等价于free;
- 返回值:成功返回新地址,失败返回
NULL,原内存保持不变。
(4)free:内存释放
void free(void *ptr);- 功能:释放ptr指向的动态分配的堆内存;
- 核心规则:
- ptr为
NULL时,函数无任何操作; - 只能释放malloc/calloc/realloc分配的堆内存,不能释放栈内存、常量区地址;
- 同一块内存不能重复释放,否则会导致未定义行为;
- 内存释放后,ptr变为野指针,建议立即置为
NULL。
- ptr为
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. 文件指针定位
控制文件读写指针的位置,实现随机读写。
rewind(FILE *stream):将文件指针移动到文件开头;ftell(FILE *stream):返回当前文件指针的位置,失败返回-1L;fseek(FILE *stream, long offset, int origin):移动文件指针到指定位置offset:偏移字节数,正数向后偏移,负数向前偏移;origin:偏移基准点,可选:SEEK_SET:文件开头;SEEK_CUR:当前位置;SEEK_END:文件结尾。
4. 文件状态判断
int feof(FILE *stream):判断是否到达文件末尾,到达返回非0,否则返回0;- 注意:必须执行读操作失败后,EOF标志才会被设置,不能提前用feof判断文件结束。
int ferror(FILE *stream):判断文件操作是否出错,出错返回非0,否则返回0;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 |