从sizeof(string)到引用计数的漫游

前言:

说是漫游,其实就是扯,一点一点的扯。

话说之前参加华为的德州扑克比赛,我用C++解析消息的时候碰到一个小问题,就是定长收消息的时候出错,在Linux下调了很久很久,终于发现,sizeof(string)不是string的size,而是string类型的大小。当然,用string.size()就可以轻松解决了,而作品也在昨晚提交了,不过,交的是python的。我的C++程序,送给了两支队伍,让他们去参赛,可惜,白眼狼。

既然有闲暇时间了,那么就要深究一下,sizeof(string)是个什么鬼,我从代码测试开始,查阅到容器的几个属性的不同,再定位到了string的几种实现,终于明白了sizeo(string)f的大小,又对引用计数产生量兴趣。下文将按照这个路线,简单的阐述一下过程与实现。

一、sizeof(string)的两个值

先上测试代码:

#include <string>
#include <iostream>

int main()
{
    std::string first = "abcde";
    std::string second = first;
    std::string c;

    std::cout << "sizeof result of each str " << std::endl;
    std::cout << "first:" << sizeof(first) << std::endl;
    std::cout << "second:" << sizeof(second) << std::endl;
    std::cout << "c:" << sizeof(c) << std::endl;

    return 0;
}

本代码是测试三种不同形式的string的大小,运行结果如下:

从sizeof(string)到引用计数的漫游

全是28,是char*的7倍。此结果是在windows下,vs2013跑的结果。而在Linux下,g++4.6的结果是4。

那么问题来了为什么first,second,c的sizeof都是一样大小,而又为什么在不同编译器下,值不同呢?

首先,回答为什么first,second,c的sizeof都是一样大小。

1、什么是sizeof。

sizeof是一个关键字,返回对象在内存中的大小,例如sizeof(char*) = 4. 那么sizeof(string)返回的当然是string这个变量在内存(栈)中的大小了。不过为什么对于不同string,在同一平台下返回的值都是相同的。

2、string在vs2013中实现的猜测。

首先,string是一种容器,那么它有这容器的函数size()与capacity(),其中,size()是容器对象实际存储的对象个数,而capacity()返回的是容器对象总共可以存储的值。那么,既然sizeof(string)的返回值是char * 的7倍,那么是不是value存在一个动态内存中,而size(),capacity()等在一块呢,加上点什么,然后凑够了7倍的char *?

经过查阅资料发现,有一种string的实现是这样的:string对象指针大小的7倍,包含:一个分配子,默认大小为15的存实际字符的value,一个size(),一个capacity(),总共:4+15+1+4+4 = 28;其中1代表字符串预留的''0',方便c_str()的实现.正好是char*的7倍。示意图如下:

从sizeof(string)到引用计数的漫游

也就是说,当字符串的有效字符少于15时,创建一个新的对象,不会导致动态内存分配,那么当字符串长度大于15呢?则在Value部分挤出一块作为指向动态内存的指针,动态内存中存实际的字符,示意图如下:

从sizeof(string)到引用计数的漫游

通过对有效字符小于15的string调用capacity()发现,确实是15,而大于15的,则为会大于等于实际字符,这个与容器的内存分配策略有关。属于预分配空间的一种。至此,解决了,为什么first,second,c的sizeof返回值在同一平台下一样的问题。

其次,解释为什么g++与vs2013下结果不同。

原因很简单了,string在不同平台下可能存在不同的实现。既然Linux的g++下返回值是4,那么猜测它就一个指针,然后指针指向了具体的结构。经过查资料发现,至少有两种string的实现,在sizeof下都会返回4.

1、第一种可能的实现

这种实现是一个指针指向一个结构,然后这个结构一个地方指向有效字符,示意图如下:

从sizeof(string)到引用计数的漫游

Other是一些与同步相关的数据。创建该实现的对象时,至少会导致两次内存动态分配。

2、第二种可能的实现

该实现比上一种直接一些,示意图如下:

从sizeof(string)到引用计数的漫游

创建该对象时,只会引发一次动态内存分配。

二、“拖延症”与引用计数

细心的读者可能发现在最后两种视线中有个RefCnt。这个是引用计数,那么它是做什么用的呢?

1、写时复制

在后两种实现中,你当用出如下语句时 :

string s = s1;

s 与 s1只是pointer不同,其余的都是共享的,这样可以尽量减少构造函数与析构函数的调用。当其中一个发生变化时,被写入新值时,才会真正的创建一个实实在在的对象。Linux中的fwrite等IO都拖延症的思想,fork也是如此,这样尽量减少内存分配,构造与析构。

2、引用计数

由于多个指针共享同一动态内存,只有当RefCnt = 0时,才会析构内存,这样保证每个共享的字符串都是能正常工作的。Linux中文件描述符就是这样的。

参考:

Effective STL

原文链接: https://www.cnblogs.com/tntboom/p/4550271.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/217140

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 上午9:38
下一篇 2023年2月13日 上午9:39

相关推荐