一篇文章带你弄懂string的几种底层实现

文章目录

  • 一、C++标准库中string的三种底层实现方式
    • 1.Eager Copy(深拷贝)
    • 2.COW(Copy-On-Write 写时复制)
    • 3.SSO(Short String Optimization - 短字符串优化)
  • 二、模拟实现string
    • 1.结构的定义
    • 2.对于reserve()和resize()的理解
    • 3.STL规定都是在pos之前进行插入
    • 4.STL关键所在——迭代器设计思维
    • 5.重载<< 和 >> 运算符


一、C++标准库中string的三种底层实现方式

1.Eager Copy(深拷贝)

最简单的就是深拷贝了。无论什么情况,都是采用拷贝字符串内容的方式解决,这也是之前已经实现过的方式。这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。所以需要对其实现进行优化,之后便出现了下面的COW的实现方式。

在这里插入图片描述

2.COW(Copy-On-Write 写时复制)

当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,执行效率为O(1)。只有当需要修改其中一个字符串内容时,才执行真正的复制。
在这里插入图片描述

引用计数只可以存放到堆空间,要对引用计数进行修改

  • 引用计数(_count)为 static 全局静态数据成员。 这样创建多个对象时,使用的引用计数都为同一个
  • 引用计数在堆空间放在存放数据位置的前面,放在后面对数据产生影响

实现的示意图如下,有两种形式:

第一种:

在这里插入图片描述

std::string的数据成员就是:
class string 
{
private:
	Allocator _allocator;
	size_t size;
	size_t capacity;
	char * pointer;
};

第二种:

在这里插入图片描述

std::string的数据成员就只有一个了:
class string {
private:
	char * _pointer;
};

为了实现的简单,在GNU4.8.4的中,采用的是实现2的形式。从上面的实现,我们看到引用计数并没有与std::string的数据成员放在一起,为什么呢?

当执行复制构造或赋值时,引用计数加1,std::string对象共享字符串内容;

当 std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1, 直到引用计数为0时,则真正释放字符串内容所在的空间。

再思考一下,既然涉及到了引用计数,那么在多线程环境下,涉及到修改引用计数的操作,是否是线程安全的呢?为了解决这个问题,GNU4.8.4的实现中,采用了原子操作(不会被线程调度机制打断的操作)。

例如在g++下进行测试:

在这里插入图片描述

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

  • 空间总大小
  • 字符串有效长度
  • 引用计数
struct _Rep_base
{
 	size_type _M_length;
 	size_type _M_capacity;
 	_Atomic_word _M_refcount;
};

如果还想深入剖析linux GCC 4.4的STL string,可以参考下面的文章:

深入剖析linux GCC 4.4的STL string

3.SSO(Short String Optimization - 短字符串优化)

目前,在VC++、GNU5.x.x以上、Clang++上,std::string实现均采用了SSO的实现。

通常来说,一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char* 指针就占用了8个字节,所以SSO就出现了,其核心思想是:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。其实现示意图如下:

在这里插入图片描述

class string 
{
	union Buffer
	{
		char * _pointer;
		char _local[16];
	}; 
	
	Buffer _buffer;
	size_t _size;
	size_t _capacity;
};

当字符串的长度小于等于15个字节时,buffer直接存放整个字符串;当字符串大于 15个字节时,buffer存放的就是一个指针,指向堆空间的区域。 这样做的好处是, 当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。

例如Microsoft Visual Studio中作如下测试:

在这里插入图片描述
在64位机器下,string总共占40个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:

  • 当字符串长度小于16时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ 
	// storage for small buffer or pointer to larger one
 	value_type _Buf[_BUF_SIZE];
 	pointer _Ptr;
 	char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

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

  • 其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
  • 最后:还有一个指针做一些其他事情。

故总共占16+8+8+4+4=40个字节。

在这里插入图片描述

二、模拟实现string

对于本次模拟实现string,我们不打算完全模拟其底层结构,而是根据STL的设计思路来实现并让其能满足我们的使用需求。

1.结构的定义

	class string {
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	}

我们根据文档挑选了一些常用的函数进行模拟实现。

c++标准库中string的文档介绍

2.对于reserve()和resize()的理解

	void reserve(size_t newcapacity)
	{	
		//扩容只扩大,不缩小
		if (newcapacity > _capacity)
		{
			char* s = new char[newcapacity + 1];
			strcpy(s, _str);

			//释放原空间
			delete[] _str;
			_str = s;
			_capacity = newcapacity;
		}
	}

		//不缩容,只改变string大小
	void resize(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
		else
		{
			_str[n] = '\0';
			_size = n;
		}
	}
	

3.STL规定都是在pos之前进行插入


		string& insert(size_t pos, char ch)
		{
			assert(pos < _size);//此处_size如果为0,则直接断言错误,若assert(pos<=_size),则当pos==_size时,不满足在pos之前插入

			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : newcapacity * 2;
				reserve(newcapacity);
			}

			//注意边界检查以及隐式类型转换
			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* s)
		{
			assert(pos < _size);
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			size_t end = _size + len;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				--end;
			}
			strncpy(_str + pos, s, len);
			_size += len;

			return *this;
		}

4.STL关键所在——迭代器设计思维

迭代器(iterators)是一种抽象的设计概念,现实程序语言中并没有直接应对这个概念的实物,《Design Patterns》一书提供有23个设计模式的完整描述,其中iterator模式定义如下:提供一种方法,使之能够依序巡防某个容器所含的各个元素,而又无需暴露该聚合物的内部表述方式。 ——《STL源码剖析》

STL的中心思想在于:

将数据容器和算法分开,彼此独立设计,最后再以一贴胶着剂将他们撮合在一起。

迭代器是一种类似指针的对象,而指针的各种行为中最常见也最重要的便是内容提领和成员访问,因此,迭代器最重要的编程工作就是对operator*和operator->进行重载工作。

对于string,它的原生指针经过封装后就可以得到iterator:

typedef char* iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }	

//const迭代器
typedef const char* const_iterator;
iterator begin() const { return _str; }
iterator end() const { return _str + _size; }	

5.重载<< 和 >> 运算符

这两个运算符的重载应该设置为类外成员,因为如果设置成类内成员,那么this指针会占用第一个参数的位置,那么调用时就是 s << out,而非我们习惯的out << s。所以out必须占据第一个参数的位置。在类外要访问类内成员,所以这两个函数应该设置为友元函数。

而返回引用是为了支持连续输入或输出。

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

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buffer[128] = "\0";
		size_t i = 0;
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			if (i == 127) { s += buffer; i = 0; }
			buffer[i++] = ch;
			ch = in.get();
		}
		if (i > 0)
		{
			buffer[i] = '\0';
			s += buffer;
		}

		return in;
	}

代码实现如下:

#pragma once
#include<iostream>
#include<cassert>
using namespace std;

namespace myString
{
	class string {
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		const static size_t npos = -1;


	public:
		string(const char* s = "")
		{
			_size = strlen(s);
			_capacity = _size;//此处的capacity不考虑结束的'\0',表示可用的容量
			_str = new char[_capacity + 1];
			strcpy(_str, s);
		}

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

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

		iterator begin() { return _str; }
		iterator begin() const { return _str; }
		iterator end() { return _str + _size; }
		iterator end() const { return _str + _size; }
		size_t size() const { return _size; } 
		size_t capacity() const { return _capacity; }
		const char* c_str() const { return _str; }
		char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; }//普通对象:可读可写
		const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
		string& operator=(string s) { swap(s); return *this; }
		string& operator+=(char ch) { push_back(ch); return *this; }
		string& operator+=(const char* s) { append(s); return *this; }


		//不缩容
		void reserve(size_t newcapacity)
		{
			if (newcapacity > _capacity)
			{
				char* s = new char[newcapacity + 1];
				strcpy(s, _str);

				//释放原空间
				delete[] _str;
				_str = s;
				_capacity = newcapacity;
			}
		}

		//不缩容
		void resize(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
			else
			{
				_str[n] = '\0';
				_size = n;
			}
		}

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

		void push_back(char c)
		{
			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size++] = c;
			_str[_size] = '\0';
		}

		void append(const char* s)
		{
			size_t len = strlen(s);
			if (_size + len > _capacity) reserve(_size + len);

			strcpy(_str + _size, s);
			_size += len;
		}


		//stl规定都是在pos之前进行插入
		string& insert(size_t pos, char ch)
		{
			assert(pos < _size);//此处_size如果为0,则直接断言错误,若assert(pos<=_size),则当pos==_size时,不满足在pos之前插入

			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : newcapacity * 2;
				reserve(newcapacity);
			}

			//注意边界检查以及隐式类型转换
			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* s)
		{
			assert(pos < _size);
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			size_t end = _size + len;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				--end;
			}
			strncpy(_str + pos, s, len);
			_size += len;

			return *this;
		}

		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; }
			else { strcpy(_str + pos, _str + pos + len); _size -= len; }

			return *this;
		}

		size_t find(char ch, size_t pos = 0) const
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (_str[pos] == ch) return pos;
				pos++;
			}
			return pos;
		}

		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr)return npos;
			else return ptr - _str;
		}

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



	};

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

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buffer[128] = "\0";
		size_t i = 0;
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			if (i == 127) { s += buffer; i = 0; }
			buffer[i++] = ch;
			ch = in.get();
		}
		if (i > 0)
		{
			buffer[i] = '\0';
			s += buffer;
		}

		return in;
	}
}









  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值