前言
在Linux环境下进行C/C++开发,掌握基础开发工具是必不可少的技能。本文将系统介绍Linux下常用的开发工具,包括软件包管理器yum/apt、编辑器vim、编译器gcc/g++、自动化构建工具make/Makefile、版本控制器git以及调试器gdb。
一、软件包管理器
1.1 什么是软件包管理器
在Linux下安装软件,传统方式是下载源代码手动编译,但这样太繁琐。于是有人将常用软件提前编译好做成软件包,放在服务器上,通过包管理器可以方便地获取和安装。
-
yum:CentOS/RHEL系列使用的包管理器
-
apt:Ubuntu/Debian系列使用的包管理器
1.2 查看软件包
cpp
# CentOS:查看lrzsz软件包
yum list | grep lrzsz
# Ubuntu:搜索软件包
apt search lrzsz
# Ubuntu:查看软件包详细信息
apt show lrzsz
1.3 安装软件
1.31替换 CentOS 7 源为国内镜像(关键)
CentOS 7 官方源已经停止维护,原来的 mirrorlist.centos.org 已经失效,需要替换为阿里云 / 清华镜像源:
-
备份原有源文件:
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak -
下载阿里云 CentOS 7 镜像源:
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo -
清除缓存并更新:
yum clean all yum makecache yum update -y
完成上面三步后,重新执行安装命令:
cpp
# CentOS
sudo yum install -y lrzsz
# Ubuntu
sudo apt install -y lrzsz

安装完成后,直接输入:

终端里就会出现一辆跑过的小火车 🚂

sl安装在哪里嘞

为什么是 /usr/bin/?
Linux 里,用户可执行的系统命令,默认都放在 PATH 环境变量包含的目录里,比如:
/usr/bin/:绝大多数用户命令(ls、cd、sl都在这里)/usr/local/bin/:你手动编译安装的程序,默认放这里
yum 安装的软件,都会自动把可执行文件放到 /usr/bin/,所以你直接敲 sl 就能运行,不用写完整路径 /usr/bin/sl。
PATH 里存了一堆文件夹列表,系统规则:
当你只输入一个文件名(比如 sl、ls)系统会 自动依次去 PATH 里的所有文件夹里查找找到这个程序,就直接运行
1.4 卸载软件
cpp
# CentOS
sudo yum remove -y lrzsz
# Ubuntu
sudo apt remove -y lrzsz
1.5 配置国内镜像源
国内常用的镜像源:
| 镜像站 | 链接 |
|---|---|
| 阿里云 | https://developer.aliyun.com/mirror/ |
| 清华大学 | https://mirrors.tuna.tsinghua.edu.cn/ |
| 中科大 | http://mirrors.ustc.edu.cn/ |
二、编辑器Vim
2.1 三种基本模式
Vim有三种基本模式:
-
命令模式(Normal mode) :控制光标移动、删除、复制等
-
插入模式(Insert mode) :输入文字
-
底行模式(Last line mode) :保存文件、退出、查找替换
2.2 常用操作
| 操作 | 命令 |
|---|---|
| 进入插入模式 | i(光标前)/ a(光标后)/ o(新一行) |
| 返回命令模式 | ESC |
| 进入底行模式 | : |
| 保存文件 | :w |
| 保存并退出 | :wq |
| 强制退出 | :q! |
2.3 光标移动
cpp
h # 左移
j # 下移
k # 上移
l # 右移
gg # 跳到文件开头
G # 跳到文件末尾
w #单词为单词向后移动
b #单词为单位向前移动
2.4 删除与复制
cpp
x # 删除光标处字符
dd # 删除整行
yy # 复制整行
p # 粘贴
u # 撤销
Ctrl+r # 恢复撤销
shift+x #删除关标右边的字符
多行操作 :前面加数字
3 yy 复制当前行和下面的两行
2.5 简单配置(~/.vimrc)
cpp
syntax on " 语法高亮
set nu " 显示行号
set shiftwidth=4 " 缩进4空格
测试:


三、编译器gcc/g++
3.1 编译四个阶段

3.2 完整编译示例
创建hello.c:
cpp
#include <stdio.h>
#define NUM 100
int main()
{
printf("Hello, Linux!\n");
printf("NUM = %d\n", NUM);
return 0;
}
分步编译:
cpp
# 1. 预处理(展开宏、头文件)
gcc -E hello.c -o hello.i
# 2. 编译(生成汇编代码)
gcc -S hello.i -o hello.s
# 3. 汇编(生成目标文件)
gcc -c hello.s -o hello.o
# 4. 链接(生成可执行文件)
gcc hello.o -o hello
# 一步到位编译
gcc hello.c -o hello
# 带调试信息编译(用于gdb调试)
gcc -g hello.c -o hello_debug
测试:


查看预处理的文件test.i。头文件展开了

3.3 静态库与动态库讲解
库就是一堆编译好的目标代码 (.o) 的集合,封装函数 / 接口,不暴露源码,只给别人调用。
分为两种:
- 静态库 :
.a - 动态库 :
.so
静态库(.a)
把多个 .a 目标文件 打包成一个归档文件。
编译链接原理:
编译时:把库中用到的代码直接拷贝进可执行程序
- 运行时不再依赖原库文件
- 程序体积大
- 升级库需要重新编译整个程序
动态库(.so)
位置无关代码 PIC,打包成共享对象。
编译链接原理:
编译时只做符号引用 ,不拷贝代码;运行时才加载动态库到内存,多个程序共享同一份库。
- 程序体积小
- 升级库不用重新编译程序
- 运行必须依赖 .so 文件
静态库 vs 动态库 核心对比:
| 特性 | 静态库 .a |
动态库 .so |
|---|---|---|
| 链接时机 | 编译时链接拷贝 | 运行时加载 |
| 程序体积 | 大 | 小 |
| 运行依赖 | 不依赖原库 | 必须有 .so |
| 库升级 | 需重新编译程序 | 直接替换库即可 |
| 内存占用 | 每个程序独有副本 | 内存中只一份,多进程共享 |
| 扩展名 | .a |
.so |
3.4 静态链接与动态链接
:什么是链接
程序编译分两步:
- 编译 :
.c→.o目标文件(只做语法、生成机器码,不处理外部符号) - 链接 :把多个
.o+ 库文件 合并、解析符号地址,生成可执行文件
静态链接:
链接阶段,把程序用到的库代码,直接复制合并到可执行文件内部 。对应 静态库 .a
核心特点:
- 把库中相关机器码拷贝进最终程序
- 生成的可执行文件体积大
- 运行时不再依赖任何外部库文件,拷贝到别的机器也能直接跑
- 库更新后,必须重新链接编译程序才能生效
- 运行速度略快(不用运行时加载库)
动态链接
链接阶段不拷贝库代码 ,只记录「库名、函数符号」;等到程序运行时 ,再加载动态库 .so 到内存、绑定地址。
核心特点
- 可执行文件体积小
- 运行必须依赖对应的
.so动态库,缺库直接报错 - 多个程序可以共享同一份动态库内存,节省内存
- 库升级直接替换
.so即可,不用重新编译程序 - 运行稍慢一点(多了运行时加载、符号绑定)
cpp
静态链接方式:
gcc test.c -o test1
# 查看动态链接库依赖
ldd hello
# 静态链接方式(需要安装静态库)
gcc -static test.c -o test_static
动态链接方式:(ldd 查看动态链接库依赖)


得出:gcc编译器默认使用动态链接(g++也默认使用动态链接)
静态链接方式测试:

发现没有安装静态库,centos 7 yum 安装静态库:

检查静态库安装完成没有:(发现 .a 文件安装完成)

重新测试:

静态链接与动态链接可执行文件的大小对比:(分析静态链接体积比动态链接体积大的多)

静态链接查找不到动态链接库依赖

四、自动化构建make/Makefile
这是 Linux C 语言开发最核心的工具 ,用来自动编译、自动链接、自动清理,不用每次敲长长的 gcc 命令。
4.1什么是 make /Makefile?
- Makefile :一个文本文件 ,里面写好编译规则、依赖关系、命令。
- make :一个命令工具 ,读取当前目录下的
Makefile,自动执行编译。
一句话作用 :一次写好规则,以后只输一个 make 命令,全自动编译整个项目。
Makefile 核心三要素(必考)
makefile
目标文件 : 依赖文件
<Tab> 执行命令
- 目标 :要生成的文件(可执行文件、.o 文件)
- 依赖:生成目标需要哪些文件(.c、.h、库)
- 命令 :用什么命令生成目标(必须以 Tab 键开头,不能用空格!)
最简单标准 Makefile 模板(背会就能用)
makefile
# 1. 目标:最终生成的可执行文件 myproc
# 2. 依赖:源文件 myproc.c
myproc: myproc.c
# 下面这行必须按 Tab 开头!
gcc -o myproc myproc.c
# 清理命令(伪目标)
.PHONY: clean
clean:
rm -f myproc
怎么使用?
-
编译(生成可执行文件)
make
- make 会自动找 Makefile
- 检查依赖是否更新,只编译修改过的文件
-
运行程序
./myproc
-
清理编译文件
make clean
两个重要知识点
1.伪目标 .PHONY
.PHONY: clean
.PHONY: 目标名是伪目标声明- 作用:让 make 知道这不是文件,是命令
- 目的:防止目录里有同名文件,导致命令不执行
-
工作原理(重点)
-
找到目标文件
-
检查依赖文件是否更新(依赖的.c文件的Modify时间是否更新)
-
时间戳对比 :依赖比目标新 → 重新编译
-
执行命令生成目标
总结(必背)
- Makefile = 编译规则文件
- make = 执行编译的命令
- 格式 :
目标:依赖+Tab 命令 - 特点:自动检查更新,只编译修改的文件
- clean :清理编译产物,配合
.PHONY使用
测试:

make:生成可执行文件

运行(没有换行)

再次make:(发现失败,因为test依赖的源文件test.c没有更新)


4.2 多文件Makefile
假设我们有三个文件:
add.h
cpp
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif
add.c
cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
main.c
cpp
#include <stdio.h>
#include "add.h"
int main()
{
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
Makefile
cpp
# 编译器
CC = gcc
# 最终要生成的可执行文件
TARGET = main
# 所有源文件 .c
SRC = main.c add.c
# 所有目标文件 .o
OBJ = main.o add.o
# 第一步:链接生成可执行文件
$(TARGET): $(OBJ)
$(CC) $(OBJ) -o $@
# 第二步:编译 main.c -> main.o
main.o: main.c add.h
$(CC) -c main.c -o $@
# 第三步:编译 add.c -> add.o
add.o: add.c add.h
$(CC) -c add.c -o $@
# 清理编译产物
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJ)
使用:
cpp
make # 编译
./main # 运行
make clean # 清理
五、第一个Linux程序:进度条
5.1 回车与换行
-
\r:回车(将光标移到行首) -
\n:换行(将光标移到下一行) -
Linux/Unix:
\n同时完成回车+换行 -
Windows:
\r\n才是回车+换行
5.2 行缓冲区演示
5.2.1 标准输入输出流(stdin、stdout、stderr)
cpp
extern FILE *stdin; // 键盘 读
extern FILE *stdout; // 屏幕 普通输出
extern FILE *stderr; // 屏幕 错误输出
- 先看懂每部分
FILE *:文件指针,专门用来操作读、写数据流stdin:标准输入 ,默认就是键盘extern:意思是这个变量别人已经定义好了,我这里只是告诉编译器:有这么个东西,直接用就行,不用自己重新创建
- 一句话大白话
extern FILE *stdin; 就是声明一个代表键盘的文件指针,用来从键盘读数据。
-
小白最需要懂的
-
你不用自己写这行,#include <stdio.h> 里面已经帮你写好了
-
scanf、fgets底层默认都在用 stdin(键盘) -
不用理解复杂原理,只要记住:stdin = 键盘输入
-
缓冲小白口诀
- stdin、stdout:行缓冲 ,碰到
\n才显示 - stderr:无缓冲,立刻马上就显示
5.2.2什么是行缓冲区?
标准输出 stdout(printf 输出的位置)默认是 行缓冲模式
- 缓冲区:一段内存,用来暂存输出内容
- 行缓冲 :遇到换行符
\n才会把内容刷到屏幕上 - 没遇到
\n时,数据一直存在缓冲区里,不显示
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
// 有\n:立即输出
printf("带换行符\n");
sleep(3);
// 无\n:等待3秒后一次性输出
printf("不带换行符");
sleep(3);
// 强制刷新缓冲区
printf("强制刷新");
fflush(stdout);
sleep(3);
return 0;
}

- 带
\n→ 立刻输出 - 不带
\n→ 先存缓存 fflush(stdout)→ 把当前缓冲区所有内容强制输出- 程序结束 → 自动刷新剩余内容
运行顺序(重点)
-
立刻输出:
带换行符 -
等待 3 秒
-
等待 3 秒后,一次性输出:
不带换行符强制刷新 -
再等待 3 秒
-
程序结束
行缓冲的 3 种刷新时机(必背)
- 遇到换行符
\n - 缓冲区满了(默认一般 1024 字节)
- 程序正常结束 (
return 0/exit()) - 手动调用
fflush(stdout)
5.3 倒计时程序
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 10;
while(i >= 0) {
printf("%-2d\r", i); // \r回车,覆盖输出
fflush(stdout); // 强制刷新
i--;
sleep(1);
}
printf("\n");
return 0;
}

5.4 进度条完整代码
process.h
cpp
#pragma once
#include <stdio.h>
void process_v1();
void FlushProcess(double total, double current);
process.c
cpp
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
// 版本1:简单进度条
void process_v1()
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char* lable = "|/-\\";
int len = strlen(lable);
int cnt = 0;
while(cnt <= 100) {
printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt % len]);
fflush(stdout);
buffer[cnt] = STYLE;
cnt++;
usleep(50000); // 50ms
}
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;
printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt]);
cnt++;
fflush(stdout);
}
main.c
cpp
#include "process.h"
#include <unistd.h>
double total = 1024.0;
double speed = 1.0;
void Download()
{
double current = 0;
while(current <= total) {
FlushProcess(total, current);
usleep(30000); // 模拟下载
current += speed;
}
printf("\ndownload %.2lfMB Done!\n", current);
}
int main()
{
// 简单进度条
printf("=== 简单进度条 ===\n");
process_v1();
// 下载模拟
printf("\n=== 文件下载模拟 ===\n");
Download();
return 0;
}
Makefile
cpp
CC = gcc
CFLAGS = -Wall
TARGET = progress
OBJS = main.o process.o
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o:%.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY:clean
clean:
rm -f $(OBJS) $(TARGET)