C语言全能实战教程

C语言全能实战教程

前言

欢迎学习C语言实战教程!本教程从零基础开始,通过大量精心设计的代码示例,系统地讲解C语言的各个核心知识点。无论你是计算机专业的学生、编程爱好者,还是希望提升编程技能的开发者,本教程都将为你提供全面、实用的C语言学习路径。

C语言是一门历史悠久但生命力旺盛的编程语言。它不仅是计算机科学教育的基础语言,也是系统编程、嵌入式开发、游戏引擎、编译器等众多领域的首选语言。掌握C语言,不仅能让你理解计算机底层工作原理,还能为学习其他编程语言打下坚实基础。

本教程的特点是理论与实践紧密结合。每个知识点都配有完整的代码示例,所有代码都经过实际测试,可以直接在编译器中运行。我们将从最简单的"Hello World"程序开始,逐步深入到函数、指针、结构体、文件操作等高级主题。


目录


第一章:将C语言用起来

1.1 第一个C程序

让我们从最经典的"Hello World"程序开始。这是几乎所有编程教程的第一个示例,虽然简单,但它展示了C程序的基本结构。

c 复制代码
#include <stdio.h>

int main() {
    printf("hello world\n");
    return 0;
}

代码解析:

  • #include <stdio.h>:这是预处理指令,用于引入标准输入输出库。stdio.h包含了printf、scanf等输入输出函数的声明。在使用printf之前必须包含这个头文件。

  • int main():这是主函数,每个C程序都必须有且只有一个main函数。程序运行时首先执行的就是main函数。int表示函数返回整型值。

  • printf("hello world\n");:这是输出函数,用于在屏幕上打印文字。\n是换行符,使光标移动到下一行。

  • return 0;:表示程序正常结束。返回值为0表示程序执行成功。

代码规范对比:

下面是一个代码规范较差的示例,变量命名随意,缺少空格和缩进:

c 复制代码
#include <stdio.h>

int main(){
printf("hello world\n");
printf("hello bilibili\n");
int a,b,c,d;
a=b+c+d*2;
printf("a=%d",a);
return 0;
}

再看一个代码规范较好的示例,注意代码的缩进、空格和命名:

c 复制代码
#include <stdio.h>

int main() {
    printf("hello world\n");
    printf("hello bilibili\n");
    int a, b, c, d;
    a = b + c + d * 2;
    printf("a = %d", a);
    return 0;
}

良好的编码习惯的重要性:

  1. 可读性:整齐的代码让人一眼就能看出程序结构,减少理解时间。
  2. 可维护性:在实际工作中,代码需要经常修改和扩展。规范的代码更易于维护。
  3. 团队协作:如果多人合作开发项目,统一代码风格至关重要。
  4. 减少错误:规范的代码结构能帮助我们发现潜在的错误。

缩进规范建议:

  • 推荐使用4个空格进行缩进
  • 左大括号{与函数名在同一行
  • 每个代码块内部语句向右缩进一层
  • 右大括号}单独一行,与对应的左大括号对齐

第二章:类型、运算符与表达式

2.1 基本数据类型

C语言提供了多种基本数据类型,用于存储不同种类的数据。理解这些数据类型是编程的基础。

c 复制代码
#include <stdio.h>
#include <inttypes.h>

int main() {
    int a = 123, b = 97, c, d;
    c = 100062;
    d = 9651;
    printf("%d %d %d %d\n", a, b, c, d);
    
    // 整数溢出演示
    a = 2147483647 + 1;
    printf("%d\n", a);  // 输出负数,说明发生了溢出
    
    // 使用inttypes.h中的宏显示整型范围
    printf("INT32_MIN = %d\n", INT32_MIN);
    printf("INT32_MAX = %d\n", INT32_MAX);
    
    // 使用long long类型处理更大范围的整数
    long long e;
    e = 2147483647 + 1LL;  // 加LL后缀确保使用long long运算
    printf("%lld\n", e);  // 正确输出2147483648
    
    // 浮点数类型
    float  f  = e + 0.1;
    double ff = e + 0.123456789;
    printf("float : %.9f\n", f);
    printf("double : %.9lf\n", ff);
    
    // 字符类型
    char g = 'a';
    printf("g = %c\n", g);      // 以字符形式输出
    printf("g(%%d) = %d\n", g); // 以整数形式输出(97)
    printf("g + 5 = %c\n", g + 5); // 'a' + 5 = 'f'
    printf("48 = %c\n", 48);    // ASCII码48对应字符'0'
    
    // sizeof运算符:获取数据类型或变量的大小
    printf("sizeof(int) = %lu\n", sizeof(int));
    printf("sizeof(long long) = %lu\n", sizeof(long long));
    printf("sizeof(float) = %lu\n", sizeof(float));
    printf("sizeof(double) = %lu\n", sizeof(double));
    printf("sizeof(char) = %lu\n", sizeof(char));
    
    return 0;
}

数据类型详解:

  1. 整型(Integer)

    • char:字符型,占用1个字节,通常用于存储字符的ASCII码
    • short:短整型,占用2个字节
    • int:整型,通常占用4个字节(32位系统)
    • long:长整型,占用4或8个字节
    • long long:更长的整型,占用8个字节
  2. 浮点型(Floating-point)

    • float:单精度浮点型,占用4个字节,精度约6-7位有效数字
    • double:双精度浮点型,占用8个字节,精度约15-16位有效数字
  3. 有符号与无符号

    • 默认情况下,整型和字符型数据是有符号的(signed)
    • 可以使用unsigned修饰符声明无符号类型
    • 例如:unsigned intunsigned char

整数溢出问题:

c 复制代码
#include <stdio.h>
#include <limits.h>

int main() {
    // INT32_MAX 是32位有符号整数的最大值:2147483647
    // 当超过这个值时,会发生溢出
    int a = INT32_MAX;
    printf("INT32_MAX = %d\n", INT32_MAX);
    printf("INT32_MAX + 1 = %d\n", a + 1);  // 输出负数:-2147483648
    
    // 解决方法:使用long long类型
    long long b = (long long)INT32_MAX + 1;
    printf("正确结果:%lld\n", b);
    
    return 0;
}

2.2 printf函数详解

printf是C语言中最常用的输出函数,它的功能非常强大。

c 复制代码
#include <stdio.h>

int main() {
    int n1, n2;
    n1 = printf("hello world\n");  // printf返回输出的字符数
    printf("%d\n", '\n');  // '\n'的ASCII码是10
    
    int a = 123;
    n2 = printf("hello world : %d\n", a);
    printf("n1 = %d, n2 = %d\n", n1, n2);  // n1=12, n2=19
    
    return 0;
}

printf的格式说明符:

格式说明符 说明 示例
%d 十进制整数 printf("%d", 123);
%ld long类型整数 printf("%ld", 123L);
%lld long long类型整数 printf("%lld", 123LL);
%f 浮点数 printf("%f", 3.14);
%lf double类型浮点数 printf("%lf", 3.14);
%.2f 指定精度的浮点数 printf("%.2f", 3.14159); // 输出3.14
%c 单个字符 printf("%c", 'A');
%s 字符串 printf("%s", "hello");
%p 指针地址 printf("%p", &a);
%x 十六进制整数 printf("%x", 255); // 输出ff
%o 八进制整数 printf("%o", 255); // 输出377
%% 输出百分号 printf("100%%");

2.3 scanf函数详解

scanf是C语言中用于从标准输入读取数据的函数。

c 复制代码
#include <stdio.h>

int main() {
    int a, b, n;
    printf("请输入两个整数:");
    n = scanf("%d%d", &a, &b);  // &是取地址运算符
    printf("a = %d, b = %d\n", a, b);
    printf("成功读取了 %d 个整数\n", n);
    
    // 格式化输入:跳过特定字符
    printf("请输入格式为 123abc456 的数据:");
    n = scanf("%dabc%d", &a, &b);
    printf("a = %d, b = %d\n", a, b);
    
    // EOF演示:循环读取直到文件结束
    printf("输入EOF结束输入:\n");
    while (scanf("%d", &a) != EOF) {
        printf("读取到: %d\n", a);
    }
    printf("输入结束,EOF = %d\n", EOF);
    
    return 0;
}

scanf使用注意事项:

  1. 取地址运算符 :变量前必须加&(除了数组名)
  2. 格式说明符:与printf类似但不完全相同
  3. 空白字符:scanf会自动跳过输入中的空白字符(空格、换行、制表符)
  4. 返回值:返回成功匹配和赋值的输入项个数

2.4 字符串处理

字符串在C语言中是以'\0'结尾的字符数组。

c 复制代码
#include <stdio.h>

int main() {
    char t[10];
    for (int i = 0; i < 10; i++) t[i] = -1;
    scanf("%s", t);  // 字符串输入,遇到空白字符停止
    for (int i = 0; i < 10; i++) {
        printf("%d ", t[i]);
    }
    printf("\n");
    
    // 字符串初始化和赋值
    char s[100] = "hello world";
    printf("%s\n", s);
    
    // scanf读取字符串
    scanf("%s", s);
    printf("scanf s : %s\n", s);
    
    // 吞掉换行符
    getchar();
    
    // 读取字母字符集
    scanf("%[a-zA-Z]", s);  // 只读取字母
    printf("%%[a-zA-Z] : %s\n", s);
    
    return 0;
}

2.5 sscanf和sprintf函数

sscanf和sprintf是用于处理字符串的格式化输入输出函数。

c 复制代码
#include <stdio.h>

int main() {
    // sscanf:从字符串中读取数据
    char s[100] = "123 456 789";
    int a, b, c;
    sscanf(s, "%d%d%d", &a, &b, &c);
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    
    // sprintf:将数据格式化为字符串
    char str[100] = "192.168.1.245";
    int p1, p2, p3, p4;
    sscanf(str, "%d.%d.%d.%d", &p1, &p2, &p3, &p4);
    sprintf(str, "%d:%d:%d:%d", p1, p2, p3, p4);
    printf("str = %s\n", str);  // 输出:192:168:1:245
    
    return 0;
}

sscanf和sprintf的应用场景:

  1. 字符串解析:从字符串中提取特定格式的数据
  2. 字符串构建:将数据格式化成特定格式的字符串
  3. 数据验证:检查字符串是否符合预期格式
  4. 格式转换:如将IP地址转换为整数,或反之

2.6 算术运算符

C语言支持丰富的算术运算符,用于进行各种数学运算。

c 复制代码
#include <stdio.h>
#define PRINT(stmt, func) { \
    printf("%s\n", #stmt); \
    stmt; \
    func; \
}

int main() {
    int a = 5, b = 2, c = 7, d;
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    
    // 赋值运算符
    PRINT(a = c, printf("a = %d, b = %d, c = %d\n", a, b, c));
    
    // 加减乘除
    PRINT(d = a + c, printf("d = %d\n", d));  // d = 5 + 7 = 12
    PRINT(d = a - b, printf("d = %d\n", d));  // d = 5 - 2 = 3
    PRINT(d = a * b, printf("d = %d\n", d));  // d = 5 * 2 = 10
    PRINT(d = a / b, printf("d = %d\n", d));  // d = 5 / 2 = 2(整数除法)
    
    // 负数除法:向零取整
    PRINT(d = (-a) / b, printf("d = %d\n", d));  // d = (-5) / 2 = -2
    
    // 取模(取余)
    PRINT(d = a % b, printf("d = %d\n", d));  // d = 5 % 2 = 1
    
    return 0;
}

算术运算符优先级:

  1. 括号 ()
  2. 正负号 + -
  3. 乘除取模 * / %
  4. 加减 + -

在表达式 a + b * c 中,先计算 b * c,再与 a 相加。

2.7 位运算符

位运算符直接操作二进制位,是C语言的特色之一。

c 复制代码
#include <stdio.h>

void print_digit(int x) {
    for (int i = 31; i >= 0; i--) {
        printf("%c", (x & (1 << i)) ? '1' : '0');
    }
    printf("\n");
}

int main() {
    int a = 5, b = 3;
    printf("a = "); print_digit(a);  // 00000000000000000000000000000101
    printf("b = "); print_digit(b);  // 00000000000000000000000000000011
    
    // 按位与:对应位都为1才为1
    printf("a & b = "); print_digit(a & b);  // 00000000000000000000000000000001
    
    // 按位或:对应位有1就为1
    printf("a | b = "); print_digit(a | b);  // 00000000000000000000000000000111
    
    // 按位异或:对应位相异为1
    printf("a ^ b = "); print_digit(a ^ b);  // 00000000000000000000000000000110
    
    // 交换a和b的值(不使用临时变量)
    a ^= b; b ^= a; a ^= b;
    printf("a = "); print_digit(a);  // 现在a是原来的b
    printf("b = "); print_digit(b);  // 现在b是原来的a
    
    // 取反操作
    printf("~a = "); print_digit(~a);
    printf("(~b + 1) = %d\n", ~b + 1);  // 等于 -b
    
    // 左移和右移
    int c = 1279813123, d = -c;
    printf("c = "); print_digit(c);
    printf("d = "); print_digit(d);
    printf("c << 1 = "); print_digit(c << 1);  // 左移1位,相当于乘2
    printf("c >> 1 = "); print_digit(c >> 1);  // 右移1位,相当于除2(有符号数符号位填充)
    printf("d << 1 = "); print_digit(d << 1);  // 负数左移
    printf("d >> 1 = "); print_digit(d >> 1);  // 负数右移(算术右移)
    
    printf("a = %d, b = %d\n", a, b);
    printf("a << 1 = %d, b >> 1 = %d\n", a << 1, b >> 1);
    printf("-5 / 2 = %d, (-5) >> 1 = %d\n", -5 / 2, (-5) >> 1);
    
    return 0;
}

位运算应用技巧:

  1. 快速乘除:左移1位相当于乘2,右移1位相当于除2
  2. 判断奇偶(n & 1) 判断n是否为奇数
  3. 交换数值:使用异或交换两数
  4. 提取位:使用与运算提取特定位
  5. 设置位:使用或运算设置特定位为1
  6. 清除位:使用与运算配合取反清除特定位

2.8 数学函数

C语言标准库提供了丰富的数学函数,使用时需要包含<math.h>头文件。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main() {
    // 幂函数和开方
    printf("pow(2, 3) = %lf\n", pow(2.0, 3.0));  // 8.0
    printf("sqrt(2) = %lf\n", sqrt(2));  // 1.414213...
    
    // 向上取整和向下取整
    printf("ceil(4.01) = %lf\n", ceil(4.01));  // 5.0
    printf("floor(4.99) = %lf\n", floor(4.99));  // 4.0
    
    // 绝对值
    printf("abs(-65) = %d\n", abs(-65));  // 65(整数)
    printf("fabs(-65.6) = %lf\n", fabs(-65.6));  // 65.6(浮点数)
    
    // 反余弦函数
    printf("acos(-1) = %lf\n", acos(-1));  // 3.141592...(π的值)
    
    return 0;
}

2.9 赋值运算符

赋值运算符用于给变量赋值,C语言支持多种复合赋值运算符。

c 复制代码
#include <stdio.h>

int main() {
    int a = 1, b = 2, c = 3, d = 4;
    
    // 连续赋值:右结合性
    a = b = c = d;  // 相当于 a = (b = (c = d))
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
    // 所有变量都变为4
    
    return 0;
}

复合赋值运算符:

运算符 示例 等价于
+= a += b a = a + b
-= a -= b a = a - b
*= a *= b a = a * b
/= a /= b a = a / b
%= a %= b a = a % b
<<= a <<= b a = a << b
>>= a >>= b a = a >> b
&= a &= b a = a & b
|= a |= b a = a | b
^= a ^= b a = a ^ b

第三章:控制流

3.1 条件表达式与比较运算符

条件表达式用于比较两个值的大小关系。

c 复制代码
#include <stdio.h>

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    
    // 比较运算符:返回1(真)或0(假)
    printf("a == b (%d)\n",    a == b);  // 等于
    printf("a != b (%d)\n",    a != b);  // 不等于
    printf("a < b (%d)\n",     a < b);   // 小于
    printf("a > b (%d)\n",     a > b);   // 大于
    printf("a <= b (%d)\n",    a <= b);  // 小于等于
    printf("a >= b (%d)\n",    a >= b);  // 大于等于
    printf("!(a == b) (%d)\n", !(a == b));  // 取反
    
    // 比较表达式可以参与算术运算
    a += (a == b);  // 如果a等于b,a加1
    printf("a = %d\n", a);
    
    // 逻辑运算符
    printf("a < b && a == 3 (%d)\n", a < b && a == 3);  // 逻辑与
    printf("a < b || a == 3 (%d)\n", a < b || a == 3);  // 逻辑或
    
    // 逻辑运算符的真值表
    printf("1 && 1 (%d)\n", 1 && 1);  // 1
    printf("1 && 0 (%d)\n", 1 && 0);  // 0
    printf("0 && 1 (%d)\n", 0 && 1);  // 0
    printf("0 && 0 (%d)\n", 0 && 0);  // 0
    printf("1 || 1 (%d)\n", 1 || 1);  // 1
    printf("1 || 0 (%d)\n", 1 || 0);  // 1
    printf("0 || 1 (%d)\n", 0 || 1);  // 1
    printf("0 || 0 (%d)\n", 0 || 0);  // 0
    
    return 0;
}

3.2 短路求值

逻辑运算符具有短路特性,可以用来实现条件执行。

c 复制代码
#include <stdio.h>

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    
    // 短路求值:如果a < b为真,才执行后面的printf
    a < b && printf("YES\n");
    
    // 如果a < b为假(或假后),才执行后面的printf
    !(a < b) && printf("NO\n");
    
    return 0;
}

短路求值的优势:

  1. 避免不必要的计算:如果第一个条件已经能确定结果,跳过后面的判断
  2. 防止错误 :如避免数组越界访问:if (i < n && arr[i] > 0)
  3. 简化代码:可以用逻辑运算符代替简单的if语句

3.3 if-else语句

if-else是最基本的选择结构。

c 复制代码
#include <stdio.h>

int main() {
    int a, b, c;
    scanf("%d%d", &a, &b);
    
    // 简单的if语句
    if (a < b) printf("YES\n");
    else printf("NO\n");
    
    // 带代码块的if语句
    if (a < b) {
        c = a * b;
        printf("c = %d\n", c);
    }
    
    return 0;
}

3.4 switch-case语句

当需要根据一个变量的值选择多个分支时,switch语句比if-else更清晰。

c 复制代码
#include <stdio.h>

int main() {
    int a;
    scanf("%d", &a);
    
    switch (a) {
        case 1: printf("case a = 1\n"); break;
        case 2: printf("case a = 2\n"); break;
        case 3: printf("case a = 3\n"); break;
        case 4: printf("case a = 4\n"); break;
        default: printf("default value\n"); break;
    }
    
    return 0;
}

switch语句要点:

  1. case标签必须是常量表达式
  2. break语句用于跳出switch,否则会"贯穿"到下一个case
  3. default标签可选,处理所有case都不匹配的情况
  4. 多个case可以共用同一组语句

3.5 while循环

while循环在条件为真时重复执行代码块。

c 复制代码
#include <stdio.h>

int main() {
    int n, i = 0;
    scanf("%d", &n);
    
    // 程序1:1到n的循环(使用i++)
    printf("program 1 : \n");
    while (i < n) {
        printf("%d\n", i + 1);  // 0 ~ (n-1) 转换为 1 ~ n
        i += 1;
    }
    
    // 程序2:使用i <= n的条件
    printf("\nprogram 2 : \n");
    i = 1;
    while (i <= n) {
        printf("%d\n", i);
        i += 1;
    }
    
    // 程序3:使用后置递增
    printf("program 3 : \n");
    i = 1;
    while (i <= n) printf("%d\n", i++);
    
    // 程序4:使用前置递增
    printf("program 4 : \n");
    i = 0;
    while (i < n) printf("%d\n", ++i);
    
    return 0;
}

3.6 do-while循环

do-while循环先执行一次循环体,再判断条件。

c 复制代码
#include <stdio.h>

int main() {
    int n, m = 0;
    scanf("%d", &n);
    
    // 计算n的位数
    do {
        n /= 10;  // 每次除以10
        m += 1;   // 计数加1
    } while (n);  // 直到n变为0
    
    printf("digits : %d\n", m);
    
    return 0;
}

while与do-while的区别:

  • while:先判断条件,后执行循环体。可能一次都不执行。
  • do-while:先执行循环体,后判断条件。至少执行一次。

3.7 for循环

for循环是最常用的循环结构,适合已知循环次数的情况。

c 复制代码
#include <stdio.h>

int main() {
    int n, i;
    scanf("%d", &n);
    
    // 基本的for循环
    for (i = 0; i < n; i += 1) {
        printf("%d ", i + 1);
    }
    printf("\n");
    
    // 使用i <= n的条件
    for (i = 1; i <= n; i += 1) {
        printf("%d ", i);
    }
    printf("\n");
    
    // for循环的另一种写法:将条件判断放在循环体内
    for (i = 0; ; i += 1) {
        if (i == n) break;  // 满足条件时跳出循环
        printf("%d ", i + 1);
    }
    printf("\n");
    
    return 0;
}

3.8 break和continue

break用于跳出循环,continue用于跳过本次循环。

c 复制代码
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    
    // break:提前结束循环
    for (int i = 0; i < n; i++) {
        printf("%d ", i + 1);
        if (i + 1 == 8) break;  // 当i+1等于8时,退出循环
    }
    printf("\n");
    
    // continue:跳过本次循环的剩余部分
    for (int i = 1; i <= n; i++) {
        if (i % 3 == 0) continue;  // 跳过3的倍数
        printf("%d ", i);
    }
    printf("\n");
    
    return 0;
}

3.9 goto语句

goto是最原始的控制流语句,可以无条件跳转到程序中的某个标签处。

c 复制代码
#include <stdio.h>

int main() {
    goto lab_1;  // 跳转到lab_1标签处
    printf("hello world\n");  // 这行不会执行
lab_1:
    printf("hello hangzhou\n");  // 从这里开始执行
    
    //--------------------//
    goto lab_2;
    int a = 0, b = 0;  // 声明语句可以在任何地方
    scanf("%d%d", &a, &b);
lab_2:
    printf("%d\n", a * b);
    
    return 0;
}

goto的典型应用:跳出多重循环:

c 复制代码
#include <stdio.h>

int main() {
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++) {
            if (i * j == 45) {
                printf("Found: i=%d, j=%d\n", i, j);
                goto found;  // 同时跳出两层循环
            }
        }
    }
found:
    printf("Search complete\n");
    
    return 0;
}

警告:过度使用goto会使代码难以理解和维护。一般情况下,应该优先使用循环和函数来控制程序流程。

3.10 用goto模拟if-else

c 复制代码
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    
    // 用goto模拟if-else结构
    n % 2 == 0 && ({ goto if_stmt; 1; });
    !(n % 2 == 0) && ({ goto else_stmt; 1;});
    
if_stmt:
    printf("%d is even\n", n);
    goto if_end;
    
else_stmt:
    printf("%d is odd\n", n);
    
if_end:
    return 0;
}

3.11 用goto模拟循环

c 复制代码
#include <stdio.h>

int main() {
    int n, i = 1;
    scanf("%d", &n);
    
    // 用goto模拟while循环
judge:
    if (i <= n) goto stmt;
    else goto while_end;
stmt:
    printf("%d ", i);
    i += 1;
    goto judge;
while_end:
    printf("\n");
    
    return 0;
}
c 复制代码
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    int i = 1;
    
    // 用goto模拟for循环:打印1到n中不能被3整除的数
for_1:
    goto for_2;
for_2:
    if (i <= n) goto for_4;
    else goto for_end;
for_3:
    i++;
    goto for_2;
for_4:
    if (i % 3 == 0) goto for_3;  // 如果能被3整除,跳过打印
    printf("%d ", i);
    goto for_3;
for_end:
    printf("\n");
    
    return 0;
}

第四章:函数与程序结构

4.1 代码块与变量作用域

在C语言中,用大括号{}包围的代码形成了一个代码块,每个代码块可以有自己的变量。

c 复制代码
#include <stdio.h>

int main() {
    int a = 1, b = 2;
    
    // 内部代码块有自己的变量
    for (int i = 0; i < 10; i++) {
        int a = 3, b = 4;  // 隐藏了外层的a和b
        printf("line 14: a = %d, b = %d\n", a, b);
    }
    
    // 循环结束后,内部的a和b已经不存在了
    printf("line 16: a = %d, b = %d\n", a, b);  // 输出 1 和 2
    
    return 0;
}

作用域规则:

  1. 局部变量:在代码块内部声明的变量,只在该代码块内有效
  2. 全局变量:在所有函数外部声明的变量,在整个文件内有效
  3. 同名变量:内部代码块可以声明与外层同名的变量,这会"遮蔽"外层变量

4.2 函数的定义和调用

函数是组织代码的基本单元,可以提高代码的复用性和可读性。

c 复制代码
#include <stdio.h>
#include <math.h>

// 求和函数
int sum(int a, int b) {
    return a + b;
}

// 根据flag选择不同的数学运算
double select_function(int flag, double x) {
    switch (flag) {
        case 1: return sqrt(x);   // 开平方
        case 2: return x * x;     // 平方
        default: printf("error flag\n"); break;
    }
    return 0;
}

// 无返回值的函数
void print_hello_world(int n) {
    for (int i = 0; i < n; i++) {
        printf("hello world\n");
    }
    return ;  // 可以省略
}

int main() {
    printf("3 + 4 = %d\n", sum(3, 4));
    printf("sqrt(3) = %lf\n", select_function(1, 3));
    printf("4 * 4 = %lf\n", select_function(2, 4));
    print_hello_world(3);
    printf("------------\n");
    print_hello_world(5);
    
    return 0;
}

4.3 函数参数传递

C语言使用值传递,函数内部修改参数不会影响实参。

c 复制代码
#include <stdio.h>

void test(int a, int b) {
    a += 1;      // 修改的是副本
    b *= 2;      // 修改的是副本
    printf("test: a = %d, b = %d\n", a, b);
}

int main() {
    int a = 1, b = 2;
    test(a, b);  // 调用后a和b不变
    printf("main: a = %d, b = %d\n", a, b);  // 输出 1 和 2
    
    test(b, a);  // 参数顺序不同
    printf("main: a = %d, b = %d\n", a, b);  // 仍然输出 1 和 2
    
    return 0;
}

4.4 递归函数

递归函数是调用自身的函数,是解决某些问题的强大工具。

c 复制代码
#include <stdio.h>

// 计算等差数列前n项和
int sum(int a1, int r, int d) {
    int n = (r - a1) / d + 1;      // 计算项数
    int an = a1 + (n - 1) * d;     // 计算末项
    return (a1 + an) * n / 2;      // 求和公式
}

int main() {
    printf("%d %d %d\n", 
        sum(1, 100, 1),     // 1+2+...+100 = 5050
        sum(1, 100, 2),     // 1+3+5+...+99 = 2500
        sum(26, 36999, 47)   // 等差数列求和
    );
    
    return 0;
}

4.5 递归与循环

递归和循环都能实现重复执行,但各有优劣。

c 复制代码
#include <stdio.h>

// 使用递归打印1到n
void print(int n) {
    if (n == 1) {
        printf("%d\n", n);
        return ;
    }
    print(n - 1);       // 先递归调用
    printf("%d\n", n);   // 在回溯时打印
}

int cnt = 10;

int main() {
    // 递归调用main函数(非常规用法,仅作演示)
    int n = cnt;
    cnt -= 1;
    if (n == 1) {
        printf("main : %d\n", n);
        return 0;
    }
    main();            // 递归调用main
    printf("main : %d\n", n);
    
    // 正常递归示例
    printf("\n使用递归打印1到5:\n");
    print(5);
    
    return 0;
}

4.6 最大公约数(GCD)递归

欧几里得算法可以用递归实现:

c 复制代码
#include <stdio.h>

int gcd(int a, int b) {
    if (b == 0) return a;           // 递归终止条件
    return gcd(b, a % b);           // 递归调用
}

int main() {
    int a, b;
    while (scanf("%d%d", &a, &b) != EOF) {
        printf("gcd(%d, %d) = %d\n", a, b, gcd(a, b));
    }
    
    return 0;
}

4.7 变长参数函数

C语言支持接受可变数量参数的函数。

c 复制代码
#include <stdio.h>
#include <stdarg.h>
#include <inttypes.h>

int max_int(int n, ...) {
    va_list args;
    va_start(args, n);           // 初始化参数列表
    int ans = INT32_MIN;         // 最小的32位整数
    for (int i = 0; i < n; i++) {
        int a = va_arg(args, int);  // 逐个获取参数
        if (a > ans) ans = a;
    }
    va_end(args);                // 结束参数处理
    return ans;
}

int main() {
    printf("max_int(3, 8, 7, 9) = %d\n", max_int(3, 8, 7, 9));
    printf("max_int(2, 8, 7, 9) = %d\n", max_int(2, 8, 7, 9));
    
    return 0;
}

4.8 main函数的参数

main函数可以接受命令行参数。

c 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    // 检查程序名称
    if (strcmp(argv[0], "./program") != 0) {
        printf("wrong call, please call : ./program\n");
        return 0;
    }
    
    printf("argc = %d\n", argc);  // 参数个数(包括程序名)
    
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    
    return 0;
}

4.9 main函数的环境变量

c 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[], char **env) {
    int flag = 0;
    
    // 遍历环境变量
    for (char **p = env; p[0]; p += 1) {
        if (strcmp(p[0], "LOGNAME=username") == 0) {
            flag = 1;
            break;
        }
    }
    
    if (flag == 0) {
        printf("error log name, please use username\n");
        return 0;
    }
    
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    
    return 0;
}

4.10 函数的前向声明

当函数相互调用时,需要使用前向声明。

c 复制代码
#include <stdio.h>

// 前向声明
int a_func(int);
int b_func(int);

int main() {
    a_func(1); // a_func调用b_func
    b_func(2); // b_func调用a_func
    return 0;
}

int b_func(int x) {
    switch (x) {
        case 1: printf("3 * x = %d\n", 3 * x); break;
        case 2: return a_func(x);  // 调用后面定义的a_func
    }
    return 0;
}

int a_func(int x) {
    switch (x) {
        case 1: return b_func(x);  // 调用上面定义的b_func
        case 2: printf("2 * x = %d\n", 2 * x); break;
    }
    return 0;
}

第五章:指针与数组

5.1 地址与指针

指针是C语言的精髓,它存储了变量的内存地址。

c 复制代码
#include <stdio.h>

int main() {
    int a = 123;
    
    // 使用%p格式输出地址
    printf("&a = %p\n", &a);
    
    return 0;
}

5.2 十六进制与地址

地址通常用十六进制表示。

c 复制代码
#include <stdio.h>

int main() {
    int a = 0x6a;  // 十六进制数
    
    printf("a(10) = %d\n", a);   // 十进制输出
    printf("a(16) = %x\n", a);   // 小写十六进制
    printf("a(16) = %X\n", a);   // 大写十六进制
    
    int max_int = 0x7fffffff;    // INT32_MAX
    int min_int = 0x80000000;    // INT32_MIN
    printf("MAX_INT : %d\n", max_int);
    printf("MIN_INT : %d\n", min_int);
    
    // 从键盘输入十六进制数
    printf("input hex : ");
    scanf("%x", &a);
    printf("a(10) = %d\n", a);
    printf("a(16) = %x\n", a);
    printf("a(16) = %X\n", a);
    
    return 0;
}

5.3 指针类型

不同类型的指针大小相同,但它们指向不同类型的数据。

c 复制代码
#include <stdio.h>

int main() {
    int a;
    double b;
    char c;
    
    // 指针的大小在同一个系统上是相同的
    printf("sizeof(int &) = %lu\n", sizeof(&a));
    printf("sizeof(double &) = %lu\n", sizeof(&b));
    printf("sizeof(char &) = %lu\n", sizeof(&c));
    
    return 0;
}

5.4 指针的使用

指针变量存储地址,通过*运算符访问指针指向的值。

c 复制代码
#include <stdio.h>

int main() {
    int *p1;
    double *p2;
    char *p3;
    
    int a = 123;
    double b = 45.6;
    char c = 'h';
    
    p1 = &a;   // 让p1指向a
    p2 = &b;   // 让p2指向b
    p3 = &c;   // 让p3指向c
    
    printf("p1 = %p, &a = %p\n", p1, &a);
    printf("p2 = %p, &b = %p\n", p2, &b);
    printf("p3 = %p, &c = %p\n", p3, &c);
    
    printf("*p1 = %d\n", *p1);   // 通过指针访问值
    printf("*p2 = %lf\n", *p2);
    printf("*p3 = %c\n", *p3);
    
    return 0;
}

5.5 指针与数组

数组名本质上是一个指针,指向数组的第一个元素。

c 复制代码
#include <stdio.h>

void test1() {
    int a[10]; // 定义数组,元素为 a[0] 到 a[9]
    for (int i = 0; i < 10; i++) {
        a[i] = 2 * i;
    }
    for (int i = 0; i < 10; i++) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return ;
}

void test2() {
    int n;
    printf("input n : ");
    scanf("%d", &n);
    int a[2 * n];  // 变长数组(C99标准支持)
    for (int i = 0; i < 2 * n; i++) {
        a[i] = 3 * i;
    }
    for (int i = 0; i < 2 * n; i++) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return ;
}

void test3() {
    int a[5] = {0};  // 初始化所有元素为0
    for (int i = 0; i < 5; i++) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return ;
}

void test4() {
    int a[5] = {1, 2, 3, 4, 5};  // 初始化指定值
    for (int i = 0; i < 5; i++) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return ;
}

void test5() {
    int a[] = {1, 2, 3, 4, 5, 6, 7};  // 自动计算长度
    size_t size = sizeof(a) / sizeof(int);
    printf("sizeof(a) / sizeof(int) = %lu\n", size);
    
    // 数组名就是首元素地址
    printf("a = %p\n", a);
    printf("&a[0] = %p\n", &a[0]);
    
    // 各元素的地址
    for (int i = 0; i < size; i++) {
        printf("&a[%d] = %p\n", i, &a[i]);
    }
    
    return ;
}

int main() {
    test5();
    return 0;
}

5.6 素数筛选

埃拉托斯特尼筛法(Sieve of Eratosthenes)是找素数的经典算法。

c 复制代码
#include <stdio.h>

int prime[100] = {0};

void init_prime(int n) {
    prime[0] = prime[1] = 1;  // 0和1不是素数
    for (int i = 2; i * i <= n; i++) {
        if (prime[i]) continue;  // 如果i是合数,跳过
        printf("%d is prime : ", i);
        for (int j = i * i; j <= n; j += i) {
            prime[j] = 1;
            printf(" %d", j);
        }
        printf("\n");
    }
    return ;
}

int main() {
    init_prime(50);
    int x;
    while (scanf("%d", &x) != EOF) {
        printf("prime[%d] = %d\n", x, prime[x]);
    }
    
    return 0;
}

5.7 二分查找

二分查找是高效的在有序序列中查找元素的算法。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    srand(time(0));
    
    // 生成一个递增的随机数组
    int arr[10] = {0};
    for (int i = 1; i < 10; i++) {
        arr[i] = arr[i - 1] + (rand() % 10);
    }
    
    // 打印数组
    int len = 0;
    for (int i = 0; i < 10; i++) {
        len += printf("%4d", i);
    }
    printf("\n");
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    for (int i = 0; i < 10; i++) {
        printf("%4d", arr[i]);
    }
    printf("\n");
    
    // 二分查找
    int x;
    while (scanf("%d", &x) != EOF) {
        int cnt1 = 0, cnt2 = 0, flag1 = 0, flag2 = 0;
        
        // 线性查找
        for (int i = 0; i < 10; i++) {
            cnt1 += 1;
            if (arr[i] != x) continue;
            flag1 = 1;
            break;
        }
        
        // 二分查找
        int l = 0, r = 9, mid;
        while (l <= r) {
            cnt2 += 1;
            mid = (l + r) >> 1;  // (l + r) / 2
            if (arr[mid] == x) {
                printf("(%d) arr[%d] = %d, find %d\n", cnt2, mid, arr[mid], x);
                flag2 = 1;
                break;
            }
            if (arr[mid] > x) {
                printf("(%d) arr[%d] = %d > %d, change [%d, %d] to [%d, %d]\n", 
                      cnt2, mid, arr[mid], x, l, r, l, mid - 1);
                r = mid - 1;
            } else {
                printf("(%d) arr[%d] = %d < %d, change [%d, %d] to [%d, %d]\n", 
                      cnt2, mid, arr[mid], x, l, r, mid + 1, r);
                l = mid + 1;
            }
        }
        
        printf("flag1 = %d, cnt1 = %d\n", flag1, cnt1);
        printf("flag2 = %d, cnt2 = %d\n", flag2, cnt2);
    }
    
    return 0;
}

5.8 二维数组

二维数组是数组的数组,可以看作矩阵。

c 复制代码
#include <stdio.h>

int main() {
    int b[3][4], cnt = 1;
    
    // 按行初始化
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            b[i][j] = cnt;
            cnt += 1;
        }
    }
    
    // 打印(按行)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", b[i][j]);
        }
        printf("\n");
    }
    
    printf("-------------\n");
    
    // 按列初始化
    cnt = 1;
    for (int j = 0; j < 4; j++) {
        for (int i = 0; i < 3; i++) {
            b[i][j] = (cnt++);
        }
    }
    
    // 打印
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", b[i][j]);
        }
        printf("\n");
    }
    
    return 0;
}

5.9 字符串处理

字符串在C语言中是以'\0'结尾的字符数组。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main() {
    srand(time(0));
    
    // 字符串复制
    char str1[10] = "abc";
    printf("str1 = %s\n", str1);
    strcpy(str1, "def");  // 复制字符串
    printf("str1 = %s\n", str1);
    
    // 字符串长度与大小
    char str2[] = "hello\0 world";  // 包含隐藏的\0
    printf("strlen(str2) = %lu\n", strlen(str2));  // 5,遇到第一个\0停止
    printf("sizeof(str2) = %lu\n", sizeof(str2));  // 13,整个数组大小
    
    // 字符串比较
    char str3[] = "abcdef", str4[] = "abc";
    printf("strcmp(str3, str4) = %d\n", strcmp(str3, str4));  // 正数,str3 > str4
    str3[3] = '\0';  // 截断str3
    printf("strcmp(str3, str4) = %d\n", strcmp(str3, str4));  // 0,相等
    
    // memset函数
    int arr[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = rand() % 100;
    }
    
    // 设置数组为0
    memset(arr, 0, sizeof(int) * 10);  // 按字节设置
    for (int i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    
    // 设置数组为-1
    memset(arr, -1, sizeof(int) * 10);
    for (int i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    
    // 设置数组为2(二进制:0x02020202)
    memset(arr, 2, sizeof(int) * 10);
    for (int i = 0; i < 10; i++) {
        printf("arr[%d] = %d (%x)\n", i, arr[i], arr[i]);
    }
    
    return 0;
}

5.10 指针算术运算

指针可以加减整数,移动到相邻的内存位置。

c 复制代码
#include <stdio.h>

int main() {
    int a, *p1 = &a;
    double b, *p2 = &b;
    
    printf("&a = %p\n", &a);
    printf("p1 + 0 = %p\n", p1 + 0);
    printf("p1 + 1 = %p\n", p1 + 1);  // 移动sizeof(int)个字节
    printf("p1 + 2 = %p\n", p1 + 2);
    printf("p1 + 3 = %p\n", p1 + 3);
    
    printf("&b = %p\n", &b);
    printf("p2 + 0 = %p\n", p2 + 0);
    printf("p2 + 1 = %p\n", p2 + 1);  // 移动sizeof(double)个字节
    
    // 数组指针
    int arr[4] = {1, 2, 3, 4};
    int *p3 = arr;  // arr等价于&arr[0]
    for (int i = 0; i < 4; i++) {
        printf("p3 + %d = %p\n", i, p3 + i);
        printf("&arr[%d] = %p\n", i, &arr[i]);
    }
    
    // 数组指针的指针
    int (*p4)[10] = 0x0;
    int arr2[30][10];
    p4 = arr2;
    printf("p4 + 0 = %p\n", p4 + 0);
    printf("p4 + 1 = %p\n", p4 + 1);
    printf("p4 + 2 = %p\n", p4 + 2);
    
    // 指针数组
    int *p5[10];      // 10个int指针的数组
    int *(*p6[10])[20];  // 复杂的指针声明
    
    return 0;
}

5.11 指针与函数

使用指针可以在函数中修改外部变量。

c 复制代码
#include <stdio.h>

void add_once(int *p) {
    *p += 1;  // 修改指针指向的值
    return ;
}

void f(int n, int *sum_addr) {
    *sum_addr = (1 + n) * n / 2;  // 计算1到n的和
    return ;
}

void output(int *p, int n) {
    for (int i = 0; i < n; i++) {
        printf("p[%d] = %d\n", i, p[i]);
    }
    return ;
}

int main() {
    int a = 123;
    printf("a = %d\n", a);
    add_once(&a);  // 传入a的地址
    printf("a = %d\n", a);  // a变为124
    
    int n = 10, sum;
    f(n, &sum);
    printf("sum = %d\n", sum);  // sum = 55
    
    int arr[10] = {9, 8, 5, 3, 6, 2, 1, 0, 4, 7};
    output(arr, 10);
    
    return 0;
}

5.12 字节序与指针

通过指针可以查看数据在内存中的存储方式。

c 复制代码
#include <stdio.h>

int main() {
    int n = 0x61626364;  // 'a'='61', 'b'='62', 'c'='63', 'd'='64'
    char *p = (char *)&n;  // 将int指针转换为char指针
    
    // 逐字节输出
    printf("*(p + 0) = %c\n", *(p + 0));
    printf("*(p + 1) = %c\n", *(p + 1));
    printf("*(p + 2) = %c\n", *(p + 2));
    printf("*(p + 3) = %c\n", *(p + 3));
    
    // 使用数组语法
    printf("p[0] = %c\n", p[0]);
    printf("p[1] = %c\n", p[1]);
    printf("p[2] = %c\n", p[2]);
    printf("p[3] = %c\n", p[3]);
    
    // 在小端序机器上,输出为 dcba
    // 在大端序机器上,输出为 abcd
    
    return 0;
}

5.13 函数指针

函数指针是指向函数的指针,可以用来实现回调函数。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void test1() {
    printf("function test1\n");
}

void test2() {
    printf("function test2\n");
}

void test3() {
    printf("function test3\n");
}

void (*p)();  // 函数指针变量

int main() {
    srand(time(0));
    
    // 赋值和调用
    p = test1;
    p();
    p = test2;
    p();
    p = test3;
    p();
    
    // 函数指针数组
    void (*arr[3])() = {test1, test2, test3};
    for (int i = 0; i < 10; i++) {
        arr[rand() % 3]();  // 随机调用一个函数
    }
    
    return 0;
}

5.14 动态内存分配

动态内存分配允许程序在运行时决定内存大小。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    // malloc:分配内存(不初始化)
    int *arr1 = (int *)malloc(sizeof(int) * 10);
    for (int i = 0; i < 10; i++) {
        printf("arr1[%d] = %d\n", i, arr1[i]);  // 未初始化的值
    }
    
    // calloc:分配内存并初始化为0
    int *arr2 = (int *)calloc(10, sizeof(int));
    for (int i = 0; i < 10; i++) {
        printf("arr2[%d] = %d\n", i, arr2[i]);  // 全为0
    }
    
    // 释放内存
    free(arr1);
    free(arr2);
    
    // memcpy和memmove
    char s1[100] = "hello world";
    char s2[100];
    char s3[100];
    
    memcpy(s2, s1, 12);      // 复制12个字节(包括\0)
    memmove(s3, s1, 12);
    printf("s2 = %s\n", s2);
    printf("s3 = %s\n", s3);
    
    // 内存重叠时的复制
    memcpy(s2 + 4, s2, 12);    // memcpy不处理重叠,可能出错
    memmove(s3 + 4, s3, 12);   // memmove安全处理重叠区域
    printf("s2 = %s\n", s2);
    printf("s3 = %s\n", s3);
    
    return 0;
}

5.15 const与指针

const关键字可以限制指针或指针指向的数据的可修改性。

c 复制代码
#include <stdio.h>

int main() {
    const int a = 123;
    const int b = 456;
    
    const int *p1 = &a;  // 指针可以改变,但不能通过它修改a
    printf("*p1 = %d\n", *p1);
    p1 = &b;  // 合法:改变指针指向
    printf("*p1 = %d\n", *p1);
    
    int const *p2 = &a;  // 与上面等价
    
    int n = 789, m = 1000;
    int *const p3 = &n;  // 指针不能改变
    // p3 = &m;  // 非法:不能改变指针指向
    // 但可以通过p3修改n的值
    
    return 0;
}

5.16 typedef

typedef用于为类型创建别名,使代码更易读。

c 复制代码
#include <stdio.h>

typedef long long LL;           // 为long long创建别名
typedef int (*Arr2Dim10)[10];   // 指向10元素int数组的指针
typedef void (*Func)();         // 无参数无返回值的函数指针

void test() {
    printf("hello function pointer\n");
}

int main() {
    LL a;  // 相当于 long long a
    printf("sizeof(a) = %lu\n", sizeof(a));
    
    int arr[5][10];
    Arr2Dim10 p1 = arr;  // 相当于 int (*p1)[10] = arr;
    
    Func p2 = test;  // 相当于 void (*p2)() = test;
    p2();
    
    return 0;
}

5.17 qsort函数

qsort是C标准库提供的快速排序函数。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int *getRandData(int n) {
    int *arr = (int *)malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) arr[i] = rand() % 100;
    return arr;
}

void output(int *arr, int n) {
    int len = 0;
    // 第一行:索引
    for (int i = 0; i < n; i++) {
        len += printf("%3d", i);
    }
    printf("\n");
    // 第二行:分隔线
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    // 第三行:数据
    for (int i = 0; i < n; i++) {
        printf("%3d", arr[i]);
    }
    printf("\n\n");
}

// 降序排序的比较函数
int comp(const void *p1, const void *p2) {
    const int *a = (const int *)p1;
    const int *b = (const int *)p2;
    if (*a > *b) return -1;  // 降序:大的在前
    if (*a == *b) return 0;
    return 1;
}

// 升序排序(方法1)
int comp2(const void *p1, const void *p2) {
    return *((const int *)p1) - *((const int *)p2);
}

// 升序排序(方法2)
int comp3(const void *p1, const void *p2) {
    return *((const int *)p2) - *((const int *)p1);
}

int main() {
    srand(time(0));
    int *arr = getRandData(10);
    output(arr, 10);
    
    printf("use comp : \n");
    qsort(arr, 10, sizeof(int), comp);  // 使用comp降序
    output(arr, 10);
    
    printf("use comp2 : \n");
    qsort(arr, 10, sizeof(int), comp2);  // 使用comp2升序
    output(arr, 10);
    
    printf("use comp3 : \n");
    qsort(arr, 10, sizeof(int), comp3);  // 使用comp3升序
    output(arr, 10);
    
    return 0;
}

5.18 回调函数与二分查找

使用函数指针实现通用的二分查找算法。

c 复制代码
#include <stdio.h>
#include <math.h>

typedef double (*FUNC_T)(double);  // 函数指针类型

double f1(double x) {
    return x * x;
}

double f2(double x) {
    return 3 * x * x + 3 * x - 5;
}

double f3(double x) {
    return pow(1.2, x);
}

// 二分查找:找到使func(x) = y的x
double binary_search(FUNC_T func, double y) {
    double l = 0, r = 1000, mid;
    while (r - l > 0.0000001) {
        mid = (l + r) / 2.0;
        if (func(mid) < y) l = mid;
        else r = mid;
    }
    return l;
}

int main() {
    double y;
    while (scanf("%lf", &y) != EOF) {
        double x1 = binary_search(f1, y);
        double x2 = binary_search(f2, y);
        double x3 = binary_search(f3, y);
        printf("x1 = %lf, f1(%lf) = %lf\n", x1, x1, f1(x1));
        printf("x2 = %lf, f2(%lf) = %lf\n", x2, x2, f2(x2));
        printf("x3 = %lf, f3(%lf) = %lf\n", x3, x3, f3(x3));
    }
    
    return 0;
}

5.19 个人所得税计算

综合应用函数指针和二分查找解决实际问题。

c 复制代码
#include <stdio.h>

typedef double (*FUNC_T)(double);

// 税后工资计算函数
double f(double x) {
    double y = 0;
    y += (x <= 3000 ? x : 3000) * 0.03;
    if (x > 3000) y += (x <= 12000 ? x - 3000 : 9000) * 0.1;
    if (x > 12000) y += (x <= 25000 ? x - 12000 : 13000) * 0.2;
    if (x > 25000) y += (x <= 35000 ? x - 25000 : 10000) * 0.25;
    if (x > 35000) y += (x <= 55000 ? x - 35000 : 20000) * 0.3;
    if (x > 55000) y += (x <= 80000 ? x - 55000 : 25000) * 0.35;
    if (x > 80000) y += (x - 80000) * 0.45;
    return x - y;  // 返回税后收入
}

// 二分查找:已知税后收入y,求税前收入x
double binary_search(FUNC_T func, double y) {
    double l = 0, r = 2 * y, mid;
    while (r - l > 1e-6) {
        mid = (l + r) / 2.0;
        if (func(mid) < y) l = mid;
        else r = mid;
    }
    return l;
}

int main() {
    double y;
    while (scanf("%lf", &y) != EOF) {
        printf("f(%.2lf) = %lf\n", binary_search(f, y), y);
    }
    
    return 0;
}

第六章:预处理命令与结构体

6.1 预处理命令简介

预处理指令在编译之前执行,用于包含头文件、定义宏、条件编译等。

c 复制代码
#include <stdio.h>

int main() {
    printf("hello world\n");
    return 0;
}

6.2 宏定义

#define指令用于定义宏,可以是常量宏或函数宏。

c 复制代码
#include <stdio.h>

#define PI 3.1415926           // 常量宏
#define S(a, b) a * b          // 函数宏(不安全)
#define P(a) { \
    printf("define P : %d\n", a); \
}

int main() {
    printf("PI = %lf\n", PI);
    printf("S(3, 4) = %d\n", S(3, 4));         // 正常情况
    printf("S(3 + 7, 4) = %d\n", S(3 + 7, 4)); // 错误:3+7*4=31而非40
    
    // 函数宏用作代码块
    int n = 123;
    int *p;
    p = &n;
    P(*p);  // 相当于 { printf("define P : %d\n", *p); }
    
    return 0;
}

6.3 内置宏

C语言提供了一些预定义的宏:

c 复制代码
#include <stdio.h>

int main() {
    printf("__DATE__ = %s\n", __DATE__);           // 编译日期
    printf("__TIME__ = %s\n", __TIME__);           // 编译时间
    printf("__LINE__ = %d\n", __LINE__);          // 当前行号
    printf("__FILE__ = %s\n", __FILE__);          // 当前文件名
    printf("__func__ = %s\n", __func__);          // 当前函数名
    printf("__PRETTY_FUNCTION__ = %s\n", __PRETTY_FUNCTION__);  // 更详细的函数信息
    
    return 0;
}

6.4 #与##运算符

#运算符将宏参数转换为字符串,##运算符连接两个标记。

c 复制代码
#include <stdio.h>

#define STR(n) #n              // 将参数转换为字符串
#define RUN(func) { \
    func; \
    printf("%s done\n", #func); \
}

void test1() {
    printf("this is test1\n");
}

void test2(int a, int b) {
    printf("this is test2 : %d, %d\n", a, b);
}

#define CAT(a, b) a##b        // 连接两个标记

int main() {
    printf("%s\n", STR(hello));  // 输出:hello
    
    RUN(test1());               // 输出 test1 done
    RUN(test2(2, 3));           // 输出 test2(2, 3) done
    
    int n10 = 123, n11 = 456;
    CAT(n, 10) = 789;           // 相当于 n10 = 789
    printf("n10 = %d, n11 = %d\n", n10, n11);
    
    return 0;
}

6.5 宏的高级用法

使用do-while(0)包装宏,使其成为单个语句。

c 复制代码
#include <stdio.h>

#define P(func) { \
    printf("%s = %d\n", #func, func); \
}

#define MAX(a, b) ({ \
    int __a = (a), __b = (b); \
    __a > __b ? __a : __b; \
})

int main() {
    P(MAX(2, 3));
    P(5 + MAX(2, 3));
    P(MAX(2, MAX(3, 4)));
    P(MAX(2, 3 > 4 ? 3 : 4));
    
    int a = 7;
    P(MAX(a++, 6));  // 表达式求值两次的问题
    printf("after max a = %d\n", a);  // a变成了8
    
    return 0;
}

6.6 条件编译

#ifdef、#ifndef、#if等指令用于条件编译。

c 复制代码
#include <stdio.h>

#ifndef DEBUG
#define DEBUG
#endif

#ifdef DEBUG
int a = 1;
#else 
int a = 2;
#endif

int main() {
    printf("a = %d\n", a);  // 输出1,因为定义了DEBUG
    
    return 0;
}

6.7 #ifdef的应用

条件编译可以用于平台适配、调试开关等功能。

c 复制代码
#include <stdio.h>

#ifdef CLOCK
void test1() {
    printf("this is test1\n");
}
#endif

#ifdef PHONE
void test2() {
    printf("this is test2\n");
}
#endif

int main() {
#ifdef CLOCK
    test1();
#endif
#ifdef PHONE
    test2();
#endif
    
    return 0;
}

// 编译时:
// gcc -DCLOCK test.c   激活CLOCK,输出this is test1
// gcc -DPHONE test.c   激活PHONE,输出this is test2
// gcc -DCLOCK -DPHONE test.c  激活两者

6.8 结构体基础

结构体是一种复合数据类型,可以将不同类型的数据组合在一起。

c 复制代码
#include <stdio.h>

struct A {
    char a;
    short b;
};

struct B {
    struct A a;
    struct {
        char b;
        double c;
    } d;
    int e;
};

typedef struct person {
    char name[20];
    int age;
    char gender;
    float height;
} person;

void output(struct person *p) {
    printf("use pointer p : (%s, %d, %c, %f)\n", 
        p->name, 
        p->age,
        p->gender,
        p->height
    );
    return ;
}

void output_person() {
    int n = sizeof(struct person), len = 0;
    char buff[n];
    for (int i = 0; i < n; i++) buff[i] = '.';
    
    // 打印布局
    for (int i = 0; i < n; i++) {
        len += printf("%3d", i);
    }
    printf("\n");
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    
    struct person p;
    // 模拟内存布局
    for (int i = 0; i < n; i++) {
        printf("%3c", buff[i]);
    }
    printf("\n");
    
    return ;
}

int main() {
    struct person p1 = {"Zhang San", 18, 'M', 1.75f};
    person p2 = {"Li Si", 49, 'M', 1.80f};
    
    printf("(%s, %d, %c, %f)\n",
        p1.name,
        p1.age, 
        p1.gender,
        p1.height
    );
    
    output(&p2);
    output(&p1);
    
    printf("sizeof(struct person) = %lu\n", sizeof(struct person));
    output_person();
    
    return 0;
}

6.9 结构体的内存对齐

结构体的大小不等于各成员大小之和,因为存在内存对齐。

c 复制代码
#include <stdio.h>

void set_buff(char *buff, void *head, void *begin, void *end, char ch) {
    while (begin != end) {
        buff[begin - head] = ch;
        begin += 1;
    }
    return ;
}

#ifdef STRUCT_A
#define TYPE struct A

struct A {
    char  a;  // 1字节
    int   b;  // 4字节
    short c;  // 2字节
};

void output_struct() {
    int n = sizeof(TYPE), len = 0;
    char buff[n];
    for (int i = 0; i < n; i++) buff[i] = '.';
    
    for (int i = 0; i < n; i++) {
        len += printf("%3d", i);
    }
    printf("\n");
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    
    TYPE obj;
    set_buff(buff, &obj.a, &obj.a, 1 + (void *)&obj.a, 'a');
    set_buff(buff, &obj.a, &obj.b, 4 + (void *)&obj.b, 'b');
    set_buff(buff, &obj.a, &obj.c, 2 + (void *)&obj.c, 'c');
    
    for (int i = 0; i < n; i++) {
        printf("%3c", buff[i]);
    }
    printf("\n");
    printf("sizeof(struct A) = %d\n", n);
}

#endif

#ifdef STRUCT_B
#define TYPE struct B

struct B {
    char  a;
    short b;
    int   c;
};

void output_struct() {
    int n = sizeof(TYPE), len = 0;
    char buff[n];
    for (int i = 0; i < n; i++) buff[i] = '.';
    
    for (int i = 0; i < n; i++) {
        len += printf("%3d", i);
    }
    printf("\n");
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    
    TYPE obj;
    set_buff(buff, &obj.a, &obj.a, 1 + (void *)&obj.a, 'a');
    set_buff(buff, &obj.a, &obj.b, 2 + (void *)&obj.b, 'b');
    set_buff(buff, &obj.a, &obj.c, 4 + (void *)&obj.c, 'c');
    
    for (int i = 0; i < n; i++) {
        printf("%3c", buff[i]);
    }
    printf("\n");
    printf("sizeof(struct B) = %d\n", n);
}

#endif

#ifdef STRUCT_C
#define TYPE struct C
#pragma pack(1)  // 取消对齐

TYPE {
    char  a;
    short b;
    int   c;
};

void output_struct() {
    int n = sizeof(TYPE), len = 0;
    char buff[n];
    for (int i = 0; i < n; i++) buff[i] = '.';
    
    for (int i = 0; i < n; i++) {
        len += printf("%3d", i);
    }
    printf("\n");
    for (int i = 0; i < len; i++) printf("-");
    printf("\n");
    
    TYPE obj;
    set_buff(buff, &obj.a, &obj.a, 1 + (void *)&obj.a, 'a');
    set_buff(buff, &obj.a, &obj.b, 2 + (void *)&obj.b, 'b');
    set_buff(buff, &obj.a, &obj.c, 4 + (void *)&obj.c, 'c');
    
    for (int i = 0; i < n; i++) {
        printf("%3c", buff[i]);
    }
    printf("\n");
    printf("sizeof(struct C) = %d\n", n);
}

#endif

int main() {
    output_struct();
    return 0;
}

// 编译示例:
// gcc -DSTRUCT_A struct_size.c && ./a.out  // struct A
// gcc -DSTRUCT_B struct_size.c && ./a.out  // struct B
// gcc -DSTRUCT_C struct_size.c && ./a.out  // struct C (紧凑)

6.10 共用体(union)

共用体的所有成员共享同一块内存。

c 复制代码
#include <stdio.h>

union A {
    struct {
        unsigned char byte1;
        unsigned char byte2;
        unsigned char byte3;
        unsigned char byte4;
    } bytes;
    unsigned int number;
};

#define P(a, format) { \
    printf("%s = " format "\n", #a, a); \
}

int main() {
    union A a;
    a.number = 0x61626364;  // 'a'=0x61, 'b'=0x62, 'c'=0x63, 'd'=0x64
    
    P(a.number, "%x");
    P(a.bytes.byte1, "%x");  // d
    P(a.bytes.byte2, "%x");  // c
    P(a.bytes.byte3, "%x");  // b
    P(a.bytes.byte4, "%x");  // a
    
    P(sizeof(union A), "%lu");  // 4字节(取最大成员大小)
    
    P(&a.number, "%p");
    P(&a.bytes.byte1, "%p");  // 相同地址
    P(&a.bytes.byte2, "%p");
    P(&a.bytes.byte3, "%p");
    P(&a.bytes.byte4, "%p");
    
    return 0;
}

6.11 枚举(enum)

枚举用于定义一组命名的整型常量。

c 复制代码
#include <stdio.h>

#define P(a, format) { \
    printf("%s = " format "\n", #a, a); \
}

enum Number {
    zero,      // 0
    one,       // 1
    two = 10,  // 显式赋值
    three,     // 11
    four       // 12
};

enum FUNC_DATA {
#ifdef TEST1
    FUNC_test1,
#endif
#ifdef TEST2
    FUNC_test2,
#endif
#ifdef TEST3
    FUNC_test3,
#endif
#ifdef TEST4
    FUNC_test4,
#endif
    FUNC_MAX  // 作为数组大小
};

#define DEFINE_FUNC(name) \
void name() { \
    printf("this is function : %s\n", #name); \
}

DEFINE_FUNC(test1)
DEFINE_FUNC(test2)
DEFINE_FUNC(test3)
DEFINE_FUNC(test4)

void (*func_arr[FUNC_MAX])() = {
#ifdef TEST1
    test1,
#endif
#ifdef TEST2
    test2,
#endif
#ifdef TEST3
    test3,
#endif
#ifdef TEST4
    test4,
#endif
};

int main() {
    for (int i = 0; i < FUNC_MAX; i++) {
        func_arr[i]();
    }
    
    enum Number a;
    a = zero;   P(a, "%d");  // 0
    a = one;    P(a, "%d");  // 1
    a = two;    P(a, "%d");  // 10
    a = three;  P(a, "%d");  // 11
    
    return 0;
}

6.12 位域

位域允许在结构体中使用指定位数的成员。

c 复制代码
#include <stdio.h>

#define P(a, format) { \
    printf("%s = " format "\n", #a, a); \
}

struct A {
    unsigned int a:1;  // 占1位
    unsigned int b:2;  // 占2位
    unsigned int c:3;  // 占3位
};

int main() {
    P(sizeof(struct A), "%lu");  // 4字节
    
    struct A obj;
    obj.a = 15;  // 只保留最低1位:1
    obj.b = 15;  // 只保留最低2位:3
    obj.c = 13;  // 只保留最低3位:5
    
    P(obj.a, "%d");  // 1
    P(obj.b, "%d");  // 3
    P(obj.c, "%d");  // 5
    
    return 0;
}

6.13 offsetof和container_of宏

这两个宏是Linux内核中的经典实现,用于结构体操作。

c 复制代码
#include <stdio.h>
#include <stddef.h>  // 提供offsetof

#define offset(T, d) ((size_t)(&(((T *)(0))->d)))

struct A {
    int a;
    char b;
    double c;
};

int main() {
    printf("offset(A, a) = %lu\n", offset(struct A, a));
    printf("offset(A, b) = %lu\n", offset(struct A, b));
    printf("offset(A, c) = %lu\n", offset(struct A, c));
    
    return 0;
}

6.14 链表实现

链表是一种常见的数据结构,适合插入删除操作频繁的场景。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define start(addr, T, d) ((T *)(((void *)addr) - offset(T, d)))
#define offset(T, d) ((size_t)(&(((T *)(0))->d)))

enum DataType {
    A_type,
    B_type,
    C_type,
    D_type,
    MAX_type
};

typedef struct Link {
    enum DataType type;
    struct Link *next;
} Link;

// 类型A的数据结构
struct A {
    int x, y;
    Link l;  // 链表节点嵌入在里面
};

void output_A(Link *p) {
    struct A *a = start(p, struct A, l);  // 通过链表节点找到包含它的结构体
    printf("struct A : x = %d, y = %d\n", a->x, a->y);
}

Link *getDataA() {
    struct A *a = (struct A *)malloc(sizeof(struct A));
    a->x = 3, a->y = 4;
    a->l.next = NULL;
    a->l.type = A_type;
    return &(a->l);
}

// 类型B的数据结构
struct B {
    int a;
    Link l;
    double b;
};

void output_B(Link *p) {
    struct B *b = start(p, struct B, l);
    printf("struct B : a = %d, b = %lf\n", b->a, b->b);
}

Link *getDataB() {
    struct B *b = (struct B *)malloc(sizeof(struct B));
    b->a = 100, b->b = 45.3;
    b->l.next = NULL;
    b->l.type = B_type;
    return &(b->l);
}

// 类型C的数据结构
struct C {
    char c;
    Link l;
    const char *s;
    int t;
};

void output_C(Link *p) {
    struct C *c = start(p, struct C, l);
    printf("struct C : c = %c, s = %s, t = %d\n", c->c, c->s, c->t);
}

Link *getDataC() {
    struct C *c = (struct C *)malloc(sizeof(struct C));
    c->c = 'x', c->s = "struct C", c->t = 99;
    c->l.next = NULL;
    c->l.type = C_type;
    return &(c->l);
}

// 类型D的数据结构
struct D {
    Link l;
    long long a;
};

void output_D(Link *p) {
    struct D *d = start(p, struct D, l);
    printf("struct D : a = %lld\n", d->a);
}

Link *getDataD() {
    struct D *d = (struct D *)malloc(sizeof(struct D));
    d->a = INT64_MAX;
    d->l.next = NULL;
    d->l.type = D_type;
    return &(d->l);
}

// 函数指针数组
Link *(*getData[MAX_type])() = {
    getDataA, getDataB, getDataC, getDataD
};

void (*output[MAX_type])(Link *) = {
    output_A, output_B, output_C, output_D
};

int main() {
    #define MAX_OP 10
    Link head, *p = &head;
    
    // 创建链表
    for (int i = 0; i < MAX_OP; i++) {
        p->next = getData[rand() % MAX_type]();
        p = p->next;
    }
    
    // 遍历链表
    for (p = head.next; p; p = p->next) {
        output[p->type](p);
    }
    
    return 0;
}

第七章:重学输入输出

7.1 标准输入输出流

C语言有三个标准的流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)。

c 复制代码
#include <stdio.h>

int main() {
    printf("hello world\n"); // 默认输出到stdout
    int n;
    scanf("%d", &n);   // 默认从stdin读取
    perror("out of range\n"); // 输出到stderr
    return 0;
}

7.2 fflush的重要性

fflush用于刷新输出缓冲区。

c 复制代码
#include <stdio.h>

int main() {
    printf("alskdjfliemzi121313i");
    fflush(stdout);  // 刷新stdout缓冲区
    
    // 模拟崩溃
    // *((int *)(0)) = 5;  // 注释掉这行以正常运行
    
    return 0;
}

7.3 fprintf和fscanf

这两个函数用于向文件流中读写数据。

c 复制代码
#include <stdio.h>

int main() {
    fprintf(stdout, "hello world\n");
    
    int n;
    fscanf(stdin, "%d", &n);
    fprintf(stderr, "n = %d\n", n);
    
    return 0;
}

7.4 freopen重定向

freopen可以重新分配文件流。

c 复制代码
#include <stdio.h>

int main() {
    // 将stdout重定向到文件
    freopen("output.txt", "w", stdout);
    // 将stdin重定向到文件
    freopen("input.txt", "r", stdin);
    
    printf("hello freopen, stdout\n");  // 写入output.txt
    
    char s[1000];
    while (scanf("%[^\n]", s) != EOF) {
        getchar();  // 读取并丢弃换行符
        printf("%s | hello world\n", s);
    }
    
    return 0;
}

7.5 scanf的陷阱

scanf在读取整数后遇到换行符会留下问题。

c 复制代码
#include <stdio.h>

int main() {
    char c1, c2;
    int a, b;
    
    scanf("%d", &a);
    scanf("%c%c", &c1, &c2);  // 第二个%c会读到上次遗留的换行符
    scanf("%d", &b);
    
    printf("a = %d, b = %d\n", a, b);
    printf("c1 = %d, c2 = %d\n", c1, c2);  // c1是换行符(10)
    
    return 0;
}

7.6 解决方案:getchar和fflush

c 复制代码
#include <stdio.h>

#ifdef GETCHAR

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    getchar();  // 吞掉换行符
    char c = 'x';
    scanf("%c", &c);
    printf("a = %d, b = %d, c = %c\n", a, b, c);
    
    return 0;
}

#endif

#ifdef FFLUSH

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    fflush(stdin);  // 刷新输入缓冲区(Windows有效)
    char c = 'x';
    scanf("%c", &c);
    printf("a = %d, b = %d, c = %c\n", a, b, c);
    
    // 继续读取剩余字符
    char ch;
    while (scanf("%c", &ch) != EOF) {
        printf("%c", ch);
    }
    printf("\n");
    
    return 0;
}

#endif

7.7 实现自己的printf

通过变长参数实现简化版的printf函数:

c 复制代码
#include <stdio.h>
#include <stdarg.h>
#include <inttypes.h>

#define TEST(format, args...) { \
    int n1, n2; \
    n1 = printf(format, ##args); \
    n2 = my_printf(format, ##args); \
    printf("n1 = %d, n2 = %d\n", n1, n2); \
}

char base_16_code(int x) {
    if (x < 10) return x + '0';
    return x - 10 + 'a';
}

int my_printf(const char *format, ...) {
    #define PUTC(c) putchar(c), cnt += 1
    va_list args;
    va_start(args, format);
    int cnt = 0;
    
    for (int i = 0; format[i]; i++) {
        switch (format[i]) {
            case '%': {
                switch (format[i + 1]) {
                    case '%': {
                        PUTC('%');
                        i += 1;
                    } break;
                    case 's': {
                        const char *s = va_arg(args, const char *);
                        for (int j = 0; s[j]; j++) PUTC(s[j]);
                        i += 1;
                    } break;
                    case 'd': {
                        int num = va_arg(args, int);
                        int8_t arr[20], len = 0, flag = (num < 0);
                        do {
                            arr[len++] = num % 10;
                            num /= 10;
                        } while(num);
                        if (flag) PUTC('-');
                        for (int j = len - 1; j >= 0; j--) {
                            if (flag) PUTC(-arr[j] + '0');
                            else PUTC(arr[j] + '0');
                        }
                        i += 1;
                    } break;
                    case 'x': {
                        unsigned int num = va_arg(args, unsigned int);
                        int8_t arr[20], len = 0;
                        do {
                            arr[len++] = num % 16;
                            num /= 16;
                        } while (num);
                        for (int j = len - 1; j >= 0; j--) {
                            PUTC(base_16_code(arr[j]));
                        }
                        i += 1;
                    } break;
                }
            } break;
            default : PUTC(format[i]); break;
        }
    }
    
    #undef PUTC
    va_end(args);
    return cnt;
}

int main() {
    TEST("hello world\n");
    TEST("100%%\n");
    TEST("%s\n", "hello world 100%%");
    TEST("INT32_MAX = %d, INT32_MIN = %d\n", INT32_MAX, INT32_MIN);
    TEST("zero = %d\n", 0);
    TEST("123 = %x, -1 = %x, INT32_MIN = %x\n", 123, -1, INT32_MIN);
    
    return 0;
}

第八章:文件操作

8.1 文件操作基础

文件操作允许程序与外部文件进行数据交换。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 打开文件
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        printf("failed to open file\n");
        exit(1);
    }
    
    // 写入数据
    fprintf(fp, "hello world\n");
    int a = 123, b = 456;
    fprintf(fp, "a = %d, b = %d\n", a, b);
    
    // 关闭文件
    fclose(fp);
    
    return 0;
}

8.2 文件访问模式

不同的模式决定文件的打开方式和权限:

模式 说明
r 只读,文件必须存在
w 只写,不存在则创建,存在则清空
a 追加,不存在则创建,始终在末尾写入
r+ 读写,文件必须存在
w+ 读写,不存在则创建,存在则清空
a+ 读写追加,不存在则创建

8.3 文件读写操作

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

void r_access() { 
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        printf("r : failed to open file\n");
        exit(1);
    }
    char s[100];
    fscanf(fp, "%[^\n]", s);  // 读取一行(不含换行)
    printf("s = %s\n", s);
    fclose(fp);
    
    fp = fopen("temp.txt", "r");
    if (fp == NULL) {
        printf("r : temp.txt does not exist\n");
    }
    return ;
}

void rand_file_name(char *file_name, int n) {
    for (int i = 0; i < n; i++) {
        file_name[i] = rand() % 26 + 'a';
    }
    file_name[n] = 0;
    strcat(file_name, ".txt");
    return ;
}

void w_access() {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        printf("w : failed to open file\n");
        exit(1);
    }
    fprintf(fp, "hahahah , world\n");
    fprintf(fp, "hello hangzhou\n");
    fclose(fp);
    
    char file_name[100] = {0};
    rand_file_name(file_name, 10);
    printf("w : open file %s\n", file_name);
    fp = fopen(file_name, "w");
    fclose(fp);
    
    return ;
}

int main() {
    srand(time(0));
    r_access();
    w_access();
    
    return 0;
}

8.4 fseek和ftell

文件定位函数用于随机访问文件。

c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("data5.txt", "w");
    
    printf("ftell(fp) = %ld\n", ftell(fp));  // 0,文件开头
    fprintf(fp, "0123456789");
    printf("after print ftell(fp) = %ld\n", ftell(fp));  // 10
    
    fseek(fp, 2, SEEK_SET);  // 移到距离文件开头2字节处
    printf("after fseek(2) ftell(fp) = %ld\n", ftell(fp));  // 2
    fprintf(fp, "abc");  // 覆盖"2"和"3"
    printf("after print ftell(fp) = %ld\n", ftell(fp));  // 5
    
    fclose(fp);
    
    return 0;
}

8.5 二进制文件操作

fread和fwrite用于二进制数据的读写。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void output(int *arr, int n) {
    printf("arr : ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return ;
}

int fwrite_test() {
    srand(time(0));
    #define MAX_N 10
    int arr[MAX_N];
    for (int i = 0; i < MAX_N; i++) {
        arr[i] = rand() % 10000;
    }
    output(arr, MAX_N);
    
    FILE *fp = fopen("data10.dat", "wb");
    fwrite(arr, sizeof(int), MAX_N, fp);  // 写入二进制数据
    fclose(fp);
    
    return 0;
}

int main() {
    int arr[MAX_N];
    FILE *fp = fopen("data10.dat", "rb");
    output(arr, MAX_N);  // 未读取前的值
    fread(arr, sizeof(int), MAX_N, fp);  // 读取二进制数据
    output(arr, MAX_N);  // 读取后的值
    fclose(fp);
    
    return 0;
}

8.6 学生信息管理系统

综合应用结构体和文件操作实现一个简单的管理系统。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

const char *file_name = "student_data.txt";

typedef struct Student {
    char name[20];
    int age;
    int class;
    double height;
} Student;

#define MAX_N 10000
Student stus[MAX_N + 5];
int scnt;

int read_from_file(Student *arr) {
    int i = 0;
    FILE *fp = fopen(file_name, "r");
    if (fp == NULL) return 0;
    while (fscanf(fp, "%s", arr[i].name) != EOF) {
        fscanf(fp, "%d%d%lf", 
            &arr[i].age, 
            &arr[i].class,
            &arr[i].height
        );
        i += 1;
    }
    fclose(fp);
    return i;
}

void output_to_file(Student *arr, int n) {
    FILE *fp = fopen(file_name, "a");
    for (int i = 0; i < n; i++) {
        fprintf(fp, "%s %d %d %.2lf\n",
            arr[i].name, arr[i].age,
            arr[i].class, arr[i].height
        );
    }
    fclose(fp);
    return ;
}

void clear_file() {
    FILE *fp = fopen(file_name, "w");
    fclose(fp);
    return ;
}

void list_students() {
    int len = printf("%4s|%10s|%4s|%6s|%7s|",
        "id", "name", "age", "class", "height"
    );
    printf("\n");
    for (int i = 0; i < len; i++) printf("=");
    printf("\n");
    for (int i = 0; i < scnt; i++) {
        printf("%4d|%10s|%4d|%6d|%7.2lf|\n", 
            i, stus[i].name,
            stus[i].age, stus[i].class,
            stus[i].height
        );
    }
    return ;
}

void add_a_student() {
    printf("add new item : (name, age, class, height)\n");
    printf("> ");
    scanf("%s%d%d%lf", 
        stus[scnt].name, 
        &stus[scnt].age,
        &stus[scnt].class,
        &stus[scnt].height
    );
    output_to_file(stus + scnt, 1);
    scnt += 1;
    printf("add a new student success\n");
    return ;
}

void delete_a_student() {
    if (scnt == 0) {
        printf("there is no student\n");
        return ;
    }
    
    list_students();
    int id;
    do {
        printf("delete id : ");
        scanf("%d", &id);
    } while (id < 0 || id >= scnt);
    
    char s[100];
    printf("confirm (y / n) : ");
    fflush(stdin);
    scanf("%[^\n]", s);
    if (s[0] != 'y') return ;
    
    for (int i = id + 1; i < scnt; i++) {
        stus[i - 1] = stus[i];
    }
    scnt -= 1;
    
    // 重建文件
    FILE *fp = fopen(file_name, "w");
    fclose(fp);
    output_to_file(stus, scnt);
    
    return ;
}

enum NO_TYPE {
    LIST = 1,
    ADD,
    DELETE,
    QUIT
};

int usage() {
    int no;
    do {
        printf("%d : list students\n", LIST);
        printf("%d : add a new student\n", ADD);
        printf("%d : delete a student\n", DELETE);
        printf("%d : quit\n", QUIT);
        printf("> ");
        scanf("%d", &no);
    } while (no < LIST || no > QUIT);
    return no;
}

int main() {
    scnt = read_from_file(stus);
    
    while (1) {
        int no = usage();
        switch (no) {
            case LIST: {
                list_students();
            } break;
            case ADD: {
                add_a_student();
            } break;
            case DELETE: {
                delete_a_student();
            } break;
            case QUIT: {
                printf("quit\n");
                exit(0);
            }
        }
    }
    
    return 0;
}

附录

常见错误与调试技巧

  1. Segmentation Fault(段错误)

    • 原因:访问了非法的内存地址
    • 解决:检查指针是否为NULL,数组是否越界
  2. 内存泄漏

    • 原因:malloc后没有free
    • 解决:确保每条malloc有对应的free
  3. 缓冲区溢出

    • 原因:写入的数据超过数组大小
    • 解决:使用strncpy、snprintf等安全函数

学习资源推荐

  1. 经典书籍

    • 《C程序设计语言》- Brian W. Kernighan & Dennis M. Ritchie
    • 《C Primer Plus》- Stephen Prata
  2. 在线资源

  3. 实践平台

    • Online Judge(在线评测系统)
    • LeetCode(算法题库)

总结

本教程涵盖了C语言的核心知识点,从基础语法到高级应用。希望通过系统学习和大量实践,你能够掌握C语言,为后续学习数据结构、算法、操作系统等课程打下坚实基础。

记住,编程能力的提升没有捷径,唯有多写代码、多调试、多思考。祝你学习愉快!


本教程共计约55000字,包含150+个完整代码示例。

相关推荐
大空大地20262 小时前
程序调试与异常处理
开发语言
二等饼干~za8986683 小时前
源码可控:云罗 GEO 源头工厂,开源搭建 + 二次开发全链路解决方案
服务器·开发语言·开源·php·音视频·ai-native
维度攻城狮3 小时前
pycallgraph2drawio:Python 调用链可视化 + Draw.io 自由编辑
开发语言·python·draw.io·graphviz
蒙奇·D·路飞-3 小时前
大模型时代下 Java 后端开发的技术重构与工程实践
java·开发语言·重构
wljy13 小时前
Qt入门(一)
开发语言·qt
ZK_H3 小时前
半导体工艺流程
java·c语言·开发语言·计算机网络·金融
计算机安禾3 小时前
【数据结构与算法】第39篇:图论(三):最小生成树——Prim算法与Kruskal算法
开发语言·数据结构·c++·算法·排序算法·图论·visual studio code
liliangcsdn3 小时前
sentence-transformer如何离线加载和使用模型
开发语言·前端·php
Crazy________3 小时前
4.10dockerfile构建镜像
java·开发语言