序列式容器 string 深拷贝 写时拷贝的思想 模拟实现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;

注意:

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

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

  3. 对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的时候就知道可以释放内存了,但是 在这其中有一个 线程不安全 的问题,当多个对象创建如果不加锁的话会引发错误,所以如果加锁,其实效率获取就不是很占优势了…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿的温柔香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值