第06讲:工具链

《计算机程序设计》

苏醒

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

课前准备

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

  • 打开命令行窗口,执行以下命令打开随堂练习目录

    $ mkdir -p course # 创建课程练习目录(如果已存在则无需创建)
    $ cd course       # 切换到课程练习目录
  • 在命令行中安装软件

    $ pacman -S base-devel tree man
  • 将下载的poetry-win.tar.gz拷贝到刚刚打开的目录窗口中

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

    $ tar -xf poetry-win.tar.gz
  • 使用VSCode/Cursor打开当前目录

温故

考考你

阅读下列程序,判断程序输出是什么?

#include <iostream>

void maxmin(double a, double b, double c, double &max, double &min) {
  max = min = a;
  if (b > max) max = b;
  if (c > max) max = c;
  if (b < min) min = b;
  if (c < min) min = c;
  return;
}

double max, min;

int main() {
  double a = 1.0, b = 2.0, c = 3.0;
  maxmin(a, b, c, max, min);
  std::cout << "max = " << max << std::endl;
  std::cout << "min = " << min << std::endl;
  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

3 directories, 13 files

Try it!

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

学习目标

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

学习目标

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

知其然,知其所以然

Linux命令行

常见的Linux命令

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

温馨提示

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

$ man ls
CP(1)                            User Commands                           CP(1)

NAME
       cp - copy files and directories

SYNOPSIS
       cp [OPTION]... [-T] SOURCE DEST
       cp [OPTION]... SOURCE... DIRECTORY
       cp [OPTION]... -t DIRECTORY SOURCE...

DESCRIPTION
       Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.

       Mandatory  arguments  to  long  options are mandatory for short options
       too.

       -a, --archive
              same as -dR --preserve=all

       --attributes-only
              don't copy the file data, just the attributes

       --backup[=CONTROL]
              make a backup of each existing destination file

       -b     like --backup but does not accept an argument

       --copy-contents
              copy contents of special files when recursive

       -d     same as --no-dereference --preserve=links

       --debug
              explain how a file is copied.  Implies -v

       -f, --force
              if an existing destination file cannot be opened, remove it  and
              try  again  (this  option  is ignored when the -n option is also
              used)

       -i, --interactive
              prompt before overwrite (overrides a previous -n option)

       -H     follow command-line symbolic links in SOURCE

       -l, --link
              hard link files instead of copying

       -L, --dereference
              always follow symbolic links in SOURCE

       -n, --no-clobber
              do not overwrite an existing file and do not fail  (overrides  a
              -u  or  previous  -i  option).  See also --update; equivalent to
              --update=none.

       -P, --no-dereference
              never follow symbolic links in SOURCE

       -p     same as --preserve=mode,ownership,timestamps

       --preserve[=ATTR_LIST]
              preserve the specified attributes

       --no-preserve=ATTR_LIST
              don't preserve the specified attributes

       --parents
              use full source file name under DIRECTORY

       -R, -r, --recursive
              copy directories recursively

       --reflink[=WHEN]
              control clone/CoW copies. See below

       --remove-destination
              remove each existing destination file before attempting to  open
              it (contrast with --force)

       --sparse=WHEN
              control creation of sparse files. See below

       --strip-trailing-slashes
              remove any trailing slashes from each SOURCE argument

       -s, --symbolic-link
              make symbolic links instead of copying

       -S, --suffix=SUFFIX
              override the usual backup suffix

       -t, --target-directory=DIRECTORY
              copy all SOURCE arguments into DIRECTORY

       -T, --no-target-directory
              treat DEST as a normal file

       --update[=UPDATE]
              control     which    existing    files    are    updated;    UP‐
              DATE={all,none,older(default)}.  See below

       -u     equivalent to --update[=older]

       -v, --verbose
              explain what is being done

       -x, --one-file-system
              stay on this file system

       -Z     set SELinux security context of destination file to default type

       --context[=CTX]
              like -Z, or if CTX is specified then set the  SELinux  or  SMACK
              security context to CTX

       --help display this help and exit

       --version
              output version information and exit

       ATTR_LIST  is  a  comma-separated  list  of  attributes. Attributes are
       'mode' for permissions (including any ACL and xattr permissions), 'own‐
       ership' for user and group, 'timestamps' for file  timestamps,  'links'
       for  hard  links,  'context' for security context, 'xattr' for extended
       attributes, and 'all' for all attributes.

       By default, sparse SOURCE files are detected by a crude  heuristic  and
       the corresponding DEST file is made sparse as well.  That is the behav‐
       ior  selected  by  --sparse=auto.   Specify --sparse=always to create a
       sparse DEST file whenever the SOURCE file contains a  long  enough  se‐
       quence of zero bytes.  Use --sparse=never to inhibit creation of sparse
       files.

       UPDATE  controls  which existing files in the destination are replaced.
       'all' is the default operation when an --update option  is  not  speci‐
       fied,  and  results  in all existing files in the destination being re‐
       placed.  'none' is similar to the --no-clobber option, in that no files
       in the destination are replaced, but also skipped files do not induce a
       failure.  'older' is the default operation when --update is  specified,
       and  results  in  files being replaced if they're older than the corre‐
       sponding source file.

       When --reflink[=always] is specified, perform a lightweight copy, where
       the data blocks are copied only when modified.  If this is not possible
       the copy fails, or if --reflink=auto is specified, fall back to a stan‐
       dard copy.  Use --reflink=never to ensure a standard copy is performed.

       The  backup  suffix  is  '~',  unless  set  with   --suffix   or   SIM‐
       PLE_BACKUP_SUFFIX.   The version control method may be selected via the
       --backup option or through the  VERSION_CONTROL  environment  variable.
       Here are the values:

       none, off
              never make backups (even if --backup is given)

       numbered, t
              make numbered backups

       existing, nil
              numbered if numbered backups exist, simple otherwise

       simple, never
              always make simple backups

       As  a  special  case,  cp  makes  a backup of SOURCE when the force and
       backup options are given and SOURCE and DEST are the same name  for  an
       existing, regular file.

AUTHOR
       Written by Torbjorn Granlund, David MacKenzie, and Jim Meyering.

REPORTING BUGS
       GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
       Report any translation bugs to <https://translationproject.org/team/>

COPYRIGHT
       Copyright  ©  2023  Free Software Foundation, Inc.  License GPLv3+: GNU
       GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
       This is free software: you are free  to  change  and  redistribute  it.
       There is NO WARRANTY, to the extent permitted by law.

SEE ALSO
       install(1)

       Full documentation <https://www.gnu.org/software/coreutils/cp>
       or available locally via: info '(coreutils) cp invocation'

GNU coreutils 9.4                 April 2024                             CP(1)

Linux目录结构

  • Linux系统采用单一树状目录结构
    • 所有目录均挂在根目录/
    • 用户的主目录一般为/home/<username>
  • 一些特殊目录
    • ~:当前用户主目录
    • .:当前目录
    • ..:父目录
表 1: 根目录结构
/
├── bin -> usr/bin
├── bin.usr-is-merged
├── boot
├── dev
├── etc
├── home
├── init
├── lib -> usr/lib
├── lib.usr-is-merged
├── lib32 -> usr/lib32
├── lib64 -> usr/lib64
├── libx32 -> usr/libx32
├── lost+found
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin -> usr/sbin
├── sbin.usr-is-merged
├── snap
├── srv
├── sys
├── tmp
├── usr
├── var
├── wslBPdpko
├── wslBiEINA
├── wslDBdopA
├── wslDaOfkA
├── wslEmNJjo
├── wslHDONiA
├── wslHPGdjp
├── wslHiLNLo
├── wslJcaJjp
├── wslKCknkA
├── wslMDcOko
├── wslMEJmko
├── wslMpPLap
├── wsldEiphp
├── wsldfdJlA
├── wslehBoko
├── wslfFAHAp
├── wslfimGAB
├── wslgBiboA
├── wslgIGEMA
├── wslhmJDlA
├── wslkjDNAB
├── wslmPGJkA
├── wsloFOajp
└── wsloJNNHp

52 directories, 1 file
表 2: 用户主目录结构
/home
└── xsu
    ├── a.out
    ├── cnb
    ├── course
    ├── diary
    ├── opt
    ├── supervision
    ├── t.c
    ├── teaching
    └── workspace

8 directories, 3 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  2025 Makefile
drwxr-xr-x 2 xsu xsu 4.0K Mar 10  2025 include
drwxr-xr-x 2 xsu xsu 4.0K Mar 24 12:35 lib
-rw-r--r-- 1 xsu xsu  829 Mar 10  2025 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
 36589  82290 907774 code/helloworld.i
 36596  82305 907871 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
conditional-compilation.cpp  helloworld.cpp  weaponinfo.cpp
define.cpp           pperror.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
conditional-compilation.cpp  helloworld.cpp  helloworld.s  weaponinfo.cpp
define.cpp           helloworld.o    pperror.cpp
$ cat code/helloworld.s
    .file   "helloworld.cpp"
    .text
#APP
    .globl _ZSt21ios_base_library_initv
    .section    .rodata
.LC0:
    .string "Hello World!\n"
#NO_APP
    .text
    .globl  main
    .type   main, @function
main:
.LFB1988:
    .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
.LFE1988:
    .size   main, .-main
    .section    .rodata
    .type   _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE, @object
    .size   _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE, 1
_ZNSt8__detail30__integer_to_chars_is_unsignedIjEE:
    .byte   1
    .type   _ZNSt8__detail30__integer_to_chars_is_unsignedImEE, @object
    .size   _ZNSt8__detail30__integer_to_chars_is_unsignedImEE, 1
_ZNSt8__detail30__integer_to_chars_is_unsignedImEE:
    .byte   1
    .type   _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE, @object
    .size   _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE, 1
_ZNSt8__detail30__integer_to_chars_is_unsignedIyEE:
    .byte   1
    .ident  "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.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++项目通常包含多个源文件(.cpp),每个源文件单独编译,即一个编译单元
  • 源文件之间通过头文件.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

3 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:输入输出
  • 搞懂程序如何编译、链接、执行,使用命令行给自己兜底

本次课后,不再解答有关

如何编译、执行的问题

计算机程序设计