目录

引言

一、 软件包管理器

1.1 什么是软件包?

1.2 两大主流包管理器

1.2.1 CentOS 7 更换 yum 源

1.2.2 Ubuntu 20.04 更换 apt 源

1.3 包管理器的核心:依赖解析

1.4 包管理的生态哲学

二、Linux软件生态

三、编辑器Vim

3.1 vi与vim的区别

3.2 基本概念

3.3 基本操作

3.2.1 模式切换命令

3.2.2 命令模式操作

(1)光标移动

(2)文本编辑

3.2.3 底行模式操作

3.4 简单vim配置

3.5 编辑器的发展

四、编译器gcc/g++

4.1 GCC的历史与发展

4.2 编译的四个阶段

4.2.1 预处理阶段

4.2.2 编译阶段

4.2.3 汇编阶段

4.2.4 链接阶段

4.3 静态链接 vs 动态链接

4.4 常用编译选项

4.5 前沿动态

五、 自动化构建工具:Makefile 编写与核心思想

5.1 核心概念

5.2 Makefile 核心规则

最简 Makefile 示例

5.3 伪目标.PHONY 的作用

5.5 通用 Makefile 模板

核心高级语法

通用多文件工程 Makefile 模板

六、实战项目:Linux 下进度条程序完整实现

6.1 前置核心知识

6.1.1 回车与换行的区别

6.1.2 行缓冲区机制

6.2 倒计时程序

6.3 进度条程序完整实现

6.3.1 头文件 process.h

6.3.2 功能实现 process.c

6.3.3 主函数 main.c

6.3.4 Makefile

6.4 编译运行

七、版本控制

7.1 Git 核心概念与安装

7.1.1 什么是 Git

7.1.2 Git 安装

7.1.3 Git 首次使用配置

7.2 Git 四大工作区域与核心工作流

7.3 Git 核心操作:从创建仓库到推送到 GitHub

7.3.1 第一步:在 GitHub 创建远程仓库

7.3.2 第二步:本地仓库初始化与提交

7.3.3 第三步:关联远程仓库并推送代码

7.4 Git 高频常用命令

7.5 实用补充

八、程序调试利器:gdb 调试核心技巧与实战

8.1 gdb 调试前置条件

8.2 gdb 核心命令分类大全

8.2.1 代码查看命令

8.2.2 断点管理命令

8.2.3 程序执行控制命令

8.2.4 数据查看与修改命令

8.2.5 栈信息查看命令

8.3 gdb 调试实战

步骤 1:编译 debug 版本程序

步骤 2:启动 gdb

步骤 3:查看源代码

步骤 4:设置断点

步骤 5:运行程序

步骤 6:单步调试,进入函数

步骤 7:使用监视点,监控变量变化

步骤 8:修改变量值,验证逻辑

步骤 9:查看函数调用栈,退出 gdb

8.4 进阶技巧

总结

参考资料


引言

作为一名Linux开发者,你是否曾因包管理器报错而焦头烂额?是否在面对复杂的Makefile时感到无从下手?是否在程序崩溃后不知如何定位问题?这些问题几乎每位开发者都会遇到,它们背后指向的正是Linux开发工具链的掌握程度。

Linux开发环境与Windows有着本质的不同。在Windows下,我们习惯了IDE“一键搞定”——Visual Studio帮你完成了编译、链接、调试的全部工作。但在Linux下,这些环节是解耦的:你需要用yum/apt安装工具链,用vim编写代码,用gcc/g++编译,用make/Makefile管理构建,用gdb调试,用git进行版本控制。每个工具各司其职,组合起来才是一套完整的开发工作流。

本文基于 Linux 开发全流程,系统讲解 7 大核心开发工具:软件包管理器 yum/apt、vim 编辑器、gcc/g++ 编译器、Makefile 自动化构建工具、Git 版本控制器、gdb 调试器,从原理到实操、从入门到进阶全覆盖,既适合零基础入门,也适合开发者查漏补缺、巩固基础。

本文所有实操均基于 CentOS 7/Ubuntu 20.04 发行版,所有命令与代码均经过实测验证。

一、 软件包管理器

在 Linux 中安装软件,最原始的方式是下载源码编译安装,但这种方式需要手动解决复杂的依赖关系,效率极低。包管理器的出现,就是为了自动化完成软件的查找、下载、依赖解决、安装、卸载全流程。

1.1 什么是软件包?

在Linux下安装软件的传统方式是下载源代码、配置编译环境、手动编译安装。这对于开发者和普通用户来说都太过繁琐。于是,一些人把常用软件提前编译好,打包成软件包,放在服务器上供用户获取,这就是包管理器的雏形。软件包和包管理器的关系,好比“App”和“应用商店”

1.2 两大主流包管理器

Linux世界中最主流的包管理系统分别是:

  • yum/dnf(RPM系) :主要应用于Fedora、Red Hat、CentOS等发行版,底层使用RPM包格式,依赖解析由DNF(基于libsolv的SAT求解器)完成,比旧版yum更快更鲁棒。

  • apt(Debian系) :主要应用于Ubuntu、Debian等发行版,底层使用DEB包格式(ar归档),依赖解析由C++编写的APT resolver进行布尔可满足性求解,支持回溯和候选版本推荐。

APT将元数据更新(apt update)和软件包升级分开,让用户显式控制;YUM(在DNF底层支持下)则倾向于将元数据刷新与升级操作更紧密地绑定。因此,当你在这两类系统间切换时,需要理解操作的语义而不仅仅是命令名称。

操作场景 CentOS(yum)命令 Ubuntu(apt)命令 说明
查找软件包 yum list | grep 软件名 apt search 软件名 筛选目标软件包,查看是否存在、版本信息
查看软件详情 yum info 软件名 apt show 软件名 查看软件的版本、依赖、描述等详细信息
安装软件 sudo yum install -y 软件名 sudo apt install -y 软件名 -y 参数自动确认安装,无需手动输入 y
卸载软件 sudo yum remove -y 软件名 sudo apt remove -y 软件名 卸载软件,保留配置文件可改用 remove,彻底删除用 purge
更新所有软件包 sudo yum update -y sudo apt upgrade -y 升级系统中所有已安装的软件包到最新版本

清理缓存

yum clean all apt clean

清理下载的安装包缓存,释放磁盘空间

注意事项:

  1. 安装 / 卸载软件需要向系统目录写入内容,必须加 sudo 提权或切换到 root 用户
  2. 包管理器不支持并行安装,同时执行两个 install 命令会报错
  3. 所有操作必须保证服务器网络畅通,可通过 ping www.baidu.com 验证
1.2.1 CentOS 7 更换 yum 源
# 1. 备份原有yum源
sudo mkdir /etc/yum.repos.d/backup
sudo mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/

# 2. 下载阿里云yum源配置文件(也可替换为清华源)
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

# 3. 安装扩展源epel-release(提供更多官方源没有的软件包)
sudo yum install -y epel-release

# 4. 清理旧缓存,生成新缓存
sudo yum clean all
sudo yum makecache

# 5. 验证源是否生效
sudo yum repolist

1.2.2 Ubuntu 20.04 更换 apt 源
# 1. 备份原有apt源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

# 2. 下载清华源配置文件(也可替换为阿里云源)
sudo wget -O /etc/apt/sources.list https://mirrors.tuna.tsinghua.edu.cn/ubuntu/sources.list

# 3. 确保配置文件适配Ubuntu 20.04(focal版本)
# 手动编辑检查:sudo nano /etc/apt/sources.list,确保所有行包含focal关键词

# 4. 更新apt缓存
sudo apt update

# 5. 验证源是否生效
sudo apt policy

1.3 包管理器的核心:依赖解析

包管理器最核心的价值是自动解决依赖关系——现代软件往往依赖数十甚至上百个其他库,手动处理几乎不可能。无论是yum还是apt,都会自动计算依赖树并下载所有必需组件。

但这并不意味着依赖解析总是顺利的。当遇到依赖冲突时:

  • YUM解决方案:使用 yum deplist <包名> 查看依赖关系,或使用 --skip-broken 跳过冲突包。

  • APT解决方案:面对“未满足的依赖关系”错误,首先尝试 apt --fix-broken install 修复。

1.4 包管理的生态哲学

Linux包管理背后反映的是一种协作式软件生态。为什么会有大量开发者免费贡献软件包并提供云服务器供下载?这源于开源社区的文化和激励机制——贡献者通过提升自身技术影响力获得职业机会,企业通过参与开源降低研发成本并建立行业标准。理解这一点,有助于你更深入地理解Linux生态。

二、Linux软件生态

  • Linux下载软件的过程(Ubuntu、Centos、other)。

  • 操作系统的好坏评估——生态问题。

  • 为什么会有人免费在特定社区提供软件,还发布?还提供云服务器让你下载?

  • 软件包依赖的问题。

  • 国内镜像源。

以下是一些国内Linux软件安装源的官方链接:

1. 阿里云官方镜像站

2. 清华大学开源软件镜像站

3. 中国科学技术大学开源镜像站

4. 北京交通大学自由与开源软件镜像站

  • mirror.bjtu.edu.cnhttps://mirror.bjtu.edu.cn/
  • 北交大镜像站提供了多种Linux发行版的镜像,以及相关的软件仓库和工具。该镜像站还提供了详细的文档和指南,帮助用户配置和使用这些软件源。

5. 中国科学院软件研究所镜像站(ISCAS)

6. 上海交通大学开源镜像站 

  • Index of /https://ftp.sjtu.edu.cn/
  • 上海交大镜像站提供了丰富的Linux软件资源,包括多种发行版的镜像和软件仓库。用户可以通过该镜像站方便地下载和安装所需的软件包。

7. 网易开源镜像站

此外,还有一些其他的国内镜像源,如搜狐开源镜像站等,但可能由于时间变化或政策调整,部分镜像站的链接或状态可能有所变动。因此,建议用户在使用前访问官方网站或咨询相关社区以获取最新的信息和帮助。

sudo yum install -y epel-release

好玩的命令:Linux命令行的有趣命令_linux有趣的命令-CSDN博客

三、编辑器Vim

3.1 vi与vim的区别

vim 是 vi 编辑器的升级版本,是 Linux 系统中最主流的命令行文本编辑器,所有 Linux 开发场景,最终都绕不开 vim 的使用。它不仅兼容vi的所有指令,而且还有一些新的特性在里面,例如语法加亮、可视化操作,不仅可以在终端运行,也可以运行于X Window、Mac OS、Windows。

3.2 基本概念

我们讲解vim的三种模式,分别是命令模式(command mode)、插入模式(Insert mode)和底行模式(last line mode),各模式的功能区分如下:

  • 正常/普通/命令模式(Normal mode)

控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入Insert mode下,或者到最后一行模式。

  • 插入模式(Insert mode)

只有在Insert mode下,才可以做文字输入,按「ESC」键可回到命令行模式。该模式是我们后面用的最频繁的编辑模式。

  • 末行模式(last line mode)

文件保存或退出,也可以进行文件替换,找字符串,列出行号等操作。在命令模式下,shift+:即可进入该模式。要查看你的所有模式:打开vim,底行模式直接输入 :help vim-modes。

vim一共有12种模式:six BASIC modes和six ADDITIONAL modes。

3.3 基本操作

3.2.1 模式切换命令

命令

模式切换效果

i

从命令模式进入插入模式,从光标当前位置开始输入

a

从命令模式进入插入模式,从光标当前位置的下一个字符开始输入

o

从命令模式进入插入模式,在光标所在行下方插入新的空行,从行首开始输入

ESC

从插入模式 / 底行模式,切回命令模式

shift+;(即输入:)

从命令模式进入底行模式

3.2.2 命令模式操作
(1)光标移动
命令 功能
h j k l 分别控制光标 左、下、上、右 移动一格
gg 跳转到文本开头
shift+g(G) 跳转到文本末尾
#G 跳转到第 #行,例如 15G 跳转到第 15 行
^ 跳转到光标所在行的行首
$ 跳转到光标所在行的行尾
w 光标跳到下一个单词的开头
b 光标跳回上一个单词的开头
ctrl+b / ctrl+f 屏幕向后 / 向前翻一页
ctrl+u / ctrl+d 屏幕向后 / 向前翻半页

(2)文本编辑
命令 功能
x 删除光标所在位置的 1 个字符,#x 删除光标后 #个字符(含自身)
dd 删除光标所在整行,#dd 删除从光标开始的 #行
yy 复制光标所在整行,#yy 复制从光标开始的 #行
yw 复制光标所在位置到单词结尾的内容
p 将复制 / 删除的内容粘贴到光标所在位置
r 替换光标所在处的单个字符
R 进入替换模式,持续替换光标经过的字符,直到按 ESC 退出
u 撤销上一次操作,支持多次撤销
ctrl+r 恢复撤销的操作
cw 更改光标所在处的单词到词尾,删除后自动进入插入模式

3.2.3 底行模式操作
命令 功能
:w 保存当前文件
:wq 保存文件并退出 vim
:q! 不保存文件,强制退出 vim
:set nu 显示文件行号
:set nonu 取消显示行号
/ 关键词 从光标位置向下查找指定关键词,按 n 跳转到下一个匹配项
? 关键词 从光标位置向上查找指定关键词,按 n 跳转到上一个匹配项
:% s / 旧字符串 / 新字符串 /g 全局替换文本中的旧字符串为新字符串

3.4 简单vim配置

配置文件的位置

  • 在目录 /etc/ 下面,有个名为 vimrc 的文件,这是系统中公共的vim配置文件,对所有用户都有效。

  • 而在每个用户的主目录下,都可以自己建立私有的配置文件,命名为:“.vimrc”。例如,/root 目录下,通常已经存在一个 .vimrc 文件,如果不存在,则创建之。

  • 切换用户成为自己执行 su,进入自己的主工作目录,执行 cd ~

  • 打开自己目录下的 .vimrc 文件,执行 vim .vimrc

常用配置选项,用来测试

  • 设置语法高亮:syntax on

  • 显示行号:set nu

  • 设置缩进的空格数为4:set shiftwidth=4

使用插件
要配置好看的vim,原生的配置可能功能不全,可以选择安装插件来完善配置。保证用户是你要配置的用户,接下来:

  • 安装TagList插件:下载 taglist_xx.zip,解压完成,将解压出来的 doc 的内容放到 ~/.vim/doc,将解压出来的 plugin 下的内容拷贝到 ~/.vim/plugin

  • 在 ~/.vimrc 中添加:  let Tlist_Show_One_File=1  let Tlist_Exit_OnlyWindow=1  let Tlist_Use_Right_Window=1

  • 安装文件浏览器和窗口管理器插件:WinManager

  • 下载 winmanager.zip,2.X版本以上的

  • 解压 winmanager.zip,将解压出来的 doc 的内容放到 ~/.vim/doc,将解压出来的 plugin 下的内容拷贝到 ~/.vim/plugin

  • 在 ~/.vimrc 中添加: let g:winManagerWindowLayout='FileExplorer|TagList'nmap wm :WMToggle<cr>

  • 然后重启vim,打开 ~/XXX.c 或 ~/XXX.cpp,在normal状态下输入 "wm",你将看到上图的效果。更具体移步:手把手教你把Vim改装成一个IDE编程环境(图文)_vim 打造成 ide-CSDN博客,其他手册,请执行 vimtutor 命令。

3.5 编辑器的发展

除了传统Vim,Neovim在2026年已成为更推荐的选择。作为Vim的现代分支,Neovim从2014年开始重构代码库,具有以下核心优势:

  • Lua配置:相比Vimscript,Lua更快、更清晰、更现代化,且在游戏引擎、嵌入式系统和Redis脚本等生产环境中被广泛使用。

  • 内置LSP客户端:Neovim原生支持Language Server Protocol,无需像Vim那样依赖Node.js运行时的重量级插件来获得LSP特性。

  • Treesitter集成:原生语法树解析,实现更精准的语法高亮和代码导航。

  • 异步I/O优先支持:插件执行不阻塞编辑体验。

特性对比:

特性

Vim

Neovim

配置语言

Vimscript

Lua(兼容Vimscript)

内置LSP

❌ 需插件

✅ 原生LSP客户端

Treesitter

异步I/O

部分(vim 8+)

✅ 优先支持

预安装

✅ 大多数Unix系统

❌ 需手动安装

开发活跃度

较慢

✅ 非常活跃

对于2026年新搭建开发环境的用户,Neovim是更好的选择——其现代架构、Lua配置和丰富的插件生态使其能够真正替代IDE;而Vim仍然适用于服务器管理和安装受限的环境。

四、编译器gcc/g++

4.1 GCC的历史与发展

GCC(GNU Compiler Collection)最初于1987年3月22日由理查德·斯托曼(Richard Stallman)发布,当时仅支持C语言。同年12月,GCC开始支持C++语言。此后,随着支持语言的增多,其含义从“GNU C Compiler”演变为“GNU Compiler Collection”。

GCC主要版本的发展历程:

版本 发布年份 主要里程碑
GCC 1.0 1987 初始版本,仅支持C语言
GCC 2.0 1992 开始支持C++
GCC 2.95 1999 经典稳定版,长期作为Unix标配
GCC 3.0 2001 引入IA-64架构支持,改进C++标准兼容性
GCC 4.0 2005 重写优化架构,引入Tree SSA框架,优化能力显著提升
GCC 4.8 2013 第一个完整支持C++11的版本,成为RHEL 7/CentOS 7默认编译器
GCC 5.1 2015 默认标准改为C++14,引入新libstdc++ ABI
GCC 8.1 2018 开始实验性支持C++2a(后为C++20)特性
GCC 14 2024 深度优化后端、增强静态分析能力、原生支持最新ARM和RISC-V架构
GCC 16 2026(预计) 集成AMD Zen 6架构优化、默认启用C++20、改进LTO分区数量

4.2 编译的四个阶段

GCC将源码转换为可执行程序需要经过四个阶段:

  1. 预处理(Preprocessing) :宏替换、去注释、条件编译、头文件展开

  2. 编译(Compilation) :将预处理后的代码翻译为汇编语言,检查语法错误

  3. 汇编(Assembly) :将汇编代码转换为机器可识别的二进制目标代码(.o文件)

  4. 链接(Linking) :将多个目标文件和库文件合并为可执行程序

各阶段的命令:

# 预处理:生成.i文件
gcc -E hello.c -o hello.i

# 编译:生成.s汇编文件
gcc -S hello.i -o hello.s

# 汇编:生成.o目标文件
gcc -c hello.s -o hello.o

# 链接:生成可执行文件
gcc hello.o -o hello

# 一步到位(默认动态链接)
gcc hello.c -o hello

4.2.1 预处理阶段

核心作用:处理所有以#开头的预处理指令,包括宏替换、头文件展开、条件编译、去注释。gcc 参数-E,让 gcc 在预处理结束后停止编译,不进行后续阶段。输出文件.i后缀的预处理文件。

实操命令:

gcc -E hello.c -o hello.i

查看hello.i文件可以看到:

  • 宏定义NUM被全部替换为 100

  • stdio.h头文件的内容被完整展开到文件中

  • 代码中的注释被全部删除

4.2.2 编译阶段

核心作用:检查代码的语法规范性、是否有语法错误,无误后将预处理后的 C 代码翻译成汇编语言。gcc 参数-S,让 gcc 在编译结束后停止,不进行汇编。输出文件.s后缀的汇编文件。

实操命令

gcc -S hello.i -o hello.s

查看hello.s文件,即可看到生成的 x86-64 架构的汇编代码,这是 C 代码到机器码的关键过渡。

4.2.3 汇编阶段

核心作用:将汇编代码翻译成 CPU 可识别的二进制机器指令,生成目标文件。gcc 参数-c,让 gcc 在汇编结束后停止,不进行链接。输出文件.o后缀的二进制目标文件(Windows 下为.obj)。

实操命令:

gcc -c hello.s -o hello.o

此时的hello.o是二进制文件,无法直接用 cat 查看,可通过objdump工具反汇编查看内容。注意:该文件虽然是机器码,但无法直接执行,因为缺少链接步骤。

4.2.4 链接阶段

核心作用:将多个目标文件、系统库文件链接在一起,解决函数调用、符号引用问题,最终生成可执行文件。gcc 参数:无专用参数,直接输入目标文件即可完成链接。输出文件:无后缀的可执行文件。

实操命令:

gcc hello.o -o hello

生成可执行文件后,通过./hello即可运行程序,输出Hello Linux! NUM=100

4.3 静态链接 vs 动态链接

在链接阶段,程序所需的库函数有两种链接方式【PDF p17-p18】:

静态链接:编译时将库文件的代码全部复制到可执行文件中。

  • 优点:程序独立性强,启动速度快,不受系统库更新影响

  • 缺点:可执行文件体积大(每个程序都有库的副本),更新库时需重新编译

  • Linux后缀:.a(Windows:.lib

动态链接:编译时只记录库的引用信息,运行时由系统加载共享库。

  • 优点:可执行文件体积小,多个程序可共享同一份库文件,库更新方便

  • 缺点:启动稍慢(需加载库),依赖系统库环境

  • Linux后缀:.so(Windows:.dll

特性 静态链接 动态链接(gcc 默认方式)
链接时机 编译链接阶段,将库文件代码全部打包到可执行文件中 程序运行时,由系统动态加载器将库文件加载到内存,可执行文件仅保留函数引用
库文件后缀 Linux:.a;Windows:.lib Linux:.so;Windows:.dll
可执行文件大小 大,包含所有依赖的库代码 小,仅保留符号引用
内存占用 高,多个程序使用同一个库,会在内存中生成多份副本 低,多个程序共享同一份库文件,内存中仅一份副本
程序运行速度 快,运行时无需加载库 略慢,运行时需要动态加载库
更新维护 困难,库文件修改后,程序必须重新编译链接 简单,库文件更新后,程序无需重新编译
可移植性 强,可执行文件不依赖外部库,可直接在同架构系统运行 弱,运行环境必须安装程序依赖的动态库,否则无法运行

4.4 常用编译选项

选项 功能
-E 只进行预处理
-S 生成汇编代码,不汇编和链接
-c 编译为目标代码,不链接
-o <file> 指定输出文件名
-g 生成调试信息(用于gdb调试)
-O0/-O1/-O2/-O3 优化级别(O0无优化,O3最高)
-Wall 生成所有警告信息
-w 不生成警告信息
-static 静态链接
-shared 生成共享库(动态库)
-I<dir> 指定头文件搜索路径
-L<dir> 指定库文件搜索路径
-l<lib> 链接指定库

4.5 前沿动态

2026年,GCC开发已进入第16个大版本。一个值得关注的趋势是“硬件未发,软件先行” 的策略——AMD的Zen 6架构支持已在GCC 16.1中提前集成,使发行版和开发者能够在处理器上市前就准备好优化的工具链,有效减少软硬件兼容性问题。这一做法已成为现代处理器发布的标准范式。

五、 自动化构建工具:Makefile 编写与核心思想

当项目中存在多个源文件时,每次修改代码都手动执行 gcc 编译命令,不仅繁琐,还会重复编译未修改的文件,效率极低。Makefile 就是用来解决这个问题的,它定义了项目的编译规则,实现自动化编译,一次编写,终身受益

5.1 核心概念

  • make:是一个命令工具,用来解释 Makefile 中的规则,执行编译指令

  • Makefile:是一个文件,定义了项目的依赖关系和编译规则,二者搭配完成项目自动化构建

会不会写 Makefile,从侧面反映了一个开发者是否具备完成大型工程的能力,因为大型项目的源码文件成千上万,必须通过 Makefile 来管理编译流程。

5.2 Makefile 核心规则

Makefile 的核心语法由依赖关系依赖方法组成,基础格式如下:

目标文件: 依赖文件
	依赖方法(必须以Tab键缩进,不能用空格)
  • 依赖关系:说明目标文件是由哪些文件生成的

  • 依赖方法:生成目标文件需要执行的命令

最简 Makefile 示例

基于前文的hello.c,编写最简单的 Makefile:

# 终极目标:生成hello可执行文件
hello: hello.c
	gcc -o hello hello.c

# 伪目标:清理编译生成的文件
.PHONY: clean
clean:
	rm -f hello

使用方式:

# 执行编译,相当于执行gcc -o hello hello.c
make

# 执行清理,相当于执行rm -f hello
make clean

5.3 伪目标.PHONY 的作用

上面的示例中,clean.PHONY修饰,称为伪目标,它的核心特性是:总是被执行

为什么需要伪目标?

  • 对于hello这个目标,make 会对比hellohello.c的修改时间,只有hello.chello新,才会执行依赖方法,避免重复编译。

  • 对于clean,它没有依赖文件,也不会生成对应的 clean 文件,如果不加.PHONY,当当前目录下存在一个名为clean的文件时,make clean会提示「clean is up to date」,无法执行清理命令。

  • 加了.PHONY后,make 会忽略文件存在性,每次都执行对应的依赖方法。

5.4 Makefile 的工作原理

make 执行时,会按照以下逻辑工作:

  1. 在当前目录下查找名为Makefilemakefile的文件

  2. 找到文件中的第一个目标文件(终极目标),作为最终要生成的文件

  3. 如果目标文件不存在,或依赖文件的修改时间比目标文件新,就执行对应的依赖方法生成目标文件

  4. 如果依赖文件不存在,就会在 Makefile 中查找生成该依赖文件的规则,递归执行上述过程(类似栈的入栈和出栈)

  5. 直到所有依赖文件都存在,最终生成终极目标文件

我们可以把编译的四个阶段拆解,写出更详细的 Makefile,理解这个推导过程:

# 终极目标
hello: hello.o
	gcc hello.o -o hello

# 生成hello.o
hello.o: hello.s
	gcc -c hello.s -o hello.o

# 生成hello.s
hello.s: hello.i
	gcc -S hello.i -o hello.s

# 生成hello.i
hello.i: hello.c
	gcc -E hello.c -o hello.i

# 清理所有中间文件和可执行文件
.PHONY: clean
clean:
	rm -f *.i *.s *.o hello

执行make时,make 会从hello开始,递归查找依赖,依次执行hello.ihello.shello.ohello的生成命令,和我们手动执行的顺序完全一致。

5.5 通用 Makefile 模板

在实际项目中,我们不会手动写每个文件的编译规则,而是通过变量、自动变量、函数,编写通用的 Makefile,适配多文件工程。

核心高级语法
  1. 变量定义:用=定义变量,$(变量名)引用变量

  2. 自动变量

    • $@:代表当前规则的目标文件名

    • $^:代表当前规则的所有依赖文件列表

    • $<:代表当前规则的第一个依赖文件

  3. 内置函数

    • wildcard *.c:获取当前目录下所有.c后缀的源文件

    • patsubst %.c,%.o,$(SRC):将 SRC 中所有.c后缀替换为.o后缀

通用多文件工程 Makefile 模板

以进度条程序为例,工程包含main.cprocess.c两个源文件,通用 Makefile 如下:

# 1. 定义变量
# 最终生成的可执行文件名
BIN = processbar
# 编译器
CC = gcc
# 编译选项:开启所有警告、生成调试信息
CFLAGS = -Wall -g
# 链接选项
LDFLAGS = 
# 删除命令
RM = rm -f

# 2. 自动获取源文件和目标文件
# 获取当前目录所有.c文件
SRC = $(wildcard *.c)
# 将所有.c替换为.o,生成目标文件列表
OBJ = $(patsubst %.c,%.o,$(SRC))

# 3. 终极目标规则
$(BIN): $(OBJ)
	$(CC) $(LDFLAGS) -o $@ $^
	@echo "编译完成,生成可执行文件:$@"

# 4. 模式规则:所有.c文件生成对应的.o文件
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
	@echo "编译文件:$<"

# 5. 伪目标:清理
.PHONY: clean
clean:
	$(RM) $(OBJ) $(BIN)
	@echo "清理完成"

# 6. 伪目标:查看变量值,调试用
.PHONY: debug
debug:
	@echo "源文件列表:$(SRC)"
	@echo "目标文件列表:$(OBJ)"

这个模板可以直接适配任意多文件 C 语言工程,无需修改核心规则,只需修改BIN可执行文件名即可,是实际开发中最常用的写法。

六、实战项目:Linux 下进度条程序完整实现

前面我们学习了 vim、gcc、Makefile,现在通过一个进度条程序实战,串联所有工具,同时理解 Linux 下的行缓冲区、回车与换行的核心概念。

6.1 前置核心知识

6.1.1 回车与换行的区别

很多人会混淆\r\n,二者的本质完全不同:

  • 回车\r:将光标移动到当前行的行首,不换行

  • 换行\n:将光标移动到下一行的行首(Windows 中\r\n才是换行,Linux 中\n包含了回车 + 换行)

进度条的核心原理,就是利用\r,每次都将光标回到行首,覆盖上一次的内容,实现进度条的动态刷新效果。

6.1.2 行缓冲区机制

先看三个代码示例,思考运行结果的差异:

// 示例1
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello Linux!\n"); // 带\n
    sleep(3);
    return 0;
}
// 运行结果:先打印hello Linux!,休眠3秒,程序退出
// 示例2
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello Linux!"); // 不带\n
    sleep(3);
    return 0;
}
// 运行结果:先休眠3秒,再打印hello Linux!,程序退出
// 示例3
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello Linux!"); // 不带\n
    fflush(stdout); // 强制刷新缓冲区
    sleep(3);
    return 0;
}
// 运行结果:先打印hello Linux!,休眠3秒,程序退出

核心原因:Linux 下的 printf 输出,会先写入行缓冲区,满足以下条件之一才会刷新缓冲区,输出到屏幕:

  1. 缓冲区满了

  2. 遇到换行符\n

  3. 手动调用fflush(stdout)强制刷新

  4. 程序退出时

示例 2 中,没有\n,也没有强制刷新,所以内容一直在缓冲区,直到程序退出才输出;示例 3 中,通过fflush(stdout)强制刷新,就能立即输出内容。

进度条的第二个核心原理:每次输出进度条后,必须手动刷新缓冲区,否则无法实现动态刷新效果。

6.2 倒计时程序

基于上面的两个知识点,先实现一个简单的倒计时程序,理解\rfflush的使用:

// countdown.c
#include <stdio.h>
#include <unistd.h>

int main()
{
    int i = 10;
    while(i >= 0)
    {
        // %-2d:左对齐,占2个字符宽度,避免数字位数变化导致显示异常
        printf("%-2d\r", i);
        // 强制刷新缓冲区
        fflush(stdout);
        // 休眠1秒
        sleep(1);
        i--;
    }
    printf("\n");
    return 0;
}

编译运行:

gcc countdown.c -o countdown
./countdown

可以看到,终端中数字从 10 开始,每秒减 1,在同一行动态刷新,实现了倒计时效果。

6.3 进度条程序完整实现

我们将程序分为三个文件:

  • process.h:头文件,函数声明

  • process.c:源文件,进度条功能实现

  • main.c:主函数,调用进度条功能

  • Makefile:自动化编译

6.3.1 头文件 process.h
#pragma once
#include <stdio.h>

// 版本1:基础进度条,自动循环完成
void process_v1();

// 版本2:通用进度条,支持传入总大小和当前进度,适配下载等场景
void FlushProcess(double total, double current);

6.3.2 功能实现 process.c
#include "process.h"
#include <string.h>
#include <unistd.h>

// 进度条长度
#define NUM 101
// 进度条样式,可替换为#、>等
#define STYLE '='

// 版本1:基础进度条
void process_v1()
{
    // 缓冲区,存储进度条字符
    char buffer[NUM];
    // 初始化缓冲区为全0
    memset(buffer, 0, sizeof(buffer));
    // 旋转光标,实现加载动画 |/-\
    const char *lable = "|/-\\";
    int len = strlen(lable);

    int cnt = 0;
    while(cnt <= 100)
    {
        // 格式化输出:[进度条][百分比][加载动画]
        // %-100s:左对齐,占100个字符宽度,保证进度条长度固定
        printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt%len]);
        // 强制刷新缓冲区
        fflush(stdout);
        // 填充进度条字符
        buffer[cnt] = STYLE;
        cnt++;
        // 休眠50ms,控制进度条速度
        usleep(50000);
    }
    // 进度条完成,换行
    printf("\n");
}

// 版本2:通用进度条
void FlushProcess(double total, double current)
{
    char buffer[NUM];
    memset(buffer, 0, sizeof(buffer));
    const char *lable = "|/-\\";
    int len = strlen(lable);
    // 静态变量,记录加载动画的下标
    static int cnt = 0;

    // 计算进度百分比
    int num = (int)(current * 100 / total);
    // 填充进度条
    for(int i = 0; i < num; i++)
    {
        buffer[i] = STYLE;
    }

    double rate = current / total;
    cnt %= len;
    // 输出进度条,保留1位小数百分比
    printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt]);
    cnt++;
    fflush(stdout);
}

6.3.3 主函数 main.c
#include "process.h"
#include <unistd.h>

// 模拟文件下载总大小,单位MB
#define TOTAL_SIZE 1024.0
// 模拟下载速度,单位MB/次
#define DOWNLOAD_SPEED 2.0

// 模拟下载函数
void DownLoad()
{
    double current = 0.0;
    while(current <= TOTAL_SIZE)
    {
        // 刷新进度条
        FlushProcess(TOTAL_SIZE, current);
        // 模拟下载耗时
        usleep(20000);
        // 累计下载大小
        current += DOWNLOAD_SPEED;
    }
    // 下载完成,换行提示
    printf("\n下载完成!总计下载:%.2lf MB\n", current);
}

int main()
{
    printf("===== 基础进度条演示 =====\n");
    process_v1();

    printf("\n===== 下载进度条演示 =====\n");
    DownLoad();

    return 0;
}

6.3.4 Makefile

直接使用前文的通用 Makefile 模板:

BIN = processbar
CC = gcc
CFLAGS = -Wall -g
RM = rm -f

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c,%.o,$(SRC))

$(BIN): $(OBJ)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	$(RM) $(OBJ) $(BIN)

6.4 编译运行

  1. 用 vim 创建上述 4 个文件,保存在同一目录下

  2. 执行make命令,完成编译,生成processbar可执行文件

  3. 执行./processbar,即可看到进度条的动态效果

  4. 执行make clean,清理编译生成的文件

扩展优化:可以通过 ANSI 转义序列给进度条添加颜色,例如printf("\033[32m[%-100s]\033[0m", buffer),即可实现绿色进度条,提升视觉效果。

七、版本控制

在开发过程中,我们经常需要修改代码、回滚版本、多人协同开发,手动复制文件备份版本的方式,不仅效率低,还容易出错。Git 是目前最主流的分布式版本控制系统,能够完美解决代码版本管理、多人协同开发的问题

7.1 Git 核心概念与安装

7.1.1 什么是 Git

Git 是 Linus Torvalds(Linux 内核之父)在 2005 年开发的分布式版本控制系统,核心特点:

  • 分布式:每个开发者的本地都有完整的版本仓库,断网也能提交版本

  • 速度快:处理大型项目时,性能远超集中式版本控制系统(如 SVN)

  • 强大的分支管理:支持非线性开发,成千上万个并行分支也能高效管理

7.1.2 Git 安装
# CentOS安装
sudo yum install -y git

# Ubuntu安装
sudo apt install -y git

# 验证安装是否成功
git --version

7.1.3 Git 首次使用配置

安装完成后,必须配置用户名和邮箱,用于标识提交者信息,否则无法提交代码:

# 配置全局用户名
git config --global user.name "你的GitHub用户名"
# 配置全局邮箱(GitHub注册邮箱)
git config --global user.email "你的GitHub邮箱"

# 查看配置是否生效
git config --list

7.2 Git 四大工作区域与核心工作流

Git 的核心操作,都是围绕四大工作区域展开的,理解这个工作流,就能掌握 Git 的核心逻辑:

工作区域 作用
工作区 我们编写代码的目录,能看到的所有文件都在工作区
暂存区 临时存放即将提交的文件修改,用于批量提交
本地仓库 保存在本地的所有版本记录,所有提交的版本都永久保存在这里
远程仓库 托管在远程服务器(如 GitHub)的仓库,用于备份和多人协同

7.3 Git 核心操作:从创建仓库到推送到 GitHub

7.3.1 第一步:在 GitHub 创建远程仓库
  1. 登录 GitHub 账号,点击右上角+号,选择New repository

  2. 填写仓库名称(Repository name),例如linux-processbar

  3. 选择仓库权限:Public(公开)/ Private(私有)

  4. 无需勾选Initialize this repository with a README,点击Create repository

  5. 创建完成后,复制仓库的 HTTPS 链接,例如https://github.com/你的用户名/linux-processbar.git

7.3.2 第二步:本地仓库初始化与提交

进入我们的进度条程序目录,执行以下命令:

# 1. 初始化本地Git仓库,执行后目录会生成.git隐藏文件夹
git init

# 2. 将工作区的所有文件添加到暂存区
git add .
# 也可以指定单个文件:git add main.c process.c process.h Makefile

# 3. 将暂存区的内容提交到本地仓库,-m后面是提交日志,必须填写,描述本次修改内容
git commit -m "feat: 完成Linux进度条程序基础功能与Makefile编写"

7.3.3 第三步:关联远程仓库并推送代码
# 1. 关联远程仓库,origin是远程仓库的别名,后面是你复制的GitHub仓库链接
git remote add origin https://github.com/你的用户名/linux-processbar.git

# 2. 查看远程仓库关联情况
git remote -v

# 3. 将本地仓库的代码推送到远程GitHub仓库
git push -u origin main
# 注:GitHub默认主分支为main,旧版本为master,根据实际情况修改

执行完成后,刷新 GitHub 仓库页面,就能看到我们的代码已经成功上传到 GitHub 了。

7.4 Git 高频常用命令

命令 功能
git clone 远程仓库链接 将远程仓库克隆到本地
git status 查看工作区、暂存区的文件修改状态
git log 查看提交历史记录,包括提交人、时间、日志、commit id
git pull 拉取远程仓库的最新代码,合并到本地
git reset --hard commit_id 回滚到指定的 commit 版本
git checkout -b 分支名 创建并切换到新分支
git branch 查看所有本地分支
git merge 分支名 将指定分支合并到当前分支

7.5 实用补充

.gitignore 文件:在仓库根目录创建.gitignore文件,填写不需要 Git 管理的文件,例如编译生成的.o文件、可执行文件、日志文件等,示例:

# 忽略所有.o目标文件
*.o
# 忽略可执行文件
processbar
# 忽略日志文件
*.log
# 忽略临时文件
*.tmp

SSH 免密配置:每次 push 都需要输入用户名和密码,可通过配置 SSH 密钥实现免密提交,核心步骤:

  • 本地生成 SSH 密钥对:ssh-keygen -t rsa,一路回车即可

  • 查看公钥:cat ~/.ssh/id_rsa.pub,复制全部内容

  • 进入 GitHub → Settings → SSH and GPG keys → New SSH key,粘贴公钥并保存

  • 远程仓库链接更换为 SSH 格式,即可实现免密 push/pull

八、程序调试利器:gdb 调试核心技巧与实战

程序开发中,调试是必不可少的环节,Linux 下没有图形化的 IDE,gdb(GNU Debugger)是最主流的命令行调试工具,能够精准控制程序的执行、查看变量、定位 bug

8.1 gdb 调试前置条件

Linux 下 gcc/g++ 编译生成的程序,默认是release 发布版本,该版本会去掉调试信息、进行代码优化,无法被 gdb 调试。

要使用 gdb 调试,必须在编译时添加-g参数,生成debug 调试版本,该版本会在可执行文件中加入调试信息、符号表,支持 gdb 调试。

# 生成debug版本的可执行文件,必须加-g参数
gcc mycmd.c -o mycmd -g -Wall

# 验证是否包含调试信息
file mycmd
# 输出中包含 with debug_info,说明可以正常调试

8.2 gdb 核心命令分类大全

启动 gdb:gdb 可执行文件名,例如gdb mycmd退出 gdb:在 gdb 交互界面输入quitq,或按ctrl+d

8.2.1 代码查看命令
命令 简写 功能
list 行号 l 行号 从指定行号开始,显示 10 行源代码
list 函数名 l 函数名 显示指定函数的源代码
list 文件名:行号 l 文件名:行号 显示指定文件的指定行代码(多文件工程)
list l 从上一次显示的位置,继续显示 10 行代码

8.2.2 断点管理命令
命令 简写 功能
break 行号 b 行号 在指定行号设置断点
break 函数名 b 函数名 在指定函数的入口处设置断点
break 文件名:行号 b 文件名:行号 在指定文件的指定行设置断点
break 行号 if 条件 b 行号 if 条件 设置条件断点,只有满足条件时才会触发断点
info breakpoints info b 查看所有断点的信息,包括断点编号、位置、触发次数
condition 断点编号 条件 - 给已存在的断点添加触发条件
delete breakpoints 断点编号 d 断点编号 删除指定编号的断点
delete breakpoints d 删除所有断点
disable breakpoints 断点编号 - 禁用指定断点(断点保留,不触发)
enable breakpoints 断点编号 - 启用指定断点

8.2.3 程序执行控制命令
命令 简写 功能
run r 从头开始运行程序,直到遇到断点或程序结束
continue c 从当前断点位置,继续运行程序,直到下一个断点或程序结束
next n 单步执行,不进入函数内部(逐过程,对应 IDE 的 F10)
step s 单步执行,进入函数内部(逐语句,对应 IDE 的 F11)
finish - 执行到当前函数返回,然后暂停
until 行号 u 行号 执行到指定行号,然后暂停
ctrl+c - 暂停正在运行的程序

8.2.4 数据查看与修改命令
命令 简写 功能
print 变量名 / 表达式 p 变量名 打印变量 / 表达式的值
print & 变量名 p & 变量名 打印变量的内存地址
set var 变量名 = 值 - 修改变量的值,用于调试时验证逻辑
display 变量名 - 跟踪显示指定变量,每次程序暂停都会自动打印该变量的值
undisplay 跟踪编号 - 取消指定变量的跟踪显示
info locals - 查看当前栈帧中所有局部变量的值
watch 变量名 / 表达式 - 设置监视点,当变量 / 表达式的值发生变化时,程序暂停并通知
info watchpoints - 查看所有监视点的信息

8.2.5 栈信息查看命令
命令 简写 功能
backtrace bt 查看当前函数调用栈,定位程序崩溃位置
frame 栈帧编号 f 栈帧编号 切换到指定的栈帧,查看对应函数的变量

8.3 gdb 调试实战

下面以一个求和程序为例,完整演示 gdb 调试的全流程,代码如下:

// mycmd.c
#include <stdio.h>

// 求s到e的累加和
int Sum(int s, int e)
{
    int result = 0;
    for(int i = s; i <= e; i++)
    {
        result += i;
    }
    return result;
}

int main()
{
    int start = 1;
    int end = 100;
    printf("I will begin\n");
    int n = Sum(start, end);
    printf("running done, result is: [%d-%d]=%d\n", start, end, n);
    return 0;
}

步骤 1:编译 debug 版本程序
gcc mycmd.c -o mycmd -g -Wall
步骤 2:启动 gdb
gdb mycmd

启动后进入 gdb 交互界面,所有后续命令都在该界面执行。

步骤 3:查看源代码
# 查看main函数代码
l main
# 查看Sum函数代码
l Sum
步骤 4:设置断点
# 在main函数调用Sum的行设置断点(假设是第20行)
b 20
# 在Sum函数的循环内设置条件断点,当i=30时触发
b 9 if i == 30
# 查看所有断点
info b
步骤 5:运行程序
# 运行程序,会在第20行的断点处暂停
r
步骤 6:单步调试,进入函数
# 单步进入Sum函数内部
s
# 查看当前局部变量的值
p s
p e
p result
# 单步执行,查看循环过程
n
# 跟踪i和result变量,每次暂停都自动打印
display i
display result
步骤 7:使用监视点,监控变量变化
# 设置监视点,当result的值发生变化时,程序暂停
watch result
# 继续运行程序,每次result变化都会暂停
c
步骤 8:修改变量值,验证逻辑
# 暂停后,修改end的值为50,查看求和结果变化
set var end=50
p end
# 继续执行
c
步骤 9:查看函数调用栈,退出 gdb
# 查看函数调用栈
bt
# 退出gdb
q

8.4 进阶技巧

  1. cgdb 工具:gdb 的增强版,分屏显示代码和调试命令,操作和 gdb 完全一致,使用体验更好,安装命令:

    # CentOS
    sudo yum install -y cgdb
    # Ubuntu
    sudo apt install -y cgdb
    

    使用方式:cgdb 可执行文件名

  2. core 文件调试段错误:程序崩溃报 Segmentation Fault(段错误)时,可通过 core 文件定位崩溃位置,核心步骤:

    # 1. 开启core文件生成
    ulimit -c unlimited
    # 2. 运行崩溃的程序,会生成core文件
    ./崩溃的程序
    # 3. gdb加载core文件,定位崩溃位置
    gdb 可执行文件名 core
    # 4. 查看崩溃时的函数调用栈
    bt
    

总结

本文系统梳理了Linux开发中必须掌握的七大基础工具:包管理器(yum/apt) 解决软件获取与依赖管理问题,vim/Neovim 是代码编辑的核心生产力工具,gcc/g++ 将源码转化为可执行程序,Makefile/CMake 实现自动化构建,进度条程序 是缓冲区与终端I/O的实践案例,Git 管理代码版本与团队协作,gdb 定位程序问题的根源。

这七个工具构成了Linux C/C++开发的完整工具链,掌握它们是成为一名合格Linux开发者的必经之路。其中,每个工具都有庞大的知识体系——本文只是一个入门指南。建议大家在实践中不断深入,遇到问题时查阅官方文档和社区资源,逐步形成自己的工具使用习惯和工作流。

推荐进一步学习资源:

  • 包管理器:各发行版官方Wiki(Fedora Docs、Debian Wiki)

  • vim/Neovim::help内置文档 + Neovim官方文档

  • gcc/g++:GNU GCC官方手册(man gcc

  • Makefile/CMake:GNU Make Manual + CMake官方教程

  • Git:Pro Git电子书 + Git官方文档

  • gdb:Debugging with GDB官方手册

希望这篇文章能帮助你在Linux开发的道路上走得更稳、更远!

参考资料

Linux系统各发行版软件包管理器底层原理及兼容性对比分析-linux运维-PHP中文网

【Linux入门篇】Ubuntu和CentOS包管理不一样?apt与yum对比实操,看完再也不混淆_yum和apt-CSDN博客

深入 2026:yum 与 apt-get 的演进、AI 原生管理及企业级实战指南 - 豆丁博客

YUM 与 APT:有什么区别?终端实用的2026指南——TheLinuxCode

ubuntu yum与apt的区别 - 问答 - 亿速云

极客日志 - 开发者社区、技术博客、AI 编程与在线工具

VSCode vs Vim/Neovim:现代编辑器与上古神器的碰撞-VSCode-PHP中文网

VSCode vs Vim/Neovim:现代编辑器与上古神器的碰撞-VSCode-PHP中文网

为什么顶级嵌入式团队已全面转向GCC 14?揭秘其对LTO和链接时优化的突破支持-CSDN博客

Linux C/C++构建工具的优缺点对比-CSDN博客

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐