【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
如果说模板类定义的是一种数据类型,那么模板函数定义的就是一种函数。既然是函数,那么就有输入数据和输出数据。和模板类的概念差不多,模板函数的初衷也是为了在函数操作上抽取共同的特性,屏蔽的是类型的不同和差异。我们可以通过下面一个简单的代码说明问题:
- int int_compare(int a, int b)
- {
- return a > b ? a : b;
- }
- double double_compare(double a, double b)
- {
- return a > b ? a : b;
- }
int int_compare(int a, int b)
{
return a > b ? a : b;
}
double double_compare(double a, double b)
{
return a > b ? a : b;
}
上面的一段代码是取较大值的一段代码。两个函数之间最大的差别就是输入数据类型和输出数据类型之间的差别,那我们有没有一种办法可以屏蔽这种数据类型之间的差别呢?有。那就是函数模板:
- template <typename type>
- type compare(type a, type b)
- {
- return a > b ? a : b;
- }
template <typename type>
type compare(type a, type b)
{
return a > b ? a : b;
}
可以看到,模板函数和普通函数没有什么区别,只是在函数的上面把类型抽象成了type,那么模板函数应该怎么使用呢?
- 246: int i_value = compare(2, 3);
- 00401458 push 3
- 0040145A push 2
- 0040145C call @ILT+10(compare) (0040100f)
- 00401461 add esp,8
- 00401464 mov dword ptr [ebp-4],eax
- 247: double d_value = compare(2.3, 3.1);
- 00401467 push 4008CCCCh
- 0040146C push 0CCCCCCCDh
- 00401471 push 40026666h
- 00401476 push 66666666h
- 0040147B call @ILT+5(compare) (0040100a)
- 00401480 add esp,10h
- 00401483 fstp qword ptr [ebp-0Ch]
- 248: }
246: int i_value = compare(2, 3);
00401458 push 3
0040145A push 2
0040145C call @ILT+10(compare) (0040100f)
00401461 add esp,8
00401464 mov dword ptr [ebp-4],eax
247: double d_value = compare(2.3, 3.1);
00401467 push 4008CCCCh
0040146C push 0CCCCCCCDh
00401471 push 40026666h
00401476 push 66666666h
0040147B call @ILT+5(compare) (0040100a)
00401480 add esp,10h
00401483 fstp qword ptr [ebp-0Ch]
248: }
汇编代码表明,两个compare调用的函数地址并不是一致的。其中整数的compare地址是0x40100f,而double的地址是0x0040100a。这说明编译器在编译的时候帮我们同时生成了两个compare函数。所以说,模板类的本质就是在编译器增加判断处理工作的同时,减少手工的重复劳动。同时和模板类不一样,模板函数不需要显示定义函数的参数类型,这是因为可以从入参判断出函数的类型。
如果参数类型是 class类型呢? 我们可以试一试。首先定义基本class:
- class data
- {
- int value;
- public:
- explicit data(int m): value(m) {}
- ~data() {}
- int get_value() { return value;}
- int operator > (data& d) {return this->get_value() > d.get_value();}
- };
class data
{
int value;
public:
explicit data(int m): value(m) {}
~data() {}
int get_value() { return value;}
int operator > (data& d) {return this->get_value() > d.get_value();}
};
接着,我们调用compare函数:
- 256: data m(4), n(2);
- 0040148D push 4
- 0040148F lea ecx,[ebp-10h]
- 00401492 call @ILT+40(data::data) (0040102d)
- 00401497 mov dword ptr [ebp-4],0
- 0040149E push 2
- 004014A0 lea ecx,[ebp-14h]
- 004014A3 call @ILT+40(data::data) (0040102d)
- 004014A8 mov byte ptr [ebp-4],1
- 257: data p = compare(m,n);
- 004014AC mov eax,dword ptr [ebp-14h]
- 004014AF push eax
- 004014B0 mov ecx,dword ptr [ebp-10h]
- 004014B3 push ecx
- 004014B4 lea edx,[ebp-18h]
- 004014B7 push edx
- 004014B8 call @ILT+15(compare) (00401014)
- 004014BD add esp,0Ch
- 258: }
256: data m(4), n(2);
0040148D push 4
0040148F lea ecx,[ebp-10h]
00401492 call @ILT+40(data::data) (0040102d)
00401497 mov dword ptr [ebp-4],0
0040149E push 2
004014A0 lea ecx,[ebp-14h]
004014A3 call @ILT+40(data::data) (0040102d)
004014A8 mov byte ptr [ebp-4],1
257: data p = compare(m,n);
004014AC mov eax,dword ptr [ebp-14h]
004014AF push eax
004014B0 mov ecx,dword ptr [ebp-10h]
004014B3 push ecx
004014B4 lea edx,[ebp-18h]
004014B7 push edx
004014B8 call @ILT+15(compare) (00401014)
004014BD add esp,0Ch
258: }
256行: data构造了两个基本变量m和n
257行: 我们调用模板函数compare, 函数地址为0x401014,注意dx为p的地址,也就是堆栈临时变量的地址
为了看到算术符>重载,我们跟进compare函数:
- 241: return a > b ? a : b;
- 0040212B lea eax,[ebp+10h]
- 0040212E push eax
- 0040212F lea ecx,[ebp+0Ch]
- 00402132 call @ILT+55(data::operator>) (0040103c)
- 00402137 test eax,eax
- 00402139 je compare+53h (00402143)
- 0040213B lea ecx,[ebp+0Ch]
- 0040213E mov dword ptr [ebp-18h],ecx
- 00402141 jmp compare+59h (00402149)
- 00402143 lea edx,[ebp+10h]
- 00402146 mov dword ptr [ebp-18h],edx
- 00402149 mov eax,dword ptr [ebp-18h]
- 0040214C mov dword ptr [ebp-10h],eax
- 0040214F mov ecx,dword ptr [ebp-10h]
- 00402152 mov edx,dword ptr [ecx]
- 00402154 mov eax,dword ptr [ebp+8]
- 00402157 mov dword ptr [eax],edx
- 00402159 mov ecx,dword ptr [ebp-14h]
- 0040215C or ecx,1
- 0040215F mov dword ptr [ebp-14h],ecx
- 00402162 mov byte ptr [ebp-4],1
- 00402166 lea ecx,[ebp+0Ch]
- 00402169 call @ILT+25(data::~data) (0040101e)
- 0040216E mov byte ptr [ebp-4],0
- 00402172 lea ecx,[ebp+10h]
- 00402175 call @ILT+25(data::~data) (0040101e)
- 0040217A mov eax,dword ptr [ebp+8]
241: return a > b ? a : b;
0040212B lea eax,[ebp+10h]
0040212E push eax
0040212F lea ecx,[ebp+0Ch]
00402132 call @ILT+55(data::operator>) (0040103c)
00402137 test eax,eax
00402139 je compare+53h (00402143)
0040213B lea ecx,[ebp+0Ch]
0040213E mov dword ptr [ebp-18h],ecx
00402141 jmp compare+59h (00402149)
00402143 lea edx,[ebp+10h]
00402146 mov dword ptr [ebp-18h],edx
00402149 mov eax,dword ptr [ebp-18h]
0040214C mov dword ptr [ebp-10h],eax
0040214F mov ecx,dword ptr [ebp-10h]
00402152 mov edx,dword ptr [ecx]
00402154 mov eax,dword ptr [ebp+8]
00402157 mov dword ptr [eax],edx
00402159 mov ecx,dword ptr [ebp-14h]
0040215C or ecx,1
0040215F mov dword ptr [ebp-14h],ecx
00402162 mov byte ptr [ebp-4],1
00402166 lea ecx,[ebp+0Ch]
00402169 call @ILT+25(data::~data) (0040101e)
0040216E mov byte ptr [ebp-4],0
00402172 lea ecx,[ebp+10h]
00402175 call @ILT+25(data::~data) (0040101e)
0040217A mov eax,dword ptr [ebp+8]
我们发现compare模板语句下面构建了很多汇编语句,有一些冗长,我们可以大略介绍一下:
(1) 开头调用call 0x0040103C函数就是调用重载运算符函数,[ebp-18h]表示即将被复制的是a数据还是b数据
(2) 比较返回结果后,开始复制数据,具体见0x402157,其中临时变量[ebp-14h]和临时变量[ebp-4]的操作可以忽略
(3) 函数返回前,对临时变量a和b进行析构处理,见代码0x402169和代码0x402175。
注意:
(1)编写模板函数前先保证自己的函数是编写正确的
(2)模板函数的优先级低于非模板函数
(3)模板函数的类型可以是自定义类型,也可以是c、c++语言的基本类型
(4)模板函数的使用经常和类的算术运算符混合使用,注意技巧
(5)模板函数中涉及指针部分的内容,务必注意
【预告: 下一篇模板主要讲述特化模板、缺省模板】
用汇编的眼光看C++(之缺省模板、特化模板)
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
缺省函数是C++的一个基本特色。缺省函数定义比较简单,也就是说,对于函数的某一个输入参数或者几个输入参数,如果你没有特定的数值的话,那我们就会用缺省的数据进行代替。如果你在调用的过程中使用了自己的数据,那么缺省数据将被我们自己定义的数据覆盖。下面就是一个缺省函数的示例:
- int add(int m, int n = 10)
- {
- return m + n;
- }
int add(int m, int n = 10)
{
return m + n;
}
如果调用呢,有什么区别?
- 262: int p = add(2);
- 00401488 push 0Ah
- 0040148A push 2
- 0040148C call @ILT+15(add) (00401014)
- 00401491 add esp,8
- 00401494 mov dword ptr [ebp-4],eax
- 263: p = add(3, 4);
- 00401497 push 4
- 00401499 push 3
- 0040149B call @ILT+15(add) (00401014)
- 004014A0 add esp,8
- 004014A3 mov dword ptr [ebp-4],eax
262: int p = add(2);
00401488 push 0Ah
0040148A push 2
0040148C call @ILT+15(add) (00401014)
00401491 add esp,8
00401494 mov dword ptr [ebp-4],eax
263: p = add(3, 4);
00401497 push 4
00401499 push 3
0040149B call @ILT+15(add) (00401014)
004014A0 add esp,8
004014A3 mov dword ptr [ebp-4],eax
可以从上面的代码看到,如果单独输入一个数据2,那么编译器帮我们默认输入了10;如果输了的数据是3、4呢,那么编译器将用4代替默认的数据10。所以说,编译器帮我们做了中间的替换和判断工作。那么回到我们今天讨论的缺省模板类型上面,那会是什么样的情形呢?我们可以编写一个范例:
- template <typename type1, typename type2 = int>
- class data
- {
- type2 value;
- public:
- data(type2 m): value(m) {}
- ~data() {}
- };
template <typename type1, typename type2 = int>
class data
{
type2 value;
public:
data(type2 m): value(m) {}
~data() {}
};
可以看到,我们在第二个参数使用了缺省类型int,那么怎么证明缺省类型可以使用呢?我们设计了下面一个测试用例:
- 239: data<int, int> m(2);
- 004013BD push 2
- 004013BF lea ecx,[ebp-10h]
- 004013C2 call @ILT+5(data<int,int>::data<int,int>) (0040100a)
- 004013C7 mov dword ptr [ebp-4],0
- 240: data<int> n(3);
- 004013CE push 3
- 004013D0 lea ecx,[ebp-14h]
- 004013D3 call @ILT+5(data<int,int>::data<int,int>) (0040100a)
239: data<int, int> m(2);
004013BD push 2
004013BF lea ecx,[ebp-10h]
004013C2 call @ILT+5(data<int,int>::data<int,int>) (0040100a)
004013C7 mov dword ptr [ebp-4],0
240: data<int> n(3);
004013CE push 3
004013D0 lea ecx,[ebp-14h]
004013D3 call @ILT+5(data<int,int>::data<int,int>) (0040100a)
上面的代码定义了两个临时变量,其中第一个是m,输入类型是int;第二个临时变量是n,输入类型是int和int。前面我们说过缺省类型是int,那么第一个临时变量m和第二个临时变量n的构造函数地址应该是一样的。那么事实上两者的构造函数是不是一样的呢?我们可以查看两者的函数地址,发现一个是0x0040100a,另外一个也是0x0040100a。范例证明我们的判断是正确的。
明白了上面的缺省模板构造,下面我们谈一下特化模板。特化模板是什么意思呢?其实并不复杂。因为模板类既然是通用模板,那么其中的数据类型可以是任意数据类型,但是难免有一些数据类型(比如说指针),我们需要对其中的一些操作做一些细微的修改,但是这些小的修改在原来的模板定义上是无法做的。那么怎么办?我们只好重新定义一种形式,它和模板类定义的名称一致,但是形式稍有差别。我们可以编写一个测试看看:
- template <typename type>
- class data
- {
- public:
- data() {printf("normal!\n");}
- ~data() {printf("~normal!\n");}
- };
- template <>
- class data<int*>
- {
- public:
- data() {printf("point!\n");}
- ~data() {printf("point!\n");}
- };
template <typename type>
class data
{
public:
data() {printf("normal!\n");}
~data() {printf("~normal!\n");}
};
template <>
class data<int*>
{
public:
data() {printf("point!\n");}
~data() {printf("point!\n");}
};
上面的代码定义了两个类模板。但是两者的名称是一样的,说明这两个类定义的内容其实具有很大的相似性。第一种定义就是标准模板类的定义,第二中稍微复杂一点,使用缺省的int*,因为没有使用到特定的type类型,所以此时template后面的内容为空。那么怎么判断这两个类都是可以正常使用的呢?大家可以看看下面的范例:
- 249: data<int> p;
- 004013BD lea ecx,[ebp-10h]
- 004013C0 call @ILT+45(data<int>::data<int>) (00401032)
- 004013C5 mov dword ptr [ebp-4],0
- 250: data<int*> q;
- 004013CC lea ecx,[ebp-14h]
- 004013CF call @ILT+35(data<int *>::data<int *>) (00401028)
- 251: }
249: data<int> p;
004013BD lea ecx,[ebp-10h]
004013C0 call @ILT+45(data<int>::data<int>) (00401032)
004013C5 mov dword ptr [ebp-4],0
250: data<int*> q;
004013CC lea ecx,[ebp-14h]
004013CF call @ILT+35(data<int *>::data<int *>) (00401028)
251: }
我们发现,第一个函数的call地址是0x00401032,第二个地址为0x00401028。但是这说明不了什么,因为第二个地址完全也可能是第一个模板类引申的。我们应该跟到每一个函数里面(其实这里的地址在VC下都是跳转地址)。
第一个变量的实际进入函数如下所示:
- 234: data() {printf("normal!\n");}
- 00401340 push ebp
- 00401341 mov ebp,esp
- 00401343 sub esp,44h
- 00401346 push ebx
- 00401347 push esi
- 00401348 push edi
- 00401349 push ecx
- 0040134A lea edi,[ebp-44h]
- 0040134D mov ecx,11h
- 00401352 mov eax,0CCCCCCCCh
- 00401357 rep stos dword ptr [edi]
- 00401359 pop ecx
- 0040135A mov dword ptr [ebp-4],ecx
- 0040135D push offset string "normal!\n" (0042607c)
- 00401362 call printf (00401540)
- 00401367 add esp,4
- 0040136A mov eax,dword ptr [ebp-4]
- 0040136D pop edi
- 0040136E pop esi
- 0040136F pop ebx
- 00401370 add esp,44h
- 00401373 cmp ebp,esp
- 00401375 call __chkesp (004023b0)
- 0040137A mov esp,ebp
- 0040137C pop ebp
- 0040137D ret
234: data() {printf("normal!\n");}
00401340 push ebp
00401341 mov ebp,esp
00401343 sub esp,44h
00401346 push ebx
00401347 push esi
00401348 push edi
00401349 push ecx
0040134A lea edi,[ebp-44h]
0040134D mov ecx,11h
00401352 mov eax,0CCCCCCCCh
00401357 rep stos dword ptr [edi]
00401359 pop ecx
0040135A mov dword ptr [ebp-4],ecx
0040135D push offset string "normal!\n" (0042607c)
00401362 call printf (00401540)
00401367 add esp,4
0040136A mov eax,dword ptr [ebp-4]
0040136D pop edi
0040136E pop esi
0040136F pop ebx
00401370 add esp,44h
00401373 cmp ebp,esp
00401375 call __chkesp (004023b0)
0040137A mov esp,ebp
0040137C pop ebp
0040137D ret
那么,第二个变量呢,同样需要跟入函数:
- 242: data() {printf("point!\n");}
- 00401430 push ebp
- 00401431 mov ebp,esp
- 00401433 sub esp,44h
- 00401436 push ebx
- 00401437 push esi
- 00401438 push edi
- 00401439 push ecx
- 0040143A lea edi,[ebp-44h]
- 0040143D mov ecx,11h
- 00401442 mov eax,0CCCCCCCCh
- 00401447 rep stos dword ptr [edi]
- 00401449 pop ecx
- 0040144A mov dword ptr [ebp-4],ecx
- 0040144D push offset string "point!\n" (00426074)
- 00401452 call printf (00401540)
- 00401457 add esp,4
- 0040145A mov eax,dword ptr [ebp-4]
- 0040145D pop edi
- 0040145E pop esi
- 0040145F pop ebx
- 00401460 add esp,44h
- 00401463 cmp ebp,esp
- 00401465 call __chkesp (004023b0)
- 0040146A mov esp,ebp
- 0040146C pop ebp
- 0040146D ret
242: data() {printf("point!\n");}
00401430 push ebp
00401431 mov ebp,esp
00401433 sub esp,44h
00401436 push ebx
00401437 push esi
00401438 push edi
00401439 push ecx
0040143A lea edi,[ebp-44h]
0040143D mov ecx,11h
00401442 mov eax,0CCCCCCCCh
00401447 rep stos dword ptr [edi]
00401449 pop ecx
0040144A mov dword ptr [ebp-4],ecx
0040144D push offset string "point!\n" (00426074)
00401452 call printf (00401540)
00401457 add esp,4
0040145A mov eax,dword ptr [ebp-4]
0040145D pop edi
0040145E pop esi
0040145F pop ebx
00401460 add esp,44h
00401463 cmp ebp,esp
00401465 call __chkesp (004023b0)
0040146A mov esp,ebp
0040146C pop ebp
0040146D ret
看到上面的函数,大家应该明白了两者调用的构造函数并不一样。所以说,特化模板通常就是为了那些特殊的数据类型准备的。这样我们使用者在使用模板的时候就没有什么顾虑了,可以忽略各个数据类型处理上的差别了。当然,特化模板因为考虑了特殊模型数据,使得我们的代码更加完毕,更加健壮了,建议在设计模板的时候适当多使用。
思考题:
(1)模板类第一个type可以缺省吗?为什么会这么考虑?
(2)下面的代码在vc 6.0和vc 2005上都能编译过?为什么呢?对于我们设计代码有什么思考呢? (建议从兼容性上面考虑)
- template <typename type>
- class data
- {
- public:
- data() {printf("normal!\n");}
- ~data() {printf("~normal!\n");}
- };
- template <typename type>
- class data <type*>
- {
- public:
- data() {printf("point!\n");}
- ~data() {printf("point!\n");}
- };
template <typename type>
class data
{
public:
data() {printf("normal!\n");}
~data() {printf("~normal!\n");}
};
template <typename type>
class data <type*>
{
public:
data() {printf("point!\n");}
~data() {printf("point!\n");}
};
【预告: 下面的两个博客非常有意思,介绍递归模板和模板的模板内容】