string模拟实现:(一)

简介: ❓我们这里定义了一个string类型,然后STL标准库里面也有string,两个名字一样我们分不清楚怎么办呢? 为了跟库的string区分开,我们可以定义一下命名空间

string模拟实现:

上一篇博客,我们对String类有了一个基本的认识,本篇博客我们来从0~1去模拟实现一个String类,当然我们实现的都是一些常用的接口。


❓我们这里定义了一个string类型,然后STL标准库里面也有string,两个名字一样我们分不清楚怎么办呢?

  • 为了跟库的string区分开,我们可以定义一下命名空间
namespace st
{
  class string
  {
  public:
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}

有了类的成员变量,我们需要对这些成员变量进行初始化和释放,我们来写一下string的构造函数和析构函数

首先来观察一下string类的成员变量,string类有三个成员变量_str(字符指针)、__size和 _capacity。

_size和 _capacity都比较容易初始化,直接置为0就好。

_str作为字符指针比较麻烦,具体的原因往下看!

1深浅拷贝:

我们来写一下我们自己string类的构造和析构函数

class string
  {
  public:
    string(const char* str)
      :_str(str)
            ,_size(str._size)
      , _capacity(str._capacity)
            {}
    private:
    char* _str;
    size_t _size;
    size_t _capacity;
}

❓上面这种构造函数我们调用的时候是否能编译通过呢?

💡这是不行的,因为你初始化这个 string 时,比如我们通常情况会这么写:string s1("hello world");

❓我们为string的初始化提供构造函数,这里为什么报错呢?

💡原因是这里权限放大了,str是一个const char *类型,而_str只是一个char * 类型,这里赋值过来会直接权限放大报错了,同理可得:常量字符串是不可以直接赋值给char *类型的(char*b="bcd";)


解决方法将_str也设为const char*就好啦

  • 🔥const char*类型这里是只允许读,不允许写的

但是我们写的String类需要有增删查改的功能,因此上述的写法不可以的

我们可以这样写:

string(const char* str)
    : _str(new char[strlen(str) + 1]) {    // 开strlen大小的空间
    strcpy(_str, str);
}
  • 🔥strlen函数是计算字符串的有效长度,是不含\0的!!!!!

我们这里strlen+1是为了给字符串的\0预先留一个位置的

析构函数:

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

拷贝构造函数

void TestString()
{
  String s1("hello xiaolu!!!");
  String s2(s1);
}

我们来运行一下,通过s1来拷贝构造s2

🚩 运行结果如下:

❓这里显示strcpy是unsafe(不安全的)的,这是为什么呢?如何解决呢?(当前完整代码如下)

#include<string.h>
namespace xiaolu
{
  class string
  {
  public:
    string(const char* str)
      : _str(new char[strlen(str) + 1])
    {    // 开strlen大小的空间
      strcpy(_str, str);
    }
    ~string()
    {
      delete[] _str;
      _str = nullptr;
      _size = _capacity = 0;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void TestString()
  {
    string s1("hello xiaolu!!!");
    string s2(s1);
  }
}
int main()
{
  xiaolu::TestString();
  return 0;
}

🔑详细解析:

首先我们先来了解一下strcpy函数,strcpy函数是一个值拷贝函数,她将hello xiaolu的字符一个一个按字节拷贝到s1

这里其实不是strcpy函数的问题,而是

string s2(s1);这里是发生拷贝构造,而这里我没有写拷贝构造,因此编译器调用的就是默认拷贝构造,也就是浅拷贝,因为_str是char*类型,它发生值拷贝将地址直接拷贝过去,因此s1和s2指向同一块地址

解决方法:我们这里写一个拷贝构造,来进行深拷贝!

因为这里涉及到深浅拷贝的问题,因此我们来探讨一下深浅拷贝:

深浅拷贝的区别:

简单来说:

  • 🔥浅拷贝就是编译器自己执行值拷贝(按照字节,一个一个字节拷贝)

举个例子

当发生拷贝的是指针,编译器会将指针的4个字节依次拷贝另外一个变量,这样会导致两个变量指向一个地址,而当delete的时候,这一块地址会被释放两次地址,就会报错了!!!

当一个类有动态内存的时候,类的拷贝有构造函数、赋值运算符重载以及析构函数基本上不可以用浅拷贝,会出现上面的问题,要用到深拷贝。

  • 🔥深拷贝:深拷贝就是让编译器按照我们的想法进行拷贝或者赋值,一般来说是(开一块一样大的空间,再把数据拷贝下来,指向我自己开的空间)


我们自己需要写一个string的深拷贝:

string(const string& str)
      :_size(str._size)
      , _capacity(str._capacity)
    {
      _str = new char[str._capacity + 1];
      strcpy(_str, str._str);
    }

void TestString()
  {
    string s1("hello xiaolu!!!");
      string s2;
      s2 = s1;
  }

这里的我们没有提供默认的构造函数,当我们需要创建一个新的空白的string对象的时候,就会报错,我们可以给构造函数提供缺省值

string(const char* str = "")
      :_size(strlen(str))
    {
      _capacity = _size == 0 ? 3 : _size;
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }

深拷贝的常用情景,不止经常在拷贝构造,在赋值下也很经常!

赋值的深拷贝:

赋值的深拷贝思路跟拷贝构造一样是否可以呢?他们都是拿一个已有的变量来定义一个新的变量

string& operator=(const string& str)
    {
      delete[] _str;                        
    _str = new char[strlen(str._str) + 1];  
    strcpy(_str, str._str);                
    }

显然这里报错了,我们来分析一下:

🔑详细解析:

这里我们先释放了原来的_str,然后new了一块新的对象,再strcpy

首先我们new了一块新的空间,new失败了会怎么样?

会抛异常!抛异常!抛异常!无关紧要

失败了没问题,也不会走到 strcpy,但问题是我们已经把原有的空间释放掉了,

神不知鬼不觉地,走到析构那里二次释放可能会炸,所以我们得解决这个问题!

我们将开辟空间的步骤提前,然后释放向后移动

string& operator=(const string& str)
    {
      if (&str == this)
        return *this;//防止自己给自己赋值
      char* tmp = new char[str._capacity + 1];//防止开辟失败
      strcpy(tmp, str._str);
      delete[] this->_str;
      _str = tmp;
      _size = str._size;
      _capacity = str._capacity;
      return *this;
    }

再提供一种相对现代一点的写法:

String& operator=(String s)
 {
 swap(_str, s._str);
 return *this;
 }

写时拷贝

在我们经常使用的STL标准模板库中的string类,也是一个具有写时才拷贝技术的类。C++曾在性能问题上被广泛地质疑和指责过,为了提高性能,STL中的许多类都采用了Copy-On-Write技术。这种偷懒的行为的确使使用STL的程序有着比较高要性能。

Copy-On-Write一定使用了“引用计数”,是的,必然有一个变量类似于RefCnt。当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的RefCnt为1或是0,此时,程序才会真正的Free这块从堆上分配的内存。

是的,引用计数就是striing类中写时才拷贝的原理



相关文章
|
6月前
|
存储 C语言 C++
string的使用和模拟实现-1
string的使用和模拟实现
|
5月前
|
存储 C++
C++ string模拟实现(部分接口)
C++ string模拟实现(部分接口)
31 0
|
7天前
|
编译器 C++
【C++】模拟实现string
【C++】模拟实现string
|
1月前
|
存储 C++
C++:String的模拟实现
C++:String的模拟实现
|
1月前
|
存储 编译器 C++
【C++】——string的模拟实现
【C++】——string的模拟实现
|
3月前
|
编译器 C语言 C++
string的模拟实现
string的模拟实现
64 0
|
4月前
|
缓存 编译器 C++
【C++】:string的模拟实现
【C++】:string的模拟实现
37 0
|
4月前
string的模拟(下)
string的模拟(下)
33 1
|
4月前
|
Java 编译器 C++
string的模拟(上)
string的模拟
34 1
|
6月前
|
算法 编译器 C++
【C++】string模拟实现
【C++】string模拟实现