文章目录
- 一、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,可以参考下面的文章:
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;
}
我们根据文档挑选了一些常用的函数进行模拟实现。
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;
}
}