C++ STL——模板
注:原创不易,转载请务必注明原作者和出处,感谢支持!
注:内容来自某培训课程,不一定完全正确!
一 函数模板的特性
模板技术:类型参数化,编写代码可以忽略类型
为了让编译器区分函数是模板函数还是普通函数,模板函数需要以template <class T>
开头,或者以template <typename T>
开头。每个模板函数都需要一个上述的开头,一个开头不能对应多个模板函数。
template<class T>
void MySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
void callMySwap(void)
{
// 不指定变量类型,则模板函数会根据你所传递的值进行自动类型推导
int a = 3, b = 4;
cout << "a = " << a << " b = " << b << endl;
// 自动类型推导
MySwap(a, b);
cout << "a = " << a << " b = " << b << endl;
cout << endl;
double c = 3.14, d = 6.28;
cout << "c = " << c << " d = " << d << endl;
// 自动类型推导
MySwap(c, d);
cout << "c = " << c << " d = " << d << endl;
cout << endl;
// 显式地指定变量类型
char i = 'A', j = 'B';
cout << "i = " << i << " j = " << j << endl;
MySwap<char>(i, j);
cout << "i = " << i << " j = " << j << endl;
}
模板函数和普通函数在一起的调用规则
- 模板函数可以像普通函数那样被重载
- C++编译器优先考虑普通函数
- 如果函数模板可以产生一个更好的匹配,那么选择模板函数
- 可以通过空模板实参列表的语法限定编译器只能通过模板匹配
模板函数和普通函数的区别
- 函数模板不允许自动类型转化
- 普通函数能过自动进行类型转换
// 函数模板
template<typename T>
int MyAdd(T a, T b)
{
return a + b;
}
// int char
int MyAdd(int a, char c)
{
return a + c;
}
// int int
int MyAdd(int a, int b)
{
return a + b;
}
void callMyAdd(void)
{
int a = 10, b = 20;
char c1 = 'A', c2 = 'B';
// 调用函数模板,因为函数模板是更好的匹配
cout << MyAdd(a, b) << endl;
// 调用普通函数 int char
cout << MyAdd(a, c1) << endl;
// 调用普通函数,因为普通函数能够进行参数类型转换,而函数模板不行
cout << MyAdd(c1, b) << endl;
// 调用普通函数,因为编译器优先考虑普通函数
cout << MyAdd(a, b) << endl;
// 通过空模板实参列表强制要求编译器调用函数模板
cout << MyAdd<>(a, b) << endl;
}
模板函数的重载:
// 函数模板的重载
template<class T>
void Print(T a)
{
cout << a << endl;
}
template<class T>
void Print(T a, T b)
{
cout << "a = " << a << " b = " << b << endl;
}
void callPrint(void)
{
Print(3);
Print(7, 8);
}
下面是一个函数模板的具体使用案例。
// 函数模板使用案例
// 打印数组
template<typename T>
void PrintArray(T *arr, int len)
{
for (int i = 0; i < len; ++i)
cout << arr[i] << " ";
cout << endl;
}
// 对数组按从大到小顺序排序
template<typename T>
void MySort(T *arr, int len)
{
for (int i = 0; i < len; ++i)
{
for (int j = i + 1; j < len; ++j)
{
if (arr[i] < arr[j])
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
}
void call(void)
{
int iArr[] = { 9, 1, 5, 2, 8, 6 };
int ilen = sizeof(iArr) / sizeof(int);
char cArr[] = { 'j', 'b', 'e', 'i', 'k' };
int clen = sizeof(cArr) / sizeof(char);
cout << "排序前:" << endl;
PrintArray<int>(iArr, ilen);
MySort<int>(iArr, ilen);
cout << "排序后:" << endl;
PrintArray<int>(iArr, ilen);
cout << "排序前:" << endl;
PrintArray<char>(cArr, clen);
MySort<char>(cArr, clen);
cout << "排序后:" << endl;
PrintArray<char>(cArr, clen);
}
二 模板的实现机制
C++文件编译过程
输入 | 编译过程 | 输出 |
---|---|---|
hello.cpp | 预编译(g++ -E) | hello.i |
hello.i | 编译(g++ -S) | hello.s |
hello.s | 汇编(g++ -c) | hello.o |
hello.o | 链接(g++) | hello.out |
hello.cpp
中的内容
#include <iostream>
using namespace std;
#define MAX 1024
int main(int argc, char **argv)
{
cout << "MAX = " << MAX << endl;
return 0;
}
可以看到hello.i中的头文件不见了,取而代之的是头文件iostream中的内容被插入到了hello.i中,源代码中的define语句不见了,宏MAX被替换成了1024。hello.s则是对应的汇编语言文件,hello.o是二进制文件,最后一步链接之后,生成了可执行文件hello.out。
hello.cpp
中的内容为:
#include <iostream>
using namespace std;
// 函数模板
template<class T>
T MyAdd(T a, T b)
{
return a + b;
}
int main(int argc, char **argv)
{
int a = 10;
int b = 20;
double da = 3.14;
double db = 6.28;
MyAdd(a, b); // 对应的汇编入口地址:_Z5MyAddIiET_S0_S0_
MyAdd(da, db); // 对应的汇编入口地址:_Z5MyAddIdET_S0_S0_
return 0;
}
得到它对应的汇编文件:
g++ -S hello.cpp -o hello.s
打开hello.s文件,可以看到在main函数中的两次函数调用分别对应着如下的两条汇编命令。注意到它们的调用地址是完全不一样的。
call _Z5MyAddIiET_S0_S0_
和
call _Z5MyAddIdET_S0_S0_
这说明同一个函数模板生成了两个不同的模板函数。这两个模板函数对应的汇编内容分别如下所示:
_Z5MyAddIiET_S0_S0_:
.LFB973:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE973:
.size _Z5MyAddIiET_S0_S0_, .-_Z5MyAddIiET_S0_S0_
.section .text._Z5MyAddIdET_S0_S0_,"axG",@progbits,_Z5MyAddIdET_S0_S0_,comdat
.weak _Z5MyAddIdET_S0_S0_
.type _Z5MyAddIdET_S0_S0_, @function
和
_Z5MyAddIdET_S0_S0_:
.LFB974:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movsd %xmm0, -8(%rbp)
movsd %xmm1, -16(%rbp)
movsd -8(%rbp), %xmm0
addsd -16(%rbp), %xmm0
movsd %xmm0, -24(%rbp)
movq -24(%rbp), %rax
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE974:
.size _Z5MyAddIdET_S0_S0_, .-_Z5MyAddIdET_S0_S0_
.text
.type _Z41__static_initialization_and_destruction_0ii, @function
将hello.cpp的内容改成如下的形式,也即是加了两条代码。
#include <iostream>
using namespace std;
// 函数模板
template<class T>
T MyAdd(T a, T b)
{
return a + b;
}
int main(int argc, char **argv)
{
int a = 10;
int b = 20;
double da = 3.14;
double db = 6.28;
MyAdd(a, b);
MyAdd(da, db);
MyAdd(3, 4);
MyAdd(1.0, 2.3);
return 0;
}
再次得到它对应的hello.s文件。可以看到main函数里的四次函数调用汇编语句为:
1 call _Z5MyAddIiET_S0_S0_
2 call _Z5MyAddIdET_S0_S0_
3 call _Z5MyAddIiET_S0_S0_
4 call _Z5MyAddIdET_S0_S0_
可以看到,第1次和第3次调用是相同的,第2次和第4次调用也是相同的。也就是说MyAdd(a, b)
和MyAdd(3, 4)
调用的是同一个模板函数,MyAdd(da, db)
和MyAdd(1.0, 2.3)
调用的是同一个模板函数。
结论:
(1) 编译器并不是把函数模板处理成能够处理任何类型的函数
(2) 函数模板可以通过具体类型产生不同的函数
(3) 编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
三 类模板
类模板与之前的函数模板非常类似,下面是一个类模板的具体小例子。
// 类模板Person
template<typename T>
class Person
{
public:
Person(T id, T age)
{
mId = id;
mAge = age;
}
void Show(void)
{
cout << "ID = " << mId << endl;
cout << "AGE = " << mAge << endl;
}
private:
T mId;
T mAge;
};
void call(void)
{
// 函数模板在调用时可以进行自动类型推导
// 而类模板则只能显示地指定变量类型
// 不显式地指定类模板的变量类型会报错
// Person p(10, 20);
Person<int> p(10, 20);
p.Show();
}
需要注意的地方在于,之前的函数模板可以进行自动类型推导而无需显式地指定变量类型。类模板就不行了,必须指定参数类型,否则无法通过编译。
四 类模板如何派生子类
类模板派生普通类
类模板的派生和普通类的派生很类似,需要注意的是,类模板在派生普通类的时候需要指定类模板的具体数据类型。
// 类模板Person
template<typename T>
class Person
{
public:
Person(T id, T age)
{
mId = id;
mAge = age;
}
void Show(void)
{
cout << "ID = " << mId << endl;
cout << "AGE = " << mAge << endl;
}
private:
T mId;
T mAge;
};
// 类模板派生普通类需要指定类模板的具体数据类型
class Student : public Person<int>
{
};
类模板派生类模板
Animal的数据类型就是实例化Cat时指定的数据类型T。
template<typename T>
class Animal
{
public:
void Jiao(void)
{
cout << mAge << "岁的动物在叫!" << endl;
}
private:
T mAge;
};
// Animal的数据类型就是实例化Cat时指定的数据类型T(Cat的T传递给了Animal)
template<typename T>
class Cat : public Animal<T>
{
};
当然你也可以跟上面派生普通类时那样,为父类模板Animal指定特定的数据类型。
template<typename T>
class Animal
{
public:
void Jiao(void)
{
cout << mAge << "岁的动物在叫!" << endl;
}
private:
T mAge;
};
// 指定Animal的数据类型为int
template<typename T>
class Cat : public Animal<int>
{
public:
void eat() {}
private:
T color;
};
五 普通类的.h和.cpp文件分离
Person.h里只做声明,具体实现放在Person.cpp当中。Person.h中的内容如下:
#pragma once // 防止头文件被重复包含
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(string name, int age);
void Show(void);
private:
string mName;
int mAge;
int mID;
};
Person.cpp中的内容如下。
#include "Person.h"
Person::Person(string name, int age)
{
this->mName = name;
this->mAge = age;
}
void Person::Show()
{
cout << "Name = " << this->mName << endl;
cout << "Age = " << this->mAge << endl;
}
在main.cpp中使用该类。
int main(int argc, char **argv)
{
Person p("Jone", 20);
p.Show();
getchar();
return 0;
}
六 类模板在类内类外的实现
类模板在类内部实现
// 类模板在类内部实现
template<typename T1, typename T2>
class Person
{
public:
Person(T1 name, T2 age)
{
this->mName = name;
this->mAge = age;
}
void Show(void)
{
cout << "Name = " << mName << endl;
cout << "Age = " << mAge << endl;
}
private:
T1 mName;
T2 mAge;
};
void call(void)
{
Person<string, int> p("Jone", 20);
p.Show();
}
类模板在类外部实现
模仿普通类那样声明和实现分离的方式来写类模板,也即类模板在类外部实现。Person.h和Person.cpp分离的方式,在Person.h只声明不实现。
#pragma once
template<typename T>
class Person
{
public:
Person(T age);
void Show(void);
public:
T mAge;
};
在Person.cpp中实现。
#include "Person.h"
#include <iostream>
template<typename T>
Person<T>::Person(T age)
{
this->mAge = age;
}
template<typename T>
void Person<T>::Show(void)
{
std::cout << "Age = " << mAge << std::endl;
}
在main.cpp中调用。
#include <iostream>
#include "Person.h"
using namespace std;
int main(int argc, char **argv)
{
Person<int> p(23);
p.Show();
getchar();
return 0;
}
像上面这样写是不行的!
首先,上述代码能够通过编译步骤但无法通过链接步骤!也就是说上述代码无法达到目的!
原因在于C++模板的二次编译特性!模板的初次编译只是对模板本身进行编译,它不生成具体类型对应的类的目标代码。具体类型对应的类代码只有在指定了变量类型的调用时后才能够生成。而在链接main.cpp时,因为找不到数据类型为int的具体类Person<int>
的目标代码,所以链接器会报一个无法解析外部符号的错误,无法完成编译!
怎样解决这个问题?
很简单,在main.cpp中不再包含Person.h而是包含Person.cpp,利用include指令强行将类的定义内容给包含到main.cpp当中。这样,虽然模板类是分开写的,但是编译时却是在main.cpp当中一起被编译的。为了以示区别,一般将Person.cpp改名为Person.hpp。这样,Person.h里包含了模板类的定义,Person.hpp里包含了它的实现,而在main.cpp当中将Person.hpp给include进来即可。
模板类里的static变量
每一个由同一个模板类派生出来的具体类都将拥有与模板类里相同的static变量。所派生出来的不同的具体类所有的同名static变量互不影响,如下面的例子所示。
// 模板类内定义static变量a
template <typename T>
class Person
{
public:
static int a;
};
// 模板类外进行static变量初始化
template <typename T> int Person<T>::a = 0;
int main(int argc, char **argv)
{
// Person<int>和Person<char>将拥有不同的static变量a
Person<int> p1, p2;
Person<char> pp1, pp2;
// p1.a的修改只影响Person<int>里的a的值
p1.a = -1;
// p1和p2共享Person<int>类的static变量a
/*
输出:
p1.a = -1
p2.a = -1
pp1.a = 0
pp2.a = 0
*/
cout << "p1.a = " << p1.a << endl;
cout << "p2.a = " << p2.a << endl;
cout << "pp1.a = " << pp1.a << endl;
cout << "pp2.a = " << pp2.a << endl;
// pp1.a的修改只影响Person<char>里的a的值
pp1.a = 10;
/*
输出:
p1.a = -1
p2.a = -1
pp1.a = 10
pp2.a = 10
*/
cout << "p1.a = " << p1.a << endl;
cout << "p2.a = " << p2.a << endl;
// pp1和pp2共享Person<char>类里的static变量a
cout << "pp1.a = " << pp1.a << endl;
cout << "pp2.a = " << pp2.a << endl;
getchar();
return 0;
}
七 模板的应用实例
下面是一个C++模板的应用实例。MyArray.hpp中实现模板类的声明和定义
#pragma once
template <typename T>
class MyArray
{
public:
// 构造函数和析构函数
MyArray(int capacity);
MyArray(const MyArray<T> &arr);
~MyArray(void);
// 运算符重载
T& operator[] (int index);
MyArray<T> operator= (const MyArray<T> &arr);
// 追加数据
void PushBack(const T &data);
// 辅助函数
int Capacity(void);
int Size(void);
private:
int mCapacity; // 数组最大容量
int mSize; // 数组当前元素个数
T *pAddr; // 数组首地址指针
};
template <typename T>
MyArray<T>::MyArray(int capacity)
{
this->mCapacity = capacity;
this->mSize = 0;
this->pAddr = new T[this->mCapacity];
}
template <typename T>
MyArray<T>::MyArray(const MyArray<T> &arr)
{
this->mSize = arr.mSize;
this->mCapacity = arr.mCapacity;
this->pAddr = new T[this->mCapacity];
for (int i = 0; i < this->mSize; ++i)
this->pAddr[i] = arr.pAddr[i];
}
template <typename T>
MyArray<T>::~MyArray(void)
{
if (this->pAddr != nullptr)
{
delete[] this->pAddr;
this->mCapacity = 0;
this->mSize = 0;
}
}
template <typename T>
T& MyArray<T>::operator[] (int index)
{
return this->pAddr[index];
}
template <typename T>
MyArray<T> MyArray<T>::operator= (const MyArray<T> &arr)
{
if (this->pAddr != nullptr)
delete[] this->pAddr;
this->mCapacity = arr.mCapacity;
this->mSize = arr.mSize;
this->pAddr = new T[this->mCapacity];
for (int i = 0; i < this->mSize; ++i)
this->pAddr = arr.pAddr[i];
return *this;
}
template <typename T>
void MyArray<T>::PushBack(const T &data)
{
if (this->mSize >= this->mCapacity)
return;
this->pAddr[this->mSize] = data;
this->mSize++;
}
template <typename T>
int MyArray<T>::Capacity(void)
{
return this->mCapacity;
}
template <typename T>
int MyArray<T>::Size(void)
{
return this->mSize;
}
在main.cpp中调用。
#include <iostream>
#include "MyArray.hpp"
using namespace std;
void test(void)
{
MyArray<int> marray(20);
int a = 10, b = 20, c = 30, d = 40;
marray.PushBack(a);
marray.PushBack(b);
marray.PushBack(c);
marray.PushBack(d);
marray.PushBack(100);
marray.PushBack(200);
marray.PushBack(300);
// marray.PushBack(10);
// marray.PushBack(20);
cout << "marray.mCapacity = " << marray.Capacity() << endl;
cout << "marray.mSize = " << marray.Size() << endl;
for (int i = 0; i < marray.Size(); ++i)
{
cout << marray[i] << " ";
}
cout << endl;
}
int main(int argc, char **argv)
{
test();
getchar();
return 0;
}
C++ STL——模板的更多相关文章
- 泛型编程、STL的概念、STL模板思想及其六大组件的关系,以及泛型编程(GP)、STL、面向对象编程(OOP)、C++之间的关系
2013-08-11 10:46:39 介绍STL模板的书,有两本比较经典: 一本是<Generic Programming and the STL>,中文翻译为<泛型编程与STL& ...
- DLL中导出STL模板类的问题
接上一篇. 上一篇的dll在编译过程中一直有一个警告warning C4251: ‘CLASS_TEST::m_structs’ : class ‘std::vector<_Ty>’ ne ...
- STL模板_容器概念
一.STL(Standard Template Library,标准模板库)概述1.容器:基于泛型的数据结构.2.算法:基于泛型的常用算法.3.迭代器:以泛型的方式访问容器中的元素,是泛型的算法可以应 ...
- STL模板_概念
模板和STL一.模板的背景知识1.针对不同的类型定义不同函数版本.2.借助参数宏摆脱类型的限制,同时也因为失去的类型检查而引 入风险.3.借助于编译预处理器根据函数宏框架,扩展为针对不同类型的 具体函 ...
- C++STL模板库序列容器之vector
目录 STL之Vecter 一丶STL简介 二丶Vector用法 1.vector容器的使用 2.vector迭代器. 3.vector中的方法. 三丶常用算法 1.常见算法中的算法方法. 2.sor ...
- c++ STL模板(一)
一.sort函数 1.头文件:#include < algorithm>: 2.它使用的排序方法是类似于快排的方法,时间复杂度为n*log2(n): 3.Sort函数有三个参数:(第三个参 ...
- STL模板整理 Binary search(二分查找)
前言: 之前做题二分都是手动二分造轮子,用起来总是差强人意,后来看到STL才发现前辈们早就把轮子造好了,不得不说比自己手动实现好多了. 常用操作 1.头文件 #include <algorith ...
- STL模板整理 vector
一.什么是标准模板库(STL)? 1.C++标准模板库与C++标准库的关系 C++标准模板库其实属于C++标准库的一部分,C++标准模板库主要是定义了标准模板的定义与声明,而这些模板主要都是 类模板, ...
- C++面试笔记--STL模板与容器
1.C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作.vec ...
- 从零开始写STL—模板元编程之any
any class any; (since C++17) The class any describes a type-safe container for single values of any ...
随机推荐
- impala 建表时报错,不支持中文
1.错误信息 (1366, "Incorrect string value: '\\xE6\\x8E\\x88\\xE6\\x9D\\x83...' for column 'search' ...
- Spark集群任务提交流程----2.1.0源码解析
Spark的应用程序是通过spark-submit提交到Spark集群上运行的,那么spark-submit到底提交了什么,集群是怎样调度运行的,下面一一详解. 0. spark-submit提交任务 ...
- cnblogs排版样式预览
说明:关于本博主题及样式来源于[GitHub]:本博总体排版目录样式风格参照博文[修仙成神之路]进行预览:参照本博设置可参考博文[设置跟本博一样的效果]本博之前发表过的博文存在样式不协调,后期会逐一完 ...
- etcd简单测试类java版
为了方便现场安装完了etcd集群后确认集群是否好用,简单写了个测试类,网上搜的有点乱还有些不能运行,在这里再整理一个能够直接运行的 1.我把etcd的API设成3版本了,调用使用的jetcd,功能挺多 ...
- HLS manifest standard 翻译: HTTP Live Streaming draft-pantos-http-live-streaming-23
我为什么要干这种事 Introduction to HTTP Live Streaming 1 OVerview Multimedia presentation : specified by a Un ...
- [Abp vNext微服务实践] - vue-element-admin登录一
简介 之前的技术路线本来是angular的,后来经过一段时间的开发还是打算选择vue,原因是vue简单丰富,尽管angular规范强大,但是组件库都不太符合国人风格.看到GitHub上Vue Elem ...
- 01.CNN调参
转载:调参是个头疼的事情,Yann LeCun.Yoshua Bengio和Geoffrey Hinton这些大牛为什么能够跳出各种牛逼的网络? 下面一些推荐的书和文章:调参资料总结Neural Ne ...
- oracle时间差计算
1.months_between(date1,date2);date1和date2相减得到相差的月份. select months_between(to_date('2015-05-11','yyyy ...
- CodeForces - 113B Petr# (后缀数组)
应该算是远古时期的一道题了吧,不过感觉挺经典的. 题意是给出三一个字符串s,a,b,求以a开头b结尾的本质不同的字符串数. 由于n不算大,用hash就可以搞,不过这道题是存在复杂度$O(nlogn)$ ...
- 使用StringBuilder写XML遭遇UTF-16问题
http://www.cnblogs.com/jans2002/archive/2007/08/05/843843.html