C语言语法速查笔记:从入门到精通,核心指针与内存全掌握

整理自《C语言入门到精通》的实用速查笔记,涵盖C语言基础语法、数据类型、运算符、流程控制、函数、数组、字符串、指针、结构体、动态内存、预处理器及文件I/O等核心知识点,重点突出指针与内存管理的底层细节,适合快速查阅与巩固。

整理自《C语言入门到精通》,偏重实用速查。C 的所有知识点最后都会归结到指针和内存,这是它与高级语言最本质的区别。

一、第一个程序 & 编译运行

#include <stdio.h>

int main(void)
{
    printf("Hello, C!\n");
    return 0;
}

编译流程(用 gcc):

gcc hello.c -o hello    # 直接编译链接一步到位
./hello                 # 运行

# 分步走
gcc -E hello.c -o hello.i   # 预处理
gcc -S hello.i -o hello.s   # 编译(生成汇编)
gcc -c hello.s -o hello.o   # 汇编(生成目标文件)
gcc hello.o -o hello        # 链接

几个基础常识:

  • C 源文件后缀 .c,头文件 .h
  • main 函数是入口,标准签名 int main(void) 或 *int main(int argc, char argv[])
  • #include <stdio.h> 不是关键字,是预处理指令 — 文本级别的内容替换
  • 每个语句以 ; 结尾,忘加分号是新手最常犯的错误

二、数据类型

2.1 基本类型一览

类型大小printf 格式简介
char1字节%c字符/小整数
short2字节%hd短整型
int通常是4字节%d默认整数
long4或8字节%ld长整型
long long8字节%lldC99引入
float4字节%f单精度浮点
double8字节%lf双精度浮点

加上 unsigned 前缀后,范围从 0 到正数最大值(无符号数不会存在负数)。

unsigned int a = 100;
unsigned char b = 255;

sizeof 看真实大小(结果单位是字节):

printf("%zu\n", sizeof(int));      // 4(大多数平台)
printf("%zu\n", sizeof(long));     // 可能是4或8

2.2 整数常量

int    a = 42;          // 十进制
int    b = 052;         // 八进制(以0开头)
int    c = 0x2A;        // 十六进制
long   d = 100L;        // long后缀
long long e = 100LL;    // long long后缀
unsigned f = 100U;       // unsigned后缀

2.3 浮点常量

float  f = 3.14f;         // 不加f默认是double
double d = 2.71828;
double g = 6.022e23;      // 科学计数法
// float 和 double 的 printf:
printf("%f\n", f);        // float和double都用%f输出
printf("%lf\n", d);       // scanf时double用%lf

2.4 字符与ASCII

char ch = 'A';            // 存的就是65(ASCII码值)
char ch2 = 65;            // 等价,输出'A'
char ch3 = '\n';          // 转义字符:换行
char ch4 = '\t';          // 制表符
char ch5 = '\\';          // 反斜杠本身

// 大小写转换
char lower = upper + 32;  // 大写→小写,利用ASCII码差

2.5 类型转换

// 隐式转换(自动提升):
// char/short → int → unsigned → long → float → double
int a = 10;
double b = a;             // OK,自动提升

// 强制转换(截断):
double pi = 3.14159;
int n = (int)pi;          // 3,小数部分丢弃

// 注意:有符号和无符号混用时,有符号会被转成无符号
int x = -1;
unsigned int y = 1;
if (x < y)                // 结果是0(false),因为-1变成4294967295了

2.6 变量声明 & 常量

int a, b, c;              // C89必须在块开头声明所有变量
int a = 10, b = 20;       // 声明时初始化

// C99后可以在任意位置声明
for (int i = 0; i < 10; i++) { ... }

// 常量(两种方式)
#define PI 3.14159          // 宏常量(预处理器,无类型)
const double PI = 3.14159;  // const常量(有类型,占内存)

2.7 Bool 型(C99)

#include <stdbool.h>

bool flag = true;           // 本质是 _Bool
bool flag2 = false;
printf("%d\n", flag);       // 输出1

三、运算符

3.1 算术运算符

运算符名称示例结果说明
+7 + 310数字相加
-7 - 34数字相减
*7 * 321数字相乘
/7 / 23整数除法结果截断为整数
%取余7 % 31只能用于整数
7 / 2     // 3,整数除法直接截断小数
7 / 2.0   // 3.5,只要有一边是浮点就是浮点除法
7 % 3     // 1
-7 / 2    // -3(C99起向零截断,不是-4)
-7 % 3    // -1(C99起余数符号跟随被除数)

// 浮点取余要用 fmod
#include <math.h>
double r = fmod(7.5, 2.0);   // 1.5

注意:C 的 /% 在负数时是"向零截断"(C99 起),和 Python 的"向下取整"不同,容易踩坑。整数除法结果还是整数,想得到小数至少让一边是浮点。

3.2 关系(比较)运算符

运算符含义示例结果
==等于3 == 31(真)
!=不等于3 != 41(真)
>大于5 > 31(真)
<小于5 < 30(假)
>=大于等于5 >= 51(真)
<=小于等于5 <= 40(假)
  • 比较结果永远是 01(int 类型),C 语言里 0 表示假,非 0 表示真
  • C 没有"链式比较":a < b < c 会被解析成 (a < b) < c,逻辑完全错误,必须写成 a < b && b < c
  • 浮点数不要用 == 直接比较(二进制精度误差)
  • 注意 =(赋值)和 ==(等于)的区别,这是新手第一大坑
int a = 8;
a > 3 && a < 10          // 1,真

// 陷阱:连续比较不是数学里的链式比较
if (1 < 2 < 3)           // (1<2)=1,再算 1<3=1,碰巧为真但逻辑是错的
if (1 < 2 && 2 < 3)      // 正确写法

// 浮点数比较
if (0.1 + 0.2 == 0.3)    // 可能是假!精度问题,推荐用差值判断
if (fabs(a - b) < 1e-9)  // 正确姿势

3.3 逻辑运算符

运算符含义示例结果
&&逻辑与(短路)1 && 00
||逻辑或(短路)1 || 01
!逻辑非!01

短路规则

  • &&:左边为假,右边不再求值
  • ||:左边为真,右边不再求值

短路最常见的用法是"保护"后面的操作不越界:

// 保护:避免除以 0
if (b != 0 && a / b > 2) { ... }

// 保护:先判空指针再解引用
if (p != NULL && p->age > 18) { ... }

!0        // 1
!5        // 0
!""       // 0(字符串指针非空即为真)

短路保证右边不执行,这个特性也叫"短路求值",是写出安全代码的基础。

3.4 赋值运算符

运算符等价写法示例
=直接赋值x = 10
+=x = x + nx += 5
-=x = x - nx -= 5
*=x = x * nx *= 5
/=x = x / nx /= 5
%=x = x % nx %= 5
<<=x = x << nx <<= 1
>>=x = x >> nx >>= 1
&=x = x & nx &= 3
|=x = x | nx |= 3
^=x = x ^ nx ^= 3

两个关键点

  • 赋值表达式本身有值(就是赋进去的值),所以 if (x = 5) 永远为真
  • 赋值是右结合a = b = c = 10 从右往左依次赋值
int a, b, c;
a = b = c = 10;      // 从右往左:c=10, b=10, a=10

if (x = 5) { }       // 陷阱!等价于 x=5; if(5),永远为真
if (x == 5) { }      // 正确写法
if (5 == x) { }      // 防御式写法,少写一个=会编译报错

3.5 自增 / 自减

int a = 5;
int b = a++;     // b=5, a=6(先用后加)
int c = ++a;     // a=7, c=7(先加后用)
a--; --a;        // 同理

两个警告

  • 同一个表达式里不要多次修改同一个变量 — 结果未定义
  • 不要依赖函数参数的求值顺序(C 标准不保证从左到右)
i = i++;                        // 未定义行为!禁止
printf("%d %d\n", i, i++);      // 未定义行为!禁止

3.6 位运算符

运算符名称示例(二进制)结果
&按位与5 & 3(101 & 011)1
|按位或5 | 3(101 | 011)7
^按位异或5 ^ 3(101 ^ 011)6
~按位取反~5-6(~x = -x - 1)
<<左移5 << 110(相当于乘 2)
>>右移5 >> 12(逻辑/算术右移取决于类型)
5 & 3     // 1   (101 & 011 = 001)
5 | 3     // 7   (101 | 011 = 111)
5 ^ 3     // 6   (101 ^ 011 = 110)
~5        // -6  (~x = -x - 1)
5 << 1    // 10  (左移1位 = 乘2)
5 >> 1    // 2   (右移1位 = 整除2,正数)

// 常见用途:开关标记(掩码)
int flags = 0;
flags |= (1 << 3);        // 置第3位为1
flags &= ~(1 << 3);       // 清第3位为0
flags ^= (1 << 3);        // 翻转第3位
(flags >> 3) & 1          // 读第3位的值

// 判断奇偶(比 % 快)
if (n & 1) { ... }        // 奇数

右移细节:无符号数是逻辑右移(高位补 0);有符号负数通常是算术右移(高位补符号位)。位运算的优先级比比较运算符低,见 3.10。

3.7 条件(三元)运算符

max = a > b ? a : b;    // 条件 ? 真值 : 假值

// 可以嵌套(不推荐,可读性差)
max = a > b ? (a > c ? a : c) : (b > c ? b : c);

// 三元是表达式,可以赋值、传参
printf("%s\n", score >= 60 ? "及格" : "不及格");

3.8 逗号运算符

// 从左到右依次求值,整个表达式的值是最右边那个
int a = (1, 2, 3);          // a = 3
for (int i = 0, j = 10; i < j; i++, j--) { ... }

注意区分:函数参数里的逗号不是逗号运算符f(a, b) 只是分隔参数。

int x = (a, b, c);      // 逗号运算符:x = c
f(a, b, c);             // 三个参数,不是逗号运算符

3.9 其他常用运算符

运算符名称示例说明
sizeof大小运算符sizeof(int)返回类型/变量的字节数(size_t)
&取地址&a获得变量的内存地址
*解引用*p访问指针指向的内容
[]下标arr[i]等价于 *(arr + i)
.成员访问s.age访问结构体成员
->箭头访问p->age通过指针访问结构体成员
()函数调用/强转add(1,2)(int)x函数调用、类型转换
,逗号(a, b)依次求值,结果是最后一项

3.10 运算符优先级速记(高 → 低)

优先级运算符说明
1()[]->.括号、下标、成员访问
2!~++--+x-xp&x、**(类型)*一元运算符
3*/%乘、除、取余
4+-加、减
5<<**、**>>移位
6<<=**、**>>=关系比较
7==!=相等比较
8&按位与
9^按位异或
10|按位或
11&&逻辑与
12||逻辑或
13?:三元条件
14=+=-==/=%=&=|=**、*^=<<=、**>>=赋值(右结合)
15,逗号

不确定就加括号,别赌记忆力。容易踩坑的几个:

if (flags & MASK == 0) { ... }       // 错!==优先级比&高→等价于flags & (MASK==0)
if ((flags & MASK) == 0) { ... }     // 对

*p++      // 等价 *(p++),不是(*p)++
*p->field // 等价 *(p->field)

四、流程控制

4.1 if-else

if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {
    grade = 'B';
} else {
    grade = 'C';
}
// 条件表达式必须用()括起来
// 花括号永远不要省,哪怕只有一行

4.2 switch

switch (day) {
    case 1:
        printf("Monday\n");
        break;              // 忘了break会穿透到下一个case!
    case 2:
    case 3:                 // 多个case共享代码是合法的
        printf("Tuesday or Wednesday\n");
        break;
    default:
        printf("Unknown\n");
        break;
}
// switch 只能用于整数类型(char/short/int/long)和枚举
// C语言不支持在case中判断字符串

4.3 循环

// for(知道循环次数)
for (int i = 0; i < 10; i++) {
    printf("%d\n", i);
}

// while(条件满足就一直循环)
while (n > 0) {
    n /= 10;
    count++;
}

// do-while(至少执行一次,先斩后奏)
do {
    printf("至少执行一次\n");
} while (0);

// 嵌套循环 + 带标签的break(用goto实现)
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) goto end_loop;
    }
}
end_loop:

4.4 break / continue / goto

break;       // 跳出当前循环或switch
continue;    // 跳过本次循环剩余语句,进入下一次迭代

// goto 不是洪水猛兽,清理多个资源时它是最清晰的方案:
if (open_resource1() < 0) goto out1;
if (open_resource2() < 0) goto out2;
if (open_resource3() < 0) goto out3;
// ... 正常逻辑 ...
out3: close_resource3();
out2: close_resource2();
out1: close_resource1();

五、函数

5.1 定义与声明

// 声明(告诉编译器函数存在,通常在.h里)
int add(int a, int b);

// 定义(函数体)
int add(int a, int b)
{
    return a + b;
}

// C要求函数必须先声明后使用。如果没声明,编译器会默认返回int

5.2 参数传递

C 只有值传递

  • 基本类型:传的是值的副本,改形参不影响实参
  • 数组:传的是首地址(退化为指针),在函数内可以改元素内容
  • 想改普通变量本身?必须传指针:
void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 3, y = 5;
swap(&x, &y);        // x=5, y=3

5.3 作用域与生命周期

int global = 10;              // 全局变量:整个程序,程序结束才释放

void func(void)
{
    static int count = 0;     // 静态局部变量:只初始化一次,函数结束不释放
    int local = 5;            // 自动局部变量(栈):函数返回就释放
    count++;
    printf("count = %d\n", count);
}

static 修饰全局变量/函数:限制为文件内部可见(其他 .c 文件用不了)。

5.4 可变参数

#include <stdarg.h>

double average(int count, ...)   // 至少有一个固定参数
{
    va_list args;
    va_start(args, count);

    double sum = 0;
    for (int i = 0; i < count; i++) {
        sum += va_arg(args, double);
    }

    va_end(args);                // 必须配对调用
    return sum / count;
}

double avg = average(5, 1.0, 2.0, 3.0, 4.0, 5.0);

5.5 递归

long factorial(int n)
{
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
// C的递归深度受限于栈大小,太深就爆栈

六、数组

6.1 一维数组

int arr[5];                   // 声明,元素未初始化(垃圾值)
int arr2[5] = {1, 2, 3};      // 初始化为1,2,3,0,0
int arr3[] = {1, 2, 3, 4};    // 自动推断大小

// C不检查数组越界,下面能编译但不该写:
arr[100] = 42;                // 写到别人家的内存了

// 数组名在大多数场合退化为指向首元素的指针
int size = sizeof(arr) / sizeof(arr[0]);   // 数组大小(只在定义处有效)

6.2 二维数组

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

matrix[0][2];        // 3

// 作为函数参数时,第二维必须指定
void print_matrix(int rows, int cols, int m[][4]) { ... }  // 第二维写4

6.3 变长数组 VLA(C99)

int n = 10;
int arr[n];             // 数组大小由运行时变量决定

// 但VLA不能整体初始化,不能声明时赋初值
// VLA不是所有编译器都支持(MSVC不支持),用它要小心

七、字符串

C 没有独立的字符串类型,字符串就是 char 数组,以 \0 结尾。

7.1 声明与初始化

char s1[] = "hello";         // 6个字节:h e l l o \0
char s2[10] = "hello";       // 前6个是hello\0,后4个是\0
char *s3 = "hello";          // 指向常量区的字符串,不能修改!
// s3[0] = 'H';              // 运行时报错或未定义行为

s1s3 的本质区别:


char s[] = "..."*char s = "..."
内容在栈上,可修改在只读数据段,不可写
sizeof 结果整个数组大小指针大小(4或8)

7.2 string.h 常用函数

#include <string.h>

strlen(s);                     // 长度(不含\0)
strcpy(dst, src);              // 复制(危险!不检查边界)
strncpy(dst, src, sizeof(dst)-1);  // 带边界复制,但记得手动补\0
strcat(dst, src);              // 拼接(危险!)
strncat(dst, src, sizeof(dst) - strlen(dst) - 1);  // 安全拼接

strcmp(a, b);                  // 比较:0=相等,负数=a<b,正数=a>b

strchr(s, 'a');                // 找字符,返回指针或NULL
strstr(haystack, needle);      // 找子串

memcpy(dst, src, n);           // 复制n字节(不能处理重叠)
memmove(dst, src, n);          // 复制n字节(能处理重叠,更安全)
memset(ptr, 0, n);             // 填充n字节为某个值

安全字符串法则

// 带 n 的版本永远手动补 \0
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

八、指针

C 的核心,没有之一。指针就是存地址的变量。

8.1 基本操作

int a = 42;
int *p = &a;          // p存的是a的地址

*p = 100;             // 通过指针修改a的值
printf("%d\n", a);    // 100

// 解引用:*p 拿到p指向的值
// 取地址:&a  拿到a的地址

8.2 指针与 const

const int *p;         // p能变,*p不能变(指向的内容是常量)
int * const p;        // p不能变,*p能变(指针本身是常量)
const int * const p;  // 都不能变

// 记法:const在*左边 → 内容只读;const在*右边 → 指针只读

8.3 指针运算

int arr[] = {1, 2, 3, 4, 5};
int *p = arr;          // 等价于 int *p = &arr[0];

printf("%d\n", *p);    // 1
p++;                    // p指向arr[1](地址+sizeof(int))
printf("%d\n", *p);    // 2

// arr[i] 完全等价于 *(arr + i)
arr[2] 和 *(arr + 2) 没有任何区别

数组下标操作本质上就是指针运算的语法糖。写 a[i] 跟写 *(a + i) 一模一样,反过来 i[a] 也能工作(虽然没人这么写)。

8.4 多级指针

int a = 42;
int *p = &a;
int **pp = &p;        // 指向指针的指针

printf("%d\n", **pp);  // 42(两次解引用)

二维数组名不过是一个行指针:int (*ptr)[3]

8.5 函数指针

// 定义
int add(int a, int b) { return a + b; }

// 声明函数指针
int (*func_ptr)(int, int) = add;

// 调用
int result = func_ptr(3, 5);   // 8

// 函数指针的实用场景:回调函数
void for_each(int *arr, int len, void (*callback)(int))
{
    for (int i = 0; i < len; i++) {
        callback(arr[i]);
    }
}

函数指针 typedef 更可读:

typedef int (*Operation)(int, int);
Operation op = add;

8.6 数组指针 vs 指针数组 vs 函数指针数组

int arr[5];              // 整型数组
int *pArr[5];            // 指针数组:5个元素的数组,每个元素是int*
int (*p)[5];             // 数组指针:指向长度为5的int数组的指针
int* (*f[5])(int);       // 函数指针数组:5个函数指针,参数int返回int*

8.7 void 指针

// void* 是万能指针,可以指向任何类型
void *vp = &a;
// 但不能解引用,必须强转回来
int *ip = (int *)vp;
printf("%d\n", *ip);

// malloc返回的就是void*,正是此意

九、结构体 & 联合体 & 枚举

9.1 结构体 struct

struct Student {
    char name[20];
    int age;
    double score;
};                            // 注意分号不能少

// 使用
struct Student s1 = {"Alice", 20, 95.5};
struct Student s2;
s2.age = 21;

// typedef 简化
typedef struct {
    char name[20];
    int age;
} Student;

Student s3 = {"Bob", 22};

// 通过指针访问(两种等价方式)
Student *p = &s3;
p->age = 23;                  // 推荐,语义清晰
(*p).age = 23;                // 等价但不推荐

9.2 结构体内存对齐

struct Demo {
    char a;      // 1字节
    int b;       // 4字节
    char c;      // 1字节
};
// sizeof(struct Demo) = 12,不是1+4+1=6
// 因为存在对齐填充:a后补3字节,c后补3字节

控制对齐(非标准但主流编译器都支持):

#pragma pack(1)           // 按1字节对齐
struct Packed {
    char a;
    int b;
    char c;
};
#pragma pack()            // 恢复默认
// sizeof = 6

9.3 联合体 union

所有成员共享同一块内存,存一个覆盖另一个。

union Data {
    int i;
    float f;
    char str[20];
};

union Data d;
d.i = 10;
d.f = 3.14;              // 覆盖了i的值

// 典型用途1:判断大小端
union { int i; char c; } u;
u.i = 1;
if (u.c == 1) printf("小端\n");
// 典型用途2:多种数据类型复用同一内存

9.4 枚举 enum

enum Color { RED, GREEN, BLUE };           // RED=0, GREEN=1, BLUE=2
enum Weekday { MON = 1, TUE, WED, THU };   // MON=1, 依次递增

enum Color c = RED;
// enum 本质是int的别名,可以和int混用(不推荐)

十、动态内存管理

10.1 栈 vs 堆


分配方式自动(函数调用)手动(malloc/free)
大小有限(默认几MB)取决于系统内存
生命周期函数返回即释放手动 free 才释放
速度

10.2 malloc / free

#include <stdlib.h>

// 分配
int *arr = (int *)malloc(10 * sizeof(int));
if (arr == NULL) {
    // 分配失败处理
    exit(1);
}

// 初始化为0(calloc 会清零)
int *arr2 = (int *)calloc(10, sizeof(int));

// 重新调整大小
int *tmp = (int *)realloc(arr, 20 * sizeof(int));
if (tmp != NULL) arr = tmp;       // realloc可能返回新地址

// 释放
free(arr);
arr = NULL;                       // 防止野指针

10.3 三大铁律

  1. 每个 malloc 必须有对应的 free — 忘了就是内存泄漏
  2. free 后立即置 NULL — 防止 use-after-free
  3. 永远不要 free 同一个指针两次 — 结果不可预测
// 好习惯模板
int *ptr = (int *)malloc(100 * sizeof(int));
if (ptr == NULL) {
    perror("malloc");
    exit(1);
}

// ... 使用 ...

free(ptr);
ptr = NULL;

10.4 常见内存错误

// 1. 返回局部变量地址
char *get_str(void) {
    char buf[] = "hello";
    return buf;               // 返回后buf被销毁,悬空指针!
}

// 2. use-after-free
free(p);
printf("%d\n", *p);           // 未定义

// 3. 内存越界
int *arr = malloc(10 * sizeof(int));
arr[10] = 42;                 // 越界!

// 4. 忘记free导致泄漏
void leak(void) {
    char *buf = malloc(1024);
    // 使用buf,但忘了free
}                             // 每次调用泄1024字节

十一、预处理器

预处理器在编译之前执行纯文本替换,不是 C 语言的一部分,但没有它几乎写不了代码。

11.1 #include

#include <stdio.h>        // 尖括号:从系统头文件目录找
#include "myheader.h"     // 双引号:先从当前目录找,再找系统目录

11.2 #define 宏定义

// 对象式宏
#define PI 3.14159
#define MAX_SIZE 1024

// 函数式宏(参数和整体都必须加括号!)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

// 为什么必须加括号?
#define BAD_SQUARE(x) x * x
int result = BAD_SQUARE(3 + 2);     // 展开为 3+2*3+2 = 11 !!!

// 副作用陷阱
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 3;
int m = BAD_MAX(x++, y++);          // x和y可能被自增两次

宏的替代方案 —— 内联函数(C99): 

inline int max(int a, int b) {
    return a > b ? a : b;
}
// 有类型检查,没有宏的副作用,性能一样好

11.3 条件编译

// 调试开关
#ifdef DEBUG
    printf("x = %d\n", x);
#endif

// 平台适配
#if defined(__linux__)
    // Linux代码
#elif defined(_WIN32)
    // Windows代码
#else
    #error "Unsupported platform"
#endif

// 防止头文件重复包含
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... 头文件内容 ...
#endif

11.4 # 和

#define STR(x) #x               // 字符串化
#define CAT(a, b) a ## b        // 拼接

printf("%s\n", STR(hello));     // "hello"
int CAT(val, 123) = 10;         // int val123 = 10;

11.5 预定义宏

__FILE__        // 当前文件名
__LINE__        // 当前行号
__func__        // 当前函数名(C99)
__DATE__        // 编译日期
__TIME__        // 编译时间

// 实用日志宏
#define LOG(fmt, ...) \
    fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

十二、文件 I/O

12.1 基本流程

#include <stdio.h>

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("fopen");        // 打印系统错误信息
    return 1;
}
// ... 读写操作 ...
fclose(fp);

12.2 打开模式

模式含义
"r"只读,文件必须存在
"w"只写,存在则清空,不存在则创建
"a"追加写,文件指针在末尾
"r+"读写,文件必须存在
"w+"读写,存在则清空
"a+"读和追加写
"rb"/"wb"二进制模式(Windows上b才有意义)

12.3 文本读写

// 逐字符
int ch;
while ((ch = fgetc(fp)) != EOF) {       // ch必须是int!
    fputc(ch, stdout);
}

// 逐行
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    // 去掉末尾换行符
    line[strcspn(line, "\n")] = '\0';
    printf("读取: [%s]\n", line);
}

// 格式化
fprintf(fp, "%s %d %.2f\n", "Alice", 25, 1.65);   // 写入
fscanf(fp, "%s %d %f", name, &age, &height);      // 读取

12.4 二进制读写

// fread / fwrite
int arr[] = {1, 2, 3, 4, 5};
FILE *fp = fopen("data.bin", "wb");
fwrite(arr, sizeof(int), 5, fp);           // 数组名, 每元素大小, 元素个数, 文件指针
fclose(fp);

int loaded[5];
FILE *fp2 = fopen("data.bin", "rb");
size_t n = fread(loaded, sizeof(int), 5, fp2);  // 返回实际读取的元素个数
if (n != 5) printf("读取不完整\n");
fclose(fp2);

警告:直接二进制读写结构体跨平台不可靠(对齐、大小端、指针成员都会出问题)。

12.5 文件定位

fseek(fp, 0, SEEK_SET);      // 移到开头
fseek(fp, 0, SEEK_END);      // 移到末尾(配合ftell获取文件大小)
fseek(fp, -10, SEEK_CUR);    // 从当前位置往前移10字节
long pos = ftell(fp);         // 获取当前位置
rewind(fp);                   // 等价于fseek(fp, 0, SEEK_SET)

12.6 三个标准流

fprintf(stdin, ...);    // 标准输入
fprintf(stdout, ...);   // 标准输出(行缓冲)
fprintf(stderr, ...);   // 标准错误(不缓冲,即时输出)

十三、C 标准库速览

常用头文件一览

头文件主要功能
<stdio.h>输入输出:printfscanffopenfclosefreadfwritefgets
<stdlib.h>通用工具:mallocfreecallocreallocatoirandqsortbsearchexit
<string.h>字符串:strlenstrcpystrcmpstrcatstrchrmemcpymemset
<ctype.h>字符判断:isalphaisdigitisspaceisuppertolowertoupper
<math.h>数学:sqrtpowsincoslogceilfloorfabs(编译加 -lm
<time.h>时间:timelocaltimestrftimeclock
<stdarg.h>可变参数:va_listva_startva_argva_end
<assert.h>断言:assert(debug用,NDEBUG宏可关闭)
<stdint.h>固定宽度整数:int32_tuint64_t(C99)
<stdbool.h>布尔类型:booltruefalse(C99)
<errno.h>错误码:errnoperrorstrerror

常用函数签名

// qsort 泛型排序(需要比较回调)
void qsort(void *base, size_t n, size_t size,
           int (*cmp)(const void *, const void *));

// 使用示例
int cmp_int(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}
int arr[] = {5, 2, 9, 1};
qsort(arr, 4, sizeof(int), cmp_int);

// bsearch 二分搜索(需排序后)
void *bsearch(const void *key, const void *base,
              size_t n, size_t size,
              int (*cmp)(const void *, const void *));

断言 assert

#include <assert.h>

// 调试时检查"绝不应该出现"的情况
void divide(int a, int b) {
    assert(b != 0);              // b为0时程序直接终止并报错
    printf("%d\n", a / b);
}

// 发布版本编译加 -DNDEBUG,所有assert自动移除

附录一:经典陷阱清单

  1. = 当成 ==if (x = 5) 永远为 true(赋值返回值就是5)。写成 if (5 == x) 可以防止(左值必须是变量,写错就编译报错)
  2. 有符号和无符号混用if (-1 < 1u) 结果是0(false),因为 -1 被转成无符号大数
  3. 数组名不是指针sizeof(arr) 是数组总大小,但传给函数就退化为真正的指针,sizeof 只剩指针大小
  4. 忘记 breakswitchcase 不写 break 会穿透到下一个 case
  5. fgetc 用 char 接收:必须用 int,因为 EOF(-1)超出 char 范围
  6. gets 已废弃:C11 移除了,用 fgets
  7. strncpy 不补 \0:源串 ≥ 目标长度时,不会自动补 \0,必须手动加
  8. 宏参数的副作用MAX(x++, y++) 会让参数被多次求值
  9. 返回局部变量地址:函数返回后,局部变量所在栈帧已销毁
  10. memset 只能设 0 或 -1:对 int 数组 memset(arr, 1, sizeof(arr)) 不会得到 1,而是 0x01010101
  11. realloc 不检查返回值arr = realloc(arr, size); 失败会返回 NULL,原来的 arr 就泄漏了,必须用临时变量接收
  12. **int *p; *p = 5;**:野指针解引用,p 没有指向有效内存

到这里,C语言基础语法概要就介绍完了。

C 的难度不在于语法多复杂,而在于它把计算机底层的所有细节都摊在你面前。指针、内存、位运算 —— 这些才是 C 真正的 "语法"。用 Valgrind、AddressSanitizer 这些工具辅助排查内存问题,比肉眼 debug 靠谱得多。

学习有两忌,自高和自狭。 —— 书摘
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号