Linux系统--开发工具

大家好,我们今天来继续学习Linux的内容,今天我们来学习一些在Linux中使用的工具。

目录

基础开发⼯具

[1. 软件包管理器](#1. 软件包管理器)

[1-1 什么是软件包](#1-1 什么是软件包)

[1-2 Linux软件⽣态](#1-2 Linux软件⽣态)

[1-3 yum具体操作](#1-3 yum具体操作)

[1-3-1 查看软件包](#1-3-1 查看软件包)

[1-3-2 安装软件](#1-3-2 安装软件)

[1-3-3 卸载软件](#1-3-3 卸载软件)

[1-4 安装源](#1-4 安装源)

[2. 编辑器Vim](#2. 编辑器Vim)

[2-1 Linux编辑器-vim使⽤](#2-1 Linux编辑器-vim使⽤)

[2-2 vim的基本概念](#2-2 vim的基本概念)

[2-3 vim的基本操作](#2-3 vim的基本操作)

[2-4 vim正常模式命令集](#2-4 vim正常模式命令集)

[2-5 vim末⾏模式命令集](#2-5 vim末⾏模式命令集)

[2-6 vim操作总结](#2-6 vim操作总结)

[2-7 简单vim配置](#2-7 简单vim配置)

[3. 编译器gcc/g++](#3. 编译器gcc/g++)

[3-1 背景知识](#3-1 背景知识)

[3-2 gcc编译选项](#3-2 gcc编译选项)

[3-2-1 预处理(进⾏宏替换)](#3-2-1 预处理(进⾏宏替换))

[3-2-2 编译(⽣成汇编)](#3-2-2 编译(⽣成汇编))

[3-2-3 汇编(⽣成机器可识别代码)](#3-2-3 汇编(⽣成机器可识别代码))

[3-2-4 连接(⽣成可执⾏⽂件或库⽂件)](#3-2-4 连接(⽣成可执⾏⽂件或库⽂件))

[3-3 动态链接和静态链接](#3-3 动态链接和静态链接)

[3-4 静态库和动态库](#3-4 静态库和动态库)

[3-5 gcc其他常⽤选项](#3-5 gcc其他常⽤选项)

[4. ⾃动化构建-make/Makefile](#4. ⾃动化构建-make/Makefile)

[4-1 背景](#4-1 背景)

[4-2 基本使⽤](#4-2 基本使⽤)

[4-3 推导过程](#4-3 推导过程)

[4-4 适度扩展语法](#4-4 适度扩展语法)

[5. Linux第⼀个系统程序−进度条](#5. Linux第⼀个系统程序−进度条)

[5-1 回⻋与换⾏](#5-1 回⻋与换⾏)

[5-2 ⾏缓冲区](#5-2 ⾏缓冲区)

[5-3 进度条代码](#5-3 进度条代码)

[6. 版本控制器Git](#6. 版本控制器Git)

[6-1 版本控制器](#6-1 版本控制器)

[6-2 git 简史](#6-2 git 简史)

[6-3 安装 git](#6-3 安装 git)

[6-4 三板斧](#6-4 三板斧)

[7. 调试器 - gdb/cgdb使⽤](#7. 调试器 - gdb/cgdb使⽤)

[7-1 样例代码](#7-1 样例代码)

[7-2 预备](#7-2 预备)

[7-3 常⻅使用](#7-3 常⻅使用)

[7-4 常⻅技巧](#7-4 常⻅技巧)

[7-4-1 watch](#7-4-1 watch)

[7-4-2 set var确定问题原因](#7-4-2 set var确定问题原因)

[7-4-3 条件断点](#7-4-3 条件断点)


基础开发⼯具

本节⽬标
学习yum⼯具,进⾏软件安装
掌握vim编辑器使⽤,学会vim的简单配置
掌握gcc/g++编译器的使⽤,并了解其过程,原理
掌握简单的Makefile编写,了解其运⾏思想
编写⾃⼰的第⼀个Linux 程序:进度条
学习 git 命令⾏的简单操作, 能够将代码上传到 Github 上
掌握简单gdb使⽤于调试

  1. 软件包管理器

1-1 什么是软件包
1.在Linux下安装软件, ⼀个通常的办法是下载到程序的源代码, 并进⾏编译, 得到可执⾏程序.
2.但是这样太⿇烦了, 于是有些⼈把⼀些常⽤的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在⼀个服务器上, 通过包管理器可以很⽅便的获取到这个编译好的软件包, 直接进⾏安装.
3.软件包和软件包管理器, 就好⽐ "App" 和 "应⽤商店" 这样的关系.
4.yum(Yellow dog Updater, Modified)是Linux下⾮常常⽤的⼀种包管理器. 主要应⽤在Fedora, RedHat, Centos等发⾏版上.
5.Ubuntu:主要使⽤apt(Advanced Package Tool)作为其包管理器。apt同样提供了⾃动解决依赖关系、下载和安装软件包的功能。

我们在Linux中安装软件有三种方法:

  1. 源码安装
  2. 软件包安装 ---- rpm
  3. 包管理器 (yum (centos) apt/apt - get (ubuntu))

针对源码安装和软件包安装,会出现依赖和版本型兼容的问题,而使用包管理器,它会自动处理这两个问题。

包管理器就像我们的手机上的应用商店一样,但是应用商店里的软件可以通过应用商店来获得用户进而实现盈利,但Linux上的软件是谁提供的呢?

1-2 Linux软件⽣态
Linux下载软件的过程(Ubuntu、Centos、other)

操作系统的好坏评估--- ⽣态问题

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

软件包依赖的问题

国内镜像源

作者把工具写好放在了社区的云服务器上,而在Linux操作系统内部内置链接,当包管理器需要下载软件时,就会访问这个链接来到社区的云服务器上进行相应的下载。因为网速等一些问题,国内对该云服务器进行了"拷贝",叫做镜像源,我们若是要使用国内的镜像源就需要更换操作系统内部的下载链接,也叫做切换镜像源。

"开发者上传软件包 → 软件源存储 → 包管理器下载安装" 的完整闭环,核心是让 Linux 软件的获取和管理更高效、更稳定,尤其是通过国内镜像源优化了国内用户的使用体验。

1-3 yum具体操作

1-3-1 查看软件包

通过 yum list 命令可以罗列出当前⼀共有哪些软件包. 由于包的数⽬可能⾮常之多, 这⾥我们需要使⽤grep 命令只筛选出我们关注的包.

Centos

$ yum list | grep lrzsz
lrzsz.x86_64 0.12.20-36.el7 @base

Ubuntu

$ apt search lrzsz
Sorting... Done
Full Text Search... Done
cutecom/focal 0.30.3-1build1 amd64
Graphical serial terminal, like minicom
lrzsz/focal,now 0.12.21-10 amd64 [installed]
Tools for zmodem/xmodem/ymodem file transfer

这是在Centos 和Ubuntu两个镜像源下搜索lrzsz工具的方法

注意事项:
软件包名称: 主版本号.次版本号.源程序发⾏号-软件包的发⾏号.主机平台.cpu架构.
"x86_64" 后缀表⽰64位系统的安装包, "i686" 后缀表⽰32位系统安装包. 选择包时要和系统匹配.
"el7" 表⽰操作系统发⾏版的版本. "el7" 表⽰的是 centos7/redhat7. "el6" 表⽰ centos6/redhat6.
最后⼀列, base 表⽰的是 "软件源" 的名称, 类似于 "⼩⽶应⽤商店", "华为应⽤商店" 这样的概念.
Ubuntu 有上述有详细介绍

1-3-2 安装软件
通过包管理器, 我们可以通过很简单的⼀条命令完成lrzsz的安装:

Centos

$ sudo yum install -y lrzsz

Ubuntu

$ sudo apt install -y lrzsz

1-3-3 卸载软件

Centos

sudo yum remove [-y] lrzsz

Ubuntu

sudo apt remove [-y] lrzsz

1-4 安装源

Cetnos 安装源路径:
ll /etc/yum.repos.d/ total 16 -rw-r--r-- 1 root root 676 Oct 8 20:47 CentOS-Base.repo *#* *标准源* -rw-r--r-- 1 root root 230 Aug 27 10:31 epel.repo *#* *扩展源* Ubuntu 安装源路径: cat /etc/apt/sources.list # 标准源
$ ll /etc/apt/sources.list.d/ # 扩展源
1. CentOS 安装源(yum 体系)

配置路径: /etc/yum.repos.d/ 目录(存放 .repo 结尾的配置文件)。

CentOS-Base.repo:标准源,提供 CentOS 系统基础、稳定的软件包(如系统库、默认工具)。

epel.repo:扩展源(EPEL),全称 "Extra Packages for Enterprise Linux",补充官方源没有的软件(如一些开发工具、小众应用 )。

扩展源安装: 通过 sudo yum install -y epel-release 自动配置 epel.repo,开启更多软件的安装权限。
2. Ubuntu 安装源(apt 体系)

配置路径:

/etc/apt/sources.list:标准源,配置官方或镜像源的地址(如清华镜像、阿里镜像 )。

/etc/apt/sources.list.d/:扩展源目录,存放额外的软件源配置(如 Docker、Node.js 官方源 )。

**扩展源使用:**需手动添加 .list 配置文件(或通过脚本),比如安装 Docker 时,会在此目录添加 Docker 官方源。

  1. 编辑器Vim

2-1 Linux编辑器-vim使⽤

vi/vim的区别简单点来说,它们都是多模式编辑器,不同的是vim是vi的升级版本,它不仅兼容vi的所有指令,⽽且还有⼀些新的特性在⾥⾯。例如语法加亮,可视化操作不仅可以在终端运⾏,也可以运⾏于x window、 mac os、 windows。我们课堂上,统⼀按照vim来进⾏讲解。

2-2 vim的基本概念

课堂上我们讲解vim的三种模式(其实有好多模式,⽬前掌握这3种即可),分别是命令模式(command mode)、插⼊模式(Insert mode)和底⾏模式(last line mode),各模式的功能区分如下:
正常/普通/命令模式(Normal mode)
控制屏幕光标的移动,字符、字或⾏的删除,移动复制某区段及进⼊Insert mode下,或者到 last line mode
插⼊模式(Insert mode)
只有在Insert mode下,才可以做⽂字输⼊,按「ESC」键可回到命令⾏模式。该模式是我们后⾯⽤的最频繁的编辑模式。
末⾏模式(last line mode)
⽂件保存或退出,也可以进⾏⽂件替换,找字符串,列出⾏号等操作。
在命令模式下,*shift+:* 即可进⼊该模式。要查看你的所有模式:打开 vim,底⾏模式直接输⼊:help vim-modes

这就是三种模式的切换方法,其中命令模式是最主要的。

2-3 vim的基本操作

进⼊vim,在系统提⽰符号输⼊vim及⽂件名称后,就进⼊vim全屏幕编辑画⾯:
$ vim test.c
不过有⼀点要特别注意,就是你进⼊vim之后,是处于[正常模式],你要切换到[插⼊模式]才能够输⼊⽂字。

正常模式\]切换⾄\[插⼊模式

输⼊a
输⼊i
输⼊o

插⼊模式\]切换⾄\[正常模式

⽬前处于[插⼊模式],就只能⼀直输⼊⽂字,如果发现输错了字,想⽤光标键往回移动,将该字删除,可以先按⼀下「ESC」键转到[正常模式]再删除⽂字。当然,也可以直接删除。

正常模式\]切换⾄\[末⾏模式

「shift + ;」, 其实就是输⼊「:」
退出vim及保存⽂件,在[正常模式]下,按⼀下「:」冒号键进⼊「Last line mode」,例如:
: w (保存当前⽂件)
: wq (输⼊「wq」,存盘并退出vim)
: q! (输⼊q!,不存盘强制退出vim)

2-4 vim正常模式命令集

插⼊模式
按「i」切换进⼊插⼊模式「insert mode」,按"i"进⼊插⼊模式后是从光标当前位置开始输⼊
⽂件;
按「a」进⼊插⼊模式后,是从⽬前光标所在位置的下⼀个位置开始输⼊⽂字;
按「o」进⼊插⼊模式后,是插⼊新的⼀⾏,从⾏⾸开始输⼊⽂字。
从插⼊模式切换为命令模式
按「ESC」键。
移动光标
vim可以直接⽤键盘上的光标来上下左右移动,但正规的vim是⽤⼩写英⽂字⺟「h」、「j」、 「k」、「l」,分别控制光标左、下、上、右移⼀格
按「G」:移动到⽂章的最后
按「 $ 」:移动到光标所在⾏的"⾏尾"
按「^」:移动到光标所在⾏的"⾏⾸"
按「w」:光标跳到下个字的开头
按「e」:光标跳到下个字的字尾
按「b」:光标回到上个字的开头
按「#l」:光标移到该⾏的第#个位置,如:5l,56l
按[gg]:进⼊到⽂本开始
按[shift+g]:进⼊⽂本末端
按「ctrl」+「b」:屏幕往"后"移动⼀⻚
按「ctrl」+「f」:屏幕往"前"移动⼀⻚
按「ctrl」+「u」:屏幕往"后"移动半⻚
按「ctrl」+「d」:屏幕往"前"移动半⻚
删除⽂字
「x」:每按⼀次,删除光标所在位置的⼀个字符
「#x」:例如,「6x」表⽰删除光标所在位置的"后⾯(包含⾃⼰在内)"6个字符
「X」:⼤写的X,每按⼀次,删除光标所在位置的"前⾯"⼀个字符
「#X」:例如,「20X」表⽰删除光标所在位置的"前⾯"20个字符
「dd」:删除光标所在⾏
「#dd」:从光标所在⾏开始删除#⾏
复制
「yw」:将光标所在之处到字尾的字符复制到缓冲区中。
「#yw」:复制#个字到缓冲区
「yy」:复制光标所在⾏到缓冲区。
「#yy」:例如,「6yy」表⽰拷⻉从光标所在的该⾏"往下数"6⾏⽂字。
「p」:将缓冲区内的字符贴到光标所在位置。注意:所有与"y"有关的复制命令都必须
与"p"配合才能完成复制与粘贴功能。
替换
「r」:替换光标所在处的字符。
「R」:替换光标所到之处的字符,直到按下「ESC」键为⽌。
撤销上⼀次操作
「u」:如果您误执⾏⼀个命令,可以⻢上按下「u」,回到上⼀个操作。按多次"u"可以执⾏
多次回复。
「ctrl + r」: 撤销的恢复
更改
「cw」:更改光标所在处的字到字尾处
「c#w」:例如,「c3w」表⽰更改3个字
跳⾄指定的⾏
「ctrl」+「g」列出光标所在⾏的⾏号。
「#G」:例如,「15G」,表⽰移动光标⾄⽂章的第15⾏ ⾸。

2-5 vim末⾏模式命令集

在使⽤末⾏模式之前,请记住先按「ESC」键确定您已经处于正常模式,再按「:」冒号即可进⼊末⾏模式。
列出⾏号
「set nu」: 输⼊「set nu」后,会在⽂件中的每⼀⾏前⾯列出⾏号。
跳到⽂件中的某⼀⾏
「#」:「#」号表⽰⼀个数字,在冒号后输⼊⼀个数字,再按回⻋键就会跳到该⾏了,如输⼊数字15,再回⻋,就会跳到⽂章的第15⾏。
查找字符
「/关键字」: 先按「/」键,再输⼊您想寻找的字符,如果第⼀次找的关键字不是您想要的,可以⼀直按「n」会往后寻找到您要的关键字为⽌。
「?关键字」:先按「?」键,再输⼊您想寻找的字符,如果第⼀次找的关键字不是您想要的,可以⼀直按「n」会往前寻找到您要的关键字为⽌。
保存⽂件
「w」: 在冒号输⼊字⺟「w」就可以将⽂件保存起来
退出vim
「q」:按「q」就是退出,如果⽆法离开vim,可以在「q」后跟⼀个「!」强制离开vim。
「wq」:⼀般建议离开时,搭配「w」⼀起使⽤,这样在退出的时候还可以保存⽂件。

2-6 vim操作总结

三种模式
正常模式
插⼊模式
底⾏模式
vim操作
打开,关闭,查看,查询,插⼊,删除,替换,撤销,复制等等操作。

这是我们今天学到的五种模式切换图:

2-7 简单vim配置

配置⽂件的位置
在⽬录 /etc/ 下⾯,有个名为vimrc的⽂件,这是系统中公共的vim配置⽂件,对所有⽤⼾都有
效。
⽽在每个⽤⼾的主⽬录下,都可以⾃⼰建⽴私有的配置⽂件,命名为:".vimrc"。例如,/root
⽬录下,通常已经存在⼀个.vimrc⽂件,如果不存在,创建就好了。
切换⽤户成为⾃⼰执⾏ su ,进⼊⾃⼰的主⼯作⽬录,执⾏ cd ~
打开⾃⼰⽬录下的.vimrc⽂件,执⾏ vim .vimrc
常⽤配置选项,⽤来测试
设置语法⾼亮: syntax on
显⽰⾏号: set nu
设置缩进的空格数为4: set shiftwidth=4
使⽤插件
要配置好看的vim,原⽣的配置可能功能不全,可以选择安装插件来完善配置

  1. 编译器gcc/g++

3-1 背景知识

  1. 预处理(进⾏宏替换/去注释/条件编译/头⽂件展开等)
  2. 编译(⽣成汇编)
  3. 汇编(⽣成机器可识别代码)
  4. 连接(⽣成可执⾏⽂件或库⽂件)

3-2 gcc编译选项

格式 gcc [选项] 要编译的⽂件 [选项] [⽬标⽂件]

3-2-1 预处理(进⾏宏替换)

预处理功能主要包括宏定义,⽂件包含,条件编译,去注释等。
预处理指令是以#号开头的代码⾏。
实例: gcc --E hello.c --o hello.i
选项"-E",该选项的作⽤是让 gcc 在预处理结束后停⽌编译过程。
选项"-o"是指⽬标⽂件,".i"⽂件为已经过预处理的C原始程序。

3-2-2 编译(⽣成汇编)

在这个阶段中,gcc ⾸先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的⼯作,在检查⽆误后,gcc 把代码翻译成汇编语⾔。
⽤⼾可以使⽤"-S"选项来进⾏查看,该选项只进⾏编译⽽不进⾏汇编,⽣成汇编代码。
实例: gcc --S hello.i --o hello.s

3-2-3 汇编(⽣成机器可识别代码)

汇编阶段是把编译阶段⽣成的".s"⽂件转成⽬标⽂件
读者在此可使⽤选项"-c"就可看到汇编代码已转化为".o"的⼆进制⽬标代码了
实例: gcc --c hello.s --o hello.o

3-2-4 连接(⽣成可执⾏⽂件或库⽂件)

在成功编译之后,就进⼊了链接阶段。
实例: gcc hello.o --o hello
那么为什么c/c++进行编译的时候要先变成汇编呢,本质上是因为在编程语言的发展中,机器语言 ->汇编语言->高级语言,而汇编语言是一种标准化的低级语言,几乎所有处理器架构都有对应的汇编表示,所以在编译时应该变成汇编语言。

3-3 动态链接和静态链接

在我们的实际开发中,不可能将所有代码放在⼀个源⽂件中,所以会出现多个源⽂件,⽽且多个源⽂件之间不是独⽴的,⽽会存在多种依赖关系,如⼀个源⽂件可能要调⽤另⼀个源⽂件中定义的函数,但是每个源⽂件都是独⽴编译的,即每个*.c⽂件会形成⼀个*.o⽂件,为了满⾜前⾯说的依赖关系,则需要将这些源⽂件产⽣的⽬标⽂件进⾏链接,从⽽形成⼀个可以执⾏的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:
浪费空间:因为每个可执⾏程序中对所有需要的⽬标⽂件都要有⼀份副本,所以如果多个程序对同⼀个⽬标⽂件都有依赖,如多个程序中都调⽤了printf()函数,则这多个程序中都含有
printf.o,所以同⼀个⽬标⽂件都在内存存在多个副本;
更新⽐较困难:因为每当库函数的代码修改了,这个时候就需要重新进⾏编译链接形成可执⾏程序。但是静态链接的优点就是,在可执⾏程序中已经具备了所有执⾏程序所需要的任何东西,在执⾏的时候运⾏速度快。
动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对独⽴部分,在程序运⾏时才将它们链接在⼀起形成⼀个完整的程序,⽽不是像静态链接⼀样把所有程序模块都链接成⼀个单独的可执⾏⽂件。
动态链接其实远⽐静态链接要常⽤得多。
1.动态链接是当调用一个函数的时候,去库中对应的函数将该函数的地址记录下来,确保在程序运行的时候能在库中找到对应函数。

2.静态链接是当调用一个函数的时候,去库中对应的函数将其实现记录下来,确保在程序运行的时候该函数可以运行。
在这⾥涉及到⼀个重要的概念: 库
我们的C程序中,并没有定义"printf"的函数实现,且在预编译中包含的"stdio.h"中也只有该
函数的声明,⽽没有定义函数的实现,那么,是在哪⾥实"printf"函数的呢?
最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库⽂件中去了,在没有特别指定
时,gcc 会到系统默认的搜索路径"/usr/lib"下进⾏查找,也就是链接到 libc.so.6 库函数中去,这样
就能实现函数"printf"了,⽽这也就是链接的作⽤

3-4 静态库和动态库

1.静态库是指编译链接时,把库⽂件的代码全部加⼊到可执⾏⽂件中,因此⽣成的⽂件⽐较⼤,但在运⾏时也就不再需要库⽂件了。其后缀名⼀般为".a"
2.动态库与之相反,在编译链接时并没有把库⽂件的代码加⼊到可执⾏⽂件中,⽽是在程序执⾏时由运⾏时链接⽂件加载库,这样可以节省系统的开销。动态库⼀般后缀名为".so",如前⾯所述的libc.so.6 就是动态库。gcc 在编译时默认使⽤动态库。完成了链接之后,gcc 就可以⽣成可执⾏⽂件,如下所⽰。 gcc hello.o --o hello

所以对于动/静态库:

动静态库对比:

  1. 动态库形成的可执行程序体积一定很小
  2. 可执行程序对静态库的依赖最小,动态库不能缺失
  3. 程序运行,需要加载到内存,静态链接的,会在内存中出现大量的重复代码。
  4. 动态链接,比较节省内存和磁盘资源

3-5 gcc其他常⽤选项

-E 只激活预处理,这个不⽣成⽂件,你需要把它重定向到⼀个输出⽂件⾥⾯
-S 编译到汇编语⾔不进⾏汇编和链接
-c 编译到⽬标代码
-o ⽂件输出到 ⽂件
-static 此选项对⽣成的⽂件采⽤静态链接
-g ⽣成调试信息。GNU 调试器可利⽤该信息。
-shared 此选项将尽量使⽤动态库,所以⽣成⽂件⽐较⼩,但是需要系统由动态库.
-O0
-O1
-O2
-O3 编译器的优化选项的4个级别,-O0表⽰没有优化,-O1为缺省值,-O3优化级别最⾼
-w 不⽣成任何警告信息。
-Wall ⽣成所有警告信息。

  1. ⾃动化构建-make/Makefile

4-1 背景

会不会写makefile,从⼀个侧⾯说明了⼀个⼈是否具备完成⼤型⼯程的能⼒
⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义了⼀系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄于进⾏更复杂的功能操作
makefile带来的好处就是⸺"⾃动化编译",⼀旦写好,只需要⼀个make命令,整个⼯程完全⾃动编译,极⼤的提⾼了软件开发的效率。
make是⼀个命令⼯具,是⼀个解释makefile中指令的命令⼯具,⼀般来说,⼤多数的IDE都有这个命令,⽐如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可⻅,makefile都成为了⼀种在⼯程⽅⾯的编译⽅法。
make是⼀条命令,makefile是⼀个⽂件,两个搭配使⽤,完成项⽬⾃动化构建。

4-2 基本使⽤

实例代码:

#include <stdio.h>
int main()
{
printf("hello Makefile!\n");
return 0;
}
Makefile⽂件
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
依赖关系
上⾯的⽂件myproc,它依赖myproc.c
依赖⽅法
gcc -o myproc myproc.c ,就是与之对应的依赖关系
项⽬清理
⼯程是需要被清理的
像clean这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动执⾏,不过,我们可以显⽰要make执⾏。即命令⸺"make clean",以此来清除所有的⽬标⽂件,以便重编译。
但是⼀般我们这种clean的⽬标⽂件,我们将它设置为伪⽬标,⽤ .PHONY 修饰,伪⽬标的特性
是,总是被执⾏的。

什么叫做总是被执行

首先我们看一下不被执行的情况:

默认老代码不做重新编译
那么make是怎么知道bin文件和.c文件的新旧的呢?

主要根据以下三个时间:
$ stat XXX
File: 'XXX'
Size: 987 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1321125 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ whb) Gid: ( 1000/ whb)
Access: 2024-10-25 17:05:30.430619002 +0800
Modify: 2024-10-25 17:05:25.940595116 +0800
Change: 2024-10-25 17:05:25.940595116 +0800
⽂件 = 内容 + 属性
Modify: 内容变更,时间更新
Change:属性变更,时间更新
Access:常指的是⽂件最近⼀次被访问的时间。
.PHONY:让make忽略源⽂件和可执⾏⽬标⽂件的Modify时间对⽐ ,也就是文件总是能被执行

4-3 推导过程

myproc:myproc.o
gcc myproc.o -o myproc
myproc.o:myproc.s
gcc -c myproc.s -o myproc.o
myproc.s:myproc.i
gcc -S myproc.i -o myproc.s
myproc.i:myproc.c
gcc -E myproc.c -o myproc.i
.PHONY:clean
clean:
rm -f *.i *.s *.o myproc
编译
$ make
gcc -E myproc.c -o myproc.i
gcc -S myproc.i -o myproc.s
gcc -c myproc.s -o myproc.o
gcc myproc.o -o myproc
make是如何⼯作的,在默认的⽅式也就是我们只输⼊make命令。那么:

  1. make会在当前⽬录下找名字叫"Makefile"或"makefile"的⽂件。
  2. 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 myproc 这个⽂件,并把这个⽂件作为最终的⽬标⽂件。
  3. 如果 myproc ⽂件不存在,或是 myproc 所依赖的后⾯的 myproc.o ⽂件的⽂件修改时间要
    ⽐ myproc 这个⽂件新(可以⽤ touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成
    myproc 这个⽂件。
  4. 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为
    myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀
    个堆栈的过程)
  5. 当然,你的C⽂件和H⽂件是存在的啦,于是 make 会⽣成 myproc.o ⽂件,然后再⽤
    myproc.o ⽂件声明 make 的终极任务,也就是执⾏⽂件 hello 了。
  6. 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个⽬标⽂件。
  7. 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
  8. make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不在,那么对不起,我就不⼯作啦。

4-4 适度扩展语法

BIN=proc.exe # 定义变量
CC=gcc
#SRC=(shell ls \*.c) #* *采⽤* *shell* *命令⾏⽅式,获取当前所有* *.c* *⽂件名* SRC=(wildcard *.c) # 或者使⽤ wildcard 函数,获取当前所有 .c ⽂件名
OBJ=(SRC:.c=.o) *#* *将* *SRC* *的所有同名* *.c* *替换 成为* *.o* *形成⽬标⽂件列表* LFLAGS=-o *#* *链接选项* FLAGS=-c *#* *编译选项* RM=rm -f *#* *引⼊命令* (BIN):(OBJ) @(CC) (LFLAGS) @ \^ *# @:
代表⽬标⽂件名。 \^:* *代表依赖⽂件列表* @echo "linking ... ^ to @" %.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同名.o @(CC) (FLAGS) < # %<: 对展开的依赖 .c ⽂件,⼀个⼀个的交给 gcc
@echo "compling ... \< to @" # @ :不回显命令
.PHONY:clean
clean:
(RM) (OBJ) (BIN) *# (RM):
替换,⽤变量内容替换它
.PHONY:test
test:
@echo (SRC) @echo (OBJ)

这里使用变量来代替各种文件和选项来完成Makefile的编写。

  1. Linux第⼀个系统程序−进度条

5-1 回⻋与换⾏

回⻋概念
换⾏概念
\r&&\n

我们拿这一张作文纸举例,\r代表的是回车\n代表的是换行,当我们换到下一行时,是换行;当我们换到行首时,是回车。

5-2 ⾏缓冲区

什么现象?
#include <stdio.h>
int main()
{
printf("hello world!\n");
sleep(3);
return 0;
}
什么现象??
#include <stdio.h>
int main()
{
printf("hello bite!");
sleep(3);
return 0;
}
什么现象???
#include <stdio.h>
int main()
{
printf("hello bite!");
fflush(stdout);
sleep(3);
return 0;
}
在第一段和第三段代码中hello world可以被正常打印,而在第二段代码中hello world是在程序结束时被打印,这是因为有缓冲区,\n 在代码中还有行刷新的作用。在程序退出时,会自动刷新缓冲区,若是想手动刷新缓冲区,就要使用fflush,而在计算机中有三种流

复制代码
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

其中stdin对应键盘输入,stdout和stderr对应显示器输出,所以我们想刷新输出就使用fflush(stdout)

5-3 进度条代码

复制代码
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='

// verison2
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); // 11.0 / 1000
    int i = 0;
    for(; 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);
}

process.h
#pragma once
#include <stdio.h>
void FlushProcess(double total, double current);

main.c
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0;
double speed = 1.0;
void DownLoad()
{
    double current = 0;
    while(current <= total)
    {
        FlushProcess(total, current);
        // 下载代码
        usleep(3000); // 充当下载数据
        current += speed;
    }
    printf("\ndownload %.2lfMB Done\n", current);
}
int main()
{
    DownLoad();
    return 0;
}

Makefile
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar

$(BIN):$(OBJ)
    gcc -o $@ $^
%.o:%.c
    gcc -c $<
.PHONY:
clean:
    rm -f $(OBJ) $(BIN)
  1. 版本控制器Git

不知道你⼯作或学习时,有没有遇到这样的情况:我们在编写各种⽂档时,为了防⽌⽂档丢失,更改失误,失误后能恢复到原来的版本,不得不复制出⼀个副本,⽐如:
"报告-v1"
"报告-v2"
"报告-v3"
"报告-确定版"
"报告-最终版"
"报告-究极进化版"
...
每个版本有各⾃的内容,但最终会只有⼀份报告需要被我们使⽤ 。
但在此之前的⼯作都需要这些不同版本的报告,于是每次都是复制粘贴副本,产出的⽂件就越来越多,⽂件多不是问题,问题是:随着版本数量的不断增多,你还记得这些版本各⾃都是修改了什么吗?
⽂档如此,我们写的项⽬代码,也是存在这个问题的!!

6-1 版本控制器

1.为了能够更⽅便我们管理这些不同版本的⽂件,便有了版本控制器。所谓的版本控制器,就是能让你了解到⼀个⽂件的历史,以及它的发展过程的系统。通俗的讲就是⼀个可以记录⼯程的每⼀次改动和版本迭代的⼀个管理系统,同时也⽅便多⼈协同作业。
2.⽬前最主流的版本控制器就是 Git 。Git 可以控制电脑上所有格式的⽂件,例如 doc、excel、dwg、dgn、rvt等等。对于我们开发⼈员来说,Git 最重要的就是可以帮助我们管理软件开发项⽬中的源代码⽂件!

6-2 git 简史

  1. 同⽣活中的许多伟⼤事物⼀样,Git 诞⽣于⼀个极富纷争⼤举创新的年代。
  2. Linux 内核开源项⽬有着为数众多的参与者。 绝⼤多数的 Linux 内核维护⼯作都花在了提交补丁和保存归档的繁琐事务上(1991−2002年间)。 到 2002 年,整个项⽬组开始启⽤⼀个专有的分布式版本控制系统 BitKeeper 来管理和维护代码。
  3. 到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux内核社区免费使⽤ BitKeeper 的权⼒。 这就迫使 Linux 开源社区(特别是 Linux 的缔造者 LinusTorvalds)基于使⽤ BitKeeper 时的经验教训,开发出⾃⼰的版本系统。 他们对新的系统制订了若⼲⽬标:
    速度
    简单的设计
    对⾮线性开发模式的强⼒⽀持(允许成千上万个并⾏开发的分⽀)
    完全分布式
  4. 有能⼒⾼效管理类似 Linux 内核⼀样的超⼤规模项⽬(速度和数据量)
    ⾃诞⽣于 2005 年以来,Git ⽇臻成熟完善,在⾼度易⽤的同时,仍然保留着初期设定的⽬标。 它的速度⻜快,极其适合管理⼤项⽬,有着令⼈难以置信的⾮线性分⽀管理系统。

git是一个底层的版本控制器软件,它既是一个客户端,也是一个服务器。在我们主机中的仓库叫做 本地仓库,而像gitee和github中使用的仓库是远端仓库。

6-3 安装 git

sudo yum install git
sudo apt install git

6-4 三板斧

1. git add
将代码放到刚才下载好的⽬录中
git add [⽂件名]
将需要⽤ git 管理的⽂件告知 git
2. git commit
提交改动到本地
git commit -m "XXX"
最后的 "." 表⽰当前⽬录
提交的时候应该注明提交⽇志, 描述改动的详细内容.
3. git push
同步到远端服务器上
git push
需要填⼊⽤户名密码. 同步成功后, 刷新 Github ⻚⾯就能看到代码改动了.

git在这里先不做过多介绍,后面会单独出一个系列。

  1. 调试器 - gdb/cgdb使⽤

7-1 样例代码

复制代码
// mycmd.c
#include <stdio.h>
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;
}

7-2 预备

  1. 程序的发布⽅式有两种, debug 模式和 release 模式, Linux gcc/g++ 出来的⼆进制程
    序,默认是 release 模式。
  2. 要使⽤gdb调试,必须在源代码⽣成⼆进制程序的时候, 加上 -g 选项,如果没有添加,程序⽆法被编译
    gcc mycmd.c -o mycmd # 默认模式,不⽀持调试 file mycmd
    mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
    linked, interpreter /lib64/ld-linux-x86-64.so.2,
    BuildID[sha1]=82f5cbaada10a9987d9f325384861a88d278b160, for GNU/Linux
    3.2.0, not stripped
    gcc mycmd.c -o mycmd -g # debug模式 file mycmd
    mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
    linked, interpreter /lib64/ld-linux-x86-64.so.2,
    BuildID[sha1]=3d5a2317809ef86c7827e9199cfefa622e3c187f, for GNU/Linux
    3.2.0, with debug_info, not stripped

7-3 常⻅使用

开始: gdb binFile
退出: ctrl + d 或 quit 调试命令

|------------------------|------------------------|--------------------------|
| 命令 | 作⽤ | 样例 |
| list/l | 显⽰源代码,从上次位置开始,每次列出 10⾏ | list/l 10 |
| list/l 函数名 | 列出指定函数的源代码 | list/l main |
| list/l ⽂件名:⾏号 | 列出指定⽂件的源代码 | list/l mycmd.c:1 |
| r/run | 从程序开始连续执⾏ | run |
| n/next | 单步执⾏,不进⼊函数内部 | next |
| s/step | 单步执⾏,进⼊函数内部 | step |
| break/b [⽂件名:]⾏号 | 在指定⾏号设置断点 | break 10 break 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 |

7-4 常⻅技巧

7-4-1 watch

执⾏时监视⼀个表达式(如变量)的值。如果监视的表达式在程序运⾏期间的值发⽣变化,GDB 会暂停程序的执⾏,并通知使⽤者

复制代码
(gdb) l main
11
12         return result;
13     }
14
15     int main()
16     {
17         int start = 1;
18         int end = 100;
19         printf("I will begin\n");
20         int n = Sum(start, end);
(gdb) b 20
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.
(gdb) info b
Num    Type       Disp    Enb    Address            What
1      breakpoint keep    y      0x00000000000011c3 in main at mycmd.c:20
(gdb) r
Starting program: /home/whb/test/test/mycmd
I will begin
Breakpoint 1, main () at mycmd.c:20
20         int n = Sum(start, end);
(gdb) s
Sum (s=32767, e=-7136) at mycmd.c:5
5     {
(gdb) n
6         int result = 0;
(gdb) watch result
Hardware watchpoint 2: result
(gdb) c
Continuing.
Hardware watchpoint 2: result
Old value = -6896
New value = 0
Sum (s=1, e=100) at mycmd.c:7
7         for(int i = s; i <= e; i++)
(gdb) c
Continuing.
Hardware watchpoint 2: result
Old value = 0
New value = 1
Sum (s=1, e=100) at mycmd.c:7
7         for(int i = s; i <= e; i++)
(gdb) c
Continuing.

Hardware watchpoint 2: result

Old value = 1
New value = 3
Sum (s=1, e=100) at mycmd.c:7
7         for(int i = s; i <= e; i++)
(gdb) c
Continuing.
Hardware watchpoint 2: result
Old value = 3
New value = 6
Sum (s=1, e=100) at mycmd.c:7
7         for(int i = s; i <= e; i++)
(gdb) info b
Num   Type       Disp     Enb    Address            What
1     breakpoint keep     y      0x00005555555551c3 in main at mycmd.c:20
      breakpoint already hit 1 time
2     hw watchpoint keep y result
      breakpoint already hit 4 times
(gdb) d 2
(gdb) info b
Num   Type       Disp     Enb    Address            What
1     breakpoint keep     y      0x00005555555551c3 in main at mycmd.c:20
      breakpoint already hit 1 time
(gdb) finish
Run till exit from #0 Sum (s=1, e=100) at mycmd.c:7
0x00005555555551d2 in main () at mycmd.c:20
20         int n = Sum(start, end);
Value returned is $1 = 5050

注意:
如果你有⼀些变量不应该修改,但是你怀疑它修改导致了问题,你可以watch它,如果变
化了,就会通知你
7-4-2 set var确定问题原因
更改⼀下标志位,假设我们想得到 + -result

复制代码
// mycmd.c
#include <stdio.h>
int flag = 0; // 故意错误
//int flag = -1;
//int flag = 1;
int Sum(int s, int e)
{
	int result = 0;
	for (int i = s; i <= e; i++)
	{
		result += i;
	}
	return result * flag;
}
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;
}

(gdb)l main
15
16 return result * flag;
17 }
18
19 int main()
20 {
21		int start = 1;
22		int end = 100;
23		printf("I will begin\n");
24		int n = Sum(start, end);
(gdb)b 24
Breakpoint 1 at 0x11ca: file mycmd.c, line 24.
(gdb)r
Starting program : / home / user / test / test / mycmd
I will begin
Breakpoint 1, main() at mycmd.c : 24
24		int n = Sum(start, end);
(gdb)n
25		printf("running done, result is: [%d-%d]=%d\n", start, end, n);
(gdb)n
running done, result is : [1 - 100] = 0 # 这⾥结果为什么是0?
26		return 0;
(gdb)r
The program being debugged has been started already.
Start it from the beginning ? (y or n) y
Starting program : / home / user / test / test / mycmd
I will begin
Breakpoint 1, main() at mycmd.c : 24
24		int n = Sum(start, end);
(gdb)s
Sum(s = 32767, e = -7136) at mycmd.c:9
9		{
(gdb)n
10			int result = 0;
(gdb)n
11			for (int i = s; i <= e; i++)
(gdb)
13				result += i;
(gdb)
11			for (int i = s; i <= e; i++)
(gdb)
13				result += i;
(gdb)until 14
Sum(s = 1, e = 100) at mycmd.c:16
16			return result * flag;
(gdb)p result
$1 = 5050
(gdb)p flag
$2 = 0
(gdb)set var flag = 1 # 更改flag的值,确认是否是它的原因
(gdb) p flag
$3 = 1
(gdb)n
17			}
(gdb)n
main() at mycmd.c:25
25				printf("running done, result is: [%d-%d]=%d\n", start, end, n);
(gdb)n
running done, result is : [1 - 100] = 5050 # 是它的原因
26				return 0;

7-4-3 条件断点
添加条件断点

复制代码
(gdb)l main
11
12			return result;
13		}
14
15		int main()
16		{
17			int start = 1;
18			int end = 100;
19			printf("I will begin\n");
20			int n = Sum(start, end);
(gdb)b 20
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.
(gdb)r
Starting program : / home / user / test / test / mycmd
I will begin
Breakpoint 1, main() at mycmd.c : 20
20			int n = Sum(start, end);
(gdb)s
Sum(s = 32767, e = -7136) at mycmd.c:5
5		{
(gdb)n
6			int result = 0;
(gdb)n
7			for (int i = s; i <= e; i++)
(gdb)n
9			result += i;
(gdb)display i
1: i = 1
(gdb)n
7			for (int i = s; i <= e; i++)
1 : i = 1
(gdb)n
9			result += i;
1: i = 2
(gdb)n
7			for (int i = s; i <= e; i++)
1 : i = 2
(gdb)n
9			result += i;
1: i = 3
(gdb)
7			for (int i = s; i <= e; i++)
1 : i = 3
(gdb)info b
Num Type	   Disp	 Enb	Address			   What
1	breakpoint keep	 y		0x00005555555551c3 in main at mycmd.c : 20
	breakpoint already hit 1 time
(gdb) b 9 if i == 30 # 9是⾏号,表⽰新增断点的位置
Breakpoint 2 at 0x555555555186: file mycmd.c, line 9.
(gdb)info b
Num Type	   Disp  Enb	Address			   What
1	breakpoint keep  y		0x00005555555551c3 in main at mycmd.c : 20
	breakpoint already hit 1 time
2	breakpoint keep  y		0x0000555555555186 in Sum at mycmd.c : 9
	stop only if i == 30
(gdb)finish
Run till exit from #0 Sum(s = 1, e = 100) at mycmd.c:7
Breakpoint 2, Sum(s = 1, e = 100) at mycmd.c : 9
9			result += i;
1: i = 30
(gdb)finish
Run till exit from #0 Sum(s = 1, e = 100) at mycmd.c:9
0x00005555555551d2 in main() at mycmd.c : 20
20			int n = Sum(start, end);
Value returned is $1 = 5050

给已经存在的端点新增条件

复制代码
(gdb)l main
11
12			return result;
13		}
14
15		int main()
16		{
17			int start = 1;
18			int end = 100;
19			printf("I will begin\n");
20			int n = Sum(start, end);
(gdb)b 20
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.
(gdb)r
Starting program : / home / user / test / test / mycmd
I will begin
Breakpoint 1, main() at mycmd.c : 20
20		int n = Sum(start, end);
(gdb)s
Sum(s = 32767, e = -7136) at mycmd.c:5
5 {
(gdb)n
6			int result = 0;
(gdb)n
7		for (int i = s; i <= e; i++)
(gdb)n
9			result += i;
(gdb)
7		for (int i = s; i <= e; i++)
(gdb)
9			result += i;
(gdb)
7		for (int i = s; i <= e; i++)
(gdb)
9			result += i;
(gdb)
7		for (int i = s; i <= e; i++)
(gdb)b 9 # 我们在第9⾏新增⼀个断点,⽤来开始测试
Breakpoint 2 at 0x555555555186: file mycmd.c, line 9.
(gdb)info b
Num Type	   Disp  Enb	Address			   What
1   breakpoint keep  y		0x00005555555551c3 in main at mycmd.c : 20
	breakpoint already hit 1 time
2	breakpoint keep  y		0x0000555555555186 in Sum at mycmd.c : 9
(gdb)n
Breakpoint 2, Sum(s = 1, e = 100) at mycmd.c : 9
9			result += i;
(gdb)n
7		for (int i = s; i <= e; i++)
(gdb)n
Breakpoint 2, Sum(s = 1, e = 100) at mycmd.c:9
9			result += i;
(gdb)condition 2 i == 30 #给2号断点,新增条件i == 30
(gdb)info b
Num Type	   Disp  Enb	Address			   What
1	breakpoint keep  y		0x00005555555551c3 in main at mycmd.c:20
	breakpoint already hit 1 time
2	breakpoint keep  y		0x0000555555555186 in Sum at mycmd.c : 9
	stop only if i == 30
	breakpoint already hit 2 times
(gdb) n
7		for (int i = s; i <= e; i++)
(gdb)n
9			result += i;
(gdb)c
Continuing.
Breakpoint 2, Sum(s = 1, e = 100) at mycmd.c:9
9			result += i;
(gdb)p i
$1 = 30
(gdb)p result
$2 = 435

注意:

  1. 条件断点添加常⻅两种⽅式:1. 新增 2. 给已有断点追加
  2. 注意两者的语法有区别,不要写错了。
  3. 新增: b ⾏号/⽂件名:⾏号/函数名 if i == 30(条件)
  4. 给已有断点追加:condition 2 i==30, 其中2是已有断点编号,没有if

今天的内容就学习到这里,我们下次再见