目录
3、C++标准库中string类的类体中已显示实现的常见的类成员函数接口
3.4、C++标准库中的string类所实例化出的对象的访问及遍历操作
简单了解 C++ 标准库中的string类
C++标准库中的string类出现的时间比STL容器还要早,存在于C++的标准库中,严格来说它并不属于STL容器,但是它和STL容器有很多相似的操作,因此我们把它和其他STL容器放到一起学习、
1、为什么学习 C++ 标准库中的 string 类?
1.1、C 语言中的字符串
在C语言中,常量字符串是以字符 '\0' 结尾的一些字符的集合,为了操作方便,C语言标准库中提供了一些字符串系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问,所以在C语言中存在许多使用字符串不方便的地方,因此在C++标准库中提供了一个自定义的类模板,即:basic_string ,可以通过该自定义的类模板进行实例化出具有特定类型(如:char,char16_t,char32_t,wchar_t )的具体的类来管理字符串,在此我们只讨论常用的C++标准库中的string类,在OJ中,有关字符串的题目基本以C++标准库中的string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用C++标准库中的string类,很少有人去使用C语言标准库中的操作字符串的库函数,当使用C++标准库中的string类的时候,需要包头文件:<string>,以及引入命名空间:using namespace std;
2、C++ 标准库中的 string 类
1、标准的字符串类,即:C++标准库中的 string 类,提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性、2、C++标准库中的 string 类是C++标准库中的 basic_string 自定义的类模板的一个实例,它使用 char 类型来实例化 basic_string 自定义的类模板,并用默认的 char_traits 和默认的分配器类型 allocator 作为C++标准库中的 basic_string 自定义的类模板的默认参数、3、注意:这个C++标准库中的 string 类独立于所使用的编码来处理字节:如果用来处理多字节字符或变长字符(如UTF-8)的序列,这个C++标准库中的 string 类的类体中的所有的类成员变量:如长度或大小,以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作、4、知识点拓展:#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; namespace byte { class string { public: string(const char* str = "") { 1、 底层逻辑类似于下述代码,但不一定就是下面这种,后期还会进行完善、 //_size = strlen(str); //_capacity = _size; //_str = new char[_capacity + 1]; //strcpy(_str, str); //2、 _str = str; //权限不变、 //底层逻辑不会像这种把字符指针变量str直接赋值给某一个自定义string类型的对象中的类成员变量 _str, //否则,该自定义string类型的对象中的类成员变量 _str 就指向了一个最常规的常量字符串,而常量字符串 //不支持增删改等功能,不满足要求、 } //对于C++标准库中的string类的类体中的其他的已经显式实现的构造函数(包括拷贝构造函数)也是类似的情况、 private: 1、 //char* _str; //2、 const char* _str; size_t _size; size_t _capacity; }; } int main() { byte::string s("hello"); return 0; }
C++标准库中的 string 类实际上是使用自定义的类模板 basic_string ,实例化出来的具有特定类型(字符char类型)的一个具体的类,此时可以直接使用 basic_string<char> 作为自定义类型来实例化出自定义类型的对象,但是此时经过重命名 typedef 操作,将 basic_string<char> 重命名为 string,所以,那么此时则可以直接使用 string 作为自定义类型来实例化出自定义类型的对象,如下所示:
typedef basic_string<char> string;
//C++标准库中的string类不能操作 多字节字符或者变长字符 的序列、
除此之外,还可以通过使用自定义的类模板 basic_string ,实例化出来具有其他的特定类型(如:char16_t,char32_t,宽字符 wchar_t 类型)的具体的类,然后分别进行重命名 typedef 操作,具体如下所示:
typedef basic_string<char16_t> u16string; //C++11
typedef basic_string<char32_t> u32string; //C++11
typedef basic_string<wchar_t> wstring;
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
cout << sizeof(char) << endl; //1 byte、
cout << sizeof(char16_t) << endl; //2 byte、
cout << sizeof(char32_t) << endl; //4 byte、
cout << sizeof(wchar_t) << endl; //2 byte、
return 0;
}
为什么会这样分类呢,是因为涉及到编码的问题,编码分为:ASCII码表,unicode(utf-8,utf-16,utf-32)编码表(万国码),gbk编码表,由于计算机是美国发明的,所以在最初只需要在计算机上能够表示英文即可,而英文是由26个英文字母组成的,对于有符号的字符类型char,即可表示128种状态,对于无符号的字符类型char则可以表示256中状态,所以,26个英文字母加上其他的符号只需要一个字符类型char就可以全部表示,但是对于其他国家的文字而言,此处以中文为例,即使是无符号的字符char类型也没办法全部表示出来所有的汉字,所以要表示出所有的汉字,可能就需要使用到多个字符char类型,此时就引出了 gdk编码表,即中文的编码表,而 unicode编码表 则是针对于全世界的文字而设计的,对于要表示汉字而言,在Windows系统下一般常使用 gdk编码表 ,而在Linux系统下,默认一般常使用 utf-8 ,不管使用的是 gdk 编码表,还是 utf-8 ,两者都需要兼容 ASCII 码表,故英文和中文混用也是可以的,且两者对于常见的汉字来说,可以使用三个或四个字符char类型来表示,但是过于浪费,常见的汉字只需要两个字符char类型就可以表示出来,对于不常见的汉字,则需要三个或四个字符char类型才可以表示,所以,若不设计自定义的类模板 basic_string ,直接让 string 作为类名,把模板参数 T 直接设置为字符char类型 的话,那么此时,此时的 string类 只能很好的表示出英文,对于世界上其他国家的语言就没办法很好的用计算机进行表示,那么计算机就不会在全世界盛行了,其次,下图中显式的是字符 '?' 的原因是因为,此时的 ASCII码表 已经不再为 '惠','俊','明' 编写对应的符号了、
在 gdk编码表 和 utf-8 中,把读音类似的汉字编到了一起,存在一定的规律,而计算机内存中只有整型 int 类型的0和1,那么,整型 int 类型的0和1是怎么用来表示英文的呢,此时就引出了 ASCII 码表,字符是把他对应的十进制的整型 int 类型的数值以二进制的形式存入内存中的、
知识点拓展:
编码格式utf-32固定对应的是4byte,此时,编码格式utf-32足够表示出unicode中的所有的字符,但是在某些情况下会造成所占空间的浪费,为了改善空间效率,则引出了编码格式utf-8 ,该编码格式是针对于unicode编码表的可变长度的编码,不同于编码后长度固定位32bit位的编码格式utf-32,utf-8 针对于不同字符,编码后的长度可以是32bit,24bit,16bit,8bit,具体规则为:码点,即字符对应的十进制的整型int类型的数据在0-127范围的字符,直接映射为1byte长度的二进制数,让该字节由0开头,用来表示一个字符,unicode编码表的二进制码点会被分割成一个部分,填入utf-8编码的数字里,码点在128-2047范围的字符,映射为2byte的二进制数,编码格式utf-8为了解决计算机需要知道各个字符之间到底在哪里分割,则让二字节编码的第一个字节由110开头,表示自己和后面的一个字节是一起的,都在表示同一个字符,第二个字节由10开头,unicode编码表的二进制码点会被分割成两个部分,填入utf-8编码的数字里,码点在2048-65535范围的字符,映射为3byte的二进制数,第一个字节由1110开头,表示自己及后面的两个字节是一起的,都在表示同一个字符,后面两个字节都由10开头,unicode编码表的二进制码点会被分割成三个部分,填入utf-8编码的数字里,码点在65536-1114111范围的字符,映射为4byte的二进制数,第一个字节由11110开头,表示自己和后面的三个字节是一起的,后面三个字节都是由10开头,unicode编码表的二进制码点会被分割成四个部分,填入utf-8编码的数字里,编码格式utf-8兼容ASCII码,unicode编码表的前128个字符正好也是ASCII码表中的字符,码点一样,且把这些字符映射为1byte的长度,和ASCII码表编码相同,所以,把一个ASCII编码的英文文本直接使用编码格式utf-8编码进行读取是不会存在问题的,除此之外,编码格式utf-8节约空间,让unicode编码表中的码点小的字符也相应拥有更短的长度,比编码格式utf-32一视同仁的4byte会省下很多空间,通过前缀信息,也可以让计算机辨识出各个字符在内存中所占的总长度,解决分割不明的问题,编码格式utf-8目前是最主流的编码,大部分都是默认选择它,但有时还要注意该编码格式是否被支持或兼容,或者是是否是默认的编码、
//E:\天津工业大学\VS2013 #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //某个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间的底层逻辑就是使用 //顺序表进行实现的、 int main() { string path = "E:\\"; //Windows系统下的转义字符,前面的字符'\'对后面的字符'\'进行转义,若常量字符串是"\0",此时则是前面的字符'\'对后面的字符'0'进行转义,进而两者组成一个新的字符、 //此处属于隐式类型转换,先使用常量字符串"E:\\",自动调用构造函数构造出一个自定义string类型的临时对象(此处已完成该临时对象的定义) //然后再使用该已经定义完的自定义类型的临时对象去自动调用拷贝构造函数,此处在一个步骤中连续调用构造函数加拷贝构造函数,VS编译器(VS集成开发环境简称为vs编译器)将 //进行优化,则两步合成一步,只会自动调用构造函数,即使用常量字符串"E:\\"直接自动调用构造函数对自定义类型的对象path进行构造即可,则该行代 //码就等价于:string path("E:\\"); 具体的底层逻辑见模拟实现、 path += "天津工业大学\\"; //具体的底层逻辑见模拟实现、 //在C语言中,此处则应该使用strcat库函数,但是不太好,首先要找字符'\0',则时间复杂度就是O(n),还要保证原字符数组要有足够的空间,否 //则会报错,而此时的自定义类型的对象path中类成员变量所指向的在堆区上动态申请的内存空间若不够使用时,则会自动扩容,而由于string类 //中的类成员变量所指向的在堆区上动态开辟内存空间的底层实现原理是使用的顺序表,所以不需要利用O(n)的时间复杂度去找字符'\0'的位置, //直接就能知道他的所在位置,此处的运算符 += 是运算符重载,本质原理就是在常量字符串"E:\\"的字符'\0'处使用strcpy库函数把常量字符串 //"天津工业大学\\"都拷贝过去,包括该常量字符串中的字符'\0',并且该 += 运算符重载函数已经在C++标准库中的 string 类的类体中显式的 //进行了定义,故可以直接使用、 path += "VS2013"; cout << path << endl; //具体的底层逻辑见模拟实现、 //此处由于对象cout和path均是自定义类型的对象,故这里的运算符<<属于运算符重载,而<<运算符重载函数已经在C++标准库中的string类的类体 //外的全局区域中进行了显式的定义,故此处可以直接使用、 return 0; }
#define _CRT_SECURE_NO_WARNINGS 1 //vector就是顺序表,list就是链表、 #include<iostream> #include<string> //使用C++标准库中的string类则需要包头文件<string>,可以不写后缀 .h 或 .hpp ,按理说,这里涉及到了模板,故应该带上后缀 .hpp ,但即使这里涉及到了模板,也可以不写后缀 .hpp //因为,后缀只是一个标识,不重要,只是编译器会默认的去识别以 .h 和 .hpp 为后缀的头文件,除此之外,这里不加后缀 .h 的其他原因是因为在C语言中已经存在了一个头文件<string.h>, //为了避免我们混淆,在此处则可以不加后缀或加上 .hpp 后缀,由于后缀只是一个标识,并不重要,所以在理论上即使后缀写成 .cpp 也是可以的,但是实际上是不行的: //当在其他源文件test.cpp中包了该以 .cpp 为后缀的头文件后,该头文件也会在其他源文件test.cpp中被展开,此时已经在C++标准库中的string类的类体中显式实现的类成员函数的声明和定义都是 //在类体中的,故都会被编译器默认看做是内联函数,不管这些内联函数的定义是否被展开,那么他们定义处的函数名和地址都不会放进当前源文件所生成的目标文件的符号表中,当在test.cpp //源文件中调用这些类成员函数时,在该源文件中既有这些类成员函数的声明,也有这些类成员函数的定义,所以根本就不会去多个符号表中查找这些类成员函数的地址,并且在test.cpp源文件 //中调用这些类成员函数时,编译器能够在test.cpp源文件中找到这些类成员函数的声明和定义,虽然在以.cpp为后缀的头文件中也存在这些类成员函数的声明和定义,但由于他们不在同一个文件 //中,且test.cpp源文件和以.cpp为后缀的头文件也不会进行合并,所以不会出现重定义的问题,此时,test.cpp源文件中调用类成员函数时,使用的只是在该源文件中的类成员函数的声明和定义、 //对于C++标准库中的string类的类体外面的全局区域中还声明和定义了全局函数,这些全局函数不会被编译器默认看做是内联函数,故这些全局函数定义处的函数名和地址都会被放进当前所在的源文件 //生成的目标文件中的符号表里面,所以在这两个.cpp文件生成的目标文件中的符号表中都有这些全局函数的函数名和地址,所以C++标准库中的string类的类体外显式声明和定义的全局函数调用时在链 //接过程中就会出现重定义的问题,本质原因是因为,在链接过程中,多个目标文件会与链接库整体链接成可执行程序,可以看做把多个目标文件和链接库放在了同一个作用域中,此时就会出现重定义问题、 using namespace std; //因为,string类存在于C++标准库中,而C++标准库中的东西均包含在命名空间std中的,此处若不把命名空间std展开的话,则在使用C++标准库中的string类时,必须要指定类域为std:: 、 int main() { string st; return 0; }
关于常见的链接问题的原因分析:
1、
//Test.cpp源文件: #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class A { public: //类成员函数的声明和定义、 void Func(int a) { _a = a; } //类成员函数的声明和定义、 void Print() { cout << _a << endl; } //此处的两个类成员函数Func和Print的声明和定义均在类体中,所以会被默认为内联函数,那么不管这两个内联函数的定义是否被展开 //他们定义处的函数名和地址都不会放到由该Test.cpp源文件生成的.o目标文件中的符号表中,所以由该源文件生成的.o目标文件中的符号表 //中并不存在这两个类成员函数的函数名和地址,当在main函数中调用这两个类成员函数时,编译器会在当前源文件中向上查找,能够找到这两个类成员函数 //的声明,故目前为止可以成功编译,然后再去当前源文件中找这两个类成员函数的定义,又因,这两个类成员函数的定义也在该Test.cpp源文件中 //所以就能够成功运行程序,压根就不会去多个符号表中查找这两个类成员函数的函数名和地址、 //只有当函数的声明和定义分离(在不同文件中)时,且通过函数的声明已经成功编译,但在找函数定义时,在当前源文件中找不到函数 //的定义时,才会去多个符号表中查找该函数的函数名和地址,再通过该函数名和地址去找该函数的定义、 private: int _a; }; int main() { A d1; d1.Func(10); d1.Print(); return 0; }
2、
//1、 // A.h #pragma once #include<iostream> using namespace std; class A { public: //类成员函数的声明、 void Func(int a); //类成员函数的声明、 void Print(); //此处是两个类成员函数的声明,此时,这两个类成员函数的声明和定义是分离(在不同文件中)的,并没有把这两个类成员函数的 //声明和定义都放在类体中,故此处的这两个类成员函数并不会被默认为是内联函数、 private: int _a; }; //2、 // A.cpp #define _CRT_SECURE_NO_WARNINGS 1 #include"A.h" //当在此处包头文件后,此时当前A.cpp源文件中就存在了两个类成员函数的声明、 //类成员函数的定义、 void A::Func(int a) //指定类域、 { _a = a; } //类成员函数的定义、 void A::Print() //指定类域、 { cout << _a << endl; } //上述这是两个类成员函数的定义,由于这两个类成员函数都不会被默认为内联函数,故此处的这两个类成员函数的定义都不会被展开 //所以编译器会把此处的两个类成员函数定义处的函数名和地址放到当前源文件生成的目标文件中的符号表里面、 //当内联函数的定义在不展开时,不会把该内联函数定义处的函数名和地址放到当前所在的源文件生成的目标文件中的符号表中,除此之外,其他函数的定义在不展开时 //都会把该其他函数定义处的函数名和地址放到当前所在的源文件生成的目标文件中的符号表中,由于除了内联函数外的其他函数的定义都不会被展开,所以可以认为,只要 //不是内联函数的定义,那么编译器就会把该非内敛函数定义处的函数名和地址放到当前所在源文件生成的目标文件的符号表中、 //该源文件中有这两个类成员函数的声明,也有这两个类成员函数的定义、 //3、 //Test.cpp #include"A.h" int main() { A d1; d1.Func(10); d1.Print(); //当在该源文件中包含头文件后,那么该源文件中就存在了这两个类成员函数的声明,所以在此处的这两个类成员函数的调用处,编译器向上查找,能够找到这两个类成员函 //数的声明,就会编译成功,然后编译器会先从当前源文件中的找这两个类成员函数的定义,发现找不到,所以编译器才会去多个源文件生成的.o目标文件中的符号表中查找 //这两个类成员函数的函数名和地址,再通过该函数名和地址去找两个类成员函数的定义,由于该源文件中只有两个类成员函数的声明,并不存在这两个类成员函数不被展开的定义,所以该源 //文件生成的目标文件中的符号表中不存在这两个类成员函数的函数名和地址,但在A.cpp源文件生成的目标文件中的符号表中能够找到这两个类成员函数的函数名和地址,再通过该函数名和地址去找 //两个类成员函数的定义所以链接成功,能够正常运行程序、 return 0; }
3、
//1、 //A.h #pragma once #include<iostream> using namespace std; class A { public: //类成员函数的声明、 inline void Func(int a); //类成员函数的声明、 inline void Print(); //此处是两个类成员函数的声明,此时,这两个类成员函数的声明和定义是分离(在不同文件中)的,并没有把这两个类成员函数的 //声明和定义都放在类体中,故编译器不会把这两个类成员函数默认看成内联函数,但由于显式的指定这两个类成员函数是内联函数 //所以此时这两个类成员函数就是内联函数了、 private: int _a; }; //2、 //A.cpp #define _CRT_SECURE_NO_WARNINGS 1 #include"A.h" //当在此处包头文件后,此时当前A.cpp源文件中就存在了两个类成员函数的声明、 //类成员函数的定义、 void A::Func(int a) //指定类域、 { _a = a; } //类成员函数的定义、 void A::Print() //指定类域、 { cout << _a << endl; } //在上面包头文件之后,此时当前A.cpp源文件中就存在了两个类成员函数的声明,并且告诉编译器这两个类成员函数都是内联函数,那么不管这两个内联函数的定义是否被展开 //他们定义处的函数名和地址都不会放到由该A.cpp源文件生成的.o目标文件中的符号表中、 //该源文件中有这两个类成员函数的声明,也有这两个类成员函数的定义,但是当前源文件生成的目标文件中不存在这两个类成员函数的函数名和地址、 //3、 //Test.cpp #include"A.h" int main() { A d1; d1.Func(10); d1.Print(); //当在该源文件中包含头文件后,那么该源文件中就存在了这两个类成员函数的声明,所以在此处的这两个类成员函数的调用处,编译器向上查找,能够找到这两个类成员函 //数的声明,就会编译成功,然后编译器会先从当前源文件中的找这两个类成员函数的定义,发现找不到,所以编译器才会去多个源文件生成的.o目标文件中的符号表中查找 //这两个类成员函数的函数名和地址,再通过该地址去找两个类成员函数的定义,由于该源文件中只有两个类成员函数的声明,没有这两个类成员函数的定义,即使存在这两个类成员函数的定义 //那么在该源文件生成的目标文件中也不存在这两个类成员函数定义处的函数名和地址,因为,这两个类成员函数都是内联函数,所以不管这两个类成员函数的定义是否被展开,那么 //这两个类成员函数定义处的函数名和地址都不会被放到该源文件生成的目标文件的符号表中,故该源文件生成的目标文件中的符号表中不存在这两个类成员函数的函数名和地址, //在A.cpp源文件生成的目标文件中的符号表中也找不到这两个类成员函数的函数名和地址,所以就不找不到这两个类成员函数的定义,故此时编译器会报出两个链接错误、 return 0; }
3、C++标准库中string类的类体中已显示实现的常见的类成员函数接口
注意:
C++标准库中的string类的类体中对常见的类成员函数接口均已经进行了显式的实现,我们可以直接使用,不考虑默认类成员函数的概念,对于C++标准库中的string类整体而言,在其类体中大概已经显式的实现了120多个类成员函数,在其类体外的全局区域中也显式的实现了一些全局函数,特别多,但这并不是说我们都要全部学习,只需要掌握比较重要的即可,其他的不需要掌握,需要用的时候来查一下用法即可,不仅如此,当我们在后期学习 vector 类模板和 list 类模板等等STL容器的时候,也会像这里的C++标准库中的string类一样,只需要掌握一些常用的函数接口即可,不需要全部学习,STL是C++标准库的一部分,STL是数据结构和算法的库、
3.1、构造函数(包括拷贝构造函数)
注意:
上述这些C++98或C++11版本下的构造函数,在C++标准库中的string类的类体中均已经显式的实现,他们均能够构成函数重载,此时我们不考虑编译器自动生成的无参的默认构造函数和编译器自动生成的拷贝构造函数,由于已经显式的在C++标准库中的string类的类体中实现了构造函数(默认或非默认)和拷贝构造函数,故编译器不会再自动生成无参的默认构造函数和拷贝构造函数、
C++标准库中的string类的类体中常用的已经显式实现的构造函数的函数接口,具体如下所示:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //C++标准库中的string类是用来管理动态增长的字符数组,底层逻辑使用的就是顺序表,这个字符串以字符 '\0' 结束以此来兼容C语言、 //C++标准库中的: string类和STL容器vector的底层实现逻辑都是顺序表、 int main() { //1、 string st1; //具体的底层逻辑见模拟实现、 cout << st1 << endl; string st(""); //等价于:string st("\0"); cout << st << endl; //此时打印出来也是一个空白,不存在有效字符,上述两者打印出来的结果都是一样的,但是一个传参,一个不传参,是存在区别的,前者调用 //C++标准库中string类类体中已经显式实现的string()构造函数接口,而后者则是调用C++标准库中string类类体中已经显式实现的string(const char*)构造函数接口、 //2、 string st2("hello world"); //在自定义类型string类型的对象st2中,C++标准库中string类类体中的类成员变量指向了一块使用操作符new或者是malloc等库函数在堆区上动态开辟的内存空间, //然后再把这里的常量字符串,包括字符'\0',都拷贝到该堆区上的内存空间中,底层逻辑是顺序表,当容量不够时,可以自动进行扩容操作、 //具体的底层逻辑见模拟实现、 st2 += "!!!!!"; cout << st2 << endl; //3、 //自动调用C++标准库中string类类体中的已经显式定义的拷贝构造函数,且已经完成了深拷贝、 string st3(st2); cout << st3 << endl; string st4 = st2; cout << st4 << endl; //具体的底层逻辑见模拟实现、 return 0; }
C++标准库中的string类类体中不太常用的已经显式实现的构造函数的函数接口,具体如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
//1、string (const char* s, size_t n);
//使用最常规的常量字符串中的前n个字符来构造自定义string类型的对象、
string st1("AAAAABBBBBB", 5);
cout << st1 << endl; //AAAAA
//会使用即可,具体底层逻辑与其模拟实现不需要掌握,不太常用,和C++标准库中的string类类体中的已经显式实现的
//构造函数的函数接口 string (const char* s); 的底层逻辑和模拟实现存在一定区别,具体的我们不需要掌握、
//2、string (size_t n, char c);
//使用 n个 char类型的字符变量来构造自定义string类型的对象、
string st2(10, 'x');
cout << st2 << endl; //xxxxxxxxxx
//会使用即可,具体底层逻辑与其模拟实现不需要掌握,不太常用、
//3、string (const string& str, size_t pos, size_t len = npos);
//半缺省,其中,npos是自定义string类型的对象str中的静态类成员变量,static const size_t npos = -1; 由于静态类成员变量 npos 的类型是size_t,故其是一个非常大的值,
//那么形参变量 len 也是一个非常大的值(正数),所以若形参变量 len 不显示给出的话,则默认是从自定义string类型的对象str中的类成员变量所指向的在堆区上动态开辟的内
//存空间中所存储的的最常规的常量字符串中的pos位置开始,往后取42亿多个字符来构造这里的自定义string类型的对象st4,由于自定义string类型的对象str中的类成员变量所
//指向的在堆区上动态开辟的内存空间中存储的最常规的常量字符串的有效长度一定小于42亿多,所以此时就相当于直接从该最常规的字符串pos位置开始,往后一直取,直到把该最
//常规的常量字符串中的有效字符全部取完,不包括该最常规的常量字符串末尾位置隐藏的无效字符'\0',再用取出来的这些字符去构造这里的自定义类型string类型的对象st4,具
//体的底层逻辑不用考虑,会使用即可、
//使用自定义string类型的对象str中的类成员变量所指向的在堆区上动态开辟的内存空间中存储的最常规的常量字符串,从其pos位置开始,往后取len长度个字符来构造这里的自定
//义string类型的对象、
string st3(st1, 0, 3);
cout << st3 << endl;//AAA
//会使用即可,具体底层逻辑与其模拟实现不需要掌握,不太常用、
string st4(st1, 0);
cout << st4 << endl;//AAAAA
//会使用即可,具体底层逻辑与其模拟实现不需要掌握,不太常用、
//5、迭代器区间初始化、
//template <class InputIterator>
//string(InputIterator first, InputIterator last);
//具体在后面迭代器讲完之后再进行阐述、
return 0;
}
3.2、析构函数
在C++标准库中的string类的类体中已经显式的实现了析构函数(类成员函数)的函数接口,由于编译器会在自定义string类型的对象的生命周期结束时自动调用C++标准库中的string类的类体中已经显式实现的(类成员函数)析构函数,所以我们不需要考虑它,此时,C++标准库中的string类的类体中必须要显式的实现析构函数(类成员函数),并其在该析构函数(类成员函数)的函数体内对内置类型(char*类型)的类成员变量所指向的在堆区上动态开辟的内存空间进行资源的清理,不能使用编译器自动生成的析构函数,因为它不会对内置类型进行资源清理,此时我们需要对内置类型的类成员变量所指向的在堆区上动态开辟的内存空间进行资源的清理、
由于在我们自己定义的string类的类体中(非C++标准库中的string类的类体中)的类成员变量指向了在堆区上动态开辟的内存空间,并且该类成员变量还是内置类型(char*类型),所以当我们在模拟实现C++标准库中的string类的类体中已经显式实现的析构函数(类成员函数)时,必须要在我们自己定义的string类的类体中(非C++标准库中的string类的类体中)显式的实现析构函数(类成员函数)用于资源的清理,不能使用编译器自动生成的析构函数,因为它不会对内置类型进行资源清理,此时我们需要对内置类型的类成员变量所指向的在堆区上动态开辟的内存空间进行资源的清理、
3.3、赋值运算符重载函数
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; int main() { string s1("hello"); string s2("world"); cout << s1 << endl; cout << s2 << endl; //自动调用C++标准库中的string类的类体中已经显式实现的赋值运算符重载函数(类成员函数)、 //1、 s1 = s2; //使用一个已经定义好的自定义类型的对象去对另外一个已存在的同类型的自定义类型的对象进行赋值操作、 cout << s1 << endl; //具体的底层逻辑见模拟实现、 //2、 s1 = "hello world"; //使用常量字符串去对一个已经存在的自定义类型的对象进行赋值、 cout << s1 << endl; //底层逻辑和模拟实现不需要掌握,会使用即可、 //3、 s1 = 'A'; //使用一个字符去对一个已经存在的自定义类型的对象进行赋值、 cout << s1 << endl; //底层逻辑和模拟实现不需要掌握,会使用即可、 return 0; }
3.4、C++标准库中的string类所实例化出的对象的访问及遍历操作
所谓C++标准库中的string类所实例化出的对象的访问及遍历操作,指的就是对该自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串的访问和遍历操作,具体方法如下所示:
3.4.1、方法一:下标+运算符[ ]的运算符重载 、
注意:
在C语言和C++中,对于运算符[ ]而言,若是该运算符[ ]的两个操作数均为内置类型的话,则可以直接使用该运算符[ ],本质上就是通过下标进行访问或修改操作,就是在进行解引用一类的操作,但若运算符[ ]的两个操作数中出现了自定义类型(此处为自定义的string类型)的对象且想要对该C++标准库中的自定义的string类类型的对象对应的自定义的类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个字符进行访问和修改操作的话,那么则不可以直接使用该运算符[ ],就必须进行 [ ] 运算符的重载,在C++标准库中的string类的类体中,已经显式的实现了运算符[ ]的运算符重载函数,我们直接使用即可,即,可以通调过用该[ ]运算符重载函数来获得C++标准库中的string类类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个字符的别名、
const char* p="hello world"; p[1]; //*(p+1); 要保证字符指针变量p所指向的是一块连续的内存空间、
在C++标准库中的string类的类体中已经显式的实现了运算符 [ ] 的运算符重载函数,具体如下所示:
1、char& operator[] (size_t pos); 2、const char& operator[] (size_t pos) const; //两者能够构成函数重载、 //此处返回的是C++标准库中的string类的类体中的类成员变量指向的在堆区上动态开辟的内存空间中所存储的字符串中某个字符变量的别名,由于该字符变量所占的内存空间是在堆区上动态开辟的,此处出来这两个在C++标准库中的string类的类体中已经显式实现的两个类成员函数的函数体后,该字符变量所占的在堆区上动态开辟的内存空间还没有被释放,故,在堆区上动态开辟的内存空间中所存储的某个字符变量的生命周期还没结束,直到自定义string类型的对象在生命周期结束时,编译器会自动调用析构函数才会释放在堆区上动态开辟的内存空间,那么此时在堆区上动态开辟的内存空间中所存储的某个字符变量的生命周期才结束,所以此处可以使用传引用返回、
上述1,2中均属于内置类型传引用返回,那么这里为什么要使用传引用返回呢?
1、
在C++中,自定义类型的对象在进行传值返回和传值传参时,均会自动调用该自定义类型的对象对应的自定义的类的类体中的拷贝构造函数,上述即使是传值返回,即,即使是内置类型传值返回时,也不会自动调用拷贝构造函数,按理说上述使用内置类型传值返回也是可以的,但内置类型传值返回避免不了会存在一定的拷贝过程,若使用内置类型传引用返回时,就能够避免这个影响不大的拷贝过程,则此处使用内置类型传引用返回比使用内置类型传值返回效率要高,虽然效果不是很明显,但也会存在一定的优势、
2、
上述的内置类型返回时使用传引用返回最重要的目的就是为了得到C++标准库中的string类类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个字符的别名,这样在我们需要对该原字符串中的该字符进行修改时,则可以直接对该字符的别名进行修改操作即可,此时就需要调用C++标准库中的string类的类体中的已经显式实现的运算符 [ ] 的运算符重载函数的接口:char& operator[ ] (size_t pos); 即可,此时可以对该原字符串中的该字符进行可读可写的操作,若想对该原字符串中的该字符进行可读不可写操作的话,只需要调用C++标准库中的string类的类体中的已经显式实现的运算符 [ ] 的运算符重载函数的接口:const char& operator[ ] (size_t pos) const; 即可,若上述1和2中均使用内置类型传值返回的话,那么返回的则是该原字符串中的某一个字符的临时拷贝,此时,通过修改该字符的临时拷贝是不能修改该原字符串中的该字符的、
拓展:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<assert.h> #include<string> using namespace std; namespace byte { class string { public: string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); } char& operator[](size_t pos) { //1、 assert(pos < _size); return _str[pos]; 2、 //assert(pos <= _size); //return _str[pos]; C++标准库中的string类的类体中已经显式实现的类成员函数的函数接口: char& operator[](size_t pos) 的 函数体中的底层逻辑应是这样、 } const char& operator[](size_t pos)const { //1、 assert(pos < _size); return _str[pos]; 2、 //assert(pos <= _size); //return _str[pos]; C++标准库中的string类的类体中已经显式实现的类成员函数的函数接口: const char& operator[](size_t pos)const 的 函数体中的底层逻辑应是这样、 } private: char* _str; size_t _size; size_t _capacity; }; } int main() { //1、 byte::string s("hello"); cout << s[4] << endl; //此处使用的并不是C++标准库中的string类,而使用的是自己模拟实现在命名空间byte中的string类、 //默认如果访问到该自定义string类型的对象s中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的最常规的常量字符串 //中末尾位置隐藏的无效字符 '\0'时,也属于越界行为,虽然本质上并没有越界、 //2、 string s1("hello"); cout << s1[5] << endl; //此处使用的是C++标准库中的string类、 //若使用C++标准库中的string类来实例化出对象s1,则默认访问到该自定义string类型的对象s1中的类成员变量所指向的在堆区上动态 //开辟的内存空间中所存储的最常规的常量字符串中末尾位置隐藏的无效字符 '\0'时,不属于越界行为,本质上就没有越界、 return 0; } //在C语言和C++中,对于运算符[]而言,若是该运算符[]的两个操作数均为内置类型的话,则可以直接使用该运算符[],本质上就是通过下标进行访问或修改操作, //就是在进行解引用一类的操作,此过程中,不管是对下标所对应位置的元素是读还是写,对于越界的行为都是抽查,并不一定每次都会报错,但不报错也不代表他 //是正确的,这本质上是通过下标访问或修改数组中的元素、 //但若运算符[]的两个操作数中出现了自定义类型(此处为自定义的string类型,包括C++标准库中的string类和我们模拟实现在byte命名空间中的string类)的对象 //则此时必须要进行运算符[]的运算符重载,在使用过程中属于调用C++标准库中的string类或我们模拟实现在byte命名空间中的string类的类体中的已经显式实现 //的类成员函数的函数接口,由于该类成员函数的函数接口中会进行断言判断,所以,当我们在使用运算符[]的运算符重载时,不管是读还是写,只要发生越界,即,只要 //不满足断言判断语句,就一定会报错,一定能检查出来、
注意:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; int main() { string s1("hello world"); //具体的底层逻辑见模拟实现、 //s1.operator[](&s1,0); cout << s1[0] << endl; //h //具体的底层逻辑见模拟实现、 s1[0] = 'A'; //s1.operator[](&s1,0); cout << s1[0] << endl; //A //在C++标准库中的string类的类体中的已经显式的实现了两个运算符[]的运算符重载函数,分别是: //1、char& operator[] (size_t pos); 即, char& operator[] (string* const this,size_t pos); //2、const char& operator[] (size_t pos) const;,即:const char& operator[] (const string* const this,size_t pos); //而此处的 s1.operator[](&s1,0); 调用的就是在C++标准库中的string类的类体中的已经显式实现的第一个运算符[]的运算符重载函数, //原因是: 自定义string类型的对象s1的地址的类型为: string* ,调用在C++标准库中的string类的类体中的已经显式实现的第一个运算 //符[]的运算符重载函数,则属于权限不变,调用在C++标准库中的string类的类体中的已经显式实现的第二个运算符[]的运算符重载函数, //则属于权限缩小,按理说,此处 s1.operator[](&s1,0); 调用C++标准库中的string类的类体中的已经显式的实现了两个运算符[]的运算 //符重载函数均是可以的,但编译器会自动调用在C++标准库中的string类的类体中的已经显式的实现了两个运算符[]的运算符重载函数中 //最合适(权限不变)的那个运算符[]的运算符重载函数,所以,此处的 s1.operator[](&s1,0); 调用的就是在C++标准库中的string类的类 //体中的已经显式实现的第一个运算符[]的运算符重载函数:char& operator[] (size_t pos); 即, char& operator[] (string* const this,size_t pos); //其返回类型为char&,所以,我们对C++中的string类类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个字符的权限是 //可读可写的、 //若上述调用的在C++标准库中的string类的类体中的已经显式实现的第一个运算符[]的运算符重载函数的返回类型为char类型,即,内置类型传值返回,会进行一定的拷贝步骤 //返回的则是一个临时的字符变量,又由于临时变量具有常属性,即,const char 类型,所以,返回出来的该临时字符变量是不能对其进行修改的,其次,就算能够修改,修改的也是 //该临时字符变量,并不能对该临时字符变量对应在C++中的string类类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的与该临时字符变量对应的字符变量进行修改, const string s2("hello world"); //具体的底层逻辑见模拟实现、 //s2.operator[](&s2,0); cout << s2[0] << endl; //h //具体的底层逻辑见模拟实现、 //s2[0] = 'A'; //s2.operator[](&s2,0); 报错,表达式必须是可修改的左值、 cout << s2[0] << endl; //在C++标准库中的string类的类体中的已经显式的实现了两个运算符[]的运算符重载函数,分别是: //1、char& operator[] (size_t pos); 即, char& operator[] (string* const this,size_t pos); //2、const char& operator[] (size_t pos) const;,即:const char& operator[] (const string* const this,size_t pos); //而此处的 s2.operator[](&s2,0); 调用的就是在C++标准库中的string类的类体中的已经显式实现的第二个运算符[]的运算符重载函数, //原因是: 自定义string类型的对象s2的地址的类型为: const string* ,调用在C++标准库中的string类的类体中的已经显式实现的第一个运算 //符[]的运算符重载函数,则属于权限放大,则编译错误,调用在C++标准库中的string类的类体中的已经显式实现的第二个运算符[]的运算符重载函数, //则属于权限不变,所以,此处的 s2.operator[](&s2,0); 调用的就是在C++标准库中的string类的类体中的已经显式实现的第二个运算符[]的运算符重 //载函数:const char& operator[] (size_t pos) const;,即:const char& operator[] (const string* const this,size_t pos);其返回类型为const //char&,所以,我们对C++中的string类类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个字符的权限是只可读但不可写、 return 0; }
在C++标准库中的string类的类体中已经显式的实现了计算C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的常量字符串的有效长度,这是因为通常在任何情况下我们都只考虑最常规的常量字符串,对于该字符串末尾的字符 '\0' ,不算入该字符串所求长度中,具体如下所示:
size_t size() const; // size_t size(const string* const this);
此时,左边的关键字const放在了 * 的左边,修饰的是自定义string类型的指针变量this所指向的内容不能被修改,又因自定义string类型的指针变量this指向了某一个自定义string类型的对象,故该自定义string类型的对象中的数据均不能被修改,还可以理解为,由于该size类成员函数接口的函数体中并没有修改该size类成员函数所在的类体中的类成员变量,故常常加上关键字const,即,对于类成员函数而言,若该类成员函数的函数体中不对该类成员函数所在的类的类体中的类成员变量进行改变的话,最好都在该类成员函数的形参列表后面加上关键字const、
在C++标准库中的string类的类体中还显式的定义了一个类成员函数 length,它的功能和C++标准库中string类的类体中已经显式定义的类成员函数 size 的功能一模一样,这是因为,由于C++标准库中的string类的出现比STL要早,在早期,如果计算C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串的有效长度时,通常使用的是在C++标准库中的string类的类体中已经显式定义的类成员函数 length,直到STL出现后,STL容器均在其对应的类的类体中显式的实现了类成员函数 size ,虽然C++标准库中的string类在严格上来说并不属于STL容器,但由于他们在很多方便的操作都比较类似,所以为了统一,又在C++标准库中的string类的类体中显式的实现了一个类成员函数 size,要注意,C++标准库中的string类的类体中已经显式实现的类成员函数size和length的功能是一模一样的,底层实现原理完全相同,没有任何区别、
size_t length() const; //size_t length(const string* const this);
方式一:下标+运算符[ ]的运算符重载 、
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //方式一: 下标加[] 、 最方便,最便捷,最常用的方法、 int main() { string st("hello world"); cout << st.size() << endl; for (size_t i = 0; i < st.size(); i++) { cout << st[i] << " "; // st.operator[](&st,i); 运算符[]的运算符重载、 } cout << endl; return 0; //具体的底层逻辑见模拟实现、 }
3.4.2、方法二:迭代器 、
迭代器是STL六大组件中的其中一个组件,迭代器是用来访问或遍历这些STL容器(数据结构)的,同时也可以对C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串进行访问或遍历操作,像STL容器(数据结构):list (链表)和vector (顺序表) 等,均存在迭代器,对于STL容器中的 list,只能使用迭代器或者范围for的方法进行访问和遍历操作,这是因为,STL容器中的 list,也即数据结构中的链表,由于其物理内存不是连续的,故不能使用下标加运算符[ ]的运算符重载的方法对 list(链表) 进行访问或遍历操作,只能使用迭代器或范围for的方式进行访问或遍历操作、
对于部分STL容器(部分数据结构)而言,若其物理内存不连续,则只能使用迭代器或者范围for的方法进行访问和遍历操作,不能使用下标加运算符[ ]的运算符重载的方法对这些STL容器(这些数据结构),进行访问或遍历操作,使用迭代器或者范围for的方法进行访问和遍历操作,这种方法是通用的,不管物理内存是否连续,都可以使用这种方法,是访问和遍历所有STL容器和自定义string类型的对象的利器、
还要知道,对于C++标准库中的string类属于自定义的类,则使用该自定义的类实例化出来的对象属于自定义类型的对象,同理,对于STL中的容器所对应的在C++标准库中的类,也属于自定义的类,故使用这些自定义的类实例化出来的对象,也都是自定义类型的对象、
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //方式二: 迭代器 、 int main() { //正向迭代器、 //具体的底层逻辑见模拟实现、 string st("hello world"); string::iterator iter = st.begin(); //st.begin(&st); //st.begin(&st);调用的就是在C++标准库中的string类的类体中已经显式实现的类成员函数接口: iterator begin(); 这属于权限不变,若调用 //在C++标准库中的string类的类体中已经显式实现的类成员函数接口:const_iterator begin() const;的话,这属于权限缩小,按理说也是可以的, //但由于在C++标准库中的string类的类体中这两个类成员函数接口同时存在,故编译器会优先调用最合适,即优先调用权限不变的类成员函数接口, //该类成员函数接口的返回类型为: iterator ,又因为,目前而言,在C++标准库中的string类的类体中的迭代器,可以直接看做是指针变量,即,字符 //指针变量,故,可以直接把迭代器iter看做是字符指针变量,所以,这里的迭代器的类型iterator可以看做是char*类型,故,迭代器iter或者说是字符 //指针变量iter,所指向的内容是可读可写的、 while (iter != st.end()) //1、 //此处使用的是C++标准库中的string类的类体中的迭代器,所以可以直接把迭代器看做是字符指针变量,所以,迭代器iter的类型可以直接看做是char*类型, //这里的st.end();等价于st.end(&st);会调用在C++标准库中的string类的类体中已经显式实现的类成员函数接口:iterator end();,该类成员函数的返回 //类型为iterator,也可以直接看成是char*类型,又因,所有的指针变量的类型均属于内置类型,故此处的运算符!= 不需要进行运算符重载、 //注意:此处的运算符!=可以写成运算符<,且不需要进行运算符<的运算符重载,是因为C++标准库中的string类的底层实现逻辑就是顺序表,其物理内存是连 //续的,在顺序表中,后一个位置的地址一定比前一个位置的地址要大,所以此处写成运算符<也是可以的,但是不建议这样写,直接写成最标准的形式即可、 //2、 //在STL容器(vector)所对应的在C++标准库中的类的类体中的迭代器也可以直接看做是一个指针变量, 具体是什么类型的指针变量, 取决于该STL容器 //(vector)顺序表中存储的数据的类型,若存储的是整型int类型的数据,则此时迭代器可以看做是一个整型int类型的指针变量,对于STL容器(vector)所对应 //的C++标准库中的类的类体中的迭代器而言,此处的运算符!=也不需要进行运算符重载,因为,所有的指针变量的类型均属于内置类型、 //注意:此处的运算符!=可以写成运算符<,且不需要进行运算符<的运算符重载,是因为C++标准库中的vector类的底层实现逻辑就是顺序表,其物理内存是连 //续的,在顺序表中,后一个位置的地址一定比前一个位置的地址要大,所以此处写成运算符<也是可以的,但是不建议这样写,直接写成最标准的形式即可、 //3、 //在STL容器(list)所对应的C++标准库中的类的类体中的迭代器而言,此时,不能直接把迭代器看做是一个指针变量,会将原生态指针变量在自定义的类的类体中 //进行封装,即此时的迭代器是一个像,但又不是指针变量的一个东西,此时的迭代器的类型属于自定义类型,故此处的运算符!=需要进行运算符重载、 //注意:此处的运算符!=的运算符重载不可以写成运算符<的运算符重载,是因为C++标准库中的list类的底层实现逻辑就是链表,其物理内存不是连续的,在链表中, //后一个位置的地址不一定比前一个位置的地址要大,所以此处写成运算符<的运算符重载是不可以的,只能写成运算符!=的运算符重载这种最标准的形式、 //对于C++标准库中的map类的底层实现逻辑就是树形结构,其物理内存不是连续的,后一个节点的地址不一定比前一个节点的地址要大,所以此处写成运算符<的运 //算符重载是不可以的,只能写成运算符!=的运算符重载这种最标准的形式,只要物理内存不是连续的,则必须写成运算符!=或运算符!=的重载这种最标准的形式, //不能写成运算符<或运算符<的运算符重载的形式、 //注意: //不能把最标准的形式写成运算符<或运算符<的运算符重载,这样的话,就不能再使用STL容器(list)对应的在C++标准库中的list类的类体中的迭代器对链表数据结构(STL容器list) //进行访问和遍历了,否则会出错,所以,最标准的形式是此处写成!=运算符或!=运算符重载,这样,不管C++标准库中的哪个类的类体中的迭代器都是可以使用的、 { //*iter += 1; //可写、 cout << *iter << " "; //可读、 ++iter; } cout << endl; //C++标准库中的string类和STL容器对应的在C++标准库中的各个类(类模板)中都有迭代器、 //此处,iter代表迭代器,iterator就是迭代器的类型,而又因为,迭代器的类型iterator是内嵌在C++标准库中的string类的类体中的或者是内嵌在STL容器 //所对应的在C++标准库中的类的类体中,迭代器的类型iterator是在这些C++标准库中的类的类体中直接定义的或者是被typedef出来的,本质上是被typedef出来的,故,此处必须要指 //明类域,所以,此处的string::iterator整体作为迭代器iter的类型,再如:vector<int>::iterator或vector<char>::iterator等等整体作为迭代器iter的 //类型,迭代器的名称一般使用iter来表示,当然使用其他字母也是可以的、 //对于C++标准库中的string类的类体中和STL容器所对应的在C++标准库中的类的类体中都已经显式的实现了类成员函数begin和end,对于类成员函数begin而言, //返回的则是开始位置的迭代器,对于类成员函数end而言,返回的则是末尾位置的下一个位置的迭代器,对于在C++标准库中的string类的类体中所显示实现的类 //成员函数begin和end而言: //1、若是正向迭代器,则类成员函数begin返回的就是常量字符串中的首元素的迭代器,类成员函数end返回的则是常量字符串中最后一个有效字符的下一个位置的 //迭代器,也就是常量字符串中无效字符'\0'处的迭代器、 //2、若是反向迭代器,则类成员函数begin返回的则是常量字符串中无效字符'\0'的的前一个位置,即,常量字符串中最后一个有效字符处的迭代器,类成员函数end //返回的则是常量字符串中首字符的前一个位置的迭代器、 //即,对于在C++标准库中的string类的类体中所显示实现的类成员函数begin和end而言,所谓的开始位置和末尾位置均不考虑常量字符串中的无效字符'\0',这一点 //与其他的STL容器所对应的在C++标准库中的类的类体中所显式实现的类成员函数begin和end而言,有所区别,字符'\0'不是有效字符,属于无效字符,他是一个用来 //标识的字符、 //迭代器是像指针变量一样的东西(但不是指针变量),或者可以看做就是指针变量,具体要看其底层实现逻辑,就目前而言,在C++标准库中的string类的类体中的迭代器 //,可以直接看做是指针变量,即,字符指针变量,在STL容器(list)所对应的在C++标准库中的类的类体中的迭代器就不再是指针变量,而是一个像,但又不是指针变量的一 //个东西,还要知道,在STL容器(vector)所对应的在C++标准库中的类的类体中的迭代器也可以直接看做是一个指针变量,具体是什么类型的指针变量,取决于该STL容器 //(vector)顺序表中存储的数据的类型,若存储的是整型int类型的数据,则此时迭代器可以看做是一个整型int类型的指针变量、 return 0; }
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<vector> //注意:C++标准库中的vector类和list类均是类模板,必须先通过该类模板显式的实例化出具有特定类型(各种类型)的具体的类, //然后再使用具有特定类型(各种类型)的具体的类去实例化出对象,C++标准库中的basic_string类,本质上也是类模板,必须先通 //过该类模板显式的实例化出具有特定类型(char,char16_t,char32_t,wchar_t)的具体的类,然后再使用具有特定类型(char,char16_t,char32_t,wchar_t)的 //具体的类去实例化出对象,由于basic_string<char>被typedef为string,所以我们可以直接使用自定义类型string来实例化出对象、 using namespace std; //使用迭代器遍历STL容器(数据结构),也是类似的做法,以使用迭代器遍历STL容器(vector,顺序表)为例,如下所示: int main() { vector<int> v; //此处必须使用自定义类型vector<int>来实例化出自定义类型的对象v,这是因为自定义类型vector<int>并没有进行typedef操作、 v.push_back(1); //v.push_back(&v,1); 调用在C++标准库中的vectoe类模板的类体中的已经显式实现的函数接口: void push_back (const value_type& val);即:void push_back (const int& val); v.push_back(2); v.push_back(3); v.push_back(4); //指定类域为vector<int>:: vector<int>::iterator iter = v.begin();//v.begin(&v); 调用在C++标准库中的vectoe类模板类体中的已经显式实现的函数接口: iterator begin(); 编译器优先调用最合适(权限不变)、 while (iter != v.end()) { //*iter += 1; //可写、 cout << *iter << " "; //可读、 iter++; } cout << endl; //具体的底层逻辑见模拟实现、 return 0; }
C++标准库中的string类的类体中的迭代器的分类:
1、正向迭代器
在C++标准库中的string类的类体中已经显式实现了类成员函数为:
//1、 iterator begin(); const_iterator begin() const; //2、 iterator end(); const_iterator end() const;
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //1、正向迭代器 int main() { //1、 //具体的底层逻辑见模拟实现、 string s1("hello world"); string::iterator it1 = s1.begin(); //s1.begin(); -> s1.begin(&s1);调用C++标准库中的string类的类体中已经显式实现的类成员函数接口: iterator begin(); 由于 //C++标准库中的string类的类体中的迭代器可以直接看做是指针变量,即,字符指针变量,所以迭代器的类型iterator可以直接看做是 //char*类型,故对C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个 //字符变量的权限是可读可写的、 while (it1 != s1.end()) { *it1 += 1; //可写、 cout << *it1 << " "; //可读、 it1++; } cout << endl; //2、 //C++11中,在C++标准库中的string类的类体中又显式的实现了类成员函数接口:const_iterator cbegin() const noexcept; 和 const_iterator cend() const noexcept; 这是为了:当自定义string类型的对象被关键字const修饰时 //就去调用这两个类成员函数接口,这是为了更加规范,但是这种方法并不常有,还使用下述方法即可、 const string s2("hello world"); string::const_iterator it2 = s2.begin(); //s2.begin(); -> s2.begin(&s2); 调用C++标准库中的string类的类体中已经显式实现的类成员函数接口:const_iterator begin() const; 由于 //C++标准库中的string类的类体中的迭代器可以直接看做是指针变量,即,字符指针变量,所以迭代器的类型const_iterator(新类型)可以直接看做 //是const char* 类型,故对C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个 //字符变量的权限是可读不可写、 while (it2 != s2.end()) { //*it2 += 1; //不可写、 cout << *it2 << " "; //可读、 it2++; } cout << endl; //具体的底层逻辑见模拟实现、 //对于C++标准库中的string类的类体中和STL容器所对应的在C++标准库中的类模板的类体中都已经显式的实现了类成员函数begin,rbegin和end,rend,对于类成员函数begin和rbegin而言, //返回的则是开始位置的迭代器,对于类成员函数end和rend而言,返回的则是末尾位置的下一个位置的迭代器、 //对于在C++标准库中的string类的类体中所显示实现的类成员函数begin,rbegin和end,rend而言: //1、若是正向迭代器,则类成员函数begin返回的就是常量字符串中的首元素的迭代器,类成员函数end返回的则是常量字符串中最后一个有效字符的下一个位置的迭代器,也就是常量字符串中无效字符'\0'处的迭代器、 //2、若是反向迭代器,则类成员函数rbegin返回的则是常量字符串中最后一个有效字符的迭代器,类成员函数rend返回的则是常量字符串中首字符的前一个位置的迭代器、 //即,对于在C++标准库中的string类的类体中所显示实现的类成员函数begin,rbegin和end,rend而言,所谓的开始位置和末尾位置均不考虑常量字符串中的无效字符'\0'、 //其他的STL容器所对应的在C++标准库中的类模板的类体中所显式实现的类成员函数begin,rbegin和end,rend返回的分别是开始位置的迭代器和末尾位置的下一个位置的迭代器, //此处所谓的开始位置指的就是所有位置中的 最 左边(正)或 最 右边(反),此处所谓的末尾位置指的就是所有位置中的 最 右边(正)或 最 左边(反)、 字符'\0'不是有效字符,属于无效字符,他是一个用来标识的字符、 return 0; }
2、反向迭代器
在C++标准库中的string类的类体中已经显式实现了类成员函数为:
//1、 reverse_iterator rbegin(); const_reverse_iterator rbegin() const; //2、 reverse_iterator rend(); const_reverse_iterator rend() const;
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> using namespace std; //2、反向迭代器 int main() { //1、 //具体的底层逻辑见模拟实现、 string s1("hello world"); string::reverse_iterator it1 = s1.rbegin(); //迭代器的新类型:reverse_iterator //s1.rbegin(); -> s1.rbegin(&s1);调用C++标准库中的string类的类体中已经显式实现的类成员函数接口: reverse_iterator rbegin(); 由于 //C++标准库中的string类的类体中的迭代器可以直接看做是指针变量,即,字符指针变量,所以迭代器的新类型 reverse_iterator 可以直接看做是 //char*类型,故对C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个 //字符变量的权限是可读可写的、 while (it1 != s1.rend()) { *it1 += 1; //可写、 cout << *it1 << " "; //可读、 it1++; //此处不能写成:it1--; } cout << endl; //2、 //C++11中,在C++标准库中的string类的类体中又显式的实现了类成员函数接口:const_reverse_iterator crbegin() const noexcept; 和const_reverse_iterator crend() const noexcept; 这是为了:当自定义string类型的对象被关键字const修饰时 //就去调用这两个类成员函数接口,这是为了更加规范,但是这种方法并不常有,还使用下述方法即可、 const string s2("hello world"); //auto it2 = s2.rbegin(); string::const_reverse_iterator it2 = s2.rbegin(); //迭代器的新类型:const_reverse_iterator //s2.rbegin(); -> s2.rbegin(&s2); 调用C++标准库中的string类的类体中已经显式实现的类成员函数接口:const_reverse_iterator rbegin() const;由于 //C++标准库中的string类的类体中的迭代器可以直接看做是指针变量,即,字符指针变量,所以迭代器的类型const_reverse_iterator(新类型)可以直接看做 //是const char* 类型,故对C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的某一个 //字符变量的权限是可读不可写、 while (it2 != s2.rend()) { //*it2 += 1; //不可写、 cout << *it2 << " "; //可读、 it2++; //此处不能写成:it1--; } cout << endl; //具体的底层逻辑见模拟实现、 return 0; }
3.4.3、方法三:范围for(语法糖) 、
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> #include<list> using namespace std; //方式三: 范围for(语法糖) 、 //C++11版本才支持,范围for(语法糖)的底层逻辑就是使用的迭代器,编译器会自动把范围for(语法糖)的语法替换为迭代器(正向迭代器:iterator begin(); 和 iterator end();)的语法,可以根据汇编代码看出、 int main() { //1、 string st("hello world"); for (auto ch : st) { ch += 1; //可写、 cout << ch << " "; //可读、 } cout << endl; //自动遍历,依次自动取常量字符串中的每一个有效字符,赋值给字符变量ch,自动判断结束,自己进行迭代、 //所有的数组或顺序表均可按照该方法去遍历,不管数组或顺序表中所存储的数据的类型是什么,也不管数组或顺序表中所存储的数据的个数是多少个、 //2、 //对于STL容器list(数据结构:链表),也可以使用范围for(语法糖)的方式来访问和遍历,即,像这种物理内存不连续的情况,也可以使用范围for(语法糖)的方式访问和遍历、 list<int> L; //此处必须使用自定义类型list<int>来实例化出自定义类型的对象L,这是因为自定义类型list<int>并没有进行typedef操作、 L.push_back(1); L.push_back(2);//L.push_back(&L,2); 调用在C++标准库中的list类模板的类体中的已经显式实现的函数接口: void push_back (const value_type& val);即:void push_back (const int& val); L.push_back(3); L.push_back(4); for (auto e : L) { cout << e << " "; } cout << endl; return 0; }
例题:
仅仅反转字母 力扣
方法一:
class Solution { public: bool isletter(char ch) { if(ch>='a' && ch<='z') { return true; } else if(ch>='A' && ch<='Z') { return true; } else { return false; } } //方法一: // 下标+运算符[]的运算符重载 的方法: string reverseOnlyLetters(string s) { int left=0; int right=s.size()-1; while(left<right) { while(left<right && !isletter(s[left])) { left++; } while(left<right && !isletter(s[right])) { right--; } swap(s[left],s[right]); left++; right--; } return s; } };
方法二:
class Solution { public: bool isletter(char ch) { if(ch>='a' && ch<='z') { return true; } else if(ch>='A' && ch<='Z') { return true; } else { return false; } } //方法二: //迭代器: string reverseOnlyLetters(string s) { //auto itbegin=s.begin(); //auto itend=s.end()-1; string::iterator itbegin=s.begin(); string::iterator itend=s.end()-1; //此处写成:string::iterator itend=s.end(); 也能通过,但这是凑巧,因为,无效字符'\0'不是某一个字母,其次,当*itend时,也不会出现越界,这是因为,常量字符串的最后面有一个无效字符'\0',若在C++标准库中的vector和list类模板中的迭代器按照上述写法的话,当解引用或解引用运算符重载时,就会出现越界问题、 while(itbegin < itend) { //此处最好不要写成运算符!=,因为,可能在itbegin++和itend--的过程中,会把 itbegin==itend 的这种情况越过去,此时while循环还会继续进行,这就出错了,就比如常量字符串中有偶数个有效字符,且有效字符均是字母的时候,就会出现这种问题,所以此处,虽然我们知道对于迭代器的话,这里最好写成最标准的形式:运算符!=或者是运算符!=的运算符重载,但当前这种情况下,最好还是写成运算符<比较简单,就不需要再分类讨论了、 while(itbegin < itend && !isletter(*itbegin)) { itbegin++; } while(itbegin < itend && !isletter(*itend)) { itend--; } swap(*itbegin,*itend); itbegin++; itend--; } return s; } };
注意:在本题中使用 范围for(语法糖) 的方法不是很容易,尽量不使用该方法、
3.5、类成员函数 at
在C++标准库中的string类的类体中已经显式的实现了类成员函数 at 的函数接口,具体如下所示:
char& at (size_t pos);
const char& at (size_t pos) const;
该类成员函数 at 与C++标准库中的string类的类体中已经显式实现的类成员函数 operator[ ] 的功能是一模一样的,具体就不再进行阐述,只是在他们调用时会有一点区别:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
//1、
string s1("hello");
cout << s1.at(0) << endl; //可读、
s1.at(0) = 'H'; //可写、
cout << s1 <<endl; //具体的底层逻辑见模拟实现、
//2、
const string s2("world");
cout << s2.at(0) << endl; //可读、
//s2.at(0) = 'W'; //不可写、
//具体的底层逻辑见模拟实现、
return 0;
}
其次,C++标准库中的string类的类体中和STL容器在C++标准库中所对应的类模板的类体中,已经显式实现的类成员函数 at 和类成员函数 operator[ ] ,两者对于越界问题处理的方式不同,类成员函数 operator[ ] 当遇到越界问题时,会报断言错误,而类成员函数 at 遇到越界问题时,则会抛异常,只有在C++标准库中的string类的类体中显式实现的类成员函数 at 中,若通过该类成员函数访问到了C++标准库中的string类的类体中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的字符串中的无效字符 '\0' 时,也会抛异常,即,在C++标准库中的string类的类体中显式实现的类成员函数 at 中,只要形参变量pos的值大于等于字符串的长度(有效字符的个数),则就会抛异常,而STL容器在C++标准库中所对应的类模板的类体中的已经实现的类成员函数 at 中,都是当遇到越界问题时,才会抛异常、
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("hello");
const string s2("world");
1、
越界 -> 报断言错误、
//cout << s1[6] << endl; //Debug Assertion Failed!
//cout << s2[6] << endl; //Debug Assertion Failed!
2、
//try
//{
// cout << s1.at(6) << endl; //越界 -> 抛异常、
//}
//catch (const exception& e) //捕捉异常信息、
//{
// cout << e.what() << endl;
//}
//try
//{
// cout << s2.at(6) << endl; //越界 -> 抛异常、
//}
//catch (const exception& e) //捕捉异常信息、
//{
// cout << e.what() << endl;
//}
//3、
try
{
cout << s2.at(5) << endl; //未越界 -> 仍抛异常、
}
catch (const exception& e) //捕捉异常信息、
{
cout << e.what() << endl;
}
//具体的底层逻辑见模拟实现、
return 0;
}
3.6、类成员函数 back 和 front
在C++标准库中的string类的类体中已经显式的实现了类成员函数 back 和 front 的函数接口,具体如下所示:
//C++11:
char& back();
const char& back() const;
char& front();
const char& front() const;
//一、
// string s1("hello"); 或 { string s2("hello\0a"); //完全等价于string s1("hello"); 编译器会直接把 string s2("hello\0a"); 替换成 string s1("hello"); }
// 或 { string s3("hello"); s3.push_back('\0'); }
//1、
//C++标准库中的string类的类体中的类成员函数back返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中的下标为 _size-1 位置
//处所存储的字符(有效字符或无效字符'\0')、
//2、
//C++标准库中的string类的类体中的类成员函数front返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中的下标为0位置处所存储
//的字符(有效字符或无效字符'\0')、
//二、(常用)
//但如果像 string s1("hello"); 或 { string s2("hello\0a"); //完全等价于string s1("hello"); 编译器会直接把 string s2("hello\0a"); 替换成 string s1("hello"); }
//这两种情况而言,则有:
//1、
//C++标准库中的string类的类体中的类成员函数front返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的常规(一定是常规的)的
//常量字符串中第一个有效字符的别名、
//2、
//C++标准库中的string类的类体中的类成员函数back返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的常规(一定是常规的)的
//常量字符串中最后一个有效字符的别名、
//通常在任何情况下,我们只考虑最常规的常量字符串,因此,只需要知道如下所示的即可:
//1、
//C++标准库中的string类的类体中的类成员函数front返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的常规(一定是常规的)的
//常量字符串中第一个有效字符的别名、
//2、
//C++标准库中的string类的类体中的类成员函数back返回的是某一个自定义string类型的对象中的类成员变量所指向的在堆区上动态开辟的内存空间中所存储的常规(一定是常规的)的
//常量字符串中最后一个有效字符的别名、
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
//1、
string s1("hello");
cout << s1.front() << endl; //可读、 //h
s1.front() = 'H'; //可写、
cout << s1 << endl; //Hello
cout << s1.back() << endl; //可读、 //o
s1.back() = 'O'; //可写、
cout << s1 << endl; //HellO
//2、
const string s2("world");
cout << s2.front() << endl; //可读、 //w
//s2.front() = 'W'; //不可写、
cout << s2 << endl; //world
cout << s2.back() << endl; //可读、 //d
//s2.back() = 'D'; //不可写、
cout << s2 << endl; //world
//具体的底层逻辑见模拟实现、
return 0;
}
未完待续、