string类基础接口和模拟实现
1. string 常用构造方法
函数用法 | 功能说明 |
---|---|
string s1 | 构造空的string类对象s1 |
string s2(“hello bit”) | 用C格式字符串构造string类对象s2 |
string s3(10, ‘a’) | 用10个字符’a’构造string类对象s3 |
string s4(s2) | 拷贝构造s4 |
string 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,否则返回false |
void 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的时候就知道可以释放内存了,但是 在这其中有一个 线程不安全 的问题,当多个对象创建如果不加锁的话会引发错误,所以如果加锁,其实效率获取就不是很占优势了…