【C++】模板初阶&&STL简介

简介: 【C++】模板初阶&&STL简介

今天,你内卷了吗?

8d3b43c5584d4d3f8f0c655c5abff714.jpeg


一、泛型编程


1.

假设要交换两个变量的值,如果只是用普通函数来做这个工作的话,那么只要变量的类型发生变化,我们就需要重新写一份普通函数,如果是C语言,函数名还不可以相同,但是这样很显然非常的麻烦,代码复用率非常的低。

那么能否告诉编译器一个模板,让编译器通过模板来根据不同的类型产生对应的代码呢?答案是可以的。


2.

而上面这样利用模板来生成类型所对应的代码,这样的思想实际上就是泛型编程。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。

模板正是泛型编程的基础,模板又可以分为类模板和函数模板。

void Swap(int& left, int& right)
{
  int tmp = left;
  left = right;
  right = tmp;
}
void Swap(double& left, double& right)
{
  double tmp = left;
  left = right;
  right = tmp;
}


二、函数模板(显示实例化和隐式实例化)

1.函数模板格式


1.
函数模板格式:
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){ }

2.
typename和class是用来定义模板参数的关键字。

template<typename T>//typename和class这里都可以
void Swap(T& left, T& right)
{
  T tmp = left;
  left = right;
  right = tmp;
}
int main()
{
  int a = 1, b = 2;
  Swap(a, b);
  double c = 1.1, d = 2.2;
  Swap(c, d);
  return 0;
}


2.单参数模板


1.

模板的实例化有两种方式,一种是显示实例化,一种是隐式实例化,隐式实例化就是让编译器根据实参所传类型确定模板参数,然后推导出来函数,显式实例化是告诉编译器指定模板参数的类型。


2.

如果显示实例化后,实参与指定模板参数类型不同,则编译器会自动发生隐式类型转换。

template<class T>
T Add(const T& left, const T& right)
{
  return left + right;
}
int main()
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  //自动推演实例化,让编译器推导T的类型
  cout << Add(a1, a2) << endl;
  cout << Add(d1, d2) << endl;
  cout << Add((double)a1, d2) << endl;//强制类型转换,产生临时变量,临时变量具有常性
  cout << Add(a1, (int)d2) << endl;
  //显示实例化,直接告诉编译器T的类型
  cout << Add<double>(a1, d2) << endl;//隐式类型转换,产生临时变量,临时变量具有常性
  cout << Add<int>(a1, d2) << endl;
  return 0;
}


3.多参数模板

模板参数除单个外,也可以是多个,在使用上和单参数模板没什么区别,同样实例化的方式也可分为两种,一种是隐式实例化,一种是显示实例化。

template<class T1,class T2>
T1 Add(const T1& left, const T2& right)
{
  return left + right;
}
int main()
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  //自动推演实例化,让编译器推导T的类型
  cout << Add(a1, a2) << endl;//T1和T2都推成int
  cout << Add(d1, d2) << endl;//都推成double
  cout << Add(a1, d2) << endl;//推成一个是int,一个是double
  cout << Add(d1, a2) << endl;
  return 0;
}


4.模板参数的匹配原则


1.

普通函数和模板函数若同名,是可以同时存在的,相当于两个函数构成重载,在调用上,一般会优先调用普通函数,因为编译器虽然勤快,但是它不傻,他知道调用模板函数需要进行推演实例化,如果有现成的普通函数,他肯定不会去推导模板实例化的。但是如果强行显式实例化模板参数,那编译器也没辙,就会显示调用模板推导出来的函数。

// 专门处理int的加法函数
int Add(int left, int right)// _Z3Addii
{
  return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)// _Z3TAddii(修饰规则不一定这样,只是假设而已)
{
  return left + right;
}
//一个具体的函数和模板函数能不能同时存在呢?答案是可以的,他们是可以同时存在的
int main()
{
  int a = 1, b = 2;
  Add(a, b);//会优先调用具体的函数,而不是调用模板进行推导。编译器很勤快,但它不傻。
  Add<int>(a, b);//显示调用模板推导出来的函数。
  //上面两行代码可以说明模板推导的int函数和具体的int函数可以同时存在,那么就可以证明这两个函数的函数名修饰规则是不一样的
  return 0;
}


三、类模板(没有推演的时机,统一显示实例化)

1.类模板的使用


1.

类模板和函数模板在使用上有些区别,函数模板可以隐式实例化,通过实参类型进行函数推演,而类模板是无法隐式实例化的,因为没有推演的时机,所以对于类模板,统一使用显示实例化,即在类后面加尖括号,尖括号中存放类型名,进行类模板的实例化。


2.

值得注意的是类模板不是具体的类,具体的类是需要进行实例化的,只有类名后面的尖括号跟上类型才算实例化出真正的类。

例如下面的栈,如果想让栈存储不同类型的数据,就需要显示实例化类模板。

template<typename T>
class Stack
{
public:
  Stack(int capacity = 4)
  {
    cout << "Stack(int capacity = )" <<capacity<<endl;
    _a = (T*)malloc(sizeof(T)*capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  ~Stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void Push(const T& x)//用引用效率高一点,因为类对象可能所占字节很大,如果不改变最好加const
  {
    // ....
    // 扩容
    _a[_top++] = x;
  }
private:
  T* _a;
  int _top;
  int _capacity;
};
int main()
{
  //Stack st1; 
  st1.push(1);
  //st1.Push(1.1);
  //Stack st2; 
  //st2.Push(1);
  //一个栈存int,一个栈存double这样的场景,用typedef无法解决,必须使用类模板来解决。
  //函数模板可以通过实参传递形参,推演模板参数。但是类模板一般没有推演的时机,统一使用显示实例化。
  Stack<int> st1;
  st1.Push(1);
  Stack<double>st2;
  st2.Push(1.1);
  //180和182行是两个不同的类类型,因为类的大小有可能不同,所以类模板相同,但模板参数不同,则类模板实例化出来的类是不同的。
  //st1 = st2;//这样的操作是不允许的,因为如果类不同,则实例化对象肯定也不相同,所以不可以赋值。
  return 0;
}

2.类模板实现静态数组


1.

std命名空间中的array可能和我们的array产生冲突,所以我们可以利用自己的命名空间将自己的类封装起来,以免产生冲突。


2.

利用运算符重载可以实现对静态数组中每一个元素进行操控。

与C语言不同的是,这种运算符重载可以不依赖于抽查行为,而是进行严格的越界访问检查,通过assert函数来进行严格检查。

#define N 10
namespace wyn
{
  template<class T>
  class array
  {
  public:
    inline T& operator[](size_t i)//内联,不会建立栈帧,提高效率,减少性能损耗。
    {
      assert(i < N);//不依赖于抽查行为
      return _a[i];
    }
  private:
    T _a[N];
  };
}
int main()
{
  int a2[10];
  //C语言对于越界访问是抽查的方式,访问近一点的位置,会查到报错,但访问远一点的位置,就有可能不会报错。
  //a2[10] = 10;
  //a2[20] = 20;//对于越界访问写,勉强会检查到。
  a2[10];
  a2[20] ;//但对于越界访问读,基本不会检查出来。
  wyn::array<int> a1;//array有可能和std命名空间里面的array冲突,所以我们自己定义一个命名空间
  for (size_t i = 0; i < N; i++)
  {
    a1[i] = i;
    //等价于 a1.operator[](i)=i;
  }
  for (size_t i = 0; i < N; i++)
  {
    a1[i]++;
  }
  for (size_t i = 0; i < N; i++)
  {
    cout << a1[i] << " ";
  }
  cout << endl;
  return 0;
}


3.类模板能否声明和定义分离?


1.

首先明确一点,类模板是不允许声明和定义分离的,因为这会发生链接错误。


2.

其实原因很简单,因为在用的地方类模板的确进行了实例化,可是用的地方只有声明没有定义,而定义的地方又没有进行实例化,所以就会发生链接错误。


3.

说白了就是Stack.cpp里面的类模板由于没有实例化,那就是没有真正的类,所以类中成员函数的地址无法进入符号表,那么在链接阶段,Test.cpp就无法链接到类成员函数的有效地址。


4.

解决方式有两种:

a.既然在Stack.cpp里面类模板没有实例化么,那我们就手动在Stack.cpp里面进行实例化就好了,但是这样也有一个弊端,只要类模板参数类型改变,我们手动实例化时就需要多加一行,这未免有些太繁琐了吧!

68cb6753338443d88dee2909f0c787f4.png

b.

所以最好的方式就是不要将类成员函数定义和声明分文件存放,而是将类模板中的成员函数直接放在.h文件里面。这样就不会出现找不到有效地址的问题了,因为一旦Test.cpp中进行了模板实例化,则.h文件中的那些方法也就会实例化,此时他们的地址就会进入符号表。也有人会将.h文件重命名为.hpp文件,这就是典型的模板类的声明和定义方式。


template<class T>
Stack<T>::Stack(int capacity)//声明处写了缺省值,定义处就不用写了。
{
  cout << "Stack(int capacity = )" << capacity << endl;
  _a = (T*)malloc(sizeof(T) * capacity);
  if (_a == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  _top = 0;
  _capacity = capacity;
}
template<class T>
Stack<T>::~Stack()
{
  cout << "~Stack()" << endl;
  free(_a);
  _a = nullptr;
  _top = _capacity = 0;
}
template<class T>
void Stack<T>::Push(const T& x)
{
  // ....
  // 
  _a[_top++] = x;
}

9fd592eb661e4e1685519c5bbedda714.png

74b1bb8e12974f3db48f0770466978dd.png


四、STL简介

1.什么是STL

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。


2.STL的版本


原始版本:

Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本–所有STL实现版本的始祖。

P. J. 版本:

由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。

RW版本:

由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。

SGI版本:

由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。


3.STL的六大组件


bfe94600697b4ca6bf74ac55c3173f4a.png


































































































































相关文章
|
1天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
19 7
|
19天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
45 4
|
20天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
45 5
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
22天前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
37 4
|
23天前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
32 3
|
26天前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
42 0
|
5天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
17 0
|
26天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
67 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
70 4