STL 模板库第一日:什么是模板

在c++Template中非常多地方都用到了typename与class这两个关键字,并且好像能够替换,是不是这两个关键字全然一样呢?
答:class用于定义类,在模板引入c++后,最初定义模板的方法为:template,这里class关键字表明T是一个类型。后来为了避免class在这两个地方的使用可能给人带来混淆,所以引入了typename这个关键字。它的作用同class一样表明后面的符号为一个类型。这样在定义模板的时候就能够使用以下的方式了: template.在模板定义语法中关键字class与typename的作用大致一样。
/*附:也有略微差别:
typename另外一个作用为:使用嵌套依赖类型(nested depended name),如下所示:
class MyArray 
{ 
public:
typedef int LengthType;
.....
}
template<class T>
void MyMethod( T myarr ) 
{ 
typedef typename T::LengthType LengthType; 
LengthType length = myarr.GetLength; 
}
这个时候typename的作用就是告诉c++编译器,typename后面的字符串为一个类型名称,而不是成员函数或者成员变量,这个时候如果前面没有
typename,编译器没有任何办法知道T::LengthType是一个类型还是一个成员名称(静态数据成员或者静态函数),所以编译不能够通过。 
*/


函数模块与类模板
写在前面:C++提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
 1)C++中提供了两种模板机制:函数模板、类模板
    2)类属 —— 类型参数化,又称参数模板
 使得程序(算法)可以从逻辑功能上抽象,把被处理的对象(数据)类型作为参数传递。
 总结:
  模板把函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属。
  模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
一:为什么要有函数模板 ?
需求:写n个函数,交换char类型、int类型、double类型变量的值。
案例:
#include <iostream>
using namespace std;
/*
void myswap(int &a, int &b)
{
 int t = a;
 a = b;
 b = t;
}
void myswap(char &a, char &b)
{
 char t = a;
 a = b;
 b = t;
}
*/
//这时就有一种需求:可不可以将参数的类型作为一个参数传入
//template 关键字告诉C++编译器 我要开始泛型了.你不要随便报错  
//数据类型T 参数化数据类型
template <typename T>
void myswap(T &a, T &b)
{
 T t;
 t = a;
 a = b;
 b = t;
}
void main()
{
 //char a = 'c';
 
 int  x = 1;
 int  y = 2;
 myswap(x, y); //自动数据类型 推导的方式 
 float a = 2.0;
 float b = 3.0;
 myswap(a, b); //自动数据类型 推导的方式 
 myswap<float>(a, b); //显示类型调用 
 cout<<"hello..."<<endl;
 system("pause");
 return ;
}


这样就可以实现编译时多态,在编译期间将参数的类型固定,做到一个函数分别实现多种不同参数的不同功能。
二:函数模板的语法规则
函数模板定义形式
template    < 类型形式参数表 >   
类型形式参数的形式为:
   typename T1 ,  typename T2 , …… , typename Tn
或 class T1 ,  class T2 , …… , class Tn 
注意点:
1.函数模板是以函数模板申明与函数定义组成的
2.模板中说明的类属一定至少要在函数中出现一次(如果一次都不出现的话,也不会出问题,但是这样模板申明的意义就消失了)
3.函数参数表中可以使用一般类型作为参数,也可以使用类属类型参数作为函数的参数。
三:函数模板做函数参数
直接上例子:
#include <iostream>
using namespace std;
template<typename T, typename T2>
void sortArray(T *a, T2 num)
{
 T tmp ;
 int i, j ;
 for (i=0; i<num; i++)
 {
  for (j=i+1; j<num; j++)
  {
   if (a[i] < a[j])
   {
    tmp = a[i];
    a[i] = a[j];
    a[j] = tmp;
   }
  }
 }
}
template<class T>
void pirntArray(T *a, int num)
{
 int i = 0;
 for (i=0; i<num; i++)
 {
  cout<<a[i]<<" ";
 }
}
void main()
{
 int num = 0;
 char a[] = "ddadeeettttt";
 num = strlen(a);
 printf("排序之前\n");
 pirntArray<char>(a, num);
 sortArray<char, int>(a, num); //显示类型调用 模板函数 <>
 printf("排序之后\n");
 pirntArray<char>(a, num);
 cout<<"hello..."<<endl;
 system("pause");
 return ;
}


四:(重点)函数模板遇到函数重载
首先搞明白几个点:
1.函数模板和普通函数区别结论:
函数模板不允许自动类型转化
普通函数能够进行自动类型转换
2.函数模板和普通函数在一起,调用规则:
 1 函数模板可以像普通函数一样被重载
 2 C++编译器优先考虑普通函数(重点)
 3 如果函数模板可以产生一个更好的匹配,那么选择模板
 4 可以通过空模板实参列表的语法限定编译器只通过模板匹配
例子1:
#include <iostream>
using namespace std;
//定义一个函数模板
template <typename T>
void myswap(T &a, T &b)
{
 T t;
 t = a;
 a = b;
 b = t;
 cout << "myswap 模板函数do" << endl;
}
//再定义一个普通函数
void myswap(char &a, int &b)
{
 int t;
 t = a;
 a = b;
 b = t;
 cout << "myswap 普通函数do" << endl;
}
//首先需要注意的是:函数模板和普通函数再一起会不会出现出错的情况(就是说会不会重载)
void main()
{
 char cData = 'a';
 int  iData = 2;
 //myswap<int>(cData, iData);  //结论 函数模板不提供隐式的数据类型转换  必须是严格的匹配,如果出现类型不匹配,编译器会再编译期间直接报错
 //myswap(cData, iData);//这里调用的是普通函数
 
 //myswap(iData, cData);//但是再普通函数中就可以实现类型的隐式转换
 cout << "hello..." << endl;
 system("pause");
 return;
}


例子2:
#include "iostream"
using namespace std;

int Max(int a, int b)
{
 cout << "int Max(int a, int b)" << endl;
 return a > b ? a : b;
}
template<typename T>
T Max(T a, T b)
{
 cout << "T Max(T a, T b)" << endl;
 return a > b ? a : b;
}
template<typename T>
T Max(T a, T b, T c)
{
 cout << "T Max(T a, T b, T c)" << endl;
 return Max(Max(a, b), c);
}

void main()
{
 int a = 1;
 int b = 2;
 cout << Max(a, b) << endl; //当函数模板和普通函数都符合调用时,优先选择普通函数
 cout << Max<>(a, b) << endl; //若显示使用函数模板,则使用<> 类型列表
 cout << Max(3.0, 4.0) << endl; //如果 函数模板产生更好的匹配 使用函数模板
 cout << Max(5.0, 6.0, 7.0) << endl; //重载
 cout << Max('a', 100) << endl;  //调用普通函数 可以隐式类型转换 
 system("pause");
 return;
}


五:类模板与类模板的派生
这次先上代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
//类模板语法
template<typename T>
class Teacher
{
public:
 Teacher();
 ~Teacher();
private:
 int age;
 T node;
};
template<typename T>
Teacher<T>::Teacher()//在加入模板后在类外实现函数的时候要这么做。
{
 age = 0;
}
template<typename T>
Teacher<T>::~Teacher()
{
 cout << "Teacher is deleted!" << endl;
}
template<typename T>
class Teacher1:public Teacher<T>
{
public:
protected:
private:
};
int main()
{
 Teacher1<int>t1;
 
 system("pause");
 return 0;
}


//从简单的代码中可以看出,在派生的时候,就是需要在每一个函数的前面添加一个模板的申明,其实可以如果从内存的角度看这段代码还是比较清楚的,不管是进行申明步骤,只要指定清楚类型,就能知道内存的大小,只有知道大小,编译器才可以知道自己需要做什么事情,程序才能顺利的进行。
//所以在派生的时候需要指定类型,不管是指定一个显示的int,还是继续模板下去都可以,让编译器知道自己要分配的大小是多少。

六:(重点)类模板遇到友元
在类中经常会使用一些运算符重载,而且如果要重载的运算符为<< >>时,就必须使用友元方式去做(/*重载的要求:
1、内置类型的操作符不能被重载
2、不能为内置类型定义其他的操作符
3、操作符重载不能改变操作符的优先级
4、操作数个数不能改变
重载运算符时,函数声明在类内和类外是有区别的,比方说+ - *  / 等需要2个操作数的运算符,
当声明在类的外部时,则参数列表为2个参数,第一个参数为运算符左边的操作数,而第二个参数为
操作符右边的操作数:如下
classType operator+(classType& left, classType& right);
而当函数声明在类的内部时,即为类的成员函数时,
classType operator+(classType& right );
而第一个操作数就是调用该操作符的对象的引用,第二个操作数是传进来的参数,
所以,如果要重载<<运算符,一般写法是这样的
ostream& operator<<(ostream& os, const classType& obj);
则第一个参数是该运算符的第一个操作数,然而,却不是类对象,
所以当该类运算符重载时写在类的内部时,又为了访问类内除public外的其它变量或函数,
则应当声明为友元函数:
friend ostream& operator<<(ostream& os, const classType& obj);
所以,为什么有些运算符的重载要声明为友元函数,是类似的。*/)
但是在编写代码中会经常出现这样的问题:
#include <iostream>
using namespace std;
template <typename T>
class Complex ; //类的前置声明
template <typename T>
Complex<T> MySub (Complex<T> &c1, Complex<T> &c2);

template <typename T>
class Complex
{
 friend Complex<T> MySub<T> (Complex<T> &c1, Complex<T> &c2);
 friend ostream & operator<< <T> (ostream &out, Complex &c3);
public:
 Complex(T a, T b);
 void printCom();
 Complex operator+ (Complex &c2); 
 
private:
 T a;
 T b;
};
//构造函数的实现 写在了类的外部
template <typename T>
Complex<T>::Complex(T a, T b)
{
 this->a = a;
 this->b = b;
}
template <typename T>
void Complex<T>::printCom()
{
 cout << "a:" << a << " b: " << b << endl;
}
//本质是 : 模板是两次 编译生成的 第一次生成的函数头 和第二次生成的函数头 不一样
//成员函数 实现 +运算符重载
template <typename T>
Complex<T>  Complex<T>::operator+ (Complex<T> &c2)
{
 Complex tmp(a+c2.a, b+c2.b);
 return tmp;
}
//友元函数 实现 << 运算符重载
/*
1>dm08_复数类_所有函数都写在类的外部(在一个cpp中).obj : error LNK2019: 无法解析的外部符号 "
class std::basic_ostream<char,struct std::char_traits<char> > &
 __cdecl operator<<
 (class std::basic_ostream<char,struct std::char_traits<char> > &,class Complex<int> &)" 
 (??6@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@std@@AAV01@AAV?$Complex@H@@@Z),
 该符号在函数 _main 中被引用
1>E:\01-work\21_CPlusPlus课程\day09\泛型编程课堂操练\Debug\泛型编程课堂操练.exe : fatal error LNK1120: 1 个无法解析的外部命令
*/
template <typename T>
ostream & operator<<(ostream &out, Complex<T> &c3)
{
 out <<  c3.a << " + " << c3.b <<  "i" << endl;
 return out;
}
//
//滥用 友元函数
template <typename T>
Complex<T> MySub(Complex<T> &c1, Complex<T> &c2)
{
 Complex<T> tmp(c1.a - c2.a, c1.b - c2.b);
 return tmp;
}

void main()
{
 //需要把模板类 进行具体化以后  才能定义对象  C++编译器要分配内存
 Complex<int> c1(1, 2);
 Complex<int> c2(3, 4);
 Complex<int> c3 = c1 + c2;
 //c3.printCom();
 cout << c3 << endl;
 //滥用友元函数
 {
  Complex<int> c4 = MySub<int>(c1, c2);
  cout << c4 << endl;
 }
 
 cout<<"hello..."<<endl;
 system("pause");
 return ;
}

结论:友元函数只用来进行 左移 友移操作符重载。
归纳以上的介绍,可以这样声明和使用类模板:
1) 先写出一个实际的类。由于其语义明确,含义清楚,一般不会出错。
2) 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)。
3) 在类声明前面加入一行,格式为:
    template <class 虚拟类型参数>
如:
    template <class numtype> //注意本行末尾无分号
    class Compare
    {…}; //类体
4) 用类模板定义对象时用以下形式:
    类模板名<实际类型名> 对象名;
    类模板名<实际类型名> 对象名(实参表列);
如:
    Compare<int> cmp;
    Compare<int> cmp(3,7);
5) 如果在类模板外定义成员函数,应写成类模板形式:
   template <class 虚拟类型参数>
   函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {…}
关于类模板的几点说明:
1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如:
    template <class T1,class T2>
    class someclass
    {…};
在定义对象时分别代入实际的类型名,如:
    someclass<int,double> obj;
2) 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
3) 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

七:如何继承别人的类模板
前端时间,有一个朋友帮我写了一个关于链表的类模板,但是痛苦的是,我不管怎么调用他的.h都不能很好的让程序跑起来,后来我才知道,TMD,C++的这帮人让我在前面写“XX.cpp”.......简直是qj我这么多年的变成习惯啊,日!!!!!
好了,被qj的不止我一个,所以有一帮好心人出了一个新的东西,叫hpp,并在boost库中大用特用~~~~~
/*hpp,其实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该hpp文件即可,无需再 将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用 project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll,因此非常适合用来编写公用的开源库。
1、是Header Plus Plus 的简写。
2、与*.h类似,hpp是C++程序头文件 。
3、是VCL 专用的头文件,已预编译。
4、是一般模板类的头文件。
5、一般来说,*.h里面只有声明,没有实现,而*.hpp里声明实现都有,后者可以减 少.cpp的数量。*/
下面是由此而来的数不清的问题。。。。
/*
*.hpp要注意的问题有:
a)不可包含全局对象和全局函数
由于hpp本质上是作为.h被调用者include,所以当hpp文件中存在全局对象或者全局函数,而该hpp被多个
调用者include时,将在链接时导致符号重定义错误。要避免这种情况,需要去除全局对象,将全局函数封
装为类的静态方法。
b)类之间不可循环调用
在.h和.cpp的场景中,当两个类或者多个类之间有循环调用关系时,只要预先在头文件做被调用类的声明
即可,如下:
class B;
class A{
public:
void someMethod(B b);
};
class B{
public:
void someMethod(A a);
};
在hpp场景中,由于定义与实现都已经存在于一个文件,调用者必需明确知道被调用者的所有定义,而不能等到cpp
中去编译。因此hpp中必须整理类之间调用关系,不可产生循环调用。同理,对于当两个类A和B分别定义在各自的
hpp文件中,形如以下的循环调用也将导致编译错误:
//a.hpp
#include "b.hpp"
class A{
public:
void someMethod(B b);
};
//b.hpp
#include "a.hpp"
class B{
public:
void someMethod(A a);
}
c)不可使用静态成员
静态成员的使用限制在于如果类含有静态成员,则在hpp中必需加入静态成员初始化代码,当该hpp被多个文档include时,将产生符号重定义错误。唯 一的例外是const static整型成员,因为在vs2003中,该类型允许在定义时初始化,如:
class A{
public:
const static int intValue = 123;
};
由于静态成员的使用是很常见的场景,无法强制清除,因此可以考虑以下几种方式(以下示例均为同一类中方法)
一、类中仅有一个静态成员时,且仅有一个调用者时,可以通过局域静态变量模拟
//方法模拟获取静态成员
someType getMember()
{
static someType value(xxx);//作用域内静态变量
return value;
}
二、.类中有多个方法需要调用静态成员,而且可能存在多个静态成员时,可以将每个静态成员封装一个模拟方法,供其他方法调用。
someType getMemberA()
{
static someType value(xxx);//作用域内静态变量
return value;
}
someType getMemberB()
{
static someType value(xxx);//作用域内静态变量
return value;
}
void accessMemberA()
{
someType member = getMemberA();//获取静态成员
};
//获取两个静态成员
void accessStaticMember()
{
someType a = getMemberA();//获取静态成员
someType b = getMemberB();
};


三、第二种方法对于大部分情况是通用的,但是当所需的静态成员过多时,编写封装方法的工作量将非常
巨大,在此种情况下,建议使用Singleton模式,将被调用类定义成普通类,然后使用Singleton将其变为
全局唯一的对象进行调用。
如原h+cpp下的定义如下:
class A{
public:
type getMember(){
return member;
}
static type member;//静态成员
}
采用singleton方式,实现代码可能如下(singleton实现请自行查阅相关文档)
//实际实现类
class Aprovider{
public:
type getMember(){
return member;
}
type member;//变为普通成员
}
//提供给调用者的接口类
class A{
public:
type getMember(){
return Singleton<AProvider>::getInstance()->getMember();
}
}
*/
“注:以上代码来自百度百科”

八:static在类模板中使用方法:
/* 从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员
  和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化
  每个模板类有自己的类模板的static数据成员副本*/
九:应用场景:
/* 模板是C++类型参数化的多态工具。C++提供函数模板和类模板。
  模板定义以模板说明开始。类属参数必须在模板定义中至少出现一次。
  同一个类属参数可以用于多个模板。
  类属参数可用于函数的参数类型、返回类型和声明函数中的变量。
  模板由编译器根据实际数据类型实例化,生成可执行代码。实例化的函数。
    模板称为模板函数;实例化的类模板称为模板类。
  函数模板可以用多种方式重载。
  类模板可以在类层次中使用 。*/
附:STL中默认都是所有容器提供的都是值(value)语意,而非引用(reference)语意。容器执行插入元素的操作时,内部实施拷贝动作。所以STL容器内存储的元素必须能够被拷贝(必须提供拷贝构造函数)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ym影子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值