2. C++的编译/链接模型

C++的编译/链接模型

  • 简单的加工模型

image-20230921181626931

  • 问题:无法处理大型程序
    • 加工耗时较长
    • 即使少量修改,也需要重新加工
  • 解决方案:分块处理

image-20230921181841541

  • 好处
    • 编译消耗资源,但一次处理输入较少
    • 链接程序较多,但处理速度较快
    • 便于程序修改升级
  • 由“分块处理”衍生出的概念
    • 定义 / 声明
    • 头文件 / 源文件
    • 翻译单元
      • 源文件 + 相关头文件(直接 / 间接)- 应忽略的预处理语句
    • 一处定义原则:
      • 程序级:一般函数
      • 翻译单元级:内联函数、类、模板

image-20230921183410297

编译过程

预处理 --> 编译 --> 汇编 --> 链接

假如我们的源文件名为 hello.cpp

#include <iostream>

int main(void)
{
    std::cout << "Hello world!\n";
    return 0;
}
  • 预处理,生成预处理文件 hello.i
g++ -E hello.c -o hello.i
  • 编译,生成汇编文件 hello.s
g++ hello.i -S -o hello.s
  • 汇编,生成目标文件 hello.o
g++ hello.s -c -o hello.o
  • 链接,将多个目标文件进行链接,生成可执行文件 hello
g++ hello.o -s -o hello

然后执行可执行文件:

./hello

执行结果:

Hello world!
  • 预处理

    • 将源文件转换为翻译单元的过程
    • 防止头文件被循环展开
      • #ifndef 解决方案

        举一个循环定义的例子,目录结构和源码如下:

        .
        ├── header1.h
        ├── header2.h
        └── main.cpp
        

        header1.h

        #include "header2.h"
        
        int fun();
        

        header2.h

        #include "header1.h"
        
        int fun2();
        

        main.cpp

        #include "header1.h"
        
        int main()
        {
            
        }
        

        我们进行编译:

        g++ -Wall -g main.cpp -o main
        

        出错,如下:

        In file included from header2.h:1,
                         from header1.h:1,
                         from header2.h:1,
                         from header1.h:1,
                         ......
        header1.h:1:21: error: #include nested depth 200 exceeds maximum of 200 (use -fmax-include-depth=DEPTH to increase the maximum)
            1 | #include "header2.h"
              | 
        

        如果是智能的编译器,现在会提示“嵌套层数太多”这种提示:

        我们对上述代码添加如下语句:

        header1.h

        #ifndef HEADER2
        #define HEADER2
        #include "header2.h"
        int fun();
        #endif
        

        header2.h

        #ifndef HEADER2
        #define HEADER2
        #include "header1.h"
        int fun2();
        #endif
        

        然后再进行编译,就不会出问题了。但是如果使用了 clangd 来做提示,它仍旧会出现错误提醒,这时候编译是没有问题的,但我们最好不要这样写代码,在这里也只是举个例子。

        该解决方案的不足之处:

        • 会写多余的代码;
        • 由于宏取名(如HEADER2)是随意的,如果发生名称冲突,导致程序出错;
      • #pragma once 解决方案

        该方案更为优秀,原理为对展开进行计数,progma once 即为只允许展开一次,修改后的代码类似这样:

        header1.h

        #pragma once
        
        #include "header2.h"
        
        int fun();
        

        这种处理方式比传统的 ifdef 处理方式更好。

  • 编译

    • 将翻译单元转换为相应的汇编语言表示

    • 编译优化

      optimize.cpp

      int main()
      {
          int res = 0;
          for (int i = 0; i < 1000; ++i)
          {
              res += i;
          }
          return res;
      }
      

      上面的这段代码,编译器在编译期间是可以进行优化的,优化等级从O0-O3,另外还有Os, Ofast 等,O0表示没有任何优化,O3表示最高等级优化:

      g++ -S -O0 optimize.cpp -o optimize.s
      

      从简单的汇编代码行数中我们也能看出,编译优化后的汇编代码更短,执行效率更高。

      更多关于编译优化的资料:

      img

      -O0 :这个等级(字母“O”后面跟个零)关闭所有优化选项,也是 CFLAGS 或 CXXFLAGS 中没有设置-O等级时的默认等级。这样就不会优化代码,这通常不是我们想要的。

      -O1 :这是最基本的优化等级。编译器会在不花费太多编译时间的同时试图生成更快更小的代码。这些优化是非常基础的,但一般这些任务肯定能顺利完成。

      -O2 :-O1 的进阶。这是推荐的优化等级,除非你有特殊的需求。-O2 会比 -O1 启用多一些标记。设置了 -O2 后,编译器会试图提高代码性能而不会增大体积和大量占用的编译时间。

      -O3 :这是最高最危险的优化等级。用这个选项会延长编译代码的时间,并且在使用gcc4.x的系统里不应全局启用。自从3.x版本以来gcc的行为已经有了极大地改变。在3.x,-O3生成的代码也只是比-O2快一点点而已,而gcc4.x中还未必更快。用-O3来编译所有的软件包将产生更大体积更耗内存的二进制文件,大大增加编译失败的机会或不可预知的程序行为(包括错误)。这样做将得不偿失,记住过犹不及。在gcc 4.x.中使用-O3是不推荐的。

      -Os :这个等级用来优化代码尺寸。其中启用了-O2中不会增加磁盘空间占用的代码生成选项。这对于磁盘空间极其紧张或者CPU缓存较小的机器非常有用。但也可能产生些许问题,因此软件树中的大部分ebuild都过滤掉这个等级的优化。使用-Os是不推荐的。

      编译优化的缺点:可能导致单步调试出错,虽然执行速度加快了,但是不利于调试。

    • 增量编译 VS 全量编译

  • 链接

    • 合并多个目标文件,关联声明与定义
    • 连接(Linkage)种类:内部连接、外部连接、无连接
    • 链接常见错误:找不到定义

    link_err.cpp

    #include <iostream>
    
    extern int x;   // 使用外部定义的x
    
    int main(void)
    {
        std::cout << x << std::endl;
        return 0;
    }
    
    // g++ -Wall -g link_err.cpp define_x.cpp -o link_err 
    

    define_x.cpp

    int x = 111;
    

    编译上述代码:

    g++ -Wall -g link_err.cpp define_x.cpp -o link_err
    

    可以正常生成可执行文件link_err

    但当我们编译的时候,遗忘了 define_x.cpp 的时候,就会出现如下错误:

    g++ -Wall -g link_err.cpp -o link_err
    /usr/bin/ld: /tmp/ccp6ekAb.o: warning: relocation against `x' in read-only section `.text'
    /usr/bin/ld: /tmp/ccp6ekAb.o: in function `main':
    /home/yuzu/deeping_into_cpp/ch1/link_err.cpp:7: undefined reference to `x'
    /usr/bin/ld: warning: creating DT_TEXTREL in a PIE
    collect2: error: ld returned 1 exit status
    

    其中 /home/yuzu/deeping_into_cpp/ch1/link_err.cpp:7: undefined reference to x 说明了问题所在:指向了未定义的引用 x

注意:

  • C++ 的编译 / 链接过程是复杂的,预处理、编译与链接都可能出错
  • 编译可能产生警告、错误,都要重视
posted @ 2024-01-24 21:44  kobayashilin1  阅读(45)  评论(0编辑  收藏  举报