在学习类模板之前,建议先学习下函数模板的内容。
类是C++的核心,那是否能够将函数模板的思想应用于类呢?
类模板
一些类主要用于存储和组织数据元素。如:数组类,链表类,Stack类,Queue类等等。C++中可以将模板的思想应用于类,使得类可以不关注具体所操作的数据类型,而只关注类所需要实现的功能。
C++中的类模板,提供一种特殊的类以相同的行为处理不同的类型,在类声明前使用template进行标识,<typename T>用于说明类中使用的泛指类型T。
声明的泛指类型 T可用于声明成员变量和成员函数
编译器对类模板的处理方式和函数模板相同
1. 编译器从类模板通过具体类型产生不同的类;
2. 编译器在声明的地方对类模板代码本身进行编译;
3. 编译器在使用的地方对参数替换后的代码进行编译;
类模板的应用
使用具体类型定义对象。注:类模板不支持自动类型推导定义对象。
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T>
class Operator
{
public:
T add(T a, T b);
T minus(T a, T b);
};
template<typename T>
T Operator<T>::add(T a, T b)
{
return a + b;
}
template<typename T>
T Operator<T>::minus(T a, T b)
{
return a - b;
}
int main(int argc, char *argv[])
{
Operator<int> op1;
Operator<double> op2;
cout<<op1.add(5, 4)<<endl;
cout<<op1.minus(4, 5)<<endl;
cout<<op2.add(1.3, 0.01)<<endl;
cout<<op2.minus(0.01, 1.3)<<endl;
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
注:在模板类外部定义成员函数的实现时,需要加上template<typename T>的声明。
类模板的工程应用
由于类模板的编译机制不同,所以不能像普通类一样分开实现后(即cpp文件实现各个成员函数的功能,.h文件中定义),然后在使用时只包含头文件。在工程实践上,一般会把类模板的定义直接放到头文件中。只有被调用的类模板成员函数才会被编译器生成可执行代码!!!
通过类模板实现数组类。
//Array.cpp
#ifndef _ARRAY_DEF_H_
#define _ARRAY_DEF_H_
#include "Array.h"
template<typename T>
Array<T>::Array(int length)
{
if( length < 0 )
{
length = 0;
}
mLength = length;
mSpace = new T[mLength];
}
template<typename T>
Array<T>::Array(const Array& obj)
{
mLength = obj.mLength;
mSpace = new int[mLength];
for(int i=0; i<mLength; i++)
{
mSpace[i] = obj.mSpace[i];
}
}
template<typename T>
int Array<T>::length()
{
return mLength;
}
template<typename T>
Array<T>::~Array()
{
mLength = -1;
delete[] mSpace;
}
template<typename T>
T& Array<T>::operator[](int i)
{
return mSpace[i];
}
template<typename T>
Array<T>& Array<T>::operator= (const Array<T>& obj)
{
delete[] mSpace;
mLength = obj.mLength;
mSpace = new int[mLength];
for(int i=0; i<mLength; i++)
{
mSpace[i] = obj.mSpace[i];
}
return *this;
}
template<typename T>
bool Array<T>::operator== (const Array<T>& obj)
{
bool ret = true;
if( mLength == obj.mLength )
{
for(int i=0; i<mLength; i++)
{
if( mSpace[i] != obj.mSpace[i] )
{
ret = false;
break;
}
}
}
else
{
ret = false;
}
return ret;
}
template<typename T>
bool Array<T>::operator!= (const Array& obj)
{
return !(*this == obj);
}
#endif
//Array.h
#ifndef _ARRAY_H_
#define _ARRAY_H_
template<typename T>
class Array
{
private:
int mLength;
T* mSpace;
public:
Array(int length);
Array(const Array& obj);
int length();
~Array();
T& operator[](int i);
Array& operator= (const Array& obj);
bool operator== (const Array& obj);
bool operator!= (const Array& obj);
};
#endif
#include <cstdlib>
#include <iostream>
#include "Array.h"
using namespace std;
int main(int argc, char *argv[])
{
Array<int> ai(5);
for(int i=0; i<ai.length(); i++)
{
ai[i] = i + 1;
}
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
我们正常的逻辑是这样写代码,头文件中写定义,cpp文件中实现好,然后主程序中只要包含头文件就好。但是如果是类模板这不能这样做,这样会导致编译不能通过。编译过程如下所示:
不难发现,编译器报了编译链接的错误,并且提示未定义。可我们明明在Array.cpp中定义 了,为什么会链接不到呢???这是因为编译器对模板和普通函数/普通类的的处理方式是不同的。
编译器对普通函数和普通类进行编译的时候,会将上述工程中的三个文件进行编译,因此会找到对应的定义,则会链接成功,不会编译报错。
编译器对类模板进行编译的时候,第一次编译器首先对这三个文件进行语法检查,当发现Array.cpp中都是模板的时候,只会进行类型申明、参数是否合法的检查,不会生成代码。当编译main.cpp的ai[i] = i + 1;这条语句的时候,会对Array.cpp的类模板进行进行第二次编译,main.cpp中并没有说明怎么生成ai[i]的代码,即main.cpp和Array.cpp没有任何的关系。因此就编译出错了。为了解决这个问题,做了已下改进。
//Array.hpp
#ifndef _ARRAY_DEF_H_
#define _ARRAY_DEF_H_
#include "Array.h"
template<typename T>
Array<T>::Array(int length)
{
if( length < 0 )
{
length = 0;
}
mLength = length;
mSpace = new T[mLength];
}
template<typename T>
Array<T>::Array(const Array& obj)
{
mLength = obj.mLength;
mSpace = new int[mLength];
for(int i=0; i<mLength; i++)
{
mSpace[i] = obj.mSpace[i];
}
}
template<typename T>
int Array<T>::length()
{
return mLength;
}
template<typename T>
Array<T>::~Array()
{
mLength = -1;
delete[] mSpace;
}
template<typename T>
T& Array<T>::operator[](int i)
{
return mSpace[i];
}
template<typename T>
Array<T>& Array<T>::operator= (const Array<T>& obj)
{
delete[] mSpace;
mLength = obj.mLength;
mSpace = new int[mLength];
for(int i=0; i<mLength; i++)
{
mSpace[i] = obj.mSpace[i];
}
return *this;
}
template<typename T>
bool Array<T>::operator== (const Array<T>& obj)
{
bool ret = true;
if( mLength == obj.mLength )
{
for(int i=0; i<mLength; i++)
{
if( mSpace[i] != obj.mSpace[i] )
{
ret = false;
break;
}
}
}
else
{
ret = false;
}
return ret;
}
template<typename T>
bool Array<T>::operator!= (const Array& obj)
{
return !(*this == obj);
}
#endif
//Array.h
#ifndef _ARRAY_H_
#define _ARRAY_H_
template<typename T>
class Array
{
private:
int mLength;
T* mSpace;
public:
Array(int length);
Array(const Array& obj);
int length();
~Array();
T& operator[](int i);
Array& operator= (const Array& obj);
bool operator== (const Array& obj);
bool operator!= (const Array& obj);
};
#endif
//main.cpp
#include <cstdlib>
#include <iostream>
#include "Array.hpp"
using namespace std;
int main(int argc, char *argv[])
{
Array<int> ai(5);
for(int i=0; i<ai.length(); i++)
{
ai[i] = i + 1;
}
for(int i=0; i<ai.length(); i++)
{
cout<<ai[i]<<endl;
}
Array<double> ad(10);
for(int i=0; i<ad.length(); i++)
{
ad[i] = (i + 1) / 100.0;
}
for(int i=0; i<ad.length(); i++)
{
cout<<ad[i]<<endl;
}
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
类模板的特化
类模板可以被特化,用template<>声明一个类时,表示这是一个特化类。
特化类模板的意义,当类模板在处理某种特定类型有缺陷时,可以通过类模板的特化来克服处理这种特定类型带来的不足。注:编译器优先选择特化类生成对象。
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T>
class Test
{
public:
T test(T v)
{
cout<<"T test(T v)"<<endl;
cout<<"sizeof(T) = "<<sizeof(T)<<endl;
return v;
}
};
template<>
class Test<int>
{
public:
int test(int v)
{
cout<<"int test(int v)"<<endl;
cout<<"sizeof(int) = "<<sizeof(int)<<endl;
return v;
}
};
int main(int argc, char *argv[])
{
Test<int> t1;
cout<<t1.test(1)<<endl;
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
类模板可以定义多个类型参数:
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T1, typename T2>
class Test
{
public:
void add(T1 a, T2 b)
{
cout<<(a + b)<<endl;
}
};
int main(int argc, char *argv[])
{
Test<double, int> t;
t.add(10.0001, 8);
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
类模板可以被局部特化,可以指定类模板的特定实现,并要求某些类型参数仍然必须得模板的用户指定。注:编译器优先选择特化类。
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T1, typename T2>
class Test
{
public:
void add(T1 a, T2 b)
{
cout<<(a + b)<<endl;
}
};
template<typename T>
class Test<T, int>
{
public:
void add(T a, int b)
{
cout<<"add(T a, int b)"<<endl;
cout<<a + b<<endl;
}
};
template<typename T1, typename T2>
class Test<T1*, T2*>
{
public:
void add(T1* a, T2* b)
{
cout<<"add(T1* a, T2* b)"<<endl;
}
};
int main(int argc, char *argv[])
{
int i = 0;
int j = 0;
Test<double, int> t; // <T, int>
Test<long, long> ti; // <T1, T2>
Test<float, int> tt; // <T, int>
Test<int*, int*> tp; // <T*, T*>
t.add(10.0001, 8);
ti.add(2, 3);
tt.add(4, 5);
tp.add(&i, &j);
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
为什么需要特化,而不重新定义新类?
特化和重新定义新类看上去没有本质区别,但是如果定义新类,那么将变成一个类模板和一个新类,使用的时候需要考虑究竟是用类模板还是用新类。而特化可以统一的方式使用类模板和特化类,编译器自动优先选择特化类。
函数模板和类模板的模板参数可以是普通数值。
#include <cstdlib>
#include <iostream>
using namespace std;
template<typename T, int N>
void func()
{
T array[N] = {0};
for(int i = 0; i < N; i++)
{
array[i] = i + 1;
cout<<array[i]<<" ";
}
cout<<endl;
}
int main(int argc, char *argv[])
{
func<int, 5>();
func<float, 10>();
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
非类型模板参数与特化
#include <cstdlib>
#include <iostream>
using namespace std;
template<int N>
class Sum
{
public:
static const int VALUE = Sum<N - 1>::VALUE + N;
};
template<>
class Sum<1>
{
public:
static const int VALUE = 1;
};
int main(int argc, char *argv[])
{
cout<<Sum<10>::VALUE<<endl;
cout<<Sum<100>::VALUE<<endl;
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
编译运行结果如下:
非类型模板参数的限制
1. 变量不能作为模板参数;
2. 浮点数和类对象不能作为模板参数;
3. 全局指针不能作为模板参数;
注:编译器的推导过程是在编译阶段完成的。因此,编译器的推导必须依赖于特化类,否则推导过程无法结束。
在实际工程中内存操作是bug的重要来源。C++将堆内存交由程序员自由使用,因此:
1. 未及时释放,将产生内存泄漏;
2. 重复释放同一段内存,行为未知;
3. 使用越界,操作了不属于自己的内存;
怎样最大限度的避开上述的使用问题?
内存越界的问题常发生于数组的使用中
解决方案:数组类
工程中,在非特殊情况下,要求开发者使用预先编写的数组类对象代替C语言中的原生数组。
内存泄漏和内存多次释放常发生于指针的使用过程中
解决方案:智能指针
工程中,要求开发者使用预先编写的智能指针类对象代替C语言中的原生指针
什么是智能指针?
工程中的智能指针是一个类模板
1. 通过构造函数接管申请的堆内存;
2. 通过析构函数确保堆内存被及时释放;
3. 通过重载指针运算符 * 和 -> 模拟指针的行为;
4. 通过重载比较运算符 == 和 != 模拟指针的比较;
//main
#include <cstdlib>
#include <iostream>
#include "SmartPointer.hpp"
using namespace std;
class Test
{
public:
int i;
void print()
{
cout<<i<<endl;
}
};
int main(int argc, char *argv[])
{
SmartPointer<int> pi = new int(5);
SmartPointer<Test> pt = new Test();
cout<<*pi<<endl;
*pi = 10;
cout<<*pi<<endl;
pt->i = 20;
pt->print();
cout << "Press the enter key to continue ...";
cin.get();
return EXIT_SUCCESS;
}
//SmartPointer.hcpp
#ifndef _SMARTPOINTER_DEF_H_
#define _SMARTPOINTER_DEF_H_
#include "SmartPointer.h"
template<typename T>
SmartPointer<T>::SmartPointer()
{
m_pointer = NULL;
}
template<typename T>
SmartPointer<T>::SmartPointer(const T* pointer)
{
m_pointer = const_cast<T*>(pointer);
}
template<typename T>
SmartPointer<T>::~SmartPointer()
{
delete m_pointer;
}
template<typename T>
T* SmartPointer<T>::operator->()
{
return m_pointer;
}
template<typename T>
T& SmartPointer<T>::operator*()
{
return *m_pointer;
}
#endif
//SmartPointer.h
#ifndef _SMARTPOINTER_H_
#define _SMARTPOINTER_H_
template<typename T>
class SmartPointer
{
protected:
T* m_pointer;
public:
SmartPointer();
SmartPointer(const T* pointer);
~SmartPointer();
T* operator->();
T& operator*();
};
#endif
编译运行结果如下:
小结:
1. 类模板中可以有一个或多个未指定的泛指类型
2. 可以在需要的特化类模板
3. 特化可以统一的方式使用类模板和新定义的类
4. 特化类总是被编译器优先选择使用
5. 模板的参数可以是普通数值
6. 数组类和智能指针可以最大限度的避免内存相关的bug