【一】概述

1、C语言的诞生背景

C 诞生于1972 年美国贝尔实验室,设计者:丹尼斯・里奇 (Dennis Ritchie),肯・汤普森 (Ken Thompson) 主导前期铺垫,诞生核心驱动力是UNIX 操作系统开发需求。

1973 年 的 里程碑:团队使用 C 完整重写 UNIX 内核,操作系统彻底脱离汇编,实现跨硬件移植,C 随 UNIX 快速在科研圈、小型机领域普及。

2、主要的开发场景

由于 C 无GC、性能高、贴近硬件,高性能刚需场景必备,主要有一下 开发场景

  1. 中间件&服务底座:Nginx、自研网关、Redis等核心底层,字节Tengine、云厂商负载均衡。
  2. 数据库内核:TDSQL、PolarDB等存储引擎、底层IO模块。
  3. 音视频:抖音/快手/微信音视频编解码、直播转码、剪映底层算法。
  4. AI底层:飞桨、大模型推理算子、加速库。
  5. 网络安全:DDoS防护、WAF、抓包分析、DPDK高性能网卡转发。
  6. 游戏:游戏引擎底层、服务器核心、反作弊。
  7. 嵌入式/IoT/车载:智能家居固件、车载控制系统、边缘网关。
  8. 系统内核:定制Linux内核、云服务器虚拟化组件。

3、优缺点

优点

  1. 性能极致:运行速度接近汇编,无垃圾回收(GC)开销,资源占用极低
  2. 底层操控:可直接操作内存、指针、硬件,适合写系统、驱动、底层组件
  3. 通用性强:跨平台编译,生态成熟,是C++/Java/Go等语言的基础
  4. 体积小巧:编译后文件小,适配嵌入式、物联网等资源受限设备

缺点

  1. 内存风险高:手动申请/释放内存,极易出现泄漏、越界、崩溃
  2. 开发效率低:无面向对象、异常处理等高级特性,代码量大
  3. 安全性差:指针操作无严格校验,容易产生难以调试的bug
  4. 标准库简陋:复杂功能(网络、GUI)需手动实现或依赖第三方库

4、不可替代的核心原因

  • 性能极致:无 GC 开销,内存访问精准,比 Python/Java 快 5-100 倍,适合计算密集型场景
  • 底层控制:直接操作内存地址、寄存器、硬件中断,适配驱动开发与内核编程
  • 跨平台兼容:一套代码可编译运行于 x86/ARM/RISC-V 等架构,适配云、边缘、终端全场景
  • 生态成熟:50 年积累的海量库与工具链(如 GCC、Clang、Valgrind),降低开发成本

【二】基本语法

前要

编译执行

维度 GCC Clang
出身 GNU 官方编译器,Linux 原生标配 LLVM 项目编译器,苹果主导开发
报错信息 晦涩难懂,信息杂乱 清晰易懂,精准定位错误,新手友好
编译速度 较慢,内存占用较高 更快,轻量高效
兼容性 拉满,支持所有硬件平台、老旧标准、Linux 内核 兼容性优秀,冷门硬件/极老代码略逊
许可协议 GPL(开源强约束) BSD(宽松自由,可商用闭源)
工具链 独立生态 依托 LLVM,静态分析/代码检查工具极强
1. GCC 适用场景(Linux 系统开发首选)
  1. Linux 内核开发(官方传统编译器,生态最成熟)
  2. 编译老旧开源项目、服务器底层组件
  3. 全平台兼容需求(x86/ARM/RISC-V 等所有硬件)
  4. 对编译兼容性要求极高的系统级开发
2. Clang 适用场景(学习/现代开发首选)
  1. C 语言学习、日常开发(报错友好,效率拉满)
  2. 代码静态检查、格式化、调试优化
  3. macOS/iOS 开发(默认编译器)
  4. 现代 Linux 项目、轻量级编译场景
3. 编译
gcc test.c -o test && ./test

clang test.c -o test && ./test

两者命令参数 100% 通用,学会一个就会另一个
编译:gcc 源码.c -o 可执行文件 / clang 源码.c -o 可执行文件
运行:./可执行文件
开发必须加 -Wall,显示所有代码警告(规范、避错)

一句话总结:
Linux 内核、底层系统兼容 → 用 GCC
学习 C 语言、现代开发、追求效率 → 用 Clang
两者语法完全兼容,写的 C 代码不用修改,可直接互换编译。


打印函数

C 语言最基础的输出函数,调试必用

#include <stdio.h> // 使用 printf 必须包含头文件

int main() {
    // 直接打印文字
    printf("你好,C语言!");

    int age = 20;          // 整数
    char ch = 'A';         // 字符
    float score = 95.5f;   // 小数

    // 单个变量
    printf("%d\n", age);   // \n 是换行符

    // 多个变量混合输出
    printf("年龄:%d,字符:%c,分数:%.2f\n", age, ch, score);
 

    int reg = 0x1F;  // 寄存器值
    printf("十六进制打印 寄存器值:%x\n", reg);


    return 0;
}

1、变量 & 常量

1-1 变量

变量 的本质 就是 程序可操作的 存储区 的名称。

  • 定义
类型 变量名;

// 例如:
int age;
float salary;
char grade;
int *ptr;
int    i, j, k;
  • 初始化
int x = 10;

// or
int x;          
x = 20;
  • 默认值
    在 C 语言中,如果 变量 没有显式 初始化,那么它的 默认值 将取决于该 变量 的 类型 和 其所在的 作用域
整型变量(intshortlong等):默认值为0。

浮点型变量(floatdouble等):默认值为0.0。

字符型变量(char):默认值为'\0',即空字符。

指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。

数组、结构体、联合等复合类型的变量:它们的元素或成员将按照相应的规则进行默认初始化。
全局变量、静态变量: 它们的默认初始值为零

局部变量:不会自动初始化为 默认值,它们的初始值是未定义的
  • C89标准
    程序中使用的变量,C89 要求声明必须在代码块最开头。
    现在的 C 语言(C99 及以后):取消了这个限制,变量可以随用随声明(和三弟你熟悉的 Rust 一样)
    为什么要知道?
    因为:嵌入式开发、老项目、单片机编程大量还在用 C89,你写代码报错时,能立刻知道原因!
❌ C89 报错(不允许:先执行代码,再声明变量)
int test() {
    printf("hello");  // 执行代码
    int a;            // 中途声明变量 → C89 直接报错!
    return 0;
}

✅ C89 正确(所有变量写在代码块最开头)
int test() {
    // 代码块开头:先声明所有变量
    int a;
    int b;

    // 声明完了,才能写执行代码
    printf("hello");
    a = 10;
    return 0;
}

1-2 常量

常量 可以是任何的 基本数据类型

在 C 中,有 两种简单 的 定义常量 的方式:

  • 使用 #define 预处理器: #define 可以在程序中定义一个常量,它在编译时会被替换为其对应的值。
  • 使用 const 关键字:const 关键字用于声明一个只读变量,即该变量的值不能在程序运行时修改。
#include <stdio.h>
 
#define LENGTH 10   
#define WIDTH  5
#define NEWLINE '\n'
 
int main()
{
 
   int area;  
  
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);
 
   return 0;
}
const int MAX_VALUE = 100;

核心区别:

  1. 本质
    #define预处理宏,编译前做纯文本替换,无语法语义;
    const只读变量修饰符,编译期处理,是合法变量。

  2. 类型安全
    #define无类型检查,易引发隐式错误;
    const强类型检查,Linux 开发优先用(安全、规范)。

  3. 作用域
    #define:全局生效,无法限制作用域;
    const:遵循变量作用域(函数/代码块/文件级)。

  4. 内存与调试
    #define:不分配内存,GDB 无法调试
    const:分配内存(数据段/栈),支持调试

  5. 核心用途
    #define:宏定义、条件编译、开关配置;
    const:定义只读常量、函数入参保护、全局只读数据。

常量优先用 const(安全可调试),仅宏/配置开关用 #define。

2、数据类型

定义变量的存储大小取值范围,是 Linux 底层开发、内存操作的基础。

2.1 基本数据类型(最常用)

类型 说明 Linux 开发场景
int 整型(整数) 计数、状态、变量定义
char 字符型(单字节) 字符串、硬件数据
float 单精度浮点 简单小数计算
double 双精度浮点 高精度小数计算

代码示例

#include <stdio.h>
int main() {
    int num = 1024;       // 整型
    char ch = 'L';        // 字符型
    float pi = 3.14f;     // 单精度浮点, 推荐 结尾 ➕ f
    double rate = 0.99;   // 双精度浮点

    printf("int: %d\n", num);
    printf("char: %c\n", ch);
    printf("float: %.2f\n", pi);
    return 0;
}

2.2 类型修饰符

配合基本类型使用,控制长度和符号:

  • unsigned:无符号(仅正数,Linux 寄存器、硬件操作必备)
  • short:短整型
  • long:长整型

short、unsigned 这两个修饰符,绝对不能用于 浮点数(float/double),编译直接报错!

只有 long 可以修饰 浮点数,搭配 double 使用 → long double(长双精度浮点数),这是唯一合法的写法。

代码示例

#include <stdio.h>
int main() {
    unsigned int u_num = 100;  // 无符号整数(无负数)
    long l_num = 100000L;      // 长整型

    printf("无符号int: %u\n", u_num);
    printf("长整型: %ld\n", l_num);

    // 唯一合法:long 修饰 double
    long double pi = 3.1415926535L;  // 常量加 L 标记为 long double
    // 打印格式符:%Lf (大写L)
    printf("高精度圆周率:%Lf\n", pi);
    
    return 0;
}

2.3 空类型 void

  • 表示无类型,Linux 开发高频使用
  • 用途:无返回值函数、无类型指针(通用指针)

代码示例

// 无返回值函数
void print_hello() {
    printf("Hello Linux\n");
}

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

3、运算符 & 位运算

3.1 基础运算符

3.1.1 算术运算符

+ - * / %(加减乘除、取余)

int a = 10, b = 3;
printf("%d\n", a % b); // 输出 1(取余)
3.1.2 赋值运算符

= += -= *= /=

int a = 5;
a += 2; // 等价 a = a + 2 → a=7
3.1.3 关系运算符(条件判断用)

== != > < >= <=

int a = 5;
printf("%d\n", a > 3); // 成立输出 1,不成立输出 0
3.1.4 逻辑运算符

&&(与)、||(或)、!(非)

int a = 1, b = 0;
printf("%d\n", a && b); // 0

3.2 位运算(Linux 系统开发 核心技能)

直接操作二进制位,是CPU 效率最高的运算;

运算符 含义 作用(Linux 场景) 示例
& 按位与 清零、判断位状态 a & 0x01
` ` 按位或 设置位为 1
^ 按位异或 翻转位 a ^ 0x01
~ 按位取反 全部位翻转 ~a
<< 左移 乘以 2,快速计算 a << 1
>> 右移 除以 2,快速计算 a >> 1

代码示例

  • 场景 1:高效整数乘除法(替代 * /,速度快 10 倍 +)
int a = 8;

a << 1;   // 8 * 2 = 16  (比 a*2 快)
a >> 1;   // 8 / 2 = 4   (比 a/2 快)
  • 场景 2:Linux 文件权限判断(最实用!系统开发必用)
    Linux 读写权限 r=4(100)、 w=2(010) 、x=1(001) 全靠位运算实现。

这段代码利用 按位与 & 位运算,判断一个 数字 中是否包含 某一个 二进制位,这就是 Linux 文件权限判断的底层逻辑。

int perm = 5; // 二进制 101 → 读+执行

// 判断是否有 读权限(4)
if (perm & 4) {
    printf("有读权限\n");
}

4、流程语句

4.1 分支语句(条件判断)

4.1.1 if-else 语句

最常用的条件判断

#include <stdio.h>
int main() {
    int num = 12;

    if (num > 10) {
        printf("大于10\n");
    } else if (num == 10) {  // 关键:else if
        printf("等于10\n");
    } else {
        printf("小于10\n");
    }

    return 0;
}
4.1.2 switch 语句

多分支判断(固定值匹配)

#include <stdio.h>
int main() {
    int status = 2;
    switch (status) {
        case 1: printf("运行中\n"); break;
        case 2: printf("停止中\n"); break;
        default: printf("未知状态\n");
    }
    return 0;
}
4.1.3 三元运算符 ? :

简化 简单的二分支 if-else 逻辑,用一行代码完成赋值、输出、返回值,让代码更简洁紧凑。

#include <stdio.h>
int main() {
    int num = 10;
    // 三元运算符:一行完成赋值
    char* result = (num > 5) ? "大于5" : "小于等于5";
    printf("%s\n", result);
    return 0;
}

4.2 循环语句(重复执行)

4.2.1 while 循环
// 循环打印 1~3
int i = 1;
while (i <= 3) {
    printf("%d ", i);
    i++;
}
// 输出:1 2 3
4.2.2 for 循环(最常用)
// 循环打印 1~3
for (int i = 1; i <= 3; i++) {
    printf("%d ", i);
}
4.2.3 do-while 循环

先执行,后判断

int i = 1;
do {
    printf("%d ", i);
    i++;
} while (i <= 3);

4.3 跳转语句

4.3.1 break

跳出当前循环/switch

for (int i=1; i<=5; i++) {
    if (i == 3) break; // 到3直接退出循环
    printf("%d ", i);
}
// 输出:1 2
4.3.2 continue

跳过本次循环,执行下一次

for (int i=1; i<=3; i++) {
    if (i == 2) continue;
    printf("%d ", i);
}
// 输出:1 3
4.3.3 return

函数返回,结束函数执行

int add(int a, int b) {
    return a + b; // 返回结果,函数结束
}

5、函数

函数是 C 语言中封装代码、实现模块化的核心单元。

5.1 函数的基本概念

5.1.1 作用
  1. 复用代码,避免重复编写
  2. 拆分复杂逻辑,让代码更清晰
  3. 支持模块化开发(Linux 项目标准写法)
5.1.2 标准语法
返回值类型  函数名(参数列表) {
    函数体;         // 具体逻辑
    return 返回值;   // 有返回值时必须写
}
  • 返回值类型:函数执行后返回的数据类型(无返回值用 void
  • 函数名:自定义名称(见名知意)
  • 参数列表:外部传入的数据(无参数用 void

5.2 函数的分类(4种基础形式)

5.2.1 无参无返回值

适用:执行固定逻辑,无需输入输出

#include <stdio.h>

// 函数定义
void print_hello(void) {
    printf("Hello Linux System\n");
}

int main() {
    print_hello();  // 函数调用
    return 0;
}
5.2.2 有参无返回值

适用:接收参数,执行逻辑,不返回结果

// 打印一个整数
void print_num(int num) {
    printf("输入的数字:%d\n", num);
}
5.2.3 无参有返回值

适用:无需参数,直接返回结果

// 模拟获取Linux进程PID
int get_pid(void) {
    return 10086;
}
5.2.4 有参有返回值(最常用)

适用:接收参数,计算后返回结果

// 两数相加
int add(int a, int b) {
    return a + b;
}

5.3 函数声明与定义分离

5.3.1 规则

C 语言要求:函数必须先声明后调用

  • 声明:告诉编译器函数的名称、参数、返回值(无函数体
  • 定义:函数的 具体 实现逻辑
5.3.2 代码示例

写法一: (推荐, 头文件分离)

#include <stdio.h>

// 函数声明(原型声明)
int add(int a, int b);

int main() {
    printf("%d\n", add(10, 20)); // 调用
    return 0;
}

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

写法二:

#include <stdio.h>

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

int main() {
    printf("%d\n", add(10, 20)); // 调用
    return 0;
}

5.4 函数参数详解

5.4.1 形参 和 实参
  • 形参:函数定义时的占位符
  • 实参:函数调用时传入的真实数据
5.4.2 数组作为函数参数

传递数组时,本质传递的是数组首地址

// 遍历打印数组
void print_array(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

5.5 【核心进阶】可变参数

5.5.1 作用

函数可以接收任意数量的参数(printf 就是可变参数函数)

5.5.2 依赖头文件
#include <stdarg.h>
5.5.3 核心API
  1. va_list:定义参数列表变量
  2. va_start:初始化参数列表
  3. va_arg:获取下一个参数
  4. va_end:结束遍历
5.5.4 实战示例
#include <stdio.h>
#include <stdarg.h>

// 功能:求n个整数的和
// count:参数个数 ...:可变参数
int sum(int count, ...) {
    int total = 0;
    va_list ap;

    // 初始化:从count之后开始读取参数
    va_start(ap, count);

    // 循环获取所有参数
    for (int i = 0; i < count; i++) {
        total += va_arg(ap, int);
    }

    // 结束释放
    va_end(ap);
    return total;
}

int main() {
    printf("1+2+3 = %d\n", sum(3, 1, 2, 3));
    printf("10+20 = %d\n", sum(2, 10, 20));
    return 0;
}

5.6 函数递归

5.6.1 概念

函数自己调用自己,必须有终止条件

5.6.2 示例:求阶乘
// n! = n * (n-1) * ... * 1
int factorial(int n) {
    if (n == 1) {  // 终止条件
        return 1;
    }
    return n * factorial(n - 1);
}

5.7 函数指针 【Linux 开发核心】

5.7.1 作用

把 函数 作为 变量 传递,实现回调函数(内核、驱动、系统编程必备)

5.7.2 代码示例
#include <stdio.h>

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

int main() {
    // 定义函数指针:指向返回值int、参数(int,int)的函数
    int (*func_ptr)(int, int) = add;

    // 通过指针调用函数
    int res = func_ptr(5, 6);
    printf("结果:%d\n", res);
    return 0;
}

6、存储类

内存区域 存储内容 分配/释放 对应存储类
栈区 (Stack) 局部变量、函数参数 系统自动管理(自动创建/销毁) autoregister
静态数据区 (Static/Data) 全局变量、static 静态变量 程序启动分配,程序退出才释放 staticextern
堆区 (Heap) 动态申请的内存(malloc/calloc 手动管理(自己申请、自己释放) 无存储类对应
代码区 程序二进制指令 只读,程序全程存在 -
内存区域 是否在内存条上 说明
代码区 ✅ 是 程序指令,加载到内存
静态数据区 ✅ 是 全局/静态变量,全程在内存
栈区 (Stack) ✅ 是 局部变量,高频使用,在内存
堆区 (Heap) ✅ 是 malloc 动态内存,在内存

特殊情况(Linux Swap 交换分区
内存条空间不足 时:
Linux 会把暂时不用的内存数据,临时放到硬盘的交换分区(Swap),腾出内存条空间。

  • 这时候数据不在内存条上,而在硬盘
  • 程序需要用时,Linux 再自动放回内存条;
  • 这是系统自动优化,你写代码完全不用感知

寄存器大小由CPU 架构决定,是硬件出厂时就固定的规格,不是软件能改的。

# 速度 大比拼
CPU 寄存器  >  CPU 缓存  >  内存条(RAM)  >  硬盘

存储类是 C 语言的 核心特性,决定变量/函数的 存储位置、生命周期(存活时间)、作用域(能用的范围)、链接属性

C 语言共有 4 种标准存储类autoregisterstaticextern


6.1 核心概念(先看懂这3个词)

  1. 生命周期:变量从创建到销毁的时间
  2. 作用域:变量/函数能被访问的代码范围
  3. 存储位置:栈区、寄存器、静态数据区

6.2 四大 存储类 详解

6.2.1 auto(自动变量)
  1. 特点

    • C 语言局部变量 默认 存储类,可以省略不写
    • 存储在 栈区,函数/代码块结束 自动销毁
    • 作用域:仅当前代码块内有效
    • 生命周期:随代码块执行创建/销毁
  2. 代码示例

#include <stdio.h>

void test() {
    // 完整写法:auto int a = 10;
    // 省略auto(默认就是auto)
    int a = 10; 
    printf("auto变量:%d\n", a);
}

int main() {
    test();
    // a 在这里无法访问(作用域仅限test函数)
    return 0;
}
  1. 使用场景
    所有普通局部变量,无需手动写 auto,编译器自动识别。

6.2.2 register(寄存器变量)
  1. 特点

    • 存储在 CPU 寄存器 中(速度最快,比内存快10倍+)
    • 高频使用的变量专用(Linux 底层循环、计数)
    • 不能取地址(寄存器没有内存地址)
    • 编译器可优化忽略(资源不足 时 直接降级,自动转为auto
    • 只能用 register 修饰 基本数据类型:
      ✅ 支持:int、char、float、指针
      ❌ 不支持:数组、结构体、联合体(太大 / 太复杂,寄存器存不下)
    • register 只是「礼貌建议」,不是「强制命令」。register ≠ 保证存入寄存器,只是你的一个愿望,编译器可以拒绝。
    • 现代 GCC/Clang 编译器,完全不需要 手动写 register!
      编译器会 自动 把 最频繁 使用的 变量 放进寄存器,比你手动指定更高效。
      register 现在基本属于 废弃语法,仅在极底层的嵌入式 / 内核汇编里偶尔用到
  2. 代码示例

#include <stdio.h>

int main() {
    // 定义寄存器变量:高频循环计数器
    register int i;
    for(i = 0; i < 5; i++) {
        printf("register循环:%d\n", i);
    }

    // 错误用法:寄存器变量不能取地址
    // printf("%p", &i); 
    return 0;
}
  1. 使用场景
    Linux 底层开发、高频循环、性能敏感的局部变量。

6.2.3 static(静态变量/函数)

static存储类核心,分三种用法:静态局部变量、静态全局变量、静态函数

1)静态 局部变量
  • 存储在静态数据区整个程序运行期都不销毁
  • 作用域:仅函数内有效
  • 初始化仅执行一次
  • 静态局部变量 只在第一次调用函数时初始化,之后永远存在于静态数据区,不会销毁;
    第二次、第三次…… 调用函数时,直接复用上一次保留的值,完全自动,不需要你做任何操作。
#include <stdio.h>

void count() {
    // 静态局部变量:只初始化1次,函数退出不销毁
    static int num = 0; // 再次 调用时, 执行 static int num = 0; → 直接跳过!不执行初始化
    num++;
    printf("调用次数:%d\n", num);
}

int main() {
    count(); // 1
    count(); // 2
    count(); // 3
    return 0;
}
2)静态 全局变量 / 静态 函数
  • 作用域:仅限当前 .c 文件,外部文件无法访问
  • Linux 开发用途:封装内部函数,防止命名冲突
// static全局变量:仅当前文件可用
static int g_val = 100;

// static函数:仅当前文件可用(内部函数)
static void show() {
    printf("静态全局变量:%d\n", g_val);
}

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

6.2.4 extern(外部变量/函数)
  1. 作用

    • 声明外部定义的变量/函数,实现跨文件访问
    • 仅声明,不分配内存
    • 多文件编程、Linux 系统开发核心
  2. 多文件示例

  • 文件1:test.c(定义全局变量、函数)
#include <stdio.h>

// 全局变量(默认extern链接属性)
int num = 200;

// 全局函数
void extern_test() {
    printf("外部函数:%d\n", num);
}
  • 文件2:main.c(extern声明后调用)
#include <stdio.h>

// 声明外部变量
extern int num;
// 声明外部函数
extern void extern_test();

int main() {
    num = 300;
    extern_test(); // 调用外部文件函数
    return 0;
}
  • 编译:
# 方式一:直接编译并链接两个文件
clang main.c test.c -o main
./main

# 方式二: 分步编译再链接
clang -c main.c -o main.o
clang -c test.c -o test.o
clang main.o test.o -o main
./main

6.3 四大存储类核心对比表

存储类 存储位置 生命周期 作用域 核心用途
auto 栈区 代码块内 局部 默认局部变量
register CPU寄存器 代码块内 局部 高频性能变量
static 静态数据区 全程 局部/当前文件 持久变量、文件内部封装
extern 静态数据区 全程 全局/跨文件 跨文件调用

6.4 总结

  1. auto:局部变量默认,不用写
  2. register:高速寄存器变量,性能优化用
  3. static:静态持久化、文件私有,Linux 开发高频
  4. extern:跨文件访问,多文件项目必备

【三】复合类型

1、数组

1.1 核心概念

一次性定义多个相同类型的变量,这些变量在内存中连续存放,用同一个名字+下标访问,大小固定不可修改。

1.2 定义语法
数据类型 数组名[数组长度];
  • 数据类型:数组中所有元素的类型(int/char 等)
  • 数组名:自定义名称(见名知意)
  • 长度:数组能存放的元素个数(必须是常量)
1.3 初始化方式(3种常用)
#include <stdio.h>
int main() {
    // 1. 完全初始化(赋值个数 = 数组长度)
    int arr1[3] = {10, 20, 30};

    // 2. 部分初始化(未赋值元素自动为 0)
    int arr2[5] = {1, 2};  // 等价 [1,2,0,0,0]

    // 3. 省略长度初始化(编译器自动计算长度)
    int arr3[] = {11, 22, 33};  // 长度自动为3

    return 0;
}
1.4 核心特性(⭐必背重点)
  1. 下标从 0 开始:第一个元素下标 0,最后一个下标 长度-1
  2. 内存连续:所有元素在内存条上挨着存放
  3. 大小固定:定义后不能新增/删除元素
  4. 数组名 = 数组首地址(Linux 指针/函数传参核心)
1.5 数组遍历(访问所有元素)

通过 for 循环 + 下标访问:

#include <stdio.h>
int main() {
    int arr[4] = {1, 2, 3, 4};
    // 遍历数组
    for (int i = 0; i < 4; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}
1.6 数组作为函数参数

传递数组时,只会传递首地址,不会拷贝整个数组(高效):

// 数组参数:int arr[] 等价于 int *arr
void print_arr(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}
1.7 数组长度

总大小 = 单个元素大小 × 元素个数

#include <stdio.h>
int main() {
    int arr[5];
    // sizeof(数组名) = 整个数组的字节数
    printf("数组总大小:%zu\n", sizeof(arr));   // 20(4字节×5)
    printf("单个元素大小:%zu\n", sizeof(arr[0])); // 4
    printf("元素个数:%zu\n", sizeof(arr)/sizeof(arr[0])); // 5(开发必用写法)
    return 0;
}

2、字符串(char 数组)

2.1 核心概念

C 语言没有专门的字符串类型,字符串的本质是:
        \0(结束符)结尾的字符数组(char 数组)

  • \0:ASCII 码为 0 的字符,标记字符串结束
  • 所有字符串操作(打印、拷贝)都依赖 \0
2.2 定义与初始化(2种标准写法)
#include <stdio.h>
int main() {
    // 1. 双引号初始化(自动在末尾加 \0,最常用)
    char str1[] = "hello";

    // 2. 字符数组初始化(手动加 \0,否则不是字符串)
    char str2[] = {'h', 'e', 'l', 'l', 'o', '\0'};

    // 直接打印字符串
    printf("%s\n", str1);  // %s 是字符串格式符
    return 0;
}
2.3 重点区分:字符串 vs 普通字符数组
// ✅ 字符串:有 \0,可正常打印、操作
char str[] = "abc";  // 内存:a b c \0

// ❌ 普通字符数组:无 \0,不是字符串,打印会乱码
char arr[] = {'a','b','c'};
2.4 字符串常用操作(需引头文件)
#include <stdio.h>
#include <string.h>  // 字符串操作必备头文件
int main() {
    char str[] = "linux";
    // 1. 求长度(不计 \0)
    int len = strlen(str);
    printf("长度:%d\n", len);  // 5

    // 2. 字符串拷贝
    char dest[20];
    strcpy(dest, str);  // 把 str 复制到 dest
    return 0;
}
2.5 注意事项
  1. 定义字符串数组时,长度要比实际内容多 1(存 \0
  2. 打印字符串用 %s,不能用 %c
  3. 字符串 不能直接用 = 赋值,必须用 strcpy

3、typedef 类型别名

3.1 核心作用

已有的数据类型起一个别名简化代码提高可读性、统一类型规范,是 Linux 内核/系统开发标配语法

3.2 语法格式
typedef 原类型名 别名;
3.3 常用场景 + 代码示例
场景1:简化基本类型(精简代码)
// 给 int 起别名 u32(Linux 内核常用)
typedef unsigned int u32;

// 等价:unsigned int a = 10;
u32 a = 10;
场景2:简化数组类型(复用复杂类型)
// 给 int[5] 数组起别名 IntArray
typedef int IntArray[5];

// 等价:int arr[5];
IntArray arr = {1,2,3,4,5};
场景3:统一命名(跨平台/大型项目)
// 统一大小端类型,方便移植
typedef char int8;
typedef short int16;
typedef int int32;

4、枚举

4.1 核心概念

枚举(enum)用来定义一组固定的整型常量,专门表示状态、类型、选项等有限的取值,替代代码中的“魔法数字”,让代码可读性拉满。
开发场景:Linux 状态机、错误码、设备类型、权限类型。

4.2 定义语法

// 定义枚举类型
enum 枚举名 {
    常量1,
    常量2,
    常量3
};
  • 默认取值:从 0 开始自动递增
  • 可手动指定值,后续自动顺延
// 定义枚举 变量
方式一:
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
day = TUE; // 可以接受 任意一个 枚举值


方式二:
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

方式三:
enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

4.3 代码示例(开发标准写法)

#include <stdio.h>

// 定义:文件状态枚举(Linux开发常用)
enum FileStatus {
    CLOSED,   // 0:关闭
    OPENED,   // 1:打开
    LOCKED,   // 2:锁定
    ERROR = -1,// 手动赋值 后, 自动顺延
    ERROR2,// 0
    ERROR3,// 1
};

int main() {
    printf("文件状态:%d\n", OPENED);
    printf("文件状态:%d\n", ERROR2);
    printf("文件状态:%d\n", ERROR3);


    // 定义 枚举变量
    enum FileStatus status = OPENED;

    // 打印枚举值(本质是整数)
    printf("文件状态:%d\n", status);

    int a = 1;

    printf("文件状态:%s\n", (status == a) ? "打开" : "其他");

    // 实战:分支判断
    if (status == OPENED) {
        printf("文件已打开\n");
    }
    return 0;
}
  • 将 整数 转换为 枚举
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
 
    enum day
    {
        saturday,
        sunday,
        monday,
        tuesday,
        wednesday,
        thursday,
        friday
    } workday;
 
    int a = 1;
    enum day weekend;
    
    weekend = ( enum day ) a;  //类型转换
    
    //weekend = a; // 直接赋值 是 错误的
    
    printf("weekend:%d",weekend);
    return 0;
}

4.4 开发核心特性

  1. 本质是整型,底层和 int 完全一样
  2. 一组相关常量,禁止随意赋值
  3. 提高可读性:用 OPENED 代替数字 1
  4. 适用于固定取值:状态、类型、错误码

5、共用体(了解即可,实际用得极少)

5.1 核心概念

共用体(union):是一种特殊的 数据类型,允许你在 相同的内存位置 存储 不同的 数据类型
您可以定义一个带有多成员的共用体,但是 同一时刻 只能有一个成员带有值。
对应 你之前掌握的 Rust 的 联合体 的概念。

价值:
通过内存复用,压缩 / 优化程序的内存占用, 对于 小内存的设备(嵌入式 / 单片机开发, 设备内存极小,只有 几百字节~几 KB) 是很有用的。

5.2 定义语法

union 共用体名 {
    成员1;
    成员2;
};

5.3 核心特性

  1. 共享内存:所有成员共用一段内存
  2. 大小 = 最大成员的大小
  3. 赋值一个成员,会覆盖其他成员的值

5.4 代码示例

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}

输出:

data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming   // 只要 字符串  能 正常输出

共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。


6、结构体(⭐⭐⭐ C语言开发核心)

6.1 核心概念

结构体(struct):封装不同类型的数据,描述一个复杂对象(如:用户、文件、进程、设备),是 Linux 内核/应用开发最核心的数据结构
例:用户 = 整型ID + 字符串名字 + 整型年龄。

6.2 定义与变量声明(3种开发常用方式)

#include <stdio.h>

// 1. 标准定义:先定义结构体,再声明变量
struct User {
    int id;         // 整型
    char name[20];  // 字符串
    int age;        // 整型
};

int main() {
    // 声明变量 + 初始化
    struct User u1 = {1001, "张三", 25};
    return 0;
}

6.3 成员访问运算符(开发必背)

  1. 普通变量:用 . 访问成员
  2. 指针变量:用 -> 访问成员
// 普通变量访问
u1.id = 1002;
printf("%s", u1.name);

// 指针变量访问
struct User *p = &u1;
p->age = 26;        // 等价于 (*p).age

6.4 结构体数组(批量存储对象)

开发中用于存储多个用户、多个设备

// 结构体数组
struct User users[3] = {
    {1001, "张三", 25},
    {1002, "李四", 26},
    {1003, "王五", 27}
};

// 遍历
for(int i=0; i<3; i++){
    printf("ID:%d,姓名:%s\n", users[i].id, users[i].name);
}

6.5 结构体指针(模拟 结构体方法 的基础)

函数传参、内存分配必用指针,避免拷贝整个结构体

// 指针访问成员
void print_user(struct User *p) {
    printf("ID:%d\n", p->id);
}

相比于 你熟悉的 Golang,对于方法这个概念而言:
C 语言的结构体:完全没有「方法」的概念!
Go 语言的结构体:原生支持绑定「方法」,是面向对象的核心特性

C 是面向过程语言,结构体仅用来打包数据
Go 是面向对象语言,结构体 = 数据 + 行为(方法),这是两者最本质的区别。

可以 借助 结构体指针 间接的 让 C 语言 拥有 “方法”。

1. 方式一: 独立函数
#include <stdio.h>

// 1. 结构体:仅封装数据,没有任何函数/方法
typedef struct {
    char name[20];
    int age;
} Student;

// 2. 独立函数:专门用来操作结构体(模拟方法)
// 必须传入结构体指针,才能修改/访问数据
void print_student(Student *stu) {
    printf("姓名:%s,年龄:%d\n", stu->name, stu->age);
}

int main() {
    Student stu = {"小明", 18};
    print_student(&stu);  // 调用函数,手动传结构体地址
    return 0;
}
2. 方式二: 函数指针

C 语言没有原生方法,但可以把函数指针放进结构体,实现最接近方法的效果(Linux 驱动、内核大量使用):

#include <stdio.h>

typedef struct Student {
    char name[20];
    int age;
    // 函数指针:模拟结构体方法
    void (*print)(struct Student *);
} Student;

// 函数实现
void print(struct Student *stu) {
    printf("%s, %d岁\n", stu->name, stu->age);
}

int main() {
    Student stu = {"小红", 20, print};
    stu.print(&stu);  // 看起来像方法,本质是函数指针调用
    return 0;
}

6.6 组合

// 地址结构体
struct Address {
    char city[20];
};

// 用户结构体嵌套地址
struct User {
    int id;
    struct Address addr;
};

// 访问嵌套成员
u1.addr.city = "北京";

6.7 typedef 简化结构体(开发强制规范)

Linux 内核/工业代码 100% 用这种写法

// typedef + 结构体:简化名称,去掉 struct
typedef struct {
    int id;
    char name[20];
    int age;
} User;  // 别名:User

// 直接使用别名(无需写struct)
User u1 = {1001, "张三", 25};

核心前提必须明确
C 语言 原生没有 动态数组、Map(键值对) 这两种数据结构!
需要通过 内存管理(malloc/realloc)+ 结构体 手动实现,这是 Linux 开发的标准做法。

7、动态数组

7.1 核心概念

  • 普通数组:长度固定,定义后无法改变大小;
  • 动态数组:运行时可自动扩容/缩容,通过 malloc/realloc/free 手动管理内存;
  • 本质:指针 + 已用长度 + 总容量,是 C 语言处理可变长数据的唯一方案。

7.2 核心结构(开发标准封装)

// 动态数组结构体(存储int类型数据)
typedef struct {
    int *data;       // 指向数组内存的指针
    int len;         // 已使用的元素个数
    int capacity;    // 总容量(最大能存多少)
} DynamicArray;

7.3 完整实战代码(初始化、增、删、改、查、扩容)

#include <stdio.h>
#include <stdlib.h>

// 动态数组结构体
typedef struct {
    int *data;
    int len;
    int capacity;
} DynamicArray;

// 1. 初始化动态数组
void initArray(DynamicArray *arr, int initCapacity) {
    arr->data = (int *)malloc(sizeof(int) * initCapacity);
    arr->len = 0;
    arr->capacity = initCapacity;
}

// 2. 自动扩容(核心:容量不足时翻倍)
void resizeArray(DynamicArray *arr) {
    // 新容量 = 原容量 * 2
    int newCapacity = arr->capacity * 2;
    // realloc:重新分配内存(自动拷贝旧数据)
    arr->data = (int *)realloc(arr->data, sizeof(int) * newCapacity);
    arr->capacity = newCapacity;
    printf("扩容成功,新容量:%d\n", newCapacity);
}

// 3. 添加元素(尾部追加)
void addArray(DynamicArray *arr, int val) {
    // 容量不足 → 扩容
    if (arr->len >= arr->capacity) {
        resizeArray(arr);
    }
    arr->data[arr->len++] = val;
}

// 4. 遍历数组
void showArray(DynamicArray *arr) {
    printf("动态数组元素:");
    for (int i = 0; i < arr->len; i++) {
        printf("%d ", arr->data[i]);
    }
    printf("\n");
}

// 5. 释放内存(必须手动释放,防止泄漏)
void freeArray(DynamicArray *arr) {
    free(arr->data);
    arr->data = NULL;
    arr->len = 0;
    arr->capacity = 0;
}

// 测试
int main() {
    DynamicArray arr;
    // 初始化容量为2
    initArray(&arr, 2);

    // 添加元素(自动触发2次扩容)
    addArray(&arr, 10);
    addArray(&arr, 20);
    addArray(&arr, 30);
    addArray(&arr, 40);
    addArray(&arr, 50);

    showArray(&arr);
    freeArray(&arr);
    return 0;
}

7.4 重点

  1. 动态数组 = 指针 + 长度 + 容量,缺一不可;
  2. 扩容用 realloc自动拷贝旧数据,开发首选;
  3. 必须手动释放内存,否则会造成内存泄漏;
  4. Linux 开发中,动态数组是处理可变长数据的基础。

8、Map(键值对)

8.1 核心概念

  • Map:key-value(键值对) 结构,通过键快速查找值;
  • C 语言无原生 Map,工业开发用 glib 库哈希表,学习阶段用结构体+数组实现简易版;
  • 适用场景:配置映射、ID-数据对应、哈希存储。

8.2 核心结构(键值对封装)

// 键值对结构体(key=int, value=int)
typedef struct {
    int key;    // 键(唯一)
    int value;  // 值
} KeyValue;

// Map结构体(管理多个键值对)
typedef struct {
    KeyValue *data;
    int len;
    int capacity;
} Map;

8.3 完整实战代码(简易整型Map:增、查、删)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 键值对
typedef struct {
    int key;
    int value;
} KeyValue;

// Map结构体
typedef struct {
    KeyValue *data;
    int len;
    int capacity;
} Map;

// 1. 初始化Map
void initMap(Map *map, int initCapacity) {
    map->data = (KeyValue *)malloc(sizeof(KeyValue) * initCapacity);
    map->len = 0;
    map->capacity = initCapacity;
}

// 2. 根据key查找值(返回下标,-1=未找到)
int findMap(Map *map, int key) {
    for (int i = 0; i < map->len; i++) {
        if (map->data[i].key == key) {
            return i;
        }
    }
    return -1;
}

// 3. 添加/修改键值对
void putMap(Map *map, int key, int value) {
    int index = findMap(map, key);
    // key已存在 → 修改值
    if (index != -1) {
        map->data[index].value = value;
        return;
    }
    // 容量不足 → 扩容
    if (map->len >= map->capacity) {
        map->data = (KeyValue *)realloc(map->data, sizeof(KeyValue) * map->capacity * 2);
        map->capacity *= 2;
    }
    // 新增键值对
    map->data[map->len].key = key;
    map->data[map->len].value = value;
    map->len++;
}

// 4. 根据key获取值
int getMap(Map *map, int key) {
    int index = findMap(map, key);
    if (index == -1) {
        printf("key不存在!\n");
        return -1;
    }
    return map->data[index].value;
}

// 5. 释放内存
void freeMap(Map *map) {
    free(map->data);
    map->data = NULL;
    map->len = 0;
    map->capacity = 0;
}

// 测试
int main() {
    Map map;
    initMap(&map, 2);

    putMap(&map, 101, 99);
    putMap(&map, 102, 88);
    putMap(&map, 101, 100); // 重复key,修改值

    printf("key=101,value=%d\n", getMap(&map, 101));
    printf("key=102,value=%d\n", getMap(&map, 102));

    freeMap(&map);
    return 0;
}

8.4 开发必背重点

  1. C 原生无 Map,简易版用数组+结构体实现,适合学习;
  2. 工业级 Linux 开发:使用 glib 库的 GHashTable 哈希表(高性能、成熟);
  3. Key 必须唯一,查找效率是 Map 的核心;
  4. 同样需要手动管理内存

8.5 工业级方案(Linux 开发推荐)

// 安装 glib 库(Ubuntu)
// sudo apt install libglib2.0-dev
// 官方哈希表(高性能Map,开发直接用)
#include <stdio.h>
#include <glib.h>  // GLib 核心头文件

// 遍历回调函数:打印哈希表所有键值对
void print_map(gpointer key, gpointer value, gpointer user_data) {
    printf("Key: %-8s | Value: %d\n", (char*)key, GPOINTER_TO_INT(value));
}

int main() {
    // ======================================
    // 1. 创建哈希表(工业级标准写法)
    // 参数1:key 哈希函数 g_str_hash   (字符串专用)
    // 参数2:key 比较函数 g_str_equal (字符串专用)
    // ======================================
    GHashTable* map = g_hash_table_new(g_str_hash, g_str_equal);

    // ======================================
    // 2. 插入键值对(Key唯一,重复Key会覆盖旧值)
    // ======================================
    g_hash_table_insert(map, "name", GINT_TO_POINTER(100));
    g_hash_table_insert(map, "age",  GINT_TO_POINTER(25));
    g_hash_table_insert(map, "pid",  GINT_TO_POINTER(8888));
    g_hash_table_insert(map, "age",  GINT_TO_POINTER(26)); // 覆盖:age=26

    // ======================================
    // 3. 根据 Key 查找 Value(核心:极速O(1)查找)
    // ======================================
    gpointer val = g_hash_table_lookup(map, "pid");
    if (val != NULL) {
        printf("\n【查找成功】pid = %d\n", GPOINTER_TO_INT(val));
    }

    // ======================================
    // 4. 遍历所有键值对
    // ======================================
    printf("\n【遍历所有键值对】\n");
    g_hash_table_foreach(map, print_map, NULL);

    // ======================================
    // 5. 删除指定 Key
    // ======================================
    g_hash_table_remove(map, "name");
    printf("\n【删除name后】\n");
    g_hash_table_foreach(map, print_map, NULL);

    // ======================================
    // 6. 销毁哈希表(自动释放所有内存,无泄漏)
    // ======================================
    g_hash_table_destroy(map);
    printf("\n哈希表已销毁,内存释放完成\n");

    return 0;
}

/*

编译命令(必须链接 GLib 库)
gcc main.c -o map `pkg-config --cflags --libs glib-2.0`

运行:
./map

*/

【四】进阶特性

1、预处理器

1.1 核心概念

C 语言预处理器 是 编译动作之前 自动执行的工具, 专门处理 # 开头的指令,作用:
文本替换、文件包含、条件编译不参与程序运行,仅在编译前工作。

1.2 #include 文件包含指令

作用:将 头文件的内容原封不动 复制到 当前文件中。
两种写法(开发必区分)

  1. #include <xxx.h>:包含系统标准头文件(如 stdio.h、stdlib.h)
  2. #include "xxx.h":包含自定义头文件(自己写的 .h 文件)

代码示例

// 包含系统头文件
#include <stdio.h>
// 包含自定义头文件
#include "my_utils.h"

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

1.3 #define 宏定义指令

作用:定义常量、带参函数、代码片段,纯文本替换,无类型检查。
是 Linux 开发中定义常量、简化代码的核心工具。

1.3.1 常量宏(最常用)
#include <stdio.h>

// 定义常量:纯文本替换,编译时 PI 替换为 3.14159
#define PI 3.14159
// 定义数组最大容量(Linux 开发标准写法)
#define MAX_SIZE 1024

int main() {
    printf("圆周率:%f\n", PI);
    printf("最大容量:%d\n", MAX_SIZE);
    return 0;
}
1.3.2 带参宏(模拟函数,效率更高)

注意:宏没有函数调用开销,但是纯文本替换,必须加括号!

#include <stdio.h>

// 错误写法:无括号,计算会出错
#define ADD(a,b) a+b
// 正确写法:带参宏必须给参数和整体加括号
#define ADD_SAFE(a,b) ((a)+(b))

int main() {
    // 错误:2*3+4 = 10
    printf("%d\n", 2 * ADD(3,4));
    // 正确:2*(3+4) = 14
    printf("%d\n", 2 * ADD_SAFE(3,4));
    return 0;
}
1.3.3 #undef 取消宏定义
#define NUM 100
#undef NUM   // 取消 NUM 宏
// NUM 失效,无法使用

1.4 条件编译指令(#ifdef/#ifndef/#endif)

作用:根据条件,决定哪些代码参与编译,Linux 开发核心用途

  1. 防止头文件重复包含
  2. 跨平台编译
  3. 调试日志开关
1.4.1 #ifdef / #else / #endif:判断宏是否定义
#include <stdio.h>

// 定义调试宏
#define DEBUG

int main() {
#ifdef DEBUG
    // 调试模式:打印日志
    printf("调试模式已开启\n");
#else
    // 发布模式:不打印
    printf("生产环境\n");
#endif
    return 0;
}
1.4.2 #ifndef:判断宏未定义(头文件保护专用)
// 如果未定义 XXX_H,则编译后续代码
#ifndef XXX_H
#define XXX_H

// 声明内容

#endif

1.5 系统预定义宏(调试必备, 对应 php 的 魔法变量)

C 语言内置的宏,用于打印日志、定位错误,Linux 开发高频使用

#include <stdio.h>

int main() {
    printf("文件名:%s\n", __FILE__);     // 当前文件名
    printf("行号:%d\n", __LINE__);       // 当前行号
    printf("编译日期:%s\n", __DATE__);   // 编译日期
    printf("编译时间:%s\n", __TIME__);   // 编译时间
    return 0;
}

1.6 预处理器运算符

1.6.1 宏延续运算符(\)

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
1.6.2 字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:

#include <stdio.h>
 
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
 
int main(void)
{
   message_for(Carole, Debra);
   return 0;
}

输出: Carole and Debra: We love you!
1.6.3 字符串常量化运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

#include <stdio.h>
 
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

2、头文件规范

头文件(.h) 是 C 语言多文件开发的灵魂,Linux 内核、大型项目的核心规范。
一句话总结头文件只做声明不做实现

2.1 头文件核心作用

  1. 声明 函数结构体枚举 (巧记: 韩红姐妹)
  2. 对外提供接口,隐藏实现细节
  3. 实现多文件代码复用

2.2 头文件保护(重中之重)

问题:头文件被多个文件包含,会导致重复定义编译报错
解决方案:所有头文件必须加保护宏(Linux 标准写法)。

标准写法

// 1. 判断是否已定义该宏
#ifndef MY_UTILS_H
// 2. 定义该宏
#define MY_UTILS_H

// 3. 所有声明写在这里
void print_hello();
#define VERSION "1.0.0"

// 4. 结束判断
#endif

现代写法(部分编译器支持)

#pragma once

// 声明内容

工业级开发优先用 #ifndef 方案,跨平台兼容性最好。


2.3 标准书写规范

  1. 只放声明,不放实现
    函数声明、结构体声明、宏定义 → 放 .h
    函数实现、变量定义 → 放 .c
  2. 必须加头文件保护
  3. 不定义全局变量(极易引发重复定义错误)
  4. 文件名 和 功能对应(如 file.h 对应文件操作)

2.4 多文件开发标准示例(Linux 实战)

my_utils.h(头文件:仅声明)
#ifndef MY_UTILS_H
#define MY_UTILS_H

// 函数声明
void print_hello();
int add(int a, int b);

// 宏声明
#define MAX 100

#endif
my_utils.c(源文件:仅实现)
#include "my_utils.h"
#include <stdio.h>

// 函数实现
void print_hello() {
    printf("hello from utils\n");
}

int add(int a, int b) {
    return a + b;
}
main.c(主程序:调用接口)
#include <stdio.h>
#include "my_utils.h"

int main() {
    print_hello();
    printf("1+2=%d\n", add(1,2));
    printf("MAX=%d\n", MAX);
    return 0;
}
编译运行
gcc main.c my_utils.c -o demo
./demo

2.5 开发避坑

  1. ❌ 头文件中写函数实现
  2. ❌ 头文件中定义全局变量
  3. ❌ 忘记加头文件保护
  4. ❌ 循环包含头文件(a.h 包含 b.h,b.h 包含 a.h)

2.6 C语言标准库常用头文件

Linux 系统开发 / 日常C语言开发 高频使用场景,整理最核心、必背的标准库头文件,分为 标准C通用库Linux POSIX专用库 两类,一目了然。

  • 标准C通用头文件(跨平台,所有系统都能用)
头文件名称 功能分类 核心作用简介
stdio.h 输入输出 最常用,提供printf/scanf、文件操作fopen/fread/fwrite、缓冲区操作
stdlib.h 通用工具 动态内存malloc/free/realloc、程序退出exit、随机数rand、数值转换
string.h 字符串处理 字符串操作strlen/strcpy/strcmp/strcat、内存拷贝memcpy/memset
math.h 数学运算 数学函数sqrt/pow/sin/cos/fabs,编译需加 -lm
ctype.h 字符操作 字符判断isdigit/isalpha、大小写转换toupper/tolower
time.h 时间日期 获取系统时间time、格式化时间localtime、随机数种子srand
stdbool.h 布尔类型 提供C语言布尔值true/false
stdint.h 固定宽度整型 Linux开发必备,定义uint8_t/u32/int32_t等跨平台整数类型
stddef.h 通用定义 定义size_tNULL、偏移量offsetof

  • Linux POSIX 专用头文件(工业级Linux开发必用)
    仅用于 Linux/Unix 系统,是服务端、驱动、系统工具开发的核心头文件
头文件名称 功能分类 核心作用简介
unistd.h 系统调用 Linux核心,进程/文件操作fork/read/write/close/sleep
fcntl.h 文件控制 文件打开模式、文件锁、异步IO控制
sys/stat.h 文件属性 获取文件状态、修改文件权限chmod
signal.h 信号处理 Linux进程信号注册、处理signal/kill
pthread.h 多线程 Linux多线程创建、管理pthread_create,编译需加 -pthread

日常开发优先用 标准C库,Linux系统级开发才使用 POSIX专用库


3、指针

指针是 C 语言的灵魂,也是 Linux 系统开发的核心基础。指针本质是内存地址,指针变量就是专门用来存储内存地址的变量。
掌握指针,才能真正理解 C 语言的 内存操作。

3.1 指针基础概念

  • 内存地址:系统给每一字节内存分配的唯一编号(用十六进制表示)。
  • 指针变量:存储「内存地址」的变量。
  • 核心符号
    • &取地址符,获取变量的内存地址
    • *解引用符,通过地址访问对应内存的值

三弟 看到这里 问你一个问题: 为什么 内存地址 用十六进制表示, 而不是 二进制、十进制?

  • 二进制:位数过长,32 位地址要写 32 个 0/1,查看、记录极麻烦。
  • 十进制:和二进制无便捷换算关系,没法直观看出字节边界,底层调试不友好。
  • 十六进制:1 位 = 4 个二进制位,两位恰好 1 字节,是二进制的简写,换算轻松、字符简短,贴合内存按字节编址规则,适配调试、指针查看。

3.2 指针的 定义 与 基本使用

#include <stdio.h>

int main() {
    int a = 100;         // 普通变量
    int *p = &a;         // 定义指针变量 p,存储 a 的地址

    // 1. 打印变量a的值、地址
    printf("a 的值:%d\n", a);
    printf("a 的地址:%p\n", &a);
    
    // 2. 打印指针变量存储的地址、通过指针访问值
    printf("指针p 存储的地址:%p\n", p);
    printf("通过指针访问 a 的值:%d\n", *p);

    // 3. 通过指针修改变量值
    *p = 200;
    printf("修改后 a 的值:%d\n", a);
    return 0;
}

3.3 指针的步长

指针的类型决定了能操作的数据类型,今儿决定了 指针移动 时的步长

  • int *p:操作 int 型数据,步长 4 字节
  • char *p:操作 char 型数据,步长 1 字节
  • float *p:操作 float 型数据,步长 4 字节

3.4 指针与数组(开发高频用法)

数组名本质是数组首元素的地址,可以用指针直接操作数组:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40};
    int *p = arr;  // 数组名 = 首地址,无需 &

    // 指针遍历数组
    for (int i = 0; i < 4; i++) {
        printf("%d ", *(p + i));  // 等价 arr[i]
    }
    return 0;
}

说明: *(p + i) 为什么 = arr[i]

  • 第一步:指针运算 p + i
p 是 int 型指针,指针 + 数字,不是加字节,是加「元素个数」
p + 0 → 指向 arr[0]
p + 1 → 指向 arr[1]
p + 2 → 指向 arr[2]
p + i → 指向 arr[i]
  • 第二步:解引用 *(p + i)
* 代表:取指针指向的数值
*(p+0) → 取 arr[0] 的值
*(p+1) → 取 arr[1] 的值
*(p+i) → 取 arr[i] 的值

3.5 空指针 & 野指针(避坑)

3.5.1 空指针(NULL)
  • 指向合法的空地址,表示指针不指向任何有效内存
  • 安全、可判断,是指针初始化的标准写法
int *p = NULL;  // 初始化空指针(推荐)
3.5.2 野指针
  • 指向无效/未知内存的指针,极易导致程序崩溃
  • 产生原因:指针未初始化内存已释放未置空
// ❌ 错误:未初始化的野指针
int *p;

// ✅ 正确:初始化为空指针
int *p = NULL;

3.6 空指针 判断

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a = NULL; // 声明一个指针变量a,并初始化为NULL

    if (a)
    {
        printf("a is not NULL\n");
    }
    else
    {
        printf("a is NULL\n"); 
    }

    // 给 指针赋值
    a = (int *)malloc(sizeof(int));
    *a = 10;

    if (a)
    {
        printf("a is not NULL\n");
    }
    else
    {
        printf("a is NULL\n");
    }


    return 0;
}

输出:
a is NULL
a is not NULL

3.7 指针与函数:传址调用(核心用途)

普通函数传参是值拷贝,无法修改实参;
指针传参是传地址,可以直接修改外部变量

#include <stdio.h>

// 传址调用:修改实参的值
void change(int *p) {
    *p = 999;
}

int main() {
    int a = 10;
    change(&a);  // 传入地址
    printf("修改后 a:%d\n", a);  // 999
    return 0;
}

4、内存管理

C 语言的内存分为 四大区域,内存管理的核心是 堆内存的手动申请与释放,这是 Linux 开发、动态数据结构(动态数组、Map)的基础。

4.1 程序四大内存分区(必背)

内存区域 存储内容 管理方式 生命周期
栈区(Stack) 局部变量、函数参数 系统自动分配/释放 函数/代码块执行期间
静态数据区 全局变量、static 变量 程序启动分配,退出释放 整个程序运行期间
堆区(Heap) 动态申请的内存 手动 malloc 申请、free 释放 手动控制
代码区 程序二进制指令 系统管理 整个程序运行期间

4.2 堆内存管理核心函数(<stdlib.h>

堆内存是开发中最常用的动态内存,必须手动管理,四个核心函数:

重点说明:
变量 放在 堆内存 还是 栈内存 本质上是由于 生命周期 决定的, 而不是 变量的 数据类型

4.2.1 malloc:申请堆内存(未初始化)
// 申请 4 字节 int 类型内存
int *p = (int *)malloc(sizeof(int));
4.2.2 calloc:申请堆内存(自动初始化为 0)
// 申请 4 个 int 大小内存,全部初始化为 0
int *p = (int *)calloc(4, sizeof(int));
4.2.3 realloc:扩容已申请的堆内存
// 扩容为 8 个 int 大小
p = (int *)realloc(p, 8 * sizeof(int));
4.2.4 free:释放堆内存(必须用!)
  • 防止内存泄漏
  • 释放后置空,避免野指针
free(p);
p = NULL;  // 关键:置空指针

4.3 内存管理完整实战代码

#include <stdio.h>
#include <stdlib.h>  // 内存管理必备头文件

int main() {
    // 1. 申请堆内存(int 变量)
    int *p = (int *)malloc(sizeof(int));
    if (p == NULL) {  // 判断是否申请成功
        printf("内存申请失败!\n");
        return 1;
    }
    *p = 666;
    printf("堆内存值:%d\n", *p);

    // 2. 申请数组堆内存
    int *arr = (int *)calloc(3, sizeof(int));
    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;

    // 3. 扩容数组
    arr = (int *)realloc(arr, 5 * sizeof(int));

    // 4. 释放内存(必须成对使用)
    free(p);
    free(arr);
    p = NULL;
    arr = NULL;

    printf("内存释放完成!\n");
    return 0;
}

4.4 malloc 和 calloc

malloccalloc 两者都是申请内存,作用完全一致,唯一不同只有 2 点:

  1. 参数格式不同
  2. 是否自动把内存清零
    自动清 0 含义:
    就是将 申请的内存 用类型 的 默认值 进行填充。
    不清0 的话,内存 存的是「随机垃圾值」,
    因为之前别的程序用过、释放后残留数据没擦除,
    malloc 只划内存、不改写原有内容,所以内存中存的是无规律乱数(随机值)
函数 参数格式 内存初始化 适用场景
malloc(总字节数) 只填总共要多少字节 不初始化(内存是随机垃圾值) 追求速度、不需要清零
calloc(个数, 单个字节数) 元素个数 + 单个大小 自动全部填 0 需要初始为0的场景(数组、缓冲区)
// 用malloc申请1个int(基本类型)→ 只是演示
int *p = (int *)malloc(sizeof(int));

// 用calloc申请3个int的数组 → 只是演示「批量申请」
int *arr = (int *)calloc(3, sizeof(int));
// 1. calloc 申请 基本数据类型(完全可以!)
int *p = (int *)calloc(1, sizeof(int)); // 申请1个int,自动清0

// 2. malloc 申请 数组/复合类型(完全可以!)
int *arr = (int *)malloc(5 * sizeof(int)); // 申请int数组,不清0

总结:

  1. malloc / calloc 和 基本类型/复合类型 无关
  2. 两者都能给 int数组结构体 申请内存
  3. 选谁只看一件事
    • 需要内存自动清0 → 用 calloc
    • 不需要初始化、追求快 → 用 malloc

4.5 开发中常见内存问题(致命坑)

  1. 内存泄漏
    只申请 malloc,不释放 free,内存越用越少,最终程序崩溃
  2. 重复释放
    对同一块内存调用两次 free,程序直接崩溃
  3. 内存越界
    访问超出申请范围的内存,破坏数据
  4. 野指针
    内存释放后未置空,继续使用指针

4.6 内存管理 黄金法则

  1. 申请 与 释放成对出现
  2. 释放 后 立即置空 指针
  3. 使用前 判断 指针 是否 为空
  4. 杜绝 内存 越界访问

5、安全函数

5.1 核心概念

C 语言传统字符串/IO 函数无长度限制,极易引发缓冲区溢出(程序崩溃、安全漏洞)。
安全函数 = 带长度限制的改良函数,强制指定最大操作长度,是 Linux 工业开发、企业项目强制使用 的规范。

5.2 传统函数 VS 安全函数(开发必背)

不安全函数 安全替代函数 核心区别
strcpy strncpy 限制拷贝最大长度,杜绝越界
strcat strncat 限制拼接最大长度
sprintf snprintf 限制格式化写入长度
gets fgets 限制输入长度(gets 已被废弃)

5.3 常用安全函数 代码示例

1. 安全字符串拷贝 strncpy
#include <stdio.h>
#include <string.h>

int main() {
    char dest[10] = {0};  // 目标缓冲区
    char src[] = "hello_world_123456";  // 长字符串

    // 安全拷贝:最多拷贝 9 字节,留1位存\0,绝对不越界
    strncpy(dest, src, sizeof(dest)-1);
    printf("安全拷贝结果:%s\n", dest);
    return 0;
}
2. 安全字符串拼接 strncat
#include <stdio.h>
#include <string.h>

int main() {
    char str[20] = "hello ";
    char add[] = "linux_c_123456789";

    // 限制拼接长度,保护缓冲区
    strncat(str, add, sizeof(str)-strlen(str)-1);
    printf("安全拼接结果:%s\n", str);
    return 0;
}
3. 安全格式化写入 snprintf
#include <stdio.h>

int main() {
    char buf[15];
    // 限制写入长度,自动截断,安全无溢出
    snprintf(buf, sizeof(buf), "num=%d, str=%s", 100, "test_abcdefg");
    printf("安全格式化:%s\n", buf);
    return 0;
}
4. 安全字符串输入 fgets
#include <stdio.h>

int main() {
    char buf[10];
    // 限制最多读9个字符,彻底解决输入溢出
    fgets(buf, sizeof(buf), stdin);
    printf("你输入的内容:%s\n", buf);
    return 0;
}

5.4 安全函数 核心规则(开发必守)

  1. 所有字符串操作必须用带 n 的安全函数
  2. 长度参数填缓冲区总大小 - 1(预留 \0 位置)
  3. 禁止使用 getsstrcpysprintf 等高危函数

5.5 两套 标准

C 语言安全函数分为两套完全独立的标准语法、平台、用法都不同

第一套:POSIX n 系列(Linux / GCC 通用 · 你必须掌握)

基础信息:

  • 标准:POSIX 标准 + C99 标准(Linux 官方标准)
  • 适用平台Linux、嵌入式、macOS、GCC/Clang 编译器
  • 命名规则:在旧函数末尾加 n
    • strcpystrncpy
    • strcatstrncat
  • 核心设计:限制最大操作长度,超长自动截断,保证不内存溢出
  • 工业地位Linux 开发 100% 使用这套,是全球通用事实标准

常用函数对照表:

危险函数 安全函数(n 系列) 功能
strcpy strncpy 安全拷贝字符串
strcat strncat 安全拼接字符串
sprintf snprintf 安全格式化字符串
gets fgets 安全读取输入

核心特点:

  1. 多一个参数:最大操作长度
  2. 超长内容自动截断,程序不会崩溃
  3. 不需要额外宏定义,直接包含头文件就能用
  4. Linux 系统、内核、服务端全用这套
第二套:C11 Annex K _s 后缀系列(微软 VS 专用 · 教程里的版本)

基础信息:

  • 标准:C11 标准附录 K(可选标准,不是强制标准
  • 适用平台Windows + Visual Studio(VS)
  • 命名规则:函数末尾加 _s
    • strcpystrcpy_s
    • strcatstrcat_s
  • 核心设计:传入缓冲区总大小,空间不足直接报错、不执行
  • 工业地位Linux 几乎不用,仅微软系项目使用

常用函数对照表:

危险函数 安全函数(_s 系列) 功能
strcpy strcpy_s 安全拷贝字符串
strcat strcat_s 安全拼接字符串
sprintf sprintf_s 安全格式化
gets gets_s 安全输入

核心特点:

  1. 多两个参数:目标缓冲区大小 + 源数据
  2. 空间不足直接返回错误,不写入数据
  3. Linux 默认不支持,GCC 必须加特殊宏才能用
  4. 仅微软 VS 原生支持
两套安全函数 · 终极对比表(最关键)
对比项 POSIX n 系列(我讲的) C11 _s 系列(菜鸟教程)
标准 POSIX / C99 C11 附录 K(可选)
支持平台 Linux、嵌入式、GCC Windows、VS
命名 strncpy / strncat strcpy_s / strcat_s
超长处理 自动截断,程序继续 直接报错,不执行
Linux 支持 ✅ 原生完美支持 ❌ 默认不支持
VS 支持 ✅ 支持 ✅ 原生支持
你的学习场景 ✔✔✔ 必须掌握 了解即可

6、文件操作

6.1 核心概念

C 语言通过 标准文件流(FILE* 操作文件,跨平台、简单稳定,是 Linux 开发处理文本/二进制文件的标准方案
所有文件操作遵循:打开 → 读写 → 关闭

6.2 文件打开模式(重中之重)

模式 含义 注意事项
r 只读 文件必须存在,否则打开失败
w 只写 文件不存在则创建,存在则清空内容
a 追加 文件不存在则创建,写入内容追加到末尾
r+ 读写 文件必须存在
w+ 读写 创建/清空文件
a+ 读写 追加模式,支持读取

6.3 核心操作函数

  1. fopen:打开文件,返回 FILE* 文件指针
  2. fclose:关闭文件(必须执行,防止数据丢失
  3. fwrite:二进制/字符串写入文件
  4. fread:从文件读取数据
  5. fputs:写入字符串
  6. fgets:读取一行字符串
  7. feof:判断文件是否读取完毕

6.4 完整实战代码(工业级标准写法)

#include <stdio.h>
#include <string.h>

int main() {
    // ==================== 1. 写入文件 ====================
    FILE *fp = fopen("test.txt", "w");  // 只写模式打开
    if (fp == NULL) {  // 必须判断打开是否成功
        perror("文件打开失败");  // 打印错误原因
        return 1;
    }
    // 写入字符串
    fputs("Hello Linux C\n", fp);
    fputs("文件操作演示\n", fp);
    fclose(fp);  // 关闭文件

    // ==================== 2. 读取文件 ====================
    fp = fopen("test.txt", "r");
    char buf[100];
    printf("===== 文件内容 =====\n");
    // 循环读取每一行
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("%s", buf);
    }
    fclose(fp);

    // ==================== 3. 追加写入 ====================
    fp = fopen("test.txt", "a");
    fputs("追加的内容\n", fp);
    fclose(fp);

    printf("\n文件操作完成!\n");
    return 0;
}

6.5 二进制文件读写(开发常用)

#include <stdio.h>

int main() {
    // 写入二进制数据
    int num = 666;
    FILE *fp = fopen("data.bin", "wb");  // wb=二进制写
    fwrite(&num, sizeof(int), 1, fp);   // 写入int数据
    fclose(fp);

    // 读取二进制数据
    int read_num;
    fp = fopen("data.bin", "rb");  // rb=二进制读
    fread(&read_num, sizeof(int), 1, fp);
    printf("读取二进制数据:%d\n", read_num);
    fclose(fp);
    return 0;
}

6.6 fclose & free 区别

fclosefree 是两个完全不同用途的函数,我来详细对比一下:

对比维度 fclose free
作用对象 文件流 (FILE*) 动态内存 (堆内存)
所属库 标准I/O库 (<stdio.h>) 动态内存管理库 (<stdlib.h>)
主要功能 关闭文件,刷新缓冲区 释放内存,归还给堆
必须配对 fopen 配对 malloc/calloc/realloc 配对

fclose 做了什么:

  • 将缓冲区中未写入的数据刷新到磁盘
  • 释放文件流相关的资源(文件描述符、缓冲区等)
  • 断开程序与文件的连接
  • 之后 fp 变成悬空指针(建议置为 NULL

free 做了什么:

  • 将堆内存标记为可用(归还给堆管理器)
  • 不会清空或修改这块内存的内容
  • 不会自动将指针置为 NULL
  • 不会调用任何"析构"函数(C语言没有)

关键区别示例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    // ===== fclose 示例 =====
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    
    fprintf(fp, "Hello World");
    // 此时数据可能还在内存缓冲区,未写入磁盘
    
    fclose(fp);  // 1. 刷新缓冲区(写入磁盘)
                 // 2. 关闭文件
                 // 3. fp 现在无效
    
    // fclose 之后不能再用 fp 操作文件
    // fprintf(fp, "Again");  // ❌ 错误!未定义行为
    
    
    // ===== free 示例 =====
    int *arr = (int *)malloc(100 * sizeof(int));
    if (arr == NULL) {
        return 1;
    }
    
    arr[0] = 42;
    free(arr);     // 1. 释放内存
                   // 2. arr 变成悬空指针
    
    // free 之后的内存不能再使用
    // arr[0] = 100;  // ❌ 错误!未定义行为
    
    return 0;
}

"配对"函数不同:

配对关系 申请/打开 释放/关闭
文件操作 fopen / freopen fclose
内存操作 malloc / calloc / realloc free

命名规律:

  • f 前缀 → file 相关(fopen, fclose, fprintf, fscanf
  • 没有特定前缀 → 内存管理(malloc, free

常见错误示范:

// ❌ 错误1:用 free 关闭文件
FILE *fp = fopen("test.txt", "r");
free(fp);  // 严重错误!破坏了文件流内部结构

// ❌ 错误2:用 fclose 释放内存
int *p = malloc(sizeof(int));
fclose(p);  // 错误!参数类型不匹配,编译警告/运行崩溃

// ❌ 错误3:忘记配对
FILE *fp = fopen("test.txt", "r");
// 忘记 fclose(fp);  // 内存泄漏 + 文件句柄泄漏

// ❌ 错误4:重复释放
free(p);
free(p);  // 未定义行为(可能崩溃)

// ❌ 错误5:对 NULL 调用 free(这个其实是安全的,但不建议依赖)
free(NULL);  // 安全,什么也不做(标准规定)

实际开发建议:

// 良好的资源管理模式
int main() {
    FILE *fp = NULL;
    int *buffer = NULL;
    
    // 1. 打开资源
    fp = fopen("data.txt", "r");
    if (fp == NULL) goto cleanup;
    
    buffer = (int *)malloc(1024);
    if (buffer == NULL) goto cleanup;
    
    // 2. 业务逻辑
    // ...
    
cleanup:
    // 3. 逆序释放(后打开的先释放)
    if (buffer != NULL) free(buffer);
    if (fp != NULL) fclose(fp);
    
    return 0;
}

记住: fclose 管文件,free 管内存,井水不犯河水!


6.7 文件操作 必避坑(致命错误)

  1. ❌ 打开文件后不判断是否成功(文件不存在/权限不足会崩溃)
  2. ❌ 操作完成后不关闭文件(数据丢失、文件占用)
  3. ❌ 用 w 模式打开已有文件(直接清空所有内容
  4. ❌ 读取时不判断文件结束(死循环)

7、错误处理

C 语言没有面向对象语言的 try-catch 异常机制,工业级(Linux)开发依靠 全局错误码 + 错误提示函数 处理程序运行错误,是所有系统调用、文件操作、内存管理的必备规范

7.1 核心原理

  1. errno 全局变量
    系统头文件 <errno.h> 定义的 全局错误码,函数执行失败时,会自动给 errno 赋值(不同数字代表不同错误)。
  2. 两个核心 错误处理 函数
    • perror():直接打印 自定义提示 + 后跟一个冒号、一个空格 + 系统原生错误信息(最简单、最常用)。
    • strerror():将 errno 错误码 转换 为 文字描述(灵活定制),返回一个指针,指针指向当前 errno 值的文本表示形式。

7.2 必备头文件

#include <stdio.h>   // perror
#include <errno.h>   // errno 错误码
#include <string.h>  // strerror

7.3 代码示例(Linux 开发标准错误处理)

  • 覆盖文件操作、内存申请两个最高发错误场景:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    // ============== 场景1:打开不存在的文件(必报错) ==============
    FILE *fp = fopen("不存在的文件.txt", "r");
    if (fp == NULL) {
        // 方式1:perror 直接打印错误(推荐!)
        perror("文件打开失败");
        // 方式2:strerror + errno 手动打印
        printf("错误码:%d,错误描述:%s\n", errno, strerror(errno));
        return 1; // 出错退出程序
    }

    // ============== 场景2:内存申请失败 ==============
    // 申请极大内存,模拟失败
    int *p = (int *)malloc(1024 * 1024 * 1024 * 10LL);
    if (p == NULL) {
        perror("内存申请失败");
        return 1;
    }

    // 正常流程
    fclose(fp);
    free(p);
    return 0;
}

运行结果

文件打开失败: No such file or directory
错误码:2,错误描述:No such file or directory
  • 被零除的错误
    在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。
    为了避免这种情况发生,下面的代码在进行除法运算前会先检查除数是否为零:
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   int dividend = 20;
   int divisor = 0;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(-1);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为 : %d\n", quotient );
 
   exit(0);
}

7.4 开发必背规则

  1. 函数返回值必判断fopen/malloc 返回 NULL 代表失败)。
  2. 失败后优先用 perror,一键打印系统错误,无需记忆码值。
  3. 错误处理后及时退出/回收资源,避免程序乱执行。

8、未定义行为

8.1 通俗定义

未定义行为(Undefined Behavior,简称 UB)
代码语法完全合法,能编译通过,但运行结果完全不可预测
编译器不做任何保证,可能:正常运行、打印乱码、程序崩溃、死机。

一句话:语法没错,逻辑作死,结果看运气

8.2 最常见的 未定义行为

1. 数组越界访问(最高发)

访问超出数组长度的元素,读写非法内存:

int arr[3] = {1,2,3};
// 错误:数组只有 0/1/2,访问 3 越界 → UB
printf("%d", arr[3]); 
2. 空指针 / 野指针解引用

对无效指针取值,直接导致程序崩溃:

// 空指针
int *p = NULL;
// 错误:解引用 NULL → UB
printf("%d", *p);
3. 整数除零错误

数学上不允许,计算机也无法处理:

int a = 10;
int b = 0;
// 错误:除零 → UB
printf("%d", a / b);
4. 有符号整数溢出

int 超出最大值,无法存储:

// int 最大值约 21亿
int a = 2147483647;
// 错误:溢出 → UB
a = a + 1;
5. 位移操作数太大

当执行位移操作时,位移的位数大于或等于操作数的位数时,结果是未定义的。例如:

int x = 1;
int y = x << 32; // 位移操作数太大,结果未定义
6. 使用未初始化的变量

变量没赋值,内存是随机垃圾值:

int a;
// 错误:a 未初始化 → UB
printf("%d", a);
7. 错误的类型转换

当我们进行不安全的类型转换时,结果是未定义的。例如:

int *ptr = (int *)malloc(sizeof(int));
float *fptr = (float *)ptr; // 错误的类型转换,结果未定义
8. 内存越界

当我们向已经释放或未分配的内存写入数据时,结果是未定义的。例如:

int *ptr = (int *)malloc(sizeof(int));
free(ptr);
*ptr = 10; // 内存越界,结果未定义
9. 未定义的浮点数行为

比如比较两个 NaN(非数字)值是否相等,这是未定义的行为。例如:

float x = sqrt(-1);
float y = sqrt(-1);
if (x == y) {
    printf("NaN values are equal\n");
}

8.3 未定义行为的后果

  1. 偶尔正常,偶尔崩溃(最难调试);
  2. 数据错乱、业务逻辑失效;
  3. 服务器程序直接宕机;
  4. 嵌入式设备死机、重启。

8.4 如何避免未定义行为(开发铁律)

  1. 数组访问必检查索引,不越界;
  2. 指针使用前必判断非空
  3. 除法运算前必判断除数不为 0
  4. 变量先赋值,后使用
  5. 整数运算避免超出类型范围

9、项目工程结构

C 项目工程结构是指组织 C 语言项目源代码、头文件、构建脚本和资源文件的目录布局方式。

一套约定俗成的目录结构,能帮你和团队成员快速理解项目全貌,也让自动化构建工具如 Make、CMake 能够高效工作。

9.1 规范目录的作用

  1. 小型代码单文件即可,项目代码变多后,全部堆在一起会出现:头文件冲突、代码难找、编译杂乱、多人协作困难。
  2. 标准化目录:代码分层清晰、方便编译工具(Make/CMake)自动化构建、便于版本管理与团队开发。

9.2 通用标准项目目录(Linux C中小型项目规范)

my_demo/
├── src/        // 源码实现(.c)
├── include/    // 头文件(.h)
├── tests/      // 单元测试代码
├── lib/        // 第三方静态库.a、动态库.so
├── build/      // 编译生成.o、可执行程序(自动生成)
├── doc/        // 项目说明文档
├── tools/      // 辅助脚本、工具
├── Makefile    // Make构建脚本
├── CMakeLists.txt // CMake构建文件(可选)
├── README.md   // 项目说明
└── .gitignore  // git忽略配置
目录 存储内容 是否提交git
src .c源码实现文件 ✅ 是
include .h头文件,函数/结构体/宏声明 ✅ 是
tests 模块测试代码 ✅ 是
lib 第三方预编译库 按需
build .o目标文件、可执行程序 ❌ 否
doc 设计文档、接口说明 ✅ 是

9.3 各目录详解 + 实战代码示例

9.3.1 include:头文件目录(只声明,无函数实现)

路径:include/math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 结构体声明
typedef struct {
    int x;
    int y;
} Point;

// 函数声明
double distance(Point a, Point b);
int is_prime(int n);

#endif

规范:必须加头文件保护宏,禁止在.h写函数实现。

9.3.2 src:源码实现目录(.c写具体逻辑)
  1. src/math_utils.c(对应头文件实现)
#include "math_utils.h"
#include <math.h>

// 计算两点距离
double distance(Point a, Point b)
{
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    return sqrt(dx*dx + dy*dy);
}

// 判断质数
int is_prime(int n)
{
    if(n < 2) return 0;
    for(int i = 2; i*i <= n; i++){
        if(n%i == 0) return 0;
    }
    return 1;
}
  1. src/main.c 程序入口
#include <stdio.h>
#include "math_utils.h"

int main(void)
{
    Point p1 = {0,0};
    Point p2 = {3,4};
    printf("两点距离:%.2f\n", distance(p1,p2));
    printf("7是否质数:%s\n", is_prime(7)?"是":"否");
    return 0;
}
9.3.3 tests:测试目录

新建tests/test_math.c,单独测试模块功能。

9.3.4 build:编译产物目录

所有编译产生的中间文件、可执行程序统一放这里,不手写代码。

9.4 C程序 编译 四大阶段(预处理→编译→汇编→链接)

单个文件分步编译命令,理解编译全过程:

  1. 预处理(-E):展开#include#define,生成.i
gcc -E src/main.c -I./include -o build/main.i
  1. 编译(-S):C代码转汇编,生成.s
gcc -S build/main.i -o build/main.s
  1. 汇编(-c):汇编转目标文件.o
gcc -c build/main.s -o build/main.o
  1. 链接:多个.o+系统库 → 最终可执行文件
gcc build/main.o build/math_utils.o -o build/app -lm

-I./include:指定头文件搜索路径;-lm链接数学库math.h

  1. 一条命令
    上面 分步执行是为了教学和理解原理,不是实际开发的做法。
# 直接生成可执行文件(最常用)
gcc src/main.c src/math_utils.c -I./include -o build/app -lm

9.5 两种自动化构建方案

工具篇有详细说明,这里先根据例子理解。

9.5.1 Makefile(中小型项目首选,根目录)

文件名:Makefile

CC = gcc
CFLAGS = -Wall -g -O2
INC = -I./include
TARGET = build/app

SRCS = $(wildcard src/*.c)
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))

# 链接生成程序
$(TARGET):$(OBJS)
	$(CC) $(OBJS) -o $(TARGET) -lm

# .c编译成.o
build/%.o:src/%.c
	$(CC) $(CFLAGS) $(INC) -c $< -o $@

# 清理编译产物
.PHONY:clean
clean:
	rm -rf build/*.o build/app

使用命令:

make       # 编译生成程序
make clean # 清空编译产物
./build/app # 运行
9.5.2 CMake(跨平台、大中型项目)

根目录CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(demo C)
set(CMAKE_C_STANDARD 11)

include_directories(include)
file(GLOB SRC_LIST src/*.c)

add_executable(app ${SRC_LIST})
target_link_libraries(app m)

编译步骤:

mkdir build && cd build
cmake ..
make
./app

9.6 .gitignore配置(过滤不需要提交的文件)

# 编译产物
build/
*.o
*.out
# IDE配置
.vscode/
.idea/
# 系统垃圾文件
.DS_Store

9.7 开发高频规范&避坑

  1. 头文件包含区分
    • <xxx.h>:系统标准头文件(stdio.h/stdlib.h)
    • "xxx.h":项目自定义头文件
  2. ❌ 禁止#include "xxx.c",会造成函数重复定义,所有.c统一参与编译
  3. undefined reference报错:头文件有声明,但对应.c没加入编译
  4. 库区别
    • .a静态库:链接打包进程序,程序独立,体积大
    • .so动态库:运行时加载,体积小,运行环境必须存在对应库

9.8 项目快速创建shell命令

mkdir -p my_demo/{src,include,tests,build,doc,lib,tools}
cd my_demo

10、动、静 态库

10.1 先搞懂:什么是库文件?(超级通俗比喻)

你写了一段通用代码(比如数学计算、工具函数),不想把源码给别人,也不想每次都重复编译。
库 = 把编译好的 .c 源码打包成一个二进制文件
别人拿到这个文件,不用看源码,直接调用里面的函数,这就是库。

核心作用:代码复用、保护源码、模块化开发
Linux 下后缀

  • 静态库:.a
  • 动态库:.so

10.2 两个库的核心区别(一句话记住)

  1. 静态库(.a):编译时直接把库打包进可执行程序
    → 程序体积大,运行不需要带库文件
  2. 动态库(.so):编译时只记录引用,运行时再加载库
    → 程序体积小,运行必须带库文件

10.3、静态库(Static Library).a

核心特点

  1. 编译时把库代码焊死集成到程序里
  2. 运行时不需要库文件,程序独立运行
  3. 缺点:程序体积大,多个程序用同一个库,会重复占用内存
制作 + 使用 完整步骤(基于你的项目结构)

项目结构:

my_demo/
├── include/      # 头文件
├── src/          # 源码
├── lib/          # 存放库文件
├── build/        # 编译产物
└── main.c        # 测试程序
1. 准备源码(复用之前的工具函数)

include/math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int sub(int a, int b);
#endif

src/math_utils.c

#include "math_utils.h"
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
2. 制作静态库(2 行命令)
# 1. 编译源码为 目标文件 .o(不链接)
gcc -c src/math_utils.c -I include -o build/math_utils.o

# 2. 打包为静态库 libmath.a (固定命名:lib+库名.a)
ar rcs lib/libmath.a build/math_utils.o
  • ar:静态库打包工具
  • rcs:固定参数,创建静态库
3. 使用静态库

main.c

#include <stdio.h>
#include "math_utils.h"

int main() {
    printf("10+5=%d\n", add(10,5));
    printf("10-5=%d\n", sub(10,5));
    return 0;
}

编译命令(链接静态库):

gcc main.c -I include -L lib -l math -o build/app_static
  • -L lib:指定库文件在 lib 目录
  • -l math:指定库名 math(自动匹配 libmath.a
4. 运行
./build/app_static

运行成功!即使删除 libmath.a,程序依然能跑


10.4、动态库(Shared Library).so

核心特点:

  1. 编译时不打包,只标记“我需要这个库”
  2. 运行时动态加载库文件
  3. 优点:程序体积小,多个程序共享一个库
  4. 缺点:运行必须带库文件,否则报错
制作 + 使用 完整步骤
1. 制作动态库(2 行命令)
# 1. 编译为位置无关目标文件(-fPIC 动态库必备)
gcc -c -fPIC src/math_utils.c -I include -o build/math_utils.o

# 2. 生成动态库 libmath.so
gcc -shared build/math_utils.o -o lib/libmath.so
2. 使用动态库(编译命令和静态库一样)
gcc main.c -I include -L lib -l math -o build/app_dynamic
3. 运行(关键:必须告诉系统库的位置)
# 临时指定库路径(Linux 必须)
export LD_LIBRARY_PATH=./lib
# 运行
./build/app_dynamic

✅ 运行成功!删除 libmath.so 程序立刻报错


10.5 静态库 VS 动态库 终极对比

特性 静态库 .a 动态库 .so
打包时机 编译时 打包进程序 运行时 加载
程序体积
运行依赖 不需要库文件 必须带库文件
内存占用 多个程序重复占用 共享库,省内存
更新库 必须重新编译程序 直接替换库,不用重编译
使用场景 小工具、不想带依赖 大型项目、系统服务

【五】工具篇

编译 C 程序:

  • 小项目 直接用 gcc
  • 中等项目 用 Makefile
  • 大型项目 用 CMake

1、Makefile

你可以把 Makefile 理解成:
一个写给电脑看的「自动化施工说明书」

你写 C 语言项目,代码分散在 srcinclude 里,
手动编译要敲一长串 gcc 命令,又麻烦又容易错。

Makefile 就是:
所有编译命令、清理命令提前写好,
以后你只需要输一个命令:

make

电脑就自动帮你把整个项目编译好,生成可执行程序。

他可以:

  1. 一键编译整个项目(不用手敲几十字的 gcc 命令)
  2. 自动管理文件依赖(改了一个文件,只重新编译改了的部分,超快)
  3. 一键清理垃圾文件.o 文件、可执行程序)
  4. 多人协作统一编译方式(不会出现“我这能跑,你那跑不了”)

一句话:
Makefile = C 项目的自动化工具,解放双手,不敲重复命令


1、Makefile 怎么安装?

Linux 系统(Ubuntu/Centos)默认 100% 自带!
不用安装!直接用!


极少数情况:没安装(输入 make 提示找不到命令)

执行一行命令安装即可:

Ubuntu/Debian 系统(你用的)
sudo apt update
sudo apt install make
Centos 系统
sudo yum install make

2、最简单的 Makefile 怎么写?

我按照你之前的项目结构,写一个最简单、能直接运行的 Makefile,
逐行给你大白话翻译!

项目结构

my_demo/
├── src/        # 所有 .c 代码
├── include/    # 所有 .h 头文件
├── build/      # 存放编译结果
└── Makefile    # 就在根目录创建这个文件!
第一步:创建 Makefile 文件

在项目根目录 my_demo/ 下,新建一个文件,名字必须叫 Makefile
(大小写严格,不能错,没有后缀)

第二步:复制下面的极简模板(完全够用)
# ==================== 大白话版 Makefile ====================
# 1. 编译器:用 gcc
CC = gcc

# 2. 编译参数:-Wall 显示警告,-Iinclude 告诉电脑头文件在哪
CFLAGS = -Wall -I./include

# 3. 最终生成的程序,放在 build 文件夹里
TARGET = build/app

# 4. 找到 src 里所有的 .c 文件
SRCS = src/*.c

# ================ 编译命令 ================
all:
	$(CC) $(SRCS) -o $(TARGET) $(CFLAGS)

# ================ 清理命令 ================
clean:
	rm -f $(TARGET)
逐行大白话翻译
代码 大白话意思
CC = gcc 用 gcc 编译器
CFLAGS = -Wall -I./include 开启警告 + 告诉编译器头文件在 include 文件夹
TARGET = build/app 最终程序叫 app,放在 build 里
SRCS = src/*.c 要编译 src 里所有的 C 代码
all: 执行 make 时运行的命令
$(CC) ... 自动执行 gcc 编译整个项目
clean: 执行 make clean 时运行的命令
rm -f $(TARGET) 删除编译好的程序,清理垃圾

3、怎么使用 Makefile?(3 步搞定)

第 1 步:进入项目根目录
cd my_demo
第 2 步:一键编译项目
make

✅ 运行后,自动在 build/ 文件夹生成可执行程序 app

第 3 步:运行程序
./build/app
额外:一键清理编译产物
make clean

✅ 自动删除 build/app,恢复干净状态


2、CMake

  • Makefile = 你手写的「Linux 专用编译说明书」
    优点:简单;缺点:只能在 Linux 用、大型项目写起来巨麻烦、换系统就要重写。

  • CMake = 自动生成 Makefile 的智能工具
    你写一份简单的 CMakeLists.txt 配置,
    CMake 会自动根据你的系统(Linux/Windows/macOS)生成对应的编译文件(Linux 生成 Makefile,Windows 生成 VS 工程)。

一句话总结
CMake 是跨平台的「编译自动生成器」,写一次配置,全系统通用。


1、CMake 有什么用?

  1. 跨平台(最牛的点)
    一份配置,Linux、Windows、Mac 都能编译,不用改代码。
  2. 比 Makefile 简单10倍
    不用写复杂的编译规则,几行代码就能搞定大型项目。
  3. 大型项目必备
    所有大厂、开源项目(Linux 内核、Redis、Nginx 周边)全用 CMake。
  4. 自动管理依赖
    自动找库、自动链接、自动处理头文件路径。

2、CMake 怎么安装?

Linux 系统直接一行命令安装(Ubuntu/Debian):

sudo apt update
sudo apt install cmake

安装完查看版本:

cmake --version

3、CMake 的配置文件叫什么?

和 Makefile 必须叫 Makefile 一样,
CMake 的配置文件必须叫:CMakeLists.txt
(放在项目根目录,大小写固定)


4、极简 CMake 模板(适配你的项目)

你的项目结构

my_demo/
├── src/        # 所有 .c 代码
├── include/    # 所有 .h 头文件
├── build/      # 编译目录
└── CMakeLists.txt  # 根目录创建这个文件

直接复制这个模板(零基础全能用)

# 1. CMake 最低版本要求(不用改)
cmake_minimum_required(VERSION 3.10)

# 2. 项目名字(随便起,叫demo也行)
project(my_c_project C)

# 3. 告诉CMake:头文件在 include 文件夹
include_directories(include)

# 4. 自动找到 src 里所有 .c 代码
file(GLOB SRC_LIST src/*.c)

# 5. 生成可执行程序 app,编译所有源文件
add_executable(app ${SRC_LIST})

逐行大白话翻译

代码 大白话意思
cmake_minimum_required 规定CMake版本,照抄就行
project 给项目起个名字
include_directories(include) 头文件在include文件夹,别找不到
file(GLOB SRC_LIST src/*.c) 把src里所有.c文件都拿来编译
add_executable(app ...) 最终生成叫app的可执行程序

5、CMake 怎么使用?(固定 3 步,永远不变)

第1步:进入项目根目录
cd my_demo
第2步:进入 build 文件夹(必须在build里执行)
mkdir -p build  # 没有就创建
cd build
第3步:让CMake生成 Makefile
cmake ..

✅ 执行完,build 文件夹里自动生成 Makefile

第4步:一键编译
make
第5步:运行程序
./app

6、清理编译产物(超简单)

直接删除 build 文件夹即可:

cd ..
rm -rf build
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐