C++模板编程

原文链接:http://www.cppblog.com/besterChen/archive/2010/07/22/121000.html

当我们越来越多的使用C++的特性, 将越来越多的问题和事物抽象成对象时, 我们不难发现:很多对象都具有共性。 比如 数值可以增加、减少;字符串也可以增加减少。 它们的动作是相似的, 只是对象的类型不同而已。

C++ 提供了“模板”这一特性, 可以将“类型” 参数化, 使得编写的代码更具有通用性。 因此大家都称模板编程为 “通用编程”或 “泛型编程”。

一般而言, 模板分为 函数模板 和 类模板,下面就让我们分别来了解一下它们。

一、 函数模板

1、 函数模板的定义和使用

定义一个模板函数的格式并不复杂, 如下:

template <模板参数列表>

返回类型 函数名(函数参数列表)

{

    // code ...

}

下面, 让我们来举一个例子来说明 模板函数的作用和用法(具体代码见 Exp01)。

// 定义一个函数模板, 用来实现任意类型数据的相互交换。

template <typename T> // 声明一个 T 数据类型, 此类型随 调用方 类型的变化而变化

void swap(T& a, T& b)

{

    T tmp = a; 

    a = b;

    b = tmp;

}

  

上面的代码, 说明了模板函数的用法。下面再给出调用的代码, 我们看看如何使用这个函数模板:

int main(int argc, char* argv[])

{

    int nNum1 = 50;

    int nNum2 = 30;

    double dfNum1 = 2.1;

    double dfNum2 = 3.0;

    char *pszFirst = "Hello ";

    char *pszSec = "world!";

    swap <int> (nNum1, nNum2); // 将swap函数模板实例化为 int类型的模板函数再调用

    printf("nNum1 = %d, nNum2 = %d\r\n", nNum1, nNum2);

    swap<double> (dfNum1, dfNum2);

    printf("dfNum1 = %f, pszSec = %f\r\n", dfNum1, dfNum2);

    swap<char *> (pszFirst, pszSec);

    printf("pszFirst = %s, pszSec = %s\r\n", pszFirst, pszSec);

return 0;

}

具体的执行结果如下:

我相信,如果你是第一次见到模板的代码,那你一定也会像我一样好奇,这个功能是怎么实现的,它是怎么做到让一段代码来兼容各种类型的呢?

当我要反汇编该EXE得时候,无意间查看了下编程生成的map文件,让我看到了如下的内容:


  Address                           


Publics by Value


Rva+Base


Lib:Object


0001:00000140


?swap@@YAXAAH0@Z


00401140 f i


Exp01.obj


0001:00000190


?swap@@YAXAAN0@Z


00401190 f i


Exp01.obj


0001:000001f0


?swap@@YAXAAPAD0@Z


004011f0 f i


Exp01.obj

由此可见, 我们编写的void swap(T& a, T& b), 只是一个“函数模板”, 要使用它需要先将它实例化为一个“模板函数”(如:swap <int>)。编译器在编译此程序的时候,为每个调用此模板的代码生成了一个函数。而且在后期的使用过程中,更加让我认识到:

a、 函数模板 并不是函数,它仅在编译时根据调用此模板函数时传递的实参类型来生成具体的函数体!若 函数模板没有被调用责不生成任何代码也不对模板代码作任何语法检查。

b、 在模板类型参数中, 实参与形参要求严格匹配,它不会进行任何的类型转换。

c、 对于函数模板,我们在调用时不一定必须先进行将它实例化为一个函数也是可以的,编译器会自动的识别模板的类型。(换句话说:Exp01中的代码可以直接使用swap, 而不需要<>)

2、  函数模板的重载

当编写的一个模板无法满足所有需要的情况时,就需要对模板进行重载(或叫 特例化),例如:我们编写了一个较大值的模板Max:

template <typename T>

T const& Max(T const& a, T const& b)

{

    return a < b ? b : a;

}

A、 当我们需要传入两个指针类型的实参时,该模板就失效了,需要重载该模板:

template <typename T>

T const& Max(T* const& a, T* const& b)

{

    return *a < *b ? *b : *a;

}

B、 倘若我们再需要比较两个字符串大小时,上面两个模板都失效了。因为char* 并没有提供 operator < 运行,我们只能通过调用strcmp库函数自己实现一个Max模板的特例(见Exp02):

const char* Max(const char*& a, const char*& b)

{

    return strcmp(a, b) < 0 ? b : a;

}

说明:

C++模板机制规定,如果一个调用,即匹配普通函数,又能匹配模板函数的话,则优先匹配普通函数。因此,当我们模板特例化的时候,会先匹配特例化的函数。

二、 类模板

1、  基本概念

类模板一般应用于容器类中,使得容器能够处理各种类型的对象,如(详见Exp03):

struct Node

{

   Node( int nData = 0 )

   {

      m_nData = nData;

      m_pPrev = m_pNext = NULL;

   }

   int   m_nData; // 数据元素

   Node* m_pPrev; // 指向上一个元素的指针

   Node* m_pNext; // 指向下一个元素的指针

};

class CDList

{

private:

  int  m_nCount;

  Node* m_pHead;

  Node* m_pTail;

  int    m_nMessage;

public:

CDList();

virtual ~CDList();

public:

int GetLen() const

{

m_nCount;

}

Node* GetHead() const

{

return m_pHead;

}

Node* GetTail() const

{

return m_pTail;

}

public:

bool Change(int nIndex1,int nIndex2);

void Release();

//增加

Node* AddTail( int nData );

Node* AddHead( int nData );

Node* operator[](int nIndex);

//删除

bool DeleteNode( int nIndex );

void PrintAll();

//查找

Node* FindNode( int nIndex );

};

对于这样的链表,其节点的元素只能存放整型数据。如果要想让此双向链表能够存放任何一种类型的元素,那此时我们需要的问题与函数模板就一样了,将此类修改成类模板,现在先不管类模板的写法,让我们按照函数模板的方法将类修改一下:

template <typename T>

struct Node

{

   Node( T Data )

   {

      m_Data  = Data;

      m_pPrev = m_pNext = NULL;

   }

   T m_Data;  // 通用类型的数据元素

   Node<T>* m_pPrev; // 指向上一个元素的指针

   Node<T>* m_pNext; // 指向下一个元素的指针

};

这样,我们每个节点都可以使用通用的类型了,当然,对节点的处理也一样得处理。按照这个样子将类型参数化(为节省篇幅,具体的实现部分请参考Exp04):

template <typename T>

class CDList

{

private:

int     m_nMessage; // 消息号

int     m_nCount; // 链表中 元素的数量

Node<T>* m_pHead; // 链表头指针

Node<T>* m_pTail; // 链表尾指针

public:

CDList();

virtual ~CDList();

public:

int GetLen() const

{

m_nCount;

}

Node<T>* GetHead() const

{

return m_pHead;

}

Node<T>* GetTail() const

{

return m_pTail;

}

public:

bool Change(int nIndex1,int nIndex2);

void Release();

//增加

Node<T>* AddTail( T Data );

Node<T>* AddHead( T Data );

Node<T>* operator[](int nIndex);

//删除

bool DeleteNode( int nIndex );

void PrintAll();

//查找

Node<T>* FindNode( int nIndex );

};

这样就修改好了,很简单吧,貌似类模板没有什么太多的新语法规范。完整的模板代码,大家可以参考Exp04,下面我们总结一下类模板的一些语法小细节。

2、  类模板的定义

通过上面的一番修改,我相信你一定对类模板有了一定的了解,下面我们大致的总结一下类模板的定义格式:

Template <typename T>

Class 类名

{

// code,可以使用模板参数T来指定通用的数据类型。

}

正如上面的Exp04中一样,我们的模板写好了,但是它不能直接使用,就像函数模板一样,我们需要先将模板实例化成一个模板类才可以使用。在函数模板中,编译器会针对我们传递的实参类型等信息自动的给我们实例化函数模板为模板函数,但是类模板就没有这么智能了,必须手工实例化:

int main(int argc, char* argv[])

{

CDList<int> MyList; // 将CDList实例化为一个int类型,也就是说链表中数据元素为整型

//(20) (80) 100 200 50 60

MyList.AddTail(20);

MyList.AddTail(80);

MyList.AddTail(100);

MyList.AddTail(200);

MyList.AddTail(50);

MyList.AddTail(60);

MyList.PrintAll();

MyList.Change(0,1);

MyList.PrintAll();

return 0;

}

程序执行结果:

总结:

a、 类模板 同样也不是类,它仅在编译时根据实例化本模板时传递的实参来生成具体的类代码!若 类模板没有被实例化也没有被调用,那编译器不会为本模板生成任何代码也不对模板代码作任何语法检查。

3、  类模板的特化

类模板的特化又被叫做类模板的定做,首先让我们来了解下什么叫作定做。

通过上面几个小节的学习,我相信,大家都知道模板不能直接被使用:必须先给模板传递一个实参,将它实例化为一个模板类,然后才可以用它来定义具体的对象。这样就隐含了一个问题:

我们通过给模板传递一个实参来实例化的模板类中的代码都是在模板中定义好的,如果我们不能用与定义好的模板代码来生成模板类,这时就需要使用模板的特化,也就是“定做”。

比如,我们刚才写好的双向链表模板中,对于某一个类(比如CStudent)来说,不允许添加重复的节点,但是对于像普通的int,double等数据类型以及其它一些类时,又不需要有这类的限制。这时我们就需要将此双向链表模板针对这个不允许有重复节点的类(如:CStudent)进行特化,大致代码如下:

template <>

class CDList<CStudent>

{

private:

int     m_nMessage; // 消息号

int     m_nCount; // 链表中 元素的数量

Node<CStudent>* m_pHead; // 链表头指针

Node<CStudent>* m_pTail; // 链表尾指针

public:

bool Change(int nIndex1,int nIndex2);

void Release();

//增加

Node<CStudent>* AddTail( CStudent Data );

Node<CStudent>* AddHead( CStudent Data );

Node<CStudent>* operator[](int nIndex);

//删除

bool DeleteNode( int nIndex );

void PrintAll();

//查找

Node<CStudent>* FindNode( int nIndex );

};

Node<CStudent>* CDList<CStudent>::AddTail( CStudent Data )

{

Node<CStudent>* pNewNode = new Node<CStudent>(Data);

if ( m_pTail )

m_pTail->m_pNext = pNewNode;

pNewNode->m_pPrev = m_pTail;

if ( m_pTail == NULL )

m_pHead = pNewNode;

m_pTail = pNewNode;

m_nCount++;

return pNewNode;

}

由此可知,为CStudent类定做的CDList模板类,就是以CDList<CStudent>为类名重写一份CDList<CStudent>实现而抛弃编译器为我们生成的CDList<CStudent>类。

当一个模板拥有多个模板参数时,如果我们只对其部分参数定做则称为“局部定做”,这样定做出来的“物件”仍然是一个模板,因为我们只特化了一部分模板参数….

说明:

刚才,我们为CStudent类定做的CDList模板类,其实我们没有必要将整个CDList<CStudent>类都写一遍,只需要重写Add函数即可,例如:

Template<>

CDList<CStudent>::Add(CStudent& n)

{

……

}

当然,这样的代码,需要写在Cpp文件中,并在.h文件中声明。

三、 模板程序的组织

当然,如果你足够细心,你一定会好奇,为什么我给的例子中,模板的代码都写在头文件中(.h文件),这里我们就讨论模板代码的组织方式。C++支持两种模板代码的组织方式,分别是包含方式(我们使用的就是包含方式)和分离方式。这两种组织方式没有太根本的区别,就是一个将代码全写在头文件中,分离方式是像写类一样声明和定义分别写在头文件(.h文件)和实现文件(cpp文件)中。

下面我们分别讨论下这两种代码组织方式。

1、  包含方式

本专题中,所有的实例代码中的模板代码都是以包含方式组织的。因为好多的编译器(如VC6的cl)并不支持分离方式组织代码,将代码写在头文件也只是方便编译器的预处理工作能方便的将.h文件中的代码根据模板实参的类型生成相应的模板类。

当然,将代码都写在头文件中还有一点点小要求:

A、 如果模板的成员函数写在类外,则需要写成如下样式(见Exp04):

template <typename T> // 每个类外成员函数前都要有这句

Node<T>* CDList<T>::AddTail( T Data )

{

Node<T>* pNewNode = new Node<T>(Data);

if ( m_pTail )

m_pTail->m_pNext = pNewNode;

pNewNode->m_pPrev = m_pTail;

if ( m_pTail == NULL )

m_pHead = pNewNode;

m_pTail = pNewNode;

m_nCount++;

return pNewNode;

}

B、 对于特化的代码则需要在.h文件中声明并在.cpp文件中定义,如果都写在.h文件中编译会报重定义错误。

2、  分离方式

上面已经提到过,所谓的分离方式组织代码,就是将模板的声明和定义分别写在头文件(.h文件)和实现文件(cpp文件)中,需要注意的是,并不是所有的编译器都支持这种写法,目前我只知道GCC支持这种写法。

当然,分离方式组织代码也有个小要求,就是在模板的声明和定义的template关键字前都加上export关键字。比如:

// .h 头文件中

export template <typename T>

class CDList

{

public:

CDList();

virtual ~CDList();

public:

bool Change(int nIndex1,int nIndex2);

void Release();

//增加

Node<T>* AddTail( T Data );

Node<T>* AddHead( T Data );

Node<T>* operator[](int nIndex);

//删除

bool DeleteNode( int nIndex );

void PrintAll();

//查找

Node<T>* FindNode( int nIndex );

};

在实现文件(cpp文件)中。

export template <typename T> // 每个类外成员函数前都要有这句

Node<T>* CDList<T>::AddTail( T Data )

{

Node<T>* pNewNode = new Node<T>(Data);

if ( m_pTail )

m_pTail->m_pNext = pNewNode;

pNewNode->m_pPrev = m_pTail;

if ( m_pTail == NULL )

m_pHead = pNewNode;

m_pTail = pNewNode;

m_nCount++;

return pNewNode;

}

….

四、 学习小结

经过上面各个小节的学习,我相信大家一定像我一样,对模板有了一点认识。大家也一定都知道,模板只是在编译期间编写,所有的代码都只有效与编译期。

因此,模板的重载、特化等多态性也都是在编译期间体现出来的,如果我们对编译生成的可执行文件进行反汇编时,我们不会找到任何与模板有关的代码,因为模板只是编译期间的产物。

关于模板的作用,我相信大家也一定都体会到了,它可以大大的减轻我们的编码负担,提高编程效率。关于模板的用法和技巧还有很多,单单模板特性足可以出一本书的篇幅来描述其特性及用法。

因此本专题也只是带领大家了解模板的基础用法,关于模板的更多更深入知识,请参考 “模板元编程”相关内容。

时间: 2024-10-18 14:12:24

C++模板编程的相关文章

c++模板编程问题-C++模板编程问题,各种报错,但一直找不到错,求大侠们指导,感激不尽

问题描述 C++模板编程问题,各种报错,但一直找不到错,求大侠们指导,感激不尽 在实现一个C++模板类时出现诸多错误,刚入手模板编程,找错找了大半天,未果,纠结中,求助大神们指导,感激不尽.这是一个类声明,定义部分感觉不会出错,大多也类似,只给出了部分 编译错误报告 语法错误 :1. 缺少";"(在"<"的前面)--报错行1 2: 缺少类型说明符 - 假定为 int.注意: C++ 不支持默认 int--报错行1(我传入模板实参为int) template c

c++模板编程问题-关于模板类里的静态函数指针变量

问题描述 关于模板类里的静态函数指针变量 template class RedBlack{ protected: -- static int (*compareKey)(T *k1, T *k2); //可以这样定义么,可以的话如何初始化它? -- }; 解决方案 自己找到答案了,可以定义,初始化在类外: int compareInt(int a, int *b); typedef int (*PfunPintPint)(int, int*); template<> PfunPintPint

模板编程中的技巧

关键字 typename 在C++标准化过程中,引入关键字typename是为了说明:模板内部的标识符可以是一个类型.譬如下面的例子: template<typename T> class MyClass{ typename T::SubType *ptr; ... }; 上面的程序中,第2个typename被用来说明:SubType是定义与类T内部的一种类型.因此,ptr是一个指向T::SubType类型的指针. 如果不使用typename,SubType就会被认为是一个静态成员,那么它应该

c++模板编程问题-怎样用DEV~C++在.cpp文件中插入一个.c文件,用在.cpp的头文件中去

问题描述 怎样用DEV~C++在.cpp文件中插入一个.c文件,用在.cpp的头文件中去 .cpp文件: #include #include using namespace std; int main() { template m; m.CreateList(10); for(int i = 0; i < length; i++) cout << elem[i] << " "; cout << endl; m.Insert(2, 7); for

解读C++编程中类模板的三种特化_C 语言

1.类模板显式特化为了进行特化,首先需要一个通用的版本,称主模板.主模板使用了标准库堆算法.  堆 是一种线性化的树形结构,将一个值压入一个堆中, 实际上等于将该值插入到一个树形结构中;将一个值从堆中取出就等于移除并返回堆中最大值.但在处理字符的指针时会碰钉子.堆将按照指针的值进行组织. 我们可以提供一个显式特化版本解决此问题(例1)如果希望除了一个针对const char*的Heap外,还希望提供一个针对char *的Heap;(例2) //主模板 template <typename T>

祥解Visual C#数据库编程

visual|编程|数据|数据库 关于数据库编程,微软提供了一个统一的数据对象访问模型,在Visual Studio6.0中称为ADO,在.NET中则统一为ADO.NET,掌握ADO.NET就等于掌握了数据库编程的核心. 针对数据库编程始终是程序设计语言的一个重要方面的内容,也是一个难点.数据库编程的内容十分丰富,但最为基本编程的也就是那么几点,譬如:连接数据库.得到需要的数据和针对数据记录的浏览.删除.修改.插入等操作.其中又以后面针对数据记录的数据操作为重点.本文就来着重探讨一下Visual

利用C++模板,代替虚函数实现类的静态多态性

熟悉模板编程的朋友或许听到过这个技巧或者模式:Barton-Nackmann 技巧或者称 奇异 循环模板模式(Curiously Recurring Template Prattern). 其实在 <c++ 编程语 言>这本bible 书里,在模板那章提到过一个很奇妙的类的实现,用的就是这个技术.当时 ,我就被C++模板技术叹为观止.近期在学boost库时偶然碰到了这个技巧,同时在写一个类 时引发了我的思考,这里就利用这个技巧来实现,静态多态函数(我自己发明的叫法,呵呵 ). 我们知道C++的

我们来谈谈面向指针编程的那些事

面向对象编程,面向设计模式编 程(亦即设计模式),面向接口编程,面向模板编程(亦即泛型编程),面向函数编程(亦即函数式编程),面向多核时代的并行编程,面向大数据的机器学习编 程--这么多年,大家要面向的东西已经够多了,然而我看到的现象是,很多编程语言让大家面向 xxx 的同时在竭力回避指针.我可不想面向这么多东西,所以我只好加入指针的黑暗势力.我要不自量力的来写一篇<面向指针编程>作为投名状,借以表示我与软件世 界的光明势力的彻底决裂. 这个世界上,提供指针的编程语言很少,这样的语言有汇编语言

深入解析C++编程中对设计模式中的策略模式的运用_C 语言

策略模式也是一种非常常用的设计模式,而且也不复杂.下面我们就来看看这种模式. 定义:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模式让算法独立于使用它的客户而独立变化. 角色:     抽象策略角色(Strategy): 抽象策略类.     具体策略角色(ConcreteStrategy):封装了继续相关的算法和行为.     环境角色(Context):持有一个策略类的引用,最终给客户端调用. UML图: 例子: #include <iostream>