• C++(第七篇):string 容器(介绍、使用、深浅拷贝、模拟实现、写时拷贝)


    前言

    ​ 在C语言中,字符串是以’\0’结尾的一些字符的集合。为了操作方便,C语言中还提供了一些控制字符串的函数例如strcpy,strcmp,strcat等等。但是这些函数与字符串是分离开的,并不符合C++封装的特性。于是C++中由单独产生了一个string类。

    📒博客主页:要早起的杨同学的博客
    🎉欢迎关注🔎点赞👍收藏⭐️留言📝
    📌本文所属专栏: 【C++拒绝从入门到跑路】
    ✉️坚持和努力一定能换来诗与远方!
    💬参考在线编程网站:🌐牛客网🌐力扣
    🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!

    本文相关练习题及讲解

    【C++】string类练习题


    了解标准库中的string类

    文档介绍:string - C++ Reference (cplusplus.com)

    string是一个类,string实例化的对象实际是一个字符数组。

    string在底层实际是:basic_string模板类的别名,typedef basic_string string

    在string类使用时,必须包含#include头文件和展开命名空间using namespace std

    下面介绍string类的一些常用接口,大部分都是string类的成员函数。如果由什么下面没有提到的接口或者不明白的可以查文档http://www.cplusplus.com/reference/string/

    看文档主要看三个板块:

    1. 接口函数的声明
    2. 接口函数的功能及参数和返回值说明
    3. 使用样例的代码

    补充

    • 编码:计算机中只存储二进制数(0 / 1),那如何去表示文字呢,需要制定对应的编码表,规定用哪些二进制数字表示(映射)哪个符号,当然每个人都可以约定自己的一套,而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,比如美国有关的标准化组织就出台了 ASCII 码表(基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言)

    • ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符,到目前为止共定义了 128 个字符。

    • 所以早期的计算机中只能表示英文,不能表示其它国家的文字,当全世界各个国家都开始使用计算机了,就需要建立出对应语言的编码表。

    • UTF - 8 是对不同范围的字符使用不同长度的编码,这样能够适应不同的语言,比如有些语言单字节编码就够了,有些语言需要多字节编码才够。

      image-20220501105903743

    • 还有其它编码表,GBK码(对多达2万多的简繁汉字进行了编码,简体版的Win95和Win98都是使用GBK作系统内码)等等。

    注意:使用 string 类需要包含头文件

    一. string类的常用接口说明

    1.1 string类对象的构造函数

    函数名称功能说明
    string()无参默认构造函数,构造空string类对象,即空字符串
    string(const char * s)用c字符串构造string类对象
    string(size_t n,char c)实例化的类对象中包含n个字符c
    string(const string& s)拷贝构造函数,将对象s拷贝构造一个新对象

    image-20220723214535239

    void Teststring()
    {
        string s1; // 构造空的string类对象s1
        string s2("hello world"); // 用C格式字符串构造string类对象s2
        string s3(s2); // 拷贝构造s3
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    👉思考:空串是什么都没有吗,存储空间为空吗?

    img

    1.2 string对象的容量函数

    函数名称功能说明
    size返回字符串中有效字符长度,对应字符串结尾的‘\0’的下标
    length返回字符串中有效字符长度
    capacity返回空间的总长度,字符长度size超过capacity会自动扩容
    empty检测字符串是否为空串,是返回true,不是返回false
    clear清空有效字符串,空间不释放,内容没改变,只是size变为0
    reserve为字符串预留空间,就是提前开辟一段你想要大小的空间,如果不用reserve,编译器会自己开辟一段空间。
    resize将有效字符串个数改成n个,n小于size,缩小size到n。n大于size,增大size到n,多出来的空间用字符c填充,不给c,用’\0’填充

    image-20220723214612449

    注意:length和size底层实现是一样的,引入size的原因是为了与其它容器保持一致,一般情况下用size。

    image-20220723214628413

    clear()函数只是将对象的size置0,不改变底层空间大小。

    👉resize 函数的两种重载形式:

    void resize (size_t n);
    void resize (size_t n, char c);
    
    • 1
    • 2

    image-20220723214738434

    resize(size_t n,char c),当n小于对象size时,直接改变对象的size到n。当n大于对象size时,直接增大对象size到n,多出来的空间用给的字符c填充,没给字符c默认用\0填充。

    👉reserve 函数介绍

    void reserve (size_t n = 0);
    
    • 1

    image-20220723214753348

    • reserve是提前将对象capacity空间直接开辟到某一值。

    • 如果reserve里的值开辟空间小于capacity值,不会改变capacity。

    • size始终都不会改变。

    为什么上面代码reserve(50),会扩容到63。因为字符保存时存在一定的内存对齐,一般是某一值的正数倍。

    这样看下面一个代码:

    image-20220723214912590

    如果提前知道套插入多少字符,提前开辟空间,防止扩容效率损失。

    image-20220723214825693

    思考:resize 和 reserve 的意义在哪里呢?

    reserve 的作用:如果知道需要多大的空间,可以利用 reserve 提前一次性把空间开好,避免增容带来的开销

    resize 的作用:既要开好空间,还要对这些空间初始化,就可以使用 resize

    1.3 string类对象的访问及遍历操作

    函数名称功能说明
    operator[][]操作符重载函数,返回对字符串中 pos 位置处的字符的引用(string 类对象支持随机访问)(一般物理地址是连续的才支持)
    begin()获取第一个字符位置的迭代器
    end()获取最后一个字符下一个位置的迭代器
    rbegin()反向迭代器(可以反向遍历对象),和end一样
    rend()反向迭代器,和begin一样
    范围forC++11支持的一种更简洁的范围for的新遍历方式

    image-20220512185848868

    image-20220723214846179

    说明:

    1. 因为重载了[]操作符,可以直接用**对象+[],**就像数组的形式访问字符。 .at(i) 越界抛异常, [] 越界断言报错。
    2. 迭代器现在的理解:string类里含有一个迭代器,所以定义一个迭代器要声明是string类里的,begin和end返回的是迭代器,可以用迭代器接收或者作比较。
    3. 关于迭代器:[begin(),end() ) end()返回的不是最后一个数据位置的迭代器,返回的是最后一个位置下一个位置。也要注意的是,C++中凡是给迭代器一般都是给的 [ ) 左闭右开的区间,迭代器是类似指针一样东西,循环判断就要用 不等于 !=。
    4. 迭代器的意义:像string、vector支持[]遍历,但是list、map等容器不支持[],我们就要用迭代器遍历,所以迭代器是一种统一使用的方式。

    对于const类型的对象有对应的const类型的迭代器

    image-20220711205013798

    iterator 遍历 可读可写

    const_iterator const对象遍历 只读

    反向迭代器

    image-20220711204301043

    也是使用++操作符,如果按照指针来理解的话就是从字符串的末尾依次--往前走。

    总结一下,一共四种迭代器

    1. iterator
    2. const_iterator
    3. reverse_iterator
    4. const_reverse_iterator

    前两个用的多

    1.4 string类修改插入操作

    函数名称功能
    push_back(char c)在字符串尾插字符
    append(const char * s)常用的参数是一个字符串类型的参数,也可以使用迭代器
    insert(size_t pos, char* s)第一个参数是位置,第二个参数是字符或字符串。在某一个位置开始插入字符串,给的位置要合法,不合法会报错。
    erase(char* s, size_t len = npos)第一个参数是开始地址,第二个参数是删除的字符数len,从起始位置开始删除len个字符,不给参数值就是默认从给的地址开始删完。
    operator+=(常用)在字符串尾插字符或字符串
    c_str返回c格式字符串
    find(char c,size_t pos)在字符串pos位置开始往后找字符或字符串,返回该字符在字符串中第一次出现位置,不输入位置默认从开始往后找
    rfind在字符串pos位置开始往前找字符或字符串,返回该字符在字符串中第一次出现位置,不输入位置默认从后往前找
    substr(size_t pos, szie_t len)在字符串中从pos位置开始,截取len个字符,然后返回
    sort(s.begin(), s.end())排序,s是string类对象,参数是迭代器
    nposstring的静态成员变量,static const size_t npos=-1;全1,size_t为无符号整形,实际40几亿

    👉append 用法

    image-20220711211444312

    image-20220723214947705

    push_back在字符串后尾插字符,append在对象后尾插字符串,+=重载即可尾插字符又可尾插字符串,所以常用+=。

    注意

    1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
    2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

    image-20220724103002989

    这样看不出什么不同,但是看下面这样。

    image-20220723215016617

    原生打印是全部字符都打印出来(包括‘\0’),c_str是遇到‘\0’截止。

    由于C语言字符串以\0结尾所以ret只打印了hello,但是打印对象也没有将\0打印出来,这是因为有的字符在输出框是不可见的\0就属于不可见的。

    👉substr()接口

    image-20220723215044952

    取出url中的域名

    image-20220723215122610

    以上图代码为例,

    i1等于4,指向冒号:。

    i2从i1+3(指向第一个w)位置找第一个出现的 / 并指向这个/

    补充

    函数名称功能
    insert(pos,char * s)在pos位置插入字符或者有效字符串,给的位置要合法,不合法会报错
    erase(pos,n)从pos位置开始删除n个字符,不给参数n传值就是默认从pos位置开始删完
    1. 尽量少用insert,因为底层实现是数组,头部或者中间插入需要挪动数据
    2. erase要删除的字符数多于字符串本身长度不会报错,有多少删多少
    3. erase如果第一第二个参数都不给就是默认删除整个字符串

    1.5 string 非成员函数

    函数功能
    operator+一对象加上字符或者字符串,返回一对象,对象自身不改变。少用,返回不是引用。效率低
    operator<<输出操作符重载函数
    operator>>输入操作符重载函数
    getline(cin,对象)获取一行字符,包括空格。
    relational operators (string)比较大小

    cin输入遇到空格,换行结束。getline遇到换行结束。

    例题

    仅仅反转字母

    class Solution {
    public:
        string reverseOnlyLetters(string S) 
        {
            char* pLeft = (char*)S.c_str();
            char* pRight = pLeft + (S.size()-1);
            while(pLeft < pRight)
            {
                // 从前往后找,找到一个字母
                while(pLeft < pRight)
                {
                    // 找到有效字母后停下来
                    if(isalpha(*pLeft))
                        break;
                    ++pLeft;
                }
                // 从后往前找,找一个字母
                while(pLeft < pRight)
                {
                    // 找到有效字母后停下来
                    if(isalpha(*pRight))
                        break;
                    --pRight;
                }
                if(pLeft < pRight)
                {
                    swap(*pLeft, *pRight);
                    ++pLeft;
                    --pRight;
                }
            }
            return S;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    1.6 补充一些接口

    C 语言库文件 中的处理 C 字符的接口

    • 字符处理函数:

      函数名称功能说明
      int isalpha(int c)检查字符是否为字母,是返回非零(true),不是则返回0(false)
      int isdigit(int c)检查字符是否为十进制数字,是返回非零(true),不是则返回0(false)
    • 字符转换函数:

      函数名称功能说明
      int tolower(int c)把字母转换成小写字母,返回转换之后的字符
      int toupper(int c)把字母转换成大写字母,返回转换之后的字符

    头文件 中:

    头文件 中:

    • 函数 std::reverse:反转范围 [first,last) 中元素的顺序。

      // 传一段迭代器区间 [first, last)
      template <class BidirectionalIterator> // 双向迭代器
        void reverse (BidirectionalIterator first, BidirectionalIterator last);
      123
      
      • 1
      • 2
      • 3
      • 4
    • 函数 std::sort:将 [first,last) 范围内的元素按升序排序。

      // 传一段迭代器区间 [first, last),默认排升序,若要排降序,需要传仿函数
      template <class RandomAccessIterator, class Compare> // 随机访问迭代器
        void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
      123
      
      • 1
      • 2
      • 3
      • 4

      👉Example:

      // 也可传数组,因为指向数组空间的指针是天然的迭代器
      int arr[] = { 1, 5, 4, 2, 3 };
      sort(arr, arr + 5);
      
      • 1
      • 2
      • 3

    二. string类的模拟实现

    2.1 深拷贝和浅拷贝

    上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。string类如果采用浅拷贝的方式,会造成一些问题:

    image-20220723223131759

    说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

    ① 浅拷贝

    浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。

    ② 深拷贝

    如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

    image-20220723223314410

    2.2 模拟实现string类

    string是一个管理字符数组的类,要求这个字符数组结尾用’\0’标识,我们需要实现以下接口:

    1、拷贝构造和赋值重载实现深拷贝
    2、增删查改的相关接口(跟顺序表类似)
    3、重载一些常见运算符。如: >、<、<<、>>、[]…
    4、迭代器

    在上文中我们熟知了C++string类的一些常用接口的使用,下文我们来简单实现一下这些常用的接口,带你了解string类的底层原理,让你能轻而易举的掌握string类的使用。

    说明注意点:在我们自己实现string类时,如果你的类名用的与标准库类名相同时,需要一个命名空间将其与标准库里的string类分开。

    下面是代码的分开实现,完整代码在最下面给出。

    构造,析构函数和赋值操作符的重载

    //要在堆上开辟空间将字符串拷贝过去,不能直接在初始化列表里初始化,否则不可修改
    //默认构造函数
    string(const char* str = "")//空串里面有一个\0
        :_str(new char[strlen(str)+1]) //空间大小等于有效字符串个数+\0
        {
            strcpy(_str, str);
            _size = strlen(_str);
            _capacity = _size;
        }
    //传统写法
    //拷贝构造
    //string(const string& s)
    //	:_str(new char[strlen(s._str) + 1])
    //{
    //	strcpy(_str, s._str);
    //	_size = strlen(_str);
    //	_capacity = _size;
    //}
    赋值运算符重载
    //string& operator=(const string& s)
    //{
    //	if (this != &s)
    //	{
    //		delete[] _str;
    //		_str = new char[strlen(s._str) + 1];
    //		strcpy(_str, s._str);
    //		_size = s._size;
    //		_capacity = s._capacity;
    //	}
    
    //	return *this;
    //}
    
    //现代写法 多写一个交换函数
    //创建一个临时对象,然后将临时对象的值交换过去,函数结束后,临时对象自动销毁
    //_str会交换指向空间的地址
    //构造一个和S1一样的tmp,然后交换S2和tmp成员变量里面保存的地址,这样S2的成员变量就指向了开辟的空间。
    //S2的成员变量必须要被初始化为nullptr,否则交换值后调用tmp的析构函数会报错。
    string(const string& s)
        :_str(nullptr)//要赋空指针,保证tmp析构无误
            ,_size(0)
            ,_capacity(0)
        {
            string tmp(s._str);
            swap(tmp); // 等价于this->swap(tmp);即(*this).swap(tmp);调用的是我们实现的
        }
    string& operator=(string ps)
    {
        swap(ps);
        return *this;
    }
    void swap(string& s)//我们自己实现的swap
    {
        ::swap(_str, s._str);//调用全局域的swap函数(库里的函数)
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);
    }
    //析构函数
    ~string()
    {
        delete[] _str;
        _str = nullptr;
        _size = 0;
        _capacity = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    注意拷贝构造和赋值操作符重载函数,这里蕴含着深浅拷贝问题。具体细节见上文的讲解。 如果不定义着两个函数,编译器会自动生成,但是仅仅只是按字节进行拷贝,也就是值拷贝/浅拷贝。在上面函数中,如果只是进行浅拷贝,两字符指针变量指向堆上的同一块空间。当对象出作用域时,调用析构函数,但是同一块空间不能释放两次,会导致程序奔溃。

    解决方法就是使用深拷贝

    在堆上再申请一块空间,将拷贝值或者赋值值copy过去。将需要被赋值或者被拷贝的对象的字符指针指向新申请的空间。

    容量函数,operator[]重载函数和c_str、clear函数

    //capacity 容量相关接口
    size_t size()const
    {
        return _size;
    }
    size_t capacity()const
    {
        return _capacity;
    }
    bool empty()const
    {
        return _size == 0;
    }
    // []
    char& operator[](size_t index)
    {
        assert(index < _size);
        return _str[index];
    }
    
    const char& operator[](size_t index)const
    {
        assert(index < _size);
        return _str[index];
    }
    
    void clear()
    {
        _size = 0;
        _str[0] = '\0';
    }
    const char* c_str() const
    {
        return _str;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    resize和reserve函数

    void resize(size_t n, char c = '\0')
    {
        if (n < _size)
        {
            _size = n;
            _str[_size] = '\0';
        }
        else
        {
            //n比容量大先扩容
            if (n > _capacity)
            {
                reserve(n);
            }
            for (size_t i = _size; i < n; i++)
            {
                _str[i] = c;
            }
            _str[n] = '\0';
            _size = n;
    
        }
    }
    void reserve(size_t n)
    {
        if (n > _capacity)
        {
            _capacity = n;
            char* tmp = new char[_capacity + 1];
            strncpy(tmp, _str, _size+1);
    
            delete[] _str;
            _str = tmp;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    插入和删除函数

    void push_back(char c)
    {
        if (_size == _capacity)
        {
            reserve(_capacity == 0 ? 4 : _capacity * 2);
        }
        _str[_size] = c;
        _str[_size + 1] = '\0';
        ++_size;
    }
    string& operator+=(char c)
    {
        push_back(c);
        return *this;
    }
    
    void append(const char* str)
    {
        size_t len = _size + strlen(str);
        if (len > _capacity)
        {
            reserve(len);
        }
        strcpy(_str + _size, str);
        _size = len;
    }
    
    string& operator+=(const char* str)
    {
        append(str);
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    查找函数和比较函数

    bool operator<(const string& s)
    {
        char* end1 = _str;
        char* end2 = s._str;
        while (*end1 == *end2)
        {
            end1++;
            end2++;
        }
        if (*end1 < *end2)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    bool operator<=(const string& s)
    {
        return (*this < s || *this == s);
    }
    
    bool operator>(const string& s)
    {
        return !(*this <= s);
    }
    
    bool operator>=(const string& s)
    {
        return (*this > s || *this == s);
    }
    
    bool operator==(const string& s)
    {
        char* end1 = _str;
        char* end2 = s._str;
        while (*end1 == *end2)
        {
            if (*end1 == '\0')
            {
                return true;
            }
            end1++;
            end2++;
        }
        return false;
    }
    
    bool operator!=(const string& s)
    {
        return !(*this == s);
    }
    
    // 返回c在string中第一次出现的位置
    size_t find(char c, size_t pos = 0) const
    {
        assert(pos < _size);
        for (size_t i = pos; i < _size; i++)
        {
            if (_str[i] == c)
            {
                return i;
            }
        }
        return npos;
    }
    
    // 返回子串s在string中第一次出现的位置
    size_t find(const char* str, size_t pos = 0) const
    {
        assert(pos < _size);
        const char* ret = strstr(_str + pos, str);
        if (ret)
        {
            return ret - _str;
        }
        else
        {
            return npos;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    插入和删除函数

    // 在pos位置上插入字符c/字符串str,并返回该字符串
    string& insert(size_t pos, const char c)
    {
        assert(pos < _size + 1);
        // xxxxxxxxc\0
        *this += c; // 能实现扩容
        //循环也可以用指针实现
        for (int i = _size - 1; i > pos; i--)
        {
            _str[i] = _str[i - 1];
        }
        _str[pos] = c;
        return *this;
    }
    string& insert(size_t pos, const char* str)
    {
        assert(pos <= _size);//等于_size相当于尾插
        int len = strlen(str);
        *this += str;
        //i-len等于pos的时候把pos位置的数据挪走
        for (int i = _size - 1; i - len != pos - 1; i--)
        {
            _str[i] = _str[i - len];
    
        }
        strncpy(_str + pos, str, len);
        return *this;
    }
    //或者
    string& insert2(size_t pos, const char* str)
    {
        assert(pos <= _size);//等于_size相当于尾插
        size_t len = strlen(str);
        if (_size + len > _capacity)
        {
            reserve(_size + len);
        }
    
        char* end = _str + _size;
        while (end >= _str + pos)
        {
            *(end + len) = *end;
            --end;
        }
        strncpy(_str + pos, str, len);
        _size += len;
        return *this;
    }
    
    // 删除pos位置上的元素,并返回该元素的下一个位置
    string& erase(size_t pos, size_t len = npos)//npos默认删完
    {
        assert(pos < _size);
        //要删的元素个数比剩余的元素多
        //不用额外判断npos,-1传给size_t会类型转换
        if (len >= _size - pos)
        {
            _str[pos] = '\0';
            _size = pos;
        }
        //剩余字符多
        else
        {
            循环写法
            +1 保存\0
            //for (int i = pos; i < _size - len + 1; i++)
            //{
            //	_str[i] = _str[i + len];
            //}
            strcpy(_str + pos, _str + pos + len);
            _size -= len;
    
        }
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    operator<<和operator>>操作符重载及getline函数

    这三个函数在全局域(我们定义的命名空间)中实现,我们在前面的博客中提到过,因为会有争夺第一参数位置的问题。

    ostream& operator<<(ostream& out, const string& s)
    {
        for (auto e : s)
        {
            out << e;
        }
        return out;
    }
    istream& operator>>(istream& in, string& s)
    {
        s.clear();
    
        char ch;
        ch = in.get();
        while (ch != ' ' && ch != '\n')
        {
            s += ch;
            ch = in.get();
        }
        return in;
    }
    istream& getline(istream& in, string& s)
    {
        s.clear();
    
        char ch;
        ch = in.get();
        while (ch != '\n')
        {
            s += ch;
            ch = in.get();
        }
        return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    完整代码

    #pragma once
    #include
    #include
    using  std::cout;
    using  std::cin;
    using  std::endl;
    using  std::ostream;
    using  std::istream;
    using  std::swap;
    namespace yzy
    {
    	class string
    	{
    	public:
    		typedef char* iterator;
    		const iterator begin() const
    		{
    			return _str;
    		}
    		const iterator end() const
    		{
    			return _str + _size;
    		}
    		///
    		//默认构造函数
    		string(const char* str = "")//空串里面有一个\0
    			:_str(new char[strlen(str)+1]) //空间大小等于有效字符串个数+\0
    		{
    			strcpy(_str, str);
    			_size = strlen(_str);
    			_capacity = _size;
    		}
    		//传统写法
    		//拷贝构造
    		//string(const string& s)
    		//	:_str(new char[strlen(s._str) + 1])
    		//{
    		//	strcpy(_str, s._str);
    		//	_size = strlen(_str);
    		//	_capacity = _size;
    		//}
    		赋值运算符重载
    		//string& operator=(const string& s)
    		//{
    		//	if (this != &s)
    		//	{
    		//		delete[] _str;
    		//		_str = new char[strlen(s._str) + 1];
    		//		strcpy(_str, s._str);
    		//		_size = s._size;
    		//		_capacity = s._capacity;
    		//	}
    
    		//	return *this;
    		//}
    		//现代写法
    		string(const string& s)
    			:_str(nullptr)
    			,_size(0)
    			,_capacity(0)
    		{
    			string tmp(s._str);
    			swap(tmp); // 等价于this->swap(tmp);即(*this).swap(tmp);
    		}
    		string& operator=(string ps)
    		{
    			swap(ps);
    			return *this;
    		}
    		void swap(string& s)
    		{
    			::swap(_str, s._str);
    			::swap(_size, s._size);
    			::swap(_capacity, s._capacity);
    		}
    
    		//析构函数
    		~string()
    		{
    			delete[] _str;
    			_str = nullptr;
    			_size = 0;
    			_capacity = 0;
    		}
    		
    		//capacity 容量相关接口
    		size_t size()const
    		{
    			return _size;
    		}
    		size_t capacity()const
    		{
    			return _capacity;
    		}
    		bool empty()const
    		{
    			return _size == 0;
    		}
    		//不完整的resize
    		//void resize(size_t n, char c = '\0')
    		//{
    		//	reserve(n);
    		//	if (n < _size)
    		//	{
    		//		_size = _capacity;
    		//		_str[_size] = '\0';
    		//	}
    		//	else
    		//	{
    		//		for (size_t i = _size; i < _capacity; i++)
    		//		{
    		//			_str[i] = c;
    		//		}
    		//		_str[n] = '\0';
    		//		_size = _capacity;
    		//	}	
    		//}
    		void resize(size_t n, char c = '\0')
    		{
    			if (n < _size)
    			{
    				_size = n;
    				_str[_size] = '\0';
    			}
    			else
    			{
    				//n比容量大先扩容
    				if (n > _capacity)
    				{
    					reserve(n);
    				}
    				for (size_t i = _size; i < n; i++)
    				{
    					_str[i] = c;
    				}
    				_str[n] = '\0';
    				_size = n;
    				
    			}
    		}
    		void reserve(size_t n)
    		{
    			if (n > _capacity)
    			{
    				_capacity = n;
    				char* tmp = new char[_capacity + 1];
    				strncpy(tmp, _str, _size+1);
    
    				delete[] _str;
    				_str = tmp;
    			}
    		}
    		
    		//modify
    		void push_back(char c)
    		{
    			if (_size == _capacity)
    			{
    				reserve(_capacity == 0 ? 4 : _capacity * 2);
    			}
    			_str[_size] = c;
    			_str[_size + 1] = '\0';
    			++_size;
    		}
    		string& operator+=(char c)
    		{
    			push_back(c);
    			return *this;
    		}
    
    		void append(const char* str)
    		{
    			size_t len = _size + strlen(str);
    			if (len > _capacity)
    			{
    				reserve(len);
    			}
    			strcpy(_str + _size, str);
    			_size = len;
    		}
    
    		string& operator+=(const char* str)
    		{
    			append(str);
    			return *this;
    		}
    
    		void clear()
    		{
    			_size = 0;
    			_str[0] = '\0';
    		}
    
    		const char* c_str() const
    		{
    			return _str;
    		}
    		//
    		// []
    		char& operator[](size_t index)
    		{
    			assert(index < _size);
    			return _str[index];
    		}
    
    		const char& operator[](size_t index)const
    		{
    			assert(index < _size);
    			return _str[index];
    		}
    		//
    	    //relational operators
    		bool operator<(const string& s)
    		{
    			char* end1 = _str;
    			char* end2 = s._str;
    			while (*end1 == *end2)
    			{
    				end1++;
    				end2++;
    			}
    			if (*end1 < *end2)
    			{
    				return true;
    			}
    			else
    			{
    				return false;
    			}
    		}
    
    		bool operator<=(const string& s)
    		{
    			return (*this < s || *this == s);
    		}
    
    		bool operator>(const string& s)
    		{
    			return !(*this <= s);
    		}
    
    		bool operator>=(const string& s)
    		{
    			return (*this > s || *this == s);
    		}
    
    		bool operator==(const string& s)
    		{
    			char* end1 = _str;
    			char* end2 = s._str;
    			while (*end1 == *end2)
    			{
    				if (*end1 == '\0')
    				{
    					return true;
    				}
    				end1++;
    				end2++;
    			}
    			return false;
    		}
    
    		bool operator!=(const string& s)
    		{
    			return !(*this == s);
    		}
    
    		// 返回c在string中第一次出现的位置
    		size_t find(char c, size_t pos = 0) const
    		{
    			assert(pos < _size);
    			for (size_t i = pos; i < _size; i++)
    			{
    				if (_str[i] == c)
    				{
    					return i;
    				}
    			}
    			return npos;
    		}
    
    		// 返回子串s在string中第一次出现的位置
    		size_t find(const char* str, size_t pos = 0) const
    		{
    			assert(pos < _size);
    			const char* ret = strstr(_str + pos, str);
    			if (ret)
    			{
    				return ret - _str;
    			}
    			else
    			{
    				return npos;
    			}
    		}
    
    		// 在pos位置上插入字符c/字符串str,并返回该字符串
    		string& insert(size_t pos, const char c)
    		{
    			assert(pos < _size + 1);
    			// xxxxxxxxc\0
    			*this += c; // 能实现扩容
    			//循环也可以用指针实现
    			for (int i = _size - 1; i > pos; i--)
    			{
    				_str[i] = _str[i - 1];
    			}
    			_str[pos] = c;
    			return *this;
    		}
    		string& insert(size_t pos, const char* str)
    		{
    			assert(pos <= _size);//等于_size相当于尾插
    			int len = strlen(str);
    			*this += str;
    			//i-len等于pos的时候把pos位置的数据挪走
    			for (int i = _size - 1; i - len != pos - 1; i--)
    			{
    				_str[i] = _str[i - len];
    
    			}
    			strncpy(_str + pos, str, len);
    			return *this;
    		}
    		//或者
    		string& insert2(size_t pos, const char* str)
    		{
    			assert(pos <= _size);//等于_size相当于尾插
    			size_t len = strlen(str);
    			if (_size + len > _capacity)
    			{
    				reserve(_size + len);
    			}
    
    			char* end = _str + _size;
    			while (end >= _str + pos)
    			{
    				*(end + len) = *end;
    				--end;
    			}
    			strncpy(_str + pos, str, len);
    			_size += len;
    			return *this;
    		}
    
    		// 删除pos位置上的元素,并返回该元素的下一个位置
    		string& erase(size_t pos, size_t len = npos)//npos默认删完
    		{
    			assert(pos < _size);
    			//要删的元素个数比剩余的元素多
    			//不用额外判断npos,-1传给size_t会类型转换
    			if (len >= _size - pos)
    			{
    				_str[pos] = '\0';
    				_size = pos;
    			}
    			//剩余字符多
    			else
    			{
    				循环写法
    				+1 保存\0
    				//for (int i = pos; i < _size - len + 1; i++)
    				//{
    				//	_str[i] = _str[i + len];
    				//}
    				strcpy(_str + pos, _str + pos + len);
    				_size -= len;
    
    			}
    			return *this;
    		}
    	
    	private:
    		char* _str;
    
    		size_t _size;
    		size_t _capacity;
    
    		static const size_t npos;
    	};
    	const size_t string::npos = -1;
    
    	ostream& operator<<(ostream& out, const string& s)
    	{
    		for (auto e : s)
    		{
    			out << e;
    		}
    		return out;
    	}
    	istream& operator>>(istream& in, string& s)
    	{
    		s.clear();
    
    		char ch;
    		ch = in.get();
    		while (ch != ' ' && ch != '\n')
    		{
    			s += ch;
    			ch = in.get();
    		}
    		return in;
    	}
    	istream& getline(istream& in, string& s)
    	{
    		s.clear();
    
    		char ch;
    		ch = in.get();
    		while (ch != '\n')
    		{
    			s += ch;
    			ch = in.get();
    		}
    		return in;
    	}
        //测试函数
    	void Test_String1()
    	{
    		string s1("good morning");
    		cout << s1.c_str() << endl;
    		string s2(s1);
    		cout << s2.c_str() << endl;
    		string s3 = "good";
    		cout << s3.c_str() << endl;
    		s3 = s1;
    		cout << s3.c_str() << endl;
    	}
    	void Test_String2()
    	{
    		string s1("good morning");//12
    		cout << s1.size() << endl;
    		cout << s1.capacity() << endl;
    		cout << s1.empty() << endl;
    		s1.reserve(20);
    		cout << s1.c_str() << endl;
    		cout << s1.capacity() << endl;
    		s1.resize(10, 'x');
    		cout << s1.c_str() << endl;
    		cout << s1.capacity() << endl;
    
    	}
    	void Test_String3()
    	{
    		string s1("good morning");//12
    		s1 += " afternoon";//10
    		cout << s1.c_str() << endl;
    		cout << s1.size() << endl;
    		cout << s1.capacity() << endl;
    		s1 += '!';//1
    		cout << s1.c_str() << endl;
    		cout << s1.size() << endl;
    		cout << s1.capacity() << endl;
    
    		string::iterator it = s1.begin();
    		s1[0] = 's';
    		while (it != s1.end())
    		{
    			cout << *it << " ";
    			it++;
    		}
    	}
    	void Test_String4()
    	{
    		string s1 = "abcdf";
    		string s2 = "abcde";
    		cout << (s1 < s2) << endl;
    		cout << (s1 <= s2) << endl;
    		cout << (s1 > s2) << endl;
    		cout << (s1 >= s2) << endl;
    		cout << (s1 == s2) << endl;
    		cout << (s1 != s2) << endl;
    	}
    	void Test_String5()
    	{
    		string s1 = "abcde";
    		cout << s1.find('b', 1) << endl;
    		s1.insert(0, 'x');
    		cout << s1.c_str() << endl;
    		s1.insert2(0, "qqq");
    		cout << s1.c_str() << endl;
    		s1.erase(2,6);
    		cout << s1.c_str() << endl;
    
    	}
    	void Test_String6()
    	{
    		string s1("hello world");
    		s1.resize(20, 'x');
    		s1 += "!!!";
    		cout << s1 << endl;
    		cout << s1.c_str() << endl;
    		string s2("hello world");
    		cin >> s2;
    		cout <<"s2:" << s2 << endl;
    
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498

    不同的编译器实现的方式不同,结构不同,但是增删查改的逻辑和用法都是一样的。如果同样的代码在另一个编译器上不通过,这是正常的。

    三. 写时拷贝(了解)

    首先回顾一下浅拷贝引发的问题:

    1. 同一块空间会被析构(释放)多次
    2. 一个对象修改会影响另外一个对象

    为了解决这两个问题:

    1. 为了应对同一块空间会被析构多次这个问题,提出了引用计数。

      引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

    2. 为了应对一个对象修改会影响另外一个对象这个问题,提出了写时拷贝计数。

      写时拷贝就是一种拖延症,是在「浅拷贝」的基础之上增加了引用计数的方式来实现的。

      多个对象共用同一块内存空间,哪个对象去写数据,哪个对象就再进行深拷贝,本质是一种延迟深拷贝。当然,如果不进行写数据,那就不用进行深拷贝,提高了效率。

      但这种方案也是有副作用的,现在基本上也被放弃了。

    推荐文章:

    👉我们来验证一下 STL string 是否用的是写时拷贝技术

    #include
    #include
    int main()
    {
    	std::string s1("hello world");
    	std::string s2(s1);              // 拷贝构造
    	printf("%p\n", s1.c_str()); // c_str()函数返回其指向字符数组的地址
    	printf("%p\n", s2.c_str());
    	// 修改
        s2[0] = 'x';
        printf("%p\n", s1.c_str());
    	printf("%p\n", s2.c_str());
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果(VS2019 下 PJ 版本的 STL):没有用写时拷贝技术,直接深拷贝。

    image-20220508161244248

    运行结果(Linux 下 SGI 版本的 STL):这个早期15年的版本用了写时拷贝技术,加上了引用计数。

    最新版本的 gcc 编译器

    image-20220508161619966

  • 相关阅读:
    超级炫酷的终端神器 eDEX-UI
    Cobbler 服务搭建及Cobbler api 使用
    安全学习DAY23_Cookie&Session&Token
    Matlab中fdatool结合STM32F4设计滤波器
    shell-运算符
    编译原理--中间代码优化算法总结
    Linux操作系统使用及C高级编程-D9D10Linux 服务搭建与使用
    Kubernetes学习笔记-StatefulSet:部署有状态的多副本应用(3)20220626
    【第十二篇】Camunda系列-事件篇-信号事件
    时间序列(四):单变量时间序列的神经网(LSTM)
  • 原文地址:https://blog.csdn.net/Y673789476/article/details/126328338