我们在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;
}
执行结果