下面纯属个人理解,请理智看待
吐槽:其实标题不算太对.
1.头文件其实没什么作用,头文件大多只是给予开发人员在开发的时候的一种方便查找接口声明,或者方便组内开发的规范,或者模块化定义。
其实头文件仅仅是使用在预处理的情况下。而预处理的工作也仅是将头文件里的信息拷贝到cpp文件里面进而生成预处理的文件。
2.模板函数的声明和定义其实可以分开- -,只是这样做没什么意义了。
下面我们完整的编译链接一个模板函数的程序
fun.h文件:
#ifndef FUN_H_
#define FUN_H_
template<typename T>
T add(T a, T b) {
return a + b;
}
#endif
main.cpp文件
#include "fun.h"
int main() {
add(1, 3);
return 0;
}
1.预处理g++ -E main.cpp
# 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 "fun.h" 1
template<typename T>
T add(T a, T b) {
return a + b;
}
# 2 "main.cpp" 2
int main() {
add(1, 3);
return 0;
}
可以看到预处理的文件,是直接将fun.h的头文件信息直接拷贝了过来。
2.编译g++ -S main.cpp,生成汇编代码
.file "main.cpp"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $3, %esi
movl $1, %edi
call _Z3addIiET_S0_S0_
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.section .text._Z3addIiET_S0_S0_,"axG",@progbits,_Z3addIiET_S0_S0_,comdat
.weak _Z3addIiET_S0_S0_
.type _Z3addIiET_S0_S0_, @function
_Z3addIiET_S0_S0_:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size _Z3addIiET_S0_S0_, .-_Z3addIiET_S0_S0_
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
为了可以更好的查看,我们可以进行汇编成.o文件再进行objdump查看
g++ -c main.s && objdump -S main.o
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: be 03 00 00 00 mov $0x3,%esi
9: bf 01 00 00 00 mov $0x1,%edi
e: e8 00 00 00 00 callq 13 <main+0x13>
13: b8 00 00 00 00 mov $0x0,%eax
18: 5d pop %rbp
19: c3 retq
Disassembly of section .text._Z3addIiET_S0_S0_:
000000000000001a <_Z3addIiET_S0_S0_>:
1a: 55 push %rbp
1b: 48 89 e5 mov %rsp,%rbp
1e: 89 7d fc mov %edi,-0x4(%rbp)
21: 89 75 f8 mov %esi,-0x8(%rbp)
24: 8b 45 f8 mov -0x8(%rbp),%eax
27: 8b 55 fc mov -0x4(%rbp),%edx
2a: 01 d0 add %edx,%eax
2c: 5d pop %rbp
2d: c3 retq
这里的edx和eax都是32位寄存器,再结合_Z3addIiET_S0_S0_可以推断出是int形add函数的实现(
其实可以直接通过c++filt进行查看,可得到int add<int>(int, int))。
从这里可以知道main.cpp在编译成.o文件的时候,其是先将头文件的方法声明和定义都拷贝了下来,然后再结合main方法内部的add(1,3)来联系上下文,可以知道
会有一个int的add实现需要生成。这看起来很自然。
分割线----------------------------------------------------------------------------------------------------------
那么下面我们来看分开的:
fun.h
#ifndef FUN_H_
#define FUN_H_
template<typename T>
T add(T a, T b);
#endif
main.cpp
#include "fun.h"
int main() {
add(1, 3);
return 0;
}
fun.cpp
#include "fun.h"
template<typename T>
T add(T a, T b) {
return a + b;
}
1.预处理:g++ -E main.cpp
# 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 "fun.h" 1
template<typename T>
T add(T a, T b);
# 2 "main.cpp" 2
int main() {
add(1, 3);
return 0;
}
同上,也是直接拷贝头文件信息
2:查看汇编代码(objdump .0文件):
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: be 03 00 00 00 mov $0x3,%esi
9: bf 01 00 00 00 mov $0x1,%edi
e: e8 00 00 00 00 callq 13 <main+0x13>
13: b8 00 00 00 00 mov $0x0,%eax
18: 5d pop %rbp
19: c3 retq
现在我们职能看到callq 13 <main+0X13> 对应的二进制e8 00 00 00 00,意思是这个函数调用的地址,在链接(ld)的时候在从其他.o文件中进行寻找,找到的话便会将
这个地址重定向到那个位置上去
3.我们来编译下fun.cpp文件,g++ -S fun.cpp
.file "fun.cpp"
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
是不成功的,其实想想也能理解,就只给你fun.cpp和fun.h文件,根本没有应用场景,又该如何生成具体类型的实例呢,模板只是将通用的方法流程抽象出来,在编写代码的时候
不用在根据多个类型进行重复的编码,只是这样的工作转交给编译工具而已,实际上最终生成的代码可能不会有什么变化。
既然无法生成具体实例的.o文件,那么在链接的时候就注定main.o文件的callq无法重定向到具体的地址,便会报ld错误,没有具体引用实例undefined reference to `int add<int>(int, int)'
4.如果我们在fun.cpp特例化一个int add(int a, int b)
#include "fun.h"
template<typename T>
T add(T a, T b) {
return a + b;
}
template<>
int add(int a, int b) {
return a + b;
}
会发现是可以成功编译成.o文件的,objdump -S fun.o查看汇编代码:
0000000000000000 <_Z3addIiET_S0_S0_>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 8b 45 f8 mov -0x8(%rbp),%eax
d: 8b 55 fc mov -0x4(%rbp),%edx
10: 01 d0 add %edx,%eax
12: 5d pop %rbp
13: c3 retq
生成了一个int add(int a, int b)的函数实现
4.链接成可执行文件:g++ fun.o main.o成功,再次使用objdump -S a.out
0000000000400501 <main>:
400501: 55 push %rbp
400502: 48 89 e5 mov %rsp,%rbp
400505: be 03 00 00 00 mov $0x3,%esi
40050a: bf 01 00 00 00 mov $0x1,%edi
40050f: e8 d9 ff ff ff callq 4004ed <_Z3addIiET_S0_S0_>
400514: b8 00 00 00 00 mov $0x0,%eax
400519: 5d pop %rbp
40051a: c3 retq
40051b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
直接查看main函数,可以发现callq 4004ed <_Z3addIiET_S0_S0_,e8 d9 ff ff ff,函数地址已经由之前的00 00 00 00更改为d9 ff ff ff,因此在./a.out可以正常执行。
5.因此将模板函数的声明和定义是可以分开来存放的
只是这样是没有意义的,因为你需要特例出当前使用者可能使用的所有类型情况,那么便要为这些类型特例相同的代码,这样就相当于将编译器的工作又交回给我们了。
因此,不如直接将声明和定义直接写在头文件上,以此交由编译器根据上下文实例化需要的函数,以此减轻重复编码的工作。
end.