模拟实现C++中的string类(详细解析)

学习C++,特别是C++中的STL部分,重点不是学习如何去使用STL,而是知道其底层原理是怎么样的,是怎么去实现的。因此,本篇文章带来的是对C++中的string的模拟实现。废话不多说,让我们去了解string是如何实现的吧!

一.模拟实现构造函数

对于构造函数,在官方库中,C99有下面种类:

 我们主要实现的是

string();

string(const char* s);

string(const string& str);

前两个称为构造函数,第三个称为拷贝构造

①无参构造函数string();

        //无参构造
        string()
        {
            _str = new char[1];
            _str[0] = '\0';
            _size = _capacity = 0;
        }

解析:由于是无参构造,在创建string对象时,字符串是空的,需要一个'\0',所以_str开辟一个字符的空间,用来存放'\0',然后将字符串有效个数和字符串空间赋值为0.

②带参构造函数string(const char* str);

        //带参构造
        string(const char* str)
        {
            _size = strlen(str);
            _capacity = strlen(str);
            _str = new char[_capacity + 1];

            strcpy(_str, str);

        }

解析:先对_size和_capacity进行初始化,而且_capacity没有额外+1,而是在_str开辟空间的时候才加1,加1的目的是放'\0',而先不加1的原因是在后面实现插入数据的时候,我们需要用到if(_size==_capacity)这个条件来判断容量是否满了,需要扩容,如果先加1,就不好操作了。最后将str的内容拷贝到_str中去。

温馨提示:带参构造函数没有使用初始化列表,原因有其下:如果使用初始化列表,在初始化_str,_size和_capacity的时候,有以下方法:

方法①:

        string(const char* str)
            :_str(str)//不能这样初始化:因为这样给,给的常量字符串,常量字符串存在常量区,常量区不能被修改
        {

                //......      

        }

方法②:

        //将上面写法改成下面这种,使用new来开辟空间,空间的大小为str的大小+1
        string(const char* str)
            :_str(new char[strlen(str)+1])//+1存放'\0',strlen不包含'\0'
            ,_size(strlen(str))
          ,_capacity(strlen(str))
        {

        }

        //但我们发现,如果使用上面的方法,我们需要使用3次strlen,这样的方法不好
        //其实我们可以不使用初始化列表,这样就能只是用一次strlen

对于带参构造函数,上面那种方法还不够好,因为我们还可以使用缺省参数!

看下面的实现代码:

        string(const char* str = "")
        {
            _size = strlen(str);//0
            _capacity = _size;//0
            _str = new char[_capacity + 1];//0+1=1
            strcpy(_str, str);
        }

这里的缺省值为什么是""?

解析:""代表的是空字符,里面就隐藏着一个'\0',由于strlen是以第一个'\0'为终点的,所以当我们给的缺省值是""的时候,strlen计算出来的长度是0。到这里,肯定会有人想,为什么不直接给nullptr或'\0'?                                                                                                                                        因为nullptr给了str后,str为空指针,strlen不能在空指针上使用,就会报错。而给'\0','\0'是一个字符,其ascll码值为0,直接给指针类型的变量,也相当于是空指针了。所以着两种都不可以。如果给"\0",这个是可以的,这个跟""类似,""是带一个'\0',而"\0"是字符串,里面有两个"\0",计算出来的长度都为0.

③拷贝构造函数

拷贝构造函数,在C++中,有两种版本的写法,称为传统版本和现代版本。

先来看传统版本的:

		//拷贝构造的传统写法
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			_capacity = s.capacity();
			_size = s._size;

			strcpy(_str, s._str);

		}

解析:拷贝构造跟构造函数的实现方法差不多,区别就在于拷贝构造是将参数s的属性内容全部拷贝到this中,所谓this,就是调用拷贝构造的string类对象的指针。

对于拷贝构造函数,最重点的是深拷贝和浅拷贝!

这里重点解析一下深拷贝和浅拷贝。

浅拷贝:说实在的,浅拷贝虽然带着个拷贝,但是其空间内存是共用的。看下图:

 这会导致什么呢?当我们调用完s2,但后面的程序还需要用到s1的时候,析构函数会将调用完,不需要再使用的s2释放掉,将其空间内存还给操作系统。到了这一步,问题就出现了,我的s2的内存是还给操作系统了,但是s1也是指向这个内存空间的啊!s1还需要用到,这不就导致内存泄漏了吗!报错就报错了在这里了!

所以,我们需要用到深拷贝。

深拷贝:给s2独自开辟一块空间,用来存放"hello world",这样以后,s1和s2是独立的,有自己各自的房间,谁也不会影响谁。所以我们看到上面的代码,给_str开辟了一块与s1同样大的空间,这就是深拷贝。

这里我们额外结合之前的知识总结一下,什么时候用到深拷贝,什么时候不需要深拷贝,只需浅拷贝即可。

非自定义类型,比如int、char、double等等的类型,不需要用到深拷贝,同样的它们也不需要用到析构函数。

对于自定义类型:比如string就需要用到深拷贝了,因为每个对象都需要有自己的小房间。

然后来看看现代版本的,对于现代版本,追求是代码简介,不是效率。看下面代码:

		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);//调用构造函数
			swap(_str, tmp._str);
			swap(_size, tmp._size);
			swap(_size, tmp._capacity);

			//tmp不能指向随机值,所以要初始化
		}

解析:现代版本的构造函数,使用了初始化列表来初始化s2。因为我们是创建了tmp对象,来为s2打工,让tmp和s2交换,交换后的tmp不能指向随机值,而是指向nullptr,所以要初始化s2。通俗的来讲,tmp相当于s2的打工人,为s2获取s1的属性,然后将其交给s2,而s2把工资(nullptr)发给tmp。

但是这样的写法不够简介,而且需要调用swap,调用3次拷贝构造(传值传参嘛)。在改写之前,我们来区分一下string自带的swap和C++库自带的swap的区别:

string自带的swap:

 C++库自带的swap:

 我们可以看到,C++库自带的swap函数,是模板类型的,每一次调用,都需要推演出参数的类型,在上面的代码中,就需要推演int、char*两种类型,需要拷贝3次,比较花时间。而使用string类自带,已经定义好string类,并且是引用,不需要拷贝。所以我们来实现一下string类的swap。

其实,我上面讲的,虽然我们模拟实现了string类的swap,调用了string的swap,但效率是一样的,因为实现的代码是这样的:

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

我上面说了,一切都是为了代码的简洁哈哈哈哈哈哈哈哈哈,上面说了那么多,是为了让我们了解一下底层原理。

最后,我们可以将现代版本改写成这样子:

		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);
			//this->swap(tmp);
			swap(tmp);
		}

④析构函数

析构函数对于自定义类型也是很重要的。因为需要释放掉内存空间。

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

⑤operator=()赋值

operator=()跟拷贝构造一样,也有传统和现代写法

先来个传统的写法:string s2(s1);

		string& operator=(const string& s)
		{
			if (this != &s)//不能给自己赋值
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;//需要把s2原本的空间给释放掉
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

解析:思路很简单,就是创建一个新的char* ,来接收s,然后释放掉s2原本的内存空间,再指向tmp的内存空间即可。

然后来看现代写法:

跟拷贝构造的现代版本一样,使用交换,找个打工人。

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);//直接调用拷贝构造
				swap(tmp);
			}
			return *this;
		}

二.模拟实现string类的容量操作

①size() 和 len(),还有capacity()

这size() 和 len()的功能是相同的,都是返回字符的有效个数。这里就写size()。

		size_t size() const
		{
			return _size;
		}

为什么要加个const?因为size是只读,加const保护起来吧。

capacity()是返回总空间的大小。可以理解为:size是水的体积容量,capacity是水杯瓶子的容量

		size_t capacity() const
		{
			return _capacity;
		}

②clear()

清除掉有效字符,但是不改变空间大小。把水瓶里面的水喝完吧。这个实现的方法是直接将_size置为0,然后将0位置的_str[0]赋值'\0',就行了

		void clear()
		{
			_size = 0;
			_str[0] = '\0';
		}

③resize(size_t n = 0)/resize(size_t n  = 0, char c = '\0');

这个函数重要。我们先来分析这个函数的功能如何:

1.这个函数的功能是对有效字符的个数进行操作。它有三种情况。①当n大于字符串的长度并且小于capacity的时候,就是进行插入数据的处理②当n大于capacity,并且大于字符串长度的时候,是进行插入数据+扩容的处理。③当n小于字符串长度的时候,便是删除数据。

2.对于resize(size_t n)/resize(size_t n,char c)这两种重载,需要知道的是,resize(size_t n)是将多余的空间用0来填充,而resize(size_t n,char c)用字符c来填充。

了解到这里,我们就可以去模拟实现它了。当然啦,我们发现我们这里实现的时候,用到了reserve和operator[],这两个我们还没实现,但这样说明了C++在设计的时候,很多功能都是互相辅助的,没你没我都不行。

	void resize(size_t n = 0, char val = '\0')
		{
			//当n大于_size
			if (n > _size)
			{
				reserve(n);//判断是否需要扩容,即n是否大于capacity
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = val;
				}
				_size = n;
				_str[n] = '\0';

			}
			else//小于  删除
			{
				_str[n] = '\0';
				_size = n;
			}
		}

解析这段代码:我们通过上面的情况,当n>_size,自然是要插入数据,然后再看看是否需要扩容,这一步交给reserve(n)来完成。插入数据这一步,从第_size这个位置开始,这个位置是值是'\0',自然会被覆盖掉,然后一直到n-1,在第n个位置补上'\0',最后将_size的长度变为n。完成!当n<_size,自然是相当于删除数据,直接在第n个位置补'\0',然后_size改成n,完成!当n==_size,什么都不用干。

④reserve(size_t n)

这个函数的功能,是增大水杯的容量,一升的水杯不够我喝,那我就用两升的,两升的不够,那就十升。所以,对于这个函数,它的功能是扩容,为字符串预留空间,是把空间(capacity)增大,不会缩小空间,而且不会改变有效字符的个数或长度。具体是①当n大于当前的空间的时候,那么reserve会将空间扩大。②当n小于等于当前空间的时候,那么不会对原有的空间做什么事。

看下面实现的代码:

		void reserve(size_t n)
		{
			if (n > _capacity)//判断是否需要扩容
			{
				char* tmp = new char[n + 1];//多加1,给'\0'
				strcpy(tmp, _str);//将_str的内容拷贝过去
				delete[] _str;//释放原本的空间
				_str = tmp;//拿到tmp的内存空间
				_capacity = n;//最后将容量改为n
			}
		}

三.模拟实现访问及遍历的函数

①operator[](size_t pos);

这个比较简单实现,在返回前最后断言一下。当然,需要写两个重载类型,一个是可读可写,一个是只读不写。

		//可读可写
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		//只读
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

②begin()+end()

实现begin()和end(),用到迭代器。迭代器就是一种用法跟指针差不多,但不一定是指针的东西。现阶段,我们可以只知道它的用法,就是把它当成指针用就行了,但是不一定是指针。

 代码如下:

		typedef char* iterator;
		iterator begin() const
		{
			return _str;
		}

		iterator end() const
		{
			return _str + _size;
		}

解析:对于begin(),返回的_str,因为_str是指针类型, 指向的是首元素的地址,便是最开始的那个位置,即begin。对于end(),返回的是_str+_size,指向的是最后一个位置,即'\0'这个位置。

③范围for

其实范围for,看着好像很高大上一样,我们在用的时候,不知道它为什么能够识别到循环的起点和重点,为什么有这样的功能。这里揭秘:范围for其实就是迭代器的分身!它相当于宏替换一样,当我们写出范围for后,在编译代码的时候,编译器就会将其展开,变成了使用迭代器来实现循环的代码一样了。看下面代码:

		string s("12345");
        
        //使用迭代器
		string::iterator it = s.begin();
		while (it != s.end())
		{
			(*it)++;
			++it;
		}
		cout << s.c_str() << endl;
        
        //范围for
		for (auto& ch : s)
		{
			ch--;
		}
		cout << s.c_str() << endl;

结果分别为:23456和12345

然后,如果我们将迭代器中的begin()改为Begin(),也就是将函数名称改一改,接下来我们就会发现,虽然while循环依然可以使用,范围for用不了了。

 报错的内容为:

 咦?不对啊,我都没有在范围for循环中用到begin()这个函数,怎么会在报错信息中出现这个?这也就很好地解释了我上面所讲的,范围for类似于宏替换,将使用迭代器的循环替换成了范围for。

四.模拟实现string类对象修改操作

①push_back()

push_back的实现,相当于数据结构中的顺序表差不多,如果我们对顺序表的实现熟悉的话,实现push_back一点问题都没有。

在插入数据前,判断一下容量是否满了,如果满了就进行扩容。然后在_size处插入数据,然后将_size++,最后在_size处添上'\0'。代码如下:

		void push_back(char ch)//可以二倍扩容,但是要注意,如果是个空字符串,也就是说capacity是0
		{
			if (_size == _capacity)
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newCapacity);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

加个小知识:

我们来看看每次是如何扩容的?

使用下面代码来测试一下:

void TestPushBack()//每次扩容代价都比价大,提前把空间开好
{
	string s;
	
	size_t sz = s.capacity();
	cout << "making s grow:" << endl;
	for (int i = 0; i < 1000; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}

在windows下,vs2019编译器:

 在Linux下:

结论:在vs2019的编译器下,每一次扩容的容量是一点五倍左右,而在Linux系统下便是两倍。

②append();

append()的实现也差不多,不过当容量满了,扩容的时候,扩二倍就一定够吗?肯定不是的,比如当原来的内容是"hello\0",而我要插入"world hello world\0",这个字符串就已经比本身的二倍还要长,所以是不能直接扩二倍。而是扩容到需要追加的字符串的长度再加1,这个长度才行。

		void append(const char* str)
		{
			int len = strlen(str);
			if (_size + len > _capacity)//判断原有的长度加上要追加的字符串长度是否大于现有的容量
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);//size下标的位置开始,拷贝_str,结尾有'\0',不需要额外加'\0'
			_size += len;

		}

③operator+=();

这个需要模拟实现两种情况,因为它既可以追加一个字符,也能追加一个字符串。而且,它的模拟实现,我们可以直接调用push_back和append。

		//追加字符串
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		//追加字符
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;

		}

④c_str()

这个函数,是返回C格式的字符串。何为C格式的字符串?在c里面,一个字符指针拿到的是字符串的首元素的地址。使用这个函数,就可以拿到这个地址了。

代码如下:

		const char* c_str() const
		{
			return _str;
		}

⑤find()

find()函数返回的是目标字符的位置下标,默认从0开始,也就是说缺省值为0,是半缺省函数。我们通过循环来找。当然,find()函数可以找字符,也可以找字串。对于找某个字符来说,直接使用循环遍历,找到就返回下标。对于找子串,我们可以使用strstr函数来找,返回的是子串的首元素的地址,用char*类型的变量来接收,最后返回首元素的下标。代码如下:

		//找字符
		size_t find(char ch, size_t pos = 0) const
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (_str[pos] == ch)
				{
					return pos;
				}
				++pos;
			}
			return npos;
		}
		//找子串
		size_t find(const char* str, size_t pos = 0) const 
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr)//判空,如果没找到,就返回npos
			{
				return npos;
			}
			else
			{
				return ptr - _str;//找到,就ptr-_str,即为首元素的下标
			}
		}

⑥insert()

inset()函数相当于是顺序表里面的任意位置插入,所以我们就知道了它的效率不太行,如果字符串很长很长,但我们想要在字符串里面插入字符或字符串,那么就需要挪动很多字符。这也是我们为什么必须学习其底层的实现方法,只有了解了,学习了才能知道哪些接口好用,哪些不好用。

在插入之前,老办法,先判断容量是否满了,如果满了,那就扩容。同样了,insert重载了两种方法,一个是插入字符,一个是插入字符串。

注意,这部分有坑,所以要重点讲解。

插入字符:先来看下面代码:

		string& insert(size_t pos, char ch)
		{
			assert(pos < _size);
			if (_size == _capacity)
			{
				int NewCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(NewCapacity);
			}
			size_t end = _size;
			while (end >= pos)
			{
				_str[end + 1] = _str[end];//往后挪
				end--;
			}
			_str[pos] = ch;
			_size++;
			return *this;
		}

这一看,是没问题。如果我们给出的例子是这样的

 根据上面代码,最后将第一个位置的w挪到第二个位置后,x就会插入到下标为0的这个位置,但实际上.......我们一运行,陷入了死循环。

为什么?原因就在于end我们的类型是无符号的,不会有负数,当end等于0的时候,将第一个w字符挪动后,不会变成-1,而是变成一个很大的值,直接进入下一个循环。

下面给出调试的结果:

 那么,就有同学会想,我们可以把size_t改为int呀!

好的,我们现在来改一下:

 当我们的end变为-1的时候,我们会发现一个问题,怎么回事?明明end为-1,pos为0,而循环条件是end>=pos,还会进入循环?其实原因就在于int隐式提升,int会提升为size_t。在C/C++中,当小的类型于相较大的类型做运算时,小的类型会向大的类型提升,比如int跟double做运算时,int会提升为double。

其解决方法就是,将pos强制转换成int类型。还有就是,在C++的string类的库中,end的类型就是size_t的,我们既然要模拟实现string,我们就遵循规则。那么我们该如何取解决这个问题呢?

好办!让end再往后挪一格,此时,我们循环挪动数据的目标,就是挪动数据,不是放数据,此时循环条件变成end>pos,不能等于。

看下面代码:

		string& insert(size_t pos, char ch)
		{
			assert(pos < _size);
			if (_size == _capacity)
			{
				int NewCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(NewCapacity);
			}
			//size_t end = _size;
			//int end = _size;
			//while (end >= (int)pos)
			//{
			//	_str[end + 1] = _str[end];//往后挪
			//	end--;
			//}
			size_t end = _size+1;
			while (end > pos)
			{
				_str[end] = _str[end-1];//数据挪的位置而不是放的位置
				end--;
			}
			_str[pos] = ch;
			_size++;
			return *this;
		}

插入字符串:先看下面代码:

根据上面的插入字符的代码,我们很容易就写出下面这样的代码:

		string& insert(size_t pos, const char* str)
		{
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end > pos)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strcpy(_str + pos, str);
			_size += len;
			return *this;

		}

我们测试下面这个代码:

 一运行,发现,只打印了hello,怎么回事?其实我们不能使用strcpy,因为它会将hello 里面的'\0'也拷贝了过去,所以也只打印了hello出来。

所以我们改一下这个代码,将strcpy改成strncpy;

 运行,发现结果对了!好啦,就这样了!错!错哪?我们先来调试瞅一瞅

我们其实可以好好想想,看看这个调试的结果

当pos等于6,而end也等于6的时候,应该就停止了,直接将最近的字符串拷贝过去, 覆盖掉,但是循环并没有停止,反而进行进行,这会导致什么结果?end小于pos了,减掉是负数,在无符号下,是一个很大的数,所以我们接着调试运行看看结果:

 它会将随机数给挪过去了!这显然是不可以的。所以我们必须控制好循环条件,将循环条件改为end>=pos+len,或者是end > pos+len-1.

⑦erase()

erase()函数是个半缺省函数,如果我们不写需要删除的字符串的长度,那么就会默认使用npos长度,也就是说从pos位置开始删,删完全部。又或者说是给出的长度加上pos,这个长度大于_size,也会将所有的字符串删掉

 而当长度小于_size,那么我们就需要往前挪动数据即可。代码如下:

		string& erase(size_t pos, size_t len = npos)
		{
			//当不给长度len时,默认是npos,或者需要删除的长度大于_size,那么就是删除从pos开始到结尾
			if (len == npos || pos + len > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t begin = pos + len;
				while (begin<=_size)
				{
					//往前挪动数据
					_str[pos] = _str[begin];
					pos++;
					begin++;
				}
				_size -= len;
			}
			return *this;
		}

⑧operator<<()

要实现string的流输出,我们要明确一下格式,是cout<<s<<endl;所以我们不能将operator<<()函数写在类域中,因为类域中的函数,默认第一个参数是this。但我们又必须拿到string类中的私有变量,那就使用友元吧,但也不一定需要友元,我们可以直接在类域外写。

	ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}
		return out;
	}

⑨operator>>()

实现流输入,跟流输出一样,写在类域外。重点在于,我们有几个细节需要考虑。第一个,在输入的时候,可能已经有内容了,是重新输入的,所以在输入之前,先将原本的内容情况。第二个,为了避免不断扩容,我们可以定义一个字符数组,用来存放输入的字符,当字符数组满了,再追加到string对象中,然后重新输入。第三个,由于流输入cin跟scanf一样,遇到空格或换行就会停下来。最后,代码如下:

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128] = { '\0' };
		size_t i = 0;
		char ch = in.get();//一个字符一个字符地拿
		while (ch != ' ' && ch != '\n')
		{
			if (i == 127)//127这个位置留给'\0'
			{
				s += buff;
				i = 0;
			}
			buff[i++] = ch;
			ch = in.get();
		}

		if (i >= 0)//当i!=127但是又有空格了,那么跳出循环,将当前i这个位置给上'\0'表示字符串结束位置
		{
			buff[i] = '\0';
			s += buff;//最后追加给s
		}
		return in;
	}

vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节

vs下string的结构:

string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字
符串的存储空间:

①当字符串长度小于16时,使用内部固定的字符数组来存放
②当字符串长度大于等于16时,从堆上开辟空间

union _Bxty
{ 
	char _buff[16];
	char* _ptr;
	size_t _size;//保存字符串长度
	size_t _capacity;//保存从堆上开辟空间的总容量

} _Bx;

这种设计是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内
部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高,本质上是以空间换时间。

g++下string结构:

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:

空间总大小
字符串有效长度
引用计数
 

struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};

比如有两个string类的对象s1和s2,然后进行s1(s2)这样的拷贝构造时,会默认是浅拷贝,然后在析构的时候,根据引用计数的个数来判断是否释放空间。那什么时候会进行写时拷贝呢?那就是s2要修改数据的时候,就会额外给s2一个空间。这就跟操作系统中的父子进程概念类似!

本篇文章结束~这就是模拟实现string的详细过程,如果有什么不懂的可以下方评论留言~喜欢的朋友可以点个收藏~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

山雾隐藏的黄昏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值