C++17剖析:string在Modern C++中的实现
概述
GCC 8.2提供了两个版本的std::string:一个是基于Copy On Write的,另一个直接字符串拷贝的。前者针对C++11以前的,那时候没有移动构造,一切以效率为先,需要使用COW这种奇技淫巧。后者针对C++11,也就是_GLIBCXX_USE_CXX11_ABI
宏被设置为非零时会被用到,并没有COW的功能,更简单,用户可以在必要时使用移动构造。如果不使用移动构造,字符串的频繁拷贝将会是异常灾难,这一点,在我的项目中已经踩过坑。
本文主要研究Modern C++中的string实现,因为使用C++11以后的标准都使用它。
数据组织:16字节的栈上存储,如果超出16字节则在堆上分配
数据的组织很简单,如果字符串长度不超过15字节(加上后缀0一共16字节),则在栈上保存,否则会在堆上分配内存。
// Use empty-base optimization: http://www.cantrip.org/emptyopt.html
struct _Alloc_hider : allocator_type // TODO check __is_final
{
#if __cplusplus < 201103L
_Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
: allocator_type(__a), _M_p(__dat) { }
#else
_Alloc_hider(pointer __dat, const _Alloc& __a)
: allocator_type(__a), _M_p(__dat) { }
_Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc())
: allocator_type(std::move(__a)), _M_p(__dat) { }
#endif
pointer _M_p; // The actual data.
};
_Alloc_hider _M_dataplus; ////数据指针,指向本地或者堆内存
size_type _M_string_length; ////实际长度,如果大于_S_local_capacity则是堆内存,否则,是栈内存
enum { _S_local_capacity = 15 / sizeof(_CharT) };
////注意这是Union,16字节的栈内存和整个堆的容量(Capacity)共用一块存储
union
{
_CharT _M_local_buf[_S_local_capacity + 1];
size_type _M_allocated_capacity;
};
里面那个union挺有意思,如果_M_string_length>=_S_local_capacity,那么_M_allocated_capacity保存了整块缓冲区的大小(注意不是字符串的大小);如果_M_string_length<_S_local_capacity则_M_dataplus._M_p指针指向_M_local_buf这块栈上内存区。union的特性是全部元素共用一块空间,根据程序的语义只使用其中一个变量,在这里_M_local_buf有效的时候_M_allocated_capacity无效,反之亦然,巧妙地运用空间,减少内存浪费。
对象的构造:第一组,不带参数的构造函数(默认构造函数)
- 始化长度为0,数据指针指向栈内存
_M_local_buf
,包括以下函数:basic_string()
basic_string(const _Alloc& __a)
对象的构造:第二组,根据字符串构造
- 调用
_M_construct()
构造对象,也就是把字符串拷贝一个(而不是增加引用计数);关于_M_construct()
的细节,在下面会分析。包括以下函数: - 2.1 基于basic_string构建字符串拷贝
basic_string(const basic_string& __str)
:拷贝__str的完整字符串;basic_string(const basic_string& __str, const _Alloc& __a)
:同上,指定构造器;basic_string(const basic_string& __str, size_type __pos, const _Alloc& __a = _Alloc())
:拷贝__str从__pos位置到末尾的所有字符;basic_string(const basic_string& __str, size_type __pos, size_type __n)
:拷贝__str从__pos位置开始的N个字符(或者到末尾);basic_string(const basic_string& __str, size_type __pos, size_type __n, const _Alloc& __a)
:同上,指定构造器;
- 2.2 基于指针构建字符串拷贝,指针可以是字符串指针,也可以是其他指针,只要编译器不抱怨
basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
:拷贝__s的全部字符串,构造时会计算__s的长度,使用char_traits<char>::length()
内联函数;basic_string(const _CharT* __s, size_type __n, const _Alloc& __a = _Alloc())
:拷贝__s字符串的前__n个字符,不检测越界问题,留给调用方保证;basic_string(const _Tp& __t, size_type __pos, size_type __n, const _Alloc& __a = _Alloc())
:这个比上面两个构造函数更加宽泛,可以是任意数据指针,编译器会对数据能否正确转换进行检测(warnning或者error);
- 2.3 其他
basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())
:使用初始化列表构造;basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
:以n个相同的字符填充缓冲区:调用_M_construct()
初始化缓冲区并填充字符。
对象的构造:第三组:移动构造函数
- 对于栈上保存的字符串,会拷贝到目标字符串(最多16Bytes);如果是堆上分配的字符串,则把指针转到目标字符串。完成这些拷贝和转移后,右值字符串回归到默认构造的状态(长度为0,字符串指针指向栈缓冲区的起始位置);
basic_string(basic_string&& __str)
basic_string(basic_string&& __str, const _Alloc& __a)
对象的构造:第四组:基于迭代器构造
- 调用
_M_construct()
,根据迭代器的类型进行分类处理:basic_string(_InputIterator __beg, _InputIterator __end, const _Alloc& __a = _Alloc())
:把迭代器区间内的数据(允许不是char/wchar_t,只要编译器不抱怨);
对象的构造:第五组:根据std::string_view构造
- 可以轻量级使用std::string_view构造一个std::string,同样是使用字符串拷贝
basic_string(const _Tp& __t, const _Alloc& __a = _Alloc())
:需要__t是std::string_view类型,取整个std::string_view字符串构建std::string;basic_string(const _Tp& __t, size_type __pos, size_type __n, const _Alloc& __a = _Alloc())
:需要__t是std::string_view类型,并且取它的字符串一部分构建新的字符串,调用上面的构造函数完成构造。basic_string(__sv_wrapper __svw, const _Alloc& __a)
:允许某个类型,如果可以转换成std::string_view,则使用此构造函数,类似于basic_string(sv.data(), sv.size(), alloc)
。
对象的构造:第六组:赋值
- 调用
assign()
函数,进而调用_M_assign()
系列函数,这部分下面会详细分析operator=(const basic_string& __str)
:代码相对比较复杂,其实就是字符串拷贝,没有COW。operator=(const _CharT* __s)
:同上operator=(_CharT __c)
:只插入一个字符
先谈谈构造时的调用流程
上面大部分版本的构造函数都是给定一个起始区间,调用_M_construct(_InIterator __beg, _InIterator __end)
函数。下面分析这个函数的来龙去脉。
_M_construct(_InIterator __beg, _InIterator __end)
:根据std::__is_integer<_InIterator>::__type
分析这个“迭代器”是否数值类型;- 如果是数值类型,则表示调用方是
basic_string(size_type __n, _CharT __c)
这一组的函数,那么就调用_M_construct_aux(_Integer __beg, _Integer __end, std::__true_type)
去生成,最终调用_M_construct(size_type __req, _CharT __c)
函数; - 如果不是数值类型,则表示调用方是上面第二组的调用方式,则调用
_M_construct_aux(_Integer __beg, _Integer __end, std::__true_type)
,继而调用_M_construct(_InIterator __beg, _InIterator __end)
函数;
- 如果是数值类型,则表示调用方是
现在回到_M_construct()函数
_M_construct()函数执行字符串的实际构造操作。它按照迭代器/参数的类型,有两种实现:
_M_construct(size_type __req, _CharT __c)
:实现的算法很简单,申请一段__req的内存空间(如果小于15字节则直接写在栈上),并拷贝__req个__c字符;_M_construct(_InIterator __beg, _InIterator __end, std::forward_iterator_tag)
:只有当迭代器类型是forward_iterator
或者比它更宽的迭代器时使用,它会根据迭代器之间的长度来确定字符串的长度,并逐字符拷贝。关于迭代器的“更宽”,可以参考迭代器类型。_M_construct(_InIterator __beg, _InIterator __end, std::input_iterator_tag)
:用于input_iterator
迭代器,如输入迭代器等。这个构造比较低效,每次输入一个字符,会检测是否达到缓冲区的末尾,如果是,则把缓冲区的大小加1,重新分配空间并拷贝字符串。- 讲完了
字符串赋值专用的_M_assign()函数
- 赋值类的函数都会在预分配足够内存后,调用_M_assign()函数,该函数的逻辑很简单,只是单纯的字符串拷贝,拷贝到需要的位置。
结论:std::string在程序在使用的注意事项(划重点)
- 两个字符串连接:根据std::string的实现代码,字符串的连接一定会引起内存的重新分配和字符串拷贝。很遗憾,C++没有使用C的
realloc()
函数。如果使用realloc()
函数,有可能在扩大内存的同时,不需要拷贝字符串(也就是原来分配的内存区域末尾还有足够的字符串)。C++很单纯地分配一块新内存,再把就内存拷贝过去。这一点在效率上会比较低下。个人认为解决办法是,使用std::deque代替std::string。在字符串不断变动的时候,字符数组可以写入std::deque,因为它的插入、追加操作比std::string高效。等字符串稳定后,再使用basic_string(_InputIterator __beg, _InputIterator __end)
构造函数生成std::string。- google的abseil提供了一个可用的办法:也就是先计算多个字符串的长度,预分配一大块内存来存放,然后分别拷贝字符串,这样避免反复分配内存和拷贝字符串,请参考abseil中的string库。但是这种方法只适用于已知固定个数的字符串连接。
- 有人测试出C++拼接字符串的比较中,std::string::append和operator+的效率是最高的,同样的代码,我用C++98和C++11、C++17分别测试过,O3级别优化,最终结果类似。代码和测试结果见这里。
- 字符串的赋值:C++11下,因为有移动构造,所以std::string的复杂性比之前的版本有所降低,但是如果直接使用赋值,而不使用移动赋值,效率反而较“史前”版本慢,因为C++11以前,使用基于引用计数的COW,拷贝更加轻量级,而C++11出现以后,移动赋值成为提升效率几乎唯一的途径了,因此C++11以后的程序尽可能使用移动赋值。
- 另外,字符串的中间插入,也是需要重新分配内存,并完整拷贝整个字符串,这一点需要在使用中注意。
- 如果只是需要字符串的只读操作,可以使用std::string_view代替。
- 最后说一个地方,虽然比较少用,就是基于
std::input_iterator
的构造,它每次重新分配内存只会分配“刚刚适合需要的大小”,而不会如std::vector那样分配2倍,所以将会有频繁分配内存和拷贝的操作。解决的办法还是用std::deque,等字符足够之后再写入std::string。 - 总结上述几条:任何对已有字符串的插入、追加操作,都很可能造成内存的重新分配和整个字符串拷贝,这种重新分配和拷贝,并不是realloc(),而是new+memcpy,这是必须注意的。解决的办法,一是使用reserver()函数申请足够的内存,二是使用std::deque临时代替。
C++17剖析:string在Modern C++中的实现的更多相关文章
- 计算机程序的思维逻辑 (29) - 剖析String
上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...
- 深入源码剖析String,StringBuilder,StringBuffer
[String,StringBuffer,StringBulider] 深入源码剖析String,StringBuilder,StringBuffer [作者:高瑞林] [博客地址]http://ww ...
- C++17剖析:string_view的实现,以及性能
主要内容 C++17标准发布,string_view是标准新增的内容.这篇文章主要分析string_view的适用范围.注意事项,并分析string_view带来的性能提升,最后从gcc 8.2的li ...
- Java编程的逻辑 (29) - 剖析String
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- C字符串和C++中string的区别 &&&&C++中int型与string型互相转换
在C++中则把字符串封装成了一种数据类型string,可以直接声明变量并进行赋值等字符串操作.以下是C字符串和C++中string的区别: C字符串 string对象(C++) 所需的头文件名称 ...
- WCF技术剖析之八:ClientBase<T>中对ChannelFactory<T>的缓存机制
原文:WCF技术剖析之八:ClientBase<T>中对ChannelFactory<T>的缓存机制 和传统的分布式远程调用一样,WCF的服务调用借助于服务代理(Service ...
- 剖析Asp.Net Web API中HttpController的激活
在Asp.Net Web API中,请求的目标是定义在某个HttpController中的某个Action方法.当请求经过Asp.Net Web API消息处理管道到达管道"龙尾" ...
- [LeetCode] 186. Reverse Words in a String II 翻转字符串中的单词 II
Given an input string, reverse the string word by word. A word is defined as a sequence of non-space ...
- [LeetCode] 557. Reverse Words in a String III 翻转字符串中的单词 III
Given a string, you need to reverse the order of characters in each word within a sentence while sti ...
随机推荐
- Python面试真题第二节
26.字符串a = "not 404 found 张三 99 深圳",每个词中间是空格,用正则过滤掉英文和数字,最终输出"张三 深圳" 27.filter方法求 ...
- 使用jquery日期选择器flatpickr.js,使用js动态创建input元素时插件失效
最近写页面时需要用到,日期选择器,网上搜索了一些插件,最后使用了flatpickr.js.我是从npm 上拉下的依赖 npm install flatpickr --save 随后在页面中引入css ...
- HBase之CF持久化系列(续2)
正如上篇博文所说,在本节我将为大家带来StoreFlusher.finalizeWriter..如果大家没有看过我的上篇博文<HBase之CF持久化系列(续1)>,那我希望大家还是回去看一 ...
- Linux 中改变主机名的 4 种方法
今天,我们将向你展示使用不同的方法来修改主机名.你可以从中选取最适合你的方法. 使用 systemd 的系统自带一个名为 hostnamectl 的好用工具,它可以使我们能够轻易地管理系统的主机名. ...
- 本地连接虚拟机_环境搭建01_VMWARE/XShell/CentOS
今天起准备搭建一套环境用来学习redis,dubbo,docker,nginx. 环境准备: 1.VMware12: https://pan.baidu.com/s/1-LnqfrWw8ZjQdmG ...
- nginx切割日志脚本
nginx切割日志脚本 #!/bin/bash #cut nginx log #2018年9月26日14:26:44 #by jiajiezhao ########################## ...
- 【朝花夕拾】Lint使用篇
工作中Lint工具使用实录及整理 AndroidStudio内置的Lint工具,对app中的代码规范带来了极大的方便.对内存泄漏.代码冗余.代码安全.国际化.代码规范等很多方面都能检测,是 ...
- 一个简单的案例带你入门Dubbo分布式框架
相信有很多小伙伴都知道,dubbo是一个分布式.高性能.透明化的RPC服务框架,提供服务自动注册.自动发现等高效服务治理方案,dubbo的中文文档也是非常全的,中文文档可以参考这里dubbo.io.由 ...
- 【c#】RabbitMQ学习文档(二)Work Queues(工作队列)
今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题. (使用.NET 客户端 进行事例演示) 在第一个教程中,我们编写了一个从命名队列中发送和接收消息的程序. ...
- XSS Stored 测试
dvwa存储型XSS 存储型XSS:会把用户输入的数据“存储”在服务器端,一般出现在需要用户可以输入数据的地方,比如网站的留言板.评论等地方,当网站这些地方过滤不严格的时候,就会被黑客注入恶意攻击代码 ...