前言
C++提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
1)C++提供两种模板机制:函数模板、类模板
2)类属 —— 类型参数化,又称参数模板
使得程序(算法)可以从逻辑功能上抽象,把被处理的对象(数据)类型作为参数传递。
总结:
- 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属。
- 模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
1、函数模板
1.1为什么要有函数模板
需求:写n个函数,交换char类型、int类型、double类型变量的值。
案例:
#include using /* void myswap(int &a, int &b) { int t = a; a = b; b = t; }
void myswap(char &a, char &b) { char t = a; a = b; b = t; } */
//template 关键字告诉C++编译器 我要开始泛型了.你不要随便报错 //数据类型T 参数化数据类型 template <typename void myswap(T &a, T &b) { T t; t = a; a = b; b = t; }
void main() { //char a = 'c';
int x = 1; int y = 2; myswap(x, y); //自动数据类型 推导的方式
float a = 2.0; float b = 3.0;
myswap(a, b); //自动数据类型 推导的方式 myswap<float>(a, b); //显示类型调用
cout << system("pause"); return; } |
1.2函数模板语法
函数模板定义形式
template < 类型形式参数表 >
类型形式参数的形式为:
typename T1 , typename T2 , …… , typename Tn
或 class T1 , class T2 , …… , class Tn
函数模板调用
myswap<float>(a, b); //显示类型调用
myswap(a, b); //自动数据类型推导
1.3函数模板和模板函数
1.4函数模板做函数参数
#include using
template<typename void sortArray(T *a, T2 { T tmp; int i, j; for (i = 0; i<num; i++) { for (j = i + 1; j<num; j++) { if (a[i] < a[j]) { tmp = a[i]; a[i] = a[j]; a[j] = tmp; } } } }
template<class void pirntArray(T *a, int { int i = 0; for (i = 0; i<num; i++) { cout << a[i] << " "; } }
void main() { int num = 0; char a[] = "ddadeeettttt"; num = strlen(a);
printf("排序之前\n"); pirntArray<char>(a, num);
sortArray<char, int>(a, num); //显示类型调用 模板函数 <> printf("排序之后\n"); pirntArray<char>(a, num); cout << system("pause"); return; } |
1.5函数模板遇上函数重载
函数模板和普通函数区别结论:
/*
函数模板不允许自动类型转化
普通函数能够进行自动类型转换
*/
函数模板和普通函数在一起,调用规则:
/*
1 、函数模板可以像普通函数一样被重载
2 、C++编译器优先考虑普通函数
3 、如果函数模板可以产生一个更好的匹配,那么选择模板
4 、可以通过空模板实参列表的语法限定编译器只通过模板匹配
*/
案例1:
#include using
template <typename void myswap(T &a, T &b) { T t; t = a; a = b; b = t; cout << }
void myswap(char &a, int &b) { int t; t = a; a = b; b = t; cout << }
void main() { char cData = 'a'; int iData = 2;
//myswap<int>(cData, iData); //结论 函数模板不提供隐式的数据类型转换 必须是严格的匹配
myswap(cData, iData); //myswap(iData, cData);
cout << system("pause"); return; } |
案例2:
#include using
int Max(int { cout << return }
template<typename T Max(T { cout << return }
template<typename T Max(T { cout << return Max(Max(a, b), c); }
void main() { int a = 1; int b = 2;
cout << Max(a, b) << endl; //当函数模板和普通函数都符合调用时,优先选择普通函数 cout << Max<>(a, b) << endl; //若显示使用函数模板,则使用<> 类型列表
cout << Max(3.0, 4.0) << endl; //如果 函数模板产生更好的匹配 使用函数模板
cout << Max(5.0, 0, 7.0) << endl; //重载
cout << Max('a', 100) << endl; //调用普通函数 可以隐式类型转换 system("pause"); return; } |
1.6 C++编译器模板机制剖析
思考:为什么函数模板可以和函数重载放在一块。C++编译器是如何提供函数模板机制的?
编译器编译原理
什么是gcc
gcc(GNU C Compiler)编译器的作者是Richard Stallman,也是GNU项目的奠基者。 |
什么是gcc:gcc是GNU Compiler Collection的缩写。最初是作为C语言的编译器(GNU C Compiler),现在已经支持多种语言了,如C、C++、Java、Pascal、Ada、COBOL语言等。 |
gcc支持多种硬件平台,甚至对Don Knuth 设计的 MMIX 这类不常见的计算机都提供了完善的支持 |
gcc主要特征
1)gcc是一个可移植的编译器,支持多种硬件平台 2)gcc不仅仅是个本地编译器,它还能跨平台交叉编译。 3)gcc有多种语言前端,用于解析不同的语言。 4)gcc是按模块化设计的,可以加入新语言和新CPU架构的支持 5)gcc是*软件 |
gcc编译过程
预处理(Pre-Processing) 编译(Compiling) 汇编(Assembling) 链接(Linking) Gcc *.c –o 1exe (总的编译步骤) Gcc –E 1.c –o 1.i //宏定义 宏展开 Gcc –S 1.i –o 1.s Gcc –c 1.s –o 1.o Gcc 1.o –o 1exe 结论:gcc编译工具是一个工具链。。。。 |
hello程序是一个高级C语言程序,这种形式容易被人读懂。为了在系统上运行hello.c程序,每条C语句都必须转化为低级机器指令。然后将这些指令打包成可执行目标文件格式,并以二进制形式存储器于磁盘中。 |
gcc常用编译选项
选项 |
作用 |
-o |
产生目标(.i、.s、.o、可执行文件等) |
-c |
通知gcc取消链接步骤,即编译源码并在最后生成目标文件 |
-E |
只运行C预编译器 |
-S |
告诉编译器产生汇编语言文件后停止编译,产生的汇编语言文件扩展名为.s |
-Wall |
使gcc对源文件的代码有问题的地方发出警告 |
-Idir |
将dir目录加入搜索头文件的目录路径 |
-Ldir |
将dir目录加入搜索库的目录路径 |
-llib |
链接lib库 |
-g |
在目标文件中嵌入调试信息,以便gdb之类的调试程序调试 |
练习
gcc -E hello.c -o hello.i(预处理) gcc -S hello.i -o hello.s(编译) gcc -c hello.s -o hello.o(汇编) gcc hello.o -o hello(链接) 以上四个步骤,可合成一个步骤 gcc hello.c -o hello(直接编译链接成可执行目标文件) gcc -c hello.c或gcc -c hello.c -o hello.o(编译生成可重定位目标文件) |
建议初学都加这个选项。下面这个例子如果不加-Wall选项编译器不报任何错误,但是得到的结果却不是预期的。 #include <stdio.h> int main(void) { printf("2+1 is %f", 3); return 0; } |
Gcc编译多个.c |
hello_1.h hello_1.c main.c 一次性编译 gcc hello_1.c main.c –o newhello 独立编译 gcc -Wall -c main.c -o main.o gcc -Wall -c hello_1.c -o hello_fn.o gcc -Wall main.o hello_1.o -o newhello |
模板函数反汇编观察
命令:g++ -S 7.cpp -o 7.s
.file "7.cpp" .text .def __ZL6printfPKcz; .scl 3; .type 32; .endef __ZL6printfPKcz: LFB264: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 pushl %ebx subl $36, %esp .cfi_offset 3, -12 leal 12(%ebp), %eax movl %eax, -12(%ebp) movl -12(%ebp), %eax movl %eax, 4(%esp) movl 8(%ebp), %eax movl %eax, (%esp) call ___mingw_vprintf movl %eax, %ebx movl %ebx, %eax addl $36, %esp popl %ebx .cfi_restore 3 popl %ebp .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE264: .lcomm __ZStL8__ioinit,1,1 .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "a:%d b:%d \12\0" LC1: .ascii "c1:%c c2:%c \12\0" LC2: .ascii "pause\0" .text .globl _main .def _main; .scl 2; .type 32; .endef _main: LFB1023: .cfi_startproc .cfi_personality 0,___gxx_personality_v0 .cfi_lsda 0,LLSDA1023 pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp call ___main movl $0, 28(%esp) movl $10, 24(%esp) movb $97, 23(%esp) movb $98, 22(%esp) leal 24(%esp), %eax movl %eax, 4(%esp) leal 28(%esp), %eax movl %eax, (%esp) call __Z6myswapIiEvRT_S1_ //66 ===>126 movl 24(%esp), %edx movl 28(%esp), %eax movl %edx, 8(%esp) movl %eax, 4(%esp) movl $LC0, (%esp) call __ZL6printfPKcz leal 22(%esp), %eax movl %eax, 4(%esp) leal 23(%esp), %eax movl %eax, (%esp) call __Z6myswapIcEvRT_S1_ //77 ===>155 movzbl 22(%esp), %eax movsbl %al, %edx movzbl 23(%esp), %eax movsbl %al, %eax movl %edx, 8(%esp) movl %eax, 4(%esp) movl $LC1, (%esp) call __ZL6printfPKcz movl $LC2, (%esp) LEHB0: call _system LEHE0: movl $0, %eax jmp L7 L6: movl %eax, (%esp) LEHB1: call __Unwind_Resume LEHE1: L7: leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1023: .def ___gxx_personality_v0; .scl 2; .type 32; .endef .section .gcc_except_table,"w" LLSDA1023: .byte 0xff .byte 0xff .byte 0x1 .uleb128 LLSDACSE1023-LLSDACSB1023 LLSDACSB1023: .uleb128 LEHB0-LFB1023 .uleb128 LEHE0-LEHB0 .uleb128 L6-LFB1023 .uleb128 0 .uleb128 LEHB1-LFB1023 .uleb128 LEHE1-LEHB1 .uleb128 0 .uleb128 0 LLSDACSE1023: .text .section .text$_Z6myswapIiEvRT_S1_,"x" .linkonce discard .globl __Z6myswapIiEvRT_S1_ .def __Z6myswapIiEvRT_S1_; .scl 2; .type 32; .endef __Z6myswapIiEvRT_S1_: //126 LFB1024: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $16, %esp movl 8(%ebp), %eax movl (%eax), %eax movl %eax, -4(%ebp) movl 12(%ebp), %eax movl (%eax), %edx movl 8(%ebp), %eax movl %edx, (%eax) movl 12(%ebp), %eax movl -4(%ebp), %edx movl %edx, (%eax) leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1024: .section .text$_Z6myswapIcEvRT_S1_,"x" .linkonce discard .globl __Z6myswapIcEvRT_S1_ .def __Z6myswapIcEvRT_S1_; .scl 2; .type 32; .endef __Z6myswapIcEvRT_S1_: //155 LFB1025: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $16, %esp movl 8(%ebp), %eax movzbl (%eax), %eax movb %al, -1(%ebp) movl 12(%ebp), %eax movzbl (%eax), %edx movl 8(%ebp), %eax movb %dl, (%eax) movl 12(%ebp), %eax movzbl -1(%ebp), %edx movb %dl, (%eax) leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1025: .text .def ___tcf_0; .scl 3; .type 32; .endef ___tcf_0: LFB1027: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $8, %esp movl $__ZStL8__ioinit, %ecx call __ZNSt8ios_base4InitD1Ev leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1027: .def __Z41__static_initialization_and_destruction_0ii; .scl 3; .type 32; .endef __Z41__static_initialization_and_destruction_0ii: LFB1026: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $24, %esp cmpl $1, 8(%ebp) jne L11 cmpl $65535, 12(%ebp) jne L11 movl $__ZStL8__ioinit, %ecx call __ZNSt8ios_base4InitC1Ev movl $___tcf_0, (%esp) call _atexit L11: leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1026: .def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef __GLOBAL__sub_I_main: LFB1028: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $24, %esp movl $65535, 4(%esp) movl $1, (%esp) call __Z41__static_initialization_and_destruction_0ii leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc LFE1028: .section .ctors,"w" .align 4 .long __GLOBAL__sub_I_main .ident "GCC: (rev2, Built by MinGW-builds project) 4.8.0" .def ___mingw_vprintf; .scl 2; .type 32; .endef .def _system; .scl 2; .type 32; .endef .def __Unwind_Resume; .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、类模板
2.1为什么需要类模板
类模板与函数模板的定义和使用类似,我们已经进行了介绍。 有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,如下面语句声明了一个类:
- 类模板用于实现类所需数据的类型参数化
- 类模板在表示如数组、表、图等数据结构显得特别重要,
这些数据结构的表示和算法不受所包含的元素类型的影响
2.2单个类模板语法
//类的类型参数化 抽象的类 //单个类模板 template<typename class { public: A(T { this->t = t; }
T &getT() { return t; } protected: public: T t; }; |
void main() { //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则 A<int> a(100); a.getT(); printAA(a); return; } |
2.3继承中的类模板语法
//结论: 子类从模板类继承的时候,需要让编译器知道 父类的数据类型具体是什么(数据类型的本质:固定大小内存块的别名)A<int> // class { public: B(int {
} void printB() { cout << "A:" << t << endl; } protected: private: };
//模板与上继承 //怎么样从基类继承 //若基类只有一个带参数的构造函数,子类是如何启动父类的构造函数 void pintBB(B &b) { b.printB(); } void printAA(A<int> &a) //类模板做函数参数 { // a.getT(); }
void main() { A<int> a(100); //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则 a.getT(); printAA(a);
B b(10); b.printB();
cout << "hello..." << endl; system("pause"); return; } |
2.4类模板语法知识体系梳理
2.4.1所有的类模板函数写在类的内部
2.4.2所有的类模板函数写在类的外部,在一个cpp中
//构造函数 没有问题
//普通函数 没有问题
//友元函数:用友元函数重载 << >>
// friend ostream& operator<<
<T> (ostream &out, Complex<T> &c3) ;
//友元函数:友元函数不是实现函数重载(非 << >>)
//1)需要在类前增加 类的前置声明 函数的前置声明
template<typename T>
class Complex;
template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2);
//2)类的内部声明 必须写成:
friend Complex<T> mySub <T> (Complex<T> &c1, Complex<T> &c2);
//3)友元函数实现 必须写成:
template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)
{
Complex<T> tmp(c1.a - c2.a, c1.b-c2.b);
return tmp;
}
//4)友元函数调用 必须写成
Complex<int> c4 = mySub<int>(c1, c2);
cout<<c4;
结论:友元函数只用来进行
左移
友移操作符重载。
2.4.3所有的类模板函数写在类的外部,在不同的.h和.cpp中,
也就是类模板函数说明和类模板实现分开
//类模板函数
构造函数
普通成员函数
友元函数
用友元函数重载<<>>;
用友元函数重载非<< >>
//要包含.cpp
2.4.4总结
归纳以上的介绍,可以这样声明和使用类模板:
1) 先写出一个实际的类。由于其语义明确,含义清楚,一般不会出错。
2) 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)。
3) 在类声明前面加入一行,格式为:
template <class 虚拟类型参数>
如:
template <class numtype> //注意本行末尾无分号
class Compare
{…}; //类体
4) 用类模板定义对象时用以下形式:
类模板名<实际类型名> 对象名;
类模板名<实际类型名> 对象名(实参表列);
如:
Compare<int> cmp;
Compare<int> cmp(3,7);
5) 如果在类模板外定义成员函数,应写成类模板形式:
template <class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {…}
关于类模板的几点说明:
1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如:
template <class T1,class T2>
class someclass
{…};
在定义对象时分别代入实际的类型名,如:
someclass<int,double> obj;
2) 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
3) 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
最后,我的建议是类和类函数的定义实现都写在一个文件中或者就写在类中。给我自己看的。
2.5类模板中的static关键字
- 从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员
- 和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化
- 每个模板类有自己的类模板的static数据成员副本
原理图:
3、类模板在项目开发中的应用
小结
- 模板是C++类型参数化的多态工具。C++提供函数模板和类模板。
- 模板定义以模板说明开始。类属参数必须在模板定义中至少出现一次。
- 同一个类属参数可以用于多个模板。
- 类属参数可用于函数的参数类型、返回类型和声明函数中的变量。
- 模板由编译器根据实际数据类型实例化,生成可执行代码。实例化的函数。
模板称为模板函数;实例化的类模板称为模板类。
- 函数模板可以用多种方式重载。
- 类模板可以在类层次中使用 。
训练题
-
请设计一个数组模板类( MyVector ),完成对int、char、Teacher类型元素的管理。需求
设计:
类模板 构造函数 拷贝构造函数 << [] 重载=操作符
a2=a1
实现
-
请仔细思考:
- 如果数组模板类中的元素是Teacher元素时,需要Teacher类做什么工作
- 如果数组模板类中的元素是Teacher元素时,Teacher类含有指针属性哪?
class Teacher { friend ostream & operator<<(ostream &out, const Teacher &obj); public: Teacher(char *name, int age) { this->age = age; strcpy(this->name, name); }
Teacher() { this->age = 0; strcpy(this->name, ""); }
private: int age; char name[32]; };
|
class Teacher { friend ostream & operator<<(ostream &out, const Teacher &obj); public: Teacher(char *name, int age) { this->age = age; strcpy(this->name, name); }
Teacher() { this->age = 0; strcpy(this->name, ""); }
private: int age; char *pname; }; |
结论1: 如果把Teacher放入到MyVector数组中,并且Teacher类的属性含有指针,就是出现深拷贝和浅拷贝的问题。
结论2:需要Teacher封装的函数有:
- 重写拷贝构造函数
- 重载等号操作符
- 重载左移操作符。
理论提高:所有容器提供的都是值(value)语意,而非引用(reference)语意。容器执行插入元素的操作时,内部实施拷贝动作。所以STL容器内存储的元素必须能够被拷贝(必须提供拷贝构造函数)。
- 请从数组模板中进行派生
//演示从模板类 派生 一般类 #include
class { public: MyArray01(int { ; } protected: private: };
//演示从模板类 派生 模板类 //BoundArray template <typename class { public: MyArray02(int { ; } protected: private: }; |
测试案例:
//演示 从模板类 继承 模板类 void main() { MyArray02<double> dArray2(10); dArray2[1] = 3.15;
}
//演示 从模板类 继承 一般类 void main11() { MyArray01 d_array(10);
for (int i = 0; i<d_array.getLen(); i++) { d_array[i] = 3.15; }
for (int i = 0; i<d_array.getLen(); i++) { cout << d_array[i] << " "; }
cout << "hello..." << endl; system("pause"); return; }
|