c++学习笔记(15.类模板)

本节知识点:

1.类模板是函数模板的升级:
   a. c++中可以将模板的思想应用于类,使得类可以不关注具体所操作的数据类型,而只关注类所需要实现的功能。
   b. 函数模板是使一个函数体适合各种类型的数据,类模板是使一个类适合各种类型的数据(包括成员函数和成员变量,当使得成员函数满足各种类型的数据的时候,其实就是在使用函数模板,所以说类模板是函数模板的升级) ,函数模板是类模板中的一部分。
   c.类模板常用于一些存储和组织数据元素的类中,如:数组类、链表类、stack类、queue类等
2.类模板的语法规则:
   a. 类模板的作用就是提供一种特殊的类以相同的行为处理不同的类型
   b.类模板的使用跟函数模板的使用一样,在定义类的前面声明template <typename T>,说明类中使用泛指类型T
   c.类模板的应用,当使用类模板定义对象时,要指明具体类型,如:test<int> t1 即t1就是满足int类型的对象
示例代码,如下:
#include <iostream>

using namespace std;

template <typename T>
class test
{
public:
	int a;
	T a1;
	T add(T b, T c)
	{
		return b + c;
	}
	T min(T b, T c);
};

template <typename T>
T test<T>::min(T b, T c)
{
	return b - c;
}

int main()
{
	test<int> t1;
	cout << t1.add(4, 6) << endl; 
	cout << t1.min(6, 4) << endl;
	return 0;
} 

注意:声明的泛指类型T可用于声明成员变量和成员函数
3.类模板的深入理解:
   a. 对于函数模板,编译器进行了两次编译,第一次是直接对函数模板进行编译,看函数模板是否存在语法错误。第二次是在指明类型,让函数模板创建出具体类型的函数,调用具体类型的函数的时候,对具体函数进行编译。最终程序中只存在具体类型的函数,不存在函数模板。
   b. 类模板跟函数模板的处理方式是一样的,也是进行两次编译,第一次是对类模板进行编译,判断类模板是否存在语法错误。第二次是在类模板创建出具体类型的对象之后  test<int> t1 ,当t1对象调用类模板中的成员函数的时候,调用那个成员函数就会产生那个类模板成员函数的具体类型的函数(这里是int类型的函数),没调用的类模板成员函数则不会产生可执行代码,调用的类模板成员函数才会产生具体类型的函数(具体类型是根据调用者的对象决定的),才会产生可执行代码。 
4.函数模板与类模板的工程应用:
   a. 一般在工程中,函数的实现和函数的调用是在不同的文件中的,类的实现和类的使用也是在不同的文件中的。但是对于函数模板和类模板,模板的实现和使用必须在同一个文件中。因为模板是要在使用的时候,进行二次编译的,如果不在同一个文件中,当使用模板的时候编译器进行二次编译就找不到模板了。会产生链接错误!所以函数模板和类模板不能像普通函数和普通类一样分开实现后,在使用时只包括头文件!必须要把使用和实现放在一起。在工程实践中,一般会把类模板的定义直接放在头文件中,此时使用的头文件不再是 .h文件了,是 .hpp文件。hpp文件,就是告诉程序员,其中存放的是模板的实现,其实在编译的时候,hpp文件中的内容会被包含到#include "test.hpp"指定的cpp文件中去,这里面之所以表面上分开存放在hpp文件和cpp文件中,是为了开发的时候,使得代码工程清晰明了,有层次感!
注意:第一,Operator<T> :: add 中的 Operator<T> 表示的是Operator是一个类模板
            第二,在类模板中的成员函数中,凡是有类名(即Operator)表示类的时候(即不是表示构造与析构函数名的时候),都要在类名后面加上<T> 如Operator <T>,来表示是一个类模板,不管是函数的返回值类型啊,表示所属类啊,还是函数的参数类型啊,都要注意这一点!
示例代码(报链接错误的):
main.cpp:
#include <iostream>
#include "test.h"

using namespace std;

int main()
{
	test<int> t1;
	cout << t1.add(4,6) << endl;
	return 0;
}
test.cpp:
#include "test.h"

template <typename T>
T test<T> :: add(T a, T b)
{
	return a + b;
}

test.h:
#ifndef TEST_H_
#define TEST_H_
template <typename T>
class test
{
public:
	int a;
	T add(T a, T b);
};

#endif

示例代码(正确的):
main.cpp:
#include <iostream>
#include "test.hpp"

using namespace std;

int main()
{
	test<int> t1;
	cout << t1.add(4,6) << endl;
	return 0;
}
test.hpp:
#include "test.h"

template <typename T>
T test<T> :: add(T a, T b)
{
	return a + b;
}

test.h:
#ifndef TEST_H_
#define TEST_H_
template <typename T>
class test
{
public:
	int a;
	T add(T a, T b);
};

#endif
5.类模板的特化:
   a.类模板是可以被特化的,用template<>声明一个类时,表示这是一个特化类!
   b. 类模板的特化与函数模板的特化是一样的,这种特化都是不需要进行二次编译的,所以说是编译器事先编译好的,当然这种特化不管是调用与否,都会被编译成为可执行代码的,这一点是与模板的本质区别!
   c. 特化类模板的意义:当类模板在处理某种特定类型有缺陷时,可以通过类模板的特化来克服处理这种特定类型带来的不足。
   d. 注意:编译器会优先选择特化类生成对象!!
示例代码:
#include <iostream>

using namespace std;

template <typename T>  //test 类模板 
class test
{
public:
	T a;
	T add(T b1, T b2)
	{
		return b1 + b2;
	}
};

template <>
class test<int>  //test类模板 的 int特化 
{
public:
	int add()
	{
		cout << "hello int" << endl;
		return 0;
	}	
}; 
int main()
{
	test<int> t;
	cout << t.add() << endl;
	test<double> t1;
	cout << t1.add(2.1, 3.0) << endl;
	return 0;
}
注意:函数模板的特化与类模板的特化有一个最大的区别,就是函数模板与其特化的函数原型必须完全相同(即函数的名称,参数个数必须相同),而类模板与其特化的类仅仅类名相同就行了,类里面的内容可以完全不一样。
6.类模板的局部特化:
   a.类模板可以定义多个类型参数,template <typename T1, typename T2>
   b. 类模板可以被局部特化,即有些参数指明具体类型,有些参数依然保留模板特性。所以可想而知,局部特化的类模板,依然是模板,依然需要进行二次编译,所以如果没有调用该类模板中的成员函数,函数是不会被编译成为可执行代码的。这点是与类模板的全部特化是不一样的!切记,函数模板是不能进行局部特化的,只能进行全特化!!!
示例代码:
#include <iostream>

using namespace std;

template <typename T1, typename T2>
class test
{
public:
	void fun()
	{
		cout << "T1 T2 " << endl;
	}
};

template <typename T>
class test<T, T>
{
public:
	void fun()
	{
		cout << "T T " << endl;
	}
};

template <typename T>
class test<T, int>
{
public:
	void fun()
	{
		cout << "T int" << endl;
	}
};

template <typename T1, typename T2>
class test<T1*, T2*>
{
public:
	void fun()
	{
		cout << "T1* T2*" << endl;
	}
};
int main()
{
	test<double, double> t1;
	t1.fun();
	test<double, int> t2;
	t2.fun();
	test<char*, int*> t3;
	t3.fun();
	test<int, double> t4;
	t4.fun();
	return 0;
}
注意:这种类模板的局部特化是很容易产生二义性的,比如test <T , int>和 test <int , int>就会产生二义性,使得编译器不知道怎么办。而且这些二义性是没有优先级的,所以一定要小心使用,在程序设计上避免二义性,否则就会报错!
7.为什么需要特化,而不重新定义新类:
   a.特化和重新定义新类看上去没有本质区别,但是如果定义新类,那么将变成一个类模板和一个新类,使用的时候需要考虑究竟是用类模板还是用新类
   b.而特化可以统一的方式使用类模板和特化类,编译器自动优先选择特化类
8.非类型模板参数:
   a. 函数模板和类模板的模板参数可以是普通数值,如 template<typename T, int N>
示例代码:
#include <iostream>

using namespace std;

template <typename T, int N>
void fun()
{
	T array[N] = {0};
	for(int i = 0; i<N ; i++)
	{
		array[i] = i;
		cout << array[i] << endl;
	}
}

template <typename T, int N>
class test
{
public:
	T a;
	void fun()
	{
		T array[N] = {0};
		for(int i = 0; i<N ; i++)
		{
			array[i] = i;
			cout <<  "test" << array[i] << endl;
		}
	}	
};

int main()
{
	fun<double, 4>(); //函数模板  的非类型模板参数 
	test<int, 5> t1;  //类模板    的非类型模板参数 
	t1.fun();
	return 0;
}  
    b. c++编译器会对非类型模板参数进行如下的限制:
       第一,变量不能作为非类型模板参数
       第二,浮点型和类对象不能作为非类型模板参数
       第三,全局指针不能作为非类型模板参数
       总之,非类型模板参数只允许int型常量,包括#define定义的常量,const定义的常量,enum枚举常量,普通常量!
    c. 一个经典的面试问题:如何才能使程序最快的计算出1加到n?
       第一个想法:写一个函数通过高斯公式计算1加到n,时间复杂度为O(n)
       第二个想法:使用宏,省去了函数调用过程中的时间
       第三个想法:使用类模板,非类型模板参数与特化(利用递归的思想),省去高斯公式在代码执行期间计算的时间(使其在编译期间完成计算),代码如下:
#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()
{
	cout << (sum <100> :: value) <<endl; 
	return 0;
} 
    我们分析下上面的代码:
    第一,上面的代码使用递归思想(这里的递归是不断的通过sum类模板创建具体N数值的sum类),实际上是根据sum类模板,创建出来了N个sum类(当然不是都叫sum),每一个类中又有一个类的静态成员变量value。
    第二,其实上面的递归过程,是在编译期间,创建出N个sum类,计算出每个sum类中的value值,可见sum<100> :: value 这个过程在程序执行期间,仅仅是从内存中获得一个变量的时间,连计算的时间都没(计算是在编译期间完成的),所以它的时间最短!
    第三,虽然这个方法是用时最短的,但是过于极端,所浪费空间是巨大的!
    第四,编译器的推导过程是在编译阶段完成的。因此,编译器的推导必须依赖于特化类(特化类是递归的出口),否则推导过程无法结束,编译器会报错!
9.类模板在工程中的问题:
   a.在实际工程中内存操作是bug的重要来源
   b.c++将堆内存交给程序员自由使用,因此常出现下面三个bug:
      第一,未及时释放内存,造成内存泄露
      第二,重复释放同一个段内存,行为未知
      第三,内存越界,操作了不属于自己的内存
   c. 解决内存越界的问题,内存越界常发生于数组中,可使用数组类来避免这个问题。工程中,在非特殊情况下,要求开发者使用预先编写的数组类对象代替c语言中的原生数组。数组类的优势在于,在类中有一个length成员变量,根据数组类对象就可以知道数组的具体长度!原生数组是不可以的!但是原生数组的内存是在栈区的,数组类的内存是在堆区的!
   d. 解决内存泄漏和内存多次释放的问题,这个问题常发生于指针的使用过程中,可以使用智能指针来避免这个问题。在工程中,要求开发者使用预先编写的智能指针类对象代替c语言中的原生指针!
   e. 智能指针:

示例代码:
samrtpoint.hpp:
#ifndef _SMARTPOINT_HPP_
#define _SMARTPOINT_HPP_

#include "smartpoint.h"

template <typename T>
smartpoint<T>::smartpoint()
{
	s_point = NULL;
}

template <typename T>
smartpoint<T>::smartpoint(const T* point) //这里的const是防止初始化的值被改变  其实意义不大
{
	s_point = const_cast<T*>(point);
}

template <typename T>
smartpoint<T>::~smartpoint()
{
	delete s_point;
}

template <typename T>
T& smartpoint<T>::operator*()
{
	return *s_point; 
}

template <typename T>
T* smartpoint<T>::operator->()
{
	return s_point; 
}
#endif

smartpoint.h:
#ifndef _SMARTPOINT_H_
#define _SMARTPOINT_H_

template <typename T>
class smartpoint
{
protected:
	T* s_point;
public:
	smartpoint();//为那些不需要进行初始化的对象提供的构造函数  
	smartpoint(const T* point); //为那些需要进行初始化的对象提供的构造函数 
	~smartpoint(); //析构函数  帮助释放内存  防止内存泄漏和重复释放  
	T& operator*(); //两个很奇葩的操作符重载 
	T* operator->();
};

#endif

main.cpp:
#include <iostream>
#include "smartpoint.hpp"

using namespace std;
class test
{
public:
	void fun()
	{
		cout << "hello fun" <<endl;
	}
};
int main()
{
	smartpoint<int> s_pi = new int(100); 
	cout << *s_pi << endl;
	
	smartpoint<test> s_pt = new test;
	s_pt->fun();
	//s_pt.operator->() ->fun();
	
	*s_pi = 500;
	cout << *s_pi << endl; 
	return 0;
} 
注意: 第一,因为智能指针将内存的释放处理交给了析构函数,其实就是交给了编译器,这样就避免了内存忘记释放(内存泄漏)和内存重复释放的问题!
            第二,这个程序里面有两个奇葩的操作符重载,一个是->操作符的重载,另一个是*操作符的重载。对于+加   -减     *乘   这个三个运算符来说,如果操作符重载的函数没有参数,且函数是类的成员函数。使用操作符的时候,应该是  操作符加对象,即*s_pi。对象变成了右操作数,不知道为什么?对于->操作符的重载就更加奇怪了,当->操作符重载的函数没有参数,且函数时类的成员函数。使用操作符的时候,->符号起到了两次作用!即s_pt->fun()与 s_pt.operator->() ->fun()等效!!!原因不知道???



  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值