在前面几篇博客
- 【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 : 展开 宏定义 , 得到预处理文件 ;
代码语言:javascript复制gcc Test.c -o Test.i
也可以加上 -E 选项 ;
代码语言:javascript复制gcc -E Test.c -o Test.i
② 编译 Compiling ( 编译器 )
编译 Compiling : 将预处理文件编译成 汇编文件 ;
代码语言:javascript复制gcc Test.i -o Test.S
直接从 Test.c 源码生成 汇编文件 :
代码语言:javascript复制gcc -S Test.c -o Test.S
③ 汇编 Assembling ( 汇编器 )
汇编 Assembling : 将 汇编文件 编译成 二进制机器码文件 ;
代码语言:javascript复制gcc Test.S -o Test.o
直接从 Test.c 源码生成 机器码文件 :
代码语言:javascript复制gcc -c Test.c -o Test.o
④ 链接 Linking ( 链接器器 )
链接 Linking : 将 二进制机器码文件 链接成 可执行文件 ;
代码语言:javascript复制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 函数中调用该 函数模板 ,
代码语言:javascript复制#include "iostream"
using namespace std;
template <typename T>
T add(T a, T b) {
cout << "调用函数模板 T add(T a, T b)" << endl;
return a b;
}
int main() {
int a = 10, b = 20;
int c = add(a, b);
cout << "函数模板计算结果 : c = " << c << endl;
return 0;
}
执行
代码语言:javascript复制g -S Test.cpp -o Test.S
命令 , 生成 该 C 源码对应的汇编文件 ;
生成的汇编文件 Test.S 内容如下 :
代码语言:javascript复制 .file "Test.cpp"
.lcomm __ZStL8__ioinit,1,1
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.align 4
LC0:
.ascii "345207275346225260346250241346235277350256241347256227347273223346236234 : c = "
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
leal 4(%esp), �x
andl $-16, %esp
pushl -4(�x)
pushl �p
movl %esp, �p
pushl �x
subl $36, %esp
call ___main
movl $10, -12(�p)
movl $20, -16(�p)
movl -16(�p), �x
movl �x, 4(%esp)
movl -12(�p), �x
movl �x, (%esp)
call __Z3addIiET_S0_S0_
movl �x, -20(�p)
movl $LC0, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl �x, �x
movl -20(�p), �x
movl �x, (%esp)
movl �x, �x
call __ZNSolsEi
subl $4, %esp
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
movl �x, �x
call __ZNSolsEPFRSoS_E
subl $4, %esp
movl $0, �x
movl -4(�p), �x
leave
leal -4(�x), %esp
ret
.section .rdata,"dr"
.align 4
LC1:
.ascii "350260203347224250345207275346225260346250241346235277 T add(T a, T b) "
.section .text$_Z3addIiET_S0_S0_,"x"
.linkonce discard
.globl __Z3addIiET_S0_S0_
.def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef
__Z3addIiET_S0_S0_:
pushl �p
movl %esp, �p
subl $24, %esp
movl $LC1, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
movl �x, �x
call __ZNSolsEPFRSoS_E
subl $4, %esp
movl 8(�p), �x
movl 12(�p), �x
addl �x, �x
leave
ret
.text
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
pushl �p
movl %esp, �p
subl $8, %esp
movl $__ZStL8__ioinit, �x
call __ZNSt8ios_base4InitD1Ev
leave
ret
.def __Z41__static_initialization_and_destruction_0ii; .scl 3; .type 32; .endef
__Z41__static_initialization_and_destruction_0ii:
pushl �p
movl %esp, �p
subl $24, %esp
cmpl $1, 8(�p)
jne L6
cmpl $65535, 12(�p)
jne L6
movl $__ZStL8__ioinit, �x
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
L6:
leave
ret
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
pushl �p
movl %esp, �p
subl $24, %esp
movl $65535, 4(%esp)
movl $1, (%esp)
call __Z41__static_initialization_and_destruction_0ii
leave
ret
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.ident "GCC: (i686-posix-sjlj, built by strawberryperl.com project) 4.9.2"
.def __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc; .scl 2; .type 32; .endef
.def __ZNSolsEi; .scl 2; .type 32; .endef
.def __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_; .scl 2; .type 32; .endef
.def __ZNSolsEPFRSoS_E; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
2、分析 模板函数代码 汇编文件
.file "Test.cpp"
表示这是 Test.cpp 源码的 汇编文件 ;
.text
表示 下面是代码 ;
_main:
表示 后面是 main 函数 ;
call __Z3addIiET_S0_S0_
调用的是 函数模板 , 下面看函数模板的 汇编内容 :
函数模板 的 函数声明 对应的汇编如下 :
代码语言:javascript复制LC1:
.ascii "350260203347224250345207275346225260346250241346235277 T add(T a, T b) "
.section .text$_Z3addIiET_S0_S0_,"x"
.linkonce discard
.globl __Z3addIiET_S0_S0_
.def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef
这是一个模板函数的汇编版本,函数名为add,它接受两个参数,都是int类型(T在上下文中可以推断为int)。
.ascii "350260203347224250345207275346225260346250241346235277 T add(T a, T b) "
这行代码是一个ASCII字符串,它表示函数模板的名称和一些模板参数。这个字符串在汇编代码中可能不会直接出现,而是由编译器插入的。- .section .text_Z3addIiET_S0_S0_,"x" 这行代码定义了一个section(段),其中
.linkonce discard
这个指示告诉链接器,如果该文件在其他地方被链接了,就丢弃重复的代码。这是一种优化手段,可以避免在最终的可执行文件中包含重复的代码。.globl __Z3addIiET_S0_S0_
这行代码声明了全局符号__Z3addIiET_S0_S0_
。在C 中,编译器会为每个模板函数生成一个特定的符号名称,这是模板函数的实例化。.def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef
这行代码定义了符号__Z3addIiET_S0_S0_,并设置了一些属性。这些属性可能是由链接器或其他工具使用的,以确定如何处理该符号。
函数模板 的 函数体内容 回应的汇编如下 :
代码语言:javascript复制__Z3addIiET_S0_S0_:
pushl �p
movl %esp, �p
subl $24, %esp
movl $LC1, 4(%esp)
movl $__ZSt4cout, (%esp) # 开始打印日志
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
movl �x, �x
call __ZNSolsEPFRSoS_E # 打印日志结束
subl $4, %esp
movl 8(�p), �x
movl 12(�p), �x
addl �x, �x
leave
ret
.text
.def ___tcf_0; .scl 3; .type 32; .endef
对应的 C 代码如下 :
代码语言:javascript复制template <typename T>
T add(T a, T b) {
cout << "调用函数模板 T add(T a, T b)" << endl;
return a b;
}
打印日志
代码语言:javascript复制cout << "调用函数模板 T add(T a, T b)" << endl;
对应的汇编内容 :
代码语言:javascript复制 movl $__ZSt4cout, (%esp) # 开始打印日志
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
movl �x, �x
call __ZNSolsEPFRSoS_E # 打印日志结束
3、模板函数代码 汇编文件 分析总结 ( 重要 )
C 编译器 将 函数模板 编译成了 汇编函数 call __Z3addIiET_S0_S0_
;
如果 向 函数模板 中传入不同的函数 , 会生成 多个不同的 汇编函数 ;
C 编译器 编译 函数模板 时 , 不会生成能处理任意类型参数的 函数 ,
而是 通过 函数模板 , 根据 实际传入的参数类型 生成 具体的 参数类型不同 的函数 ;
如果 函数模板 和 普通函数 定义在了一起 ,
则 C 编译器 编译 汇编文件 时 , 就直接使用 普通函数 替代 为 函数模板 重新生成一个 函数实例 ;
C 编译器 通过 两次编译 实现上述效果 ;
- 第一次编译 会对 函数模板 进行 语法分析 , 词法分析 , 句法分析 , 生成简单的 函数模板 模型 ;
- 第二次编译 根据 调用时 传入的实际数据类型 , 产生新的 函数模型 ;
如果 调用多次 , 那么会产生多个 新的函数模型 ;