【一】概述

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),降低开发成本

【二】基本语法

前要

编译执行

维度GCCClang
出身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 语言中,如果 变量 没有显式 初始化,那么它的 默认值 将取决于该 变量 的 类型 和 其所在的 作用域。
整型变量(int、short、long等):默认值为0。

浮点型变量(float、double等):默认值为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)局部变量、函数参数系统自动管理(自动创建/销毁)auto、register
静态数据区 (Static/Data)全局变量、static 静态变量程序启动分配,程序退出才释放static、extern
堆区 (Heap)动态申请的内存(malloc/calloc)手动管理(自己申请、自己释放)无存储类对应
代码区程序二进制指令只读,程序全程存在-
内存区域是否在内存条上说明
代码区✅ 是程序指令,加载到内存
静态数据区✅ 是全局/静态变量,全程在内存
栈区 (Stack)✅ 是局部变量,高频使用,在内存
堆区 (Heap)✅ 是malloc 动态内存,在内存

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

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

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

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

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

C 语言共有 4 种标准存储类:auto、register、static、extern。


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栈区代码块内局部默认局部变量
registerCPU寄存器代码块内局部高频性能变量
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_t、NULL、偏移量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

malloc 和 calloc 两者都是申请堆内存,作用完全一致,唯一不同只有 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 安全函数(开发必背)

不安全函数安全替代函数核心区别
strcpystrncpy限制拷贝最大长度,杜绝越界
strcatstrncat限制拼接最大长度
sprintfsnprintf限制格式化写入长度
getsfgets限制输入长度(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. 禁止使用 gets、strcpy、sprintf 等高危函数

5.5 两套 标准

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

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

基础信息:

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

常用函数对照表:

危险函数安全函数(n 系列)功能
strcpystrncpy安全拷贝字符串
strcatstrncat安全拼接字符串
sprintfsnprintf安全格式化字符串
getsfgets安全读取输入

核心特点:

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

基础信息:

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

常用函数对照表:

危险函数安全函数(_s 系列)功能
strcpystrcpy_s安全拷贝字符串
strcatstrcat_s安全拼接字符串
sprintfsprintf_s安全格式化
getsgets_s安全输入

核心特点:

  1. 多两个参数:目标缓冲区大小 + 源数据
  2. 空间不足直接返回错误,不写入数据
  3. Linux 默认不支持,GCC 必须加特殊宏才能用
  4. 仅微软 VS 原生支持
两套安全函数 · 终极对比表(最关键)
对比项POSIX n 系列(我讲的)C11 _s 系列(菜鸟教程)
标准POSIX / C99C11 附录 K(可选)
支持平台Linux、嵌入式、GCCWindows、VS
命名strncpy / strncatstrcpy_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 区别

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

对比维度fclosefree
作用对象文件流 (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 / freopenfclose
内存操作malloc / calloc / reallocfree

命名规律:

  • 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 语言项目,代码分散在 src、include 里,
手动编译要敲一长串 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 幸运抽奖

更多推荐