经典的string类问题

举报
跳动的bit 发表于 2022/06/15 22:59:20 2022/06/15
【摘要】 💦 经典的string类问题💨 string.h#pragma oncenamespace bit{ class string { public: string(char* str) //:_str(str) :_str(new char[strlen(str) + 1] { strcpy(_str, str); } ~string()//当生命周期结束,程序会自动...

💦 经典的string类问题

💨 string.h

#pragma once
namespace bit
{
	class string
	{
	public:
		string(char* str)
			//:_str(str)
			:_str(new char[strlen(str) + 1]
		{
			strcpy(_str, str);
		}
		~string()//当生命周期结束,程序会自动调用析构函数释放资源。
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			return _str[pos];//_str[pos]同*(_str + pos)
		}
	private:
		char* _str;
	};
	void test_string1()
	{
		string s1("hello");
		s1[0] = 'x';

		string s2(s1);
		s2[0] = 'y';
	}
}

💨 string.cpp

#include"string.h"
int main()
{
	bit::test_string1();
	return 0;
}

📝说明
在这里插入图片描述
注意这里对于新一点的编译器会报错(这里是 VS2017),需要使用 const 来修饰 _str。这里因为要演示,所以就不使用了。
在这里插入图片描述
这里 hello 是常量字符串,存储在常量区,然后调了构造函数初始化 _str,然后 s1 [0] = ‘x’ 就会报错(运行时错误)。
在这里插入图片描述
所以这里我们要改就要把常量字符串拷贝到 new 的一块空间,s1[0] = ‘x’ 才对。
在这里插入图片描述
这里没有写拷贝构造函数,编译器会默认生成,对于内置类型,字节序的浅拷贝;自定义类型,会去调用它的拷贝构造完成拷贝,程序结束,调用析构函数,对同一块空间释放两次,程序崩溃,这就是浅拷贝所带来的问题。其中析构两次空间只是其中一个问题,拷贝构造 s2 后,再去 s2[0] = ‘y’; ,s1 也会跟着修改。

解决方法就是 s2 必需是一块独立的空间,也就是深拷贝。

💦 浅拷贝

由如上代码我们了解了浅拷贝会带来两个问题:

  1. 析构两次空间
  2. 其中一个去修改,会影响另一个

浅拷贝也称为位拷贝,编译器只是将对象中的值拷贝过来,如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一个对象不知道该资源已经释放,以为该空间还有效,所以继续对资源进行操作时,就会发生访问违规。所以要解决浅拷贝问题,C++ 引入了深拷贝。同时要明白一个问题,string 做拷贝时,其实是深拷贝,也就意味着要尽量减少它的拷贝构造,因为它除了拷贝值过来,还需要开空间,代价较大,只有迫不得已的时候才用。

浅拷贝只关注美人鱼的上半身,而深拷贝探索到了美人鱼不为人知的下半身:
在这里插入图片描述
在这里插入图片描述

💦 深拷贝

深拷贝会新开辟一块与原对象一样大的空间,再把原对象空间上的值拷贝过来。

1、深拷贝的传统版写法的string类

💨 string.h

#pragma once
namespace bit
{
	class string
	{
	public:
		string(char* str)
			:_str(new char[strlen(str) + 1]
		{
			strcpy(_str, str);
		}
		//s2(s1)
		string(const string& s)
			:_str(new char[strlen(s.str) + 1])
		{
			strcpy(_str, s._str);
		}
		//s1 = s3
		string operator=(const string& s)
		{
			if(this != &s)//防止自己赋值
			{
				/*delete[] _str;//this->_str
				_str = new char[strlen(s._str) + 1];*/
				char* tmp = new char[strlen(s._str) + 1];
				delete[] _str;
				_str = tmp;
				strcpy(_str, s._str);
			}
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			return _str[pos];
		}
	private:
		char* _str;
	};
	void f1(string s)
	{}
	void f2(const string& s)
	{}
	template<class T>
	void f3(T x)
	{}
	void f3(const T& x)
	{}
	void test_string1()
	{
		string s1("hello");
		s1[0] = 'x';

		string s2(s1);
		s2[0] = 'y';
		
		string s3("hello bit");
		s1 = s3;
	
		f1(s1);
		f2(s2);
	}
}

💨 string.cpp

#include"string.h"
int main()
{
	try
	{
		bit::test_string1();	
	}
	catch(exception& e)
	{
		cout << e.what() << endl;	
	}
	return 0;
}

📝说明
在这里插入图片描述
引用的价值更进一步得以体现:f1 是传值传参,这里使用 s1 构造 s,是一个拷贝构造,并且这个拷贝构造是深拷贝;f2 是引用传参,s 是 s2 的别名,不需要拷贝构造。
在这里插入图片描述
对于模板也是一样:f3 里 T 是 int、double 都无所谓,但如果它是一个 string、vector、map 呢,那这要走深拷贝,代价是极大的。所以对于 f3 这种写法是极其不推荐的。
在这里插入图片描述
注意 s1 和 s2 所指向的空间大小不一定相同。实现:直接拷贝不一定对,因为它们各自所指向的空间大小不一定相同,比如 s1 是 6 个有效字符的空间,s2 是 10 个有效字符的空间,直接拷贝就会导致越界。比如 s1 是 100 个有字符的空间,s2 是 5 个有效字符的空间,可以直接拷贝,但是浪费空间。最好的方法就是把 s1 指向的空间释放掉,重新开辟一块与 s2 所指向的空间一样大的空间,再把 s2 所指向的数据拷贝至新空间,最后让 s1 指向新空间
在这里插入图片描述
目前我们写的赋值重载仍有问题:对于如上代码中所有的 new,当 new 失败时,我们没有去捕获异常,对于赋值重载中的 new,如果失败了,还把 s1 给破坏了,赔了夫人又折兵,所以这里可以先 new 空间,再 delete。虽然异常我们还没涉及,但是这里先完善下,不然显然咱不专业。

2、深拷贝的现代版写法的string类
//s2(s1)
string(const string& s)
	:_str(nullptr);
{
	string tmp(s._str);
	swap(_str, tmp._str);	
}
//s1 = s3//版本一
string& operator=(const string& s)
{
	if(this != &s)
	{
		string tmp(s._str);
		swap(_str, tmp._str);	
	}
	return *this;
}
//s1=s3//版本二
string& operator=(string s)
{
	swap(_str, s._str);
	return *this;
}

📝说明
在这里插入图片描述
在这里插入图片描述
至此如上代码还有一点问题:虽然已经达到深拷贝的效果,但是这里的 tmp 是一个局部对象,出了作用域,生命周期结束,它会调用析构函数,而此时 tmp 是一个随机值,这里再释放就会报错,所以说我们在一开始得把它指向空(free 和 delete 释放空都不会有问题)。

现代写法的精髓就是让别人替自己干活,有什么特别的价值呢 ❓

实际上在以后我们写深拷贝时,大部分用的都是现代写法 —— 这里拷贝的是一个数组,如果拷贝的是更复杂的东西时,自己去开空间、拷贝,代价就很大。
在这里插入图片描述
相比版本一这是更正宗的现代写法,版本一已经说过了,就不理解了。版本二是使用传值传参,s3 传给 s,s 就充当了 tmp 的作用 —— string s(s3) 拷贝构造。为什么这里不判断自己赋值给自己呢 ???因为已经深拷贝出来了,判断也已经没有意义了。注意这里并没有改变 s3,因为这里是传值。也就是说,后面在写赋值时,所有的深拷贝都可以用这样的一段代码解决。

传统写法 | 现代写法 ❓

它们俩的效率是一样的。但是:

  1. 传统写法,可读性高,便于理解,但操作性较低
  2. 现代写法,代码更加简洁高效,但是逻辑更加复杂

在以后我们都更倾向于现代写法。

💦 写时拷贝(了解)

在这里插入图片描述

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
在这里插入图片描述

当然这种方案也有不好的地方,具体可以参考如下的文章,所以说可以认为这种技术已经脱轨了。

写时拷贝

写时拷贝在读取时的缺陷

证明标准库里没有使用这种技术 ❓
在这里插入图片描述
这里有个函数 c_str,它返回指向字符串的指针,如果 _str 不一样的就是深拷贝,一样就是写时拷贝(除非去修改它,触发深拷贝)。注意这里输出地址时不能用 cout,要用 printf,因为 c_str 返回的是 char*。

==Visual Studio 2017:==
在这里插入图片描述

==Linux g++:==
在这里插入图片描述

写时拷贝是在需要修改时在去深拷贝 ❗

==Visual Studio 2017:==
在这里插入图片描述

==Linux g++:==
在这里插入图片描述


📝小结

早期 Linux 选择了写时拷贝的技术,而 VS 下选择了直接深拷贝的技术。它们本质都是深拷贝,只是说 Linux 下先做浅拷贝,如果不写就不做深拷贝,写了再去做深拷贝,并且是谁写谁做。我这里 g++ 的版本是 gcc version 4.8.5 20150623,之前好像听说最新的版本已经放弃写时拷贝了,有兴趣的可以去验证下。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。