🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- Linux 开发工具(下)
- Linux 项目自动化构建工具 —— `make` / `Makefile`
- 1. 什么是 `make` 和 `Makefile`?
- 为什么需要 `make`?
- 2. `make` 的基本工作原理
- 基本逻辑
- 规则格式
- 3. `make` 的工作流程
- 4. 伪目标 `.PHONY` 与放置位置的讨论
- 关于 `.PHONY: clean` 的放置位置
- 5. 常见符号与自动变量的使用
- `@` —— 命令隐藏符
- 自动变量 `$^` 与 `$@`
- 6. 进阶技巧(了解)
- 变量定义与使用
- 模式规则
- 传道解惑
- ==文件 = 文件内容 + 文件属性==
- Linux 下第一个小程序——进度条
- 1. 换行 vs 回车:键盘上的时光机
- 2. 行缓冲区:快递员的打包习惯
- 实验验证与现象分析
- Shell 提示符的输出行为
- 为什么覆盖?
- 3. 倒计时实现
- 4. 由于进度条相关文章内容过长,详见下一篇文章!
- `git ` 的使用
- 一、Git 的本质:时间管理大师的 "时光机"
- 二、Linus Torvalds:被逼出来的创新
- 三、Git 的革命性突破
- 四、Git 的现代发展
- 五、Git 改变了软件开发的方式
- 六、`git` 的使用([Git 教程](https://liaoxuefeng.com/books/git/introduction/index.html))
- Git 的核心概念
- Git 的工作流程
- 1. 安装 Git
- 2. 配置 Git
- 3. 创建/初始化仓库
- 4. 克隆远程仓库
- 5. 查看仓库状态
- 6. 添加文件到暂存区
- 7. 提交更改
- 8. 查看提交历史
- 9. 查看远程仓库
- 10. 添加/连接远程仓库
- 11. 推送本地分支到远程仓库
- 12. 拉取远程仓库的更新
- 13. 撤销工作区的修改
- 14. 撤销暂存区的修改
- 15. 撤销提交
- 16. 创建标签
- 17. 查看标签
- 18. 推送标签到远程仓库
- 19. 查看差异
- 20. 查看远程分支
- 21. 删除远程分支
- 1. Git 只会记录已添加到暂存区(staging area)的修改
- 2. Git 忽略某些文件:`.gitignore`
- 3. Git 配置文件的管理
- Linux 调试器 —— `gdb` 的使用
- 一、背景知识
- 二、Windows IDE 对应功能
- 三、`gdb` 常用命令
- 1. 查看源代码
- 2. 运行程序
- 3. 单步执行
- 4. 设置断点
- 5. 删除断点
- 6. 继续执行
- 7. 查看和修改变量
- 8. 跟踪变量
- 9. 查看函数调用栈
- 10. 查看局部变量
- 11. 跳转到指定行
- 12. 退出函数
- 共勉
Linux 开发工具(下)
Linux 项目自动化构建工具 —— make
/ Makefile
1. 什么是 make
和 Makefile
?
在大型软件项目中,源代码通常散布在多个文件和目录中。为了高效地管理这些文件之间的依赖关系并实现自动化编译,Linux 提供了非常强大的工具 —— make
。make
是一个自动化构建工具,它通过读取 Makefile
文件中的规则来决定如何编译和链接程序。Makefile
是一个文本文件,其中定义了项目的依赖关系和构建规则。
为什么需要 make
?
- 自动化构建:手动编译多个源文件并管理它们之间的依赖关系非常繁琐,
make
可以自动化这一过程。 - 增量编译:
make
只会重新编译那些被修改的文件及其依赖项,从而节省编译时间。 - 跨平台兼容:
Makefile
可以在不同的平台上使用,只需稍作修改。
2. make
的基本工作原理
make
的核心功能是根据文件的修改时间自动判断哪些文件需要重新编译。具体来说,它会根据文件间的依赖关系,确保只有修改过的部分被重新编译,避免无谓的重复编译。
基本逻辑
-
目标文件与依赖关系
make
通过比较目标文件与依赖文件的修改时间来决定是否需要重新编译。如果目标文件不存在,或者依赖文件较新,make
就会重新执行对应的编译命令。 -
举例说明:假设我们有如下的依赖关系:
hello
依赖于hello.o
。hello.o
依赖于hello.s
。hello.s
依赖于hello.i
。hello.i
依赖于hello.c
。
当
hello.c
被修改后,make
会检查文件的修改时间,并按照依赖关系从hello.c
开始,逐层更新直到最终目标hello
。
规则格式
目标文件:依赖文件命令
例如:
hello: hello.ogcc hello.o -o hello
如果 hello.o
发生了变化,make
会执行 gcc hello.o -o hello
来生成目标文件 hello
。
3. make
的工作流程
Makefile
中的依赖关系往往构成一个“栈式”结构,即从最终目标开始,逐层向下寻找依赖,直到最初的源文件。
- 查找文件:
make
会在当前目录下查找名为Makefile
或makefile
的文件。 - 确定目标文件:找到文件后,它会查找文件中的第一个目标文件(如示例中的
hello
)并将其作为最终的目标文件。 - 检查依赖关系:如果目标文件不存在,或者其依赖文件的修改时间比目标文件更新,则执行相应的命令生成目标文件。
- 递归处理依赖:如果目标文件的依赖文件(如
hello.o
)不存在,则会进一步在当前文件中查找该依赖文件的规则,并依此进行生成。 - 完成编译:按照依赖关系一层一层地处理,直到最终生成第一个目标文件。
- 错误处理:如果在依赖关系的查找过程中出现错误(如最后被依赖的文件找不到),
make
会直接退出并报错;对于命令执行的错误或编译不成功的情况,make
不会进行处理。
例如下面这个 Makefile
片段展示的完整依赖链:
# 目标文件 hello 依赖于 hello.o
hello: hello.o# gcc 用于编译链接生成可执行文件 (Executable)gcc hello.o -o hello# 目标文件 hello.o 依赖于 hello.s
hello.o: hello.s# -c 表示只编译生成目标文件,不进行链接gcc -c hello.s -o hello.o# 目标文件 hello.s 依赖于 hello.i
hello.s: hello.i# -S 表示生成汇编代码 (Assembly code)gcc -S hello.i -o hello.s# 目标文件 hello.i 依赖于 hello.c
hello.i: hello.c# -E 表示只预处理生成中间文件gcc -E hello.c -o hello.i
- 依赖判断:当修改了
hello.c
后,hello.i
的时间戳就会落后于hello.c
,从而触发后续所有目标的重新编译。 - 自动化依赖推导:
make
会根据目标与依赖之间的时间比较,自动“回溯”整个依赖链,直到确定哪些文件需要重新生成。
4. 伪目标 .PHONY
与放置位置的讨论
定义:伪目标(如 clean
)一般用于清理工程中的目标文件,它们没有被第一个目标文件直接或间接关联。可以通过命令(如 make clean
)显式执行其后的命令。伪目标的特性是 总是被执行,不会因为文件的存在而被忽略。
例如在 Makefile
中,我们经常会定义一些不对应实际文件的辅助目标,clean
目标通常用于删除编译产生的中间文件。为避免与同名文件产生冲突,我们使用 .PHONY
声明该目标总是需要执行:
.PHONY: clean
clean:@rm -f hello.i hello.s hello.o hello
# 使用 `.PHONY` 声明后,`make clean` 会始终执行清理命令,即使当前目录下存在名为 `clean` 的文件。
关于 .PHONY: clean
的放置位置
放在开头或结尾: 无论将 .PHONY: clean
放在 Makefile
的开头还是结尾,其功能是相同的,都会告诉 make
“clean” 不是一个真实存在的文件。然而,从 代码可读性 和 维护性 的角度来看,通常建议将辅助目标(如 clean
)放在文件的末尾。这样做可以:
- 保持主构建规则的集中:主要的目标和依赖关系放在上面,便于开发者快速了解构建流程。
- 逻辑分明:清理等辅助目标作为附加功能放在末尾,形成明显的区分。
5. 常见符号与自动变量的使用
在编写 Makefile
时,合理使用一些特殊符号和自动变量能使文件更加简洁与灵活。
@
—— 命令隐藏符
在规则的命令前加上 @
符号,可以在执行时不将该命令打印到终端。例如:
clean:@rm -f hello.i hello.s hello.o hello
- 作用:使得执行
make clean
时不会在终端中显示rm -f ...
这一行命令,从而使命令输出更干净,只显示必要的信息。
自动变量 $^
与 $@
- $@:代表规则中的目标文件(Target)。
- $^:代表规则中所有的依赖文件(Prerequisites),通常用来减少重复输入依赖文件列表。
示例: 假设有多个依赖文件构成目标文件,我们可以这样写:
hello: hello.o util.o# $^ 表示所有依赖,即 "hello.o util.o"(:右边部分,若有多个,用空格隔开)# $@ 表示目标,即 "hello"(:左边部分)gcc $^ -o $@
好处:使用自动变量能让规则更灵活且易于维护,尤其当依赖项较多时,避免重复书写目标和依赖文件名称。
6. 进阶技巧(了解)
变量定义与使用
在 Makefile
中,我们可以定义变量来简化重复的代码。例如:
CC = gcc
CFLAGS = -Wall -O2myprogram: main.o utils.o$(CC) $^ -o $@main.o: main.c utils.h$(CC) $(CFLAGS) -c main.c -o main.outils.o: utils.c utils.h$(CC) $(CFLAGS) -c utils.c -o utils.o
CC
:定义编译器为gcc
。CFLAGS
:定义编译选项为-Wall -O2
。
模式规则
当项目中有多个相似的文件需要编译时,可以使用模式规则来简化 Makefile
。例如:
%.o: %.c$(CC) $(CFLAGS) -c $< -o $@
%.o
:表示所有以.o
结尾的目标文件。%.c
:表示所有以.c
结尾的源文件。$<
:表示第一个依赖文件。
传道解惑
文件 = 文件内容 + 文件属性
在计算机系统中,文件不仅仅是包含数据内容的容器,它还具有一些属性,描述了文件的元数据(metadata)。这些属性包括文件的权限、修改时间、访问时间、所有者、大小等。
我们可以将文件的概念表示为:文件 = 文件内容 + 文件属性
1. 文件内容(File Content)
文件内容是文件实际存储的数据。对于文本文件,它通常是可读的字符串;对于二进制文件,它可以是任意的数据,如图像、音频、视频或程序代码等。文件内容是文件的核心部分,用户创建、编辑和删除文件时,主要涉及文件内容的操作。
例如,一个文本文件
hello.txt
的内容可能如下:Hello, World!
2. 文件属性(File Attributes)
文件属性是描述文件元信息的数据,通常是系统自动管理的。这些属性并不直接涉及文件的内容,但它们在文件的管理、访问和权限控制中起着至关重要的作用。常见的文件属性包括:
- 文件权限(Permissions):指定哪些用户或用户组可以读取、写入或执行文件。
- 所有者(Owner):文件的创建者或拥有者。
- 创建时间(Creation Time):文件首次创建的时间。
- 修改时间(Modification Time, mtime):文件内容最后一次修改的时间。
- 访问时间(Access Time, atime):文件最后一次被访问的时间(无论是读取、执行还是其他)。
- 改变时间(Change Time, ctime):文件的元数据(如权限、所有者、位置)最后一次修改的时间。
3. 文件属性详解
修改时间(
mtime
):修改时间表示文件内容上次修改的时间。每次文件内容改变时,mtime
就会更新。例如,如果你编辑并保存一个文本文件,那么该文件的mtime
就会更新为当前时间。访问时间(
atime
):访问时间表示文件上次被访问的时间。访问可以是读取文件内容、执行程序文件等操作。每当文件被读取时,atime
就会更新。需要注意的是,有些操作系统会进行优化,使得访问文件时不更新atime
,从而减少磁盘 I/O。例如,使用cat
命令读取文件内容会更新atime
。改变时间(
ctime
):改变时间表示文件的元数据(如文件权限、文件名、所有者等)最后一次改变的时间。ctime
仅在文件的属性发生变化时更新,而不是文件内容。例如,如果改变了文件的权限或文件的所有者,ctime
会发生变化。4. 文件操作与属性变化
修改文件内容: 当我们修改文件内容时,文件的
mtime
会更新,而atime
和ctime
通常不变,除非文件本身被移动、重命名或修改其他元数据。例如,使用echo "new content" > file.txt
命令修改file.txt
文件内容时,文件内容发生了变化,因此mtime
会更新。访问文件: 当我们访问文件(例如读取文件内容)时,文件的
atime
会更新,但mtime
和ctime
不会发生变化。例如,使用cat file.txt
读取文件内容时,文件的atime
会更新,表示文件被访问了。修改文件属性: 当我们修改文件的权限、所有者等属性时,文件的
ctime
会更新。这个时间是文件元数据的变化标志,而不是文件内容本身的变化。例如,使用chmod
修改文件权限时,ctime
会更新,但mtime
和atime
不会发生变化。5. 文件属性的查看与修改
在 Linux 系统中,可以使用
ls
命令查看文件的基本属性(如权限、所有者、时间等):ls -l file.txt
输出例子:
-rw-r--r-- 1 user user 128 Feb 7 10:15 file.txt
其中:
-rw-r--r--
是文件的权限。1
是硬链接数量。user
是文件的所有者。user
是文件的所属用户组。128
是文件的大小。Feb 7 10:15
是文件的mtime
(修改时间)。要查看文件的
atime
和ctime
,可以使用stat
命令:stat file.txt
输出例子:
File: file.txt Size: 128 Blocks: 8 IO Block: 4096 regular file Device: 803h/2051d Inode: 12345678 Links: 1 Access: 2025-01-01 10:00:00.000000000 Modify: 2025-01-01 10:00:00.000000000 Change: 2025-01-01 10:00:00.000000000
Access
:atime
(访问时间)Modify
:mtime
(修改时间)Change
:ctime
(更改时间)
Linux 下第一个小程序——进度条
1. 换行 vs 回车:键盘上的时光机
- 换行(Line Feed,
\n
):光标移动到 下一行,但水平位置不变 。在一些系统中,换行符是\n
,也有的系统中(如 Windows)回车和换行会一起使用。 - 回车(Carriage Return,
\r
):光标回到 当前行的行首,不换行 。在很多操作系统中,回车是用来将光标重置到行首的位置。换行符通常是\r
。
生活场景比喻:想象你用打字机写文章,打完一行字后需要做两个动作:
- 换行(Line Feed):把纸向上推一行(对应
\n
) - 回车(Carriage Return):把打印头移回最左侧(对应
\r
)
2. 行缓冲区:快递员的打包习惯
行缓冲区(Line Buffering)是指输出流的数据并不是直接输出到屏幕或终端,而是先存储在缓冲区中。当缓冲区满时,数据才会被输出。这种方式提升了效率,减少了对系统资源的频繁访问。在 C 语言中,printf()
是行缓冲的典型例子。它的工作原理是:当你调用 printf()
打印一行文本时,数据并不是立刻显示,而是先被放到缓冲区中,直到遇到换行符 \n
时,系统才会将缓冲区的内容真正输出到屏幕上。
生活场景比喻:快递员不会每收到一个小物件就立刻送货,而是攒满一车再出发。 行缓冲区 就像这个 “攒满一车” 的规则:
- 遇到
\n
换行符时立刻 “送货”(刷新缓冲区) - 缓冲区满时自动刷新
- 程序正常结束时也会自动刷新
标准输出(stdout)在终端中是 行缓冲 的,如果没有换行符 \n
,缓冲区不会自动刷新,导致内容被覆盖或丢失。
实验验证与现象分析
-
代码 1:
printf("123456\rAB");
- 现象:只显示
AB
。实测window
的vs 2022
显示AB 456
应该是属于vs
的“个人行为”。 - 原因:
\r
将光标移回行首,但缓冲区未刷新,内容被覆盖或未输出。
- 现象:只显示
-
代码 2:
printf("123456\rAB\n");
- 现象:终端显示
AB3456
。 - 原因:
\n
触发了缓冲区的刷新,AB
覆盖了12
,然后换行。
- 现象:终端显示
-
代码 3:
printf("123456\rAB"); fflush(stdout); // 手动刷新缓冲区
- 现象:终端显示
AB3456
。细节流程:123456
被输出到终端,光标停留在6
后面。\r
将光标移动到行首(即1
的位置)。AB
被输出,覆盖了前两个字符12
,此时终端内容为AB3456
,光标停留在3
的位置。
- 原因:
fflush(stdout)
强制刷新缓冲区,AB
覆盖了12
。
为什么我看到的只有
AB
?我们不妨修改代码,运行后,3 秒内会看到
AB3456
,随后提示符会进行覆盖,这也就是为什么我们看到的只有AB
了:int main() {printf("123456\rAB");fflush(stdout);sleep(3); // 暂停3秒,观察输出return 0; }
- 现象:终端显示
Shell 提示符的输出行为
- 当程序运行结束后,Shell 会立即在 当前光标位置 输出提示符(如
[damai@VM-16-11-centos coding]$
)。 - 由于程序结束时,光标停留在
3
的位置,Shell 提示符会从3
的位置开始输出,覆盖 掉后面的内容3456
。
为什么覆盖?
- 终端的工作机制:终端是一个字符设备,它会严格按照光标位置输出内容。如果光标不在行尾,新输出的内容会从光标位置开始覆盖已有的内容。
- Shell 提示符的输出:Shell 提示符不会主动换行,而是从当前光标位置开始输出。因此,如果程序没有将光标移动到行尾或换行,提示符就会覆盖程序输出的内容。
知道了这些内容,我们就可以尝试看看下面代码运行的结果了:
#include <stdio.h>
#include <unistd.h>int main()
{// 实验1:无换行符,无刷新printf("123456\rAB");sleep(2); // 等待2秒观察现象printf("\n"); // 换行以刷新缓冲区// 实验2:有换行符printf("123456\rAB\n");sleep(2); // 等待2秒观察现象// 实验3:手动刷新printf("123456\rAB");fflush(stdout); // 手动刷新sleep(2); // 等待2秒观察现象printf("\n"); // 换行return 0;
}
在来一遍,深刻理解:
-
现象 1:没有换行符
\n
的情况下调用printf()
:#include <stdio.h>int main() {printf("hello Makefile!");sleep(3);return 0; }
现象:这时 “hello Makefile!” 会先存入缓冲区,不会立刻显示在屏幕上。直到程序结束,缓冲区中的内容才会被显示出来。所以程序在运行完
sleep(3)
后,才会看到输出结果。 -
现象 2:加上换行符
\n
:#include <stdio.h>int main() {printf("hello Makefile!\n");sleep(3);return 0; }
现象:这时,换行符
\n
会强制刷新缓冲区,所以 “hello Makefile!” 会立即输出,等待 3 秒后程序结束。 -
现象 3:调用
fflush(stdout)
强制刷新缓冲区:#include <stdio.h> #include <unistd.h> int main() {printf("hello Makefile!");fflush(stdout); // 强制刷新缓冲区sleep(3);return 0; }
现象:调用
fflush(stdout)
后,printf()
输出的内容会立即显示,无论是否遇到换行符,缓冲区中的内容都会被强制刷新到屏幕上,等待 3 秒后程序结束。
3. 倒计时实现
理解了回车换行和行缓冲区的概念后,我们再来做一个倒计时的实现,相信下面的代码很容易理解:
int main()
{int cnt = 10;while (cnt >= 0){printf("%d\r", cnt);fflush(stdout);cnt--;sleep(1);}return 0;
}
运行一下,会发现倒计时的显示效果是:10
→ 90
→ 80
→ 70
→ ……
,而不是预期的 10
→ 9
→ 8
→ ……。数字宽度不一致, 当 cnt
为两位数 10
时,输出 10
;而当 cnt
变为一位数(如 9)时,输出的是 9
。因此,当从两位数输出变为一位数输出时,上一轮输出中的多余字符(例如 10
中的 1
)依然残留在屏幕上。我们做以下修改即可:
printf("%2d\r", cnt); // 调整输出格式:如果 cnt 只有一位数,则会在数字前补空格
再次运行会发现,倒计时站两个字符,前一个字符空出,只有后一个字符在变化,显得不太正常,我们接着做修改:
printf("%-2d\r", cnt); // 修改对其规则:左对齐
于是,我们的倒计时就实现了:
#include <stdio.h>
#include <unistd.h> // 包含 sleep 函数int main()
{int cnt = 10; // 初始化倒计时值while (cnt >= 0) // 循环直到倒计时结束{printf("%-2d\r", cnt); // 格式化输出,左对齐,固定宽度为2,并覆盖之前输出中多余的字符fflush(stdout); // 强制刷新缓冲区,确保立即输出cnt--; // 倒计时减1sleep(1); // 暂停1秒}printf("倒计时结束!\n"); // 倒计时完成后输出提示信息return 0;
}
4. 由于进度条相关文章内容过长,详见下一篇文章!
git
的使用
这里会简单叙述 Git
的诞生与核心价值,然而关于 git
远远不止于此,需要大量内容才能将 git
讲清楚,所以,未来我会专门出一个专题来对 git
进行详细的讲解!
一、Git 的本质:时间管理大师的 “时光机”
要理解 Git,我们可以想象一个科幻场景:假设你正在写一本小说,每天创作的新章节都会生成一个独立的时间胶囊。某天你发现主角设定出了问题,只需打开对应日期的胶囊就能恢复原貌。Git 本质上就是这样一个分布式时光机。它通过记录文件的 “快照” 而非差异(如 SVN),让每个开发者电脑都保存完整的版本库。例如:
- 张三改实验报告:李四作为 “版本管家”,每次收到修改都存档并标注(
git commit
) - 多人协作写代码:就像乐队分声部排练,Git 确保所有乐谱修改能精准合并(
git merge
)
二、Linus Torvalds:被逼出来的创新
Linux 之父林纳斯·托瓦兹(Linus Torvalds)与 Git
的渊源,堪称技术界的 “复仇爽文”:
在 2002 年以前,lucubrate Linux 的庞大社区还处于一种极度原始的代码管理状态,依靠着手工合并代码来维系项目 Progress。这无疑给参与其中的开发者们带来了巨大的不便与低效,令人不禁联想起原始部落艰难求生的画面。
随后,商业工具 BitKeeper 的引入仿佛曙光初现,为 Linux 内核的代码管理带来了短暂的清朗时期。却不曾想,因社区成员尝试逆向工程这一敏感行为,(BitKeeper 开发团队)愤然剥夺了 Linux 社区的使用授权。正如同好不容易借到的宝藏工具突然被夺走,开发者们再度陷入了深深的困境,这无疑是压垮骆驼的最后一根稻草。
在这一刻,Linus Torvalds,Linux 之父,心中愤怒与不甘的火焰熊熊燃烧。他深知,若不能寻得有效的解决之道,Linux 项目的未来将满是荆棘。于是,两周,仅仅两周的时间,他凭借着过人的智慧与毅力,完成了 Git 原型的编写。而一个月内,Linux 内核便成功迁移到这个自主开发的全新系统上,整个过程仿若一场奇迹。
" 就像被房东赶走的窘迫之人,在绝望之中亲手盖出了一栋更豪华的别墅。" —— 网友评价
三、Git 的革命性突破
对比传统版本控制工具,Git 实现了三大飞跃:
特性 | 集中式(如 SVN) | Git 分布式 |
---|---|---|
网络依赖 | 必须联网提交 | 本地即可完成所有操作 |
数据安全 | 中央服务器故障即丢失历史 | 每个副本都是完整备份 |
分支管理 | 创建/合并分支耗时 | 轻量级分支秒级切换 |
四、Git 的现代发展
Git 自诞生以来,逐渐在全球范围内普及并成为开发人员的标准工具之一。以下是 Git 现代发展的几个关键点:
1. GitHub 的崛起
2008 年,GitHub 上线,它为 Git 提供了一个强大的托管平台,使得开发者可以方便地在线托管代码,并且与其他开发者协作。GitHub 通过引入 Pull Request(PR)功能,让 Git 的协作模式更加高效,推动了开源社区的快速发展。很多知名的开源项目(如 jQuery、Node.js 等)都迁移到了 GitHub。
2. 中国化适配:Gitee 等本土平台
由于 GitHub 在中国大陆访问受到一定限制,本土平台如 Gitee 等逐渐兴起。Gitee 不仅解决了访问速度的问题,还提供了针对中国开发者的多种本地化功能,使得 Git 在国内的普及更加顺利。
3. 可视化工具的普及
随着 Git 的普及,越来越多的可视化工具如 GitKraken、SourceTree 等应运而生。这些工具通过图形界面简化了 Git 的操作,使得即使是没有命令行经验的开发者也能轻松上手。Git 的复杂操作变得更加直观和易用,降低了学习成本。
4. 版本控制的精确性和回溯功能
Git 的最大优势之一就是其强大的回溯能力。当开发者说“我回滚一下”,实际上就是“导演喊‘Cut!’重拍第 3 幕”。每个 commit
就像电影中的一个镜头,都可以精确回溯、查看和恢复,确保每一步的修改都能被追溯和复原。
五、Git 改变了软件开发的方式
从 Linus Torvalds 的灵感到 Git 成为全球开发者的标准工具,Git 无疑已经成为软件开发的核心技术之一。Git 的核心价值不仅仅在于它如何管理版本,更在于它如何改变了开发者的工作方式、提高了协作效率,并且推动了开源软件的发展。
正如 Linus 所说:“Talk is cheap. Show me the code.” Git 用代码改变了世界,它的出现不仅仅是一个工具的革命,更是一种工作方式、协作方式和思想方式的革命。Git 的设计思想和实践,使得开发者能够更加专注于代码的创作,而不必过度担心版本管理的问题。
Git 不仅是一个技术工具,更是一种创新精神的体现——快速响应变化、追求效率、关注团队协作、并持续优化。它的成功,也象征着开源社区强大的生命力和创新能力。
六、git
的使用(Git 教程)
Git 的核心概念
- 仓库(Repository):Git 仓库是项目的核心,包含了项目的所有文件和历史记录。每个开发者都有一个完整的仓库副本。
- 提交(Commit):每次提交都是项目的一个快照,记录了文件的更改和提交信息。
- 分支(Branch):分支是开发中的独立线路,允许开发者在不同的分支上并行工作。
- 合并(Merge):将不同分支的更改合并到一起,确保代码的一致性。
- 克隆(Clone):从远程仓库复制一个完整的仓库到本地。
- 拉取(Pull):从远程仓库获取最新的更改并合并到本地分支。
- 推送(Push):将本地的更改上传到远程仓库。
Git 的工作流程
- 初始化仓库:使用
git init
命令创建一个新的 Git 仓库。 - 添加文件:使用
git add
命令将文件添加到暂存区。 - 提交更改:使用
git commit
命令将暂存区的更改提交到仓库。 - 创建分支:使用
git branch
命令创建一个新的分支。 - 切换分支:使用
git checkout
命令切换到不同的分支。 - 合并分支:使用
git merge
命令将不同分支的更改合并到一起。 - 查看历史:使用
git log
命令查看提交历史。
我们在 Linux CentOS 7 云服务器上使用 Xshell 连接并操作 Git 时的一些常用的 Git 命令:
1. 安装 Git
首先,确保你的 CentOS 7 系统上已经安装了 Git。如果没有安装,可以使用以下命令进行安装:
# 更新 yum 软件包
sudo yum update -y# 安装 Git
sudo yum install -y git
安装完成后,可以通过以下命令检查 Git 版本,确认安装成功:
git --version
2. 配置 Git
在 第一次使用 Git 之前,需要配置用户名和邮箱,这些信息会出现在每次提交的记录中。
git config --global user.name "your_name" # 设置用户名
git config --global user.email "your_email@example.com" # 设置邮箱
通过以下命令查看当前的 Git 配置:
git config --list
3. 创建/初始化仓库
在当前目录下创建一个新的 Git 仓库,使用以下命令(这会在当前目录下生成一个 .git
目录,用于存储 Git 的版本控制信息):
git init
4. 克隆远程仓库
如果想从远程仓库克隆一个项目到本地,可以使用 git clone
命令,这会将远程仓库的内容克隆到当前目录下的一个新文件夹中:
git clone https://github.com/username/repository.git
5. 查看仓库状态
使用以下命令可以查看当前仓库的状态,包括哪些文件被修改、哪些文件被暂存等:
git status
6. 添加文件到暂存区
在对文件进行修改后,需要将文件添加到暂存区(Stage),以便后续提交:
git add file_name # 例如:git add temp.txt ,添加文件到暂存区
如果想添加所有修改过的文件,可以使用:
git add .
7. 提交更改
将暂存区的文件提交到本地仓库,-m
选项后面跟的是本次提交的描述信息。描述信息就是提交日志,尽量提交有意义的信息!:
git commit -m "Your commit message" # 例如:git commit -m "第一次提交" ,提交到本地仓库
8. 查看提交历史
使用以下命令可以查看当前仓库的提交历史:
git log
还可以通过 --oneline
选项简化输出:
git log --oneline
9. 查看远程仓库
查看当前配置的远程仓库:
git remote -v
10. 添加/连接远程仓库
添加/连接一个新的远程仓库:
git remote add origin 远程版本库的URL
# 例如:git remote add origin https://github.com/your_username/your_repo.git
11. 推送本地分支到远程仓库
将本地分支的提交推送到远程仓库:
git push origin 分支名称
git push -u origin master # 将 master 分支推送,并设置默认上游分支(常用)
12. 拉取远程仓库的更新
从远程仓库拉取最新的更改并合并到当前分支:
git pull origin 分支名称
git pull origin master # 拉取远程仓库的最新代码并合并
13. 撤销工作区的修改
撤销工作区中某个文件的修改,恢复到最近一次提交的状态:
git checkout -- 文件名称
# 例如:git checkout -- file.txt 丢弃工作区修改,恢复到最后一次提交状态
14. 撤销暂存区的修改
将某个文件从暂存区撤回到工作区:
git reset HEAD 文件名称
15. 撤销提交
撤销上一次提交,只回退 commit
,保留代码修改:
git reset --soft HEAD~1
撤销最近一次提交,并将更改放回工作区:
git reset --soft HEAD^
如果你想撤销提交并丢弃更改,可以使用:
git reset --hard HEAD^
16. 创建标签
创建一个新的标签:
git tag 标签名称
17. 查看标签
查看所有标签:
git tag
18. 推送标签到远程仓库
将本地标签推送到远程仓库:
git push origin 标签名称
19. 查看差异
查看工作区与暂存区的差异:
git diff
查看暂存区与最新提交的差异:
git diff --cached
20. 查看远程分支
查看远程仓库的所有分支:
git branch -r
21. 删除远程分支
删除远程仓库的指定分支:
git push origin --delete 分支名称
至于分支管理、多人协作和冲突管理等操作暂时用不到,放到以后再说,有兴趣可自行百度。
除了前面提到的基本操作,Git
在使用时还存在一些常见的注意事项(这里提到部分内容,如有其他问题需勤于百度):
1. Git 只会记录已添加到暂存区(staging area)的修改
Git 不会自动跟踪所有修改。它只会跟踪你手动添加到暂存区的修改,即 默认记录修改部分(工作区和暂存区)。也就是通过 git add
添加的文件或更改。
- 工作区(Working Directory):对文件的任何修改都首先体现在工作区。
- 暂存区(Staging Area):通过
git add
命令将修改暂存到 Git 中的暂存区。 - 本地仓库(Repository):通过
git commit
命令,将暂存区的修改提交到本地仓库。
关键点(工作流程): 只有 git add
命令添加到暂存区的文件才会被 git commit
提交(修改文件 → git add
添加到暂存区 → git commit
提交到仓库)。
示例:
echo "This is a test" > file.txt # 修改文件
git status # 查看状态,file.txt 会显示为修改状态
git add file.txt # 添加到暂存区
git commit -m "Updated file.txt" # 提交修改
忘记 git add
的后果
如果修改了文件但没有执行 git add
,然后直接 git commit
,Git 是不会将这些修改提交的。因此,Git 只会记录已经添加到暂存区的修改。
2. Git 忽略某些文件:.gitignore
如果不希望某些文件被 Git 跟踪(如编译出来的二进制文件、IDE 配置文件等),可以使用 .gitignore
文件来指定不需要跟踪的文件或文件夹。
创建 .gitignore
# 例如,忽略所有 *.log 文件
echo "*.log" >> .gitignore
然后 git add .gitignore
提交 .gitignore
文件!!!
关键点: .gitignore
文件是 Git 跟踪文件的方式之一。
3. Git 配置文件的管理
Git 配置文件分为三种层级:
- 系统级:影响系统上所有用户的配置,通常位于
/etc/gitconfig
。 - 全局级:影响当前用户的配置,通常位于
~/.gitconfig
。 - 仓库级:只对当前仓库生效,位于仓库的
.git/config
。
可以通过以下命令查看和修改 Git 配置:
git config --global user.name "Your Name" # 设置全局用户名
git config --global user.email "you@example.com" # 设置全局邮箱
查看当前配置:
git config --list
Linux 调试器 —— gdb
的使用
一、背景知识
在程序开发过程中,为了有效定位和修复代码中的错误,调试工具起着至关重要的作用。程序的发布方式主要有两种:debug
模式和 release
模式。
- debug 模式:此模式下编译生成的程序包含了丰富的调试信息,例如变量名、函数名、源代码行号等,方便开发者借助调试工具进行错误排查。不过,debug 模式通常会增加程序的体积,并且在一定程度上降低运行效率。
- release 模式:旨在提供高性能的可执行程序,编译时会进行各种优化,剔除不必要的调试信息,以确保程序运行速度和资源利用率最大化。在 Linux 系统中,使用 gcc 或 g++ 编译器编译出来的二进制程序,默认是 release 模式。
若要使用 gdb(GNU Debugger)对程序进行调试,必须在源代码生成二进制程序时,加上 -g
选项。这样,编译器会在生成的可执行文件中嵌入调试所需的信息,使得 gdb 能够准确地关联程序运行状态与源代码。
编译生成调试信息
假设有一个源文件 temp.c
,-g
告诉编译器生成调试信息,-o temp
指定输出文件名。可以通过以下命令编译程序并生成调试信息:
gcc -g temp.c -o temp
启动 gdb
gdb
启动时需要提供一个已经编译好的二进制文件。假设我们有一个名为 temp
的程序文件,通过以下命令启动 gdb
准备进行调试:
gdb temp
退出 GDB
退出 GDB 的方式有两种:
- 输入
quit
或q
命令(常用)。 - 按下
Ctrl + D
。
二、Windows IDE 对应功能
对于 Windows 用户习惯的 IDE 调试工具(如 Visual Studio),gdb
也提供了类似的功能。例如:
- 设置断点:在源代码中设置断点,并在程序执行时停止。
- 单步调试:支持单步执行,可以选择逐行执行代码,或者进入函数内调试。
- 变量观察:支持查看和修改变量的值,并可以在调试过程中动态修改它们。
- 堆栈跟踪:查看当前调用栈以及每个函数的参数。
与 Windows 中的 IDE 相比,gdb
是命令行工具,因此需要通过命令行输入调试命令,但功能是非常强大的。
三、gdb
常用命令
GDB 提示符的作用
- 当你启动 GDB 并加载程序后,GDB 会进入交互模式,显示
(gdb)
提示符。 - 这个提示符表示 GDB 正在等待你输入命令。
- 你只需要在
(gdb)
后面输入命令,按回车执行。
例如:
(gdb) break main # (gdb) 是 GDB 的提示符,只需要输入 break main 并按回车即可
1. 查看源代码
list
或 l
:查看源代码。
list <行号>
:从指定行号开始显示源代码。list <函数名>
:显示指定函数的源代码。- 默认每次显示 10 行,按回车键继续显示后续内容。
(gdb) list 10 # 显示从第 10 行开始的源代码
(gdb) list # 显示当前行及接下来的 10 行
(gdb) l main # 显示 main 函数的源代码
2. 运行程序
run
或 r
:从头开始运行程序。
- 如果程序需要参数,可以在
run
后面加上参数:
(gdb) run # 启动程序的执行,不带任何命令行参数
(gdb) run arg1 arg2 # 启动程序的执行,并传递两个命令行参数:arg1 和 arg2
3. 单步执行
next
或n
:单步执行(不进入函数内部)。step
或s
:单步执行(进入函数内部)。
(gdb) next # 单步执行
(gdb) step # 进入函数内部
4. 设置断点
break
或 b
:设置断点。
break <行号>
:在指定行设置断点。break <函数名>
:在函数入口处设置断点。info breakpoints
:查看当前设置的所有断点及其状态。
(gdb) break 20 # 在第 20 行设置断点
(gdb) break main # 在 main 函数的开头设置断点
(gdb) info breakpoints # 查看所有断点信息
5. 删除断点
delete breakpoints
:删除所有断点。delete breakpoints n
:删除序号为n
的断点。disable breakpoints
:禁用所有断点,使其在下次调试时不起作用。enable breakpoints
:启用所有已禁用的断点。
(gdb) delete breakpoints # 删除所有断点
(gdb) delete breakpoints 1 # 删除序号为 1 的断点
(gdb) disable breakpoints # 禁用所有断点
(gdb) enable breakpoints # 启用所有断点
6. 继续执行
continue
或 c
:从当前位置继续执行,直到遇到下一个断点或程序结束。
(gdb) continue # 继续执行程序
7. 查看和修改变量
print
或 p
:打印变量的值。
print <变量名>
:打印变量的值。print <表达式>
:计算并打印表达式的值。
(gdb) print x # 打印变量 x 的值
(gdb) print x + y
set var 变量名=值
:修改变量的值。
(gdb) set var x=10 # 将变量 x 的值修改为 10
8. 跟踪变量
display <变量名>
:每次程序暂停时,自动打印变量的值。undisplay <编号>
:取消对变量的跟踪。
(gdb) display x # 每次停止时显示变量 x 的值
(gdb) undisplay # 取消所有跟踪变量
9. 查看函数调用栈
backtrace
或 bt
:查看当前函数的调用栈(包括参数和调用位置)。
(gdb) backtrace # 查看函数调用栈
10. 查看局部变量
info locals
:查看当前函数的局部变量。
(gdb) info locals # 查看当前栈帧的局部变量
11. 跳转到指定行
until <行号>
:跳转到指定行。
(gdb) until 30 # 跳转到第30行
12. 退出函数
finish
:执行完当前函数并暂停。
(gdb) finish # 执行完当前函数并暂停
共勉