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;char& operator[] ( size_t pos ) 返回pos位置的字符,const string类对象调用
const char& operator[] ( size_t pos )const 返回pos位置的字符,非const string类对象调用
上面写了一大堆也不知道有啥用,下面实现一写有用的!
提取后缀名:比如一个文件叫 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类的构造、拷贝构造、赋值运算符重载以及析构函数
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;导致三个对象指向同一块内存空间,因为我们知道内存不能释放两次,否则会造成内存泄露。
重新实现拷贝构造函数
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;这两种方法都可以避免浅拷贝,但是第二种相对来说更好一点,因为它如果申请失败的话,并没有破坏原来的对象内容,但是第一种方法就直接把_str释放了;如果申请失败,那原来对象的内容也没有了; 我们会发现,s1 和 s2中的 _str 对象地址是不相同的,这也就避免了两个对象指向同一块内存空间的现象
拷贝构造函数的大致思想:先创建了一个临时的对象空间;然后两个交换内存地址;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); }这个方法很巧妙!!利用传值的方式创建一个临时对象,将临时对象和_str交换;感觉很奇妙~~其实本质和上面的一样都是交换两个对象的内容和地址,只不过是传引用变成了传值;
另外也可以采用引用计数的方法来解决浅拷贝问题,但是比较麻烦,其中还设计到一些 线程不安全的问题 [具体代码参见下面这篇博客]
https://blog.csdn.net/qq_43763344/article/details/91045632
而这种引用计数的方法 就是写时拷贝的一种底层实现原理。
比如:两个对象或多个对象同时指向一块内存时,不会发生拷贝,因为内存不会出错,但是当修改其中的一个对象时,会为该对象重新分配块内存,防止出错。之所以引出这一功能是为了提高效率和性能,如果每一个对象创建出来就为其分配内存,会浪费空间资源。所以在需要的时候才分配空间,在不需要的时候就共享一块内存空间。
当对象在构造的时候(复制构造函数)或者在一个对象赋值的时候(重载运算符) 因为这些不会造成内存的出错,除非去修改或者释放的行为时才会引发错误!
在共享同一块内存的类发生内容改变时,比如调用一些赋值操作符,追加字符串,释放当前内存空间等行为 才会发生Copy-On-Write
当一个对象需要写时拷贝时候,系统会为其申请一块内存空间,然后把它的资源拷贝到新的内存空间中去,这样他和原来的空间相互独立,没有关联…
上文提到 写时拷贝它的底层原理是引用计数的方式,当创建一个对象的时候引用计数会 + 1,当销毁的时候会建一,直到计数为1的时候就知道可以释放内存了,但是 在这其中有一个 线程不安全 的问题,当多个对象创建如果不加锁的话会引发错误,所以如果加锁,其实效率获取就不是很占优势了…