【C++】一文带你吃透string的模拟实现 (万字详解)(上)

简介: 【C++】一文带你吃透string的模拟实现 (万字详解)(上)

一. 构造 & 拷贝构造 & 赋值重载 & 析构 & 赋值重载


🎨传统写法


【面试题】实现一个简洁的string类,即只考虑_str这个成员,着重考察深浅拷贝


💢1.构造函数


构造函数,我们一般是带参/无参两种构造方式。其中文档可查到无参时默认构造空串"" (隐含了’\0’),给一个缺省值即可,注意不是给空指针nullptr( c_str会出错 )


0a2653c851af460fa595bd959398a8f1.png


string(const char* str = "")
{
  _size = strlen(str);//复用  —— 且不用关心初始化顺序
  _capacity = _size;
  _str = new char[_capacity + 1];
  strcpy(_str, str);
}


ps:capacity是指能存储有效字符的个数,因此开空间时要注意给\0预留空间,string类要特别注意\0的存在


💢2.拷贝构造


我们知道,拷贝构造&赋值重载这两个特殊的成员函数,如果自己不写编译器会自动生成。这份默认的拷贝构造(赋值重载)对于内置类型完成浅拷贝;对于自定义类型会调用它的拷贝构造(赋值重载)


如果使用默认生成的默认构造,完成的是浅拷贝,析构时会崩溃~~ 看图


0a2653c851af460fa595bd959398a8f1.png


对此我们要实现深拷贝,动态开辟一个空间,实现数据独立


//s2(s1)  拷贝构造
string(const string& s)
  :_str(new char[s._capacity+1])
  ,_size(s._size)
  ,_capacity(s._capacity)
{
  strcpy(_str, s._str);
}


💢3.赋值重载


如果使用默认生成的赋值重载 ,会导致以下问题 ——


0a2653c851af460fa595bd959398a8f1.png


我们还是要实现深拷贝,释放旧空间

有些点要注意:


上来最好不要直接销毁,有可能自己给自己赋值s3=s3,导致把自身释放了,所以要判断一下地址是否相同

string& operator=(const string& s)
{
  if (this != &s)
  {
  delete[] _str;
  _str = new char[strlen(s._str)];
  strcpy(_str, s._str);
  _size = s._size;
  _capacity = s._capacity;
  return *this;
  }
}


还有没有可能会new失败呢 ?还没有new成功我们就把_str给释放了,为此我先开一个中转的空间,并且给给tmp,若开辟成功才释放_str

string& operator=(const string& s)
{
  if (this != &s)
  {
  char* tmp = new char[s._capacity + 1];
  strcpy(tmp, s._str);
  delete[] _str;
  _str = tmp;
  _size = s._size;
  _capacity = s._capacity;
  }
}


💢4.析构函数

前面都是 new[] 开的空间,也要delete[]释放


~string
{
   delete[] _str;
   _str = nullptr;
   _size = _capacity = 0;
}


以上均为传统写法,但现在都什么年代?还在用传统写法??


🎨现代写法(资本家)


传统写法就是老老实实,一步一个脚印的开空间、拷贝,现代写法更注重灵活,投机取巧,要对前面比较熟悉才能掌握


🌍1.构造函数 & 析构函数


现代写法多是复用的思维,构造函数 & 析构函数只能老老实实


🌍2.拷贝构造


复用含参构造出tmp(打工人),把tmp和s2身体互换


0a2653c851af460fa595bd959398a8f1.png


注意s2的_str必须给nullptr,如果不置空,会把随机值传给tmp,tmp是局部变量出作用域时调用析构函数,会崩溃


//现代写法 —— 复用构造 —— 找替死鬼
string(const string& s)
  :_str(nullptr)
  ,_size(0)
  ,_capacity(0)
{
  string tmp(s._str);
  swap(tmp);//this -> swap(tmp)
}


🌍3.赋值重载

同样复用拷贝构造进行深拷贝,


0a2653c851af460fa595bd959398a8f1.png


string& operator=(const string& s)
{
  if (this != &s)
  {
  string tmp(s._str);
  swap(tmp); //this ->swap(tmp)
  return *this;
  }
}


更加剥削的版本:形参s同样是局部变量,出作用域也会调用析构函数 ——


// s1 = s3;
//s传参时顶替tmp当打工人,s就是s3的深拷贝
string& operator=(string s)
{
  swap(s);
  return *this;
}


🎨swap的区别


string的标准库中提供了一个swap,全局也有一个swap的模板函数(适用于内置类型),底层都是对两个对象的成员进行交换,结果相同,那为什么还要有呢string类中的?


std::string s1("hello");
std::string s2("one and only");
s1.swap(s2);
swap(s1, s2);


注意:二者不构成函数重载!!都不在一个域(函数重载的前提)


string类的中的成员函数仅仅是对对成员函数进行交换,效率高,而全局的swap则进行了三次深拷贝(一次拷贝+两次赋值),空间损耗很大


0a2653c851af460fa595bd959398a8f1.png


我们要交换三个成员,于是封装了一个swap,为了避免命名冲突,swap中的swap要指定库中的作用域,不然是先从局部开始找,发现参数不匹配就会报错


void swap(string& tmp)
{
  std:: swap(_str, tmp._str);
  std::swap(_size, tmp._size);
  std::swap(_capacity, tmp._capacity);
}


如果在模拟赋值重载时用全局的swap,会发生栈溢出


0a2653c851af460fa595bd959398a8f1.png


二. 基本接口


一些常用的接口,实现不难,但要注意小细节


🌈size & capacity


size_t size() const
{
  return _size;
}
size_t capacity() const
{
  return _capacity;
}


🌈c_str


const char* c_str() const
{
  return _str;//解引用找到'\0'
}


🌈[]


//可读可写
char& operator[](size_t pos)
{
  assert(pos < _size);
  return _str[pos];
}
//const对象提供重载版本 - 可读不可写
const char& operator[](size_t pos) const
{
  assert(pos < _size);
  return _str[pos];
}


🌈迭代器


普通迭代器

const迭代器:const对象优先调用最匹配的const成员函数,可读不可写


0a2653c851af460fa595bd959398a8f1.png


typedef char* iterator;
typedef const char* const _iterator;
iterator begin()
{
  return _str;
}
iterator end()
{
  return _str + _size;
}
const _iterator begin() const
{
  return _str;
}
const _iterator end() const
{
  return _str + _size;
}


迭代器实现出来了,范围for🍬还会远吗??


0a2653c851af460fa595bd959398a8f1.png


三. 增


⚡reserve & resize


插入字符串要考虑扩容


💦reserve

开新的空间,拷贝过去,释放旧空间


void reserve(size_t n)
  {
  if (n > _capacity)
  {
    char* tmp = new char[n + 1];//考虑'\0'
    strcpy(tmp, _str);
    delete[] _str;
    _str = tmp;
    _capacity = n;
  }
}


💦resize

要考虑多种情况


0a2653c851af460fa595bd959398a8f1.png


插入数据:如果是将元素个数增多,默认用\0来填充多出的元素空间,也可指定字符来填充

删除数据:如果是将元素个数减少,要把多出size的字符抹去

//"helloxxxxxxxxx"
  void resize(size_t n, char ch = '\0')
  {
    if (n > _size)
    { 
    //插入数据
    reserve(n);
    for (size_t i = _size; i < n; ++i)
    {
      _str[i] = ch;
    }
    _str[n] = '\0';
    _size = n;
    }
    else
    {
    //删除数据
    _str[n] = '\0';
    _size = n;
    }
  }
相关文章
|
1月前
|
C++ 容器
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
1月前
|
C++ 容器
|
1月前
|
C++ 容器
|
1月前
|
存储 C++ 容器
|
1月前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
36 4
|
1月前
|
存储 编译器 程序员
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
65 2
|
1月前
|
编译器 C语言 C++
【C++】C++ STL 探索:String的使用与理解(三)
【C++】C++ STL 探索:String的使用与理解
|
1月前
|
存储 编译器 C++
【C++】C++ STL 探索:String的使用与理解(二)
【C++】C++ STL 探索:String的使用与理解
|
1月前
|
编译器 C语言 C++
【C++】C++ STL 探索:String的使用与理解(一)
【C++】C++ STL 探索:String的使用与理解