我们学习使用C++,肯定都要了解模板这个概念。就我自己的理解,模板其实就是为复用而生,模板就是实现代码复用机制的一种工具,它可以实现类型参数化,即把类型定义为参数;进而实现了真正的代码可重用性。模版可以分为两类:一个是函数模版,另外一个是类模版。
举个最简单的例子,当在编写好了一个进行int型交换的swap函数,而此后若又要进行double型交换,那就得重新写一个,如果又要交换string类型....这里就尴尬了。使用模板的目的就是要让程序的实现与类型无关,比如这里将swap弄成模板函数,即可以实现int 型,又可以实现double型的交换。
 
而针对函数模板,其格式是:
 template <class 形参名1, class 形参名2, class 形参名n> 
  返回类型 函数名(参数列表)
 {...}
 如交换函数:

template<class T>
void swap(T& a, T& b)
{....}
值得注意的是:模板进程形参的定义既可以使用class,也可以使用typename,含义是相同的;再一个就是模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
 
 

实现机理


模板,从字面意思上大致就能知道它只是一个模子,用的时候,还需我们进行一番修饰,也就是说写好的函数模板还不是直接使用的,只有在实例化的时候,编译器才会推演出我们要用的代码,推演的依据就正是我们传入的参数了。(这里,你可以去验证一下,比如先不调用函数,此时将模板代码中的一句分号去掉,编译器并不会报错)。实质上,说的通俗一点,实例化的时候,如int a =1,b=2; swap(a, b)就是编译器编译时直接用int将T替换,以此实例化一份代码出来。
 

模板显示实例化


前面我们值得使用函数模板时,编译器会针对参数的类型推演出相应的代码。如果我们在调用一个模板函数的时候,在函数名后面加上<类型>,这时候系统不需要从形参这里推断类型了。它直接使用尖括号里面的类型就ok了。返回类型形式是:函数名<类型>(参数列表),接下来我们来验证一下
 template<class T>
void Swap(T& a, T&b)
{
T tmp = a;
a = b;
b = tmp;
}
void Test( )
{
int a =1, b=2;
Swap<int>(a, b); //显示实例化
cout<<a<<" "<<b<<endl;
}

值得注意是此时模板中形参只有一种,所以调用swap函数不要传入两种类型不同的实参,这是不合法的。

相对来讲,模板函数算比较简单的,下面来看看模板类

 
模板类

它和模板函数类似,其格式如下
template<class 形参名1, class 形参名2, ...class 形参名n>
class 类名
{ ... };

对于类模板,实例化时,模板形参的类型必须在类名后的尖括号中明确指定。举个例子,如A<10> m, 这样是绝对不行的,类模板中是不存在实参类型的推断;也不能直接A a这样定义,道理很明显,这样编译器没法推演。

然后在类模板外部定义成员函数的方法为:

 template<模板形参列表>
函数返回类型 类名<模板形参名>::函数名(参数列表){函数体},

比如有两个模板形参T1,T2的类A中含有一个void f()函数,则定义该函数的语法为:

  template<class T1,class T2> void A<T1,T2>::f(){}。

注意:当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致。

来个实例,实现一个简易带头双向链表:

 /*************************************************************************
> File Name: List.cc
> Author: tp
> Mail:
> Created Time: Sat 05 May 2018 08:39:46 PM CST
************************************************************************/ #include <iostream>
#include <string>
#include <cassert>
using namespace std; template<class T>
struct ListNode
{
T _val;
ListNode* _prev;
ListNode* _next;
ListNode(const T& x)
:_val(x), _prev( NULL),_next( NULL)
{ }
}; template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
:_head(new Node(T()))
{
_head->_prev = _head;
_head->_next = _head;
}
void PushBack(const T& x)
{
Insert(_head, x);
}
void PopBack()
{
Erase( _head->_prev);
}
void PushFront(const T& x)
{
Insert(_head->_next, x);
}
void PopFront()
{
Erase(_head->_next);
}
void Insert(Node* pos, const T& x)
{
assert( pos);
Node* prev = pos->_prev;
//prev newnode pos
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev; newnode->_next = pos;
pos->_prev = newnode;
}
void Erase(Node* pos)
{
assert( pos && pos != _head);
//能删头结点?
Node* prev = pos->_prev;
Node* next = pos->_next;
delete pos;
prev->_next = next;
next->_prev = prev;
}
bool Empty()
{
return _head == _head->_next;
}
Node* Find(const T& x)
{
Node* cur = _head->_next;
while( cur != _head)
{
if( cur->_val == x)
return cur;
cur = cur->_next;
}
return NULL;
}
//以O( 1)的时间复杂度, 多开4字节进行计数,不能让头结点存计数,因为类型不确定
//这里普通做法
size_t Size()
{
size_t count = 0;
Node* cur = _head->_next;
while( cur != _head)
{
++count;
cur = cur->_next;
}
return count;
} void Clear( )
{
Node* cur = _head->_next;
while(cur != _head)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
~List( )
{
Clear();
_head = NULL;
}
void Print();
protected:
Node* _head;
};
template<class T>
void List<T>::Print()
{
Node* cur = _head->_next;
while( cur != _head)
{
cout<<cur->_val<<" ";
cur = cur->_next;
}
cout<<endl;
}
int main( )
{
List<int> l;
l.PushBack(1);
l.PushBack(2);
l.PushBack(3);
l.PushBack(4);
l.PopBack( );
l.Print();
cout<<l.Size( )<<endl; List<string> l1;
l1.PushBack( " 11");
l1.PushBack( " 1i32");
l1.PushBack( " 1112019jhja");
l1.PushBack( " 11");
l1.Print( );
return 0;
}
 
 

模板中的形参


·类型形参
 接下我们来看模板的形参,类型形参由关见字class或typename后接说明符构成。
如:
           template<class T> void func(T a){};
 
其中T就是一个类型形参,表示一种类型,类型形参的名字由用户自已确定。模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符,可用在模板中的任何地方;它与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。
但是注意不能为同一个模板类型形参指定两种不同的类型,比如template<class T> void func(T& a, T& b){},语句调用func(2, 2.1)将出错,因为该语句给同一模板形参T指定了两种类型,第一个实参2把模板形参T指定为int,而第二个实参2.1把模板形参指定为double,两种类型的形参不一致,会出错。
当然在模板类中,这样也会有不合适,但是在这里是一个警告。但是记住,无论是模板函数还是模板类都不要这样用。
 
·非类型模板形参

非类型模板形参:模板的非类型形参也就是内置类型形参

如:template<class T, int a> class A{};其中int a 就是非类型的模板形式参。非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。

 #include <iostream>
using namespace std; template<class T, int b>
class A
{
public:
A()
:a(b){}
void show( )
{cout<<a<<endl;}
protected:
T a;
};
int main(void)
{
const int i = 10;
A<int, i> a;
a.show();
return 0;
}

1. 调用非类型模板形参的实参必须是一个常量表达式,即它必须能在编译时计算出结果。sizeof表达式的结果是一个常量表达式,全局变量的地址或引用,全局对象的地址或引用const类型变量也是常量表达式 ,它们可以用作非类型模板形参的实参。

2. 非类型模板的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针则是可以的。

3. 任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。

4、当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量

比如:template <class T, int a> class A{};

如果有int b,这时A<int, b> m;将出错,因为b不是常量,如果const int b,这时A<int, b> m;就是正确的,因为这时b是常量。

5. 非类型模板形参的形参和实参间所允许的转换:

·模板形参

1. 类模板的类型形参默认值形式为:template<class T1, class T2=int> class A{...},这样来为模板中的第二个形参T2提供int型的默认值。

2、模板形参可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。

3、类模板类型形参添加默认值的规则和函数默认参数规则一样。如果有多个类型形参,参数从右向左连续的缺省,因为要符合的参数从右向左的入栈规则。比如template<class T1=int, class T2>class A{};就是错误的,因为T1给出了默认值,而T2没有设定。

4、在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型

比如template<class  T1, class T2=int> class A{public: void func();};

定义方法为template<class T1,class T2> void A<T1,T2>::func(){},将int省略掉。

大致了解了上面模板的一些用法,我们可以来实现一个东西——适配器

//紧接上面List的头文件
.................. template <class T, template<class>class Container>
class Vector
{
public:
void Push( const T& x)
{
_con.PushBack(x);
}
void Pop()
{
_con.PopBack();
}
size_t Size()
{
return _con.Size();
}
bool Empty( )
{
return _con.Empty( );
}
void Print( )
{
_con.Print( );
}
private:
Container<T> _con;
};
void test( )
{
Vector<string, List> v;
v.Push("hello");
v.Print( );
cout<<v.Empty();
}

这里用到了上面实现的双向链表,以此简陋地来模拟STL里面的vector(其实这里用链表版本并不合适,STL里面用到是顺序表)。

然后来看看模板参数里的这个template<class> class Container ,第一次看的话,可能你我多少会有些小慌,毕竟出来个这么长的怪物。细细来看 其实就好理解了,template<class>指明了这是模板参数且是模板类类型,后面取了名字叫Container。所以这就相当于声明Container是一个模板类类型的类模板参数。这算是一种固定搭配,念着有些拗口,记住就好。

然后就是下面的  Container<T> _con; 它意思就是创建一个Container的对象,而我们也知道这个对象也是模板类对象,所以把<T>给它传进去。然后我们现在来说一下这个代码有什么用?

其实这里的Vector类相当于一个适配器。适配器有一种“让一种事物的行为类似于另外一种事物行为”的机制,它对容器进行包装,使其表现出另外一种行为。针对不同类型的数据,在存、删数据时,我们不用再去实现不同版本的线性表;我们直接去用List类里面的函数,具体就是直接定义一个List的对象,然后直接拿走进行使用,最后让Vector管理不同类型数据,进而完成适配;再比如STL里面一个管理Int数据的栈,它的实现是stack<int, vector<int> >的,其内部其实是使用顺序容器vector<int>来存储数据(相当于是vector<int>表现出了栈的行为)。这些都是灵活的复用的体现。

最后,模板不支持分离编译

还是用上面的交换函数举例,不过这时我们将.h  和.cpp文件分开。

***************Swap.h************
#include <iostream>
using namespace std; template<class T>
void Swap(T& a, T& b); ***************Swap.cpp************
#include "Swap.h" template<class T>
void Swap(T& a, T&b)
{
T tmp = a;
a = b;
b = tmp;
} ***************main.cpp************
#include "Swap.h" int main(void)
{
int a =1, b=2;
Swap<int>(a, b);
cout<<a<<" "<<b<<endl;
return 0;
}

在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好(main函数里面调用swap函数时,.h文件告诉编译器,让连接器去其它的.obj文件找地址),但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化代码出来(这里swap.cpp里面未调用swap函数,便无法推演出代码出来,自然就无法call到swap函数地址了),当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化了。
 
 那么解决这种问题的办法就是
  1. 在模板头文件 xxx.h 里面显示实例化,针对模板类,就在模板类的定义后面添加 template class List<int>; 一般不推荐这种方法,一方面老编译器可能不支持,另一方面实例化依赖调用者。
  2. 将声明和定义放到一个文件 "xxx.hpp" 里面,(这样编译时头文件展开,实例化代码便很轻松了)。通常我们会使用这种方法,但这种方法也有缺陷,因为实际开发中,.h文件是别人看的,而开发者并不希望别人细致了解内部的具体实现。
 
 
 
 
 

C++ 模板基础的更多相关文章

  1. C++ template —— 深入模板基础(二)

    上一篇C++ template —— 模板基础(一)讲解了有关C++模板的大多数概念,日常C++程序设计中所遇到的很多问题,都可以从这部分教程得到解答.本篇中我们深入语言特性.------------ ...

  2. C++模板编程-模板基础重点

    模板基础 1.模板参数自动推导,如果是已知的参数类型与个数,这调用模板时可以不写类型. Cout<<max<int>(1,3);可以写为Cout<<max(1,3) ...

  3. Myeclipse Templates详解(一) —— Java模板基础

    目录 Templates简介 MyEclipse自带Templates详解 新建Template 自定义Template 因为自己比较懒,尤其是对敲重复代码比较厌恶,所以经常喜欢用快捷键和模板,Mye ...

  4. Django模板-基础知识

    上一篇中带参数的URLconf虽然可以做到传参动态显示内容,但是最终现实的内容还是硬编码到Python代码中的 def hours_ahead(request,phours): try: phours ...

  5. MVC开发T4代码生成之一----文本模板基础

    T4文本模板 T4全写为Text Template Transformation Toolkit,是一种编程辅助工具,用来使程序代码自(懒)动(猿)生(福)成(利)的工具.MVC开发中大量使用了T4模 ...

  6. [转]C++ template —— 模板基础(一)

    <C++ Template>对Template各个方面进行了较为深度详细的解析,故而本系列博客按书本的各章顺序编排,并只作为简单的读书笔记,详细讲解请购买原版书籍(绝对物超所值).---- ...

  7. C++ template —— 模板基础(一)

    <C++ Template>对Template各个方面进行了较为深度详细的解析,故而本系列博客按书本的各章顺序编排,并只作为简单的读书笔记,详细讲解请购买原版书籍(绝对物超所值).---- ...

  8. smarty模板基础

    将前台后台隔离,前台控制显示,后台控制逻辑/内容,与cms类似 原理: 用户访问text.php页面,后台调用类smarty.class.php显示静态模板;

  9. smarty模板基础1

    smarty模板的作用可以让前端和后端分离(也就是前端的显示页面和后端的php代码). smarty模板的核心是一个类,下载好的模板中有这么几个重要的文件夹 (1)libs核心文件夹(2)int.in ...

  10. smarty模板基础2

    Smarty自带了一些内置函数,这些内置函数是Smarty模板引擎的组成部分.他们被编译成相应的内嵌PHP代码,以获得最大性能. 您创建的自定义函数不能与内置函数同名,也不必修改这些内置函数. 其中一 ...

随机推荐

  1. json进阶(一)js读取解析JSON类型数据

    js读取解析JSON类型数据 一.什么是JSON? JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式,同 ...

  2. JAVA之旅(五)——this,static,关键字,main函数,封装工具类,生成javadoc说明书,静态代码块

    JAVA之旅(五)--this,static,关键字,main函数,封装工具类,生成javadoc说明书,静态代码块 周末收获颇多,继续学习 一.this关键字 用于区分局部变量和成员变量同名的情况 ...

  3. 关于Android自定义view 你所需要知道的基本函数

    开始时之前想吐槽一句..iphone的闹钟,12小时制.我成功的把闹钟订到了下午5:00 导致错过一班飞机.心疼改签费. 候机ing,没有事做,来写一下学习自定义view必须掌握的基本函数.这里只挑一 ...

  4. SpriteBuilder中音频波长超过Timeline结尾的情况

    见如下图: 注意最后一个音频波长延续到Timeline结尾之后.表明这个音频文件播放长度超过Timeline(动画)播放的长度.这是否成为一个问题要视情况而定.而在这里无所谓. 如果节点所拥有的Tim ...

  5. 数据挖掘进阶之关联规则挖掘FP-Growth算法

    数据挖掘进阶之关联规则挖掘FP-Growth算法 绪 近期在写论文方面涉及到了数据挖掘,需要通过数据挖掘方法实现软件与用户间交互模式的获取.分析与分类研究.主要涉及到关联规则与序列模式挖掘两块.关联规 ...

  6. Android Camera开发系列(下)——自定义Camera实现拍照查看图片等功能

    Android Camera开发系列(下)--自定义Camera实现拍照查看图片等功能 Android Camera开发系列(上)--Camera的基本调用与实现拍照功能以及获取拍照图片加载大图片 上 ...

  7. 数据包接收系列 — NAPI的原理和实现

    本文主要内容:简单分析NAPI的原理和实现. 内核版本:2.6.37 Author:zhangskd @ csdn 概述 NAPI是linux新的网卡数据处理API,据说是由于找不到更好的名字,所以就 ...

  8. EBS-子库存转移和物料搬运单区别

    FROM:http://bbs.erp100.com/forum.php?mod=viewthread&tid=261550&extra=page%3D7 EBS-子库存转移和物料搬运 ...

  9. 图片像素对比OpenCV实现,实现人工分割跟算法分割图像结果的对比

    图片对比,计算不同像素个数,已经比率.实现人工分割跟算法分割图像结果的对比,但是只能用灰度图像作为输入 // imageMaskComparison.cpp : 定义控制台应用程序的入口点. // / ...

  10. 如何在服务器上配置ODBC来访问本机DB2 for Windows服务器

    如何在服务器上配置ODBC来访问本机 DB2 for Windows服务器                         马根峰             (广东联合电子服务股份有限公司, 广州 51 ...