⭐️博主: 此生决int-@CSDN博客
速胜派就是最大的投降派!!!
🔥热门专栏🔥
深入理解 C++ 系列 | 算法系列
快速复习系列 | Linux系列
Gitee地址:财哥 | GitHub地址:财哥
文章目录
-
- 上期回顾
- 4. 自动化构建:`make` / `Makefile`⭐️⭐️⭐️⭐️
-
- 4-1 背景
-
- 手动编译遇到的问题
- `make` 和 `Makefile` 的区别
- 4-2 理解
-
- 依赖关系和依赖方法
- 4-3 基本使用
-
- 一个完整的 `Makefile`
- 规则的书写格式
-
- 语法
- 功能
- 示例
- 注意事项
- 使用 `make`
-
- 语法
- 功能
- 示例
- 注意事项
- `.PHONY` 和伪目标
-
- 语法
- 功能
- 注意事项
- 为什么不会重复编译
-
- 文件的时间属性
- 判断规则
- 4-4 推导过程
-
- 像栈一样执行
- `make` 的工作方式
- `make` 不检查命令是否正确
- 4-5 适度扩展语法
-
- 变量
-
- 语法
- 示例
- 自动变量
-
- 示例
- 模式匹配
-
- 语法
- 功能
- 示例
- `@`:不回显命令
-
- 语法
- 功能
- 注意事项
- 4,总结
-
- 一个小而美的Makefile文件⭐️⭐️⭐️⭐️⭐️
- 核心概念【重点】
- 规则格式与使用
- 不重复编译原理【重点】【易忘】
- 变量与自动变量
- 5. Linux 第一个系统程序——进度条
-
- 5-1 补充:回车与换行
- 5-2 行缓冲区
-
- 5-2-1 为什么调用 `printf` 后不一定马上看见文字
- 5-2-2 什么是行缓冲
-
- 注意事项
- 5-4 进度条代码
-
- 5-4-1 进度条的显示结构
- 5-4-2 version1:固定 101 格的原理版本
-
- 解决的问题
- 完整代码
- 5-4-3 version2:根据真实进度更新
-
- 为什么需要真实进度
- 完整代码
- 5-4-4 配套 Makefile:自动化构建多个源文件
-
- 完整 Makefile
- 总结:5. 第一个系统程序:进度条
-
- 回车 vs 换行【易混】【重点】
- 行缓冲区【重点】
- 动态显示核心组合【重点】
- 进度条实现要点
- 6. 版本控制器 Git
-
-
- Git、本地仓库和远端仓库
- 6-2 Git 简史
- 6-3 安装 Git
- 6-4 在 GitHub 创建项目
-
- 创建远端仓库
- 使用 `git clone` 下载项目
- 6-5 Git 三板斧
-
- 首次使用配置用户名和邮箱
- 版本管理只管理源文件
- 使用 `.gitignore` 忽略文件
- `git push` 被拒绝时的冲突
- 其他常用命令
-
- 总结:6. 版本控制器 Git
-
- 核心概念
- 三板斧【重点】
- 辅助命令与 .gitignore
- 7. 调试器 gdb/cgdb
-
- cgdb
- 预备:必须先 -g【重点】
- gdb命令合集⭐️⭐️⭐️⭐️
- 常用命令速查
- 三个调试技巧【重点】
- 8. 附录:换源实操
-
- 步骤主线(两者一致)
- CentOS 7 换 yum 源
- Ubuntu 换 apt 源
- 易混淆对比速查卡
- 下期预告
- 线程
- 结语
上期回顾
大家好,这里是此生弃int(财哥),欢迎来到我的博客。上一篇我们主要学习了基础开发工具的前半部分,学习和认识了软件包管理器、vim、gcc/g++,那么今天我们继续来学习剩下的几个开发工具:软件包管理器、vim、gcc/g++,那我们开始吧!
4. 自动化构建:make / Makefile⭐️⭐️⭐️⭐️
4-1 背景
手动编译遇到的问题
小程序只有一个源文件时,可以手动执行:
gcc -o myproc myproc.c
gcc 是编译器:把人写的源代码转换成计算机可以运行的程序。
但大型工程可能有成千上万个源文件。每次修改一个文件后,如果都手动输入编译命令,就会遇到两个问题:
因此,需要一个文件先记录“哪些文件依赖哪些文件”以及“如何生成它们”,再用一个命令自动完成编译。
make 和 Makefile 的区别
- make:一个命令,负责读取规则并执行编译。
- Makefile:一个文件,负责保存编译规则。
make 是命令,Makefile 是文件;两者搭配完成自动化构建。
4-2 理解
依赖关系和依赖方法
假设要生成一个程序 myproc,它需要源文件 myproc.c。如果没有 myproc.c,就无法生成 myproc,这就是依赖关系:一个文件的生成需要另一个文件。
但是,仅仅知道依赖谁还不够,还需要知道如何根据 myproc.c 生成 myproc,这就是依赖方法:生成目标时要执行的命令。
因此,一条规则可以理解为:
目标文件 ← 依赖文件
生成目标文件的方法
Makefile 中的基本形式是:
目标:依赖
生成目标的命令
其中:
- 目标:想生成或操作的文件;
- 依赖:生成目标所需要的文件;
- 命令:生成目标时执行的操作。
依赖关系决定“先找谁”,依赖方法决定“找到后做什么”。
核心概念只有两个:依赖关系 + 依赖方法。先有依赖关系,才有执行依赖方法的依据。
4-3 基本使用
一个完整的 Makefile
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
这个文件包含两条主要规则:
myproc ← myproc.c
clean ← 无文件依赖
第一条规则表示:程序 myproc 依赖源文件 myproc.c,生成它时执行:
gcc -o myproc myproc.c
第二条规则表示:目标 clean 被调用时,执行:
rm -f myproc
rm 是删除文件的命令,-f 表示即使文件不存在,也尽量不要报错。
规则的书写格式
语法
目标:依赖
命令
功能
- 目标:依赖:声明目标和依赖之间的关系;
- 下一行的命令:说明如何生成或处理目标;
- 目标和依赖必须顶格写;
- 命令前必须使用一个真正的 Tab,不能用空格代替。
示例
myproc:myproc.c
gcc -o myproc myproc.c
注意事项
下面的缩进必须是 Tab:
myproc:myproc.c
gcc -o myproc myproc.c
如果把 gcc 前面的 Tab 换成空格,make 可能报错,因为 make 用缩进区分“规则声明”和“要执行的命令”。
使用 make
语法
make [目标]
功能
- 直接执行 make:执行 Makefile 中的第一个目标;
- 执行 make clean:明确要求执行名为 clean 的目标。
示例
在包含 Makefile 和 myproc.c 的目录中执行:
make
等价于让 make 根据第一条规则决定是否执行:
gcc -o myproc myproc.c
执行清理操作:
make clean
这会执行:
rm -f myproc
注意事项
clean 不会因为写在 Makefile 中就自动执行。直接执行 make 时,默认只处理第一个目标,也就是 myproc。
.PHONY 和伪目标
语法
.PHONY:目标
功能
.PHONY 用来声明一个伪目标。伪目标是“名字看起来像目标,但不一定对应真实文件”的目标。
例如:
.PHONY:clean #完成清理任务,而不是生成一个clear文件
clean:
rm -f myproc
这里的 clean 通常不是一个真实文件,而是一个“执行清理命令”的名称。
注意事项
如果当前目录中恰好存在一个名为 clean 的文件,没有声明 .PHONY 时,make clean 可能认为目标已经完成,从而不执行命令。
声明后:
.PHONY:clean
make 会忽略同名文件的影响,每次执行:
.PHONY 的作用:让 make 忽略源文件和目标文件的时间对比,把目标当成每次都需要执行的伪目标。
为什么不会重复编译
make 会比较目标文件和依赖文件的修改时间。
文件的时间属性
文件不仅包含内容,还包含属性。常见的时间属性有:
| Modify | 文件内容最后一次被修改的时间 |
| Change | 文件属性最后一次发生变化的时间 |
| Access | 文件最后一次被访问的时间 |
在判断是否需要重新构建时,make 主要比较文件的 Modify 时间。
判断规则
- myproc 不存在:执行命令;
- myproc.c 的**修改时间(Modify)**比 myproc 新:执行命令;
- myproc.c 没有比 myproc 新:通常不执行命令。
因为依赖文件更新后,目标文件可能已经过时,所以需要重新生成。
make 默认不重复编译,是因为它比较目标文件和依赖文件的 Modify 时间;只有目标不存在或依赖更新得更晚时,才重新执行命令。
4-4 推导过程
myproc 依赖 myproc.o
myproc.o 依赖 myproc.s
myproc.s 依赖 myproc.i
myproc.i 依赖 myproc.c
像栈一样执行
栈是一种“后进先出”的结构:最后放进去的内容最先取出来。
make 处理依赖链时,先沿着依赖关系向下查找,直到找到最底层的源文件;然后再从底层向上逐层生成目标:
向下查找:myproc → myproc.o → myproc.s → myproc.i → myproc.c
向上生成:myproc.c → myproc.i → myproc.s → myproc.o → myproc
因此,依赖关系像入栈,生成过程像出栈。
make 先沿依赖链向下寻找最底层文件,再从底层向上逐层执行命令。
make 的工作方式
具体来说:
make 不检查命令是否正确
make 只负责根据文件的依赖关系决定“是否执行命令”,不负责判断命令本身是否正确。
4-5 适度扩展语法
变量
语法
变量名=值
使用变量时写成:
$(变量名)
变量就是一个可以反复使用的名字,用来保存文件名、命令名或文件列表,避免同样的内容重复书写。作用类比定义宏
示例
BIN=proc.exe
CC=gcc
SRC=myproc.c
OBJ=myproc.o
RM=rm -f
# 使用时加$()
$(BIN):$(SRC)
$(CC) -o $(BIN) $(SRC)
.PHONY:clear
clear:
$(RM) $(OBJ) $(BIN)
自动变量
自动变量只能在规则的命令中使用,make 会根据当前规则自动为它们填入内容。
| $@ | 当前目标文件 |
| $^ | 当前规则的全部依赖 |
| $< | 当前规则的第一个依赖 |
示例
myproc:main.o print.o
gcc -o $@ $^
# $@ → myproc
# $^ → main.o print.o
# 因此等价于:
gcc -o myproc main.o print.o
模式匹配
语法
%.o:%.c
命令
功能
% 表示一个可以变化的部分。%.o:%.c 表示:
- 任意一个 .o 文件;
- 依赖同名的 .c 文件。
例如:
main.o ← main.c
print.o ← print.c
都可以匹配这条规则。
示例
%.o:%.c
$(CC) -c $< -o $@
当处理 main.o 时:
$< → main.c
$@ → main.o
命令等价于:
gcc -c main.c -o main.o
@:不回显命令
语法
目标:
@命令
功能
默认情况下,make 执行命令前,通常会先把命令显示在屏幕上。在命令前加 @,可以只执行命令而不显示这行命令。
注意事项
@ 只影响命令是否回显,不改变命令本身的功能。
4,总结
一个小而美的Makefile文件⭐️⭐️⭐️⭐️⭐️
BIN=proc.exe
CC=gcc
#SRC=$(shell ls *.c) #执行shell命令,列出所有.c文件
SRC=$(wildcard *.c) # makefile命令,列出所有.c文件
OBJ=$(SRC:.c=.o) # 把SRC里面的所有.c文件换成.o
LFLAGS=-o
FLAGS=-c
RM=rm -f
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^ # $^ 当前所有的依赖文件,$@ 当前目标文件,即上面的BIN
@echo "linking … $^ to $@"
%.o:%.c
@$(CC) $(FLAGS) $< # $< 当前的第一个依赖
@echo "compiling … $< to $@"
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
效果: 
核心概念【重点】
- make 是命令(执行者),Makefile 是文件(规则说明书)。
- 规则两要素:依赖关系(先找谁)+ 依赖方法(找到后做什么)。
规则格式与使用
myproc:myproc.c # 目标:依赖(顶格写)
gcc -o myproc myproc.c # 命令前必须是一个真正的 Tab,不能用空格【易错】
.PHONY:clean # 声明伪目标
clean: # 无依赖
rm -f myproc
- make = 执行第一个目标;make clean = 指定执行 clean(clean 不会自动跑)。
- .PHONY 作用:防止目录里恰好有同名文件 clean 导致 make 误判"已完成"而不执行。【重点】
不重复编译原理【重点】【易忘】
- make 比较目标与依赖的 Modify 时间(内容修改时间)。
- 目标不存在 → 执行;依赖比目标新 → 执行;否则不执行。
- 文件三时间【易混】:Modify 内容修改 / Change 属性或内容变化 / Access 访问。
- 推导链像栈:先沿依赖向下找到最底层源文件,再从底向上逐层生成(入栈查找、出栈生成)。
- make 只管"是否执行命令",不管命令本身对错(gcc 没装、代码有错它都不管)。
变量与自动变量
BIN=proc.exe # 定义变量,使用时 $(BIN)
SRC=$(wildcard *.c) # 找出当前目录所有 .c
OBJ=$(SRC:.c=.o) # 后缀替换:a.c b.c → a.o b.o
【易混】 三个自动变量(只能用在规则的命令里):
| $@ | 目标文件 |
| $^ | 全部依赖 |
| $< | 第一个依赖 |
- 模式匹配:%.o:%.c = 任意 .o 依赖同名 .c。
- 命令前加 @ = 执行但不回显该命令。【易忘】
5. Linux 第一个系统程序——进度条
进度条看起来只是不断改变一行文字,但它依赖三个关键知识:
打印文字
↓
控制光标回到行首
↓
刷新输出缓冲区
↓
同一行被不断覆盖,形成动态效果
5-1 补充:回车与换行
| \\r | 回车 | 回到当前行的行首 |
| \\n | 换行 | 移动到下一行 |
\\r 不会换到下一行,它只把光标拉回当前行开头;\\n 也不负责回到行首,它只负责向下移动一行。
强调:\\r 是回到行首,\\n 是换到下一行,它们是两个动作;键盘回车键通常对应 \\r\\n。
5-2 行缓冲区
5-2-1 为什么调用 printf 后不一定马上看见文字
假设程序打印一段文字后要等待 3 秒:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("hello");
sleep(3);
return 0;
}
有时程序运行后,等待期间看不到 hello,直到程序结束才显示。这不是 printf 没有执行,而是文字暂时没有送到显示器。
程序通常会先把输出文字放进一块临时内存中,等条件合适时再一次性送出去。这块临时内存叫缓冲区:可以把它理解为“先暂存数据的中转区”。
5-2-2 什么是行缓冲
行缓冲:程序暂时保存一行输出,遇到换行符 \\n 时,通常把这一行送到显示设备。下面有三个例子
#include <stdio.h>
#include <unistd.h>
int main(void)
{
// ==================== ① printf() 带 \\n ====================
// \\n 通常会触发 stdout 刷新,因此 hello 通常会立即显示
printf("hello\\n");
sleep(3);
// ==================== ② printf() 不带 \\n ====================
// 没有换行,hello 可能暂时停留在 stdout 缓冲区
printf("hello");
sleep(3);
// ==================== ③ 使用 fflush(stdout) ====================
// 手动刷新 stdout,使缓冲区中的 hello 立即输出
printf("hello");
fflush(stdout);
sleep(3);
return 0;
}
注意事项
- stdout 表示标准输出,通常就是终端显示器。
- fflush(stdout) 只负责“把内容送出去”,不会自动换行。
- 动态进度条通常需要同时使用 \\r 和 fflush(stdout)。
5-4 进度条代码
5-4-1 进度条的显示结构
一个简单的进度条可以设计成:
[######################### ][53%][/]
其中:
- 第一部分表示已经完成的进度;
- [53%] 表示百分比;
- 最后的 |、/、-、\\ 是旋转光标,用来表示程序仍在工作。
旋转光标按照下面的顺序变化:
| → / → – → \\ → |
注意:反斜杠在 C 字符串中有特殊含义,因此要写成 \\\\。
5-4-2 version1:固定 101 格的原理版本
解决的问题
最开始可以不考虑真实下载量,只模拟从 0% 增长到 100%。为了让进度条逐格增加,准备一个固定长度的字符数组:
[ ]
每完成一点,就把其中一个空格改成 #,然后重新输出整条进度条。
这里使用 100 个位置表示 0% 到 100%,数组还要额外留一个位置存放字符串结束标记 \\0,所以数组大小是 101。
完整代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
// buffer 用来保存进度条
// 100 个字符用于显示进度,最后 1 个字符用于存放字符串结束符 '\\0'
char buffer[101];
// 用于记录当前旋转光标的位置
int spinner_index = 0;
// 旋转光标的 4 种状态:| / – \\
const char spinner[] = "|/-\\\\";
// 将 buffer 前 100 个位置全部初始化为空格
// 此时得到的是一条长度为 100 的空进度条
memset(buffer, ' ', 100);
// C 字符串必须以 '\\0' 结尾
// 因此 buffer[100] 不能用于显示进度,必须保存 '\\0'
buffer[100] = '\\0';
// i 表示当前进度,从 0% 一直到 100%
for (int i = 0; i <= 100; i++) {
// i == 0 时还没有完成任何进度,因此不需要填充 '#'
// 从 i == 1 开始,每循环一次就将一个空格替换成 '#'
if (i > 0) {
buffer[i – 1] = '#';
}
// \\r:将光标移动到当前行开头
// %s:输出进度条字符串
// %3d:百分比至少占 3 个字符,保证 0~100 的显示宽度一致
// %%:printf 中输出字面量 '%'
// %c:输出当前旋转光标字符
printf("\\r[%s][%3d%%][%c]",
buffer,
i,
spinner[spinner_index]);
// stdout 默认可能存在缓冲
// fflush(stdout) 强制立即刷新输出,让进度条实时显示
fflush(stdout);
// 旋转光标下标循环:
// 0 -> 1 -> 2 -> 3 -> 0 -> …
// 对应:
// | -> / -> – -> \\ -> | -> …
spinner_index = (spinner_index + 1) % 4;
// 暂停 50000 微秒,即 50 毫秒
// 控制进度条更新速度
usleep(50000);
}
// 进度完成后换行
printf("\\n");
return 0;
}
5-4-3 version2:根据真实进度更新
为什么需要真实进度
模拟程序可以直接用 i 表示百分比,但真实下载时通常知道的是:
- 总大小 total;
- 当前已经下载的大小 current。
例如,总文件大小是 200 MB,已经下载 100 MB:
百分比 = current / total × 100
= 100 / 200 × 100
= 50%
完整代码
#include <stdio.h>
#include <unistd.h>
// 刷新并显示进度条
// total:任务总量
// current:当前已经完成的任务量
void FlushProcess(int total, int current)
{
// 进度条的总宽度为 50 个字符
const int bar_width = 50;
// 旋转光标的 4 种状态
// 注意:\\ 是转义字符,因此需要写成 "\\\\"
const char spinner[] = "|/-\\\\";
// static 变量只初始化一次
// 函数每次调用结束后,spinner_index 的值不会丢失
// 因此可以在多次调用 FlushProcess() 时不断切换旋转光标
static int spinner_index = 0;
// 如果总任务量 <= 0,没有意义,直接返回
if (total <= 0) {
return;
}
// 防止 current 超过 total
// 例如 total = 100,current = 120 时,按照 100% 处理
if (current > total) {
current = total;
}
// 根据当前完成量计算百分比
// 例如:current = 25,total = 100
// percent = 25 * 100 / 100 = 25
int percent = current * 100 / total;
// 根据当前完成量计算进度条中需要填充多少个 '#'
// bar_width = 50
// 例如完成 50%,filled = 25
int filled = current * bar_width / total;
// \\r:将光标移动到当前行开头
// 这样可以覆盖之前显示的进度条,实现动态刷新
printf("\\r[");
// 绘制 50 个字符的进度条
for (int i = 0; i < bar_width; i++) {
// 如果当前位置小于 filled
// 说明这个位置已经完成,用 '#' 表示
if (i < filled) {
printf("#");
} else {
// 否则用空格表示还没有完成
printf(" ");
}
}
// 输出进度条右边的百分比和旋转光标
// %3d:百分比至少占 3 个字符,保证 0~100 显示宽度一致
// %%:printf 中输出字面量 '%'
// %c:输出一个旋转光标字符
printf("][%3d%%][%c]",
percent,
spinner[spinner_index]);
// stdout 默认可能存在缓冲
// fflush() 强制立即刷新输出,使进度条能够实时显示
fflush(stdout);
// 更新旋转光标下标
// 0 -> 1 -> 2 -> 3 -> 0 -> …
// 对应:
// | -> / -> – -> \\ -> | -> …
spinner_index = (spinner_index + 1) % 4;
}
int main(void)
{
// 设置任务总量为 100
int total = 100;
// current 表示当前已经完成的任务量
// 从 0 开始,一直增加到 total
for (int current = 0; current <= total; current++) {
// 根据当前进度刷新进度条
FlushProcess(total, current);
// 暂停 50000 微秒,即 50 毫秒
// 控制进度条的刷新速度
usleep(50000);
}
// 进度完成后换行
printf("\\n");
return 0;
}
5-4-4 配套 Makefile:自动化构建多个源文件
完整 Makefile
BIN=processbar.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
$(BIN):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -c $< -o $@
.PHONY:clean
clean:
rm -f $(OBJ) $(BIN)
总结:5. 第一个系统程序:进度条
回车 vs 换行【易混】【重点】
| \\r | 回车 | 回到当前行行首(不换行) |
| \\n | 换行 | 移到下一行(不回行首) |
键盘回车键 ≈ \\r\\n 两个动作合一(老式打字机:打字头移动 + 纸张移动)。
行缓冲区【重点】
- printf 的文字先进缓冲区(中转暂存区),不一定立刻显示。
- 行缓冲:遇到 \\n 才送出;缓冲区满、程序结束也会触发刷新。
- 手动刷新:fflush(stdout); —— 只负责送出,不换行。
- 三个标准流【易忘】:stdin 键盘输入 / stdout 屏幕输出 / stderr 错误输出;终端是字符设备。
动态显示核心组合【重点】
printf("%-2d\\r", i); # %–2d:宽度2左对齐(防止 10→9 残留字符);\\r:回行首覆盖
fflush(stdout); # 立即刷新,否则留在缓冲区看不到变化
进度条实现要点
- 结构:[#### ][53%][/] = 已完成 + 百分比 + 旋转光标 |/-\\(C 字符串里反斜杠写 \\\\)。
- v1:固定 101 格数组(100 格 + \\0),每轮填一个 #,\\r 重绘。
- v2:percent = current * 100 / total,filled = current * bar_width / total,真实下载每完成一块调一次刷新。
- %3d 固定百分比宽度,防抖动;%% 输出字面量 %。
6. 版本控制器 Git
版本控制器:自动记录文件每次改动和版本变化的软件。它不仅能保存历史版本,还能帮助多人同时开发同一个项目。
版本控制器主要解决两个问题:
Git、本地仓库和远端仓库
如果每个人都把项目文件放在自己电脑上,其他人无法方便地获取最新修改。因此,需要一个专门保存项目历史记录的地方。
Git:一种版本控制器软件,负责记录文件变化、保存版本并同步修改。
仓库:保存项目文件和版本历史记录的地方。可以把它理解成“项目文件夹加上完整的修改记录”。
Git 通常涉及两个仓库:
| 本地仓库 | 自己的电脑 | 保存自己提交的版本 |
| 远端仓库 | 云服务器或其他服务器 | 让多人共享和备份项目 |
Git 既可以作为操作本地仓库的工具,也可以通过网络和其他仓库交换数据,所以它既是 client(客户端:主动连接并操作其他仓库的一方),也能作为 server(服务器端:向其他电脑提供仓库数据的一方)。
GitHub 和 Gitee 是基于 Git 建立的网站平台。它们提供云端仓库、网页管理和多人协作功能,但它们本身不是 Git 软件。
Git 是分布式版本控制系统:每个开发者的本地仓库都保存完整的版本历史,不是只能依赖一个中央服务器。即使暂时没有网络,也可以在本地查看历史、创建版本;联网后再与远端仓库同步。
本地仓库保证本地开发和历史记录,远端仓库用于备份和多人共享。
6-2 Git 简史
Linux 内核是一个非常大的开源项目,需要很多人长期共同维护。早期没有 Git,开发者主要依靠手工发送和合并补丁。
补丁:描述“应该对原文件做哪些修改”的文件。
发展过程如下:
1991—2002:主要依靠手工补丁维护 Linux 内核
↓
2002:使用商业版本控制软件 BitKeeper
↓
2005:BitKeeper 不再免费提供
↓
Linus Torvalds 自己开发 Git
Git 的设计目标是:
- 速度快:快速处理大量文件和修改记录。
- 设计简单:核心操作尽量清晰直接。
- 支持非线性开发:可以让很多开发分支同时进行,再合并结果。
- 完全分布式:每个本地仓库都拥有完整历史,不完全依赖中央服务器。
- 适合超大项目:能够高效管理像 Linux 内核这样的巨大工程。
分支:从项目当前版本分出的一条独立开发线路。例如,一组人开发登录功能,另一组人开发支付功能,可以分别在不同分支中工作。
6-3 安装 Git
不同 Linux 系统使用的安装工具不同。
sudo yum install git
sudo apt install -y git
git –version # 验证安装结果
6-4 在 GitHub 创建项目
如果想把本地项目保存到云端并与他人协作,需要先在 GitHub 上创建一个远端仓库。
创建远端仓库
操作流程:
项目链接通常类似:
https://github.com/用户名/项目名.git

仓库链接:远端仓库在网络上的地址,Git 根据这个地址连接仓库。
使用 git clone 下载项目
如果远端已经有一个项目,需要先把它复制到本地。
语法:
git clone [url] #从远端仓库下载项目文件和版本历史,并在当前目录创建一个本地仓库。
# 示例
git clone https://github.com/octocat/Hello-World.git
执行后,当前目录通常会出现一个 Hello-World 文件夹。
注意事项:
- [url] 要替换成自己复制的项目链接。
- clone 不只是下载当前文件,还会下载 Git 的版本历史。
- 执行命令前,应先进入希望保存项目的目录。
6-5 Git 三板斧
手动修改文件后,Git 不会自动把修改提交到仓库。因为有些修改可能只是临时尝试,Git 需要先让你选择哪些修改要保存。
Git 的基本流程是:
# 第一板斧:git add
git add 文件名 # 将指定文件的修改放入暂存区
git add . # 将当前目录下的所有变更放入暂存区
# 第二板斧:git commit
git commit -m "日志" # 将暂存区内容提交到本地仓库,形成新的版本
# 第三板斧:git push
git push # 将本地仓库已提交的版本上传到远程仓库
暂存区:提交前临时放置“准备保存哪些修改”的区域。
注意事项:
- push 只能上传已经 commit 的内容,不能直接上传只执行了 add 的内容。
- 第一次推送时,可能需要登录 GitHub,或按提示设置远端分支。
- push 成功后,其他协作者才能从远端获取你的提交。
首次使用配置用户名和邮箱
Git 需要知道每次提交是谁完成的,因此第一次使用时要配置用户名和邮箱。
语法:
git config –global user.name "用户名"
git config –global user.email "邮箱"
**功能:**设置当前用户的提交身份;–global 表示对当前电脑上的所有 Git 项目生效。
注意事项:
- 邮箱应填写自己使用的邮箱。
- 这只是记录提交者身份,不等同于完成 GitHub 登录。
通常需要配置私人令牌 
版本管理只管理源文件
源文件:程序员编写、可以直接阅读和修改的代码文件。例如 .cpp、.h、.py 等。
编译程序时,通常还会生成可执行文件、临时文件或缓存文件。这些文件可以由程序重新生成,不需要放进版本仓库,因此版本管理主要保存源文件和必要的配置文件。
版本管理主要管理源文件,不要把可执行文件、临时文件和缓存文件全部提交进去。
使用 .gitignore 忽略文件
如果每次执行 git add . 都把临时文件加入暂存区,就需要反复手动删除。因此,可以告诉 Git 哪些文件不要管理。
.gitignore:列出需要被 Git 忽略的文件名或文件后缀的配置文件。
在项目根目录创建名为 .gitignore 的文件,例如:
*.o # 忽略所有以 .o 结尾的文件
*.out # 忽略所有以 .out 结尾的文件
build/ # 忽略名为 build 的目录
注意事项:
- .gitignore 本身通常应该提交到仓库,让所有协作者使用同一套忽略规则。
- 已经被 Git 提交过的文件,后来再写入 .gitignore,不会自动从历史中消失;.gitignore 主要防止尚未被管理的文件被加入。
.gitignore 用来提前排除不应进入版本仓库的文件和目录。
git push 被拒绝时的冲突
有时执行 git push 会出现 rejected。这通常表示:远端仓库已经有了本地没有的更新。
例如:
远端仓库:A —— B
本地仓库:A —— C
因为远端比本地更新,如果直接推送,Git 不知道如何安全地覆盖远端内容,所以拒绝操作。
解决流程:
git pull #从远端获取最新内容,并尝试合并到当前本地分支。
git push
如果两边修改了同一位置,可能产生冲突。冲突就是 Git 无法自动判断应该保留哪一份修改,需要手动修改文件后,再执行 git add、git commit 和 git push。
远端比本地新时,先用 git pull 同步,再用 git push 上传。
其他常用命令
git status # 查看当前仓库状态:修改、暂存、未跟踪等
git log # 查看本地仓库的提交历史
git pull # 获取远程仓库的新提交,并合并到当前分支
# 基本提交流程
git status # 提交前检查仓库状态
git add main.cpp # 将修改放入暂存区
git commit -m "完成主要功能" # 将暂存区内容提交到本地仓库
git push # 将本地提交上传到远程仓库
# 如果 push 被拒绝,通常先同步远程仓库
git pull # 获取并合并远程的新提交
git push # 再次推送本地提交
总结:6. 版本控制器 Git
核心概念
- 版本控制器两大价值:数据安全(改错可找回)+ 协作开发(记录谁改了什么)。【重点】
- 仓库 = 项目文件 + 完整修改记录;本地仓库(自己电脑)vs 远端仓库(云端,共享备份)。
- Git 是分布式:每个本地仓库都有完整历史,断网也能本地提交。
- GitHub/Gitee 是基于 Git 的平台,≠ Git 本身。【易混】
- 首次使用配身份:git config –global user.name "名" + user.email "邮箱"。
三板斧【重点】
git add main.cpp # ① 修改 → 暂存区(git add . 暂存全部,慎用)
git commit -m "修复登录错误" # ② 暂存区 → 本地仓库(-m 写清做了什么;只提交变化部分)
git push # ③ 本地仓库 → 远端仓库
流程记忆:工作目录 →(add)→ 暂存区 →(commit)→ 本地仓库 →(push)→ 远端仓库。 push 只能传 commit 过的内容;只 add 没 commit 的内容传不上去。
辅助命令与 .gitignore
- git status(add/commit 前查状态)、git log(看提交历史)、git pull(拉远端并合并)。
- push 被拒绝(rejected)= 远端比本地新 → 先 git pull 同步,再 git push;同一位置两边都改 = 冲突,手动改完后重新 add/commit/push。【重点】
- .gitignore:列出要忽略的文件/后缀/目录(*.o、build/);只管理源文件,可执行文件/临时文件不进仓库。
- .gitignore 本身应提交;已被管理过的文件再加入 .gitignore 不会从历史消失。
7. 调试器 gdb/cgdb
cgdb
建议使用cgdb,两者命令完全不变
- 分屏可视化版 gdb,命令完全不变;yum/apt install -y cgdb 安装;ESC 进代码屏,i 回命令屏。
预备:必须先 -g【重点】
- debug(测试用,带调试信息)vs release(上线用,无调试信息)。
- gcc/g++ 默认编译 release,无法源码级调试 → 必须 gcc -g mycmd.c -o mycmd。
- 验证:file mycmd 看 with debug_info;或 readelf -S mycmd | grep -i debug 看 .debug_info 段。

- 开始:gdb binFile
- 退出:ctrl + d 或 quit
gdb命令合集⭐️⭐️⭐️⭐️
| list/l | 显示源代码,从上次位置开始,每次列出 10 行 | list/l 10 |
| list/l 函数名 | 列出指定函数的源代码 | list/l main |
| list/l 文件名:行号 | 列出指定文件的源代码 | list/l mycmd.c:1 |
| r/run | 从程序开始连续执行 | run |
| n/next | 单步执行,不进入函数内部,逐过程 F10 | next |
| s/step | 单步执行,进入函数内部,逐语句 F11 | step |
| break/b [文件名:]行号 | 在指定行号设置断点 | break 10break test.c:10 |
| break/b 函数名 | 在函数开头设置断点 | break main |
| info break/b | 查看当前所有断点的信息 | info break |
| finish | 执行到当前函数返回,然后停止 | finish |
| print/p 表达式 | 打印表达式的值 | print start+end |
| p 变量 | 打印指定变量的值 | p x |
| set var 变量=值 | 修改变量的值 | set var i=10 |
| continue/c | 从当前位置开始连续执行程序 | continue |
| delete/d breakpoints | 删除所有断点 | delete breakpoints |
| delete/d breakpoints n | 删除序号为 n 的断点 | delete breakpoints 1 |
| disable breakpoints | 禁用所有断点 | disable breakpoints |
| enable breakpoints | 启用所有断点 | enable breakpoints |
| info/i breakpoints | 查看当前设置的断点列表 | info breakpoints |
| display 变量名 | 跟踪显示指定变量的值(每次停止时) | display x |
| undisplay 编号 | 取消对指定编号的变量的跟踪显示 | undisplay 1 |
| until X 行号 | 执行到指定行号 | until 20 |
| backtrace/bt | 查看当前执行栈的各级函数调用及参数 | backtrace |
| info/i locals | 查看当前栈帧的局部变量值 | info locals |
| quit | 退出 GDB 调试器 | quit |
常用命令速查
| gdb 程序名 / quit 或 ctrl+d | 进入 / 退出 |
| l(list) / r(run) | 看源码 / 运行到断点或结束 |
| b 行号 / b 函数名 / info b | 设断点 / 看断点 |
| n / s / finish | 逐过程 / 逐语句 / 执行到函数返回 |
| p 变量 / set var 变量=值 | 打印 / 现场改值(不改源码) |
| c / until 行号 | 继续到下一断点 / 执行到指定行 |
| d 编号 / disable 编号 / enable 编号 | 删 / 禁用 / 启用断点 |
| display 变量 / undisplay 编号 | 每次停下自动显示 / 取消 |
| bt / info locals | 看调用栈 / 看当前函数局部变量 |
断点编号只增不减(删了 2 再设新的不会补回 2);↑ 重复上一条命令;undisplay 跟的是显示编号不是变量名。只有新增断点时会用到行号,其他情况都是使用断点编号!
三个调试技巧【重点】
| 新增条件断点 | b 9 if i==30 | 有 if |
| 给已有断点加条件 | condition 2 i==30 | 没有 if,2 是断点编号 |
cgdb窗口之间转换, ESC进入代码区,上面窗口 i,进入cgdb下面窗口
8. 附录:换源实操
步骤主线(两者一致)
备份 → 下载/替换新源配置 → 清缓存更新 →(可选升级)→ 验证 →(可选恢复)
CentOS 7 换 yum 源
sudo mkdir /etc/yum.repos.d/backup # ① 建备份目录
sudo mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ # ② 备份全部旧源
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo # ③ 下载阿里云源(清华源二选一)
sudo yum clean all && sudo yum makecache # ④ 清旧缓存 + 重建【重点:缺这步仍走旧源】
sudo yum update # ⑤ 更新系统
sudo yum repolist # ⑥ 验证
# 恢复:sudo mv /etc/yum.repos.d/backup/*.repo /etc/yum.repos.d/
Ubuntu 换 apt 源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak # ① 备份(apt 是单个文件)
sudo wget -O /etc/apt/sources.list http://mirrors.aliyun.com/repo/ubuntu-sources.list # ② 替换(二选一)
# ③ 检查每行都含 focal(Ubuntu 20.04 代号),混入 bionic/jammy 要删【易错】
sudo apt update # ④ 更新缓存
sudo apt policy # ⑤ 验证
# 恢复:sudo cp /etc/apt/sources.list.bak /etc/apt/sources.list && sudo apt update
【易混】 yum 源 = /etc/yum.repos.d/ 目录下多个 .repo 文件;apt 源 = /etc/apt/sources.list 单个文件。
易混淆对比速查卡
| 链接方式 | 动态:运行时加载 .so,gcc 默认 | 静态:复制进程序 .a,需 -static |
| vim 撤销 | u 撤销(后退) | ctrl+r 恢复(前进) |
| 回车换行 | \\r 回行首 | \\n 下一行 |
| gdb 步进 | n 不进函数(F10) | s 进函数(F11) |
| 条件断点 | 新增 b 9 if i==30(有 if) | 追加 condition 2 i==30(无 if) |
| 自动变量 | $@ 目标 / $< 第一个依赖 | $^ 全部依赖 |
| 安装源 | base 标准稳定 | epel 扩展求全 |
| 软件源配置 | yum:.repo 目录 | apt:sources.list 单文件 |
| make 时间 | Modify 内容修改(make 看这个) | Change 属性变化 / Access 访问 |
下期预告
线程
结语
本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。 也欢迎订阅我的 深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++ 算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线 快速复习系列:知识梳理、查漏补缺,考前冲刺必备 Linux系列:从零带你系统学习Linux系统,深入理解计算机组成原理!
愿每一次敲下键盘,都比昨天更进一步!
愿每一行代码落下,都让未来多一种可能!
网硕互联帮助中心



评论前必须登录!
注册