c++(24)STL引入:函数模板、类模板

我们在C语言的常规编程工作中,经常会遇到因为形参数据类型,而定义多个函数。比如功能交换A和B的值

//int 类型数据交换
void MySwap(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

//double 类型数据交换
void MySwap(double &a, double &b)
{
	double temp = a;
	a = b;
	b = temp;
}

只要A和B 这两个数据的类型不同,我就要重定义一个新函数,而这两个函数除了形参数据类型不一样,其他的逻辑都是一样的。这样就造成了代码的重复,增加维护成本。

为了解决这个问题,c++引入了 模板。

1、函数模板的基本语法

函数模板使形参类型化,实现定义的时候不关心具体的数据类型,只关心功能的实现。

编译器为了与普通函数区分,使用关键字template。

template<class T>  
template<typename T> 这两种使用方式表达的意思一样,根据习惯喜欢用哪个就用哪个。如果说需要使用多个类型的参数,那么也可以增加定义

template<typename T1, typename T2, typename T3>

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

//template<class T>  // 
template<typename T> //这两种方式都可以  //如果需要多个类型参数的时候,可以增加定义 
//template<typename T1, typename T2, typename T3>
//使用template模板函数的时候,当出现template关键字的时候,只对紧接着出现的函数名生效
void MySwap(T &a, T &b)
{
	T temp = a;
	a = b;
	b = temp;
}
//上面template的作用域消失

int main(void)
{
	int a = 10, b = 20;
	cout <<"a="<<a<<", b="<<b<<endl;
	//调用方式1,传递编译器根据传递的值,自动推导数据类型
	MySwap(a, b);
	cout <<"a="<<a<<", b="<<b<<endl;
	
	double c = 6.66, d = 9.99;
	cout <<"c="<<c<<", d="<<d<<endl;
	//调用方式2,显示的指定数据类型
	MySwap<double>(c, d);
	cout <<"c="<<c<<", d="<<d<<endl;
	
	return 0;
}


需要注意的是,当使用template模板函数的时候,当出现template关键字的时候,只对紧接着出现的函数名生效。

上面的例子,当MySwap函数结束的时候,template模板的作用域也消失。

2、函数模板和普通函数的区别

        函数模板不允许自动类型转化,必须严格的类型匹配

        普通函数能够自动进行类型转化

比如int类型的数据,在使用的时候也可以用char、short 类型接收,但是函数模板不允许,要严格进程数据类型识别。

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template <class T>
T MyAdd(T a, T b)
{
	cout<<"函数模板"<<endl;
	return (a+b);
}

int MyAdd(int a, char b)
{
	cout<<"普通函数"<<endl;
	return (a+b);
}
int main(void)
{
	int a = 10, b = 20;
	char c = 30;
	
	cout<<MyAdd(a, c)<<endl;
	cout<<MyAdd(a, b)<<endl;
	cout<<MyAdd(c, a)<<endl;
	
	return 0;
}


 3、函数模板和普通函数在一起调用的规则

(1)、函数模板可以像普通函数那样被重载

template<class T>
void Print(T a)
{
}

template<class t>
void Print(T a, T b)
{
}

(2)、c++编译器优先考虑普通函数   

                 MySwap(T a, T b);    MySwap(int a, int b); 当形参都是int时,优先考虑普通。

(3)、如果函数模板可以产生一个更好的匹配,那么选择模板

(4)、可以通过空模板实参列表的语法限定编译器只能通过模板匹配 

                 MySwap<>(a, b);限定只调用函数模板   

4、c++编译模板机制剖析,分析函数模板是如何实现的

 当我们需要编译一个test.cpp的时候,他的一个编译过程大概是这样的。首先预编译器会先将宏定义进行展开,生成test.i文件,此时的这个 .i 文件我们还能看得懂。(通过编辑器打开,可以在最下面发现 宏定义展开后的代码)

g++ -E test.cpp -o test.i

之后编译器会将.i文件,进行翻译编译,生成汇编文件test.s,这个时候已经不太能 看得懂了,当然ABCD的字母还是看得懂。

 g++ -S test.i -o test.s

在之后汇编器,会将.s文件变成二进制文件,也就是目标文件,也就是test.o文件。(在windows下是.obj文件)

 g++ -c test.s -o test.o

最后到链接器,将很多.o文件和链接库文件,最后都合在一起,生成可执行文件。(linux可执行文件,文件名可自定义。windows下是.exe文件)。

g++ test.s -o a.out

 函数模板的机制,其实是编译器在编译阶段,对函数模板进行了翻译。将整个工程中,被调用的函数模板翻译成了普通函数。调用了N种数据类型的模板,就会生成N个不同名称的普通函数。

 下面我们来验证一下

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template <class T> //注意没有分号
T MyAdd(T a, T b)
{
	return (a+b);
}

int main(void)
{
	int nVal1 = 10, nVal2 = 20;
	char chVal1 = 30, chVal2 = 40;
	float fVal1 = 6.66, fVal2 = 9.99;
	
	MyAdd(nVal1, nVal2);
	MyAdd(chVal1, chVal1);
	MyAdd(fVal1, fVal2);
	
	MyAdd(nVal1, nVal1);
	
	return 0;
}


上面这个例子一共调用了四次MyAdd模板,我们对源文件直接进行编译g++ -S test.cpp -o test.s,生成 .s 文件。然后直接打开.s文件进行分析

.s文件还是能让我们稍微看懂一点,但是对与理解原来有很大帮助。我们看到里面有一个熟悉的main函数,在这里面call了四次 MyAdd,我们在.s里也发现了熟悉的MyAdd,只不过名字不太一样,多了一点后缀。

分别叫 细心点可以发现,里面不同的i、c、f、i就是  对应的数据类型的缩写,int、char、float、int。跟我们实际的调用情况也吻合。

那么我们分析函数模板是如何实现的结论:

编译器并不是把函数模板处理成能够处理任何类型的函数,而是在编译阶段,函数模板通过具体类型产生的不同函数,编译器对函数模板进行二次编译,在声明的地方对模板函数代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

5、函数模板案例char,int,float数据排序

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template <class T> 
void MySort(T *a, int len)
{
	int i=0, j=0;
	cout<<"排序之前的数据:"<<endl;
	for (i=0; i<len; i++)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;
	
	//冒泡排序,从小到大
	for(i=0; i<len; i++)
	{
		for(j=i+1; j<len; j++)
		{
			if (a[i] > a[j])
			{
				T temp = a[i];
				a[i] = a[j];
				a[j] = temp;
			}
		}
	}
	
	cout<<"排序之后的数据:"<<endl;
	for (i=0; i<len; i++)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl<<"----------------"<<endl;
}

int main(void)
{
	int nArr[] = {3, 4, 1, 5, 2};
	char chArr[] = {'b','d','c','e','a'};
	float fArr[] = {1.1, 3.3, 5.5, 4.4, 2.2};
	
	MySort(nArr, sizeof(nArr)/sizeof(int));
	MySort(chArr, sizeof(chArr)/sizeof(char));
	MySort(fArr, sizeof(fArr)/sizeof(float));
	
	return 0;
}


6、类模板

类模板和函数模板很相似,有一点区别。函数模板在调用的时候,可以自动类型推导。但是类模板必须显示的指定类型。

要注意在类模板派生普通类的时候,子类的定义也要指定数据类型。

类模板派生类模板的时候,如果不指定类型。那么需要再使用template关键字,定义模板类型。

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template <class T> //注意没有分号
//类模板
class people
{
public:
	people(T age)
	{
		this->m_Age = age;
	}
public:
	T m_Age;
};

//类模板派生普通类
class student : public people<int>
{
public:
};

//类模板派生类模板
template <class T> //注意没有分号
class adult : public people<T>
{
public:
	//类模板的,类内实现,需要指定模板类people<T>
	adult(T age):people<T>(age)
	{
	}
	
	void show();
};

//类模板的,类外实现,也需要指定模板类people<T>
template <class T> 
void adult<T>::show()
{
	//在类模板中使用成员变量,必须要加上this,或者指定类模板作用域,否则编译报错
	// adult<T>::m_Age
	cout<<"i am adult, my age is "<<this->m_Age<<endl; 
}

int main(void)
{	
	adult<int> a1(30);
	a1.show();
	
	adult<string> a2("30岁");
	a2.show();
	return 0;
}


这里一定要注意,使用类模板的时候,类内定义实现和类外定义实现的区别。其实如果对上面编译器的编译机制理解的比较好的话,就知道在分配内存的时候必要要指定类型,来区分内存大小。

当他是模板的时候,类型不明确。当变量不指定类型或者模板的时候,就更不知道具体的数据大小了。

 7、类模板重载操作符

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

//还有一种使用比较多的方式,就是下面这种,先声明模板类,但是不常于友元
template<class T1, class T2> class people;
template<class T1, class T2> void print(people<T1, T2> &p);
template<class T1, class T2> ostream & operator<<(ostream &os, people<T1,T2> &p);

template<class T1, class T2>
class people
{
public:
	people(T1 name, T2 age)
	{
		this->m_name = name;
		this->m_age = age;
	}
	
	//声明类外友元函数的时候,编译器并不认识T1,T2,所以也要加上template关键字
	//template<class T1, class T2>  //windows下可以直接通过,但是linux下编译不通过
	//friend ostream & operator<<<T1,T2>(ostream &os, people<T1,T2> &p);
	//template<class I1, class I2>//linux下要重新修改T1 和 T2的名称
	//friend ostream & operator<<(ostream &os, people<I1,I2> &p);
	
	//普通友元的使用方法
	//template<class C1, class C2>
	//friend void print(people<C1,C2> &p);
public:
	T1 m_name;
	T2 m_age;
};

//类外友元函数,<<操作符重载
template<class T1, class T2>
ostream & operator<<(ostream &os, people<T1,T2> &p)
{
	os<<"name:"<<p.m_name<<", age:"<<p.m_age;
	return os;
}

template<class T1, class T2>
void print(people<T1,T2> &p)
{
	cout<<"name:"<<p.m_name<<", age:"<<p.m_age<<endl;
}

int main(void)
{	
	people<string, int> a("li4", 18);
	cout<<a<<endl;
	print(a);
	
	return 0;
}


警告:我们在实际使用模板的时候,千万要避免使用友元!!!

因为一旦友元碰上模板,就会特别麻烦,破坏类的封装性就算了,不同的编译器处理还不同。非常不建议使用!!!

8、linux环境中多文件分离编译

我们先了解一下c++编译机制  还有 模板的实现机制 

模板定义在编译的时候,不会生成对应的实现函数。只有在调用的时候才会根据数据类型生成对应的具体实现函数。

所以当我们使用make将几个文件放一起编译的时候,只要main文件中使用了people就会报错。因为编译器在main函数中,没有找模板函数的实现,(类模板的实现)。

比如people<string, int> p("zhang3", 18);的时候,编译器在构造函数定义在当前的文件中没有找到,编译就会认为这个函数在其他的文件中。会让链接器在链接的时候,去找这个函数的具体位置。

源文件  4-template.cpp

#include "4-template.h"

//函数模板  经过两次编译
//并没有生成具体的函数,因为在此文件中并没有具体使用
template<class T1, class T2>
people<T1, T2>::people(T1 name, T2 age)
{
	this->m_name = name;
	this->m_age = age;
}

template<class T1, class T2>
void people<T1, T2>::show()
{
	cout<<"name:"<<this->m_name<<", age:"<<this->m_age<<endl;
}

 main.cpp

#include <iostream>
using namespace std;

//编译器在main函数中,调用people的构造,会由于模板的编译实现机制,而找不到具体数据类型的函数实现。
//会通知链接器在链接的时候 去找实现,而我们的类实现cpp,由于是分开编译的。具体函数实现,是在调用的时候才会生成。
//为了解决这个问题,我们其实就把cpp文件放进来 一起编译就行了


//可以直接放在这里,include
#include "4-template.cpp"

//还有一种方式,就是我们在使用类模板的时候,直接把模板的实现cpp和h文件合成一个,叫做hpp文件
//就相当于将声明和实现  放在一起
//#include "4-template.hpp"

int main(void)
{	
	people<string, int> p("li4", 18);
	p.show();
	
	return 0;
}

4-template.h

#pragma once
#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template<class T1, class T2>
class people
{
public:
	people(T1 name, T2 age);
	void show();
	
public:
	T1 m_name;
	T2 m_age;
};


makefile

TOP := ..
COMM_DIR := .
SRC_DIR := .
INC_DIR := .

APP_TARGET  := template

## Object files that compose the target(s)
COMPILE_FILE := $(SRC_DIR)/4-template \
				$(SRC_DIR)/main

PROJ_FILES := $(foreach obj,$(COMPILE_FILE),$(obj).cpp)
PROJ_OBJS  := $(notdir $(COMPILE_FILE))
PROJ_OBJS  := $(foreach obj,$(PROJ_OBJS),$(obj).o)

## include and lib path in shared object file
INC_PATH += $(INC_DIR)

##rules
CPP := g++

all:$(APP_TARGET)

$(APP_TARGET):
	$(CPP) $(CFLAGS) -c $(PROJ_FILES)
	$(CPP) -o $(APP_TARGET) $(PROJ_OBJS)
	
clean:
	rm -f $(PROJ_OBJS) *.pdb *.map $(APP_TARGET)

上面就是使用模板类,当cpp文件h文件分开编译的时候的解决方案。其实我们在实际工作中,很罕见直接include cpp文件的。当我们使用类模板的时候,会直接把模板的实现cpp和h文件合成一个,叫做hpp文件,就相当于将声明和实现  放在一起。在使用的时候,直接包含这个hpp文件,

这样也方便其他人阅读代码,知道你在这个里面使用了模板
#include "4-template.hpp"

源文件4-template.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

template<class T1, class T2>
class people
{
public:
	people(T1 name, T2 age);
	void show();
	
public:
	T1 m_name;
	T2 m_age;
};

template<class T1, class T2>
people<T1, T2>::people(T1 name, T2 age)
{
	this->m_name = name;
	this->m_age = age;
}

template<class T1, class T2>
void people<T1, T2>::show()
{
	cout<<"name:"<<this->m_name<<", age:"<<this->m_age<<endl;
}

9、当类模板遇上static关键字

一句话,类模板中的static 变量,归具体类所有。

 使用方法

 

p1 p2 p3 共享int a;

pp1  pp2  pp3共享int a;且跟上面的a是两个地址空间 

10、MyArray类模板案例

写一个自定义数组的模板案例。比较简单,测试程序中使用了常规数据类型int,和自定义数据类型people类。这样的案例,看起来,就比较像STL提供的动态数组了。

其中还涉及到c++11中关于对右值取引用的内容,可以看代码的注释。下面是源码,有兴趣的可以跟着我的代码敲一遍,加深关于类模板的理解。

#include <iostream>
#include <cstring>
#include <memory>
using namespace std;

//people元素
class people
{
public:
	people()
	{
		this->m_name = "无名氏";
		this->m_age = 0;
	}
	
	people(string name, int age)
	{
		this->m_name = name;
		this->m_age = age;
	}
	
	people &operator=(const people &another) //=号操作符重载
	{
		if (this == &another)
		{
			return *this;
		}
	
		this->m_name = another.m_name;
		this->m_age = another.m_age;
		return *this;
	}
	
	//MyArray模板的show使用cout
	friend ostream &operator<<(ostream &os, const people &p);
public:
	string m_name;
	int m_age;
};

ostream &operator<<(ostream &os, const people &p)
{
	os<<"name:"<<p.m_name<<", age:"<<p.m_age<<endl;
	return os;
}

template<class T>
class MyArray
{
public:
	MyArray(int nCap)
	{
		cout<<"构造:MyArray(int nCap)..."<<endl;
		this->m_nCap = nCap;
		this->m_nSize = 0;
		this->pAddr = new T[this->m_nCap];
	}
	
	MyArray(const MyArray &another)//拷贝构造
	{
		cout<<"构造:MyArray(const MyArray &another)..."<<endl;
		this->m_nSize = another.m_nSize;
		this->m_nCap = another.m_nCap;
		
		this->pAddr = new T[this->m_nCap];
		for (int i=0; i<this->m_nSize; i++)
		{
			this->pAddr[i] = another.pAddr[i];
		}			
	}
	
	~MyArray()
	{
		cout<<"析构:~MyArray()..."<<endl;
		if (this->pAddr != NULL)
		{
			delete[] this->pAddr;
			this->pAddr = NULL;
			this->m_nSize = 0;
			this->m_nCap = 0;
		}
	}
	
	T& operator[](int nIndex)//[]操作符重载
	{
		return this->pAddr[nIndex];
	}
	
	MyArray<T> operator=(const MyArray &another) //=号操作符重载
	{
		cout<<"=号操作符重载:MyArray<T> &operator=(const MyArray &another)..."<<endl;
		if (this->pAddr == another.pAddr)
		{
			return *this;
		}
		
		if (this->pAddr != NULL)
		{
			delete[] this->pAddr;
			this->pAddr = NULL;
			this->m_nSize = 0;
			this->m_nCap = 0;
		}
	
		this->m_nSize = another.m_nSize;
		this->m_nCap = another.m_nCap;
		this->pAddr = new T[this->m_nCap];
		for (int i=0; i<this->m_nSize; i++)
		{
			this->pAddr[i] = another.pAddr[i];
		}
		return *this;
	}


	void PushBack(T &data)
	{
		if (this->m_nSize >= this->m_nCap)
			return ;
		
		this->pAddr[this->m_nSize++] = data;
	}

	//这是c++11的新标准,对T &&对右值取引用
	void PushBack(T &&data)
	{
		if (this->m_nSize >= this->m_nCap)
			return ;
		
		this->pAddr[this->m_nSize++] = data;
	}
	
	void show()
	{
		for (int i=0; i<this->m_nSize; i++)
		{
			cout<<this->pAddr[i]<<" ";
		}
		cout<<endl;
	}
	
public:
	int m_nCap; //能够容下元素数量的上限
	int m_nSize;//当前数组有多少元素
	T* pAddr;   //数组首地址
};

int main(void)
{	
	MyArray<int> arr(20);
	int a=10, b=20, c=30, d=40;
	arr.PushBack(a);
	arr.PushBack(b);
	arr.PushBack(c);
	arr.PushBack(d);
	
	//思考:为什么不能直接arr.PushBack(10);arr.PushBack(20);
	//因为声明时是 void PushBack(T &data);形参是引用,是一个左值,而常量不能作为左值。
	//解决方案:重载 void PushBack(T &&data);   这是c++11的新标准,对T &&对右值取引用
	arr.PushBack(100);
	arr.PushBack(200);
	arr.PushBack(300);
	arr.PushBack(400);
	for (int i=0; i<arr.m_nSize; i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<"-----------------------------"<<endl;
	
	MyArray<int> arr1 = arr;
	arr1.show();
	cout<<"-----------------------------"<<endl;

	MyArray<int> arr2(10);
	arr2 = arr;  //这里有一个值拷贝动作,会有一份临时变量被构造和析构,因为我们的=号操作符重载是返回值,而非引用
	arr2.show();
	cout<<"-----------------------------"<<endl;
	
	people p1("zhang3", 18);
	people p2("li4", 20);
	people p3("wang5", 25);
	MyArray<people> ap(5);
	ap.PushBack(p1);
	ap.PushBack(p2);
	ap.PushBack(p3);
	ap.show();
	cout<<"-----------------------------"<<endl;
	
	MyArray<people> ap1(5);
	ap1 = ap;
	ap1.show();
	cout<<"-----------------------------"<<endl;
	
	return 0;
}

执行结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值