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类中写时才拷贝的原理!