本篇是关于模板进阶得一些知识点,如果你没有看过我写的模板初阶,可以先看看我模板初阶的博客:模板的一点简单介绍
本篇主要内容有
上面挨着一个个的来。
我们之前用的模板参数,像T、iterator这种的都是某一特定类型。
下面要说的是非类型的模板参数。
下面的这些代码都是在非std命名空间中的,因为下面代码中的array会和std中的array冲突。我这里用的命名空间叫做FangZhang。
比如说,现在我们要实现一个静态的数组。
以我们学C的经验,就是#define。
像下面这样:
如果我们用这个模板类去实例化对象的时候,生成对象的存储数据个数都是相同的。
比如下面这样:
此时a1和a2,虽然存储元素的类型是不同的,但是存储的元素个数都是10个。
但是如果我们想要实现存储不同元素个数的对象时该怎么办?
此时就要用到非类型模板参数。像下面这样:
N代表的就是元素个数,而不是一个特定的类型。
此时我们就可以这样定义对象:
此时我们就可以定义如下对象:
二者都为存储10个元素的数组。
其实就和我们函数的参数很相似。
但是要注意:
上面提到了array这个容器,简单讲两句。
固定大小,就一静态数组,支持迭代器、[ ]、不支持插入(因为数组大小是固定的,不能扩容),没有构造函数,不能初始化。
其实这个容器比较鸡肋,因为我们学C时就已经有了数组这个东西,二者功能基本一样的,而且数组定义的时候还更简便一点。
对于a1而言。
a1的[ ]为函数调用,就是重载的[ ]。其会在[ ]内部检查所访问的下标是否超过了其内部的size,那么如果越界了是一定能检查到的。但是普通的数组就不是了。
对于a2而言。
a2为指针,其检查是抽检。而且只针对越界写,越界读不做检查。
什么意思嘞?看例子:
这就是越界读,就是访问了内部的数据,但是并没有修改。是可以访问到的,编译器不会报错。
但是我们是知道已经越界了的。
…
还有就是抽检,意思就是其只会在有效元素的后面那几个位置是否被修改。如果我们往后多走几步,就检查不到了。
这个就非常bug了,已经远远越界了。
只有在后面仅限的几个位置写才会检查到。比如说这里修改a2[10]就能检查到。
但是array用的也不多,这里就不细讲了。
然后就是模板的特化。
也叫模板的特殊化。就是当模板参数传参时,有一种类型所执行的功能不是我们本来模板函数或模板类中所想要的功能时,就要对这个类型进行特殊化处理。
说起来比较绕,上例子:
比如说我现在用一下以前写的日期类Date。
代码如下(这些代码不用细看,我这里就用一下其中的<重载):
class Date
{
friend ostream& operator<<(ostream& out, const Date& date);
public:
Date(int year = 2023, int month = 5, int day = 7)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& date)
{
cout << "Date(const Date& date)" << endl;
_year = date._year;
_month = date._month;
_day = date._day;
}
void show() const;
void show();
Date& operator=(const Date& date);
Date operator++(int);
Date& operator++();
int GetMonthDay(int year, int month);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
bool operator < (const Date& date);
bool operator <= (const Date& date);
bool operator > (const Date& date);
bool operator >= (const Date& date);
bool operator!=(const Date& date);
bool operator==(const Date& date);
int operator-(const Date& date);
//ostream& operator<<(ostream& out) const;
private:
int _year;
int _month;
int _day;
void Date::show() const
{
cout << "void Date::show() const" << endl;
cout << _year << "/" << _month << "/" << _day << endl;
}
void Date::show()
{
cout << "void Date::show()" << endl;
cout << _year << "/" << _month << "/" << _day << endl;
}
bool Date::operator==(const Date& date)
{
return _year == date._year
&& _month == date._month
&& _day == date._day;
}
int Date::GetMonthDay(int year, int month)
{
int arr[13] = { 0, 31, 28, 31, 30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
return arr[month];
}
Date& Date::operator=(const Date& date)
{
cout << "Date & Date::operator=(const Date & date)" << endl;
if (!(*this == date))
{
_year = date._year;
_month = date._month;
_day = date._day;
}
return *this;
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;//+=也是重载的函数,等会再讲
return tmp;
}
Date& Date::operator++()
{
*this += 1;//+=也是重载的函数,等会再讲
return *this;
}
Date Date::operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
bool Date::operator < (const Date& date)
{
if (_year < date._year)
{
return true;
}
else if (_year == date._year && _month < date._month)
{
return true;
}
else if (_year == date._year && _month == date._month && _day < date. _day)
{
return true;
}
return false;
}
bool Date::operator <= (const Date& date)
{
if (*this < date || *this == date)
return true;
return false;
}
bool Date::operator > (const Date& date)
{
if (!(*this <= date))
return true;
return false;
}
bool Date::operator >= (const Date& date)
{
if (!(*this < date))
return true;
return false;
}
bool Date::operator!=(const Date& date)
{
if (!(*this == date))
return true;
return false;
}
int Date::operator-(const Date& date)
{
int flag = 1;
int n = 0;
Date max = *this;
Date min = date;
if (max < min)
{
max = date;
min = *this;
flag = -1;
}
while (min != max)
{
++min;
++n;
}
return n * flag;
}
};
inline ostream& operator<<(ostream& out,const Date& date)
{
out << date._year << "_" << date._month << "_" << date._day;
return out;
}
然后我们定义几个对象来测试一下:
分别为int、Date、Date*
我这里想用Date*来比较两个Date的大小,但失败了。
因为这里实例化对象的时候实例化出来的是Date*的对象,p1和p2比较的时候是纯指针的比较,并没有调用Date对象的 < 重载。所以就出问题了。
但是如果我们想用Date*来比较两个Date对象呢?
我们可没有 if(T == Date*) 这种玩法,所以此时就要对Date*进行特殊化处理。
像下面这样:
写法比较特殊,这个要留个印象。
函数模板的特化步骤:
上面的是函数模板,下面来类模板的。
就用上一篇博客的仿函数,如果有看不懂的小伙伴可以点击传送门:只看仿函数部分即可
当我们进行同样的比较的时候也会出现上面函数模板中的问题。
想要解决的话也是特化就行。
但是类模板的特化和函数模板的特化有些不一样。
此时再测试:
就好了。
再说一个能够用到特化的地方:
就用我前面讲过的优先级队列,如果不知道优先级队列的,可以点这篇:优先级队列
例子:
我们在优先级队列中存方Date类型,就能得到最大或最小日期。
但是如果我传参是Date*,底层要用到库中的仿函数less,但是库中的仿函数less是没有我们写的那个Date*的特化的。所以此时就有问题了。
类模板特化可分为 全特化和偏特化。
然后用这个来特化。
全特化
偏特化(也叫半特化,但这种说法描述不准确)
这里只要第二个参数为int就走的是这个模板的特化。
根特化不沾边的类型,就走的是原模版。
还可以下面这样特化,指定其为指针或者引用。
测试:
那么这里体现的就是有更匹配的就走更匹配的,没有的话就将就一下。
我们刚刚lessStruct的例子中就可将Date*改为T*。
测试也是能过的。
下面说模板的分离编译。
在我模板初阶的那篇中,已说过了,模板不要分离编译(声明和定义放在不同文件中),出错了很麻烦,这里就细模板分离编译。
我这里用一下之前模拟实现的vector。
声明部分:
// 声明
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
size_t size() const
{
return _finish - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T)*sz);
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i]; // T对象是自定义类型时,调用T对象operator=
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
void push_back(const T& x);
iterator insert(iterator pos, const T& x);
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
定义部分(这里只将push_back和insert分离)
template<class T>
void vector<T>::push_back(const T& x)
{
insert(_finish, x);
}
template<class T>
typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
上面的代码都是放在FangZhang命名空间中的,就是为了和库中的vector分开。
再看下我们分离的:
出现了链接问题,细看的话就是push_back那有问题。
下面说为什么(下面的知识要求各位对编译和链接有一定的理解,如果有同学掌握得不是很熟练的话,可以看看我这篇博客:【C】程序环境和预处理 ):
我这里有三个文件
一个vector.h
一个vector.cpp
一个test.cppcpp文件都引了h,当我们编译了之后,两个.cpp生成对应的.o文件。
就是 vector.o 和 test.o在test.cpp中,引了vector.h,有test_template函数进行实例化,也就是里面FangZhang::vector
v;这条语句,将类模板进行了实例化。能够生成一份对应int的代码。此时.h中的构造、size、析构等函数也就实例化了。所以这些函数的地址在编译阶段就能够确定,那么test.o中是包含有这些成员函数的地址信息的,但是.h中只有push_back和insert的声明,故这两个函数的地址没法确定,只能确定函数名,所以test.o中缺少了这两个函数的函数地址信息。 在vector.cpp中,引了vector.h,而且.cpp文件中还有两个函数模板,但是该文件中没有任何的语句能够将其中的模板参数T实例化,那么push_back和insert 这两个函数也就没法实例化,也就是说编译器无法确定这两个函数的地址,vector.h中虽也有这两个函数的声明,但是都没什么用,函数地址无法确定,最终形成的符号表中只有函数名,没有对应的地址,但是编译器是能够编译通过的,因为编译器眼里还有最后链接的时也能确定函数的地址,所以最终形成的vector.o中也没有这两个函数的地址。
最终链接阶段,两个.o文件中都没有这两个函数的地址,所以这两个函数的地址都没法确定,而且我们还使用到了push_back,但是push_back并没有实例化,所以就出现了链接错误。
有两个解决方案。
- 模板的声明和定义不要分开放在 .h 和 .cpp 中,要分离也要分离在同一个文件中,意思就是一个文件中同时存放声明和定义(这样方便我们看代码框架)。
这里直接把分离的代码给出来:
namespace FangZhang
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
size_t size() const
{
return _finish - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T)*sz);
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i]; // T对象是自定义类型时,调用T对象operator=
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
void push_back(const T& x);
iterator insert(iterator pos, const T& x);
//两个函数声明
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
==================================
// 此处往下是定义
template<class T>
void vector<T>::push_back(const T& x)
{
insert(_finish, x);
}
template<class T>
typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
}
- 显示实例化(很挫,不推荐)
这个方法是在分开定义的.cpp中显示实例化出你想用的类型。
比如这里我在最下面显示实例化出一个int类型。
但是这种方法不好就在于,当我们再定义一个类型时,就要再在分离定义的.cpp中加上新添加的类型。
比如我再多搞个double的:
运行:
换个类型就要再实例化一个。
所以说这种方法比较挫,不是很推荐使用。
【优点】
【缺陷】
到此结束。。。