序列式容器 string 深拷贝 写时拷贝的思想模拟实现String

    xiaoxiao2025-01-11  12

    string类基础接口和模拟实现

    1. string 常用构造方法2. string类对象的容量操作3. string 类对象的访问操作4. string 类对象的修改操作 string 类的模拟实现如何解决浅拷贝1.利用深拷贝(传统版)方法1方法二 2.深拷贝(简洁版)赋值运算符重载:方法1赋值运算符重载:方法21、 Copy-On-Write的原理是什么?2、 string类在什么情况下才共享内存的?3、 string类在什么情况下触发写时才拷贝(Copy-On-Write)?4、 Copy-On-Write时,发生了什么? 写时拷贝的缺陷

    1. string 常用构造方法

    函数用法功能说明string s1构造空的string类对象s1string s2(“hello bit”)用C格式字符串构造string类对象s2string s3(10, ‘a’)用10个字符’a’构造string类对象s3string s4(s2)拷贝构造s4string s5(s3, 5)用s3中前5个字符构造string对象s5

    2. string类对象的容量操作

    函数名称功能说明size_t size() const返回字符串有效字符长度size_t length() const返回字符串有效字符长度size_t capacity ( ) const返回空间总大小bool empty ( ) const检测字符串释放为空串,是返回true,否则返回falsevoid clear()清空有效字符void resize ( size_t n, char c )将有效字符的个数该成n个,多出的空间用字符c填充void resize ( size_t n )将有效字符的个数改成n个,多出的空间用0填充void reserve ( size_t res_arg=0 )为字符串预留空间 string s("hello,bit!!!"); cout << s.length() << endl;//12 cout << s.size() << endl;//12 // capacity第一分配的时候自动给出15个容量 cout << s.capacity() << endl;//15 cout << s << endl;//hello,bit!!! // 将s中的字符串清空,注意清空时只是将size清0, //不改变底层空间的大小,也就是说有效字符size和length变为0 //但是容量的大小并没有改变; s.clear(); cout << s.size() << endl;//0 cout << s.capacity() << endl;//15 ================================================ // 将s中有效字符个数增加到0个,多出位置用'a'进行填充 s = "abc" s.resize(20, 'a'); cout << s << endl;//abcaaaaaaa cout << s.size() << endl; cout << s.capacity() << endl; // 将s中有效字符个数缩小到5个 //有效字符串会缩小,但是容量不会缩小 s.resize(5); cout << s.size() << endl; cout << s.capacity() << endl; cout<<s<<endl;

    resize函数 和 reserve函数的区别?

    resize 改变元素的有效个数,如果个数大于capacity ,则编译器会自动增容

    reserve 改变容器底层空间 capacity ,但不改变容器的有效个数

    有效个数增加不一定意味着容器的底层空间增加,底层空间的增加和有效个数没关系。如果理解不了~举个例子就明白了!

    你家里可以容纳10个人,这是底层空间,但是你家却只有4个人,这是有效元素的个数,所以即使来了5个朋友也不会增加底层空间,因为可以住得下。如果来了7个朋友(7个有效元素)的时候,就需要扩建了,这时候底层空间才会增加。

    string s; // 测试reserve是否会改变string中有效元素个数 s.reserve(100); cout << s.size() << endl; cout << s.capacity() << endl; // 测试reserve参数小于string的底层空间大小时,不会将空间缩小 s.reserve(50); cout << s.size() << endl; cout << s.capacity() << endl;

    3. string 类对象的访问操作

    char& operator[] ( size_t pos ) 返回pos位置的字符,const string类对象调用

    const char& operator[] ( size_t pos )const 返回pos位置的字符,非const string类对象调用

    4. string 类对象的修改操作

    string s = "hello string"; //在字符串s后面插入一个空格,只能插入字符 s.puch_back(' '); //在字符串后面追加字符串 s.append("bbb"); s += 'b'; // 在s后追加一个字符'b' s += "it";// 在s后追加一个字符串'it' cout<<s.c_s()<<endl;//以C语言的方式打印字符串

    上面写了一大堆也不知道有啥用,下面实现一写有用的!

    提取后缀名:比如一个文件叫 abcd.cpp 怎么才把 cpp 这个后缀名提取出来

    string file = "abcd.cpp"; // find 函数返回的是 . 的下标 4 size_t pos = file.find('.'); // substr 的第一个参数是从哪开始截取 // 第二个参数是截取几个字符 string end_name = file.substr(pos+1, file.size() - pos); cout << end_name << endl;//cpp 需要注意的是,截取的是一个字符串,所以加把'\0'也要加进去

    解析域名,http://www.cplusplus.com/reference/string/string/find/ 一个url包含很多,比如协议头,查找字符串,但是我们要把 www.cplusplus.com这一域名部分截取出来。

    string url("http://www.cplusplus.com/reference/string/string/find/"); // start 是 ‘:’的下标 4 size_t start = url.find("://"); //string::npos 说明查找不匹配 if (start == string::npos){ cout << "invalid url" << endl; return 0; } //第一个 w 的位置 start += 3; //从start 的位置找 /的位置,并返回它的位置 size_t finish = url.find('/', start); //从start 开始截取finish - start 长度的字符串赋值给address string address = url.substr(start, finish - start); cout << address << endl; 截取出来的字符串是 www.cplusplus.com

    删除协商前缀

    size_t pos = url.find("://"); //从 0 到 pos+3 的位置全部抹除 url.erase(0, pos + 3); cout << url << endl;

    注意:

    在 string 尾部追加字符,s.push_back(c); s.append(1, c); s += 'c';

    三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。

    对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好,避免增容带来开销,当空间不足时,编译器自动增容,大概是原来的1.5倍,在 linux 环境下增容是原来空间的2倍


    string 类的模拟实现

    实现 String类的构造、拷贝构造、赋值运算符重载以及析构函数

    class String{ public: String(const char* str = ""){ if (str == nullptr){ str = ""; } _str = new char[strlen(str) + 1]; strcpy(_str, str); } //浅拷贝的拷贝构造函数 String(const String &s) :_str(s._str) {} //浅拷贝的赋值函数 String& operator = (const String &s){ _str = s._str; return *this; } ~String(){ if (_str){ delete[] _str; } } private: char* _str; };

    好,写完了!我们可以验证一下~

    String s1;//调用构造函数 String s2("hello");//调用构造函数 String s3("a");//也是调用构造函数 String s4(nullptr);//构造函数,参数为空,所以传进去的话会将它置位零字符串 String s5(s2);//拷贝构造函数 s1 = s2;//赋值

    我们会发现前面四个调用都没问题,但是到了拷贝构造函数的时候就有问题了,具体是什么问题?其实我们前面的博客也提到过:浅拷贝 我们发现,当 s2 拷贝构造 s5 的时候会把 s2 中的对象(成员变量)_str 的内存地址也一起拷贝过去,就导致了这两个对象指向了同一块内存空间,当函数结束释放的时候会释放两次;对于 s1 = s2 这条语句也是一样;会把 s1 原来的空间也变为 s2;导致三个对象指向同一块内存空间,因为我们知道内存不能释放两次,否则会造成内存泄露。

    如何解决浅拷贝

    1.利用深拷贝(传统版)

    重新实现拷贝构造函数

    String s2("hello");//调用构造函数 String s5(s2);//拷贝构造函数

    拷贝构造函数中的 &s 其实就是 s2 的别名,s._str 就是 s2 中对象的 _str 内容

    String(const String &s) //重新申请一块内存,由于拷贝的s2,所以要依据s2的长度,加1是为了容纳\0 //因为strlen 并没有计算反斜杠0的长度 //最后把 _str 地址初始化为新申请的内存地址 :_str(new char[strlen(s._str)+1]) { //把 s2中的内容赋值到 _str中 ,也就是s5的对象 strcpy(_str, s._str); }

    我们就会发现 s2 和 s5内容都是“hello” ,但是他们对象的地址是不一样的!!这样他们就各自拥有了独立的内存地址,而不是像之前共同指向同一块内存

    重新实现赋值运算符重载

    String s2("hello"); String s1; s1 = s2;

    方法1

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

    方法二

    String& operator = (const String &s){ if (this != &s) { char* pstr = new char[strlen(s._str) + 1]; strcpy(pstr, s._str); //思考一些这里有没有必要释放_str; delete _str; _str = pstr; } return *this; } 其实应该要释放的,因为_str之前一定指向了一块内存,如果不释放就直接 让他指向pstr,那么会将之前_str 指向的资源丢失;虽然编译运行也没什么 问题,但是释放了还是更加安全一点;

    这两种方法都可以避免浅拷贝,但是第二种相对来说更好一点,因为它如果申请失败的话,并没有破坏原来的对象内容,但是第一种方法就直接把_str释放了;如果申请失败,那原来对象的内容也没有了; 我们会发现,s1 和 s2中的 _str 对象地址是不相同的,这也就避免了两个对象指向同一块内存空间的现象

    2.深拷贝(简洁版)

    拷贝构造函数的大致思想:先创建了一个临时的对象空间;然后两个交换内存地址;String s2(“hello”); String s5(s2);

    String(const String &s){ String strTemp(s._str); swap(_str, strTemp._str); }

    首先调用构造函数来构造s2对象;之后调用拷贝构造函数,进去之后调用构造函数并且将 “hello” 赋值给 strTemp;这样strTemp地址和 s2 地址不一样;最后进行交换地址和内容,函数结束之后,s5 也就被构造成功了;s5 的地址也是strTemp之前申请的地址;这里稍微注意一点就是:当前对象 this 指针可能是一个随机值,意味着this是栈上的一块内存地址,还没有被创建;所以如果交换的话就会使strTemp指向一个还未被初始化的内存地址,那如果销毁的话执行析构函数,就会崩溃;因为没有被创建就销毁当然会奔溃;但是在VS2018 编译器不会出现问题;因为 虽然在 vs2018 下编译,对象还没有创建之前 _str 是空指针,但是并不代表所有的编译器都会给NULL;所以在初始化的时候把当前对象置位空;

    String(const String &s)_str(nullptr) { String strTemp(s._str); swap(_str, strTemp._str); }

    赋值运算符重载:方法1

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

    赋值运算符重载:方法2

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

    这个方法很巧妙!!利用传值的方式创建一个临时对象,将临时对象和_str交换;感觉很奇妙~~其实本质和上面的一样都是交换两个对象的内容和地址,只不过是传引用变成了传值;


    另外也可以采用引用计数的方法来解决浅拷贝问题,但是比较麻烦,其中还设计到一些 线程不安全的问题 [具体代码参见下面这篇博客]

    https://blog.csdn.net/qq_43763344/article/details/91045632


    而这种引用计数的方法 就是写时拷贝的一种底层实现原理。

    1、 Copy-On-Write的原理是什么?

    比如:两个对象或多个对象同时指向一块内存时,不会发生拷贝,因为内存不会出错,但是当修改其中的一个对象时,会为该对象重新分配块内存,防止出错。之所以引出这一功能是为了提高效率和性能,如果每一个对象创建出来就为其分配内存,会浪费空间资源。所以在需要的时候才分配空间,在不需要的时候就共享一块内存空间。

    2、 string类在什么情况下才共享内存的?

    当对象在构造的时候(复制构造函数)或者在一个对象赋值的时候(重载运算符) 因为这些不会造成内存的出错,除非去修改或者释放的行为时才会引发错误!

    3、 string类在什么情况下触发写时才拷贝(Copy-On-Write)?

    在共享同一块内存的类发生内容改变时,比如调用一些赋值操作符,追加字符串,释放当前内存空间等行为 才会发生Copy-On-Write

    4、 Copy-On-Write时,发生了什么?

    当一个对象需要写时拷贝时候,系统会为其申请一块内存空间,然后把它的资源拷贝到新的内存空间中去,这样他和原来的空间相互独立,没有关联…

    写时拷贝的缺陷

    上文提到 写时拷贝它的底层原理是引用计数的方式,当创建一个对象的时候引用计数会 + 1,当销毁的时候会建一,直到计数为1的时候就知道可以释放内存了,但是 在这其中有一个 线程不安全 的问题,当多个对象创建如果不加锁的话会引发错误,所以如果加锁,其实效率获取就不是很占优势了…

    最新回复(0)