从这里开始,有了C++类和对象的铺垫,我们能够引入C++最为重要的一个工具–STL模板库,在STL模板库中,我们得以将许多C语言的语法的局限性完全打开,尤其是繁杂的字符串操作和对于自定义类型的处理,那么接下来,就让我们熟练STL模板的使用和模拟实现。
何为模板?想象一下我们去超市购买的雪糕,从甜筒机里我们可以按照我们想要吃的得到各种口味的相同形状的甜筒雪糕,在这里,不同口味不同材料的冰激淋被一个统一的模板构造成相同的形状然后返回给我们,我们所关注的是最后我们冰激淋的样子,这便是一个面向对象的过程。而在C++中的模板也是如此:模板代表了一种通用的程序执行逻辑,不同类型的数据在模板里都会进行相同的操作,根据你传入的数据类型进行实际的调整,从而得到我们想要的结果。
template
1.在尖括号里面,我们可以定义一个或者多个模板变量,根据自己实际的需求去定义,比如你需要两个不同数据类型的数据相加或者比较大小。
2.模板语句之后必须只跟要使用模板的模板函数或者模板类,中间不要穿插其他的语句或者变量,这是因为模板不是语句它不是独立的,必须直接跟使用它的场景,否则一旦中间有分号,模板自动跟这个分号的语句识别为一组,下面的模板就会报错没有意义了
3.注意,与函数不同,模板里面传的是类型,而函数里面传的是变量,这个别搞错了
模板根据其使用的场景,我们将其分成两类:
1.第一类为函数模板
2.第二类为类模板
下面我们分别介绍一下两种模板:
函数模板,即用于函数使用的模板类型,主要用于对函数体的程序进行模板的使用,大致的用法就是:
**模板声明后下一行直接跟函数定义即可。**如下:
template<class T>
T add(T x,T y)
{
return x+y;
}
也可以这样:
template<class T1,class T2>
T2 add(T1 x,T2 y)
{
return x+y;
}
这样,就可以分别传两种不同的数据类型的数据,但可能涉及到隐式转换的问题,所以我们最好不要这样,倘若非要这样,最好强转其中一个数据,比如在这里要返回int类型,倘若我传入的数据是5,5.5,那我就把强转为int类型,这样就不会出现结果的问题。
但是,倘若100个参数,我就得一个一个的去强转,那样太麻烦了,故我们可以这样:
template<class T>
T add(T x,T y)
{
return x+y;
}
int main()
{
cout<<add<int>(5,5.5)<<endl;
}
在这里,我们显式实例化函数模板,让对应的T变成固定的int类型,不管传入什么数据,都会被当成int类型处理这样,就可以避免数据类型不同产生的报错
故在这里,我们用两种方式来针对不同类型的参数的传入:
1.定义多个模板变量,然后分别占位
2.使用显式实例化,将所有的模板变量手动全部按照<>里面的数据类型处理
如下:
template<class T1,class T2>
int add(T1 x, T2 y)
{
return x + y;
}
int main()
{
cout<<add<int>(1, 2.2);
return 0;
}
何为实例化,首先我们要清楚,模板和类一样,本质上是一个蓝图,并不是实际占用空间的程序,而通过蓝图使变量被创建并使用的过程叫做实例化。
常见的实例化分为两种:
1.隐式实例化:即不是用户自己去定义的,而是计算机通过传入函数的参数推导出来的数据的类型并且对应使用的叫做隐式实例化
2.显式实例化:用户直接给模板规定的实例化的参数是什么,而不是计算机自己推出来的,一般在对应的位置加上<>里面写显式的变量类型,如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错
不管是函数,还是类,他们的实例化都是如此。
同名字和模板函数和普通的函数可以同时存在么?
由我们学到的函数重载的知识可知,T和int在编译器中会被标上特殊的符号,从而被识别为不同的函数,所以他们是可以同时存在的
让我们考虑下面的情况:
int Add(int left, int right)
{
return left + right;
}
template<class T> T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2);
}
针对上面的情况,同时存在模板Add函数和普通的全局Add函数,他们是可以同时存在的,那我这里的Add(1,2),它会进入哪个函数呢?
我们通过调试如下:
我们发现它进入了普通的函数add,这是为什么呢?
这是由于,模板由于参数的不确定性,编译器需要识别判断,但现成的函数就相当于已经显式实例化的模板,编译器自然会进入不用它自己推导的普通函数里面。
想要让他进入我们的模板函数,可以这样改:
int Add(int left, int right)
{
return left + right;
}
template<class T> T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add<int>(1, 2);
}
调试如下:
我们发现,它就进入我们的模板函数,这是由于,我显式实例化了add,同时也起到了告诉编译器强制进入模板函数的意思,编译器发现反正都不需要推导,不如就进入你让我进入的那个函数,故就进入了模板函数。
故我们在这里总结模板函数的匹配原则:
1.一个非模板的函数可以和一个同名的模板函数同时存在,而且该模板函数还可以实例化这个非模板函数
2.对于非模板函数和同名的模板函数,如果其他条件都相同(包括参数个数,参数的数据类型这些,主要是看参数),那么在调用时会优先调用非模板函数而不是模板函数,但我们可以显式实例化使其强制调用模板,如果模板可以产生一个具有更好匹配的函数,那么编译器就会选择调用模板函数,这个更好匹配主要看你传入的参数类型是什么
3.模板函数不允许自动类型转换,但普通函数可以进行自动类型转换,即哪怕是双目操作符两边的数据需要转换,也不会转换
类模板的书写方式和函数一样,也是在类的前面加上template
但类的使用和函数不同,类必须要显式实例化,也就是必须要加<>在里面给具体的模板参数的类型是什么
!!!注意:类模板最为重要的一点,接触到模板后,我们要清楚一个概念:
在模板出现之前,类的名字就是其类型,但有了模板后,类的名字就是名字,类的类型是名字<显式实例化>,而不是单纯的名字,故我们在使用创建模板类的时候千万别忘了加<>显式实例化的内容,同时,模板类最好不要声明和定义分离,并不是不能,而是很容易错,因为涉及到这个<>有时候很容易错
例如:
// Vector类名,Vector才是类型
Vector<int> s1;
Vector<double> s2;
现在,我们已经有了模板的概念并且清楚模板如何使用,模板的知识是我们接下来的STL模板库的一个重要的铺垫,一定要先弄明白再向下进行,否则对于模板库的理解会出现问题。
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。也就是说,通过模板库,我们只需要传参就可以实现对应的功能,不需要再像C语言那样自己手动构建这些框架和功能。
我们的STL大概分为四种版本:
1.原始版本
2.PJ版本
3.RW版本
4.SGI版本
而SGI版本由于可移植性好,被广泛使用,现在的很多STL资料基本都是SGI版本,全面而且阅读体验也很好,在这里我也用这个版本的STL库。
STL主要分为一下的内容:
首先,这里的容器就是数据结构,这个是最容易被误解的东西,其实容器本质上就是存储数据的,加容器也无可厚非。
1.STL库写的非常的冗余,很多功能的配置本质上是不需要的,这也是因为很多人为了自己的需求而不断去扩展导致的。
2.STL内部实现很复杂,设计迭代器一些东西
3其实STL的问题还有很多,但是介于我们目前所学,这两点是我们首先应该对STL有的一个大致的概念
我会在后面附上:
正如我今天的标题所说,我们首先会进入STL最常见的第一个模板:string模板,即字符串操作模板
依旧是我们经常使用的思维方式:
让我们先想想我们C语言字符串方面的问题:在C语言中,字符串以\0结尾,C语言提供了一系列操作字符串的函数,但问题在于函数的功能太过单一,不够全面,而且很容易出现访问越界的问题,通俗来讲就是用户需要管理底层,从而导致访问大量越界。而C++则直接让用户关注于字符串本身,大量的成员函数保证了其功能的完整。
** 针对string的特性,我们这样总结:
a.string是表示字符串的字符串类
b.该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作
c.string在底层实际是:basic_string模板类的别名,typedef basic_string
d. 不能操作多字节或者变长字符的序列。**
下面,我们针对string的几个重要的函数接口进行一下说明和使用:
注意,使用string类模板需要我们引用头文件#include< string >同时要开放官方命名空间std
#include< string >
using namespace std;
在这里我们常用的是无参或者一个字符串或者一个字符串的一部分,在这里我们看所给的函数的具体实现的,我们从函数的具体参数可以知道,构造是支持传入无参空字符,一个字符,一个字符串(const或者非const),一个字符串的前几位,一个字符串的起始点到中止点的一部分,或者指定长度改为一个字符去创建字符串。具体要看我们要实现怎样的功能。
这里的第2个就是拷贝构造函数千万别混淆,拷贝构造也是构造函数的一种
这个析构函数和我们类和对象里面的析构函数差不多,我们具体在析构函数里面实现我们要资源清理的内存,这里不过多阐述,主要就是当有动态内存被开辟的时候,我们的析构函数就必须要显式书写对应的内存释放的析构函数。
这个赋值运算符重载和之前讲的赋值运算符重载,**倘若两个已经被创建出来的string类,就直接调用赋值运算符重载,倘若为未创建的string类,则直接调用拷贝构造函数创建变量,**这里有很多的细节,为了不影响我们的主体内容,在这里我们先不介绍,我会在本篇的最后补充着部分的知识。
在这里设置size是可以用来返回字符串的长度的,和strlen一样,size也不会把\0计入到长度中,由于_size是private的成员函数,故没法直接访问,故只能通过函数待会得到长度。
length和size的用法相同,只不过以前叫length,经过更新引入了size,其具体的函数用法都一模一样。
capacity函数是用来返回字符串的内存大小的,这个和size不一样,size是实时控制字符串的长度,而capacity不管是否插入数据,其内存都存在,capacity就是用来记录这个内存的大小的,一个字符串的长度可能为4,但它开辟的空间可能要大于4,但绝对不能小于4.
清空字符串,它的实现原理其实就是在下标为0的位置放一个\0,由于要迁就C语言的关系,C++也同样识别到\0停止,所以我们下标为0的位置放入\0,就相当于字符串被清空了(但同时也别忘了改变_size=0),不过注意capacity一般是不轻易改变的,但size是实时改变的
注意reserve调整的是capacity而不是size,别看混了
改变的是size,而不是capacity,同时,我们这里不仅可以调整size,对于多开的位置,我们可以指定字符char c放入。
缩容空间的作用,不过多赘述,它可以缩容capacity让其和对应的size匹配到一起
容量操作类的细节理解:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小,也就是说不改变capacity的大小
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
通过这个函数,我们就可以向访问数组下标一样访问字符串的元素内容。
学会了这个函数之后,我们就可以这样遍历字符串:
string d1("hbw66");
int i=0;
for(i=0;i<d1.size();i++)
{
cout<<d1[i]<<" ";
}
看到上面的图片,我们会很疑惑它这里的iterator是什么呢?其实,这里的iterator便是迭代器类型的变量的意思,而在这里,begin会返回指向第一个元素位置的迭代器变量,而end会返回最后一个元素的下一个元素位置的迭代器变量,即\0的位置。
所以下面让我们先介绍一下迭代器:
迭代器关键字iterator,迭代器类型的本质实际上是一种指针,但它是string里面的一个内部类,要通过string域限定符去访问,即string::iterator it,迭代器适用于各种容器模板的场景,这一点是要比我们上面的单纯遍历更加的适用。由于迭代器就跟指针一样,故迭代器也需要解引用才能访问或者修改里面的具体元素。
迭代器完全体现了面向对象的封装,根据不同的适用情况去适配不同的迭代器,常见的string迭代器在这里有四种:
1.const_iterator:const的string类迭代器
2.const_reverse_iterator:const类的翻转string迭代器
3.reverse_iterator:非cosnt类的翻转迭代器
4.iterato:正常迭代器
由于迭代器前面的限制需要注意的太多,我们可以用auto自动识别类型同时简化代码,这样不会误判类型,让auto自动去匹配对应的类型。
由此,我们就可以这样遍历字符串了:
string d1("hello world");
auto it=d1.begin();
while(it!=d1.end())
{
cout<<*it<<" ";
it++;
}
通过对迭代器的解引用和操作,从而实现遍历的效果,还记得我们之前说过的范围for么?本质上,范围for可以实现++,向后走的功能就是通过迭代器,故范围for本质上就是通过迭代器来实现的,范围for在调用代码时,其实本质上调用的就是迭代器遍历,这一点是可以检验的,在后面我们模拟实现string模板的时候我会再提及。
相当于反向遍历字符串,其他的使用方式和begin end相同,本质上也是返回迭代器。
这里的at函数作用和[]运算符重载一样,但唯一的不同是,at发生越界时会抛异常,故我们需要try catch接收,而[]则是直接报错终止掉程序,相当于assert断言的效果
在字符串中尾插一个字符,注意根据函数,它只能尾插字符不能尾插字符串
在字符串中尾插一个字符串,根据函数参数的书写,我们可以插一整个字符串,也可以根据需要只尾插一部分或者指定长度的字符串的一部分,也可以插一个string类
功能相当于push_back和append的合体,更加简便和适用,所以为什么说STL模板很复杂冗余呢?明明有+=这样好的,再写一个push_back和append意义不大。
这里不过多赘述,就是对原字符加上字符从而创建一个新的字符串,但同时原字符不发生改变。
把assign里面的一部分赋值给接收值,但接收值之前的数据会被覆盖,可以控制长度
在指定位置的前面插入,可以插一个字符,也可以插一个字符串,或者一个string类,同时还可以指定个数的插入
在指定位置删除,可以指定长度,或者指定迭代器,或者指定删除的范围
可以在指定位置替换插入字符或者字符串的函数,它可以指定替换单个字符,也可以在单个字符的位置直接替换插入一整个字符串:
如下:
string d1("hello world");
d1=d1.replace(3,"hbw040115");
cout<<d1<<endl;
这样,打印出来的结果就是helhbwo4o115 world。
但由于涉及到挪动数据的问题,erase insert replace这三种函数都是不提倡使用的,因为太影响效率了
以C语言的方式返回字符串,也就是说结尾是带\0的,这是C++兼容C语言的体现。
用来交换两个字符串的this和str的函数,本质上就是交换这两个字符串的地址赋给对应的指针,让this的指针指向str的位置,str的指针指向this的位置
swap中还封装了一个全局函数swap,即this不是隐藏的而是正正好好写出来的两个string参数,这个时候一旦调用可能会涉及到深拷贝的问题,故通过这个全局函数swap,让用户无论怎样调用都是这个类里面的函数,提高了效率
找到指定的符号在字符串中的对应位置,并且返回这个位置对应的下标,如果找不到,就返回npos即极大长度值
将指定下标之间的元素返回给一个string类,这个函数一般配合find使用,可以起到分割字符串的作用如下:
hbw::string d2("http://www.google.com/feference/peffect/friend/");
hbw::string sub1, sub2, sub3;
size_t i1 = d2.find(':');
if (i1 ==d2.getnpos())
{
cout << "没有找到i1" << endl;
}
else
{
sub1 = d2.substr(0, i1);
}
size_t i2=d2.find('/',i1+3);
if (i2 == d2.getnpos())
{
cout << "没有找到i2" << endl;
}
else
{
sub2 = d2.substr(i1 + 3,i2-(i1+3));
}
sub3 = d2.substr(i2 + 1, d2.size() - (i2 + 1));
cout << sub1 << endl;
cout << sub2 << endl;
cout << sub3 << endl;
在这里,它可以将网址分隔开三个部分。
fing_first_of用来从头找字符串中是否有指定的字符,并返回下标,而last则到着找,而带not的则是反过来,找字符串中不是指定字符的字符并返回其下标,last同理也是倒着找.
getline是可以重新设置字符串的结束标志的函数,它默认会直接识别读取到一行结束,无视空格,一旦用户指定符号,就一直读到指定符号后结束,这个很关键,在oj题里使用C++很常用
不多赘述,模拟实现时我们详解,在这里我们先知道,它可以让string类型也使用<< >>符号输入输出字符串,十分方便
用来比较两个字符串大小的函数,其实现大致利用了strcmp函数,对于这一系列运算符重载函数,可以让我们对于string类也可以使用内置类型的操作符,更加方便。
只需要注意其实现的时候,运算符重载多用复用实现即可,这个后续在模拟实现的时候我会演示。
以上便是我们string模板的介绍和一些函数用法的解析,下一篇文章中我们将进一步模拟实现这些函数,从而让我们进一步理解他们的用法,想要熟练使用STL库,我们需要多多刷题反复使用这些函数,自然而然就记住了,对于一些不常见的我们直接查文档,记不住也不要焦虑,那意味着并不常用,我们只需要明确STLstring库中有这个东西,到时候直接查看使用即可。