前面花费了大量的时间在学习类和对象,其目的就是为了能够使用C++官方库中的各种类和模板,使我们的编程更加容易,接下来先看看什么是模板。
在学习模板之前,首先要知道什么是泛型编程。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
void Swap(double& x, double& y)
{
double tmp = x;
x = y;
y = tmp;
}
void Swap(char& x, char& y)
{
char tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
cout <<"交换前:"<< a << " " << b << endl;
Swap(a, b);
cout << "交换后;" << a << " " << b << endl;
double c = 1.1;
double d = 2.2;
cout << "交换前:" << c << " " << d << endl;
Swap(c, d);
cout << "交换后:" << c << " " << d << endl;
char e = 'a';
char f = 'b';
cout << "交换前:" << e << " " << f << endl;
Swap(e, f);
cout << "交换后:" << e << " " << f << endl;
return 0;
}
上图中代码是将不同类型的数据进行交换。
三种类型的数据交换就需要写三个重载函数,如果类型很多呢?我们知道C++的主要特性就是有自定义类型,如果是交换一个自定义类型呢?不难想到,每有一种数据类型需要进行交换我们就需要写一个对应的重载函数。
有一种办法,不需要考虑类型,但是仍然按照这个逻辑去写交换函数。
此时的代码仍然可以实现最开始代码的功能,而且我们只写了一个函数,没有重载多个,就可以实现多个重载函数的功能,着就是泛型编程。
template <typename T>
void Swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
函数部分,只写了如上的代码。
- template 关键字表明这是一个模板
- <>中的typename T是模板参数,其中typename也可以使用class,在这里它俩是一样的作用。
- T表示一个类型,具体什么类型不知道,也就是我们所说的泛型
- 使用模板类型T写出的Swap函数就是我们创建的一个模板,可以看到,无论什么类型的数据进行交换,它们的逻辑都是这个模板中的逻辑,只是类型不一样。
- template 的作用域就是它后面跟着的模板,该模板之后的函数就不是模板了,而是函数。
上图中,只要要党徽的模型中浇筑不同演示的塑料液就能产生不同颜色的党徽。而本喵上实现的交换函数模板也是这个意思,只要告诉模板是什么类型,它就会交换什么类型的数据。
而模板又有两种,一种是函数模板,另一种是类模板。
概念:
- 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式:
- template
- 返回值类型 函数名(参数列表){}
模板参数可以有多个,编译器会根据代码自推断参数是什么,从而代替函数模板中的模板参数,template语句和函数模板必须是挨着的。
- 函数模板是一个蓝图,它本身并不是函数,是编译器产生特定具体类型函数的模具。
- 所以其实模板就是将本来应该我们做的重复的事情交给了编译器
隐式实例化:让编译器根据实参去推演模板参数的实际类型。
它和类一样,同样是一个蓝图,是一张图纸,只有函数模板确定了具体的类型后才会在代码段存储确定的函数。
- 当调用Swap函数的时候,会给函数传实参,编译器会根据实参的类型,并且参照函数模板的逻辑,生成具体的函数来适用
- 这个过程称为函数模板的实例化,只有实例化后的函数模板才是函数,才会存放在代码段,程序才能调用它。
如上图中,当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
显式实例化:在调用的函数名后的<>中指定模板参数的实际类型。
- 相加的俩个类型分别是int类型和double类型
- 在调用Add函数时,在函数名后的<>中加了int
- 得出的结果是21,说明此时是将double转化成了int类型后再相加的
- 也就指定了函数模板中模板参数,确定了实例化后的函数是int类型的数据相加。
此时我们就指定了函数模板实例化,也就是显示实例化,至于函数模板中的第二个形参为什么加const,本喵前面就讲解过,因为类型的强制转化过程中会产生临时变量,临时变量具有常性,所以需要const修饰,否则就放大了权限。
那么函数模板的原理是什么呢?我们上面不同类型的数据交换是怎么通过函数模板实现的呢?
直接以最开始的数据交换的代码为例,本喵带大家看看它的汇编代码:
这是使用交换函数模板进行的不同数据类型交换的汇编代码,这里为了方便讲解,本喵删除了打印语句的汇编代码。
- 第一个红色框中,交换的数据类型是俩个int类型,在调用Swap函数的时候是采用的隐式实例化,在汇编代码中,可以看到,调用了Swap函数,int是编译器根据实参推演出来的。
- 第二个红色框中,交换的数据类型是俩个double的,同样在调用Swap时候采用的隐式实例化的方法,在汇编代码中可以看到,调用了Swap函数,double是编译器根据实参推演出来的。
- 第三个红色框中,交换的数据类型是俩个char类型的,同样在调用Swap的时候采用隐式实例化的方法,在汇编代码中可以看到,调用了Swap函数,char是编译器根据实参推演出来的。
以上就是函数模板实例化的原理。
模板参数的匹配原则:
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
直接调用函数Add的时候,就会调用非模板函数,使用显示实例化调用模板函数的时候,就会调用模板函数,如上图中所示。
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
- 模板函数和非模板函数是同名的,当调用Add的时候,传入的参数是俩个int类型时,直接调用非模板函数,因为编译器也是懒狗,它也不想进行推演。
- 当调用Add时的俩个实参不全都是int类型的时候,此时就和非模板函数不匹配了,所以需要编译器进行推演,此时就会调用模板函数。
此时调用的是非模板函数,可以看到,实参中有一个是double类型的参数,但是在传参的过程中进行了强制类型转化,转化成了int类型。
此时调用的是模板函数,实参中一个是int类型,一个是double类型,而模板函数中的俩个形参只有一种类型T,在编译的时候报错了,因为模板函数不能进行类型转化,无论是将int转化成double函数将double转化成int都没有发生。
不要忘了,C++相比C语言的特性主要是自定义类型,也就是类对象,所以不仅有函数模板,还有类模板。
这是之前多次使用过的栈类型,我们可以通过红色框中的typedef语句灵活控制栈中的类型。
- 栈中插入的数据是int类型,就使用typedef int DataType语句。
- 栈中插入的数据是double类型,就使用typedef double DataType语句。
如果我想要栈s1是int类型,s2是double类型呢?
此时就不能通过修改typedef语句来兼顾俩种类型的栈了,此时就需要用类模板来实现了。
将这个类制作成一个模板就可以解决上面的问题了。
但是此时创建这俩类栈的时候却报了一堆错误。
类模板不能使用隐式实例化,必须使用显式实例化。
如上图中,在类模板的后面给定模板参数,此时类模板才会实例化成真正的类,并且我们也实现了s1和s2是俩个不同类型的栈。
同样,类模板也可以有多个模板参数,但是在实例化的时候都需要显式实例化。
类模板强烈不建议声明和定义分离!!!
下面本喵给大家演示一下分离的情况:
Stack.h头文件中的代码:
- 在每个模板成员函数的定义之前必须加template语句声明模板。
- 在每个模板成员函数的名字前使用Stack::来限定模板类域,因为此时的类不是真的类,而是模板类,所以必须带上模板参数。
test.cpp中的代码:
出现了链接错误,不是编译错误,说明我们的语法没有问题,而是在链接的时候出现了错误。
假设我们这个程序是在Linux系统上的,使用的是g++编译器,VS2019的原理和它的一样,只是文件后缀不同。
俩个源文件都经过预处理,编译,汇编,并且是相互独立的俩个过程,互不干扰,互不影响,在都生成.o目标文件以后进行链接生成.exe文件。
下面就来分析俩个文件的详细过程:
- 预处理阶段,Stack.cpp和test.cpp都包含类头文件,所以头文件在这俩个源文件中都会展开,头文件中的代码是类模板及模板成员函数的声明。
- 编译阶段,Stack.cpp中,将模板成员函数生成符号表,但是,模板类并不是真的类,只有实例化以后的模板类才是真的类,类成员函数才会放在代码段,并且将函数地址和名称放在符号表中,所以这一步由于没有模板参数,所以无法实例化,也就是在符号表中没有函数的地址,只有名字。
而在test.cpp中,在创建栈对象的时候,给了模板参数,也就此时可以进行实例化,但是此时没有模板成员函数的定义,所以形成的符号表中也是没有函数的地址,只有名字。- 汇编过程中,将俩个编译后的文件翻译成了机器码,并且合并了各自的符号表。
综上所述,在经过预处理,编译,汇编以后,Stack.cpp中的模板类,有声明,有定义,但是没有实例化,test.cpp中的模板类,有声明,没有定义,但是有实例化。
所以最后链接俩个目标文件的时候,由于无法在符号标中找到模板成员函数的地址,所以出现链接错误。
此时解决这个有俩个办法。
- 红色框中的语句就是在进行模板实例化。
- 俩个蓝色框中的写法都可以。
此时链接错误的问题就解决了。
由于在预处理阶段,头文件会被复制到源文件中,所以就将模板成员函数的定义和声明放在头文件中。
如上图所示,此时就将类模板的声明和定义放在了头文件中没有分离,并且解决了链接错误。
- 将头文件的后缀写成.hpp,表示这是Stack.h和Stack.cpp的合并,在以后见到这个后缀就可以肯定它是一个类模板。
- 在类中直接进行模板成员函数的定义,因为在预处理阶段会将其复制到test.cpp源文件中,此时test.cpp中有模板成员函数的定义,有实例化,而且是在一个文件中,所以就不需要再进行链接,也就不再会有链接错误,直接可以生成。
在写代码的时候,难免要和字符串打交道,在C语言的时候,我们只能通过字符串数组,一个元素一个元素的来访问字符串,不仅繁琐,而且细节也非常的多,稍不留神就会出错。
在C++中,标准库提供了字符串的类模板,我们在使用的时候就可以不用再考虑细节,而是直接用类模板实例化后的类来达到我们的目的,使编程的效率大大提高,只需要注重对象即可。
在学习模板的时候,库文件是我们必须查阅的,在标准库网站上,对这些模板都有详细的介绍,包括各种接口,功能等等。
这是标准库中的类模板basic_string,可以看到,它就是一个类模板,模板参数一个是字符编码的类型,另一个是一个缺省值,暂且不用管。
背景知识:
- 之前我们一直用的字符编码是ASCII码,它是只有一个字节大小的编码,而且只能表示键盘上的字符,大部分都是英文,是为了适用英文单词创造的。
- 我们的汉字并不像英文单词已经只是有26个不同的英文字母组合就可以形成各种单词,我们的汉字一个就是一个,是无法组合的,用偏旁组合的话非常复杂,所以就采用一个汉字对应一个编码,常用的汉字编码有GB2312,GBK等编码格式,它们的每一个编码即一个汉字占2个字节大小,所以可以容纳65535个汉字,几乎涵盖了所有常用的汉字。
- 为了融合世界各国语言,国际标准委员会又采用UTF-8,UTF-16,UTF-32等编码发生来涵盖万国语言,一个字的大小分别占1个字节,2个字节,4个字节。
现在字符串的类模板已经有了,需要确定的就是适用哪种编码类型来讲类模板实例化成类。
可以看到,字符串的类模板可以实例为上图所示的4种,每一种都是适用对应的模板参数实例化为对应的类,再用typedef将实例化后的类重命名。
- string就是1个字节编码的字符串类,wstring是2给字节编码的字符串类,u16string是2个字节编码的字符串类,u32string是4个字节编码的字符串类。
- 我们一般采用的是UTF-8编码格式,因为它不仅包含万国文字,而且还兼容ASCCII码,它的一个字符的大小是1个字节,所以适用的字符串类就是string。
对于上诉字符编码,有兴趣的小伙伴可以自行查阅资料了解一下。
在适用标准库提供的string类的时候,必须包含头文件string:
#include
标准库中的string类中有非常多的类方法来供我们使用,下面本喵就给大家介绍一些比较常用的成员函数。
在官方提供的库中,string类的默认成员函数实现了上诉几个,其中构造函数包括构造函数和拷贝构造函数,所以就是构造函数,拷贝构造函数,析构函数,赋值运算符重载函数四个。
构造函数:
这是官方库中已经实现了的构造函数重载,我们可以看到它们的参数是使用,文档中对每个接口函数的使用还有详细的解释。
(constructor)函数名称 | 功能说明 |
---|---|
string()(重点) | 构造空的string类对象,即空字符串 |
string(const char* s)(重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n字符c |
string(const string& s)(重点) | 拷贝构造函数 |
本喵在这里重点讲解这四个成员函数,这几个也是最常用的,需要我们不查文档也知道它们怎么用。
至于析构函数没有什么可讲解的,因为它不可以重载。
与容量相关的成员函数有上图所示的这么多,同样的,这是也是我们可以直接调用的。
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty(重点) | 检测字符串是否为空串,是返回true,否返回false |
clear(重点) | 清空有效字符 |
resereve(重点) | 为字符串预留空间 |
resize(重点) | 将有效字符的格式改成n个,多出的空间用可用指定字符填充 |
可以看到,使用成员函数size和length的结果是一样的。
- 最开始求字符串的有效长度只有length成员函数,返回的是不包含\0的字符个数,就和函数strlen一样。
- 在后来有了STL标准库模板以后,涉及到的类模板变多了,比如链表,树等,它们的有效数据个数就不能像strlen那样来求长度了,所以就使用了大小size来表示。
- 而标准库中的string为了和STL库保持统一,就又写了一个size函数,它的功能和length是一样的,我们之后也尽量使用size函数。
上图中,字符串的大小是11个字节,此时类对象的空间是15个,空间表示的是有效空间,就是不包括\0所在的那个空间。
- string类对象中存放字符串的空间是动态开辟的,当空间不够用的时候会发生扩容。
使用这样一段代码来向string类对象中插入1000个字符,来看扩容的过程:
- 在类对象创建的时候,一共开辟了16个动态空间,其中15个是有效空间,剩下那一个字节是用来存放\0的。
- 当存放够15个字符后就会扩容,第一次扩容后是最开始的2倍,也就是32个空间,其中31个是有效空间
- 第二次扩容后是之前的1.5倍,也就是48个空间,其中47给是有效空间。
我们可以看到,除了第一次扩容是2倍扩容,其他扩容都是按照之前的1.5倍来扩的,这是在VS2019环境下的扩容机制。
- 在Linux下使用g++编译器编译相同的代码
- 最开始类对象的容量是0
- 第一次只开辟了一共空间
- 第二次是之前的2倍,第三次又是之前的2倍
- 此时扩容是严格按照2的规律往下扩容的,这VS的机制不同。
虽然它们都会扩容,但是在不同平台下,使用的编译器不同,扩容的机制也就不同。
- 使用clear清空字符串以后,容量不会改变,只是将字符清除掉。
- 容量是否改变并不是绝对的,在VS2019不会改变,但保不准在其他平台上会将容量也清空。
- 在创建好类对象后,预留500个字节的空间,之后再插入1000个数据。
- 虽然起始空间大小不是500个,因为这里面还会存放其他东西,所以编译器自行多开辟了一些,不同编译器的表现是不同的。
- 可以明显的看到,此时扩容的次数就少了很多,系统的消耗也减少了很多。
所以说,如果知道字符串的大概长度,最好要先预留空间,以减少系统的开销。
- 字符串hello world的长度是11,类对象的空间大小是15,所以修改字符串的尺寸有三种情况:
- 0
- 11
- resize>15:如上第三个绿色框所示,会将字符串的长度调整到18个,由于没有指定用什么字符填充多出来的字符,所以默认用空格填充,又因为18个字节超出了原本的容量,所以会发生扩容,空间容量由15变成了18。
- 迭代器是指:行为上像指针一样的类型
- 目前可以认为它是string类中的内部类,类名是iterator
- 后面本喵会详细介绍它
从文档中可以看到,string类中有很多成员函数是专门用来配合迭代器(iterator)使用的。
- 创建迭代器对象it1时,必须使用string::iterator it1,来指名类型。
- 使用string中的类方法begin获得字符串的起始位置
- 使用string中的类方法end获得字符串的结束位置
- 将迭代器解引用可访问字符串中的特定元素
可以看到,迭代器的行为完全和指针一样,只是需要string提供对应的类方法来供它使用。
还有反向迭代器,起始位置是字符串的末尾,结束位置是字符串的开始。
- 反向迭代器的类型名是string::reverse_iterator
- 使用string中的类方法rbegin获得反向迭代的起始位置,也就是字符串的末尾。
- 使用string中的类方法rend获得反向迭代器的结束位置,也就是字符串的开始位置。
- 因为是反向迭代,所以对反向迭代器加1,迭代器就从末尾向开始移动一次。
- 当string类对象被const修饰时,该对象是不可以修改的
- 当用迭代器执行该对象中的字符串成员时,必须使用const修饰的迭代器,否则就和指针一样,会导致权限放大的问题。
- 当迭代器被const修饰后,此时获取字符串的起始位置和结束位置可以使用string中的类方法begin/end,或者是cbegin/cend都可以。
- 虽然begin/end获得的位置没有被const修饰,但是迭代器是被const修饰的,此时就是权限的缩小,是允许发生的。
- cbegin/cend获得的位置就是被const修饰的,而且迭代器也是被const修饰的,此时就是权限的平移。
可以看到,cbegin/cend是有点多余的,所以我们在使用的时候,几乎都是在使用begin和end。
此时的类型名是非常长的,auto关键字就此时就排上了用场,让编译器自己去推到它是什么类型。但是初学阶段不建议使用auto。
总的来说,迭代器有4种类型:
类型 | 功能 |
---|---|
正向 | 从开始到结尾遍历读写 |
反向 | 从结尾到开始遍历读写 |
const正向 | 从开始到结尾只读 |
const反向 | 从结尾到开始只写 |
标准库中重载了操作我[],使访问string中的字符串能像访问C语言中的字符串那样方便。
可以看到,此时对string类对象中的字符串就可以像C语言一样,使用[]来访问和操作。
还可以通过本喵上面讲解的迭代器来实现。
函数名 | 功能 |
---|---|
append | 追加字符串 |
operator+=(重点) | 追加字符串 |
push_back | 尾插 |
assign | 全部替换 |
insert | 指定位置插入 |
erase | 删除字符 |
replace | 代替指定字符 |
同样的,本喵指讲解这几个,其他用到的时候可以自行查阅文档。
上图是append的多个重载函数,因为用的不多,所以本喵就不介绍了。
这是库函数中+=的运算符重载定义,它实现的功能和append是一样的,但是它使用更多,可读性更好。
上图中,使用+=运算符重载,实现了在类对象字符串的后面追加一个string类对象,追加一个c字符串,和追加一个字符。
使用push_back只能一个字符一个字符的尾插,而+=也可以实现这样的目的,而且是一步到位。
- 用string类对象取代原来的字符串:在第一个红色框中,s2中的字符串都是x,并且size是大于s1的,但是用s1取代了s2中的x后,打印出来的结果就是s1的内容。
- 用c字符串取代原来的字符串:在第二个红色框中,s3中的字符串都是c,使用hello world!!字符串取代s3中的字符串后,上中就变成了c字符串
- 用多个相同字符取代原来的字符串:在地上红色框中,s4中的字符串被5个x取代,s4的打印结果就是只有5个x。
综上所述,我们发现,成员函数assign的取代规则是,将原本的字符串删除掉,再将新的字符串放进去。
- 第一个红色框中,在字符串my之前插入类对象s1.
- 第二个红色框中,在hello world!空格处插入c字符串“wxf”
- 第三个红色框中,在hello world!的叹号处插入5个字符‘x’
使用insert还能够实现头插,只有将指定位置的写出0即可,但是能不用则不用,因为这是一个时间复杂度是O(N2)的方法,效率比较低。
- 俩个参数都是缺省值,第一个是指定删除的位置,如果不传参的话,默认从最开始处删除。
- 第二个是删除字符个数,如果不写的话默认全部删除完。
这是缺省值npos的定义,它表示的是unsigned int类型的最大值。因为不会有这么大的字符串,所以它也代表着字符串的结尾。
- 第一个红色框中,将hello world!字符串从下标为5的位置(空格处)开始,删除5个字符( worl),打印出来的内容是剩下的内容。
- 第二个红色框中,没有给成员函数erase传任何参数,使用的是缺省值,默认就是将字符全部删除,所以打印出来的是空白。
在库中的函数声明有很多,但是我们常用的也就是以俩个。
- 第一个红色框中,在字符串hello world!中的下标为6处(‘w’)开始的5个字符(world)用类对象s2替换。
- 等二个红色框中,在字符串hello world!中的下标为6处(‘w’)开始的5个字符(world)用c字符串“shanghai”替换。
replace和assign的不同之处就在于,replace只会删除原本字符串中的指定字符,再将新字符串放进去,而assign会将原字符串中的全部字符删除,然后将新字符串放进去。
还有一些其他操作,如上图所示,本喵仍然是挑几个常用的来演示讲解。
函数名 | 功能 |
---|---|
c_str | 将string类对象字符串转换成c字符串 |
find(重点) | 向后查找字符或字符串 |
rfind | 向前查找字符或字符串 |
substr | 得到字符串中的字串 |
- 将一个文件名字符串存在了类对象file中。
- 要打开这个文件时,C语言中的文件操作并没有针对自定义类型的任何操作,所以需要将file类对象中的字符串转换成c字符串。
- 如图中红色框,使用c_str成员函数后,就得到了c字符串
- 后面就读取文件中内容的操作,可以看到读取出的内容就是我们当前的代码。
这是成员函数find的函数声明。同样挑几个常用的来演示介绍。
- 都是从字符串hello world中查找
- 第一个红色框中,在该字符串中查找类对象s2,默认从下标为0出开始查找,最后返回的结果是6,也就是说s2中的字符串存在,并且从下标为6出开始的。
- 第二个红色框中,在该字符串中查找c字符串“world”,默认从下标为0处开始查找,最后返回的结果也是6,同样表明world字符串在该字符串下标为6处开始。
- 第三个红色框中,在该字符串中查找字符‘o’,由于hello和world都有o,指定从下标为5处开始查起,此时就会在在world中查找字符‘o’,返回的结果是7,说在该字符串下标为7的位置处有一个字符‘o’
- 第四个红色框中,在该字符中查找字符串“wxf”,显然是找不到的,返回结果是4294967295,这个数字就是表示字符串最大容量,就是我们前面看的成员变量std::nops。
这是函数rfind的声明,它的用法和find一样,只是查找顺序是从指定位置向前查找。
- 在hello和word中都含有字符‘o’,并且查找的位置是下标为5的地方(空格处),返回的结果是4。
- 下标为4的位置是hello中的字符‘o’所在的下标,所以此时的查找是从指定位置(空格处)往前查找的。
这是substr的函数声明,作用就是从指定位置处,获取指定长度的子串。
- 第一个红色框中,没有传参数,所以默认从下标为0处开始,取整个字符串。
- 第二个红色框中,指定位置为下标为6的地方(‘w’)开始,长度5个字节的内容作为子串,所以打印出来的就是字符串world。
- 在红色框中,使用rfind从后向前查找第一个‘.’的下标,这个‘.’之后的内容就是后缀
- 当下标不等于string::npos时,说明找到了
- 再使用substr函数,从找到的’.'的下标处开始,向后的全部内容就是后缀。
本喵上面讲解的函数都是string类中的成员函数,除了成员函数还有一些非成员函数,如上图所示,<<和>>运算符重载在学习类和对象的时候已经实现过了,这里就不再讲解。而+运算符重载用的也很少,所以这里本喵只介绍一下getline函数。
这是getline函数的声明,它的作用就是将一行字符串提取出来。
在将字符串hello world输入并保存到s1中后,打印出来的s1中只有hello,这是因为cin和scanf一样,在输入字符串的时候,当遇到空格或者换行就会停止提取,认为空格或者换行就是字符串的分隔符,所以这里s1中只有hello而没有world。
使用getline时,一个实参是cin,是一个istream类型的对象,表示输入,另一个是string类对象,是存储输入字符串的类对象,如上图红色框中所示。此时就将输入的一行字符串全部存入了类对象s1中。
所以说,为了获取完整的一行数据,并且存入string类对象中,要使用标准库中的getline函数。
这篇文章主要是在讲解C++标准库中常见string类成员函数的使用方法,上面本喵介绍到的成员函数,要做到不查看文档就知道怎么用,你会发现你不再想用C语言区写代码。而那些不常用的成员函数,根据需求要能够自己查阅文档来使用。