• c++ - 第8节 - string类


    目录

    1.为什么学习string类

    1.1.C语言中的字符串

    1.2.面试题需要

    2.标准库中的string类

    2.1.string类

    2.2.string类的常用接口说明

    2.3.string类练习题

    3.string类的模拟实现

    4.扩展阅读及补充内容

    4.1.扩展阅读

    4.2.补充内容


    1.为什么学习string

    1.1.C语言中的字符串

    C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

    1.2.面试题需要

    如上面两道题所示,在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。


    2.标准库中的string

    2.1.string

    string类的文档介绍:https://cplusplus.com/reference/string/string/?kw=string

    注:

    1.使用string类需要包含string的头文件,代码为:#include

    2.使用时,如下图左所示,这样使用并不是模板的使用方式,表面上string不是模板,

    实际上他就是模板(模板显式实例化后的结果也是模板),如下图右所示,string是被typedef过的,它实际上是由基础类模板显式实例化得到的,基础类模板为basic_string,实例化的模板参数是char

     

    3.我们在上面的string类的文档介绍网页中,回到reference目录页,如下图一所示,我们发现string类并没有在Containers容器里面,原因是string产生的比STL还要早一点,因此文档里面没有把string归到Containers容器中,我们可以找到string并点开

    4.我们发现,string里面有很多,有basic_string、string、u16string、u32string、wstring,其中basic_string是原始的类,string、u16string、u32string、wstring是根据basic_string显式实例化不同的参数得到的,如下图所示

    为什么会产生u16string、u32string和wstring呢,这其实是和编码有关系

    如今有名的编码有:ASCII,nuicode(utf-8、utf-16、utf-32),gbk

    utf-8、utf-16、utf-32的意思分别是一个字符占用8、16、32比特位

    nuicode和gbk都是兼容ASCII码表的

    ASCII码表:ascii码表如下图一所示,是值和显示值的映射表

    如下图所示,字符A在电脑里面存的是数字65(十进制),因为ASCII码表里面A对应的值是65,如果打印ch1和ch2,打印出来为A,是因为ch1和ch2是字符型,编译器会去找ASCII码表中65对应的字符然后打印

    这样用ASCII码表就可以显示英文了,因为英文就是由ASCII码表里面的大写字母小写字母和一些符号组成的,如下图所示,利用ASCII码表将英文字母和符号与数对应起来,那么存储这些数就相当于存储了英文

    利用ASCII码表使用一个char类型8个比特位(有符号有128种状态,无符号256种状态)就可以表示出英文,但是中文汉字是很多的,一个char类型8个比特位是远远不够的,中文一般使用两个char类型大小16个比特位(无符号下65536种状态),六万多种状态已经够涵盖常用的汉字了(不常见的汉字可能会用三个或四个字节)

    gbk是我们自己建立的针对中文的编码表,其中gb是国标的意思。windows操作系统下一般用的是gbk

    unicode是针对全世界的文字设计的编码表,linux下使用的是一般是unicode

    如下图一所示,汉字“吃货”占用4个字节。如下图二所示,可以看出中文编码表也不是乱编的

    ,它是把读音类似的词放在了一起,这样做是有好处的,比如说净网行动,不允许在网上说脏话,但是有些人会使用脏话的谐音,编码的时候把读音类似的词放在了一起,这样就更容易屏蔽掉这些谐音词了

    string类可以很好的兼容英文,因为string类使用的是char类型,如下图一所示。gbk和nuicode中的utf-16可以很好的兼容中文,因为一个汉字需要用两个字符的大小来表示,那么就应该使用wstring或u16string类,因为wstring类使用的是wchar_t类型,u16string类使用的是char16_t类型,如下图二三所示,wchar_t类型和char16_t类型是两个字符大小,如下图四所示

    总结:使用英文对应的是ASCII编码表,使用汉字对应gbk或unicode(unicode中的utf16)编码表,其他语言unicode里面也有编码表;英文使用一个char类型的大小就可以表示了,中文需要使用两个char类型的大小表示;因此英文使用string类(string是basic_string类模板显式实例化char类型的),中文使用wstring或u16string类(wstring和u16string类是basic_string类模板显式实例化wchar_t类型和char16_t类型得到的)

    5.string类使用起来是很方便的,如下图所示,想要字符串进行拼接,直接使用+=就可以了(这里+=在string类里面进行了运算符重载)

    2.2.string类的常用接口说明

    使用string前的说明:

    1.使用string类需要包含string的头文件,代码为:#include

    注:这里不是包含string.h是因为会和c语言的库函数冲突,不能直接包含string.cpp是因为如果项目里面同时两个人都包含string.cpp,那么就会造成重定义

    2.string类是在std里面的,因此使用string类需要using namespace std将std展开(如果不展开每次使用string类需要std::string)

    注:std和iostream是无关的,只不过帮助进行输入输出的cout、cin、enl等是在std里面定义的。如果不输入输出,不包含iostream也可以使用string类,但是需要展开std

    1. string类对象的常见构造

    函数名称
    功能说明
    string()
    构造空的string类对象,即空字符串
    string(const char* s)
    用字符串来构造string类对象
    string(const string&s)
    拷贝构造函数
    string(size_t n, char c)
    使用n个字符c进行构造
    string(const char* s, size_t n)使用字符串s前n个字符进行构造

    string (const string& str, size_t pos, size_t len = npos)

    使用str从pos处开始len个字符进行构造(如果len不给就是用缺省值npos)

    注:

    1.string类可以认为是管理动态增长字符数组,这个字符串以\0结尾

    2.常见的构造函数使用方式如下

    3.对于string(const char* s, size_t n)和string (const string& str, size_t pos, size_t len = npos)两个构造函数,构造函数如果给的n和len值过大超过了s和str的长度,那么到\0为止有多少取多少,如下图所示

    4.上面的缺省值npos如下图一所示,将-1给到无符号的npos,npos就是整形的最大值,使用缺省值就是字符串后面有多少取多少,到字符串结尾为止,如下图二所示

    2. string类对象的赋值重载函数

    函数名称
    功能说明
    string& operator= (const string& str)赋值重载函数

    string& operator= (const char* s)

    使用一个字符串来赋值

    string& operator= (char c)

    使用一个字符来赋值

    注:

    1.常见的赋值重载函数使用方式如下

    2.这里其实只有string& operator= (const string& str)才是真正的赋值重载,剩下两个是附加的功能,知道即可

    3. string类对象的访问及遍历操作
    方法解释

    对象名[ ]

    对运算符[ ]进行运算符重载,来访问对象的字符串任意字符
    迭代器使用迭代器配合begin、end函数,来访问对象的字符串任意字符
    范围for使用范围for的语法进行遍历,本质上就是转换成了迭代器
    注:
    1.常见的赋值重载函数使用方式如下

    2.之所以可以对象名[ ]访问对象的字符串任意字符是因为string里面对运算符[ ]进行了运算符重载,如下图所示,那么遇见s[i]编译器就会翻译成s.operator[ ](i)

    从上图可以看出, operator[ ]运算符重载是传引用返回的,因此如果对象不是const类型的,编译器调用第一个operator[ ]的运算符函数重载,可以对返回后的结果进行修改;如果对象是const类型的,编译器调用第二个operator[ ]的运算符函数重载,传引用返回是带const的,不能对返回后的结果进行修改,如下图所示

    3.string类里面有一个size函数,如下图所示,返回对象字符串的长度

    注意这里size函数是返回字符串的长度,不包含\0,如下图所示

    4.迭代器是一种像指针一样的东西,目前这里可以理解为指针(对于string类,string类的迭代器和指针意思相同),迭代器是类里面定义的,所以使用string类的迭代器,需要string::iterator
    使用迭代器,迭代器里面一般都是使用不等于符号,如下图所示,一般不用小于符号,对于string这种连续的存储空间使用小于是可以的,但是对于链表这种不连续存储空间使用小于就不对了

     

    5.string类里面有一个begin函数和一个end函数,如下图所示

    所有的迭代器都符合begin返回值指向第一个位置,end返回值指向最后一个位置的后一个位置,这里的指向并不一定是返回指针(迭代器和指针不完全相等),对于不同的类可能不同

    对于string类而言,类里面的begin返回的是字符串首字符的地址,string类里面的end函数返回的是字符串尾字符下一个字符的地址(end函数返回/0位置处的地址)
    从上图可以看出,对于普通的对象,begin和end返回普通的iterator类型,解引用后可读可写,对于const修饰的对象,begin和end返回const修饰的const_iterator类型,解引用后仅可读,不可写
    6.使用范围for的方法进行遍历的前提条件是编译器要支持c++11,范围for本质上其实就是被替换成了迭代器
    7.string类里面还有一个at函数,该函数与operator[ ]运算符重载的功能很像,如下图所示

    两者不同的是,operator[ ]运算符重载函数里面有下标越界的检查,如果下标指向/0后面的位置越界访问了,那么就会进行assert断言报错;at函数也有下标越界的检查,如果下标指向/0后面的位置越界访问了,会抛异常。

    4.string类迭代器

    迭代器类型迭代器名称使用的函数说明
    普通正向迭代器iteratorbegin函数、end函数用于接收普通对象的begin/end函数返回值。其中begin指向第一个位置,end指向最后一个位置的后一个位置
    普通反向迭代器reverse_iteratorrbegin函数、rend函数用于接收普通对象的rbegin/rend函数返回值。其中rbegin指向最后一个位置,rend指向第一个位置的前一个位置
    const修饰的正向迭代器const_iteratorbegin函数、end函数用于接收const修饰对象的begin/end函数返回值。其中begin指向第一个位置,end指向最后一个位置的后一个位置
    const修饰的反向迭代器const_reverse_iteratorrbegin函数、rend函数用于接收普通对象的rbegin/rend函数返回值。其中rbegin指向最后一个位置,rend指向第一个位置的前一个位置

    注:

    1.正向迭代器对正向迭代器变量++会正着走,反向迭代器对反向迭代器变量++会倒着走(让反向迭代器变量倒着走不是给反向迭代器变量--而是++)

    2.利用两个普通迭代器访问对象字符串,对字符串可读可写,如下图所示

    对于普通的对象,前面讲过begin和end的返回值是iterator类型,iterator类型可读可写,而对于const修饰的对象,begin和end的返回值是const_iterator类型,这种类型仅可读,像下图五这种情况,因为形参是const修饰的对象,begin和end返回的是const_iterator类型,因此应使用const_iterator迭代器

    对于普通的对象,rbegin和rend的返回值是reverse_iterator类型,reverse_iterator类型可读可写,而对于const修饰的对象,rbegin和rend的返回值是const_reverse_iterator类型,这种类型仅可读,像下图五这种情况,因为形参是const修饰的对象,rbegin和rend返回的是reverse_const_iterator类型,因此应使用const_iterator迭代器

    3.像reverse_const_iterator这种很长的类型,我们可以使用auto,让编译器自己去推导即可,如下图所示 

    5.string类对象的容量操作
    函数名称
    功能说明
    size
    返回字符串有效字符长度
    length
    返回字符串有效字符长度
    capacity
    返回空间总大小
    empty
    检测字符串释放为空串,是返回true,否则返回false
    clear
    清空有效字符
    reserve
    为字符串预留空间
    resize
    将有效字符的个数改成n个,多出的空间用字符c填充

    注:

    1.size函数和length函数功能是相同的,因为string类出现时间比stl还早,早期使用的是length函数,后来有了stl,为了和stl相配套,就有了size函数,其功能和length完全相同。因为size是stl配套的函数,更加通用(链表、树等其他类里面用的是size),所以更推荐用size函数

    2.capacity函数是返回容量的大小。因为string类是动态开辟空间的,所以string类成员变量一定有size和capacity成员,size成员标记当前数据个数,capacity标记当前空间容量的大小(实际的容量要比成员变量capacity大1,实际容量包括了/0,而成员变量capacity的大小是实际的容量排除了/0)

     从下图可以看出,对于vs编译器来说,capacity每次扩容大概是1.5倍扩容

    3.reserve函数的功能是阔空间,可以改变容量capacity,如果提前知道有一千个字符,那么可以直接reserve(1000),系统就不用再一次次扩容了,减少了扩容的销毁,如下图二所示。这里给了一千,系统不一定只开一千,这里涉及到对齐的问题(向系统申请内存空间,申请的空间是要按整数倍或二的倍数对齐的,这里对齐问题是和内存碎片和效率有关系),一般会多开一点,但至少会开你给的值

    4.resize函数的功能是扩空间并初始化,改变capacity的同时,size也会改变,如下图所示。resize和reserve一样括空间可能会多扩一些因为涉及到内存对齐,但size的大小和初始化的内容就是你给的值不会变。

    resize函数要是不给第二个参数,那么默认初始化为/0,如下图所示

    如果对象字符串本身有字符,使用resize函数进行扩容并初始化,不会覆盖本身的字符串内的字符,如下图所示

    在vs版本下,reserve函数和resize函数只扩容不缩容,但是resize会改变成员变量size的大小,如下图所示,只留下你给定数值个字符。其他版本reserve函数和resize函数会不会缩容是不确定的,因为stl没有规定

    resize函数一般是用来扩空间+初始化或者是删除部分数据,保留前n个两个功能

    5.clear函数的功能是清空有效字符,size变成0,但是容量capacity不会改变

    6.empty函数的功能是检测字符串释放为空串,是返回true,否则返回false

    6.string类对象的修改操作
    函数名称
    功能说明 
    push_back
    在字符串后尾插字符c
    append
    在字符串后追加一个字符串
    operator+=
    在字符串后追加字符串str
    insert在字符串pos位置插入字符串
    erase删除字符串中的一段字符
    swap将两个对象的字符串进行交换
    c_str
    返回C格式字符串
    find
    从字符串pos位置开始往后找字符串s,返回该字符s在字符串中的位置
    rfind
    从字符串pos位置开始往前找字符串s,返回该字符串s在字符串中的位置
    substr
    在str中从pos位置开始,截取n个字符,然后将其返回

    注:

    1.push_pack函数的功能是在字符串尾部插入一个字符

    2.append函数的功能是在字符串后追加一个字符串,参数可以是一个

    (1)string对象(常用)

    (2)string对象从subpos开始的sublen个字符

    (3)一个字符串(常用)

    (4)一个字符串的前n个字符
    append函数的使用方式如下图所示

     

    3.尾插字符或字符串最好用的其实是运算符+=,运算符+=是用运算符重载函数重载过的,其+=的内容可以是

    (1)string对象

    (2)一个字符串

    (3)一个字符

    4.insert函数的功能是在字符串pos位置处插入一个字符串,参数可以是

    (1)pos处和string对象(常用)

    (2)pos处和string对象从subpos开始的sublen个字符

    (3)pos处和一个字符串(常用)

    (4)pos处和一个字符串的前n个字符

    (5)pos处和n个字符c

    (6)迭代器p和n个字符c

    (7)迭代器p和字符c

    5.erase函数的功能是删除字符串中的一段字符,参数可以是

    (1)pos处和len个字符

    (2)迭代器p

    其中第一种用法,给了缺省值,pos缺省值是0,len的缺省值是npos,npos前面讲过为-1,赋值给size_t就是无符号整型的最大值,也就是说如果不给第二个参数,就是将pos处后面的字符删完。如果删除的长度超过了pos处后面的字符个数,那么有多少删多少

    6.swap函数的功能是将两个对象的字符串进行交换,参数是要交换字符串的对象
    s1.swap使用的是string类里面的swap函数进行交换操作;swap使用的是stl库里的swap函数模板,stl库里的swap函数模板支持任意类型的交换。这两种交换都是正确的,但是s1.swap高,swap效率低。交换两个对象指向的字符串空间其实只需要交换两个字符串指针即可,s1.swap交换的是字符串指针,而swap的交换其实是调用对应类的拷贝构造和赋值运算符重载进行交换,如下图所示,这样效率较低

    所以string类的交换建议使用s1.swap函数

    7.c_str函数的功能是返回C格式字符串,简单的说返回的是类的字符串的地址(第一个字符的地址),这个函数可以很好的和c语言的接口进行兼容和配合(例如strcpy函数),如下图二所示

    c_str函数也可以用来输出string类的字符串,如下图所示,对s1进行流插入调用的是string类里面运算符重载的流插入,对s1.c_str进行流插入调用的是原生的流插入运算符

    8.substr函数的功能是在str中从pos位置开始,截取len个字符,返回这len个字符构造的子类,第二个参数len是有缺省值的,其缺省值是npos,npos前面讲过为-1,赋值给size_t就是无符号整型的最大值,就是说如果不给第二个参数,就是将pos处后面的所有字符构成类然后返回

    9.find函数的功能是从字符串pos位置开始往后找字符c或字符串s,返回该字符c或字符串s在字符串中的位置(如果是找字符串则返回找到的字符串s第一个字符在大字符串中的位置),如果找到字符c或字符串s了,返回第一个匹配字符c或字符串s的位置,如果没找到字符c或字符串s会返回npos,npos前面讲过为-1,返回类型是size_t类型,那么npos就是无符号整型的最大值。因为npos是string类里面的静态成员变量,所以可以直接用string::npos来判断是否找到,如下图二所示

    如果文件名是string.c.tar.zip这种情况,使用find上图所示的方法就会出错,如下图所示,我们可以使用rfind函数来解决

    10.rfind函数的功能是从字符串pos位置开始往前找字符c或字符串s,返回该字符c或字符串s在字符串中的位置(如果是找字符串则返回找到的字符串s最后一个字符在大字符串中的位置)

    11.一个网址的内容分为网址的协议、域名、资源名,如下图一所示,如果要将网址的协议、域名、资源名(uri)分离开,我们需要使用到find函数,如下图二所示

    7.string类非成员函数
    函数
    功能说明
    operator+
    尽量少用,因为传值返回,导致深拷贝效率低
    operator>>
    输入运算符重载
    operator<<
    输出运算符重载
    getline
    获取一行字符串
    relational operators
    大小比较

    注:

    1.relational operators是比较对象字符串大小的几个函数,如下图一所示,每一个比较大小运算符都重载了三个函数,分别是string对象和string对象比较,string对象和字符串比较,字符串和string对象比较,如下图二所示

    其实对每个比较大小运算符都重载了三个函数价值不大(很多人对c++此处的吐槽点),只需要string对象和string对象比较的运算符其实就够了,如果需要string对象和字符串比较,那么构造一个匿名对象就可以了,如下图所示

    2.运算符重载operator+函数的功能是将第二个参数追加到第一个参数后面的结果进行返回(不改变第一个参数),其参数类型是如下图所示

    注意operator+函数尽量少用,因为其是传值返回,效率较低

    3.getline函数的功能是获取一行字符串。getline函数只认换行符,不认空格,如果想要获取中间有空格的字符串,无法使用scanf和cin函数得到(空格和换行对于scanf和cin函数相当于一次提取结束标志),如下图所示,但是可以用getline函数得到。

    如下图一所示,getline函数的第一个参数是istream&类型,因此实参一般是cin,第二个参数是要提取的字符串放入到的str字符串,使用方式如下图二所示

    2.3.string类练习题

    练习题一:

    题目描述:

    题目链接:917. 仅仅反转字母 - 力扣(LeetCode)

    代码1:

    代码2:(迭代器方法)

    注:

    1.这里swap(s[left],s[right])传两个值进行交换可以成功是因为operator[ ]运算符重载返回的是引用,这里使用传引用返回不仅可以减少拷贝,最主要的是可以支持修改返回对象,而swap传参形参也是引用,因此成功交换

    练习题二:

    题目描述:

    题目链接:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

    代码:

    练习题三:

    题目描述:

    题目链接:字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)

    代码:

    注: 

    1.cin和scanf都有一个特点,有可能会输入多个值,c和c++的规定当输入多个值,值和值之间用空格或换行去间隔,因此想得到中间有空格的字符串,使用cin和scanf就会有问题。想要获取中间有空格的字符串,我们需要使用getline函数,getline函数只认换行符,不认空格

    练习题四:

    题目描述:

    题目链接:125. 验证回文串 - 力扣(LeetCode)

    练习题五:

    题目描述:

     题目链接:415. 字符串相加 - 力扣(LeetCode)

    思路:

    两个字符串从最后一位开始,一位一位依次倒着相加,然后加上进位(如果此次有进位,就将进位置成1,如果此次没有进位,就将进位置成0),将三者相加(两个字符值和一个进位制)得到的结果依次头插进类字符串即可

    要注意的是,如果两个字符串长度相同(例如“1”和“9”)那么最后结束的时候如果进位值为一,就会出错,还需要将进位的值头插进类字符串

    代码1:

    代码2:(逆置函数reverse还没介绍,明白意思即可)

    注:

    1.代码1的写法效率不高,因为使用了insert函数,每一次头插都要移动后面的字符,这样时间复杂度为经典的O(n^{2})(等差数列),改进方法是每次进行尾插进类字符串,然后最后再将类字符串逆置,逆置有现成的reverse函数,如代码2所示这个函数目前还没介绍,知道有这个函数和代码意思即可即可


    3.string类的模拟实现

    1.先实现一个简单的string,只考虑资源管理深浅拷贝问题,暂且不考虑增删查改

    test.cpp文件:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. #include
    4. using namespace std;
    5. #include "string.h"
    6. void test_string1()
    7. {
    8. bit::string s1("hello world");
    9. cout << s1.c_str() << endl;
    10. s1[0] = 'x';
    11. cout << s1.c_str() << endl;
    12. for (size_t i = 0; i < s1.size(); ++i)
    13. {
    14. cout << s1[i] << " ";
    15. }
    16. cout << endl;
    17. }
    18. void test_string2()
    19. {
    20. bit::string s1("hello world");
    21. bit::string s2(s1);
    22. cout << s1.c_str() << endl;
    23. cout << s2.c_str() << endl;
    24. s1[0] = 'x';
    25. cout << s1.c_str() << endl;
    26. cout << s2.c_str() << endl;
    27. }
    28. void test_string3()
    29. {
    30. bit::string s1("hello world");
    31. bit::string s2(s1);
    32. bit::string s3("111111111");
    33. s1 = s3;
    34. cout << s1.c_str() << endl;
    35. cout << s2.c_str() << endl;
    36. cout << s3.c_str() << endl;
    37. s1 = s1;
    38. cout << s1.c_str() << endl;
    39. cout << s2.c_str() << endl;
    40. cout << s3.c_str() << endl;
    41. }
    42. int main()
    43. {
    44. try
    45. {
    46. test_string3();
    47. }
    48. catch (const exception& e)
    49. {
    50. cout << e.what() << endl;
    51. }
    52. return 0;
    53. }

    string.h文件:

    1. #pragma once
    2. #include
    3. namespace bit
    4. {
    5. // 先实现一个简单的string,只考虑资源管理深浅拷贝问题
    6. // 暂且不考虑增删查改
    7. class string
    8. {
    9. public:
    10. string(const char* str)
    11. :_str(new char[strlen(str)+1])
    12. {
    13. strcpy(_str, str);
    14. }
    15. string(const string& s)
    16. :_str(new char[strlen(s._str)+1])
    17. {
    18. strcpy(_str, s._str);
    19. }
    20. string& operator=(const string& s)
    21. {
    22. if (this != &s)
    23. {
    24. char* tmp = new char[strlen(s._str) + 1];
    25. strcpy(tmp, s._str);
    26. delete[] _str;
    27. _str = tmp;
    28. }
    29. return *this;
    30. }
    31. ~string()
    32. {
    33. if (_str)
    34. {
    35. delete[] _str;
    36. }
    37. }
    38. const char* c_str() const
    39. {
    40. return _str;
    41. }
    42. char& operator[](size_t pos)
    43. {
    44. assert(pos < strlen(_str));
    45. return _str[pos];
    46. }
    47. size_t size()
    48. {
    49. return strlen(_str);
    50. }
    51. private:
    52. char* _str;
    53. };
    54. }

    注:

    1.默认生成的拷贝构造函数是进行浅拷贝(值拷贝),对于string这种类浅拷贝不再合适,string类如果还使用浅拷贝,析构的时候会对一块空间释放两次(内存空间不能释放两次会报错),并且如果一个对象修改会影响另外一个,因此string类需要自己写拷贝构造函数来实现深拷贝。

    2.string类进行深拷贝,首先需要开辟同样大小的空间,然后将原对象字符串拷贝到新开辟的空间内,如下图所示

    3.默认的赋值也是进行类似浅拷贝(值拷贝),对于string这种类类似浅拷贝的赋值不再合适,string类如果还使用类似浅拷贝的赋值,析构的时候会对一块空间释放两次(内存空间不能释放两次会报错),并且如果一个对象修改会影响另外一个,因此string类需要自己写赋值运算符重载函数来实现深拷贝类似的赋值运算符重载。

    4.string类进行类似深拷贝的赋值运算符重载,与拷贝构造函数不同的是,拷贝构造的目标对象需要先开辟空间,再进行字符串的拷贝,而赋值运算符重载的目标对象是已经存在的对象,已经开辟有空间了,那么此时我们要进行分析:如果目标对象的空间足够存储原对象字符串,那么直接拷贝过去可以,但是如果目标对象空间过大,就会存在内存浪费;如果目标对象的空间不够存储原对象字符串,此时如果直接拷贝过去就会越界。

    简单的说,如果目标对象的空间太小,会造成越界,如果目标空间太大,空间会浪费,基于这样的原因,我们直接把目标对象之前的空间释放了,然后创建一个与原对象字符串同样大小的空间,最后将原对象字符串拷贝到新开辟的空间内,如下图所示

    上面的代码如果自己给自己赋值会出错,因为目标对象和原对象是同一个对象,那么释放目标对象空间的时候原对象空间也被释放了,释放之后空间被置成随机值了,然后new char[strlen(s._str)+1]在被释放的随机值空间开始往后找,直到/0为止开辟这么大的空间,这里开辟空间的大小是不定的因为不知道走多少能遇见/0,然后strcpy函数从随机值空间开始往后拷贝到新开辟的空间,直到遇见/0为止,所以打印出来的结果是一串随机值。因此需要加一个判断,如果是自己给自己赋值不做处理

    如果上面代码new char[strlen(s._str)+1]开空间开辟失败,此时目标对象的字符串空间也被释放了,那么目标空间没有被赋值,原本的值也没有了,因此可以先开辟空间给一个临时指针,原对象字符串拷贝给新开辟的空间,再去释放目标字符串将新开辟的空间给目标字符串,如下图所示

    5.c++代码中如果new开辟空间出错了,会抛异常,我们应该捕获这些异常,如下图所示,主函数调用的是test_string3函数,所以对test_string3函数进行捕获,捕获异常涉及到继承和多态的问题,我们后面再讲

    2.实现一个string,考虑资源管理深浅拷贝问题和考虑增删查改问题

    test.cpp文件:

    1. #include
    2. #include
    3. using namespace std;
    4. #include "string.h"
    5. void test_string1()
    6. {
    7. bit::string s1("hello world");
    8. cout << s1.c_str() << endl;
    9. s1[0] = 'x';
    10. cout << s1.c_str() << endl;
    11. for (size_t i = 0; i < s1.size(); ++i)
    12. {
    13. cout << s1[i] << " ";
    14. }
    15. cout << endl;
    16. }
    17. void test_string2()
    18. {
    19. bit::string s1("hello world");
    20. bit::string s2(s1);
    21. cout << s1.c_str() << endl;
    22. cout << s2.c_str() << endl;
    23. s1[0] = 'x';
    24. cout << s1.c_str() << endl;
    25. cout << s2.c_str() << endl;
    26. }
    27. void test_string3()
    28. {
    29. bit::string s1("hello world");
    30. bit::string s2(s1);
    31. bit::string s3("111111111");
    32. s1 = s3;
    33. cout << s1.c_str() << endl;
    34. cout << s2.c_str() << endl;
    35. cout << s3.c_str() << endl;
    36. s1 = s1;
    37. cout << s1.c_str() << endl;
    38. cout << s2.c_str() << endl;
    39. cout << s3.c_str() << endl;
    40. }
    41. void test_string4()
    42. {
    43. bit::string s1("hello world");
    44. cout << s1.c_str() << endl;
    45. bit::string s2;
    46. cout << s2.c_str() << endl;
    47. }
    48. void test_string5()
    49. {
    50. bit::string s1("hello world");
    51. s1.push_back('1');
    52. s1.push_back('1');
    53. cout << s1.c_str() << endl;
    54. s1 += '2';
    55. s1 += '3';
    56. s1 += '4';
    57. cout << s1.c_str() << endl;
    58. s1 += "bitejiuyeke";
    59. cout << s1.c_str() << endl;
    60. bit::string s2;
    61. //s2.reserve(20);
    62. s2 += '1';
    63. s2 += '1';
    64. s2 += '1';
    65. s2 += '1';
    66. s2 += '1';
    67. s2 += '1';
    68. s2 += '1';
    69. s2 += '1';
    70. s2 += '1';
    71. s2 += '1';
    72. bit::string s3("hello world");
    73. s3.reserve(15);
    74. //s3.resize(21, 'x');
    75. s3.resize(14, 'x');
    76. s3.resize(5);
    77. s1 < s2;
    78. }
    79. void func(const bit::string& s)
    80. {
    81. bit::string::const_iterator it = s.begin();
    82. while (it != s.end())
    83. {
    84. //*it -= 1;
    85. cout << *it << " ";
    86. ++it;
    87. }
    88. cout << endl;
    89. for (auto& ch : s)
    90. {
    91. //ch += 1;
    92. cout << ch << " ";
    93. }
    94. cout << endl;
    95. // 1、下表+[]
    96. for (size_t i = 0; i < s.size(); ++i)
    97. {
    98. //s[i] += 1;
    99. cout << s[i] << " ";
    100. }
    101. cout << endl;
    102. }
    103. void test_string6()
    104. {
    105. bit::string s1("hello world");
    106. // 遍历
    107. // 1、下表+[]
    108. for (size_t i = 0; i < s1.size(); ++i)
    109. {
    110. s1[i] += 1;
    111. cout << s1[i] << " ";
    112. }
    113. cout << endl;
    114. // 2、迭代器
    115. bit::string::iterator it = s1.begin();
    116. while (it != s1.end())
    117. {
    118. *it -= 1;
    119. cout << *it << " ";
    120. ++it;
    121. }
    122. cout << endl;
    123. // 3、范围for -- 底层就是被替换成迭代器访问
    124. for (auto& ch : s1)
    125. {
    126. ch -= 1;
    127. }
    128. cout << endl;
    129. for (auto ch : s1)
    130. {
    131. cout << ch << " ";
    132. }
    133. cout << endl;
    134. func(s1);
    135. }
    136. void test_string7()
    137. {
    138. bit::string s("hello world");
    139. s.insert(6, '@');
    140. cout << s.c_str() << endl;
    141. s += '@';
    142. cout << s.c_str() << endl;
    143. for (auto ch : s)
    144. {
    145. cout << ch << " ";
    146. }
    147. cout << "#" << endl;
    148. s += '\0';
    149. cout << s.c_str() << endl;
    150. for (auto ch : s)
    151. {
    152. cout << ch << " ";
    153. }
    154. cout << "#" << endl;
    155. s.insert(0, '@');
    156. cout << s.c_str() << endl;
    157. }
    158. void test_string8()
    159. {
    160. bit::string s("hello world");
    161. s.insert(0, "xxx");
    162. cout << s.c_str() << endl;
    163. s.insert(0, "");
    164. cout << s.c_str() << endl;
    165. s.earse(0, 3);
    166. cout << s.c_str() << endl;
    167. s.earse(5, 10);
    168. cout << s.c_str() << endl;
    169. s.earse(3);
    170. cout << s.c_str() << endl;
    171. s.earse(10);
    172. cout << s.c_str() << endl;
    173. }
    174. void test_string9()
    175. {
    176. bit::string s("hello world");
    177. cout << s << endl;
    178. cout << s.c_str() << endl;
    179. s += '\0';
    180. s += '\0';
    181. s += '\0';
    182. s += 'x';
    183. cout << s << endl;
    184. cout << s.c_str() << endl;
    185. //bit::string s1, s2;
    186. //cin >> s1 >> s2;
    187. //cout << s1 << endl;
    188. cin >> s2;
    189. //cout << s2 << endl;
    190. bit::string s3("hello world");
    191. cin >> s3;
    192. cout << s3 << endl;
    193. }
    194. void test_string10()
    195. {
    196. bit::string s1("hello world");
    197. bit::string s2(s1);
    198. cout << s1 << endl;
    199. cout << s2 << endl;
    200. bit::string s3("11111111111111111111111111");
    201. s1 = s3;
    202. cout << s1 << endl;
    203. cout << s3 << endl;
    204. s1 = s1;
    205. cout << s1 << endl;
    206. }
    207. void test_string11()
    208. {
    209. int i;
    210. cin >> i;
    211. string s = to_string(i);
    212. int val = stoi(s);
    213. }
    214. int main()
    215. {
    216. try
    217. {
    218. test_string11();
    219. }
    220. catch (const exception& e)
    221. {
    222. cout << e.what() << endl;
    223. }
    224. return 0;
    225. }

    string.h文件:

    1. #pragma once
    2. #include
    3. namespace bit
    4. {
    5. class string
    6. {
    7. public:
    8. typedef char* iterator;
    9. typedef const char* const_iterator;
    10. const_iterator begin() const
    11. {
    12. return _str;
    13. }
    14. const_iterator end() const
    15. {
    16. return _str + _size;
    17. }
    18. iterator begin()
    19. {
    20. return _str;
    21. }
    22. iterator end()
    23. {
    24. return _str + _size;
    25. }
    26. string(const char* str = "")
    27. : _size(strlen(str))
    28. , _capacity(_size)
    29. {
    30. _str = new char[_capacity + 1];
    31. strcpy(_str, str);
    32. }
    33. // 传统写法 : 本分,老老实实干活,该开空间自己开空间,该拷贝数据就自己拷贝数据
    34. /*string(const string& s)
    35. :_size(strlen(s._str))
    36. , _capacity(_size)
    37. {
    38. _str = new char[_capacity + 1];
    39. strcpy(_str, s._str);
    40. }
    41. string& operator=(const string& s)
    42. {
    43. if (this != &s)
    44. {
    45. char* tmp = new char[s._capacity + 1];
    46. strcpy(tmp, s._str);
    47. delete[] _str;
    48. _str = tmp;
    49. _size = s._size;
    50. _capacity = s._capacity;
    51. }
    52. return *this;
    53. }*/
    54. void swap(string& s)
    55. {
    56. std::swap(_str, s._str);
    57. std::swap(_size, s._size);
    58. std::swap(_capacity, s._capacity);
    59. }
    60. // 现代写法:剥削,要完成深拷贝,自己不想干活,安排别人是干活,然后窃取劳动成果
    61. string(const string& s)
    62. :_str(nullptr)
    63. , _size(0)
    64. , _capacity(0)
    65. {
    66. string tmp(s._str);
    67. swap(tmp);
    68. }
    69. /*string& operator=(const string& s)
    70. {
    71. if (this != &s)
    72. {
    73. string tmp(s._str);
    74. swap(tmp);
    75. }
    76. return *this;
    77. }*/
    78. string& operator=(string s)
    79. {
    80. swap(s);
    81. return *this;
    82. }
    83. ~string()
    84. {
    85. if (_str)
    86. {
    87. delete[] _str;
    88. _str = nullptr;
    89. _size = _capacity = 0;
    90. }
    91. }
    92. const char* c_str() const
    93. {
    94. return _str;
    95. }
    96. char& operator[](size_t pos)
    97. {
    98. assert(pos < _size);
    99. return _str[pos];
    100. }
    101. const char& operator[](size_t pos) const
    102. {
    103. assert(pos < _size);
    104. return _str[pos];
    105. }
    106. size_t size() const
    107. {
    108. return _size;
    109. }
    110. size_t capacity() const
    111. {
    112. return _capacity;
    113. }
    114. string& operator+=(const char* str)
    115. {
    116. append(str);
    117. return *this;
    118. }
    119. string& operator+=(char ch)
    120. {
    121. push_back(ch);
    122. return *this;
    123. }
    124. void reserve(size_t n)
    125. {
    126. if (n > _capacity)
    127. {
    128. char* tmp = new char[n + 1];
    129. strcpy(tmp, _str);
    130. delete[] _str;
    131. _str = tmp;
    132. _capacity = n;
    133. }
    134. }
    135. // 扩空间+初始化
    136. // 删除部分数据,保留前n个
    137. void resize(size_t n, char ch = '\0')
    138. {
    139. if (n < _size)
    140. {
    141. _size = n;
    142. _str[_size] = '\0';
    143. }
    144. else
    145. {
    146. if (n > _capacity)
    147. {
    148. reserve(n);
    149. }
    150. for (size_t i = _size; i < n; ++i)
    151. {
    152. _str[i] = ch;
    153. }
    154. _size = n;
    155. _str[_size] = '\0';
    156. }
    157. }
    158. void push_back(char ch)
    159. {
    160. //if (_size == _capacity)
    161. //{
    162. // reserve(_capacity == 0 ? 4 : _capacity*2);
    163. // //reserve(_capacity*2);
    164. //}
    165. //_str[_size] = ch;
    166. //++_size;
    167. //_str[_size] = '\0';
    168. insert(_size, ch);
    169. }
    170. void append(const char* str)
    171. {
    172. /*size_t len = _size + strlen(str);
    173. if (len > _capacity)
    174. {
    175. reserve(len);
    176. }
    177. strcpy(_str + _size, str);
    178. _size = len;*/
    179. insert(_size, str);
    180. }
    181. string& insert(size_t pos, char ch)
    182. {
    183. assert(pos <= _size);
    184. if (_size == _capacity)
    185. {
    186. reserve(_capacity == 0 ? 4 : _capacity * 2);
    187. }
    188. size_t end = _size + 1;
    189. while (end > pos)
    190. {
    191. _str[end] = _str[end - 1];
    192. --end;
    193. }
    194. _str[pos] = ch;
    195. _size++;
    196. return *this;
    197. }
    198. string& insert(size_t pos, const char* str)
    199. {
    200. assert(pos <= _size);
    201. size_t len = strlen(str);
    202. if (_size + len > _capacity)
    203. {
    204. reserve(_size + len);
    205. }
    206. // 往后挪动len个位置
    207. size_t end = _size + len;
    208. while (end > pos + len - 1)
    209. {
    210. _str[end] = _str[end - len];
    211. --end;
    212. }
    213. strncpy(_str + pos, str, len);
    214. _size += len;
    215. return *this;
    216. }
    217. string& earse(size_t pos, size_t len = npos)
    218. {
    219. assert(pos < _size);
    220. if (len == npos || pos + len >= _size)
    221. {
    222. _str[pos] = '\0';
    223. _size = pos;
    224. }
    225. else
    226. {
    227. size_t begin = pos + len;
    228. while (begin <= _size)
    229. {
    230. _str[begin - len] = _str[begin];
    231. ++begin;
    232. }
    233. _size -= len;
    234. }
    235. return *this;
    236. }
    237. size_t find(char ch, size_t pos = 0)
    238. {
    239. for (; pos < _size; ++pos)
    240. {
    241. if (_str[pos] == ch)
    242. {
    243. return pos;
    244. }
    245. }
    246. return npos;
    247. }
    248. size_t find(const char* str, size_t pos = 0)
    249. {
    250. const char* p = strstr(_str + pos, str);
    251. if (p == nullptr)
    252. {
    253. return npos;
    254. }
    255. else
    256. {
    257. return p - _str;
    258. }
    259. }
    260. void clear()
    261. {
    262. _str[0] = '\0';
    263. _size = 0;
    264. }
    265. private:
    266. char* _str;
    267. size_t _size; // 有效字符个数
    268. size_t _capacity; // 实际存储有效字符的空间
    269. const static size_t npos;
    270. };
    271. const size_t string::npos = -1;
    272. ostream& operator<<(ostream& out, const string& s)
    273. {
    274. for (auto ch : s)
    275. {
    276. out << ch;
    277. }
    278. return out;
    279. }
    280. istream& operator>>(istream& in, string& s)
    281. {
    282. s.clear();
    283. char ch;
    284. ch = in.get();
    285. char buff[128] = { '\0' };
    286. size_t i = 0;
    287. while (ch != ' ' && ch != '\n')
    288. {
    289. buff[i++] = ch;
    290. if (i == 127)
    291. {
    292. s += buff;
    293. memset(buff, '\0', 128);
    294. i = 0;
    295. }
    296. ch = in.get();
    297. }
    298. s += buff;
    299. return in;
    300. }
    301. bool operator<(const string& s1, const string& s2)
    302. {
    303. return strcmp(s1.c_str(), s2.c_str()) < 0;
    304. }
    305. bool operator==(const string& s1, const string& s2)
    306. {
    307. return strcmp(s1.c_str(), s2.c_str()) == 0;
    308. }
    309. bool operator<=(const string& s1, const string& s2)
    310. {
    311. return s1 < s2 || s1 == s2;
    312. }
    313. bool operator>(const string& s1, const string& s2)
    314. {
    315. return !(s1 <= s2);
    316. }
    317. bool operator>=(const string& s1, const string& s2)
    318. {
    319. return !(s1 < s2);
    320. }
    321. bool operator!=(const string& s1, const string& s2)
    322. {
    323. return !(s1 == s2);
    324. }
    325. }

    注:

    1.如果考虑增删查改问题一个成员变量_str就不够了,还需要_size和_capacity成员变量。一般使用_size来表示存储有效字符的个数,用_capacity来表示实际存储有效字符的空间(/0不算有效字符)所以_capacity一般不算/0的空间

    2.无参的默认构造函数如果给_str初始化为NULL,如下图一所示,那么运行下图二的代码就会崩掉,因为s2的_str是一个空指针,c_str返回的是s2的_str也就是空指针,打印空指针系统会崩掉。

    因此无参的默认构造函数应该开辟一个字节的空间给_str,并且将这个一个字节空间初始化为/0,如下图所示,此时运行上图二的代码系统就不会再崩了 

    其实我们可以将下图一所示的无参的默认构造函数和普通构造函数合并起来,用全缺省默认构造函数来实现,用全缺省的默认构造函数来实现只需要缺省值给空字符串即可(空字符串其实就是/0),如下图二所示

    要注意"/0"、" "、'\0'的区别,上面使用"/0"和" "都是可以的,但是'\0'不行,'\0'相当于把0传给了str指针str成为空指针,strlen(空指针)因为strlen函数内需要解引用,因此程序崩溃

    3.push_back函数里面进行判断如果_size和_capacity相等的话就将容量扩容到之前的两倍,但是需要考虑的是,如果_capacity是0,也就是创建对象的时候使用默认构造函数的缺省值,那么capacity扩容后还是0,因此这里需要进行判断,如下图所示

    4.append函数中的扩容不能像pushback一样直接括2倍,pushback每次插入一个字符扩2倍后空间是够的,append函数插入的字符串不知道有多长,如果插入的字符串长度比之前的容量还要大,那么容量括2倍的话还是不够。因此要判断原有效字符长度_size加上插入字符串的长度是否大于原实际存储有效字符的空间_capacity,如果大于就扩容到原有效字符长度_size和插入字符串的长度之和,如果小于等于空间就足够不用管,如下图所示。

    5.resize函数的实现需要分三种情况,如果第一个参数值大于容量_capacity,那么进行扩容,并且从/0开始到第一个参数值-1的位置用第二个参数字符填充,第一个参数值的位置处置成/0,_size置为第一个参数值;如果第一个参数值小于容量_capacity但是大于有效字符_size,那么从有效字符的/0开始用第二个参数字符填充直到第一个参数值-1处为止,第一个参数值的位置处置成/0,_size置为第一个参数值(第一个参数值大于容量_capacity的情况和第一个参数值小于容量_capacity大于有效字符_size的情况,除了第一种情况要扩容,其余步骤两个情况完全相同,因此可以二合一);如果第一个参数值小于有效字符_size,那么给第一个参数的位置处置成/0,_size置为第一个参数值,如下图所示

    6.从下图一可以看出std库里面实现的string类的比较大小函数不是类的成员函数,这些函数都是全局函数,也就是说这些比较大小的运算符重载函数都是在类外面定义的,这样做是因为可以利用c_str函数将类里面的成员提取出来进行比较,因此在类外面也可以实现,如下图二所示

    7.迭代器的实现,迭代器是像指针一样的东西,对于有些类迭代器就是指针,例如string类,但是对于有些类,迭代器不是指针,例如list类。对于string类来说,实现迭代器其实就是对char*进行重定义,同时还需要定义与迭代器相关的begin和end函数,如下图所示

    如果使用const修饰的对象来调用迭代器,或者使用范围for来访问const修饰的对象,那么像上图这样的begin、end函数定义将无法调用,涉及到权限放大的问题,因此begin、end函数还需要一个const版本,如下图所示

    注意:const版本的返回也应该是const修饰的迭代器,因为begin、end函数传进来的参数对象是const修饰的版本,不能进行修改,如果返回的迭代器不用const修饰,那么可以用返回的普通迭代器来对对象进行修改,虽然编译器不会报错,但是经过这个函数相当于权限进行了放大,所以返回值应该也用const修饰(与operator[]函数const修饰版本意思相同),因此我们应该再类型重定义一个const版本的迭代器const_iterator来作为begin、end函数的返回值

    8.前面讲过范围for的实现本质上就是使用迭代器,因此此处如果我们把迭代器定义的代码屏蔽掉,那么范围for也无法使用了,报错如下图所示,报错内容是没有找到begin和end函数。

    其实范围for实现的过程中在底层是被替换成迭代器来实现的,所以迭代器要使用的begin、end函数范围for也要使用

    注意:范围for底层替换成迭代器是调用约定好的begin、end函数,如果没提供begin、end函数或者提供的函数名不叫begin、end(例如Begin、End),那么范围for无法使用,报错内容还是没有找到begin和end函数

     9.实现insert函数的任意位置插入功能,这里实现两个版本的insert函数,分别实现单个字符的插入功能和字符串的插入功能

    对于单个字符的插入版本insert,如果要插入到pos位置处,那么首先应该判断_size是否等于_capacity是否需要扩容,其次应该把原字符串/0开始往前到pos位置处所有的字符都往后移动一个字符的大小,最后把insert函数第二个参数的字符ch放到pos位置处即可,如下图一所示

    注意这里pos可以等于_size,pos等于_size就相当于是尾插(那么前面的push_back函数可以直接复用insert(_size,ch),如下图二所示,append函数也可以直接复用insert(_size,str),如下图三所示)

     

    但是上面的代码在处理头插的时候是有问题的,while循环的判定条件是end>=pos,如果是头插,当end=0时进入循环将0位置处的字符移动到1位置处,然后end--,因为end是无符号的,所以end此时是无符号整型最大值,再次进入循环,造成了死循环。

    这里把end改成int类型依然是不行的,因为pos是size_t类型的,比较的时候会将end的int类型提升成unsigned int类型,因此把end改成int类型的同时,比较的时候也需要把pos强转成int类型,如下图所示。

     

    还有一种更好的方式是让end最开始的位置指向字符串尾/0后面的位置,每次移动将end前一个字符移到end处,然后end--,当end和pos相等时退出循环并将insert函数第二个参数的字符ch放到pos位置处即可,这样如果是头插,end=pos=0的时候循环终止,如下图所示。

    对于字符串的插入版本insert,首先要进行扩容,这里的扩容不能简单的扩容2倍,因为如果插入字符串的长度比原_capacity还要长,那么扩2倍空间也是不够的,这里应该判断插入字符串的长度加上原字符串长度_size是否大于_capacity,如果大于就要扩容到插入字符串的长度和原字符串长度_size之和的大小,如果小于等于就不用扩容;其次让end指向_size+len(字符串长度)的位置,每次移动将end-len处的字符移动到end位置处,然后end--,当end小于等于pos+len-1时退出循环;最后将要插入的字符串拷贝到从pos位置处开始的这一段空间中即可,如下图所示

    注意:这里要使用strncpy函数拷贝插入字符串的有效字符不能拷贝/0,这里不能使用strcpy,因为strcpy会把插入字符串的/0也拷贝过来

    如果上面的代码循环判断的条件是当end小于pos+len时退出循环,如下图所示,其实也是可以的,但是有一个极端情况是不行的,当insert(0,"")时,也就是pos是0,len是0的时候,end是一个无符号整型永远大于0,陷入死循环。上图所示的end小于等于pos+len-1时退出循环的判断条件遇到insert(0,"")时是没有问题的,因为此时pos是0,len是0,len是无符号整型和-1比较,-1要进行整型提示为无符号整型最大值,退出循环。因此判断条件写成上图所示的情况是更好的

    10.如果尾插一个/0,那么字符串最后就有两个/0,一个是有效字符的/0(算在_size个有效字符里的),一个是字符串的结束标志。如果使用cout<

    11.实现erase函数的任意位置删除功能,如下图一所示,其中第二个参数len给了缺省值npos,我们直到npos是无符号整型的-1值,因此先定义npos。要注意npos是静态的成员变量,定义的时候要在类外定义,并且库里面给的npos是用const修饰的,所以我们也用const修饰,如下图二所示

    实现erase函数的任意位置删除功能,要分析两种情况,第一种情况pos及后面字符的长度大于要删除的字符长度len,那么要把后面剩下的字符和/0移过来进行覆盖,_size-=len;第二种情况pos及后面字符的长度小于要删除的字符长度len,那么直接给pos处置/0,_size=pos即可,如下图所示 

    12.find函数的功能是从pos位置开始找某个字符或字符串的位置,实现需要分别实现对单个字符的查找和对字符串的查找,库中的find函数如下图所示,如果find函数找不到对应的字符或字符串则返回npos。

     实现对字符串的查找可以使用strstr函数,如果strstr函数没有找到对应的字符串则返回空指针,find函数的实现代码如下图一二所示

    13.流插入操作符重载operator<<和流提取操作符重载operator>>,前面讲过实现流插入操作符operator<<和流提取操作符重载operator>>要写成全局的函数,因为流插入操作符和流提取操作符第一个参数必须是cout和cin,如果写成类函数那第一个操作数默认是指向类对象的this指针。

    从下图可以看出,流插入操作符重载operator<<和流提取操作符重载operator>>不一定非要是类的友元,想要访问私有的类成员变量使用s.c_str即可

    实际上流插入操作符重载operator<<的实现是使用下面的代码,上图是打印一个字符串,下图是一个一个字符的打印,前面讲过,上图所示的方法遇到/0就结束了,如果字符串中有有效字符/0,那么有效字符/0和后面的字符都打印不出来,而下面的代码可以保证把每一个有效字符都打印出来

    实现流提取的时候,如果下一个输入缓冲区的内容不是空格或换行符就持续进行提取尾插进类里面,如下图所示

    上面代码其实是有问题的,原因出在cin上,cin其实是识别不了空格和换行的,也就是说上面的代码会陷入死循环。我们知道cin是istream类定义出的对象,我们应该使用istream类里的get成员函数,get成员函数是从输入缓冲区获取一个字符,可以识别空格和换行符,如下图所示

    如果输入的字符串非常的长,上面+=不停的扩容效率比较低,因此我们可以先给一个优化区间初始化为全/0,将提取到的字符依次放入优化区间中,如果优化区间满了,那么把优化区间的内容+=到对象中,优化区间再次初始化为全/0,然后继续将提取到的字符依次从前往后放入优化区间中......如果提取遇到空格或换行结束了,那么将优化区间的内容+=到对象中,如下图所示

    上面的代码有一个bug,如果对象s3里面已经有字符串了,再输入的话新输入的字符串内容会尾插在旧字符串的后面,如下图一所示,而std库中的string类是用新字符串代替旧字符串,如下图二所示

     

    因此,cin运算符重载函数被调用的时候首先应该调用clear函数清除一下string类对象里面的字符串,如下图所示

    注:getline函数的实现和operator<<的实现基本相同,只不过while判断那块有点不一样,getline函数遇到' '不停止遇到'\n'停止,所以这里就不实现getline函数了

    14.下图一所示的拷贝构造函数和赋值运算符重载函数的实现是一种传统写法,传统写法的特点是本分,老实干活,该开空间自己开空间,该拷贝数据就自己拷贝数据。现代写法本质还是进行深拷贝,如下图二所示的拷贝构造实现,但是它不是自己去做,它利用构造函数让中间变量tmp去做这个事情,做完了之后进行交换。

    但是上面这种新式写法是有一些问题的,进入拷贝对象的拷贝构造函数,上面的写法没有对拷贝对象进行初始化,因此成员变量是随机值,tmp和拷贝对象进行交换之后,tmp对象的成员变量变成随机值,而tmp是一个局部对象,出了作用域它要调用析构函数,析构函数delete释放tmp指向的内存空间,而tmp是随机值,系统崩溃,如下图所示

    改进方式如下图所示,对拷贝对象本身先进行初始化,这样交换之后的tmp对象成员变量就不再是随机值了,可以进行释放

    上面的写法可以改进,不使用库里面的swap函数,我们可以自己实现一个string类的swap函数,如下图一所示,那么改进的拷贝构造函数如下图二所示

    新式的赋值赋值运算符重载如下图所示,赋值赋值运算符重载函数既要将s对象的值类似深拷贝的给被赋值对象,又要将被赋值对象原来的空间释放掉。新式的实现里面临时对象tmp被构造成了对象s,然后被赋值的对象和临时对象tmp进行了交换,实现了将s对象的值类似深拷贝的给被赋值对象,还有一个工作是要将被赋值对象原来的空间释放掉,因为交换tmp对象现在指向被赋值对象原来的空间,tmp函数是局部变量,后面出了作用域会自动调用析构函数delete进行释放,这样被赋值对象原来的空间也被释放掉了

    上面的代码还有更简洁的写法,如下图所示,这种写法是将形参s对象当作临时变量,s是传值传参s改变不会影响实参对象,s对象进行交换完成了将s对象的值类似深拷贝的给被赋值对象的功能,出了作用域形参对象s会被释放完成了被赋值对象原来空间释放掉的功能

    注意:这里不判断自己给自己赋值也是没问题的(系统不会报错),只是利用形参s对象将相同的值再交换赋值给自己一遍

    15.以前我们要把字符串转整型,整型转字符串都是使用函数atoi和itoa,string还有两个函数stoi和to_string来更方便的实现这两个功能,此外还有字符串转字符型函数stod等,函数stoi和to_string介绍如下

    其中stoi的base参数是控制进制(按几进制来转),使用方式如下图所示


    4.扩展阅读及补充内容

    4.1.扩展阅读

    C++面试中string类的一种正确写法 | 酷 壳 - CoolShell

    STL 的string类怎么啦?_haoel的博客-CSDN博客

    4.2.补充内容​​​​​​​

    string类的大小是多少呢?

    ​​​​​​​string类里面存了一个指针类型成员变量、两个无符号整型成员变量总共12个字节大小,下图是vs2013编译器的测试结果,从下图来看并不是我们想的这样

    其实,vs2013编译器里面string类的成员变量类似如下图一所示设计的,这样写的好处是如果字符串大小小于等于16字节,那么就存在_buf里面,就不用再开空间了;字符串大小大于16字节,开辟空间给_ptr,全部存在_ptr指向的空间里面(相当于如果字符串大小大于16,那么_buf里面的空间就浪费了),如下图二所示

    上面讲的是vs2013编译器下的string类,不同编译器下甚至不同版本的vs编译器中string类成员变量设计可能不同(Linux下就不是这么设计的),那么string类的大小也不同

  • 相关阅读:
    软信天成:如何做好数据治理?这三点必不可少
    【Mac使用技巧】Mac和iPhone间接力功能失效解决总结
    springboot导入spring-boot-maven-plugin插件报错及打包项目到服务器上运行(手动导入加自动导入方法)-详细
    [100天算法】-连通网络的操作次数(day 46)
    高速缓存Cache详解(西电考研向)
    凉鞋的 Unity 笔记 108. 第二个通识:增删改查
    mysql 修改字段长度
    Jmeter系列-定时器Timers的基本介绍(11)
    VMware虚拟网络编辑器配置
    第4章_瑞萨MCU零基础入门系列教程之瑞萨 MCU 源码设计规范
  • 原文地址:https://blog.csdn.net/qq_45113223/article/details/127442247