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

发布于:2023-01-18 ⋅ 阅读:(242) ⋅ 点赞:(0)

前言

​ 在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<char, char_traits, allocator> string

在string类使用时,必须包含#include<string>头文件和展开命名空间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
}

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

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);

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);

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一样
范围for C++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类对象,参数是迭代器
npos string的静态成员变量,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.6 补充一些接口

C 语言库文件 <ctype.h> 中的处理 C 字符的接口

  • 字符处理函数:

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

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

头文件 <string> 中:

头文件 <algorithm> 中:

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

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

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

    👉Example:

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

二. 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;
}

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

解决方法就是使用深拷贝

在堆上再申请一块空间,将拷贝值或者赋值值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;
}

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;
    }
}

插入和删除函数

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;
}

查找函数和比较函数

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;
}

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;
}

完整代码

#pragma once
#include<iostream>
#include<assert.h>
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. 一个对象修改会影响另外一个对象

为了解决这两个问题:

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

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

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

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

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

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

推荐文章:

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

#include<iostream>
#include<string>
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;
}

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

image-20220508161244248

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

最新版本的 gcc 编译器

image-20220508161619966

本文含有隐藏内容,请 开通VIP 后查看