g++编译过程学习笔记

学习用例

  使用很简单的多文件编译项目,进行编译过程的学习,主要文件构成如下:

.
├── include
│   └── hello.h
└── src
    ├── hello.cpp
    └── main.cpp

  其中hello.h声明了一个可以输出Hello World!的函数并在hello.cpp中完成实现。main.cpp中调用hello.cpp,运行时输出相应的内容。

  • hello.h
#ifndef HELLO_H_
#define HELLO_H_
    void SayHello();
#endif
  • hello.cpp
#include "hello.h"
#include<iostream>
void SayHello(){
    std::cout<<"Hello World!"<<std::endl;
}
  • main.cpp
#include"hello.h"
int main(){
    SayHello();
    return 0;
}

g++命令及参数介绍

  • -g 编译生成带调试信息的可执行文件
    g++ -g main.cpp
  • -O[n] 优化源代码
    # -O0 不做优化
    # -O1 默认优化
    # -O2 除了完成-O1的优化之外,还进行一些额外的调整工作,如指令调整等。
    # -O3 则包括循环展开和其他一些与处理特性相关的优化工作。 
    # 选项将使编译的速度比使用 -O 时慢, 但通常产生的代码执行速度会更快
    g++ -O2 main.cpp
  • -L-l 指定库文件路径
    # -L 后为库文件路径,可以是绝对路径也可以是相对路径
    # -l 后紧跟库名,即libmytest.a 掐头(lib)去尾(.a)剩下的名字
    g++ -L ../lib/ -lmytest main.cpp
  • -I 指定头文件搜索路径
    g++ -I ../include/ main.cpp
  • -o 指定输出文件名
    g++ main.cpp -o main

编译过程

  大多数时候,作为初学者的我们使用IDE或者各种扩展工具来完成cpp文件的编译,此过程中我们习惯性地忽略编译过程。但是,想要学习使用CMake构建大型项目时,如果能够熟悉编译过程,那么就能够很快的学会编写CMakeLists.txt文件。
  这篇学习笔记参考了很多优秀的教程和说明,如有兴趣可以参考相关教程1相关教程2相关教程3

预处理过程(Pre-Proccessing)

  预处理阶段:主要完成对包含的头文件和宏定义以及注释等的处理工作,头文件的包含和宏定义其实大多采用直接替换的策略实现的。
  在src文件夹下使用以下命令可以实现对源文件的预处理操作,指定生成.ii文件:

g++ -E hello.cpp -I ../include -o hello.ii
g++ -E main.cpp -I ../include -o main.ii

  可以通过查看.ii文件,了解预处理过程产生的结果。比如,这里展示一下main.ii文件内容如下:

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "../include/hello.h" 1

    void SayHello();
# 2 "main.cpp" 2
int main(){
    SayHello();
    return 0;
}

编译过程(Compiling)

  编译阶段:主要进行语法错误的检查,如果检查无误,则将代码翻译成汇编语言。
  在src文件夹下使用以下命令执行编译过程,指定生成.s文件:

g++ -S hello.ii -o hello.s
g++ -S main.ii -o main.s

  这一过程将生成汇编代码,不同的机器可能生成的汇编代码有所不同,这里展示一下main.s的文件内容如下:

    .file   "main.cpp"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    call    _Z8SayHellov@PLT
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.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:

汇编过程(Assembling)

  汇编阶段:主要是将汇编代码生成机器可执行的目标代码,即二进制码,可以发现编辑器已经不能识别和查看二进制文件了。
  在src文件夹下使用以下命令执行汇编过程,最后将生成.o文件:

g++ -c hello.s -o hello.o
g++ -c main.s -o main.o

链接过程(Linking)

  链接阶段:主要将各个.o文件进行链接生成可执行文件,核心工作是解决各个模块之间相互引用的问题。linux系统中生成的可执行文默认为a.out;windows系统中生成的可执行文件后缀为.exe;可以使用-o参数来指定生成的可执行文件名称。
  在src文件下使用以下命令生成可执行文件:

g++ main.o hello.o -o main

运行可执行文件

  运行可执行文件,查看是否正确完成源文件到可执行文件的编译过程:

./main

  程序正常运行,并打印了Hello world!,如下图所示:

静态编译和动态编译

  编译cpp文件的过程又分为静态编译和动态编译两种。
  静态编译是将所有的模块都编译进可执行文件中,当启动可执行文件时,所有的模块都已经具备了,因此具有加载速度快,执行速度快的特点,但是程序体积也会更大,一旦静态库需要更新,程序就需要重新编译,多个程序使用时都需要单独加载,比较浪费内存。
  动态编译是将应用程序所需要的模块都编译成动态链接库,当程序启动时,并不会加载所有的模块,只有用到某个模块时才动态的加载使用的模块,多个程序可以使用同一个加载到内容中的动态库,因此Linux中动态库也称为共享库。

静态库和动态库

静态库

  Linux中的静态库由程序ar将.o文件打包生成,目标文件以lib为前缀,以.a作为文件后缀,中间的库名称可以自定义。比如:libhello.a
  生成的静态库需要连同相应的头文件发布给使用者,以链接到可执行文件中。

生成并使用静态库

  • 第一步:生成.o文件
    在src文件夹下使用以下命令由hello.cpp生成.o文件:

    g++ -c hello.cpp -I ../include/
  • 第二步:打包成静态库
    在src文件夹下使用以下命令将生成的.o文件打包成名为libhello.a静态库文件:

    ar crsv libhello.a hello.o

    其中c参数不管是否存在,都创建一个库;r参数表示如果模块名已存在则替换;s参数表示创建目标文件索引;v参数表示输出详细信息。

  • 第三步:使用静态库编译源文件
    在src文件夹下使用以下命令编译main.cpp,并指定输出名为static_main的可执行文件:

    g++ main.cpp -I ../include/ -L ./ -lhello -o static_main
  • 执行生成的可执行文件
    在src文件夹下执行可执行文件static_main,以验证是否正常编译。

    ./static_main 

  程序正常执行,并输出Hello World!

动态库

  Linux中的动态库的目标文件以lib为前缀,以.so作为文件后缀,中间的库名称可以自定义。比如:libhello.so
  动态库中的函数和变量地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址只有在应用程序加载动态库时才形成。