[课业] 04 | C++ | 类属多态(模板)

总述

之前讲的操作符重载是多态的一种
类属多态是另一种多态,对应C++中间的模板机制
模板机制也是一种源代码复用机制
泛型:用独立于任何类型的方式编写代码,使代码支持不同类型
用类型参数来参数化模块

类属函数(template function)

一些信息

  1. 涵义:同一函数对不同类型的数据完成相同的操作
  2. 宏实现:
    #define max(a, b) (((a) > (b))?(a):(b))
    
    缺点:只能实现简单功能(如比大小),复杂功能难以实现(如排序);没有类型检查
    不方便使用
  3. 函数重载实现:
    int max(int, int);
    double max(double, double);
    A max(A, A);
    
    需要定义的重载函数太多,无法定义完全
  4. 函数指针实现:
    void sort(void* ,unsigned int, unsigned int, int(* cmp)(void*, void* ));
    //首参:万能指针来接受数组
    //函数指针传递具体的比较规则
    
    缺点:
    ·需要定义额外参数
    ·大量指针运算
    ·实现起来复杂(需要定义很多cmp函数)
    ·可读性差

模板

引入template目标:实现这个需求的支持类型完全、清晰可读的目标
模板代码:将于类型相关的信息屏蔽掉,变成类型参数:

template<typename T>
void sort(T A[], unsigned int num){
	for(int i = 1;i < num;i++){
		for(int j = 0;j < num - i;j++){
			if(A[j] > A[j + 1]){
				T t = A[j];
				A[j] = A[j + 1];
				A[j + 1] = t;
			}
		}
	}
}
int a[100];
sort(a,100);

double b[100];
sort(b,100);

//自定义类型里要重载操作符,重新定义大小的含义
class C{...};
C c[100];
sort(c,100);

compiler编译时编译的是带具体类型的代码,如上例,编译的是两段代码
编译器可以隐式具体化
也支持自定义类型,不过要谨慎地重载操作符
注意:赋值操作符、拷贝构造函数要谨慎
一些描述:

  1. 函数模板定义了一类重载函数;compiler自动实例化模板
  2. 函数模板可带类型参数,也可以带普通参数(但是要写在类型参数后)
    template<typename T, int size>
    void f(T a){
    	T temp[size];
    	......
    }
    ......
    f<int, 10>(1);
    //类型参数可以推导,故不用显式实例化
    
    这就是直接使用函数模板的普通参数的例子
  3. template普通参数可以有默认值(必须是编译时就能够确认的常量表达式);类型参数也可以有默认值,如:
    template<typename T1 = int>
    
    与普通函数的默认参数值相比,函数模板的默认参数值位置更加灵活,可出现在任何位置(而前者的只能出现在参数列表右边的开始)
  4. 一个问题:编译器会推导出默认参数值,实际使用函数模板(如f(T1 a, T2 b))是可以推出参数类型的(类参的默认值)
    类参的默认值的用处:
    当函数模板定义的函数本身有默认值时,如f(T1 a, T2 b),这时compiler要求T1、T2的默认类型也要给出

一例:函数模板的使用

template<typename T>
T max(T a, T b){
	return a > b ? a : b;
}
......
int x, y, z;
double l, m, n;
z = max(x, y);
l = max(m, n);

问:max(int, double)的处理如何?
解决:用一个函数重载作为补充(对应该类型的形参列表)

函数模板与函数重载配合使用

  1. 调用函数的优先级:
    优先匹配非模板的重载函数
    其次匹配显式具体化模板
    最后匹配函数模板
  2. 实例化:编译器隐式实例化

类属类

类定义带有类型参加
一例:

template<class T>
class Stack{
	T buffer[100];
public:
	void push(T x);
	T pop();
};
template<class T>
void Stack<T>::push(T x){...}
template<class T>
T Stack<T>::pop(){...}
...
//创建时要显式实例化,编译器不会也不能自动推导
//不同的实例化类型是不同的类型
Stack<int> st1;
Stack<double> st2;
  1. 模板类定义了多个类
  2. 实例化:需要显式实例化
  3. 参数:可以有多个类型参数,也可以有多个普通参数,普通参数要写在类型参数后面
  4. 模板类们的静态成员问题:
    普通类的静态成员类内共享;
    模板类的静态成员的共享范围:静态成员属于实例化之后的类,每个静态成员只属于自己的类(而非整个模板)

一例:

template<class T, int size>			//可以有多个参数,类型参数、普通参数皆可
class Stack {
	T buffer[size];
public:
	void push(T x);
	T pop();
};
template<class T, int size>
void Stack<T,size>::push(T x){...}

template<class T, int size>
T Stack<T,size>::pop(){...}
......
//使用时,类模板必须显式实例化
//这样相当于可以通过模板方式给类赋予一定的初始值
Stack<int, 100> st1;
Stack<double, 200> st2;
  1. 类型参数也有默认值:
    模板类不按从右往左指定默认参数会有编译错误,这与模板函数的默认值机制不同;模板类的默认值机制与函数参数的默认值机制更加一致,而模板函数的更加灵活
    有了默认值,虽然还是要显式实例化,但是可以利用默认值

一例:

template<class T = int, int size = 100>
class Stack{
	T buffer[size];
public:
	void push(T x);
	T pop();
};

template<class T, int size>
void Stack<T,size>::push(T x){...}
template<class T, int size>
T Stack<T,size>::pop(){...}
...
Stack<int,100> st1;
Stack<double,200> st2;
Stack<> st3;	//表示使用默认值

总结

基础总结

  1. 模板:源代码复用机制(对不同的实例化,产生了这段代码),实例化之后生成具体函数/类
    函数实例化:隐式实现,根据具体的函数调用来实例化模板
    类模板实例化:创建对象时显式指定
    无论显式还是隐式实现,是否需要实例化由使用的点决定,即调用函数/创造对象时才来决定要不要实例化模板
    如果没有用到某个模板实例,编译系统不会生成相应的实例代码
  2. 泛型编程只是减少编写程序的负担,不影响编译程序的工作量

一例:

//file1.h
template<class T>
class S{
	T a;
public:
	void f();
};
//file1.cpp
#include "file1.h"
template<class T>
void S<T>::f(){...}	//类模板方法定义
template<class T>	
T max(T x,T y){return x>y?x:y;}	//函数模板
void main(){
	int a, b;
	max(a, b);	//实例化函数模板
	S<int> x;	//将T实例化成int
	x.f();
}
//file2.cpp
#include "file1.h"
extern double max(double, double);
void sub(){
	max(1.1, 2.2);	//编译时error
	S<float> x;		//编译时error
	s.f();
}

如上代码,file1、file2两个模块一起编译,不可通过编译

C++中模板的完整定义通常出现在头文件中
此要求原因:
以此代码为例,编译过程:
1. include "file1.h"之后进行语法检查,看有无max、S<float>等
2. 再进行链接:
如果可以,就会调用已经编译好的链接代码,但对于模板而言,只是根据调用点上的需求进行实例化
此代码中,file1单元只有两段编译出来的代码,分别为int类的max和int类的S<>
file2编译链接之后,发现他所要的double类型的max和float类型的S<>在file1中间没有被编译,他就会尝试自己去实例化模板
但是,引入到file2元文件中间的只有file1.h头文件,file2找不到max模板,S<>模板也不完全,故报编译错
3. 解决办法:
将模板所有定义都放在头文件中,file2就能自己实例化了

注意:关于是否会有多重定义的问题
编译器、连接器有机制去掉同一模板的多重定义,不会发生链接期错误
4.总结:
编译器遇到模板时没有分配存储空间,而是待实例化时才会为实例化的东西分配存储空间(要求我们使用模板时注意完整定义)

一例:

template<int N>		//模板仅有普通参数
class Fib{
public:
	enum{
		value = Fib<N - 1>::value + Fib<N - 2>::value
	};
};

template<>
class Fib<0>{		//一个实例化
	enum{
		value = 1
	};
};

template<>
class Fib<1>{		//另一个实例化
	enum{
		value = 1
	};
};

void main(){		//使用
	cout << Fib<8>::value << endl;
}

此例子是基于模板做完整编程
使用模板来算时,Fib<8>的值是在编译时期就确定的,这段代码一旦编译完就得到结果,不用运行(除了cout)

源编程(MetaProgramming)

  1. 源编程指编写一些生成、直接操作程序的程序,即这个程序可以生成、直接操作其它程序,比一般程序更高层级
  2. 源编程通过操纵程序实体,在编译时就计算出运行的一些常量、类型、代码
  3. C++通过模板来实现源编程,程序输入就是模板参数
    并期望编译时就得到返回值,故期望使用static const、enum等
    代码计算:通过类型计算进而选择具体到哪个类型
    选择语句:通过模板的特例化
    循环语句:模板的递归
    C++的模板是图灵完备的

以上面的例子为例:
通过不同参数值选择不同模板(具体实例),可以给定不同模板计算不同需求




 
 

___Fin___
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值