【c++】深入学习c++中的模板

简介: 【c++】深入学习c++中的模板

前言



我们在前几篇的学习中只是使用了最简单的模板,比如用一个模板类等等,我们这篇的学习将在以前的基础上再深入的学习一下模板。


一、非类型模板参数



模板参数分类类型形参与非类型形参


类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

下面我们来演示一下:

#define N 10
//类型模板参数
template<class T>
class array
{
public:
private:
  T _a[N];
};
int main()
{
  Array<int> a1;
  Array<double> a2;
  return 0;
}


在以前,我们想要在一个类中定义一个模板数组能实现存放不同个数的数组是不行的,因为我们define的N是一个固定值,如下图:

7b708cab6b614659afb42ab9c8cac945.png

我们如何让两个数组存放的类型不同存放的个数也不同呢?这个时候非类型模板参数的效果就来了,如下:

//非类型模板参数
template<class T,size_t n = 10>
class Array
{
public:
private:
  T _a[n];
};
int main()
{
  Array<int,5> a1;
  Array<double,8> a2;
  return 0;
}


b4b9d2d677ba49d38e5dd6a694d6496d.png

我们可以看到这两个数组类型不同,数组元素个数也不同。在这里要记住,非类型模板参数一定是整形常量,不可以是浮点数,类对象以及字符串。

7023dd15993b4022acdb04cff2e2c5e5.png15640c9a378245149f51ff4152201e90.png

讲到这里我们可以对比一下c++11的新特性的array.

e29bcd0c69cf4fe9ad1ebebe9af6c727.png

这个数组和传统数组的区别在于,传统数组对于越界检查是抽查,array对于越界检查是全面检查。


9e7ba9e08e444640aa0a425fe0c20add.png8bf6234c2e67474c81bbb7abc47b7a97.png

从上图我们可以看到,传统数组都越界写了竟然没有检查出来越界,而array是可以检查出来的。但是这个array我们会用吗?其实不会,因为我们直接用vector是最方便的,没必要用array,并且array的空间在栈上,如果数据过多会将栈搞崩溃,而vector空间在堆上完全不受影响。  


二、模板的特化



1.引入概念

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  bool operator<(const Date& d)const
  {
    return (_year < d._year) ||
      (_year == d._year && _month < d._month) ||
      (_year == d._year && _month == d._month && _day < d._day);
  }
  bool operator>(const Date& d)const
  {
    return (_year > d._year) ||
      (_year == d._year && _month > d._month) ||
      (_year == d._year && _month == d._month && _day > d._day);
  }
  friend ostream& operator<<(ostream& _cout, const Date& d)
  {
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};
template <class T>
bool Less(T left,T right)
{
  return left < right;
}
int main()
{
  cout << Less(1, 2) << endl; // 可以比较,结果正确
  Date d1(2022, 7, 7);
  Date d2(2022, 7, 8);
  cout << Less(d1, d2) << endl; // 可以比较,结果正确
  Date* p1 = &d1;
  Date* p2 = &d2;
  cout << Less(p1, p2) << endl;  //可以比较,结果错误
  return 0;
}


对于上面的代码我们用了以前的日期类做例子,然后用一个Less函数比较日期的大小,通过结果发现对于指针的比较是错误的,如下图:

ddb68f70787048418323e77d9a130933.png


可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

那么这个时候该怎么办呢?我们就需要对模板进行特化,只针对Date*类型,如下图:

//模板特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
  return *left < *right;
}


对于模板特化我们必须在函数名后面显式类型,看一下特化后的结果:

649efa9629e946d891753ab25e5b541f.png


可见确实完成了我们的任务,刚刚我们演示的模板特化被称为全特化,就是参数的类型都是确定的。

//模板特化   半特化
template<class T>
bool Less(T* left, T* right)
{
  return *left < *right;
}

a0cce2eb741d4575a9ca2bf0253ebc29.png


我们的特化不一定非要确认类型,比如上面的半特化直接将所有指针类型包含了,我们的两个int*的指针去比较结果也是正确的。

模板特化分为函数模板特化和类模板特化。


下面我们讲解一下类模板特化:

template<class T1, class T2>
class Data
{
public:
  Data() { cout << "Data<T1, T2>" << endl; }
private:
  T1 _d1;
  T2 _d2;
};
template<>
class Data<int, char>
{
public:
  Data() { cout << "Data<int, char>" << endl; }
private:
  int _d1;
  char _d2;
};
int main()
{
  Date<int,int> d1;
  Date<int, char> d2;
  return 0;
}


对于类模板特化我们可以特化不同版本的类,比如上面代码中的(int,int)的类和(int,char)的类。并且上述代码中都是全特化,将模板参数列表中所有的参数都确定化,下面我们在演示一下偏特化:

template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 T1 _d1;
 T2 _d2;
};


偏特化就是对参数的进一步限制。我们演示一下偏特化的部分特化:

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
 Data() {cout<<"Data<T1, int>" <<endl;}
private:
 T1 _d1;
 int _d2;
};

当然我们的模板参数也可以是引用:

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
 Data(const T1& d1, const T2& d2)
 : _d1(d1)
 , _d2(d2)
 {
 cout<<"Data<T1&, T2&>" <<endl;
 }
private:
 const T1 & _d1;
 const T2 & _d2; 
 };


引用做模板参数不是很常见,但是我们在实现list的迭代器的时候见到过模板参数是引用。


2.模板的分离编译


什么是模板分离编译?一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

模板是不支持分离编译(在两个文件)的,会引发奇怪的报错。 如下图:


05dbc2cdaa1a46d48c87ace5892f7e42.png6fde151579334a41b3239f2b6e1104c0.pngfebc93146f274f04b5322ceae06ecce2.png


再说为什么出现这种情况之前我们先讲一下c/c++程序运行一般要经过的几个步骤:


fae0f635835546418f979a5bd6617a5d.png


我们发现同样的方式我们定义一个函数就能正常运行,如下图:


18486274c382436486189f806cc3e62c.pngc6872879fa0c42f9a4d1187f9a7724b5.png77cc02cbe5c546aab003c4ec6cb0292f.png526b90f70afc4ab297e128c16756025d.png


现在我们就通过汇编分析一下为什么会这样:

f692e91e180c4b3d92ecda703a9f5dbc.png


我们发现函数func会跳到0611348h这个函数地址,在这里func会被编译成一堆指令,所以在func.o中有函数的地址,而模板是没有函数的地址的,因为模板需要实例化后才有具体的地址,刚开始地址为空如下图:


bded0ce4dcd647b9a559d63deb315dbe.png

在编译的时候在地址处打个问号,等到链接的时候再去拿Add的地址,如下图:


15b7bfe20640407cb7951821988ceba3.png

等到链接的时候找到func的地址,但是却找不到add的地址,如下图:

3050b82650a946adba35d45d5fa6647e.png

也就是说模板分离编译出错的原因是链接的时候无法拿到模板函数的地址,那么有办法可以解决吗?其实是有的,但是不常用因为每个类型都需要显示实例化:

e321a65f20014788b1fd3267c1df5682.png069c32c61e444cd5994d1aab39cdf3f7.png

这个时候我们把int类型的add放出来又不行了:

c5de4e731c9f48fbaf35bef9fe0dfd92.png


要想解决还是刚刚显示实例化的方法,但是这样就会很繁琐。所以我们最好的办法就是不要分离在两个文件,就在一个文件中实现。如下图:

4f25bbd7c0de4d34b4a31c897be712a6.png2076edfed81841a2995671a07326c7c3.png

以上就是本篇文章的所有内容,最重要的是理解为什么模板分离编译会报错并且如何解决这个问题

注意:

对于模板的报错,我们一定要先解决报错的第一个,因为通常模板是有一个问题带出来的其他好多问题,将第一个问题解决了就解决了其他问题。


总结



【优点】

1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生

2. 增强了代码的灵活性


【缺陷】

1. 模板会导致代码膨胀问题,也会导致编译时间变长

2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

目录
相关文章
|
1月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
101 10
|
3月前
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
3月前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
82 0
|
29天前
|
编译器 C语言 C++
配置C++的学习环境
【10月更文挑战第18天】如果想要学习C++语言,那就需要配置必要的环境和相关的软件,才可以帮助自己更好的掌握语法知识。 一、本地环境设置 如果您想要设置 C++ 语言环境,您需要确保电脑上有以下两款可用的软件,文本编辑器和 C++ 编译器。 二、文本编辑器 通过编辑器创建的文件通常称为源文件,源文件包含程序源代码。 C++ 程序的源文件通常使用扩展名 .cpp、.cp 或 .c。 在开始编程之前,请确保您有一个文本编辑器,且有足够的经验来编写一个计算机程序,然后把它保存在一个文件中,编译并执行它。 Visual Studio Code:虽然它是一个通用的文本编辑器,但它有很多插
|
1月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
18 1
|
1月前
|
Java 编译器 C++
c++学习,和友元函数
本文讨论了C++中的友元函数、继承规则、运算符重载以及内存管理的重要性,并提到了指针在C++中的强大功能和使用时需要注意的问题。
22 1
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
46 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
1月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
80 2
|
1月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
39 2
|
1月前
|
存储 算法 编译器
【C++】初识C++模板与STL
【C++】初识C++模板与STL