模拟实现C++中的string类(详细解析)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 模拟实现C++中的string类

学习C++,特别是C++中的STL部分,重点不是学习如何去使用STL,而是知道其底层原理是怎么样的,是怎么去实现的。因此,本篇文章带来的是对C++中的string的模拟实现。废话不多说,让我们去了解string是如何实现的吧!

一.模拟实现构造函数

对于构造函数,在官方库中,C99有下面种类:

B~]{YGL@WXP~QC(U5X`C%@J.png

我们主要实现的是

string();

string(const char* s);

string(const string& str);

前两个称为构造函数,第三个称为拷贝构造

①无参构造函数string();

//无参构造string()
        {
_str=newchar[1];
_str[0] ='\0';
_size=_capacity=0;
        }

image.gif

解析:由于是无参构造,在创建string对象时,字符串是空的,需要一个'\0',所以_str开辟一个字符的空间,用来存放'\0',然后将字符串有效个数和字符串空间赋值为0.

②带参构造函数string(const char* str);

//带参构造string(constchar*str)
        {
_size=strlen(str);
_capacity=strlen(str);
_str=newchar[_capacity+1];
strcpy(_str, str);
        }

image.gif

解析:先对_size和_capacity进行初始化,而且_capacity没有额外+1,而是在_str开辟空间的时候才加1,加1的目的是放'\0',而先不加1的原因是在后面实现插入数据的时候,我们需要用到if(_size==_capacity)这个条件来判断容量是否满了,需要扩容,如果先加1,就不好操作了。最后将str的内容拷贝到_str中去。

温馨提示:带参构造函数没有使用初始化列表,原因有其下:如果使用初始化列表,在初始化_str,_size和_capacity的时候,有以下方法:

方法①:

       string(const char* str)

           :_str(str)//不能这样初始化:因为这样给,给的常量字符串,常量字符串存在常量区,常量区不能被修改

       {

               //......      

       }

方法②:

       //将上面写法改成下面这种,使用new来开辟空间,空间的大小为str的大小+1

       string(const char* str)

           :_str(new char[strlen(str)+1])//+1存放'\0',strlen不包含'\0'

           ,_size(strlen(str))

         ,_capacity(strlen(str))

       {

       }

       //但我们发现,如果使用上面的方法,我们需要使用3次strlen,这样的方法不好

       //其实我们可以不使用初始化列表,这样就能只是用一次strlen

对于带参构造函数,上面那种方法还不够好,因为我们还可以使用缺省参数!

看下面的实现代码:

string(constchar*str="")
        {
_size=strlen(str);//0_capacity=_size;//0_str=newchar[_capacity+1];//0+1=1strcpy(_str, str);
        }

image.gif

这里的缺省值为什么是""?

解析:""代表的是空字符,里面就隐藏着一个'\0',由于strlen是以第一个'\0'为终点的,所以当我们给的缺省值是""的时候,strlen计算出来的长度是0。到这里,肯定会有人想,为什么不直接给nullptr或'\0'?                                                                                                                                        因为nullptr给了str后,str为空指针,strlen不能在空指针上使用,就会报错。而给'\0','\0'是一个字符,其ascll码值为0,直接给指针类型的变量,也相当于是空指针了。所以着两种都不可以。如果给"\0",这个是可以的,这个跟""类似,""是带一个'\0',而"\0"是字符串,里面有两个"\0",计算出来的长度都为0.

③拷贝构造函数

拷贝构造函数,在C++中,有两种版本的写法,称为传统版本和现代版本。

先来看传统版本的:

//拷贝构造的传统写法string(conststring&s)
        {
_str=newchar[s._capacity+1];
_capacity=s.capacity();
_size=s._size;
strcpy(_str, s._str);
        }

image.gif

解析:拷贝构造跟构造函数的实现方法差不多,区别就在于拷贝构造是将参数s的属性内容全部拷贝到this中,所谓this,就是调用拷贝构造的string类对象的指针。

对于拷贝构造函数,最重点的是深拷贝和浅拷贝!

这里重点解析一下深拷贝和浅拷贝。

浅拷贝:说实在的,浅拷贝虽然带着个拷贝,但是其空间内存是共用的。看下图:

HS%2GR2RA@IP7VPVXPTP[@6.png

这会导致什么呢?当我们调用完s2,但后面的程序还需要用到s1的时候,析构函数会将调用完,不需要再使用的s2释放掉,将其空间内存还给操作系统。到了这一步,问题就出现了,我的s2的内存是还给操作系统了,但是s1也是指向这个内存空间的啊!s1还需要用到,这不就导致内存泄漏了吗!报错就报错了在这里了!

所以,我们需要用到深拷贝。

深拷贝:给s2独自开辟一块空间,用来存放"hello world",这样以后,s1和s2是独立的,有自己各自的房间,谁也不会影响谁。所以我们看到上面的代码,给_str开辟了一块与s1同样大的空间,这就是深拷贝。

这里我们额外结合之前的知识总结一下,什么时候用到深拷贝,什么时候不需要深拷贝,只需浅拷贝即可。

非自定义类型,比如int、char、double等等的类型,不需要用到深拷贝,同样的它们也不需要用到析构函数。

对于自定义类型:比如string就需要用到深拷贝了,因为每个对象都需要有自己的小房间。

然后来看看现代版本的,对于现代版本,追求是代码简介,不是效率。看下面代码:

string(conststring&s)
            :_str(nullptr)
            , _size(0)
            , _capacity(0)
        {
stringtmp(s._str);//调用构造函数swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_size, tmp._capacity);
//tmp不能指向随机值,所以要初始化        }

image.gif

解析:现代版本的构造函数,使用了初始化列表来初始化s2。因为我们是创建了tmp对象,来为s2打工,让tmp和s2交换,交换后的tmp不能指向随机值,而是指向nullptr,所以要初始化s2。通俗的来讲,tmp相当于s2的打工人,为s2获取s1的属性,然后将其交给s2,而s2把工资(nullptr)发给tmp。

但是这样的写法不够简介,而且需要调用swap,调用3次拷贝构造(传值传参嘛)。在改写之前,我们来区分一下string自带的swap和C++库自带的swap的区别:

string自带的swap:

P1DKTB4S6O@DM2Z$_4RN4RD.png

C++库自带的swap:

LW}${VO)SUWN7I([SM)GND2.png

image.gif

我们可以看到,C++库自带的swap函数,是模板类型的,每一次调用,都需要推演出参数的类型,在上面的代码中,就需要推演int、char*两种类型,需要拷贝3次,比较花时间。而使用string类自带,已经定义好string类,并且是引用,不需要拷贝。所以我们来实现一下string类的swap。

其实,我上面讲的,虽然我们模拟实现了string类的swap,调用了string的swap,但效率是一样的,因为实现的代码是这样的:

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

image.gif

我上面说了,一切都是为了代码的简洁哈哈哈哈哈哈哈哈哈,上面说了那么多,是为了让我们了解一下底层原理。

最后,我们可以将现代版本改写成这样子:

string(conststring&s)
            :_str(nullptr)
            ,_size(0)
            ,_capacity(0)
        {
stringtmp(s._str);
//this->swap(tmp);swap(tmp);
        }

image.gif

④析构函数

析构函数对于自定义类型也是很重要的。因为需要释放掉内存空间。

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

image.gif

⑤operator=()赋值

operator=()跟拷贝构造一样,也有传统和现代写法

先来个传统的写法:string s2(s1);

string&operator=(conststring&s)
        {
if (this!=&s)//不能给自己赋值            {
char*tmp=newchar[s._capacity+1];
strcpy(tmp, s._str);
delete[] _str;//需要把s2原本的空间给释放掉_str=tmp;
_size=s._size;
_capacity=s._capacity;
            }
return*this;
        }

image.gif

解析:思路很简单,就是创建一个新的char* ,来接收s,然后释放掉s2原本的内存空间,再指向tmp的内存空间即可。

U4$AS5RU3`$FZPE(ET6[P_V.png

然后来看现代写法:

跟拷贝构造的现代版本一样,使用交换,找个打工人。

string&operator=(conststring&s)
        {
if (this!=&s)
            {
stringtmp(s);//直接调用拷贝构造swap(tmp);
            }
return*this;
        }

image.gif

二.模拟实现string类的容量操作

①size() 和 len(),还有capacity()

这size() 和 len()的功能是相同的,都是返回字符的有效个数。这里就写size()。

size_tsize() const        {
return_size;
        }

image.gif

为什么要加个const?因为size是只读,加const保护起来吧。

capacity()是返回总空间的大小。可以理解为:size是水的体积容量,capacity是水杯瓶子的容量

size_tcapacity() const        {
return_capacity;
        }

image.gif

②clear()

清除掉有效字符,但是不改变空间大小。把水瓶里面的水喝完吧。这个实现的方法是直接将_size置为0,然后将0位置的_str[0]赋值'\0',就行了

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

image.gif

③resize(size_t n = 0)/resize(size_t n  = 0, char c = '\0');

这个函数重要。我们先来分析这个函数的功能如何:

1.这个函数的功能是对有效字符的个数进行操作。它有三种情况。①当n大于字符串的长度并且小于capacity的时候,就是进行插入数据的处理②当n大于capacity,并且大于字符串长度的时候,是进行插入数据+扩容的处理。③当n小于字符串长度的时候,便是删除数据。

2.对于resize(size_t n)/resize(size_t n,char c)这两种重载,需要知道的是,resize(size_t n)是将多余的空间用0来填充,而resize(size_t n,char c)用字符c来填充。

了解到这里,我们就可以去模拟实现它了。当然啦,我们发现我们这里实现的时候,用到了reserve和operator[],这两个我们还没实现,但这样说明了C++在设计的时候,很多功能都是互相辅助的,没你没我都不行。

voidresize(size_tn=0, charval='\0')
        {
//当n大于_sizeif (n>_size)
            {
reserve(n);//判断是否需要扩容,即n是否大于capacityfor (size_ti=_size; i<n; i++)
                {
_str[i] =val;
                }
_size=n;
_str[n] ='\0';
            }
else//小于  删除            {
_str[n] ='\0';
_size=n;
            }
        }

image.gif

解析这段代码:我们通过上面的情况,当n>_size,自然是要插入数据,然后再看看是否需要扩容,这一步交给reserve(n)来完成。插入数据这一步,从第_size这个位置开始,这个位置是值是'\0',自然会被覆盖掉,然后一直到n-1,在第n个位置补上'\0',最后将_size的长度变为n。完成!当n<_size,自然是相当于删除数据,直接在第n个位置补'\0',然后_size改成n,完成!当n==_size,什么都不用干。

④reserve(size_t n)

这个函数的功能,是增大水杯的容量,一升的水杯不够我喝,那我就用两升的,两升的不够,那就十升。所以,对于这个函数,它的功能是扩容,为字符串预留空间,是把空间(capacity)增大,不会缩小空间,而且不会改变有效字符的个数或长度。具体是①当n大于当前的空间的时候,那么reserve会将空间扩大。②当n小于等于当前空间的时候,那么不会对原有的空间做什么事。

看下面实现的代码:

voidreserve(size_tn)
        {
if (n>_capacity)//判断是否需要扩容            {
char*tmp=newchar[n+1];//多加1,给'\0'strcpy(tmp, _str);//将_str的内容拷贝过去delete[] _str;//释放原本的空间_str=tmp;//拿到tmp的内存空间_capacity=n;//最后将容量改为n            }
        }

image.gif

三.模拟实现访问及遍历的函数

①operator[](size_t pos);

这个比较简单实现,在返回前最后断言一下。当然,需要写两个重载类型,一个是可读可写,一个是只读不写。

//可读可写char&operator[](size_tpos)
        {
assert(pos<_size);
return_str[pos];
        }
//只读constchar&operator[](size_tpos) const        {
assert(pos<_size);
return_str[pos];
        }

image.gif

②begin()+end()

实现begin()和end(),用到迭代器。迭代器就是一种用法跟指针差不多,但不一定是指针的东西。现阶段,我们可以只知道它的用法,就是把它当成指针用就行了,但是不一定是指针。

image.gifB7)_4)BP@YHL1(GRKGPA`GR.png

代码如下:

typedefchar*iterator;
iteratorbegin() const        {
return_str;
        }
iteratorend() const        {
return_str+_size;
        }

image.gif

解析:对于begin(),返回的_str,因为_str是指针类型, 指向的是首元素的地址,便是最开始的那个位置,即begin。对于end(),返回的是_str+_size,指向的是最后一个位置,即'\0'这个位置。

③范围for

其实范围for,看着好像很高大上一样,我们在用的时候,不知道它为什么能够识别到循环的起点和重点,为什么有这样的功能。这里揭秘:范围for其实就是迭代器的分身!它相当于宏替换一样,当我们写出范围for后,在编译代码的时候,编译器就会将其展开,变成了使用迭代器来实现循环的代码一样了。看下面代码:

strings("12345");
//使用迭代器string::iteratorit=s.begin();
while (it!=s.end())
        {
            (*it)++;
++it;
        }
cout<<s.c_str() <<endl;
//范围forfor (auto&ch : s)
        {
ch--;
        }
cout<<s.c_str() <<endl;

image.gif

结果分别为:23456和12345

然后,如果我们将迭代器中的begin()改为Begin(),也就是将函数名称改一改,接下来我们就会发现,虽然while循环依然可以使用,范围for用不了了。

J56]1T}J`9YBQWH5QUXWU7V.png

报错的内容为:

[E7P]MRHDNF5FN26RE~FDJG.png

咦?不对啊,我都没有在范围for循环中用到begin()这个函数,怎么会在报错信息中出现这个?这也就很好地解释了我上面所讲的,范围for类似于宏替换,将使用迭代器的循环替换成了范围for。

四.模拟实现string类对象修改操作

①push_back()

push_back的实现,相当于数据结构中的顺序表差不多,如果我们对顺序表的实现熟悉的话,实现push_back一点问题都没有。

在插入数据前,判断一下容量是否满了,如果满了就进行扩容。然后在_size处插入数据,然后将_size++,最后在_size处添上'\0'。代码如下:

voidpush_back(charch)//可以二倍扩容,但是要注意,如果是个空字符串,也就是说capacity是0        {
if (_size==_capacity)
            {
size_tnewCapacity=_capacity==0?4 : _capacity*2;
reserve(newCapacity);
            }
_str[_size] =ch;
_size++;
_str[_size] ='\0';
        }

image.gif

加个小知识:

我们来看看每次是如何扩容的?

使用下面代码来测试一下:

voidTestPushBack()//每次扩容代价都比价大,提前把空间开好{
strings;
size_tsz=s.capacity();
cout<<"making s grow:"<<endl;
for (inti=0; i<1000; i++)
    {
s.push_back('c');
if (sz!=s.capacity())
        {
sz=s.capacity();
cout<<"capacity changed: "<<sz<<endl;
        }
    }
}

image.gif

在windows下,vs2019编译器:

2WUL`(N$R%H0OK])}5542JL.png

在Linux下:

WAJ06MWQSOW}WNEO1@NL13Q.png

结论:在vs2019的编译器下,每一次扩容的容量是一点五倍左右,而在Linux系统下便是两倍。

②append();

append()的实现也差不多,不过当容量满了,扩容的时候,扩二倍就一定够吗?肯定不是的,比如当原来的内容是"hello\0",而我要插入"world hello world\0",这个字符串就已经比本身的二倍还要长,所以是不能直接扩二倍。而是扩容到需要追加的字符串的长度再加1,这个长度才行。

voidappend(constchar*str)
        {
intlen=strlen(str);
if (_size+len>_capacity)//判断原有的长度加上要追加的字符串长度是否大于现有的容量            {
reserve(_size+len);
            }
strcpy(_str+_size, str);//size下标的位置开始,拷贝_str,结尾有'\0',不需要额外加'\0'_size+=len;
        }

image.gif

③operator+=();

这个需要模拟实现两种情况,因为它既可以追加一个字符,也能追加一个字符串。而且,它的模拟实现,我们可以直接调用push_back和append。

//追加字符串string&operator+=(constchar*str)
        {
append(str);
return*this;
        }
//追加字符string&operator+=(charch)
        {
push_back(ch);
return*this;
        }

image.gif

④c_str()

这个函数,是返回C格式的字符串。何为C格式的字符串?在c里面,一个字符指针拿到的是字符串的首元素的地址。使用这个函数,就可以拿到这个地址了。

代码如下:

constchar*c_str() const        {
return_str;
        }

image.gif

⑤find()

find()函数返回的是目标字符的位置下标,默认从0开始,也就是说缺省值为0,是半缺省函数。我们通过循环来找。当然,find()函数可以找字符,也可以找字串。对于找某个字符来说,直接使用循环遍历,找到就返回下标。对于找子串,我们可以使用strstr函数来找,返回的是子串的首元素的地址,用char*类型的变量来接收,最后返回首元素的下标。代码如下:

//找字符size_tfind(charch, size_tpos=0) const        {
assert(pos<_size);
while (pos<_size)
            {
if (_str[pos] ==ch)
                {
returnpos;
                }
++pos;
            }
returnnpos;
        }
//找子串size_tfind(constchar*str, size_tpos=0) const        {
assert(pos<_size);
constchar*ptr=strstr(_str+pos, str);
if (ptr==nullptr)//判空,如果没找到,就返回npos            {
returnnpos;
            }
else            {
returnptr-_str;//找到,就ptr-_str,即为首元素的下标            }
        }

image.gif

⑥insert()

inset()函数相当于是顺序表里面的任意位置插入,所以我们就知道了它的效率不太行,如果字符串很长很长,但我们想要在字符串里面插入字符或字符串,那么就需要挪动很多字符。这也是我们为什么必须学习其底层的实现方法,只有了解了,学习了才能知道哪些接口好用,哪些不好用。

在插入之前,老办法,先判断容量是否满了,如果满了,那就扩容。同样了,insert重载了两种方法,一个是插入字符,一个是插入字符串。

注意,这部分有坑,所以要重点讲解。

插入字符:先来看下面代码:

string&insert(size_tpos, charch)
        {
assert(pos<_size);
if (_size==_capacity)
            {
intNewCapacity=_capacity==0?4 : _capacity*2;
reserve(NewCapacity);
            }
size_tend=_size;
while (end>=pos)
            {
_str[end+1] =_str[end];//往后挪end--;
            }
_str[pos] =ch;
_size++;
return*this;
        }

image.gif

这一看,是没问题。如果我们给出的例子是这样的

A]AZRQM`SZIN]P$276H2SV4.png

根据上面代码,最后将第一个位置的w挪到第二个位置后,x就会插入到下标为0的这个位置,但实际上.......我们一运行,陷入了死循环。

为什么?原因就在于end我们的类型是无符号的,不会有负数,当end等于0的时候,将第一个w字符挪动后,不会变成-1,而是变成一个很大的值,直接进入下一个循环。

下面给出调试的结果:

M%U6[O}I6TLHG%UI9430IJV.png

那么,就有同学会想,我们可以把size_t改为int呀!

好的,我们现在来改一下:

8GK[S6DDKTUC{F}8Y7Y9`]D.png

当我们的end变为-1的时候,我们会发现一个问题,怎么回事?明明end为-1,pos为0,而循环条件是end>=pos,还会进入循环?其实原因就在于int隐式提升,int会提升为size_t。在C/C++中,当小的类型于相较大的类型做运算时,小的类型会向大的类型提升,比如int跟double做运算时,int会提升为double。

其解决方法就是,将pos强制转换成int类型。还有就是,在C++的string类的库中,end的类型就是size_t的,我们既然要模拟实现string,我们就遵循规则。那么我们该如何取解决这个问题呢?

好办!让end再往后挪一格,此时,我们循环挪动数据的目标,就是挪动数据,不是放数据,此时循环条件变成end>pos,不能等于。

看下面代码:

string&insert(size_tpos, charch)
        {
assert(pos<_size);
if (_size==_capacity)
            {
intNewCapacity=_capacity==0?4 : _capacity*2;
reserve(NewCapacity);
            }
//size_t end = _size;//int end = _size;//while (end >= (int)pos)//{//  _str[end + 1] = _str[end];//往后挪//  end--;//}size_tend=_size+1;
while (end>pos)
            {
_str[end] =_str[end-1];//数据挪的位置而不是放的位置end--;
            }
_str[pos] =ch;
_size++;
return*this;
        }

image.gif

插入字符串:先看下面代码:

根据上面的插入字符的代码,我们很容易就写出下面这样的代码:

string&insert(size_tpos, constchar*str)
        {
intlen=strlen(str);
if (_size+len>_capacity)
            {
reserve(_size+len);
            }
size_tend=_size+len;
while (end>pos)
            {
_str[end] =_str[end-len];
end--;
            }
strcpy(_str+pos, str);
_size+=len;
return*this;
        }

image.gif

我们测试下面这个代码:

JABA1NUBQ4)7[M`5}3JHG5T.png

一运行,发现,只打印了hello,怎么回事?其实我们不能使用strcpy,因为它会将hello 里面的'\0'也拷贝了过去,所以也只打印了hello出来。

所以我们改一下这个代码,将strcpy改成strncpy;

K~TC`SIG6XH)}@4}U5L4RJW.png

运行,发现结果对了!好啦,就这样了!错!错哪?我们先来调试瞅一瞅

我们其实可以好好想想,看看这个调试的结果

7KU$O0JBL~$%SV0N1UOB0_5.png

当pos等于6,而end也等于6的时候,应该就停止了,直接将最近的字符串拷贝过去, 覆盖掉,但是循环并没有停止,反而进行进行,这会导致什么结果?end小于pos了,减掉是负数,在无符号下,是一个很大的数,所以我们接着调试运行看看结果:

Z`OCAGB)~V@2TXNA$`GUNWI.png

它会将随机数给挪过去了!这显然是不可以的。所以我们必须控制好循环条件,将循环条件改为end>=pos+len,或者是end > pos+len-1.

⑦erase()

erase()函数是个半缺省函数,如果我们不写需要删除的字符串的长度,那么就会默认使用npos长度,也就是说从pos位置开始删,删完全部。又或者说是给出的长度加上pos,这个长度大于_size,也会将所有的字符串删掉

~]M7_FD74%){FTDAOJQ$%H6.png

而当长度小于_size,那么我们就需要往前挪动数据即可。代码如下:

string&erase(size_tpos, size_tlen=npos)
        {
//当不给长度len时,默认是npos,或者需要删除的长度大于_size,那么就是删除从pos开始到结尾if (len==npos||pos+len>_size)
            {
_str[pos] ='\0';
_size=pos;
            }
else            {
size_tbegin=pos+len;
while (begin<=_size)
                {
//往前挪动数据_str[pos] =_str[begin];
pos++;
begin++;
                }
_size-=len;
            }
return*this;
        }

image.gif

⑧operator<<()

要实现string的流输出,我们要明确一下格式,是cout<<s<<endl;所以我们不能将operator<<()函数写在类域中,因为类域中的函数,默认第一个参数是this。但我们又必须拿到string类中的私有变量,那就使用友元吧,但也不一定需要友元,我们可以直接在类域外写。

ostream&operator<<(ostream&out, conststring&s)
    {
for (size_ti=0; i<s.size(); i++)
        {
out<<s[i];
        }
returnout;
    }

image.gif

⑨operator>>()

实现流输入,跟流输出一样,写在类域外。重点在于,我们有几个细节需要考虑。第一个,在输入的时候,可能已经有内容了,是重新输入的,所以在输入之前,先将原本的内容情况。第二个,为了避免不断扩容,我们可以定义一个字符数组,用来存放输入的字符,当字符数组满了,再追加到string对象中,然后重新输入。第三个,由于流输入cin跟scanf一样,遇到空格或换行就会停下来。最后,代码如下:

istream&operator>>(istream&in, string&s)
    {
s.clear();
charbuff[128] = { '\0' };
size_ti=0;
charch=in.get();//一个字符一个字符地拿while (ch!=' '&&ch!='\n')
        {
if (i==127)//127这个位置留给'\0'            {
s+=buff;
i=0;
            }
buff[i++] =ch;
ch=in.get();
        }
if (i>=0)//当i!=127但是又有空格了,那么跳出循环,将当前i这个位置给上'\0'表示字符串结束位置        {
buff[i] ='\0';
s+=buff;//最后追加给s        }
returnin;
    }

image.gif

vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节

vs下string的结构:

string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字

符串的存储空间:

①当字符串长度小于16时,使用内部固定的字符数组来存放

②当字符串长度大于等于16时,从堆上开辟空间

union_Bxty{ 
char_buff[16];
char*_ptr;
size_t_size;//保存字符串长度size_t_capacity;//保存从堆上开辟空间的总容量} _Bx;

image.gif

这种设计是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内

部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高,本质上是以空间换时间。

g++下string结构:

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指

针将来指向一块堆空间,内部包含了如下字段:

空间总大小

字符串有效长度

引用计数

 

struct_Rep_base{
size_type_M_length;
size_type_M_capacity;
_Atomic_word_M_refcount;
};
image.gif

比如有两个string类的对象s1和s2,然后进行s1(s2)这样的拷贝构造时,会默认是浅拷贝,然后在析构的时候,根据引用计数的个数来判断是否释放空间。那什么时候会进行写时拷贝呢?那就是s2要修改数据的时候,就会额外给s2一个空间。这就跟操作系统中的父子进程概念类似!

本篇文章结束~这就是模拟实现string的详细过程,如果有什么不懂的可以下方评论留言~喜欢的朋友可以点个收藏~

相关文章
|
20天前
|
自然语言处理 编译器 Linux
|
5天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
25 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
36 4
|
25天前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
8天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
28 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
68 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0

热门文章

最新文章

推荐镜像

更多
下一篇
无影云桌面