std::string的底层实现
三种方式:
深拷贝
写时复制
短字符串优化
深拷贝
无论什么情况,都是采用拷贝字符串内容的方式解决。不需要改变字符串内容时,对字符串进行频繁复制。
用一个string对象初始化另一个string对象时,源对象的内容会被完全复制到目标对象中,不是仅仅复制指针。即每一个string对象内部的指针都指向自己的内部字符存储区域。
string str = "hello";
string str1 = str;//拷贝构造的第二种形式
//重新申请空间复制内容。
//拷贝构造
//实现深拷贝,分配新内存并复制源对象的内容
string & (const string & res)
{
//如果源对象的内容非空,将delete
if(res.data)
{
size_t len = std::strlen(res.data);
data = new char[len + 1];
std::strcpy(data, res.data);
//另一种拷贝方式
//std::sprintf(data,"%s", res.data);
}
else
{
data = nullptr;
}
}
//赋值操作符
//先释放当前对象的资源,再分配内存空间并复制源对象的内容
string & operator=(const string & res)
{
//防止自赋值
if(this != &res)
{
//释放原先指针的资源
delete[] data;
if(res.data)
{
size_t len = std::strlen(res.data);
data = new char[len + 1];
std::strcpy(data, res.data);
//另一种拷贝方式
//std::sprintf(data,"%s", res.data);
}
else
{
data = nullptr;
}
}
return *this;
}
链接其它方面
1.内存布局
栈区:操作系统控制,由高地址向低地址生长,编译器做了优化,显示地址时栈区和其它区域保持一致的方向。
堆区:程序员分配,由低地址向高地址生长,堆区和栈区没有明确的界限。
全局/静态区:读写段(数据段), 存放全局变量、静态变量
文字常量区:只读段,存放程序中直接使用的常量
const char* p = "hello" hello这个内容就存放在文字常量区
程序代码区:只读段,存放函数体的二进制代码;
从上到下,地址由高到低。
std::string对象的控制块(指向堆内存的指针、长度、容量等)存储在栈上。
实际的字符串内容(字符数据)存储在堆上
字符常量本身,存储在文字常量区,但std::string会在堆上分配内存来存储它的副本。
static
static 全局变量/函数 只在本文件里面生效
static 局部变量 静态变量
static 类成员/类成员变量 指代和类相关的成员,可以用类作用域直接访问,而不限制于某个对象,所以没有this指针
写时复制
当字符串对象进行复制时,可以优化为指向同一个堆空间的字符串。回收堆空间时,引用计数refcount的-1,只有当引用计数refcount的值为0时才真正回收堆空间上的字符串。
string str1("hello"); //单独创建对象没有优化空间 //在创建对象是不会遍历所有对象保存的内容 string str2("hello");
用堆空间保存引用计数
将引用计数和字符串内容保存到一起
除了复制操作,赋值操作也可以确定两个string对象保存的字符串内容是相同的,也可以复用空间,引用计数随之改变。
相比于复制操作,还需要考虑string对象原本用来保存字符串的堆空间是否需要回收。
(1)原本空间的引用计数-1,引用计数减到0,才真正回收堆空间
(2)让自己的指针指向新的空间,并将新空间的引用计数+1
写时复制代码实现
//如果str1和str3共享一片空间存放字符串内容。
//读操作不需要进行复制,写操作应该让str1重新申请一片内存空间去修改,不应该改变str3的内容
cout << str1[0] << endl;//读操作,第一个参数os,第二个参数本对象
str1[0] = 'H'; //写操作,第一个参数本对象,第二个参数=
cout << str3[0] << endl;//str3的内容已经改变了
//要区分下标访问运算符到底是读操作还是写操作
//但是下标访问运算符都是string类对象,可以创建一个CowString类的内部类,让CowString的operator[]函数返回是这个类的对象,然后在这个新类型中对<<和=进行重载,让这两个运算符能够处理新类型对象,从而分开了处理逻辑
//综上所述str1[0]是一个对象,在这个类内部对=和<<运算符进行重载,就能够区分读操作和写操作了
#include <iostream>
#include <ostream>
#include <pstl/pstl_config.h>
#include <pthread.h>
using std::cout;
using std::endl;
using std::ostream;
#include <string.h>
#define PointCount 4
#define PointString 4
class String
{
private:
char* _pstr;
public:
class Temp
{
public:
String& _str;
int _count;
public:
Temp(String& str, int count)
:_str(str), _count(count)
{}
//<< : 读操作
friend ostream & operator<<(ostream & os, const Temp & rhs);
//=: 写操作
Temp & operator=(char c)
{
int index = _str.readCount();
//深拷贝
if(index > 1)
{
//开辟空间
char * str = _str.New(_str._pstr);
//初始化
sprintf(str, "%s", _str._pstr);
//原先的引用计数--
_str.countDecrease();
//修改数值
_str._pstr = str;
_str._pstr[_count] = c;
//修改引用计数
*((int *)(str - PointCount)) = 1;
//置空,临时变量
str = nullptr;
}
else if(index == 1)
{
//直接更改数据,不需要额外开辟空间
_str._pstr[_count] = c;
}
return *this;
}
};
public:
void UpdatePointString(int index, char c)
{
_pstr[index] = c;
}
char* New(const char* str)
{
//前四个字节是int,存放引用计数
//后面是存储字符串内容的空间
return new char[strlen(str) + 1 + PointCount]() + PointString;
}
void countadd()
{
++(*(int*)(_pstr - PointString));
}
void countDecrease()
{
--(*(int*)(_pstr - PointString));
_pstr = nullptr;
}
int readCount()
{
return *((int*)(_pstr - PointString));
}
void StringInit(const char* str)
{
*((int*)(_pstr - PointString)) = 1;
sprintf(_pstr, "%s", str);
}
void Destory()
{
//如果引用计数为1
if(readCount() == 1)
{
delete[] (_pstr - PointCount);
_pstr = nullptr;
}
else
{
countDecrease();
}
}
char* getStrPoint()
{
return _pstr;
}
public:
//无参构造
String()
{}
//有参构造
String(const char* pstr)
:_pstr(New(pstr))
{
//初始化
StringInit(pstr);
}
//析构
~String()
{
Destory();
}
//浅拷贝
//由于引用计数的出现只需要做浅拷贝即可,引用计数++
String(const String & res)
{
_pstr = res._pstr;
//引用计数++
countadd();
}
//下标操作运算符
String::Temp operator[](int count)
{
//返回的是一个用于区分读和写操作的对象
return String::Temp(*this, count);
}
};
ostream & operator<<(ostream & os, const String::Temp & rhs)
{
//String对象
os <<rhs._str.getStrPoint()[rhs._count];
return os;
}
void test()
{
String str1 = "hello";
//拷贝
String str2(str1);
String str3(str1);
cout << str1[0] << endl;
str3[0] = 'H';
cout << "str1[0] = " <<str1[0] << endl;
cout << "str3[0] = " << str3[0] << endl;
cout << "str1.readCount() = " << str1.readCount() << endl;
cout << "str3.readCount() = " << str3.readCount() << endl;
}
int main()
{
test();
return 0;
}
//重点,str1[0]是Temp对象,在Temp对象中做重载读写操作
//重载[]运算符,间接的创建了临时对象temp,此时cout << str1[0] --> cout << Temp(*this, 0); 再调用<<运算符的读操作
//读操作<<
//写操作=
//用堆空间保存引用计数,指针始终指向char开始的位置
总结,str1[0] = 'H' 和 << str1[0] 都有一个共同点就是str[0],所以在String类中重载下标运算符[],在这个重载函数中创建一个新的对象,对这个新对象重载=和<<将读写操作进行分离
当运算符处理自定义类型对象时,出现模棱两可的情况,可以嵌套一个内部类进行进一步的区分,例如str[0] = 'h';将str[0]当作一个类嵌套在String中,通过=运算符进行区分。
嵌套类
Point类是定义在Line类中的内部类,无法直接创建Point对象,需要在Line类名作用域中才能创建。
Point pt(1,2);//error Line::Point pt2(3,4);//ok
Point类是Line类的内部类,并不代表Point类的数据成员会占据Line类对象的内存空间,在存储关系上并不是嵌套的结构。
只有当Line类有Point类类型的对象成员时,Line类对象的内存布局中才会包含Point类对象(成员子对象)。
(1)如果Line类中没有Point类的对象成员,sizeof(Line) = 8;
(2)如果Line类中有两个Point类的对象成员,sizeof(Line) = 24;
重载形式
友元函数的重载形式
不会修改操作数的值的运算符,倾向于采用友元函数的方式重载
具有对称性的运算符可能转换任意一端的运算对象,例如相等性、位运算符等,通常应该是友元形式重载
普通函数的重载形式
访问类中的私有成员,给这个类加共有的get系列函数。
成员函数的重载形式
第一个操作数实际上是this指针,也是运算符左操作数所指向的对象
与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员函数形式重载
会修改操作数的值的运算符,倾向于采用成员函数的方式重载
赋值=、下标[ ]、调用()、成员访问->、成员指针访问->* 运算符必须是成员函数形式重载
短字符串优化
当字符串的字符数小于等于15时, buffer直接存放整个字符串;当字符串的字符数大于15时, buffer 存放的就是一个指针,指向堆空间的区域。这样做的好处是,当字符串较小时,直接拷贝字符串,放在 string内部,不用获取堆空间,开销小。
union表示共用体,允许在同一内存空间中存储不同类型的数据。共用体的所有成员共享一块内存,但是每次只能使用一个成员。
class string { union Buffer{ char * _pointer; char _local[16]; }; size_t _size; size_t _capacity; Buffer _buffer; };
总结:最佳策略
-
很短的(0~22)字符串用SSO,23字节表示字符串(包括'\0'),1字节表示长度
-
中等长度的(23~255)字符串用eager copy,8字节字符串指针,8字节size,8字节capacity.
-
很长的(大于255)字符串用COW, 8字节指针(字符串和引用计数),8字节size,8字节capacity.
一些感言
找到工作了,这些天一直在搞图像捕获用来选取角色和血量匹配,大概有五天的时间没有静下心来学习。老板给了我一个月的时间,但是我现在基本上已经搞出来了,而且打包生成了可执行程序。就等美术方面了。但是我明天才入职,老板还不知道我的进度,目前是一个初创公司。我需要进去稳定一个礼拜,才能慢慢的出货,而且代码是我一个人写的,打包的地方也有坑,整个公司就三个会程序的,其它全是美术。就我一个既会c++又会python,其实python我也不太懂。但我能看懂,知道该怎么改代码这就完全足够了。由于这段时间没能学习,所以放上我之前做的一些笔记。