C语言 基础

一、C 语言基础(底层机制解析)

编译与链接流程

graph LR 

A[源文件(.c)] --> B[预处理器] --> C[编译生成汇编代码(.s)] 

C --> D[汇编器生成目标文件(.o)] 

D --> E[链接器生成可执行文件] 

E --> F[操作系统加载执行] 

- 预处理阶段:处理  #include (展开头文件)、 #define (文本替换,无类型检查)、 #ifdef (条件编译,用于跨平台适配,如  #ifdef _WIN32 ... #else ... #endif )。

- 链接本质:将目标文件中的符号(函数/变量地址)与标准库/第三方库匹配,解决“未定义引用”错误(如缺少  #include   时, printf  符号无法解析)。

内存布局(程序运行时)

高地址 

├──────────────┤ 

│ 栈区(局部变量、函数参数,自动增长/收缩) │ 

├──────────────┤ 

│ 堆区(动态分配内存,malloc/calloc/realloc) │ 

├──────────────┤ 

│ 数据段(全局变量、static 变量:已初始化非零值存“初始化数据段”,0/未初始化存“BSS 段”) │ 

├──────────────┤ 

│ 代码段(程序指令,只读,包含常量字符串) │ 

低地址 

- 栈溢出场景:局部数组过大(如  int arr[10000000];  直接耗尽栈空间)、无限递归(每级递归压栈,无终止条件)。

二、数据类型(精度与跨平台差异)

类型大小的“不确定原则”

C 语言仅规定类型最小大小,实际大小由编译器/平台决定(需用  sizeof  确认):

类型 C 标准最小大小 32 位 GCC 64 位 GCC Windows MSVC(64 位)

 char  1 字节 1 1 1

 int  2 字节 4 4 4

 long  4 字节 4 8 4

 long long  8 字节 8 8 8

 指针(如 int*)  - 4 8 8

- 跨平台编程建议:使用    中的确定大小类型(如  int32_t  固定 4 字节, uint64_t  固定 8 字节),避免依赖  int / long  的默认大小。

浮点精度陷阱

float a = 1.1f;  // 二进制无法精确表示,实际存储值略小于 1.1 

if (a == 1.1f) printf("相等");  // 可能为假 

- 解决方案:比较浮点数时用误差范围(如  fabs(a - 1.1f) < 1e-6 ),避免直接用  == 。

三、运算符(优先级与结合性详解)

优先级金字塔(从高到低,关键层级)

1. 括号()、数组下标[]、结构体成员. -> 

2. 单目运算符(++/-- 前缀、!、~、*指针解引用、&取地址) 

3. 算术运算符(* / % 高于 + -) 

4. 移位运算符(<< >>,如 `a << 1` 等价于 `a * 2`,效率更高) 

5. 关系运算符(> < >= <= 高于 == !=) 

6. 逻辑运算符(&& 高于 ||,短路特性:`a && b` 中 a 为假时不计算 b) 

7. 赋值运算符(= 及其复合形式,右结合性,如 `a = b = c` 等价于 `a = (b = c)`) 

- 易错案例:

int a = 1, b = 2, c = 3; 

int d = a++ + ++b * c;  // 等价于 (a++) + ((++b) * c) → 1 + (3*3) = 10,a=2, b=3 

四、控制结构(逻辑优化与性能考量)

条件语句的“分支预测”

CPU 会缓存条件语句的执行路径(“分支预测”),若条件为真的概率远高于假,建议将高频分支放在前面,提升缓存命中率:

// 推荐:高频场景(用户登录成功)前置 

if (login_success) { 

    process_login();  // 高频逻辑 

} else { 

    handle_error();   // 低频逻辑 

循环展开(性能优化技巧)

对固定次数的循环,可手动展开减少循环控制开销(牺牲代码体积换速度):

// 原循环 

for (int i=0; i<1000; i++) sum += arr[i]; 

// 展开4次(适用于偶数次循环) 

for (int i=0; i<1000; i+=4) { 

    sum += arr[i]; 

    sum += arr[i+1]; 

    sum += arr[i+2]; 

    sum += arr[i+3]; 

五、函数(底层调用约定)

函数调用栈帧(一次函数调用的内存布局)

高地址 

├──────────────┤ 

│ 调用函数的返回地址(caller PC) │ 

├──────────────┤ 

│ 调用函数的栈帧基址(caller ebp) │ 

├──────────────┤ 

│ 函数参数(从右到左压栈,如 func(a, b) 先压 b 再压 a) │ 

├──────────────┤ 

│ 局部变量(按声明顺序分配,未初始化时为随机值) │ 

低地址(函数栈帧基址 ebp) 

- 值传递的本质:实参的值被复制到栈帧的参数区域,函数内操作的是副本,不影响实参。

- 指针传递优化:传递大结构体时用指针(如  void process(struct BigStruct *ptr) ),避免栈上复制大对象的性能开销。

可变参数函数实现(以  my_printf  为例)

#include  

void my_printf(const char *fmt, ...) { 

    va_list args;          // 定义参数列表 

    va_start(args, fmt);   // 初始化,指向第一个可变参数 

    while (*fmt) { 

        if (*fmt == '%') { 

            fmt++; 

            switch (*fmt) { 

                case 'd': { 

                    int num = va_arg(args, int);  // 按类型提取参数,指针指向下一个 

                    printf("%d", num); 

                    break; 

                } 

                // 其他类型处理... 

            } 

        } 

        fmt++; 

    } 

    va_end(args);          // 清理资源,必须调用 

- 注意:可变参数必须至少有一个固定参数(如  fmt ),用于定位可变参数的起始位置;参数提取顺序必须与格式串严格一致,否则导致内存越界(如用  va_arg(args, int)  提取  char  类型参数)。

六、数组与指针(内存寻址深度解析)

二维数组的存储与指针表示

int arr[2][3] = {{1, 2, 3}, {4, 5, 6}}; 

- 物理存储:按行优先,连续存储为  1,2,3,4,5,6 ,内存地址  &arr[0][0]  →  &arr[0][1]  →  &arr[1][0]  依次递增 4 字节(假设  int  占 4 字节)。

- 指针等价关系:

-  arr  是指向数组的指针(类型  int (*)[3] ), arr + 1  跳过一行(3 个  int ,即 12 字节)。

-  arr[0]  是首行首元素指针(类型  int * ), arr[0] + 1  指向该行下一个元素(+4 字节)。

- 错误示范:

int (*p)[3] = arr;  // 正确,p 指向二维数组 

int *q = arr;       // 错误!arr 类型是 int (*)[3],q 是 int*,类型不匹配,解引用会导致 4 字节错位 

指针数组 vs 数组指针

定义 本质 示例解析

 int *arr[5];  指针数组(数组元素是指针)  arr  是包含 5 个  int*  的数组,可指向多个独立内存块

 int (*arr)[5];  数组指针(指针指向数组)  arr  指向一个包含 5 个  int  的数组, arr + 1  跳过 5 个  int 

- 应用场景:

- 指针数组:存储多个字符串(如  char *strs[] = {"a", "b", "c"}; ,字符串常量地址存入数组)。

- 数组指针:作为函数参数传递二维数组( void func(int (*arr)[5]); ,避免数组传参退化问题)。

七、字符串(内存布局与安全函数)

字符串的两种存储方式对比

方式 内存区域 可修改性 典型场景 风险

字符数组 栈区/数据段 可修改 需动态修改字符串(如用户输入) 需注意越界(如  str[len] = '\0'  必须保留)

指针指向常量字符串 代码段(只读) 不可修改 存储固定文本(如提示信息) 误写会导致段错误(如  *str = 'a';  崩溃)

安全字符串函数(替代危险函数)

危险函数 安全替代 核心改进

 strcpy(dest, src)   strncpy(dest, src, n)  限定拷贝长度为  n-1 ,需手动添加  \0 (如  strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0'; )

 scanf("%s", str)   fgets(str, sizeof(str), stdin)  最多读取  sizeof(str)-1  字节,自动添加  \0 ,可读取含空格的字符串(需配合  getchar()  处理前缀  \n )

 sprintf(str, fmt)   snprintf(str, size, fmt, ...)  限定目标缓冲区大小  size ,避免溢出(返回值为应写入的字符数,可判断是否截断)

八、结构体(内存对齐与位段)

内存对齐的计算实例

struct Data { 

    char a;    // 1 字节,偏移 0(1 的倍数,OK) 

    int b;     // 4 字节,下一个偏移需是 4 的倍数 → 填充 3 字节,偏移 4 

    short c;   // 2 字节,下一个偏移 8(4+4=8,是 2 的倍数,OK) 

};              // 总大小 8+2=10?不!需是最大成员(int,4 字节)的倍数 → 填充 2 字节,总大小 12 字节 

- 对齐优化技巧:将大成员(如  int / double )放在前面,小成员(如  char / short )放在后面,减少填充字节(如上述结构体若调整为  int b; char a; short c; ,总大小仅 8 字节)。

位段(打包小数据,节省内存)

struct Flags { 

    unsigned int valid : 1;   // 占 1 位,存储 0/1 

    unsigned int type : 3;    // 占 3 位,取值 0~7 

    unsigned int reserved : 28; // 占 28 位,保留字段 

};  // 整个结构体占 4 字节(32 位),比用多个 `int` 节省内存 

- 注意:位段成员必须是  unsigned int  或  int ,且总长度不能超过所在  int  的大小(32 位/64 位);位段的内存分配方向(左对齐/右对齐)由编译器决定,不可跨平台依赖。

九、文件操作(二进制文件与文本文件差异)

核心区别对比

特性 文本文件 二进制文件

存储形式 ASCII 字符(每个字符占 1 字节) 数据原值(如  int  占 4 字节)

换行符 Windows 下为  \r\n (2 字节),Linux 下  \n (1 字节) 无特殊转义,按实际字节存储

适用场景 人类可读内容(如配置文件、日志) 程序间数据交互(如数据库记录、图片二进制流)

读写函数  fgets / fputs / fprintf   fread / fwrite 

二进制文件读写实例(存储结构体数组)

struct Student { 

    char name[20]; 

    int age; 

}; 

int main() { 

    struct Student stu[2] = {{"Alice", 20}, {"Bob", 25}}; 

    FILE *fp = fopen("students.dat", "wb"); 

    fwrite(stu, sizeof(struct Student), 2, fp);  // 一次性写入 2 个结构体 

    fclose(fp); 

    // 读取 

    struct Student read_stu[2]; 

    fp = fopen("students.dat", "rb"); 

    fread(read_stu, sizeof(struct Student), 2, fp); 

    for (int i=0; i<2; i++) { 

        printf("姓名: %s,年龄: %d\n", read_stu[i].name, read_stu[i].age); 

    } 

    fclose(fp); 

    return 0; 

- 关键:读写时需确保结构体定义与文件存储格式完全一致(包括内存对齐),否则会读取到乱码(如不同编译器对结构体对齐方式不同,导致二进制文件不兼容)。

十、内存管理(高级技巧与常见问题)

内存碎片问题

- 成因:频繁申请/释放小内存块,导致堆区出现不连续的空闲块(无法合并为大内存块,即使总空闲空间足够,申请大内存时仍会失败)。

- 缓解方案:

- 批量申请内存(如用  malloc(100*sizeof(int))  分配一个大块,自行管理小块分配)。

- 使用内存池(预先分配一大块内存,按需分配/回收,避免频繁系统调用  sbrk / malloc )。

野指针排查三原则

1. 初始化原则:定义指针时立即赋值为  NULL ( int *p = NULL; ),避免指向随机地址。

2. 释放置空原则: free(p)  后立即写  p = NULL; (防止后续误操作, free(NULL)  是安全的)。

3. 检查原则:解引用前判断是否为  NULL ( if (p != NULL) *p = 10; ),但无法检测已释放的非  NULL  指针(需配合调试工具)。

十一、C 语言进阶知识(扩展能力)

预处理宏的高级用法

1. 可变参数宏(模拟  printf  风格):

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__) 

// 使用:LOG("错误代码:%d", error_code); 输出文件名、行号与信息 

2. 字符串化与连接:

-  #  运算符:将宏参数转换为字符串(如  #define STR(x) #x , STR(hello)  变为  "hello" )。

-  ##  运算符:连接两个 token(如  #define CONCAT(a, b) a##b , CONCAT(var, 1)  变为  var1 )。

以下是 10个简单易懂的C语言编程代码示例,覆盖基础语法、数学运算、流程控制等场景,适合新手入门练习:

一、Hello World(入门经典)

#include  

int main() { 

    printf("Hello, World!\n");  // 输出字符串 

    return 0;  // 程序正常退出 

运行效果:

Hello, World! 

核心点:

- 头文件  stdio.h  包含输入输出函数( printf )

-  main  函数是程序入口, return 0  表示执行成功

二、两数相加(变量与运算符)

#include  

int main() { 

    int a = 5, b = 7;  // 声明并初始化变量 

    int sum = a + b;   // 加法运算 

    printf("%d + %d = %d\n", a, b, sum);  // 格式化输出 

    return 0; 

运行效果:

5 + 7 = 12 

核心点:

- 整数类型  int  的使用

-  printf  的占位符  %d  对应整数输出

三、奇偶判断(条件语句)

#include  

int main() { 

    int num; 

    printf("请输入一个整数:"); 

    scanf("%d", &num);  // 读取用户输入(注意取地址符 &) 

    if (num % 2 == 0) {  // 取模运算判断奇偶 

        printf("%d 是偶数\n", num); 

    } else { 

        printf("%d 是奇数\n", num); 

    } 

    return 0; 

运行效果(输入13):

请输入一个整数:13 

13 是奇数 

核心点:

-  if-else  条件分支逻辑

- 输入函数  scanf  的用法( %d  对应整数, &  取变量地址)

四、乘法口诀表(循环语句)

#include  

int main() { 

    for (int i = 1; i <= 9; i++) {  // 外层循环:行(1~9) 

        for (int j = 1; j <= i; j++) {  // 内层循环:列(1~i,保证 j<=i) 

            printf("%d×%d=%-2d ", j, i, i*j);  // %-2d 左对齐,占2位 

        } 

        printf("\n");  // 换行 

    } 

    return 0; 

运行效果(部分):

1×1=1 

1×2=2 2×2=4 

1×3=3 2×3=6 3×3=9 

... 

核心点:

- 双层  for  循环嵌套

- 格式化输出控制( %-2d  优化对齐)

五、求1-100累加和(循环与累加)

#include  

int main() { 

    int sum = 0;  // 累加变量初始化为0 

    for (int i = 1; i <= 100; i++) { 

        sum += i;  // 等价于 sum = sum + i 

    } 

    printf("1-100的和为:%d\n", sum); 

    return 0; 

运行效果:

1-100的和为:5050 

核心点:

- 循环变量  i  的范围控制(1~100)

- 累加逻辑(变量需先初始化,避免随机值)

六、字符与ASCII码转换(字符类型)

#include  

int main() { 

    char ch = 'A';  // 字符变量(用单引号) 

    int ascii = ch;  // 字符自动转换为ASCII码(整数) 

    printf("%c 的ASCII码是 %d\n", ch, ascii); 

    int num = 97; 

    char letter = num;  // ASCII码转换为字符 

    printf("%d 对应的字符是 %c\n", num, letter); 

    return 0; 

运行效果:

A 的ASCII码是 65 

97 对应的字符是 a 

核心点:

-  char  类型与整数的兼容性(本质存储ASCII码值)

- 字符输出  %c  与整数输出  %d  的区别

七、最大值计算(三元运算符)

#include  

int main() { 

    int a = 15, b = 28; 

    int max = (a > b) ? a : b;  // 三元运算符(条件? 真结果 : 假结果) 

    printf("最大值是:%d\n", max); 

    return 0; 

运行效果:

最大值是:28 

核心点:

- 三元运算符  condition ? x : y  的简洁用法

- 替代简单的  if-else  逻辑

八、数组元素遍历(一维数组)

#include  

int main() { 

    int arr[] = {10, 20, 30, 40, 50};  // 初始化数组 

    int len = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度 

    printf("数组元素:"); 

    for (int i = 0; i < len; i++) { 

        printf("%d ", arr[i]);  // 下标访问数组元素 

    } 

    return 0; 

运行效果:

数组元素:10 20 30 40 50 

核心点:

- 数组定义与初始化方式

-  sizeof  计算数组长度( sizeof(arr)  是总字节数,除以单个元素字节数)

九、简单函数调用(自定义函数)

#include  

// 自定义函数:计算平方 

int square(int x) { 

    return x * x;  // 返回计算结果 

int main() { 

    int num = 7; 

    int result = square(num);  // 调用函数 

    printf("%d 的平方是 %d\n", num, result); 

    return 0; 

运行效果:

7 的平方是 49 

核心点:

- 函数定义(返回类型+函数名+参数)

- 函数调用与返回值接收

十、星号金字塔(循环与字符输出)

#include  

int main() { 

    int n = 5;  // 金字塔层数 

    for (int i = 1; i <= n; i++) { 

        // 打印空格(每层空格数:n - i 个) 

        for (int j = 1; j <= n - i; j++) { 

            printf(" "); 

        } 

        // 打印星号(每层星号数:2*i - 1 个) 

        for (int k = 1; k <= 2 * i - 1; k++) { 

            printf("*"); 

        } 

        printf("\n");  // 换行 

    } 

    return 0; 

运行效果(n=5):

    * 

   *** 

  ***** 

 ******* 

********* 

核心点:

- 双层循环控制空格与星号的数量

- 数学规律:第  i  层空格数  n-i ,星号数  2i-1 

代码练习建议:

1. 动手编译运行:用  gcc code.c -o code  编译, ./code  运行(Linux/macOS),或在VS Code/Dev-C++ 中直接调试。

2. 修改参数观察:如调整乘法口诀表的层数、金字塔的行数,理解循环变量的作用。

3. 添加注释:给代码加上自己的理解,比如“// 外层循环控制行数”,强化逻辑记忆。

这些实例覆盖了C语言的基础语法和常用结构 

一、基础语法:简易计算器(输入输出+运算符)

功能:支持加减乘除、取模、自增自减运算

#include  

int main() { 

    char op; 

    int a, b, result; 

    printf("请输入运算符(+ - * / % ++ --):"); 

    scanf("%c", &op); 

    if (op == '+' || op == '-' || op == '*' || op == '/' || op == '%') { 

        printf("请输入两个整数:"); 

        scanf("%d %d", &a, &b); 

        switch (op) { 

            case '+': result = a + b; break; 

            case '-': result = a - b; break; 

            case '*': result = a * b; break; 

            case '/': result = b != 0 ? a / b : 0;  // 除数不能为0 

            case '%': result = b != 0 ? a % b : 0; 

        } 

        printf("结果:%d\n", result); 

    } else if (op == '+' || op == '-') {  // 处理单目运算符(假设作用于a) 

        printf("请输入一个整数:"); 

        scanf("%d", &a); 

        op == '+' ? printf("自增后:%d\n", ++a) : printf("自减后:%d\n", --a); 

    } else { 

        printf("不支持的运算符!\n"); 

    } 

    return 0; 

核心点:

- 区分单目/双目运算符的输入逻辑

- 处理除数为0的边界条件(防御性编程)

-  switch  语句的分支逻辑组织

二、算法实现:冒泡排序(循环+数组操作)

功能:对整数数组升序排序,输出每轮排序过程

#include  

void bubble_sort(int arr[], int n) { 

    for (int i = 0; i < n - 1; i++) {          // 外层循环:n-1轮 

        int swapped = 0;                        // 标记是否发生交换(优化:若某轮无交换,提前终止) 

        for (int j = 0; j < n - i - 1; j++) {  // 内层循环:每轮比较相邻元素 

            if (arr[j] > arr[j + 1]) { 

                // 交换元素 

                int temp = arr[j]; 

                arr[j] = arr[j + 1]; 

                arr[j + 1] = temp; 

                swapped = 1; 

            } 

        } 

        if (!swapped) break;  // 无交换,数组已有序 

        // 输出当前轮次结果(调试用) 

        printf("第%d轮排序后:", i + 1); 

        for (int k = 0; k < n; k++) printf("%d ", arr[k]); 

        printf("\n"); 

    } 

int main() { 

    int arr[] = {64, 34, 25, 12, 22, 11, 90}; 

    int n = sizeof(arr) / sizeof(arr[0]); 

    printf("原始数组:"); 

    for (int i = 0; i < n; i++) printf("%d ", arr[i]); 

    printf("\n"); 

    bubble_sort(arr, n); 

    printf("排序后数组:"); 

    for (int i = 0; i < n; i++) printf("%d ", arr[i]); 

    return 0; 

核心点:

- 双层循环逻辑(外层控制轮次,内层控制比较范围)

- 优化手段:通过  swapped  标记避免无效循环

- 数组传参时的长度计算( sizeof(arr)  在函数内失效,需外部传入  n )

三、指针应用:字符串逆序(指针操作+内存寻址)

功能:通过指针翻转字符串(如 "hello" → "olleh")

#include  

#include   // 用于 strlen() 

void reverse_string(char *str) { 

    int len = strlen(str); 

    char *start = str;          // 指向首字符 

    char *end = str + len - 1;  // 指向末字符(忽略 '\0') 

    while (start < end) { 

        // 交换字符(通过指针解引用) 

        char temp = *start; 

        *start = *end; 

        *end = temp; 

        start++;  // 指针后移 

        end--;    // 指针前移 

    } 

int main() { 

    char str[] = "hello world"; 

    printf("原字符串:%s\n", str); 

    reverse_string(str); 

    printf("逆序后:%s\n", str); 

    return 0; 

核心点:

- 指针与数组的等价性( str  数组名作为指针传递)

- 指针算术运算( str + len - 1  计算末字符地址)

- 字符串结束符  \0  的处理(逆序时不交换  \0 )

四、动态内存:学生信息管理(结构体+堆内存)

功能:动态创建学生结构体数组,录入信息并输出

#include  

#include  

#include  

// 定义学生结构体 

struct Student { 

    char name[20]; 

    int age; 

    float score; 

}; 

int main() { 

    int n; 

    printf("请输入学生人数:"); 

    scanf("%d", &n); 

    // 动态分配n个学生的内存(堆区) 

    struct Student *students = (struct Student *)malloc(n * sizeof(struct Student)); 

    if (students == NULL) {  // 内存分配失败处理 

        printf("内存分配失败!\n"); 

        return 1; 

    } 

    // 录入学生信息 

    for (int i = 0; i < n; i++) { 

        printf("\n第%d个学生信息:\n", i + 1); 

        printf("姓名:"); 

        scanf("%s", students[i].name);  // 结构体成员访问:数组下标+点运算符 

        printf("年龄:"); 

        scanf("%d", &students[i].age); 

        printf("成绩:"); 

        scanf("%f", &students[i].score); 

    } 

    // 输出学生信息(通过指针遍历) 

    printf("\n学生信息列表:\n"); 

    for (int i = 0; i < n; i++) { 

        struct Student *p = &students[i];  // 取结构体地址 

        printf("姓名:%s,年龄:%d,成绩:%.2f\n", 

               p->name, p->age, p->score);  // 指针访问成员:-> 运算符 

    } 

    // 释放内存(避免泄漏) 

    free(students); 

    students = NULL;  // 置空指针,防止野指针 

    return 0; 

核心点:

- 结构体与动态内存结合使用

- 指针与结构体成员的两种访问方式( students[i].name  与  p->name )

- 内存分配失败的检查与释放规范

五、文件操作:通讯录管理(文本文件读写)

功能:将联系人信息存入文件,支持读取与追加

#include  

#include  

// 定义联系人结构体 

struct Contact { 

    char name[20]; 

    char phone[15]; 

}; 

void save_to_file(struct Contact *contacts, int count) { 

    FILE *fp = fopen("contacts.txt", "w");  // "w" 模式:新建/覆盖文件 

    if (fp == NULL) { 

        printf("无法打开文件!\n"); 

        return; 

    } 

    // 写入文件(格式化输出) 

    for (int i = 0; i < count; i++) { 

        fprintf(fp, "姓名:%s,电话:%s\n", contacts[i].name, contacts[i].phone); 

    } 

    fclose(fp); 

    printf("信息已保存到 contacts.txt\n"); 

void append_to_file(struct Contact contact) { 

    FILE *fp = fopen("contacts.txt", "a");  // "a" 模式:追加到文件末尾 

    if (fp == NULL) { 

        printf("无法打开文件!\n"); 

        return; 

    } 

    fprintf(fp, "姓名:%s,电话:%s\n", contact.name, contact.phone); 

    fclose(fp); 

    printf("信息已追加到文件\n"); 

void read_from_file() { 

    FILE *fp = fopen("contacts.txt", "r"); 

    if (fp == NULL) { 

        printf("文件不存在或无法打开!\n"); 

        return; 

    } 

    char line[100]; 

    printf("\n通讯录内容:\n"); 

    while (fgets(line, sizeof(line), fp) != NULL) {  // 按行读取 

        printf("%s", line); 

    } 

    fclose(fp); 

int main() { 

    struct Contact contacts[2] = { 

        {"张三", "13800138000"}, 

        {"李四", "13900139000"} 

    }; 

    save_to_file(contacts, 2);  // 保存初始数据 

    read_from_file();          // 读取并打印 

    // 追加新联系人 

    struct Contact new_contact = {"王五", "15800158000"}; 

    append_to_file(new_contact); 

    read_from_file();          // 再次读取 

    return 0; 

核心点:

- 文件打开模式( w  覆盖 vs  a  追加)

-  fprintf / fgets  的文本文件读写操作

- 结构体与文件内容的格式映射

六、递归应用:汉诺塔问题(递归逻辑+参数传递)

功能:打印汉诺塔移动步骤,计算总移动次数

#include  

int move_count = 0;  // 全局变量:记录移动次数 

// 递归函数:将n个盘子从src柱通过mid柱移到dest柱 

void hanoi(int n, char src, char mid, char dest) { 

    if (n == 1) { 

        printf("移动盘子 1:%c → %c\n", src, dest); 

        move_count++; 

        return; 

    } 

    // 递归步骤: 

    hanoi(n - 1, src, dest, mid);       // 将n-1个盘子从src移到mid(借助dest) 

    printf("移动盘子 %d:%c → %c\n", n, src, dest); 

    move_count++; 

    hanoi(n - 1, mid, src, dest);       // 将n-1个盘子从mid移到dest(借助src) 

int main() { 

    int n; 

    printf("请输入汉诺塔层数:"); 

    scanf("%d", &n); 

    hanoi(n, 'A', 'B', 'C');  // 初始柱A,中间柱B,目标柱C 

    printf("\n总移动次数:%d次\n", move_count); 

    return 0; 

核心点:

- 递归终止条件( n == 1  时直接移动)

- 递归过程的“分治”思想(将n个盘子拆解为两次n-1个盘子的移动)

- 全局变量与局部变量的作用域差异(此处用全局变量统计次数,也可通过函数返回值实现)

七、内存管理:动态数组实现(模拟  ArrayList  功能)

功能:支持数组动态扩容、元素添加/删除、下标访问

#include  

#include  

#include   // 用于断言(调试) 

#define INIT_CAPACITY 4  // 初始容量 

// 定义动态数组结构体 

typedef struct { 

    int *data;       // 存储数据的指针(堆区) 

    int size;        // 已存储元素个数 

    int capacity;    // 总容量 

} DynamicArray; 

// 初始化动态数组 

DynamicArray* da_init() { 

    DynamicArray *da = (DynamicArray*)malloc(sizeof(DynamicArray)); 

    assert(da != NULL);  // 断言:确保内存分配成功 

    da->data = (int*)malloc(INIT_CAPACITY * sizeof(int)); 

    assert(da->data != NULL); 

    da->size = 0; 

    da->capacity = INIT_CAPACITY; 

    return da; 

// 检查容量,自动扩容 

void da_resize(DynamicArray *da) { 

    if (da->size >= da->capacity) { 

        int new_capacity = da->capacity * 2; 

        int *new_data = (int*)realloc(da->data, new_capacity * sizeof(int)); 

        assert(new_data != NULL); 

        da->data = new_data; 

        da->capacity = new_capacity; 

        printf("数组扩容至 %d 容量\n", new_capacity); 

    } 

// 添加元素(尾部插入) 

void da_append(DynamicArray *da, int value) { 

    da_resize(da);  // 先检查容量 

    da->data[da->size++] = value; 

// 删除指定下标的元素 

void da_remove(DynamicArray *da, int index) { 

    assert(index >= 0 && index < da->size);  // 断言:下标合法 

    // 前移元素覆盖要删除的位置 

    for (int i = index; i < da->size - 1; i++) { 

        da->data[i] = da->data[i + 1]; 

    } 

    da->size--; 

    // 可选:若容量过大且元素少,可缩容(此处暂不实现) 

// 释放动态数组内存 

void da_free(DynamicArray *da) { 

    free(da->data); 

    free(da); 

int main() { 

    DynamicArray *da = da_init(); 

    da_append(da, 1); 

    da_append(da, 2); 

    da_append(da, 3); 

    da_append(da, 4);  // 此时容量4,无需扩容 

    da_append(da, 5);  // 触发扩容至8 

    printf("数组元素:"); 

    for (int i = 0; i < da->size; i++) { 

        printf("%d ", da->data[i]); 

    } 

    printf("\n"); 

    da_remove(da, 2);  // 删除下标2的元素(值3) 

    printf("删除后元素:"); 

    for (int i = 0; i < da->size; i++) { 

        printf("%d ", da->data[i]); 

    } 

    printf("\n"); 

    da_free(da);  // 释放内存 

    return 0; 

核心点:

- 结构体封装动态数据结构

-  realloc  实现内存扩容

- 断言( assert )的调试作用(正式发布时需移除或关闭)

- 动态数组的核心操作逻辑(添加、删除、扩容)

八、进阶应用:简单shell命令解析(指针+字符串处理)

功能:解析用户输入的命令(如  ls -l ),分离命令名与参数

#include  

#include  

#include  

#define MAX_COMMAND_LENGTH 1024 

#define MAX_ARGUMENTS 64 

// 解析命令字符串,存入参数数组 

void parse_command(char *cmd, char *args[]) { 

    char *token = strtok(cmd, " ");  // 按空格分割字符串 

    int i = 0; 

    while (token != NULL && i < MAX_ARGUMENTS - 1) {  // 最多存储63个参数 

        args[i++] = token; 

        token = strtok(NULL, " ");  // 后续分割从NULL开始,继续解析原字符串 

    } 

    args[i] = NULL;  // 最后一个参数置NULL(符合C标准库函数要求,如execve) 

int main() { 

    char cmd[MAX_COMMAND_LENGTH]; 

    char *args[MAX_ARGUMENTS]; 

    while (1) { 

        printf("my_shell> "); 

        fgets(cmd, sizeof(cmd), stdin); 

        // 去除输入中的换行符(fgets会保留\n) 

        cmd[strcspn(cmd, "\n")] = '\0'; 

        if (strcmp(cmd, "exit") == 0) { 

            printf("退出程序\n"); 

            break; 

        } 

        parse_command(cmd, args); 

        printf("解析结果:\n"); 

        printf("命令名:%s\n", args[0]); 

        printf("参数列表:"); 

        for (int i = 1; args[i] != NULL; i++) { 

            printf("%s ", args[i]); 

        } 

        printf("\n\n"); 

    } 

    return 0; 

核心点:

-  strtok  函数的字符串分割用法(注意其内部状态)

八、进阶应用:简单shell命令解析(指针+字符串处理)(续)

核心点(续):

-  strtok  函数的状态保存:首次调用传入字符串和分隔符(如  " " ),后续调用传入  NULL  以继续从上次分割的位置往后解析,内部通过静态变量记录当前位置(不可用于多线程场景)。

- 输入处理细节:

-  fgets(cmd, sizeof(cmd), stdin)  会读取包括  \n  在内的输入,需通过  strcspn(cmd, "\n")  定位换行符并替换为  \0 ,避免残留换行符影响命令解析。

- 支持无限循环输入( while(1) ),通过输入  exit  退出,模拟简易命令行交互逻辑。

运行效果:

my_shell> ls -l -a 

解析结果: 

命令名:ls 

参数列表:-l -a 

my_shell> cd /home/user 

解析结果: 

命令名:cd 

参数列表:/home/user 

my_shell> exit 

退出程序 

九、预处理应用:带参数的宏定义(编译时替换)

功能:定义宏实现简单数学运算与调试日志

#include  

// 定义带参数的宏(计算平方) 

#define SQUARE(x) ((x) * (x)) 

// 定义调试宏(仅在DEBUG模式下输出日志) 

#define DEBUG 1  // 关闭调试时改为 #define DEBUG 0 

#if DEBUG 

#define LOG(fmt, ...) printf("[DEBUG] " fmt "\n", __VA_ARGS__) 

#else 

#define LOG(...) ((void)0)  // 无操作(避免未使用参数警告) 

#endif 

int main() { 

    int a = 5; 

    int result = SQUARE(a + 2);  // 宏展开为 ((a+2) * (a+2)),避免 (a+2*a+2) 这类错误 

    printf("%d 的平方是 %d\n", a + 2, result); 

    LOG("程序进入main函数,a的值为%d", a);  // 若DEBUG=1,输出调试信息;否则无操作 

    return 0; 

核心点:

- 宏参数的括号保护: SQUARE(x)  中  (x)  确保参数在任何场景下都先计算(避免  SQUARE(a+2)  因优先级导致的错误)。

- 条件编译:通过  #if DEBUG  控制调试日志的编译,发布版本只需修改  DEBUG  定义即可移除日志代码,不影响性能。

运行效果( DEBUG=1  时):

7 的平方是 49 

[DEBUG] 程序进入main函数,a的值为5 

十、位运算:掩码操作(设置/清除/查询二进制位)

功能:通过位运算操作整数的二进制位(如第3位)

#include  

// 定义位操作宏 

#define SET_BIT(num, bit) ((num) |= (1 << (bit)))   // 设置第bit位为1 

#define CLEAR_BIT(num, bit) ((num) &= ~(1 << (bit)))  // 清除第bit位为0 

#define CHECK_BIT(num, bit) ((num) & (1 << (bit)))    // 检查第bit位是否为1 

int main() { 

    unsigned int x = 0;  // 初始为0(二进制0000) 

    // 设置第2位(从0开始计数,对应二进制100) 

    x = SET_BIT(x, 2); 

    printf("设置第2位后:%u(二进制%b)\n", x, x);  // 输出4(100) 

    // 设置第0位 

    x = SET_BIT(x, 0); 

    printf("设置第0位后:%u(二进制%b)\n", x, x);  // 输出5(101) 

    // 清除第2位 

    x = CLEAR_BIT(x, 2); 

    printf("清除第2位后:%u(二进制%b)\n", x, x);  // 输出1(001) 

    // 检查第0位是否为1 

    if (CHECK_BIT(x, 0)) { 

        printf("第0位为1\n"); 

    } else { 

        printf("第0位为0\n"); 

    } 

    return 0; 

核心点:

- 位运算本质:

-  1 << bit  生成掩码(如  bit=2  时掩码为  100 )。

-  |=  按位或设置位, &=  配合  ~  按位与清除位, &  按位与查询位。

- 无符号整数的选择:避免符号位干扰(如  int  的最高位为符号位,操作时可能引发溢出)。

运行效果:

设置第2位后:4(二进制100) 

设置第0位后:5(二进制101) 

清除第2位后:1(二进制1) 

第0位为1 

十一、错误处理:文件操作的完整异常处理

功能:演示文件打开失败的多种场景及处理

#include  

#include   // 用于获取错误码 

int main() { 

    FILE *fp; 

    // 尝试打开不存在的文件(读模式) 

    fp = fopen("nonexistent.txt", "r"); 

    if (fp == NULL) { 

        printf("错误:无法打开文件(错误码%d)\n", errno); 

        perror("详细错误");  // 直接输出系统错误信息(如No such file or directory) 

        // 可根据错误码做具体处理(如ENOENT表示文件不存在) 

        if (errno == ENOENT) { 

            printf("提示:文件不存在,是否创建?\n"); 

        } 

    } else { 

        fclose(fp); 

    } 

    // 尝试以写模式打开只读文件(假设文件存在且只读) 

    fp = fopen("readonly.txt", "w"); 

    if (fp == NULL) { 

        perror("打开只读文件失败"); 

    } else { 

        fclose(fp); 

    } 

    return 0; 

核心点:

-  errno  错误码:文件操作失败时,系统会设置  errno (如  ENOENT  表示文件不存在, EACCES  表示权限不足)。

-  perror  函数:自动打印当前  errno  对应的错误描述(如“Permission denied”),无需手动匹配错误码,简化调试。

十二、多文件编程:模块化开发示例

目录结构:

project/ 

├─ main.c         // 主函数 

├─ math.h         // 数学模块头文件 

└─ math.c         // 数学模块实现 

 math.h (声明函数与宏):

#ifndef MATH_H 

#define MATH_H 

#define PI 3.14159265358979323846 

int add(int a, int b);          // 加法函数声明 

float circle_area(float radius); // 圆面积函数声明 

#endif 

 math.c (函数定义):

#include "math.h"  // 包含自定义头文件 

int add(int a, int b) { 

    return a + b; 

float circle_area(float radius) { 

    return PI * radius * radius; 

 main.c (调用模块函数):

#include  

#include "math.h"  // 引用自定义头文件 

int main() { 

    int sum = add(12, 34); 

    printf("12 + 34 = %d\n", sum); 

    float area = circle_area(5.0f); 

    printf("半径5的圆面积:%.2f\n", area); 

    return 0; 

核心点:

- 头文件保护: #ifndef MATH_H  避免头文件被重复包含(防止函数/宏重复定义错误)。

- 编译命令:

gcc main.c math.c -o project  # 同时编译多个源文件 

./project  # 运行 

- 模块化优势:将功能拆分到不同文件,提高代码复用性(如  math.c  可被其他项目引用)。

编程实例总结:

1. 从简单到复杂:先掌握基础语法(如变量、循环),再进阶到指针、动态内存、文件操作等复杂场景。

2. 关注边界条件:如除数为0、内存分配失败、文件不存在等,防御性编程是C语言的核心能力之一。

3. 结合底层原理:理解指针本质是内存地址、数组传参退化、文件操作的缓存机制等,能快速定位代码问题。

建议将这些实例逐一编译运行,尝试修改功能(如给计算器添加浮点运算、给动态数组增加缩容功能),在实践中加深对C语言的理解~(此文档学习使用)

你可能感兴趣的:(c,c语言,开发语言)