写给三弟的 C语言编程 快速入门
【一】概述
1、C语言的诞生背景
C 诞生于1972 年美国贝尔实验室,设计者:丹尼斯・里奇 (Dennis Ritchie),肯・汤普森 (Ken Thompson) 主导前期铺垫,诞生核心驱动力是UNIX 操作系统开发需求。
1973 年 的 里程碑:团队使用 C 完整重写 UNIX 内核,操作系统彻底脱离汇编,实现跨硬件移植,C 随 UNIX 快速在科研圈、小型机领域普及。
2、主要的开发场景
由于 C 无GC、性能高、贴近硬件,高性能刚需场景必备,主要有一下 开发场景
- 中间件&服务底座:Nginx、自研网关、Redis等核心底层,字节Tengine、云厂商负载均衡。
- 数据库内核:TDSQL、PolarDB等存储引擎、底层IO模块。
- 音视频:抖音/快手/微信音视频编解码、直播转码、剪映底层算法。
- AI底层:飞桨、大模型推理算子、加速库。
- 网络安全:DDoS防护、WAF、抓包分析、DPDK高性能网卡转发。
- 游戏:游戏引擎底层、服务器核心、反作弊。
- 嵌入式/IoT/车载:智能家居固件、车载控制系统、边缘网关。
- 系统内核:定制Linux内核、云服务器虚拟化组件。
3、优缺点
优点
- 性能极致:运行速度接近汇编,无垃圾回收(GC)开销,资源占用极低
- 底层操控:可直接操作内存、指针、硬件,适合写系统、驱动、底层组件
- 通用性强:跨平台编译,生态成熟,是C++/Java/Go等语言的基础
- 体积小巧:编译后文件小,适配嵌入式、物联网等资源受限设备
缺点
- 内存风险高:手动申请/释放内存,极易出现泄漏、越界、崩溃
- 开发效率低:无面向对象、异常处理等高级特性,代码量大
- 安全性差:指针操作无严格校验,容易产生难以调试的bug
- 标准库简陋:复杂功能(网络、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 系统开发首选)
- Linux 内核开发(官方传统编译器,生态最成熟)
- 编译老旧开源项目、服务器底层组件
- 全平台兼容需求(x86/ARM/RISC-V 等所有硬件)
- 对编译兼容性要求极高的系统级开发
2. Clang 适用场景(学习/现代开发首选)
- C 语言学习、日常开发(报错友好,效率拉满)
- 代码静态检查、格式化、调试优化
- macOS/iOS 开发(默认编译器)
- 现代 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;
核心区别:
-
本质
#define:预处理宏,编译前做纯文本替换,无语法语义;const:只读变量修饰符,编译期处理,是合法变量。 -
类型安全
#define:无类型检查,易引发隐式错误;const:强类型检查,Linux 开发优先用(安全、规范)。 -
作用域
#define:全局生效,无法限制作用域;const:遵循变量作用域(函数/代码块/文件级)。 -
内存与调试
#define:不分配内存,GDB 无法调试;const:分配内存(数据段/栈),支持调试。 -
核心用途
#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 作用
- 复用代码,避免重复编写
- 拆分复杂逻辑,让代码更清晰
- 支持模块化开发(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
va_list:定义参数列表变量va_start:初始化参数列表va_arg:获取下一个参数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个词)
- 生命周期:变量从创建到销毁的时间
- 作用域:变量/函数能被访问的代码范围
- 存储位置:栈区、寄存器、静态数据区
6.2 四大 存储类 详解
6.2.1 auto(自动变量)
-
特点
- C 语言局部变量
默认存储类,可以省略不写 - 存储在
栈区,函数/代码块结束 自动销毁 - 作用域:仅当前代码块内有效
- 生命周期:随代码块执行创建/销毁
- C 语言局部变量
-
代码示例
#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;
}
- 使用场景
所有普通局部变量,无需手动写auto,编译器自动识别。
6.2.2 register(寄存器变量)
-
特点
- 存储在
CPU 寄存器中(速度最快,比内存快10倍+) - 高频使用的变量专用(Linux 底层循环、计数)
- 不能取地址(寄存器没有内存地址)
- 编译器可优化忽略(资源不足 时 直接降级,自动转为auto)
- 只能用 register 修饰 基本数据类型:
✅ 支持:int、char、float、指针
❌ 不支持:数组、结构体、联合体(太大 / 太复杂,寄存器存不下) - register 只是「礼貌建议」,不是「强制命令」。register ≠ 保证存入寄存器,只是你的一个愿望,编译器可以拒绝。
- 现代 GCC/Clang 编译器,完全不需要 手动写 register!
编译器会 自动 把 最频繁 使用的 变量 放进寄存器,比你手动指定更高效。
register 现在基本属于 废弃语法,仅在极底层的嵌入式 / 内核汇编里偶尔用到
- 存储在
-
代码示例
#include <stdio.h>
int main() {
// 定义寄存器变量:高频循环计数器
register int i;
for(i = 0; i < 5; i++) {
printf("register循环:%d\n", i);
}
// 错误用法:寄存器变量不能取地址
// printf("%p", &i);
return 0;
}
- 使用场景
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(外部变量/函数)
-
作用
- 声明外部定义的变量/函数,实现跨文件访问
- 仅声明,不分配内存
- 多文件编程、Linux 系统开发核心
-
多文件示例
- 文件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 总结
auto:局部变量默认,不用写register:高速寄存器变量,性能优化用static:静态持久化、文件私有,Linux 开发高频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 核心特性(⭐必背重点)
- 下标从 0 开始:第一个元素下标
0,最后一个下标长度-1 - 内存连续:所有元素在内存条上挨着存放
- 大小固定:定义后不能新增/删除元素
- 数组名 = 数组首地址(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(存
\0) - 打印字符串用
%s,不能用%c - 字符串 不能直接用
=赋值,必须用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 开发核心特性
- 本质是整型,底层和
int完全一样 - 一组相关常量,禁止随意赋值
- 提高可读性:用
OPENED代替数字1 - 适用于固定取值:状态、类型、错误码
5、共用体(了解即可,实际用得极少)
5.1 核心概念
共用体(union):是一种特殊的 数据类型,允许你在 相同的内存位置 存储 不同的 数据类型。
您可以定义一个带有多成员的共用体,但是 同一时刻 只能有一个成员带有值。
对应 你之前掌握的 Rust 的 联合体 的概念。
价值:
通过内存复用,压缩 / 优化程序的内存占用, 对于 小内存的设备(嵌入式 / 单片机开发, 设备内存极小,只有 几百字节~几 KB) 是很有用的。
5.2 定义语法
union 共用体名 {
成员1;
成员2;
};
5.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 成员访问运算符(开发必背)
- 普通变量:用
.访问成员 - 指针变量:用
->访问成员
// 普通变量访问
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 重点
- 动态数组 = 指针 + 长度 + 容量,缺一不可;
- 扩容用
realloc,自动拷贝旧数据,开发首选; - 必须手动释放内存,否则会造成内存泄漏;
- 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 开发必背重点
- C 原生无 Map,简易版用数组+结构体实现,适合学习;
- 工业级 Linux 开发:使用 glib 库的 GHashTable 哈希表(高性能、成熟);
- Key 必须唯一,查找效率是 Map 的核心;
- 同样需要手动管理内存。
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 文件包含指令
作用:将 头文件的内容,原封不动 复制到 当前文件中。
两种写法(开发必区分):
#include <xxx.h>:包含系统标准头文件(如 stdio.h、stdlib.h)#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.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 头文件核心作用
- 声明
函数、宏、结构体、枚举(巧记: 韩红姐妹) - 对外提供接口,隐藏实现细节
- 实现多文件代码复用
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 标准书写规范
- 只放声明,不放实现
函数声明、结构体声明、宏定义 → 放.h
函数实现、变量定义 → 放.c - 必须加头文件保护
- 不定义全局变量(极易引发重复定义错误)
- 文件名 和 功能对应(如
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 开发避坑
- ❌ 头文件中写函数实现
- ❌ 头文件中定义全局变量
- ❌ 忘记加头文件保护
- ❌ 循环包含头文件(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 点:
- 参数格式不同
- 是否自动把内存清零
自动清 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
总结:
malloc/calloc和 基本类型/复合类型 无关- 两者都能给
int、数组、结构体申请内存 - 选谁只看一件事:
- 需要内存自动清0 → 用
calloc - 不需要初始化、追求快 → 用
malloc
- 需要内存自动清0 → 用
4.5 开发中常见内存问题(致命坑)
- 内存泄漏
只申请malloc,不释放free,内存越用越少,最终程序崩溃 - 重复释放
对同一块内存调用两次free,程序直接崩溃 - 内存越界
访问超出申请范围的内存,破坏数据 - 野指针
内存释放后未置空,继续使用指针
4.6 内存管理 黄金法则
- 申请 与 释放
成对出现 - 释放 后 立即置空 指针
- 使用前 判断 指针 是否 为空
- 杜绝 内存 越界访问
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 安全函数 核心规则(开发必守)
- 所有字符串操作必须用带
n的安全函数 - 长度参数填缓冲区总大小 - 1(预留
\0位置) - 禁止使用
gets、strcpy、sprintf等高危函数
5.5 两套 标准
C 语言安全函数分为两套完全独立的标准,语法、平台、用法都不同。
第一套:POSIX n 系列(Linux / GCC 通用 · 你必须掌握)
基础信息:
- 标准:POSIX 标准 + C99 标准(Linux 官方标准)
- 适用平台:Linux、嵌入式、macOS、GCC/Clang 编译器
- 命名规则:在旧函数末尾加
nstrcpy→strncpystrcat→strncat
- 核心设计:限制最大操作长度,超长自动截断,保证不内存溢出
- 工业地位:Linux 开发 100% 使用这套,是全球通用事实标准
常用函数对照表:
| 危险函数 | 安全函数(n 系列) | 功能 |
|---|---|---|
strcpy |
strncpy |
安全拷贝字符串 |
strcat |
strncat |
安全拼接字符串 |
sprintf |
snprintf |
安全格式化字符串 |
gets |
fgets |
安全读取输入 |
核心特点:
- 多一个参数:最大操作长度
- 超长内容自动截断,程序不会崩溃
- 不需要额外宏定义,直接包含头文件就能用
- Linux 系统、内核、服务端全用这套
第二套:C11 Annex K _s 后缀系列(微软 VS 专用 · 教程里的版本)
基础信息:
- 标准:C11 标准附录 K(可选标准,不是强制标准)
- 适用平台:Windows + Visual Studio(VS)
- 命名规则:函数末尾加
_sstrcpy→strcpy_sstrcat→strcat_s
- 核心设计:传入缓冲区总大小,空间不足直接报错、不执行
- 工业地位:Linux 几乎不用,仅微软系项目使用
常用函数对照表:
| 危险函数 | 安全函数(_s 系列) | 功能 |
|---|---|---|
strcpy |
strcpy_s |
安全拷贝字符串 |
strcat |
strcat_s |
安全拼接字符串 |
sprintf |
sprintf_s |
安全格式化 |
gets |
gets_s |
安全输入 |
核心特点:
- 多两个参数:目标缓冲区大小 + 源数据
- 空间不足直接返回错误,不写入数据
- Linux 默认不支持,GCC 必须加特殊宏才能用
- 仅微软 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 核心操作函数
fopen:打开文件,返回FILE*文件指针fclose:关闭文件(必须执行,防止数据丢失)fwrite:二进制/字符串写入文件fread:从文件读取数据fputs:写入字符串fgets:读取一行字符串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 是两个完全不同用途的函数,我来详细对比一下:
| 对比维度 | 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 文件操作 必避坑(致命错误)
- ❌ 打开文件后不判断是否成功(文件不存在/权限不足会崩溃)
- ❌ 操作完成后不关闭文件(数据丢失、文件占用)
- ❌ 用
w模式打开已有文件(直接清空所有内容) - ❌ 读取时不判断文件结束(死循环)
7、错误处理
C 语言没有面向对象语言的 try-catch 异常机制,工业级(Linux)开发依靠 全局错误码 + 错误提示函数 处理程序运行错误,是所有系统调用、文件操作、内存管理的必备规范。
7.1 核心原理
errno全局变量
系统头文件<errno.h>定义的 全局错误码,函数执行失败时,会自动给errno赋值(不同数字代表不同错误)。- 两个核心 错误处理 函数
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 开发必背规则
- 函数返回值必判断(
fopen/malloc返回NULL代表失败)。 - 失败后优先用
perror,一键打印系统错误,无需记忆码值。 - 错误处理后及时退出/回收资源,避免程序乱执行。
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 未定义行为的后果
- 偶尔正常,偶尔崩溃(最难调试);
- 数据错乱、业务逻辑失效;
- 服务器程序直接宕机;
- 嵌入式设备死机、重启。
8.4 如何避免未定义行为(开发铁律)
- 数组访问必检查索引,不越界;
- 指针使用前必判断非空;
- 除法运算前必判断除数不为 0;
- 变量先赋值,后使用;
- 整数运算避免超出类型范围。
9、项目工程结构
C 项目工程结构是指组织 C 语言项目源代码、头文件、构建脚本和资源文件的目录布局方式。
一套约定俗成的目录结构,能帮你和团队成员快速理解项目全貌,也让自动化构建工具如 Make、CMake 能够高效工作。
9.1 规范目录的作用
- 小型代码单文件即可,项目代码变多后,全部堆在一起会出现:头文件冲突、代码难找、编译杂乱、多人协作困难。
- 标准化目录:代码分层清晰、方便编译工具(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写具体逻辑)
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;
}
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程序 编译 四大阶段(预处理→编译→汇编→链接)
单个文件分步编译命令,理解编译全过程:
- 预处理(-E):展开
#include、#define,生成.i
gcc -E src/main.c -I./include -o build/main.i
- 编译(-S):C代码转汇编,生成
.s
gcc -S build/main.i -o build/main.s
- 汇编(-c):汇编转目标文件
.o
gcc -c build/main.s -o build/main.o
- 链接:多个.o+系统库 → 最终可执行文件
gcc build/main.o build/math_utils.o -o build/app -lm
-I./include:指定头文件搜索路径;-lm链接数学库math.h
- 一条命令
上面 分步执行是为了教学和理解原理,不是实际开发的做法。
# 直接生成可执行文件(最常用)
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 开发高频规范&避坑
- 头文件包含区分
<xxx.h>:系统标准头文件(stdio.h/stdlib.h)"xxx.h":项目自定义头文件
- ❌ 禁止
#include "xxx.c",会造成函数重复定义,所有.c统一参与编译 undefined reference报错:头文件有声明,但对应.c没加入编译- 库区别
.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 两个库的核心区别(一句话记住)
- 静态库(.a):编译时直接把库打包进可执行程序
→ 程序体积大,运行不需要带库文件 - 动态库(.so):编译时只记录引用,运行时再加载库
→ 程序体积小,运行必须带库文件
10.3、静态库(Static Library).a
核心特点
- 编译时把库代码焊死集成到程序里
- 运行时不需要库文件,程序独立运行
- 缺点:程序体积大,多个程序用同一个库,会重复占用内存
制作 + 使用 完整步骤(基于你的项目结构)
项目结构:
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 行命令)
# 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
电脑就自动帮你把整个项目编译好,生成可执行程序。
他可以:
- 一键编译整个项目(不用手敲几十字的 gcc 命令)
- 自动管理文件依赖(改了一个文件,只重新编译改了的部分,超快)
- 一键清理垃圾文件(
.o文件、可执行程序) - 多人协作统一编译方式(不会出现“我这能跑,你那跑不了”)
一句话:
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 有什么用?
- 跨平台(最牛的点)
一份配置,Linux、Windows、Mac 都能编译,不用改代码。 - 比 Makefile 简单10倍
不用写复杂的编译规则,几行代码就能搞定大型项目。 - 大型项目必备
所有大厂、开源项目(Linux 内核、Redis、Nginx 周边)全用 CMake。 - 自动管理依赖
自动找库、自动链接、自动处理头文件路径。
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
更多推荐



所有评论(0)