我们都知道, std::string的一些基本功能和用法,但它底层到底是如何实现的呢? 其实在std::string的历史中,出现过几种不同的方式。
可以从一个简单的问题来探索,一个std::string对象占据的内存空间有多大,即 sizeof(std::string)的值为多大?如果我们在不同的编译器(VC++, GNU, Clang++)上去测试,可能会发现其值并不相同;即使是GNU,不同的版本,获取的值也是不同的。
虽然历史上的实现有多种,但基本上有三种方式:
每种实现,std::string都包含了下面的信息:
最简单的就是 深拷贝 了。无论什么情况,都是采用拷贝字符串内容的方式解决,这也是之前已经实现过的方式。这种实现方式,在需要 对字符串进行频繁复制而又并不改变字符串内容 时,效率比较低下。所以需要对其实现进行优化,之后便出现了下面的COW的实现方式。
当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是 增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当 需要修改 其中一个字符串内容时,才 执行真正的复制。
引用计数只可以存放到堆空间,要对引用计数进行修改
- 当引用计数(
_count
)为普通数据成员。因为构造函数中,传入的对象用 const 进行修饰 (const 不能去掉,参见拷贝构造函数,传递一个临时对象,左值引用绑定到右值,错误),其成员_count
是不能修改的;- 当引用计数(
_count
)为 static 全局静态数据成员。 这样创建多个对象时,使用的引用计数都为同一个- 引用计数在堆空间放在存放数据位置的前面,放在后面对数据产生影响
第一种形式
std::string的数据成员就是:
class string
{
private:
Allocator _allocator;
size_t size;
size_t capacity;
char * pointer;
};
class string {
private:
char * _pointer;
};
为了实现的简单,在GNU4.8.4的中,采用的是实现2的形式。从上面的实现,我们看到引用计数并没有与std::string的数据成员放在一起,为什么呢?
当执行复制构造或赋值 时,引用计数加1,std::string对象共享字符串内容;
当 std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1, 直到引用计数为0时,则真正释放字符串内容所在的空间。
再思考一下,既然涉及到了引用计数,那么在多线程环境下,涉及到修改引用计数的操作,是否是线程安全的呢?为了解决这个问题,GNU4.8.4的实现中,采用了原子操作(不会被线程调度机制打断的操作)。
目前,在VC++、GNU5.x.x以上、Clang++上,std::string实现均采用了SSO的实现。
通常来说,一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char* 指针就占用了8个字节,所以SSO就出现了,其核心思想是:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。其实现示意图如下:
std::string的数据成员:
class string
{
union Buffer
{
char * _pointer;
char _local[16];
};
Buffer _buffer;
size_t _size;
size_t _capacity;
};
当字符串的长度小于等于15个字节时,buffer直接存放整个字符串;当字符串大于 15个字节时,buffer存放的就是一个指针,指向堆空间的区域。 这样做的好处是, 当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。
以上三种方式,都不能解决所有可能遇到的字符串的情况,各有所长,又各有缺陷。综合考虑所有情况之后,facebook 开源的 folly 库中,实现了一个 fbstring, 它根据字符串的不同长度使用不同的拷贝策略, 最终每个fbstring对象占据的空间大小都是24字节。
两个线程同时对同一个字符串进行操作的话,是不可能线程安全的,出于性能考虑, C++并没有为string实现线程安全, 毕竟不是所有程序都要用到多线程。
但是两个线程同时对独立的两个string操作时,必须是安全的。COW技术实现这一点是通过原子的对引用计数进行+1或-1操作。
CPU的原子操作虽然比mutex锁好多了, 但是仍然会带来性能损失, 原因如下:
这也是在多核时代,各大编译器厂商都选择了SS0实现的原因。
思路1:写时复制基本实现
#include
#include
#include
using std::endl;
using std::cout;
class String
{
public:
String()
: _pstr(new char[5]() + 4) //留出4字节存放引用计数, 1字节存放'\0',+4即偏移到数据的位置
{
cout << "String()" << endl;
initRefcount();
}
String(const char *pstr)
: _pstr(new char[strlen(pstr) + 5]() + 4) //申请数据+5(引用计数)的空间,指针偏移到数据的位置
{
cout << "String(const char *pstr)" << endl;
strcpy(_pstr, pstr);
initRefcount();
}
String(const String &rhs)
: _pstr(rhs._pstr)
{
cout << "String(const String &rhs)" << endl;
increRefcount();
}
String & operator=(const String &rhs)
{
cout << "String & operator=(const String &rhs)" << endl;
if(this != &rhs)//防止自复制
{
releaseFunc(); // 释放左操作数
_pstr = rhs._pstr; //浅拷贝
increRefcount();
}
return *this; // 返回*this
}
char & operator[](size_t idx)
{
if(idx < size())
{
if(getRefcount() > 1) //引用计数不是最后一个
{
char * ptmp = new char[size() + 5]() + 4;
strcpy(ptmp, _pstr);
decreRefcount();
_pstr = ptmp;
initRefcount();
}
return _pstr[idx];
}
else
{
static char nullchar = '\0';
return nullchar;
}
}
~String()
{
cout << "~String()" << endl;
releaseFunc();
}
public:
//获取引用计数
int getRefcount() const
{
return *(int *)(_pstr - 4);
}
//转C风格字符串
const char * c_str() const
{
return _pstr;
}
//字符串大小
size_t size() const
{
return strlen(_pstr);
}
private:
//初始化引用计数
void initRefcount()
{
// 引用计数初始化为1
// (pstr - 4) 指针回到堆起始位置(引用计数起始位置)
// (int *) 强转为 int*指针类型
// *(int *)(pstr - 4) 解引用
// *(int *)(pstr - 4) = 1 初始化为1
*(int *)(_pstr - 4) = 1;
}
//引用计数自增
void increRefcount()
{
++*(int *)(_pstr - 4);
}
//引用计数自减
void decreRefcount()
{
--*(int *)(_pstr - 4);
}
//堆空间释放
void releaseFunc()
{
decreRefcount();
if(getRefcount() == 0)
{
delete [] (_pstr - 4);
}
}
friend std::ostream &operator<<(std::ostream &os, const String &rhs);
private:
char * _pstr;
};
std::ostream & operator<<(std::ostream &os, const String &rhs)
{
os << rhs._pstr;
return os;
}
void test()
{
String s1 = "Hello";
String s2 = s1;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s1 count is " << s1.getRefcount() << endl;
cout << "s2 count is " << s2.getRefcount() << endl;
printf("s1 address is %p\n", s1.c_str());
printf("s2 address is %p\n", s2.c_str());
//调用拷贝构造函数
cout << endl;
String s3("world");
s3 = s1;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1 count is " << s1.getRefcount() << endl;
cout << "s2 count is " << s2.getRefcount() << endl;
cout << "s3 count is " << s3.getRefcount() << endl;
printf("s1 address is %p\n", s1.c_str());
printf("s2 address is %p\n", s2.c_str());
printf("s3 address is %p\n", s3.c_str());
//写时复制
cout << endl;
cout << "s3[0]写操作: s3[0] = 'h'" << endl;
s3[0] = 'h';
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1 count is " << s1.getRefcount() << endl;
cout << "s2 count is " << s2.getRefcount() << endl;
cout << "s3 count is " << s3.getRefcount() << endl;
printf("s1 address is %p\n", s1.c_str());
printf("s2 address is %p\n", s2.c_str());
printf("s3 address is %p\n", s3.c_str());
//读操作(出错)
cout << endl;
cout << "s1[0]读操作" << endl;
cout << "s1[0] = " << s1[0] << endl;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1 count is " << s1.getRefcount() << endl;
cout << "s2 count is " << s2.getRefcount() << endl;
cout << "s3 count is " << s3.getRefcount() << endl;
printf("s1 address is %p\n", s1.c_str());
printf("s2 address is %p\n", s2.c_str());
printf("s3 address is %p\n", s3.c_str());
}
int main()
{
test();
return 0;
}
上面的代码已经基本实现了 写时复制,但是经过过运行就可以发现一点错误,因为读操作还是有问题,没有区分下标运算符的读写操作,引用计数出错
于是我们就需要进行修改,区分下标运算符是赋值还是输出流,但是下标运算符重载的返回值为char内置类型,无法进行重载,于是我们就 重新定义一个类,让这个类为我们区分下标运算符是赋值还是输出流,分别实现下标运算符的赋值和下标运算符的输出流。
这样的方法也叫做 代理模式
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
代理者:一个类别可以作为其它东西的接口
思路2:代理模式实现读写操作
#include
#include
#include
using std::endl;
using std::cout;
class String
{
public:
String()
: _pstr(new char[5]() + 4) //留出4字节存放引用计数, 1字节存放'\0',+4 即偏移到数据的位置
{
cout << "String()" << endl;
initRefcount();
}
String(const char *pstr)
: _pstr(new char[strlen(pstr) + 5]() + 4) //申请数据+5(引用计数)的空间,指针偏移到数据的位置
{
cout << "String(const char *pstr)" << endl;
strcpy(_pstr, pstr);
initRefcount();
}
String(const String &rhs)
: _pstr(rhs._pstr)
{
cout << "String(const String &rhs)" << endl;
increRefcount();
}
String & operator=(const String &rhs)
{
cout << "String & operator=(const String &rhs)" << endl;
if(this != &rhs)//防止自复制
{
releaseFunc(); // 释放左操作数
_pstr = rhs._pstr; //浅拷贝
increRefcount();
}
return *this; // 返回*this
}
private:
//设计模式之代理模式
//区分(代理)下标运算符是赋值还是输出流
//操作String的类
class Char
{
public:
Char(String & self, size_t idx)//不加const的原因,有写操作
: _self(self)
, _idx(idx)
{
cout << "Char()" << endl;
}
char & operator=(const char &ch);
friend std::ostream & operator<<(std::ostream &os, const Char &rhs);
private:
String & _self;
size_t _idx;
};
public:
//char 不能重载,区分赋值和输出流,返回定义的自定义类型
Char operator[](size_t idx) //去掉引用,不能返回局部变量的引用
{
return Char(*this, idx);
}
~String()
{
cout << "~String()" << endl;
releaseFunc();
}
public:
//获取引用计数
int getRefcount() const
{
return *(int *)(_pstr - 4);
}
//转C风格字符串
const char * c_str() const
{
return _pstr;
}
//字符串大小
size_t size() const
{
return strlen(_pstr);
}
private:
//初始化引用计数
void initRefcount()
{
// 引用计数初始化为1
// (pstr - 4) 指针回到堆起始位置(引用计数起始位置)
// (int *) 强转为 int*指针类型
// *(int *)(pstr - 4) 解引用
// *(int *)(pstr - 4) = 1 初始化为1
*(int *)(_pstr - 4) = 1;
}
//引用计数自增
void increRefcount()
{
++*(int *)(_pstr - 4);
}
//引用计数自减
void decreRefcount()
{
--*(int *)(_pstr - 4);
}
//堆空间释放
void releaseFunc()
{
decreRefcount();
if(getRefcount() == 0)
{
delete [] (_pstr - 4);
}
}
friend std::ostream &operator<<(std::ostream &os, const String &rhs);
friend std::ostream & operator<<(std::ostream &os, const Char &rhs);
private:
char * _pstr;
};
std::ostream & operator<<(std::ostream &os, const String &rhs)
{
os << rhs._pstr;
return os;
}
char & String::Char::operator=(const char &ch)
{
if(_idx < _self.size())
{
if(_self.getRefcount() > 1) //引用计数不是最后一个
{
char * ptmp = new char[_self.size() + 5]() + 4;
strcpy(ptmp, _self._pstr);
_self.decreRefcount();
_self._pstr = ptmp;
_self.initRefcount();
}
_self._pstr[_idx] = ch; //赋值
return _self._pstr[_idx];
}
else
{
static char nullchar = '\0';
return nullchar;
}
}
std::ostream & operator<<(std::ostream &os, const String::Char &rhs)
{
os << rhs._self._pstr[rhs._idx];
return os;
}
void test()
{
//...同思路1
}
int main()
{
test();
return 0;
}
现在已经基本完成,还有一点小细节进行优化,就是输出流运算符的重载,其实可以去掉,可以直接在代理类中将 Char 转换为 char 类型 ,就可以直接输出到输出流,无需重载输出流运算符
修改代码,删除掉输出流运算符的重载,直接operator char()转换,return 数据即可
//设计模式之代理模式
//区分(代理)下标运算符是赋值还是输出流
//操作String的类
class Char
{
public:
Char(String & self, size_t idx)//不加const的原因,有写操作
: _self(self)
, _idx(idx)
{
cout << "Char()" << endl;
}
char & operator=(const char &ch);
//friend std::ostream & operator<<(std::ostream &os, const Char &rhs);
//Char转换为char类型,就可以直接输出到输出流(<<可以识别的类型)
operator char()
{
return _self._pstr[_idx];
}
private:
String & _self;
size_t _idx;
};