技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 编程语言 --> 不得不留意的STL string重载函数和隐式类型转换

不得不留意的STL string重载函数和隐式类型转换

浏览:1390次  出处信息

在360云引擎技术博客的“深入剖析linux GCC 4.4的STL string”这篇blog的指导下,看了一些STL string的实现代码,并针对我们平时对string的一些常规用法做了一些测试。这里做一下总结,希望能帮助大家更好的理解理解STL string,更高效的使用STL string。

由于本文涉及到性能对比,接下来会有一些测试程序,所以首先看一下我们的测试环境:

$ uname-sr
Linux 2.6.32-220.23.1.tb750.el5.x86_64
$ gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-51)

构造函数 std::string::string

函数声明

string ( );
string ( conststring& str );
string ( conststring& str, size_tpos, size_tn = npos );
string ( constchar* s, size_tn );
string ( constchar* s );
string ( size_tn, charc );
template<classInputIterator> string (InputIterator begin, InputIterator end);

我们对这些构造函数分别做下测试

/* FILE: string_construct.cpp */
 
#include <stdio.h>
#include <string>
#include <sys/time.h>
 
int64_t getCurrentTime()
{
    structtimeval tval;
    gettimeofday(&tval, NULL);
    return(tval.tv_sec * 1000000LL + tval.tv_usec);
}
 
intmain(intargc, char*argv[])
{
    constintloop = 10000;
 
    std::string source;
    source.resize(10240, '1');
 
    int64_t start = 0;
    int64_t end = 0;
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source);
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(const string &)", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source, 0);
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(const string &, size_t)", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source.c_str());
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(const char *)", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source.c_str(), source.size());
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(const char *, size_t)", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source.size(), source.at(0));
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(size_t, char)", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        std::string str(source.begin(), source.end());
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "string(Iterator, Iterator)", loop, (end - start));
 
    return0;
}

在运行这段测试代码前,你可以尝试分析一下这几个函数调用,哪个耗时最长,哪个耗时最短呢?
……

再来看看编译运行的结果:

$ g++ -Wall -O2 -o string_construct string_construct.cpp
$ ./string_construct
call string(const string &)              10000 times: 528 us
call string(const string &, size_t)      10000 times: 9064 us
call string(const char *)                10000 times: 30021 us
call string(const char *, size_t)        10000 times: 9092 us
call string(size_t, char)                10000 times: 5719 us
call string(Iterator, Iterator)          10000 times: 8996 us

这里输出的结果跟你的预期一样么?我想只要仔细想想,应该都能预料到这个结果:

  • string(const string &)

毫无疑问它是最快的,因为string的COW(copy-on-write)特性。至于COW就不在本文讨论范围了,感兴趣的朋友可以参考《深入剖析 linux GCC 4.4 的 STL string

  • string(const string &, size_t)

  • string(const char *, size_t)

  • string(size_t, char)

  • string(Iterator, Iterator)

这几个差别不大,都需要分配一块内存,然后将source里的内容拷贝过去

  • string(const char *)

它耗时非常大,这个我想应该会出乎部分人的意料:平时咱们很多时候就是这么用的啊?!它跟string(const char *, size_t)只是差了一个参数而已!但是要注意,这个参数是前面的字符串的长度!如果没有这个长度,那就不能确定需要分配的内存的大小,只能先计算一遍长度。可以通过stl的源码(/usr/include/c++/4.1.2/bits/basic_string.tcc)来确认下,这里摘取相关部分如下:

template<typename_CharT, typename_Traits, typename_Alloc>
  basic_string<_CharT, _Traits, _Alloc>::
  basic_string(const_CharT* __s, size_type __n, const_Alloc& __a)
  : _M_dataplus(_S_construct(__s, __s + __n, __a), __a)
  { }
 
template<typename_CharT, typename_Traits, typename_Alloc>
  basic_string<_CharT, _Traits, _Alloc>::
  basic_string(const_CharT* __s, const_Alloc& __a)
  : _M_dataplus(_S_construct(__s, __s ? __s + traits_type::length(__s) :
                 __s + npos, __a), __a)
  { }

通过这两个函数的实现代码可以看出,唯一的差别就在于 traits_type::length(__s)。traits_type::length的实现在/usr/include/c++/4.1.2/bits/char_traits.h,说简单一点就是于对string,直接调用strlen,对于wstring,则调用wcslen,这里就不详细介绍了。

小结

  1. 在做string的拷贝时,尽量使用string(const string &)的形式,以享受COW带来的好处

  2. 当需要使用char *构造一个string时,如果你已经知道这个char *表示的字符串长度,记得把这个长度一起传给string的构造函数

  3. 我想不会有人傻到用string str(source.c_str(), source.size())这种方式拷贝一个string吧?当然上面测试代码只是为了便于理解所以采用了这种形式

顺便提一下,std::string::assign跟std::string的构造函数类似,测试结果也基本一样。string& assign(const string &)跟string(const string &)一样也有copy-on-write。

string& assign ( conststring& str );
string& assign ( conststring& str, size_tpos, size_tn );
string& assign ( constchar* s, size_tn );
string& assign ( constchar* s );
string& assign ( size_tn, charc );
template<classInputIterator> string& assign ( InputIterator first, InputIterator last );

比较函数 std::string::compare

函数声明

intcompare ( conststring& str ) const;
intcompare ( constchar* s ) const;
intcompare ( size_tpos1, size_tn1, conststring& str ) const;
intcompare ( size_tpos1, size_tn1, constchar* s) const;
intcompare ( size_tpos1, size_tn1, conststring& str, size_tpos2, size_tn2 ) const;
intcompare ( size_tpos1, size_tn1, constchar* s, size_tn2) const;

对于compare函数,这里同样有一段测试代码,一起来看下:

/* FILE: string_compare.cpp */
 
#include <stdio.h>
#include <string>
#include <sys/time.h>
 
int64_t getCurrentTime()
{
    structtimeval tval;
    gettimeofday(&tval, NULL);
    return(tval.tv_sec * 1000000LL + tval.tv_usec);
}
 
#define FOO "123456789012345678901234567890"                \
    "12345678901234567890123456789012345678901234567890"
 
intcompareWithStaticString(conststd::string &str) __attribute__((noinline));
intcompareWithConstString(conststd::string &str) __attribute__((noinline));
intcompareWithCString(conststd::string &str) __attribute__((noinline));
intcompareUseStrcmp(conststd::string &str) __attribute__((noinline));
 
structTestCase
{
    std::string name;
    int(*func)(conststd::string &);
};
 
TestCase cases[] = {
    {
        "compare with static string",
        compareWithStaticString
    },
    {
        "compare with const string",
        compareWithConstString
    },
    {
        "compare with c-string",
        compareWithCString
    },
    {
        "compare use strcmp",
        compareUseStrcmp
    }
};
 
intmain(intargc, char*argv[])
{
    constintloop = 10000;
 
    int64_t start = 0;
    int64_t end = 0;
 
    conststd::string target(FOO);
 
    for(size_ti = 0; i < (sizeof(cases) / sizeof(TestCase)); ++i)
    {
        start = getCurrentTime();
        for(intj = 0; j < loop; ++j)
        {
            cases[i].func(target);
        }
        end = getCurrentTime();
        printf("%-30s %d times: %ldus\n",
               cases[i].name.c_str(), loop, (end - start));
    }
 
    return0;
}
 
intcompareWithStaticString(conststd::string &str)
{
    staticconststd::string foo(FOO);
    return(str.compare(foo));
}
 
intcompareWithConstString(conststd::string &str)
{
    conststd::string foo(FOO);
    return(str.compare(foo));
}
 
intcompareWithCString(conststd::string &str)
{
    return(str.compare(FOO));
}
 
intcompareUseStrcmp(conststd::string &str)
{
    return(strcmp(str.c_str(), FOO));
}

在这段测试程序里,我们有4个比较测试函数,各个函数都将参数传进来的string跟自已预先定义好的一个字符串做比较。函数声明里加上noinline的attribute是为了防止编译器把compareUseStrcmp这个函数调用优化掉
同样,先尝试分析下运行这个测试程序会有什么样的结果?再来编译运行:

$ g++ -Wall -O2 -o string_compare string_compare.cpp
$ ./string_compare
compare with static string     10000 times: 2399us
compare with const string      10000 times: 4156us
compare with c-string          10000 times: 2625us
compare use strcmp             10000 times: 2388us

先来分析compare with static string和compare with const string这两个case,结果很明显,compareWithConstString这个函数里,每次函数调用都会在栈上构造一个foo临时对象,调用10000次就有10000次的构造和析构,当然很耗时了。所以,我们应该尽量用static const string表示字符串常量,而不是const string
再来对比compare with static string和compare with c-string,它俩为什么还差了近3ms的时间呢?我们去源码里寻找答案:

template<typename_CharT, typename_Traits, typename_Alloc>
  int
  basic_string<_CharT, _Traits, _Alloc>::
  compare(const_CharT* __s) const
  {
    __glibcxx_requires_string(__s);
    constsize_type __size = this->size();
    constsize_type __osize = traits_type::length(__s);
    constsize_type __len = std::min(__size, __osize);
    int__r = traits_type::compare(_M_data(), __s, __len);
    if(!__r)
  __r = __size - __osize;
    return__r;
  }

看到了吗,这里又有个traits_type::length!所以,我们应该尽量用static const string表示字符串常量,而不是const string,也不是const char *。
compare with static string跟compare use strcmp结果差不多,我们就不分析了。

至于std::string的几个比较运算符(operator==,operator!=,operator>,operator<,operator>=,operator<=)都是直接调用的std::string::compare。我们也不多做分析了。

std::string里还有几个类似的函数重载,列举如下,我们在使用的时候都需要注意同样的问题:
std::string::find

size_tfind ( conststring& str, size_tpos = 0 ) const;
size_tfind ( constchar* s, size_tpos, size_tn ) const;
size_tfind ( constchar* s, size_tpos = 0 ) const;
size_tfind ( charc, size_tpos = 0 ) const;

std::string::append

string& append ( conststring& str );
string& append ( conststring& str, size_tpos, size_tn );
string& append ( constchar* s, size_tn );
string& append ( constchar* s );
string& append ( size_tn, charc );
template<classInputIterator> string& append ( InputIterator first, InputIterator last );

std::string::replace

string& replace ( size_tpos1, size_tn1, conststring& str );
string& replace ( iterator i1, iterator i2, conststring& str );
string& replace ( size_tpos1, size_tn1, conststring& str, size_tpos2, size_tn2 );
string& replace ( size_tpos1, size_tn1, constchar* s, size_tn2 );
string& replace ( iterator i1, iterator i2, constchar* s, size_tn2 );
string& replace ( size_tpos1, size_tn1, constchar* s );
string& replace ( iterator i1, iterator i2, constchar* s );
string& replace ( size_tpos1, size_tn1, size_tn2, charc );
string& replace ( iterator i1, iterator i2, size_tn2, charc );
template<classInputIterator> string& replace ( iterator i1, iterator i2, InputIterator j1, InputIterator j2 );

隐式类型转换

前面的测试结果表明,当STLstring有提供std::string作为参数的函数重载时,应该尽量使用std::string作为参数。接下来我们再看另外一个case:

/* FILE: implicit_conversion.cpp */
#include <map>
#include <string>
#include <stdio.h>
#include <sys/time.h>
 
int64_t getCurrentTime()
{
    structtimeval tval;
    gettimeofday(&tval, NULL);
    return(tval.tv_sec * 1000000LL + tval.tv_usec);
}
 
typedefstd::map<std::string, std::string> StringMap;
 
inttestCStringKeyInMap(constStringMap &m)
{
    intfound = 0;
    found += (m.find("key1") != m.end() ? 1 : 0);
    found += (m.find("key4") != m.end() ? 1 : 0);
    returnfound;
}
 
inttestStringKeyInMap(constStringMap &m)
{
    staticconststd::string key1 = "key1";
    staticconststd::string key4 = "key4";
    intfound = 0;
    found += (m.find(key1) != m.end() ? 1 : 0);
    found += (m.find(key4) != m.end() ? 1 : 0);
    returnfound;
}
 
intmain(intargc, char*argv[])
{
    constintloop = 10000;
 
    StringMap m;
    m["key1"] = "value1";
    m["key2"] = "value2";
    m["key3"] = "value3";
 
    int64_t start = 0;
    int64_t end = 0;
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        testCStringKeyInMap(m);
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "map[const char *]", loop, (end - start));
 
    start = getCurrentTime();
    for(inti = 0; i < loop; ++i)
    {
        testStringKeyInMap(m);
    }
    end = getCurrentTime();
    printf("call %-35s %d times: %ld us\n",
           "map[const std::string &]", loop, (end - start));
 
    return0;
}

这里我们主要测的是std::string作为std::map的key时,判断指定的key是否在map里,也就是std::map::find函数。那么我们先来看看std::map的find函数原型:

iterator find (constkey_type& k);
const_iterator find (constkey_type& k) const;

这里find函数的参数是const key_type &k,在我们的测试程序里也就是const std::string &k。也就是说find函数只能接受std::string作为参数,但是在我们的测试程序里将一个字符串常量”key1″传给了find函数。这里就涉及到了隐式类型转换,将”key1″转换成一个临时的std::string对象。显然,这里的隐式类型转换需要构造一个临时的std::string对象,需要消耗时间。那我们看一下测试结果:

$ g++ -O2 -o implicit_conversion implicit_conversion.cpp
$ ./implicit_conversion
call map[const char *]                   10000 times: 4653 us
call map[const std::string &]            10000 times: 2441 us

结果很明显,正如我们分析的:隐式类型转换消耗了不少时间。
所以,在我们的代码里,应该尽量使用static const std::string的形式来表示字符串常量!

几种典型的误用场景

构造函数

<pre>std::string decode(conststd::string &str);
std::string decode(constchar*str)
{
    decode(std::string(str));
}
 
voidtest()
{
    std::string input;
    std::cin >> input;
    std::output = decode(input.c_str());
}

比较函数

std::string command;
std::cin >> command;
                                                      // static const std::string get = "get";
                                                      // static const std::string set = "set";
if(command == "get")                   // if (str == get)
{
  printf("get command\n");
}
elseif(command == "set")             // if (str == set)
{
  printf(set command\n";
}

字符串拼接

std::string keyword, properties;
std::cin >> keyword >> properties;
 
std::string url;
url += "/bin/search?";        // static const std::string prefix = "/bin/search?"; url += prefix;
url += "q=";                       // static const std::string q_key = "q="; url += qkey;
url += keyword;
url += "&";                         // url += '&';
url += "properties=";         // static const std::string properties_key = "properties="; url += properties;
url += properties;
url += "&outfmt=json"       // static const std::string outfmt = "&outfmt=json"; url += outfmt;

隐式类型转换

std::map<std::string, std::string> m;
m["key1"] = "value1";                                   //
                                                                   // static const std::string key1 = "key1";
                                                                   // static const std::string value1 = "value1";
                                                                   // m[key1] = value1;
m["key2"] = "value2";
…
conststd::string &v1 = m["key1"];               // const std::string &v1 = m[key1];
conststd::string &v2 = m["key2"];

这几段代码看着眼熟么?赶紧改掉吧 :)

总结

  1. 尽量用static const string来表示字符串常量

  2. 当一个函数内部需要与参数传进来的字符串做string相关操作,那就将参数类型设为std::string &,而不要用char *

  3. 在通过const char *构造string时,如果已知字符串的长度,将长度一起传给string的构造函数!

建议继续学习:

  1. 关于使用STL的红黑树map还是hashmap的问题    (阅读:7937)
  2. 萃取(traits)编程技术的介绍和应用    (阅读:5028)
  3. 一个简单的stl中string的split函数    (阅读:3263)
  4. STL笔记之二叉查找树    (阅读:3055)
  5. String,StringBuffer,StringBuilder的区别    (阅读:3015)
  6. Java将Object对象转换为String的总结合集    (阅读:2925)
  7. 小趣闻:STL的三个版本    (阅读:2874)
  8. 关于在函数调用时传递string引用的必要性    (阅读:2842)
  9. STL笔记之hashtable    (阅读:2628)
  10. 倒置字符串中的单词    (阅读:2472)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1