第05讲:工具链

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 下载本次课程代码poetry.tar.gz

  • 打开Ubuntu命令行窗口,执行以下命令打开Ubuntu主目录

    $ mkdir -p ~/course # 创建课程练习目录(如果已存在则无需创建)
    $ cd ~/course       # 切换到课程练习目录
    $ explorer.exe .    # 使用Windows Explorer打开当前目录
  • 将下载的poetry.tar.gz拷贝到刚刚打开的目录窗口中

  • 在Ubuntu命令行中解压缩poetry.tar.gz

    $ tar -xf poetry.tar.gz
  • 在命令行中使用VSCode打开当前目录(等同VSCode连接到WSL打开~/course

    $ code .
  • 在Ubuntu命令行中安装软件

    $ sudo apt install build-essential tree

温故

测一测

补全代码,求解《孙子算经》中的“鸡兔同笼”问题

“今有雉兔同笼,上有三十五头,下有九十四足,问雉兔各几何?”

求解鸡兔同笼问题
#include <iostream>
using namespace std;

int main() {
  int head, foot;
  cout << "请输入头数和脚数:";
  cin >> head >> foot;
  // ============= begin =============

  // =============  end  =============
  return 0;
}

开发一个“大”项目!

问题描述

开发一个“大型”C++项目——麻雀虽小,五脏俱全

poetry
├── Makefile
├── include
│   ├── format.h
│   ├── poems.h
│   ├── songci.h
│   └── tangshi.h
├── lib
│   ├── dufu.cpp
│   ├── gaoshi.cpp
│   ├── libai.cpp
│   ├── liqingzhao.cpp
│   ├── poems.cpp
│   ├── sushi.cpp
│   └── xinqiji.cpp
└── main.cpp

2 directories, 13 files

Try it!

在Ubuntu命令行中,使用tree poetry命令打印目录poetry的树形结构

学习目标

  • 学习内容
    • Linux命令行
    • 编译阶段与编译器驱动程序
    • 预处理
    • 编译链接
    • Makefile

学习目标

  • 学会使用最基本的Linux命令
  • 掌握常见的预处理指令使用方法
  • 理解C++项目的组织方式并运用C++工具链构建一个完整项目

知其然,知其所以然

Linux命令行

常见的Linux命令

  • ls:列出目录内容
  • rm:删除文件
  • cp:复制文件
  • mv:移动文件
  • cd:切换目录
  • pwd:显示当前目录
  • mkdir:创建目录
  • rmdir:删除目录

温馨提示

想学习命令的详细用法,问“真男人”(man命令)

$ man ls
No manual entry for ls

Linux目录结构

  • Linux系统采用单一树状目录结构
    • 所有目录均挂在根目录/
    • 用户的主目录一般为/home/<username>
  • 一些特殊目录
    • ~:当前用户主目录
    • .:当前目录
    • ..:父目录
表 1: 根目录结构
/
├── bin -> usr/bin
├── boot
├── dev
├── etc
├── home
├── init
├── lib -> usr/lib
├── lib32 -> usr/lib32
├── lib64 -> usr/lib64
├── libx32 -> usr/libx32
├── lost+found
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin -> usr/sbin
├── snap
├── srv
├── sys
├── tmp
├── usr
├── var
├── wslBPdpko
├── wslEmNJjo
├── wslMDcOko
├── wslMEJmko
└── wslehBoko

28 directories, 1 file
表 2: 用户主目录结构
/home
└── xsu
    ├── course
    ├── opt
    ├── poetry.tar.gz
    ├── poetry.tar.gz:Zone.Identifier
    ├── teaching
    └── workspace

5 directories, 2 files
  • 目录由唯一的路径标识
    • 绝对路径:从根目录开始的完整路径,如/home/xsu/course
    • 相对路径:相对于当前工作目录,如../course

游走于目录之间

  • pwd:打印当前目录(print working directory)

    $ pwd
  • cd:切换到目录(change directory)

    $ cd workspace # 切换到workspace目录
    $ cd ..        # 切换到上级目录
    $ cd           # 切换到用户主目录(缺省切换目标)
  • mkdir:创建目录(make directory)

    $ mkdir course # 在当前目录下创建course目录
    $ mkdir -p /home/xsu/course # 逐层创建/home/xsu/course目录
  • rmdir:删除目录(remove directory),要求目录为空

    $ rmdir course # 删除course目录
    $ rmdir -p /home/xsu/course # 删除/home/xsu/course目录

查看、复制、移动、删除文件

  • ls:列出目录内容(list)

    $ ls path  # 列出目录path中的内容
    $ ls -l -h # 列出当前目录内容,长格式,以人类可读方式显示
  • cp:复制文件(copy)

    $ cp file1 file2  # 复制文件file1到file2
    $ cp -r dir1 dir2 # 复制目录dir1到dir2
  • mv:移动文件(move)

    $ mv file1 dir1/   # 将文件file1移动到目录dir1中
    $ mv file1 file2   # 将文件file1重命名为file2
  • rm:删除文件(remove)

    $ rm file1         # 删除文件file1
    $ rm -r dir1       # 删除目录dir1
  • ls示例
$ ls poetry
Makefile  include  lib  main.cpp
$ ls -lh poetry
total 16K
-rw-r--r-- 1 xsu xsu  684 Mar 10 23:17 Makefile
drwxr-xr-x 2 xsu xsu 4.0K Mar 10 23:17 include
drwxr-xr-x 2 xsu xsu 4.0K Mar 26 18:40 lib
-rw-r--r-- 1 xsu xsu  829 Mar 10 23:17 main.cpp
  • cp示例
$ ls poetry
Makefile  include  lib  main.cpp
$ cp poetry/Makefile poetry/Makefile2
$ cp -r poetry/include poetry/include2
$ ls poetry
Makefile  Makefile2  include  include2  lib  main.cpp
  • mv示例
$ ls poetry
Makefile  Makefile2  include  include2  lib  main.cpp
$ mv poetry/Makefile2 poetry/Makefile3
$ mv poetry/include2 poetry/include3
$ ls poetry
Makefile  Makefile3  include  include3  lib  main.cpp
  • rm示例
$ ls poetry
Makefile  Makefile3  include  include3  lib  main.cpp
$ rm poetry/Makefile3
$ rm -r poetry/include3
$ ls poetry
Makefile  include  lib  main.cpp

编译阶段与编译器驱动程序

打破沙锅问到底

测一测

#include是不是C++语句?

#include <iostream>

int main() {
  std::cout << "Hello, World!" << std::endl;
  return 0;
}

预处理

#include预处理指令,而非C++语句

编译阶段

  • 预处理指令在编译前预处理器处理,编译器看不到这些指令
  • 预处理过程对于C/C++开发而言是可选的,但是在实际开发中几乎都会使用

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 24px
---
flowchart LR
  编辑 --> 编译 --> 链接 --> 调试
  调试 -.-> 编辑

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 24px
---
flowchart LR
  编辑 --> 预处理 --> 编译 --> 链接 --> 调试
  调试 -.-> 编辑

  • 预处理器是一个独立程序,在Linux系统中一般名为cppC Preprocessor)
$ cpp helloworld.cpp -o helloworld.i
$ wc helloworld.cpp helloworld.i
     7     15     97 code/helloworld.cpp
 32260  71264 778447 code/helloworld.i
 32267  71279 778544 total

编译器驱动程序

  • 通常谓之“编译器”的g++实际上是编译器驱动程序,它会调用一系列的程序完成C/C++程序的构建过程,包括
    • 预处理器cpp:将包含预处理指令的C++程序转换为无预处理指令的C++程序
    • 编译器cc1plus:将C++程序转换为汇编程序
    • 汇编器as:将汇编程序转换为二进制目标代码
    • 链接器ld:将目标代码与库文件链接生成可执行程序

明察秋毫

g++ -o helloworld helloworld.cpp等价于依次执行

$ g++ -E -o helloworld.i helloworld.cpp # 调用cpp
$ g++ -S -o helloworld.s helloworld.i   # 调用cc1plus
$ g++ -c -o helloworld.o helloworld.s   # 调用as
$ g++ -o helloworld helloworld.o        # 调用ld

阶段选项

  • 编译器驱动程序提供若干阶段控制的选项
编译器阶段选项
选项 说明
-E 执行预处理后中止
-S 生成汇编代码后中止
-c 生成目标代码后中止
-o 指定输出文件名
  • 根据阶段选项以及输入文件的类型决定需要执行的阶段
$ g++ -E -o helloworld.i helloworld.cpp # 预处理
$ g++ -S -o helloworld.s helloworld.cpp # 预处理、编译
$ g++ -c -o helloworld.o helloworld.cpp # 预处理、编译、汇编
$ g++ -o helloworld helloworld.cpp      # 预处理、编译、汇编、链接

预处理

预处理指令

  • 预处理器指令以#开头,如#include#define#if
预处理器指令
指令 说明
#include 包含一个文件
#define 定义一个宏
#undef 取消一个宏定义
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if#ifdef条件不为真,当前条件为真,则编译下面代码
#else 如果前面的#if#ifdef条件不为真,则编译下面代码
#endif #if的结束
#error 输出错误信息

#include

  • #include指令用于将一个文件的内容原地展开到当前文件的当前位置

Try it!

尝试使用-E选项对poetry/main.cpp进行预处理

$ g++ -E -o poetry/main.i poetry/main.cpp
poetry/main.cpp:4:10: fatal error: poems.h: No such file or directory
    4 | #include "poems.h"
      |          ^~~~~~~~~
compilation terminated.

考考你

这是什么错误,为什么会产生这个错误?

  • #include指令的文件可以是绝对路径相对路径
  • 预处理器在搜索路径中查找文件,搜索路径包括
    • 当前目录.
    • -I path选项指定的路径path
    • 系统头文件目录,如/usr/include

#include

$ g++ -E -I poetry/include -o poetry/main.i poetry/main.cpp
tangshi.h
#ifndef TANGSHI_H
#define TANGSHI_H

void registerDufu();
void registerLibai();
void registerGaoshi();

#endif
songci.h
#ifndef SONGCI_H
#define SONGCI_H

void registerSushi();
void registerXinqiji();
void registerLiqingzhao();

#endif
main.cpp
#include <iostream>
#include <getopt.h>
using namespace std;
#include "poems.h"
#include "tangshi.h"
#include "songci.h"

int main(int argc, char *argv[]) {
  registerDufu();
  registerLibai();
  registerGaoshi();
  registerSushi();
  registerXinqiji();
  registerLiqingzhao();

  const static char *usage = "Usage: poetry [-h] [-a author] [-t title]\n";
  const char *author = nullptr;
  const char *title = nullptr;

Try it!

执行上述预处理过程,在VSCode中查看生成的main.i

#define与#undef

  • #define指令定义一个,宏在后续代码中会被替换为对应的文本
  • #undef指令取消一个宏定义
define.cpp
#define PI 3.1415926
#define SQUARE(x) ((x) * (x))
int main() {
  double r = 3.0;
  double area = PI * SQUARE(r);      // 等价于 3.1415926 * ((r) * (r))
#undef PI
  double circumference = 2 * PI * r; // PI未定义,编译错误
  return 0;
}
$ g++ -c code/define.cpp
code/define.cpp: In function ‘int main()’:
code/define.cpp:7:30: error: ‘PI’ was not declared in this scope
    7 |   double circumference = 2 * PI * r; // PI未定义,编译错误
      |                              ^~

测一测

上述错误出现在预处理阶段还是编译阶段?

带参数的宏

  • 宏可以带参数,如#define MAX(a, b) ((a) > (b) ? (a) : (b))
  • 带参数的宏可以像函数一样调用,如MAX(3, 5)

考考你

判断下面程序的输出是什么?

#include <iostream>
#define ADD(a, b) a + b
int main() {
  int a = 1, b = 2;
  std::cout << ADD(a, b) * 3 << std::endl;
  return 0;
}

避坑

使用带参数的宏,与函数调用有本质区别,因为宏只是文本替换对形式参数的引用要加括号

#if、#else、#elif、#endif

  • #if#else#elif#endif称为条件编译指令
    • 根据条件决定是否编译特定代码段
conditional-compilation.cpp
#include <iostream>

#if VERBOSE
#define log(x) std::cout << (x) << std::endl // 编译器看不到这一行
#else
#define log(x)                               // 看到了这一行
#endif

int main() {
  log("Hello, World!");
  return 0;
}
$ g++ -DDVERBOSE=1 -o code/conditional-compilation code/conditional-compilation.cpp
$ ./code/conditional-compilation
Hello, World!
$ g++ -o code/conditional-compilation code/conditional-compilation.cpp
$ ./code/conditional-compilation
# no output

条件编译指令中的条件

  • 条件编译指令的条件可以采用以下形式
    • 宏定义谓词:#if defined(VERBOSE)
      • 仅检查宏是否有定义,不关心宏的值
    • 0值判定:#if VERBOSE
    • 比较表达式:#if VERBOSE == 0
    • 逻辑表达式:#if VERBOSE == 0 || defined(DEBUG)
#include <iostream>
#define VERBOSE 0

int main() {
#if defined(VERBOSE)
  std::cout << "Verbose output" << std::endl; // 此行代码会被编译
#endif
  return 0;
}

#ifdef、#ifndef

  • #ifdef#ifndef是条件编译指令的简化形式
    • #ifdef VERBOSE等价于#if defined(VERBOSE)
    • #ifndef VERBOSE等价于#if !defined(VERBOSE)

#error

  • #error指令用于输出错误信息
pperror.cpp
#ifndef SQRT
#error "SQRT is not defined"
#endif

int main() {
  SQRT(4);
  return 0;
}
$ g++ -o code/pperror code/pperror.cpp
code/pperror.cpp:2:2: error: #error "SQRT is not defined"
    2 | #error "SQRT is not defined"
      |  ^~~~~
code/pperror.cpp: In function ‘int main()’:
code/pperror.cpp:6:3: error: ‘SQRT’ was not declared in this scope
    6 |   SQRT(4);
      |   ^~~~

预处理小结

预处理器指令
指令 说明
#include 包含一个文件
#define 定义一个宏
#undef 取消一个宏定义
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if#ifdef条件不为真,当前条件为真,则编译下面代码
#else 如果前面的#if#ifdef条件不为真,则编译下面代码
#endif #if的结束
#error 输出错误信息

编译链接

编译

  • 编译是将源代码翻译成目标体系结构指令集(ISA)的过程
    • 输出为汇编代码(.s文件)或目标代码(.o文件)
$ ls code
a.out                define.cpp      helloworld.o  pperror.cpp
conditional-compilation.cpp  helloworld.cpp  helloworld.s  weaponinfo.cpp
$ g++ -c -o code/helloworld.o code/helloworld.cpp # 生成目标文件code/helloworld.o
$ g++ -S -o code/helloworld.s code/helloworld.cpp # 生成汇编文件code/helloworld.s
$ ls code
a.out                define.cpp      helloworld.o  pperror.cpp
conditional-compilation.cpp  helloworld.cpp  helloworld.s  weaponinfo.cpp
$ cat code/helloworld.s
    .file   "helloworld.cpp"
    .text
    .local  _ZStL8__ioinit
    .comm   _ZStL8__ioinit,1,1
    .section    .rodata
.LC0:
    .string "Hello World!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB1731:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    leaq    .LC0(%rip), %rax
    movq    %rax, %rsi
    leaq    _ZSt4cout(%rip), %rax
    movq    %rax, %rdi
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1731:
    .size   main, .-main
    .type   _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2229:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    cmpl    $1, -4(%rbp)
    jne .L5
    cmpl    $65535, -8(%rbp)
    jne .L5
    leaq    _ZStL8__ioinit(%rip), %rax
    movq    %rax, %rdi
    call    _ZNSt8ios_base4InitC1Ev@PLT
    leaq    __dso_handle(%rip), %rax
    movq    %rax, %rdx
    leaq    _ZStL8__ioinit(%rip), %rax
    movq    %rax, %rsi
    movq    _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
    movq    %rax, %rdi
    call    __cxa_atexit@PLT
.L5:
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2229:
    .size   _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
    .type   _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB2230:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $65535, %esi
    movl    $1, %edi
    call    _Z41__static_initialization_and_destruction_0ii
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2230:
    .size   _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
    .section    .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I_main
    .hidden __dso_handle
    .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long   1f - 0f
    .long   4f - 1f
    .long   5
0:
    .string "GNU"
1:
    .align 8
    .long   0xc0000002
    .long   3f - 2f
2:
    .long   0x3
3:
    .align 8
4:

多文件项目组织

  • C++项目通常包含多个源文件(.c),每个源文件单独编译,即一个编译单元
  • 源文件之间通过头文件.h)相互引用
  • 头文件中一般仅包含声明语句,如函数声明类型声明变量声明
main.cpp
#include <iostream>
#include <getopt.h>
using namespace std;
#include "poems.h"
#include "tangshi.h"
#include "songci.h"

int main(int argc, char *argv[]) {
  registerDufu();
  registerLibai();
  registerGaoshi();
  registerSushi();
  registerXinqiji();
  registerLiqingzhao();

  const static char *usage = "Usage: poetry [-h] [-a author] [-t title]\n";
tangshi.h
#ifndef TANGSHI_H
#define TANGSHI_H

void registerDufu();
void registerLibai();
void registerGaoshi();

#endif
dufu.cpp
void registerDufu() {
  const string author = "杜甫";
  registerPoem(author, "登高", denggao);
  registerPoem(author, "春望", chunwang);
  registerPoem(author, "月夜", yueye);
  registerPoem(author, "登岳阳楼", dengyueyanglou);
}

“诗歌”

圣人云

“不学诗,无以言。”

——《论语·季氏》

  • “诗歌”是一个由多个源文件与头文件组成的C++项目
    • 包含一个极小诗词库
    • 支持根据作者或题名查询诗词
./poetry/poetry -t "登岳阳楼"
      登岳阳楼      
昔闻洞庭水,今上岳阳楼。
吴楚东南坼,乾坤日夜浮。
亲朋无一字,老病有孤舟。
戎马关山北,凭轩涕泗流。
./poetry/poetry -t "新安吏"
Not found: 新安吏
G main main.cpp tangshi tangshi.h main->tangshi songci songci.h main->songci poemsh poems.h main->poemsh dufu dufu.cpp dufu->tangshi dufu->poemsh libai libai.cpp libai->tangshi libai->poemsh gaoshi gaoshi.cpp gaoshi->tangshi gaoshi->poemsh sushi sushi.cpp sushi->songci sushi->poemsh xinqiji xinqiji.cpp xinqiji->songci xinqiji->poemsh liqingzhao liqingzhao.cpp liqingzhao->songci liqingzhao->poemsh poems poems.cpp poems->poemsh
图 1: 诗词项目文件组织(实线代表include,虚线表示声明
poetry
├── Makefile
├── include
│   ├── format.h
│   ├── poems.h
│   ├── songci.h
│   └── tangshi.h
├── lib
│   ├── dufu.cpp
│   ├── gaoshi.cpp
│   ├── libai.cpp
│   ├── liqingzhao.cpp
│   ├── poems.cpp
│   ├── sushi.cpp
│   └── xinqiji.cpp
└── main.cpp

2 directories, 13 files

编译“诗歌”

  • 将诗歌项目的所有源文件(.cpp)编译到目标代码
g++ -c -I poetry/include -o poetry/main.o poetry/main.cpp
g++ -c -I poetry/include -o poetry/lib/dufu.o poetry/lib/dufu.cpp
g++ -c -I poetry/include -o poetry/lib/libai.o poetry/lib/libai.cpp
g++ -c -I poetry/include -o poetry/lib/gaoshi.o poetry/lib/gaoshi.cpp
g++ -c -I poetry/include -o poetry/lib/sushi.o poetry/lib/sushi.cpp
g++ -c -I poetry/include -o poetry/lib/xinqiji.o poetry/lib/xinqiji.cpp
g++ -c -I poetry/include -o poetry/lib/liqingzhao.o poetry/lib/liqingzhao.cpp
g++ -c -I poetry/include -o poetry/lib/poems.o poetry/lib/poems.cpp
ls -R poetry
poetry:
Makefile  include  lib  main.cpp  main.o

poetry/include:
format.h  poems.h  songci.h  tangshi.h

poetry/lib:
dufu.cpp    gaoshi.o   liqingzhao.cpp  poems.o    xinqiji.cpp
dufu.o      libai.cpp  liqingzhao.o    sushi.cpp  xinqiji.o
gaoshi.cpp  libai.o    poems.cpp       sushi.o

链接

  • 链接是将多个目标代码文件与库文件链接为一个可执行文件的过程
$ g++ -o poetry/poetry poetry/main.o poetry/lib/*.o
$ ./poetry/poetry -t "一剪梅"
           一剪梅           
红藕香残玉簟秋。轻解罗裳,独上兰舟。
云中谁寄锦书来?雁字回时,月满西楼。
花自飘零水自流。一种相思,两处闲愁。
此情无计可消除,才下眉头,却上心头。
$ ./poetry/poetry -a "辛弃疾"
         破阵子         
醉里挑灯看剑,梦回吹角连营。
八百里分麾下炙,五十弦翻塞外声。
沙场秋点兵。
马作的卢飞快,弓如霹雳弦惊。
了却君王天下事,赢得生前身后名。
可怜白发生!

           青玉案           
东风夜放花千树,更吹落,星如雨。
宝马雕车香满路,凤箫声动,玉壶光转,一夜鱼龙舞。
蛾儿雪柳黄金缕,笑语盈盈暗香去。
众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。

Makefile(自学)

大型项目管理

  • 大型C++项目通常由大量源文件组成,如Linxu内核(24K+文件,40M+LOC)
    • 每个文件都需要编译
    • 需要链接生成多个可执行文件

考考你

  • 当向“诗歌”项目增加一个新的文件wangwei.cpp时,需要如何调整编译和链接过程?
  • 当向原有的dufu.cpp文件中新增一首律诗时,需要如何调整编译和链接过程?
  • 当我改变了poems.h这一被多个源文件包含的头文件时,需要如何调整编译和链接过程?
  • 为了便于管理大项目的构建过程,人们发明了Makefile
    • Makefile文件用于描述源文件与构建目标(可执行程序、库)的关系
    • make工具根据Makefile中的描述自动执行编译和链接过程
    • 一旦源文件被修改,make工具会自动重新构建整个项目

Makefile规则

  • Makefile文件由一系列规则组成,每条规则由目标依赖命令三部分组成
    • 目标是规则中要生成的对象
    • 依赖是生成目标文件所依赖的文件
      • 当目标还未生成,或任何依赖文件的修改时间晚于目标时,规则被触发
    • 命令是规则被触发时执行的命令
Makefile规则示例
# 当helloworld.cpp发生修改时,运行下方的命令以重新生成helloworld
helloworld: helloworld.cpp
    g++ -o helloworld helloworld.cpp # 注意!命令之前必须是一个Tab键

“诗歌”的Makefile

INC = -I./include # 头文件搜索路径
CFLAGS = -Wall -Wextra -Wpedantic -Werror -std=c++17 $(INC) # 编译选项

# 全部源文件列表
SRC = main.cpp $(wildcard lib/*.cpp)
# 全部目标文件列表,由SRC列表中的源文件名替换为.o后缀得到
OBS = $(SRC:.cpp=.o)

# 默认目标,make命令将以构建此文件作为终极目标
all: poetry

# poetry可执行程序依赖所有目标文件,构建命令为使用g++链接所有目标文件
poetry: $(OBS)
	g++ -o $@ $^

# 目标文件依赖其对应的源文件,构建命令为使用g++编译源文件
%.o: %.cpp
	g++ $(CFLAGS) -c -o $@ $<

# 伪目标,清除所有目标文件
clean:
	rm -rf $(OBS) poetry

构建“诗歌”

  • 进入poetry目录,执行make命令
$ cd poetry
$ make
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o main.o main.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/dufu.o lib/dufu.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/gaoshi.o lib/gaoshi.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/libai.o lib/libai.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/liqingzhao.o lib/liqingzhao.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/poems.o lib/poems.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/sushi.o lib/sushi.cpp
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/xinqiji.o lib/xinqiji.cpp
g++ -o poetry main.o lib/dufu.o lib/gaoshi.o lib/libai.o lib/liqingzhao.o lib/poems.o lib/sushi.o lib/xinqiji.o

更新“诗歌”

  • 更新dufu.cpp文件,再次执行make命令
$ touch lib/dufu.cpp # 仅更新时间戳,不实际改变文件内容
$ make
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 -I./include   -c -o lib/dufu.o lib/dufu.cpp
g++ -o poetry main.o lib/dufu.o lib/gaoshi.o lib/libai.o lib/liqingzhao.o lib/poems.o lib/sushi.o lib/xinqiji.o

总结

  • 学习内容
    • Linux命令行
    • 编译阶段与编译器驱动程序
    • 预处理
    • 编译链接
    • Makefile

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
  工具链
    Linux命令行
      pwd, cd, mkdir, rmdir
      ls, cp, rm, mv
    阶段与编译器驱动程序
      编译阶段
      编译器驱动程序
    Makefile
      Makefile规则
        目标
        依赖
        命令
    预处理指令
      #include
      #define, #undef
      #if, #else, #elif, #endif
      #ifdef, #ifndef
      #error

学习目标

  • 学会使用最基本的Linux命令
  • 掌握常见的预处理指令使用方法
  • 理解C++项目的组织方式并运用C++工具链构建一个完整项目

课后作业

  • 预习
    • 教材9.1–9.2:输入输出
  • 搞懂程序如何编译、链接、执行,使用命令行给自己兜底

本次课后,不再解答有关

如何编译、执行的问题

计算机程序设计