【C++】泛型编程 ⑤ ( 函数模板原理 | C++ 编译器原理 | C / C++ 编译器编译过程 | 分析 模板函数代码 汇编文件 | 编译 模板函数代码 汇编文件 | 模板函数汇编分析总结 )

雨点打透心脏的1/2处 2024-02-17 08:20 97阅读 0赞

文章目录

  • 一、C++ 编译器原理
    • 1、gcc 编译器简介
    • 2、C / C++ 编译器编译过程
    • 3、gcc 编译器各阶段命令
      • ① 预处理 Pre-Processing ( 预处理器 )
      • ② 编译 Compiling ( 编译器 )
      • ③ 汇编 Assembling ( 汇编器 )
      • ④ 链接 Linking ( 链接器器 )
    • 4、gcc 编译器 与 g++ 编译器 的区别
    • 5、gcc / g++ 编译器常用命令选项
  • 二、分析 模板函数代码 汇编文件
    • 1、编译 模板函数代码 汇编文件
    • 2、分析 模板函数代码 汇编文件
    • 3、模板函数代码 汇编文件 分析总结 ( 重要 )

在前面几篇博客

  • 【C++】泛型编程 ③ ( 函数模板 与 普通函数 调用规则 | 类型匹配 | 显式指定函数模板泛型类型 )
  • 【C++】泛型编程 ④ ( 函数模板 与 普通函数 调用规则 | 类型自动转换 | 类型自动转换 + 显式指定泛型类型 )

中 , 函数模板 可以与 重载的 普通函数 放在一起 , 二者之间 的调用 有 不同的优先级 ;

在一定程度上 , 说明 函数模板 和 普通函数 有着相似性 ,

在本篇博客中 分析 C++ 编译器的 函数模板 实现底层机制 ;

一、C++ 编译器原理


1、gcc 编译器简介

gcc 编译器 英文名称是 “ GNU C Compiler “ ,

  • 支持编译多种语言 , 可以解析不同的语言 , 如 : C , C++ , Java , Pascal 等语言 ;
  • 是可移植编译器 ;

    • 支持多种平台 , 如 : Linux , Windows , Mac 等 ;
    • gcc 编译器 不仅可以编译 普通的 C 语言应用程序源码 , 还能编译 Linux 内核 ;
    • 支持交叉编译 , 如 : 在 x86 硬件上编译 arm 程序 ;
  • 模块化设计 : gcc 编译器是按照模块化设计的 , 可以加入新的编程语言和新的 CPU 架构 ;

2、C / C++ 编译器编译过程

参考 【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 ) 博客 , C 语言 程序的编译 需要经过 预处理 , 编译 , 汇编 , 链接 操作 , 分别需要使用 预处理器 , 编译器 , 汇编器 , 链接器 四个工具 ;

在这里插入图片描述

集成开发环境 将 预处理器 , 编译器 , 汇编器 , 链接器 四个工具 集成到了一起 ;

打开 Visual Studio 中解决方案 所在目录 , 其中就有 编译过程 中产生的大量的 中间文件 ;

在这里插入图片描述

3、gcc 编译器各阶段命令

① 预处理 Pre-Processing ( 预处理器 )

预处理 Pre-Processing : 展开 宏定义 , 得到预处理文件 ;

  1. gcc Test.c -o Test.i

也可以加上 -E 选项 ;

  1. gcc -E Test.c -o Test.i

② 编译 Compiling ( 编译器 )

编译 Compiling : 将预处理文件编译成 汇编文件 ;

  1. gcc Test.i -o Test.S

直接从 Test.c 源码生成 汇编文件 :

  1. gcc -S Test.c -o Test.S

③ 汇编 Assembling ( 汇编器 )

汇编 Assembling : 将 汇编文件 编译成 二进制机器码文件 ;

  1. gcc Test.S -o Test.o

直接从 Test.c 源码生成 机器码文件 :

  1. gcc -c Test.c -o Test.o

④ 链接 Linking ( 链接器器 )

链接 Linking : 将 二进制机器码文件 链接成 可执行文件 ;

  1. gcc Test.o -o Test.exe

直接生成可执行文件 :

  • 生成默认的 a.exe 可执行文件命令 :

    gcc Test.c

  • 指定要生成的 可执行 文件名称 命令 :

    gcc Test.c -o Test.exe

编译 C++ 代码 , 将 gcc 改为 g++ 即可 ;

4、gcc 编译器 与 g++ 编译器 的区别

gcc 编译器 与 g++ 编译器 的区别如下 :

  • 语言区别 : gcc 编译器 是 C 语言编译器 , 编译后缀为 .c 的文件 ; g++ 编译器 是 C++ 编译器 , 编译后缀为 .cpp 的文件 和 后缀为 .c 的文件 , 两者都当C++文件处理 ;
  • 编译阶段区别 : 在编译阶段 , g++ 编译器 会自动链接 STL 库 , 而 gcc 必须要加一个参数 -lstdc++ ;
  • 预定义宏区别 : gcc 在编译 c 文件时 , 可用的预定义宏比较少 ;
  • 链接阶段区别 : 通常使用 g++ 来完成链接,为了统一起见,干脆 编译 / 链接 统统用g++了。
  • 语法区别 : 虽然 C++ 语言 是 C 语言 的超集 , 但是两者对语法的要求是有区别的,C++的语法规则更加严谨一些 ;

5、gcc / g++ 编译器常用命令选项

gcc / g++ 编译器常用命令选项 :

  • -o 选项 : 产生目标文件 , 可以是 .i 预处理文件、.s 汇编文件、.o 二进制机器码文件、可执行文件等 ;
  • -c 选项 : 通知 gcc 编译器 取消链接步骤 , 只生成 .o 二进制机器码文件 ;
  • -E 选项 : 只运行 C 预编译器 , 得到 .i 预处理文件 ;
  • -S 选项 : 通知 gcc 编译器产生汇编语言文件后停止编译 , 也就是只执行 前两步操作 , 产生 .i 预处理文件 和 .s 汇编语言文件 ;
  • -Wall 选项 : 打开编译器警告选项 , 如果源码有问题 , 会发出警告 ;
  • -Idir 选项 : 将 dir 目录加入搜索头文件的目录路径 ;
  • -Ldir 选项 : 将 dir 目录加入搜索库的目录路径 ;
  • -llib 选项 : 链接 lib 库 ;
  • -g 选项 : 在 .o 目标文件 中嵌入调试信息 , 以便 gdb 之类的调试程序调试 ;

二、分析 模板函数代码 汇编文件


1、编译 模板函数代码 汇编文件

在 Test.c 中定义一个简单 函数模板 , 然后再 main 函数中调用该 函数模板 ,

  1. #include "iostream"
  2. using namespace std;
  3. template <typename T>
  4. T add(T a, T b) {
  5. cout << "调用函数模板 T add(T a, T b)" << endl;
  6. return a + b;
  7. }
  8. int main() {
  9. int a = 10, b = 20;
  10. int c = add(a, b);
  11. cout << "函数模板计算结果 : c = " << c << endl;
  12. return 0;
  13. }

执行

  1. g++ -S Test.cpp -o Test.S

命令 , 生成 该 C++ 源码对应的汇编文件 ;

在这里插入图片描述

生成的汇编文件 Test.S 内容如下 :

  1. .file "Test.cpp"
  2. .lcomm __ZStL8__ioinit,1,1
  3. .def ___main; .scl 2; .type 32; .endef
  4. .section .rdata,"dr"
  5. .align 4
  6. LC0:
  7. .ascii "\345\207\275\346\225\260\346\250\241\346\235\277\350\256\241\347\256\227\347\273\223\346\236\234 : c = \0"
  8. .text
  9. .globl _main
  10. .def _main; .scl 2; .type 32; .endef
  11. _main:
  12. leal 4(%esp), %ecx
  13. andl $-16, %esp
  14. pushl -4(%ecx)
  15. pushl %ebp
  16. movl %esp, %ebp
  17. pushl %ecx
  18. subl $36, %esp
  19. call ___main
  20. movl $10, -12(%ebp)
  21. movl $20, -16(%ebp)
  22. movl -16(%ebp), %eax
  23. movl %eax, 4(%esp)
  24. movl -12(%ebp), %eax
  25. movl %eax, (%esp)
  26. call __Z3addIiET_S0_S0_
  27. movl %eax, -20(%ebp)
  28. movl $LC0, 4(%esp)
  29. movl $__ZSt4cout, (%esp)
  30. call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  31. movl %eax, %edx
  32. movl -20(%ebp), %eax
  33. movl %eax, (%esp)
  34. movl %edx, %ecx
  35. call __ZNSolsEi
  36. subl $4, %esp
  37. movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
  38. movl %eax, %ecx
  39. call __ZNSolsEPFRSoS_E
  40. subl $4, %esp
  41. movl $0, %eax
  42. movl -4(%ebp), %ecx
  43. leave
  44. leal -4(%ecx), %esp
  45. ret
  46. .section .rdata,"dr"
  47. .align 4
  48. LC1:
  49. .ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0"
  50. .section .text$_Z3addIiET_S0_S0_,"x"
  51. .linkonce discard
  52. .globl __Z3addIiET_S0_S0_
  53. .def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef
  54. __Z3addIiET_S0_S0_:
  55. pushl %ebp
  56. movl %esp, %ebp
  57. subl $24, %esp
  58. movl $LC1, 4(%esp)
  59. movl $__ZSt4cout, (%esp)
  60. call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  61. movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
  62. movl %eax, %ecx
  63. call __ZNSolsEPFRSoS_E
  64. subl $4, %esp
  65. movl 8(%ebp), %edx
  66. movl 12(%ebp), %eax
  67. addl %edx, %eax
  68. leave
  69. ret
  70. .text
  71. .def ___tcf_0; .scl 3; .type 32; .endef
  72. ___tcf_0:
  73. pushl %ebp
  74. movl %esp, %ebp
  75. subl $8, %esp
  76. movl $__ZStL8__ioinit, %ecx
  77. call __ZNSt8ios_base4InitD1Ev
  78. leave
  79. ret
  80. .def __Z41__static_initialization_and_destruction_0ii; .scl 3; .type 32; .endef
  81. __Z41__static_initialization_and_destruction_0ii:
  82. pushl %ebp
  83. movl %esp, %ebp
  84. subl $24, %esp
  85. cmpl $1, 8(%ebp)
  86. jne L6
  87. cmpl $65535, 12(%ebp)
  88. jne L6
  89. movl $__ZStL8__ioinit, %ecx
  90. call __ZNSt8ios_base4InitC1Ev
  91. movl $___tcf_0, (%esp)
  92. call _atexit
  93. L6:
  94. leave
  95. ret
  96. .def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
  97. __GLOBAL__sub_I_main:
  98. pushl %ebp
  99. movl %esp, %ebp
  100. subl $24, %esp
  101. movl $65535, 4(%esp)
  102. movl $1, (%esp)
  103. call __Z41__static_initialization_and_destruction_0ii
  104. leave
  105. ret
  106. .section .ctors,"w"
  107. .align 4
  108. .long __GLOBAL__sub_I_main
  109. .ident "GCC: (i686-posix-sjlj, built by strawberryperl.com project) 4.9.2"
  110. .def __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc; .scl 2; .type 32; .endef
  111. .def __ZNSolsEi; .scl 2; .type 32; .endef
  112. .def __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_; .scl 2; .type 32; .endef
  113. .def __ZNSolsEPFRSoS_E; .scl 2; .type 32; .endef
  114. .def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
  115. .def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
  116. .def _atexit; .scl 2; .type 32; .endef

2、分析 模板函数代码 汇编文件

.file "Test.cpp" 表示这是 Test.cpp 源码的 汇编文件 ;

.text 表示 下面是代码 ;

_main: 表示 后面是 main 函数 ;

call __Z3addIiET_S0_S0_ 调用的是 函数模板 , 下面看函数模板的 汇编内容 :

函数模板 的 函数声明 对应的汇编如下 :

  1. LC1:
  2. .ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0"
  3. .section .text$_Z3addIiET_S0_S0_,"x"
  4. .linkonce discard
  5. .globl __Z3addIiET_S0_S0_
  6. .def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef

这是一个模板函数的汇编版本,函数名为add,它接受两个参数,都是int类型(T在上下文中可以推断为int)。

  • .ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0" 这行代码是一个ASCII字符串,它表示函数模板的名称和一些模板参数。这个字符串在汇编代码中可能不会直接出现,而是由编译器插入的。
  • .section .text$_Z3addIiET_S0_S0_,"x" 这行代码定义了一个section(段),其中$_Z3addIiET_S0_S0_是该section的名称。Section名称通常是由编译器生成的,用于存储特定类型的代码或数据。在这种情况下,该section包含的是add函数的实现。”x”表示该section是可执行的。
  • .linkonce discard 这个指示告诉链接器,如果该文件在其他地方被链接了,就丢弃重复的代码。这是一种优化手段,可以避免在最终的可执行文件中包含重复的代码。
  • .globl __Z3addIiET_S0_S0_ 这行代码声明了全局符号__Z3addIiET_S0_S0_。在C++中,编译器会为每个模板函数生成一个特定的符号名称,这是模板函数的实例化。
  • .def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef 这行代码定义了符号__Z3addIiET_S0_S0_,并设置了一些属性。这些属性可能是由链接器或其他工具使用的,以确定如何处理该符号。

函数模板 的 函数体内容 回应的汇编如下 :

  1. __Z3addIiET_S0_S0_:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. subl $24, %esp
  5. movl $LC1, 4(%esp)
  6. movl $__ZSt4cout, (%esp) # 开始打印日志
  7. call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  8. movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
  9. movl %eax, %ecx
  10. call __ZNSolsEPFRSoS_E # 打印日志结束
  11. subl $4, %esp
  12. movl 8(%ebp), %edx
  13. movl 12(%ebp), %eax
  14. addl %edx, %eax
  15. leave
  16. ret
  17. .text
  18. .def ___tcf_0; .scl 3; .type 32; .endef

对应的 C++ 代码如下 :

  1. template <typename T>
  2. T add(T a, T b) {
  3. cout << "调用函数模板 T add(T a, T b)" << endl;
  4. return a + b;
  5. }

打印日志

  1. cout << "调用函数模板 T add(T a, T b)" << endl;

对应的汇编内容 :

  1. movl $__ZSt4cout, (%esp) # 开始打印日志
  2. call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  3. movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
  4. movl %eax, %ecx
  5. call __ZNSolsEPFRSoS_E # 打印日志结束

3、模板函数代码 汇编文件 分析总结 ( 重要 )

C++ 编译器 将 函数模板 编译成了 汇编函数 call __Z3addIiET_S0_S0_ ;

如果 向 函数模板 中传入不同的函数 , 会生成 多个不同的 汇编函数 ;

C++ 编译器 编译 函数模板 时 , 不会生成能处理任意类型参数的 函数 ,

而是 通过 函数模板 , 根据 实际传入的参数类型 生成 具体的 参数类型不同 的函数 ;

如果 函数模板 和 普通函数 定义在了一起 ,

则 C++ 编译器 编译 汇编文件 时 , 就直接使用 普通函数 替代 为 函数模板 重新生成一个 函数实例 ;

C++ 编译器 通过 两次编译 实现上述效果 ;

  • 第一次编译 会对 函数模板 进行 语法分析 , 词法分析 , 句法分析 , 生成简单的 函数模板 模型 ;
  • 第二次编译 根据 调用时 传入的实际数据类型 , 产生新的 函数模型 ;

如果 调用多次 , 那么会产生多个 新的函数模型 ;

发表评论

表情:
评论列表 (有 0 条评论,97人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C++ 函数模板

    定义    函数模板是一种特殊的函数,可以使用不同的类型进行调用,对于功能相同的函数,不需要重复编写代码,并且函数模板与普通函数看起来很类似,区别就是类型可以被参数化

    相关 c++】函数模板

    何为泛型编程呢?简单的说就是,我们按照特定语法写代码,然后让编译器去具体实现这些代码。而函数模板,就是让编译器按照调用时的实参自动生成相应的函数版本. 定义和使用函数模板

    相关 C/C++编程函数模板

    一、什么是函数模板 函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。