C/C++字符串使用军规

简介:

C/C++字符串使用军规 

1. 概述

本文对常见的C++ string使用方式进行了测试,并结合源代码分析,总结出如何高效的使用C++ string对象。

2. 测试情况

2.1. 测试环境

测试环境信息如下:

配置项目

配置信息

备注

CPU

8 * 2

Intel(R) Xeon(R) CPU E5620  主频2.40GHz 物理CPU 2个,逻辑CPU 16

内存

24G

6 * 4G  DDR3 1333 REG

OS

Redhat 5

Linux platform2 2.6.18-164.el5 #1 SMP Tue Aug 18 15:51:48 EDT 2009 x86_64 x86_64 x86_64 GNU/Linux

编译器

gcc 4.1.2

gcc version 4.1.2 20080704 (Red Hat 4.1.2-48)

 

2.2. 测试结果

测试结果如下:

操作(1M)

性能(ms)

C语言函数

C语言性能(ms)

备注

创建空串

13

NA

NA

NA

创建一个“test”串

85

char[]=”test”

5

NA

=”操作

95

strcpy()

16

 

+=”操作

95

strcat()

25

两个字符串长度都是10

+”操作

125

strcat()

循环“+10长度字符串

2852631

strcat()

769268

C++的“+”操作和Cstrcat操作性能都很差,但原因不一样

循环“+=”10长度字符串

43

strlen() +

sprintf()

3877099

C代码如下:

sprintf(pos, "%s", part);

len = strlen(buffer);

pos = buffer + len;

函数参数传引用

40

NA

NA

NA

函数参数传值

100

NA

NA

NA

返回string局部变量

110

NA

NA

NA

size()操作

4

strlen()

40

字符串长度为10

==”操作

43

strcmp()

22

两个长度为10的字符串比较

 

2.3. 数据分析

1)构造“test”串的时间是构造空串的时间的6

2)“=”和“+=”时间相近

3)“+”操作比“+=”操作性能要低30%

4循环“+”操作的性能极低,而循环“+=”好很多

5)传引用和传值的效率相差2.5倍,传值和返回对象的时间基本相同;

6size()操作是恒定时间,strlen()是和字符串长度线性相关的;

3. 源码分析

3.1. string的内存管理

string的内存申请函数实现如下(为了阅读方便,去掉了注释和一些辅助代码,详见gcc源码/libstdc++-v3/include/bits/basic_string.tcc):

template<typename _CharT, typename _Traits, typename _Alloc>

    typename basic_string<_CharT, _Traits, _Alloc>::_Rep*

    basic_string<_CharT, _Traits, _Alloc>::_Rep::

    _S_create(size_type __capacity, size_type __old_capacity,

          const _Alloc& __alloc)

    {

      // _GLIBCXX_RESOLVE_LIB_DEFECTS

      // 83.  String::npos vs. string::max_size()

      if (__capacity > _S_max_size)

    __throw_length_error(__N("basic_string::_S_create"));

 

      const size_type __pagesize = 4096;

      const size_type __malloc_header_size = 4 * sizeof(void*);

 

      //如下代码进行空间大小计算,采用了指数增长的方式,即:如果要求的空间__capacity小于当前空间__old_capacity2倍,则按照当前空间的2倍来申请。

      if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)

    __capacity = 2 * __old_capacity;

 

      // NB: Need an array of char_type[__capacity], plus a terminating

      // null char_type() element, plus enough for the _Rep data structure.

      // Whew. Seemingly so needy, yet so elemental.

      size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);

 

      const size_type __adj_size = __size + __malloc_header_size;

      if (__adj_size > __pagesize && __capacity > __old_capacity)

    {

      const size_type __extra = __pagesize - __adj_size % __pagesize;

      __capacity += __extra / sizeof(_CharT);

      // Never allocate a string bigger than _S_max_size.

      if (__capacity > _S_max_size)

        __capacity = _S_max_size;

      __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);

    }

 

      //此处开始分配空间,第一步使用allocate函数申请空间,第二步使用new (__place)的方式生成一个对象返回。此处分两步的主要原因应该是内存分配和释放是由allocator实现的,string对象只使用内存,所以使用定位new的方式返回对象给string,这样string本身无法delete内存。

      void* __place = _Raw_bytes_alloc(__alloc).allocate(__size);

      _Rep *__p = new (__place) _Rep;

      __p->_M_capacity = __capacity;

      __p->_M_set_sharable();

      return __p;

    }

 

gcc中的allocator实现如下(详见gcc源码/libstdc++-v3/include/ext/new_allocator.h):

      pointer

      allocate(size_type __n, const void* = 0)

      {

    if (__builtin_expect(__n > this->max_size(), false))

      std::__throw_bad_alloc();

      //如下代码使用new函数申请内存

    return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));

      }

 

3.2. 常见操作

 

3.2.1. “=”

代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):

      basic_string&

      operator=(const basic_string& __str)

      { return this->assign(__str); }

 

其中assign实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.tcc):

  template<typename _CharT, typename _Traits, typename _Alloc>

    basic_string<_CharT, _Traits, _Alloc>&

    basic_string<_CharT, _Traits, _Alloc>::

    assign(const basic_string& __str)

    {

      if (_M_rep() != __str._M_rep())

    {

      // XXX MT

      const allocator_type __a = this->get_allocator();

      _CharT* __tmp = __str._M_rep()->_M_grab(__a, __str.get_allocator());

      _M_rep()->_M_dispose(__a);

      _M_data(__tmp);

    }

      return *this;

    }

 

_M_grab函数实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):

    _CharT*

    _M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)

    {

      return (!_M_is_leaked() && __alloc1 == __alloc2)

              ? _M_refcopy() : _M_clone(__alloc1);

       }

 

通过_M_grab函数可以看出,对于同一个_Alloc对象即同一块内存,使用引用记数,否则使用clone进行拷贝。

clone的操作最后调用如下代码(详见gcc源码/libstdc++-v3/include/bits/ char_traits.h):

      static char_type*

      copy(char_type* __s1, const char_type* __s2, size_t __n)

      { return static_cast<char_type*>(memcpy(__s1, __s2, __n)); }

 

 

3.2.2. +

代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h

  template<typename _CharT, typename _Traits, typename _Alloc>

    basic_string<_CharT, _Traits, _Alloc>

    operator+(const basic_string<_CharT, _Traits, _Alloc>& __lhs,

          const basic_string<_CharT, _Traits, _Alloc>& __rhs)

    {

      //第一步:生成一个string对象__str包含左值

      basic_string<_CharT, _Traits, _Alloc> __str(__lhs);

      //第二步:将右值append__str

      __str.append(__rhs);

      //第三步:返回局部变量__str

      return __str;

    }

 

通过以上代码可以看出,“+”操作耗费的性能是很大的:第一步创建一个对象,在函数结束时析构对象,第三步调用拷贝构造函数构造临时对象,然后在赋值结束后析构对象。

 

对于一个连加的表达式,这样的耗费更加可观,例如如下语句:

string str1 = str2 +str3 + str4 +str5;

则以上过程会执行3次,总共6次构造和析构操作,而且随着+次数越来越多,字符串越来越长,构造析构成本更高。测试数据显示连续操作100万次,耗费时间达到了惊人的2852631ms

3.2.3. +=”

代码如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h

      basic_string&

      operator+=(const basic_string& __str)

      { return this->append(__str); }

 

通过以上代码可以看出,“+=”操作的代码很简单,只是简单的append,不需要额外的局部变量和临时变量,因此性能也会高得多。这也是测试数据两者相差巨大的原因。

 

append函数最终调用如下函数完成操作(详见gcc源码/libstdc++-v3/include/bits/ char_traits.h):

      static char_type*

      copy(char_type* __s1, const char_type* __s2, size_t __n)

      { return static_cast<char_type*>(memcpy(__s1, __s2, __n)); }

 

但我们还要继续深入思考以下:为什么“+”操作要这样做呢?我个人认为原因应该是“+”操作支持连加的原因,例如str1 = str2 +str3 + str4 +str5

 

3.2.4.  ==”操作

==“操作最终的实现代码如下:

      static int

      compare(const char_type* __s1, const char_type* __s2, size_t __n)

      { return memcmp(__s1, __s2, __n); }

 

通过代码可以看出,string==”操作最终使用的是memcmp函数实现。

 

3.2.5. size()

size()函数实现如下(详见gcc源码/libstdc++-v3/include/bits/ basic_string.h):

      size_type

      size() const

      { return _M_rep()->_M_length; }

 

通过代码可以看出,对于string对象来说,已经使用了一个成员变量来记录字符串长度,而不需要像C语言的strlen()函数那样采用遍历的方式来求长度,这也是C++的“+=”操作性能比Cstrlen+sprintf或者strcat操作高出几个数量级的原因。

4. 使用指南

从以下几方面来看,大型项目推荐使用C++的字符串:

1) 测试结果来看,除了+操作外,100万次操作的性能基本都在100ms以内;

2) 从源码分析来看,string的操作最终基本上都是调用mem*函数,这和C语言的字符串也是一致的;

3) string对象封装了内存管理,操作方便,使用安全简单,不会像C语言字符串那样容易导致内存问题(溢出、泄露、非法内存);

4) 使用“+= 循环拼接字符串性能优势很明显;

 

但在使用过程中为了尽可能的提高性能,需要遵循以下原则:

l  函数调用时使用传引用,而不要使用传值,不需要改变的参数加上const修饰符

l  使用“+=”操作,而不要使用“+”操作,即使写多个“+=”也无所谓

例如将str1 = str2 +str3 + str4 +str5写成如下语句:

str1 += str2;

str1 += str3;

str1 += str4;

str1 += str5;

 

同样,C语言的字符串处理性能总体上要比C++ string性能高,但同样需要避免C语言的性能缺陷,即:

l  要尽量避免显示或者隐式(strcat)求字符串的长度,特别是对长字符串求长度

例如,测试用例中C语言的循环字符串拼接操作sprintf + strlen并不是唯一的实现方式,参考C++ string的实现,C语言拼接字符串优化的方式如下(测试结果是78ms):

        len = strlen(part);             //计算需要拼接的字符串长度

        memcpy(pos, part, len);   //使用memcpy将字符串拼接到目标字符串末尾

            pos += len;                      //重置目标字符串的结尾指针

 

相关文章
|
2月前
|
搜索推荐 编译器 C语言
【C++核心】特殊的元素集合-数组与字符串详解
这篇文章详细讲解了C++中数组和字符串的基本概念、操作和应用,包括一维数组、二维数组的定义和使用,以及C风格字符串和C++字符串类的对比。
78 4
|
30天前
|
缓存 网络协议 API
C/C++ StringToAddress(字符串转 boost::asio::ip::address)
通过上述步骤和示例代码,你可以轻松地在C++项目中实现从字符串到 `boost::asio::ip::address`的转换,从而充分利用Boost.Asio库进行网络编程。
48 0
|
1月前
|
编译器 C语言 C++
C/C++数字与字符串互相转换
C/C++数字与字符串互相转换
|
2月前
|
C++
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具
|
2月前
|
存储 C++
C++(五)String 字符串类
本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
|
4月前
|
算法 C++
2730. 找到最长的半重复子字符串(c++,滑动窗口)
2730. 找到最长的半重复子字符串(c++,滑动窗口)
|
4月前
|
C++
567. 字符串的排列(c++)滑动窗口
567. 字符串的排列(c++)滑动窗口
|
4月前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
5月前
|
C++ 容器
C++字符串string容器(构造、赋值、拼接、查找、替换、比较、存取、插入、删除、子串)
C++字符串string容器(构造、赋值、拼接、查找、替换、比较、存取、插入、删除、子串)
|
5月前
|
编译器 C++
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
37 1