目录
1、Eager Copy(深拷贝)
2、COW(Copy-On-Write)写时复制
2.1写时复制的实现
3、SSO(Short String Optimization)短字符串优化
4、最佳策略
5、线程安全性
我们都知道, std::string的一些基本功能和用法了,但它底层到底是如何实现的呢? 其实在std::string的历史中,出现过几种不同的方式。下面我们来一一揭晓。
我们可以从一个简单的问题来探索,一个std::string对象占据的内存空间有多大,即sizeof(std::string)的值为多大?如果我们在不同的编译器(VC++, GNU, Clang++)上去测试,可能会发现其值并不相同;即使是GNU,不同的版本,获取的值也是不同的。
虽然历史上的实现有多种,但基本上有三种方式:
Eager Copy(深拷贝)
COW(Copy-On-Write 写时复制)
SSO(Short String Optimization-短字符串优化)
每种实现,std::string都包含了下面的信息:
1.字符串的大小
2.能够容纳的字符数量
3.字符串内容本身
1、Eager Copy(深拷贝)
最简单的就是深拷贝了。无论什么情况,都是采用拷贝字符串内容的方式解决,这也是我们之前已经实现过的方式。这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。所以需要对其实现进行优化,之后便出现了下面的COW的实现方式。
class String { public: String(const String &rhs) : _pstr(new char[strlen(rhs._pstr) + 1]()) { strcpy(_pstr, rhs._pstr); } private: char *_pstr; };
2、COW(Copy-On-Write)写时复制
当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当需要修改其中一个字符串内容时,才执行真正的复制。其实现的示意图,有下面形式:
为了实现的简单,在GNU4.8.4的中,采用的是这种形式。从上面的实现,我们看到引用计数并没有与std::string的数据成员放在一起,为什么呢?大家可以思考一下。
当执行复制构造或赋值时,引用计数加1,std::string对象共享字符串内容;当std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1,直到引用计数为0时,则真正释放字符串内容所在的空间。根据这个思路,大家可以自己动手实现一下。
大家再思考一下,既然涉及到了引用计数,那么在多线程环境下,涉及到修改引用计数的操作,是否是线程安全的呢?为了解决这个问题,GNU4.8.4的实现中,采用了原子操作。总结:当只是进行读操作的时候就进行浅拷贝,然后如果需要进行写操作的时候,再进行深拷贝。实现方式使用浅拷贝加上引用计数。
2.1写时复制的实现
代码:
#include <string.h> #include <iostream> using std::cout; using std::endl; class String { public: String() : _pstr(new char[5]() + 4) { cout << "String()" << endl; initRefCount(); } String(const char *pstr) : _pstr(new char[strlen(pstr) + 5]() + 4) { cout << "String(const char *)" << endl; strcpy(_pstr, pstr); initRefCount(); } //String s2 = s1; String(const String &rhs) : _pstr(rhs._pstr) { cout << "String(const String &)" << endl; increseRefCount(); } //s3 = s1; String &operator=(const String &rhs) { cout << "String &operator=(const String &)" << endl; //1、自复制 if (this != &rhs) { //2、释放左操作数 release(); //3、浅拷贝 _pstr = rhs._pstr; increseRefCount(); } //4、返回*this return *this; } private: //s3[0] = 'H' class CharProxy { public: CharProxy(String &self, size_t idx) : _self(self) , _idx(idx) { } //写操作 char &operator=(const char &ch); //读操作 /* friend std::ostream &operator<<(std::ostream &os, const CharProxy &rhs); */ operator char()//利用由自定义类型向其他类型转换的思想 { cout << "operator char()" << endl; return _self._pstr[_idx]; } private: String &_self; size_t _idx; }; public: //代理模式 CharProxy operator[](size_t idx) { return CharProxy(*this, idx);//方括号运算符执行构造函数 } #if 0 //s3 = s1; //s3[0] = 'H' char &operator[](size_t idx) { if (idx < size()) { if (getRefCount() > 1)//共享的 { //深拷贝 char *tmp = new char[size() + 5]() + 4; strcpy(tmp, _pstr); //引用计数-- descreRefCount(); //浅拷贝 _pstr = tmp; //初始化引用计数 initRefCount(); } return _pstr[idx]; } else { static char charNull = '\0'; return charNull; } } #endif ~String() { cout << "~String()" << endl; release(); } //获取引用计数 int getRefCount() const { return *(int *)(_pstr - 4); } //获取底层的指针 const char *c_str() const { return _pstr; } private: size_t size() const//字符串的长度 { return strlen(_pstr); } void initRefCount()//初始化引用技术 { *(int *)(_pstr - 4) = 1; } void increseRefCount()//增加引用计数 { ++*(int *)(_pstr - 4); } void descreRefCount()//减少引用计数 { --*(int *)(_pstr - 4); } //释放 void release() { descreRefCount(); if (0 == getRefCount()) { delete[](_pstr - 4); } } friend std::ostream &operator<<(std::ostream &os, const String &rhs); //本身是CharProxy中的友元 /* friend std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs); */ private: char *_pstr; }; std::ostream &operator<<(std::ostream &os, const String &rhs) { if (rhs._pstr) { os << rhs._pstr; } return os; } //写操作 //CharProxy = 'H' char &String::CharProxy::operator=(const char &ch)//注意这一句的书写 { if (_idx < _self.size()) { if (_self.getRefCount() > 1)//共享的 { //深拷贝 char *tmp = new char[_self.size() + 5]() + 4; strcpy(tmp, _self._pstr); //引用计数-- _self.descreRefCount(); //浅拷贝 _self._pstr = tmp; //初始化引用计数 _self.initRefCount(); } _self._pstr[_idx] = ch;//真正的进行写操作 return _self._pstr[_idx]; } else { static char charNull = '\0'; return charNull; } } #if 0 std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs) { os << rhs._self._pstr[rhs._idx]; return os; } #endif void test() { String s1("hello"); cout << "s1 = " << s1 << endl; cout << "s1.getRefCount() = " << s1.getRefCount() << endl; printf("s1'address = %p\n", s1.c_str()); cout << endl << endl; String s2 = s1; cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s1.getRefCount() = " << s1.getRefCount() << endl; cout << "s2.getRefCount() = " << s2.getRefCount() << endl; printf("s1'address = %p\n", s1.c_str()); printf("s2'address = %p\n", s2.c_str()); cout << endl << endl; String s3("world"); cout << "s3 = " << s3 << endl; cout << "s3.getRefCount() = " << s3.getRefCount() << endl; printf("s3'address = %p\n", s3.c_str()); cout << endl << endl; s3 = s1; cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s3 = " << s3 << endl; cout << "s1.getRefCount() = " << s1.getRefCount() << endl; cout << "s2.getRefCount() = " << s2.getRefCount() << endl; cout << "s3.getRefCount() = " << s3.getRefCount() << endl; printf("s1'address = %p\n", s1.c_str()); printf("s2'address = %p\n", s2.c_str()); printf("s3'address = %p\n", s3.c_str()); cout << endl << "对s3[0]执行写操作" << endl; //s3.operator[](idx) //CharProxy = char s3[0] = 'H';//char = char char ===>CharProxy cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s3 = " << s3 << endl; cout << "s1.getRefCount() = " << s1.getRefCount() << endl; cout << "s2.getRefCount() = " << s2.getRefCount() << endl; cout << "s3.getRefCount() = " << s3.getRefCount() << endl; printf("s1'address = %p\n", s1.c_str()); printf("s2'address = %p\n", s2.c_str()); printf("s3'address = %p\n", s3.c_str()); cout << endl << "对s1[0]执行读操作" << endl; //cout << CharProxy //输出单个字符的时候会进行类型的强转,执行operator char()函数 cout << "s1[0] = " << s1[0] << endl;//cout << CharProxy===>char cout << "s1 = " << s1 << endl; cout << "s2 = " << s2 << endl; cout << "s3 = " << s3 << endl; cout << "s1.getRefCount() = " << s1.getRefCount() << endl; cout << "s2.getRefCount() = " << s2.getRefCount() << endl; cout << "s3.getRefCount() = " << s3.getRefCount() << endl; printf("s1'address = %p\n", s1.c_str()); printf("s2'address = %p\n", s2.c_str()); printf("s3'address = %p\n", s3.c_str()); } int main(int argc, char **argv) { test(); return 0; }
运行结果:
String(const char *) s1 = hello s1.getRefCount() = 1 s1'address = 00F71134 String(const String &) s1 = hello s2 = hello s1.getRefCount() = 2 s2.getRefCount() = 2 s1'address = 00F71134 s2'address = 00F71134 String(const char *) s3 = world s3.getRefCount() = 1 s3'address = 00F7155C String &operator=(const String &) s1 = hello s2 = hello s3 = hello s1.getRefCount() = 3 s2.getRefCount() = 3 s3.getRefCount() = 3 s1'address = 00F71134 s2'address = 00F71134 s3'address = 00F71134 对s3[0]执行写操作 s1 = hello s2 = hello s3 = Hello s1.getRefCount() = 2 s2.getRefCount() = 2 s3.getRefCount() = 1 s1'address = 00F71134 s2'address = 00F71134 s3'address = 00F70ECC 对s1[0]执行读操作 operator char() s1[0] = h s1 = hello s2 = hello s3 = Hello s1.getRefCount() = 2 s2.getRefCount() = 2 s3.getRefCount() = 1 s1'address = 00F71134 s2'address = 00F71134 s3'address = 00F70ECC ~String() ~String() ~String() F:\Re-exam test\C study\2024-1-17\Debug\2024-1-17.exe (进程 12224)已退出,返回代码为: 0。 按任意键关闭此窗口...
3、SSO(Short String Optimization)短字符串优化
目前,在VC++、GNU5.x.x以上、Clang++上,std::string实现均采用了SSO的实现。
通常来说,一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char*指针就占用了8个字节,所以SSO就出现了,其核心思想是:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。其实现示意图如下:当字符串的长度小于等于15个字节时,buffer直接存放整个字符串;当字符串大于15个字节时,buffer存放的就是一个指针,指向堆空间的区域。这样做的好处是,当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。
总结:当字符串的长度小于16字节的时候,存放在栈上;当字符串的长度大于16字节的时候,就放在堆上
4、最佳策略
以上三种方式,都不能解决所有可能遇到的字符串的情况,各有所长,又各有缺陷。综合考虑所有情况之后,facebook开源的folly库中,实现了一个fbstring, 它根据字符串的不同长度使用不同的拷贝策略,最终每个fbstring对象占据的空间大小都是24字节。
1. 很短的(0~22)字符串用SSO,23字节表示字符串(包括'\0'),1字节表示长度
2. 中等长度的(23~255)字符串用eager copy,8字节字符串指针,8字节size,8字节capacity.
3. 很长的(大于255)字符串用COW, 8字节指针(字符串和引用计数),8字节size,8字节capacity.
5、线程安全性
两个线程同时对同一个字符串进行操作的话, 是不可能线程安全的, 出于性能考虑, C++并没有为string实现线程安全, 毕竟不是所有程序都要用到多线程。
但是两个线程同时对独立的两个string操作时, 必须是安全的. COW技术实现这一点是通过原子的对引用计数进行+1或-1操作。CPU的原子操作虽然比mutex锁好多了, 但是仍然会带来性能损失, 原因如下:
1.阻止了CPU的乱性执行.
2.两个CPU对同一个地址进行原子操作, 会导致cache失效, 从而重新从内存中读数据.
3.系统通常会lock住比目标地址更大的一片区域,影响逻辑上不相关的地址访问
这也是在多核时代,各大编译器厂商都选择了SS0实现的原因。