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

转载 2012年03月30日 11:45:29

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

本文转载自http://blog.csdn.net/yah99_wolf/article/details/6381876

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++的“+”操作和C的strcat操作性能都很差,但原因不一样

循环“+=”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倍,传值和返回对象的时间基本相同;

6)size()操作是恒定时间,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++的“+=”操作性能比C的strlen+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;                      //重置目标字符串的结尾指针


C/C++中字符串String及字符操作方法

本文总结C/C++中字符串操作方法,还在学习中,不定期更新。。。 字符串的输入方法 1、单个单词可以直接用std::cin,因为:std::cin读取并忽略开头所有的空白字符(如空格,换行符,制表符)...
  • u013171165
  • u013171165
  • 2014年06月22日 13:33
  • 7182

C字符串和C++字符串的区别

C字符串和C++字符串   在C中,并没有字符串这个数据类型,而是使用字符数组来保存字符串。C字符串实际上就是一个以null('\0')字符结尾的字符数组,null字符表示字符串的结束。需要...
  • tuolaji8
  • tuolaji8
  • 2016年05月10日 14:03
  • 1216

再议数据库军规

军规:必须使用UTF8字符集 和DBA负责人确认后,纠正为“新库默认使用utf8mb4字符集”。 这点感谢网友的提醒,utf8mb4是utf8的超集,emoji表情以及部分不常见汉字在ut...
  • szp686886
  • szp686886
  • 2017年02月17日 16:05
  • 332

C++和C对字符串操作总结

一 c语言的字符串 c语言中没有字符串这个数据类型,用两种方法来表示字符串,第一种是字符数组char s[],第二种是字符指针char *s, 两者有区别,不能任务是一样的,区别如下(不完整,后期再...
  • deyuzhi
  • deyuzhi
  • 2016年06月24日 11:04
  • 1797

AWS CTO对过去十年的经验总结 – 十条军规

作者 Werner Vogels 发布于 2016年3月14日 10 Lessons from 10 Years of Amazon Web Services    AWS(Amazon Web ...
  • u012365585
  • u012365585
  • 2016年04月06日 12:24
  • 5889

c/c++ 中的Split函数(字符串自动分割)

一、 split()方法用于将一个字符串分割成字符串数组。 语法:stringObject.split(separator,howmany) 参数介绍:separator:必需。字符串或正则表达式...
  • Vincentlmeng
  • Vincentlmeng
  • 2017年06月14日 10:33
  • 6217

C/C++库函数strstr和find实现子字符串查找

C/C++库函数strstr和string中find方法实现子字符串查找
  • FX677588
  • FX677588
  • 2017年04月25日 12:54
  • 5495

C++字符串函数与C字符串函数比较

赋值拷贝: #include #include using namespace std; void main(){ string a="hello world!"; string b; /...
  • iamgaowei
  • iamgaowei
  • 2014年08月04日 14:57
  • 2526

使用C风格字符串你必须知道的几点

最近在看C代码写的开源软件putty,因为C代码经验非常少,分析起来很吃力,觉得把C语言的一些特性在复习一下很有必要,这里利用中午的半个小时把C风格的字符串总结一下: 0. 所有人都知...
  • johnnyelf83
  • johnnyelf83
  • 2013年04月28日 12:19
  • 1156

C++与C语言字符串头文件及其对应的操作

1. #include    //不可以定义string s;可以用到strcpy等函数 using   namespace   std; #include    //可以定义string s;...
  • u012782049
  • u012782049
  • 2014年03月22日 22:40
  • 3567
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C/C++字符串使用军规
举报原因:
原因补充:

(最多只允许输入30个字)