添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

字符串的处理在代码中是高频操作,C语言定义字符串是一块以null结束的内存,相关函数接口很底层,error-prone。C++出现了string对象,可以在一个更高的抽象层面简洁高效地处理字符串。

string对象实际上是一个char类型的类似 vector 的容器,它被定义为:

std::basic_string<char>

string是类型为char的容器,因此具有容器的一些基本接口,同时它还具有方便处理字符串的接口。汇总一下string对象的接口,overview:

// element access
operator[]
front
c_str  // returns a non-modifiable standard C character array version of the string
// iterator
begin
cbegin
rbegin
crbegin
crend
// capacity
empty
length  // the same with size
max_size
reserve
capacity
shrink_to_fit
// operation
clear
insert
erase
push_back
pop_back
append  // 会改变size
operator+=
compare
starts_with
ends_with
contains
replace
substr
resize
resize_and_overwrite
// search
rfind
find_first_of
find_first_not_of
find_last_of
find_last_not_of
// constants
npos  // static, special value. The exact meaning depends on the context
// numeric conversion
stoll
stoul
stoull
stold
to_string

下面是有问题的代码片段:

string a;
a.reserve(4);
a[2] = 'a';

运行时错误:

/builddir/build/BUILD/gcc-12.2.1-20220819/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.h:1221: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::operator[](size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; reference = char&; size_type = long unsigned int]: Assertion '__pos <= size()' failed.
Aborted (core dumped)

什么情况?

reserve虽然预留了足够的空间,但直接在index=2的位置赋值的时候,没有通过__pos <= size()的检查。不是说[]操作符不检查麻.....实际上,[]是对已经存在的对象进行修改,如果对象还不存在,就是个错误,index不能超过size!

string a;
a.resize(4);
a[2] = 'a';

字符串基本处理

下面的测试代码,主要应用了string对象的创建,长度函数和转C字符串函数,以及字符串的任意拼接,位置内容的修改。

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
    string s1{"123456"}; // C++ style, same as s1 = "123456";
    string s2{};
    string s3;
    string s4 = s1;
    // cannot printf c++ string object
    cout << s1 << " " << s1.length() << endl;
    cout << "a" << s2 << "|" << s3 << "b" << endl;
    cout << s2.length() << s3.length() << endl;
    cout << s4 << endl;
    // "cccccc"
    string s5(6, 'c');  // cannot be a string with double quote
    cout << s5 << endl;
    string s6{'a'};
    cout << s6 << endl;
    printf("%s\n", s6.c_str());  // one char string object
    string s7{"abcdefg"};
    for (size_t i{}; i<s7.length(); ++i)
        cout << s7[i] << "-";
    cout << endl;
    for (size_t i{}; i<s7.length(); ++i)
        cout << string{s7[i]} + "-";  // it's working...
    cout << endl;
    for (size_t i{}; i<s7.length(); ++i)
        printf("%c-", s7[i]);  // char!
    printf("\n");
    // return a c string
    printf("%s\n", s7.c_str());
    string s8 = s1 + s7;
    cout << s8 << endl;
    s8 = s1 + s6;
    cout << s8 <<endl;
    s8 = s1 + "777";
    cout << s8 <<endl;
    s8 += s8;
    cout << s8 <<endl;
    s8 = s1 + 'g';
    cout << s8 << endl;
    s8 = s1 + 'g' + "890";
    cout << s8 << endl;
    //s8 = 'g' + "890" + s1;  // wrong, undifined result
    s8 = 'g' + s1 + "uiouio";
    cout << s8 << endl;
    s8 = "12345" "abcde";  // C style
    cout << s8 << endl;
    s8[2] = 'g';  // direct modification
    cout << s8 << endl;
    return 0;

需引用<string>头文件,才能使用string对象。(注意不是<cstring>

length接口返回字符串的长度,size_t类型;(还有个size接口,与length一样)

string对象的输出,需要使用cout,如果要使用printf打印字符串,需要转换,用c_str接口。

字符串的拼接已经与其它动态类型语言相差无几了(比如Python),直接用+号,可以拼char,也可以拼另一个string对象。(如果没有string参与加号拼接,就是非法的,但可以省略加号,这是C语言就支持的拼接语法)

string对象支持indexing,比如s7[i],indexing出来的是char。可以直接对string对象的某个位置进行修改!

相对Python,C++的string对象还是复杂,没办法,要底层,要跟C兼容。Python中没有char,用单字母字符串来代替,的确简化了统一了。而且Python提供的字符串slicing功能,真心方便。

不要再认为string的末尾有个null,string是个复杂对象,隐藏了内部实现细节。

检讨一下这一行错误代码s8 = 'g' + "890" + s1,错误的原因是,这两个加法,谁先谁后是不确定的。C++中有一个术语,叫做evaluation order,说的就是这个事儿。In general, C++ has no clearly specified execution order for operands。类似的错误还有b = ++a + a,b的值也是不确定的。(写成多行,不要有歧义,别作...)

从char数组到string对象

这是一种有风险的创建string对象的方式,有bug的代码如下:

#include <iostream>
#include <string>
using namespace std;
int main(void) {
    char aa[5]{0x30,0x31,0x00,0x32,0x33};
    string s1{aa};
    cout << s1 << " " << s1.length() << endl;
    return 0;

由于char中间有一个0x00,导致创建的string对象长度仅为2。

at,边界检查

对于string对象,可以直接使用[]的方式获取某个位置的char,但这种方式有风险,一旦越界,程序就会崩溃,这种崩溃是try...catch...无法捕获的。此时,可以使用at,它有边界检查,一旦越界,会抛出可捕获的异常。

#include <iostream>
#include <string>
using namespace std;
int main(void) {
    string s{"12345"};
    try {
        cout << s.at(10) << endl;
    } catch (exception& e) {
        cout << e.what() << endl;
    return 0;

to_string,数字转字符串

这是个真方便的接口,将很多常用的类型转换成string对象。

#include <string>
#include <iostream>
using namespace std;
int main(void) {
    cout << to_string(123) << endl;
    cout << to_string(123L) << endl;
    cout << to_string(123UL) << endl;
    cout << to_string(123ULL) << endl;
    cout << to_string(1.23f) << endl;
    return 0;

stoi,stod,stof...字符串转数字

请看示例:

#include <string>
#include <iostream>
using namespace std;
int main(void) {
    string s1{" 123\n\t\v\f\r"};
    string s2{"1.23"};
    int a1 = stoi(s1);
    double a2 = stod(s2);  // double
    float a3 = stof(s2);   // float
    cout << a1 << " " << a2 << " " << a3 << endl;
    return 0;

注意a1,它演示了stoi系列接口,能够自动trim字符串。(这里还有自己写的trim

如果转换越界,这一组接口会抛出std::out_of_range异常。(而原C标准库中的atoi这一组接口,承诺不会抛出异常,但可能会出现转换后overflow的情况)

字符串比较

可直接使用==!=来比较两个string对象所包含的内容是否相同,以及用<>按ASCII顺序比较两个字符串对象的大小(直到遇到不同的字符或末尾)。测试代码如下:

#include <cstdio>
#include <string>
using namespace std;
int main(void) {
    string s1{"12345"};
    string s2{s1};
    string s3{"123a"};
    if (s1 == s2)
        printf("s1 == s2\n");
    if (s1 != s3)
        printf("s1 != s3\n");
    if (s1 < s3)
        printf("s1 < s3\n");
    return 0;

下面三种方式,都可以用来判断是否为空string对象:

#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
int main(void) {
    string s{};
    if (s.empty())
        cout << "s.empty" << endl;
    if (s == "")
        cout << "s == \"\"" << endl;
    if (s.size() == 0)
        cout << "s.size() == 0" << endl;
    return 0;

range-based loop for string

#include <iostream>
#include <string>
using namespace std;
int main(void) {
    string s{"123456789"};
    for (auto c: s)
        cout << c;
    cout << endl;
    return 0;

insert,插入子串

insert接口用于插入一个子串,但不能插入一个char:

#include <iostream>
#include <cstdio>
#include <string>
using




    
 namespace std;
int main(void) {
    string s1{"123456"};
    string s2, s3, s4;
    s2 = s3 = s4 = s1;
    cout << s1 << endl;
    s2.insert(3, "abc");
    cout << s2 << endl;
    s3.insert(0, "qaz");
    cout << s3 << endl;
    try {
        s4.insert(10, "ghj");
        cout << s4 << endl;
    } catch(std::out_of_range& e) {
        cout << "exception out_of_range: " << e.what() << endl;
    string s5 = s1;
    //s5.insert(1, 'a');  // wrong
    s5.insert(1, string{'a'});
    cout << s5 << endl;
    return 0;

insert接口的第1个参数是index,如果index超过了string的长度,throw std::out_of_range。

直接insert一个char是非法的。

erase,删除子串

erase从一个位置开始,删除一个长度的子串,in-place,直接修改string对象。也可以只提供开始index,将后面的全部erase掉。测试代码如下:

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
    string s1{"123456"};
    string s2, s3, s4, s5;
    s2 = s3 = s4 = s5 = s1;
    cout << s1 << endl;
    // 123
    s2.erase(3, 3);
    cout << s2 << endl;
    // 456
    s3.erase(0, 3);
    cout << s3 << endl;
    try {
        s4.erase(10, 2);
        cout << s4 << endl;
    } catch(std::out_of_range& e) {
        cout << "exception out_of_range: " << e.what() << endl;
    // no erase virtually
    s5.erase(1,0);
    cout << s5 << endl;
    try {
        s5.erase(10,0);
        cout << s5 << endl;
    } catch (std::out_of_range& e) {
        cout << "exception out_of_range: " << e.what() << endl;
    // erase to the end
    string s6 = s1;
    s6.erase(1);
    cout << s6 << endl;
    return 0;

开始位置(第1个参数,index)不能越界,但是长度(第2个参数)可以越界。空string对象可以执行s.erase(0),index可以等于size,不能大于。

substr,提取子串(切片)

substr是C++字符串切片操作的接口,它的prototype如下:

basic_string substr( size_type pos = 0, size_type count = npos ) const;

两个参数都有默认值,有const表示它是个accessor,返回对象是新创建的。

可能会抛出异常,std::out_of_range if pos > size()

  • substr(),一个完整的copy;
  • substr(pos),从pos开始,切到最后;
  • substr(pos,count),普通切片。
  • #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
        string s{"123456"};
        cout << s.substr() << endl;
        cout << s.substr(2) << endl;
        cout << s.substr(2,3) << endl;
        try {
            cout << s.substr(9) << endl;
        } catch(std::out_of_range& e) {
            cout << "exception out_of_range: " << e.what() << endl;
        return 0;
    

    find接口查找子串或某个字符,可以指定查找的开始位置index,找到就停下来,返回index,如果没有找到,返回string:npos(这是一个超大的数,保证大于任何有效index,等于(size_t)-1

    #include <iostream>
    #include <cstdio>
    #include <string>
    using namespace std;
    int main(void) {
        string s1{"123456abc34567"};
        cout
    
    
    
    
        
     << s1.find("3") << endl;
        cout << s1.find("34",6) << endl;
        // not found, no throw
        size_t w = s1.find("34", 32);
        cout << string::npos << endl;
        cout << w << endl;
        // not found
        size_t p = s1.find("gg");
        if (p == (size_t)-1)
            cout << "not found" << endl;
        return 0;
    18446744073709551615
    18446744073709551615
    not found
    

    rfind与find不同的地方在于,其第2个参数,表示找到这个index后就停下来,不再继续找下去,应该是restricted find这个英文。停下来的index是要参与比较的,如果与要查到的子串的首字母匹配,会继续匹配下去。

    #include <iostream>
    #include <cstdio>
    #include <string>
    using namespace std;
    int main(void) {
        string s1{"123456abc34567"};
        cout << s1.rfind("3") << endl;
        cout << s1.rfind("34",6) << endl;
        // return 0
        cout << s1.rfind("12345",0) << endl;
        // return 1
        cout << s1.rfind("2345",1) << endl;
        // not found
        cout << s1.rfind("abc",0) << endl;
        // not found, no throw
        cout << s1.rfind("34",32) << endl;
        size_t p = s1.rfind("gg",32);
        if (p == string::npos)
            cout << "not found" << endl;
        return 0;
    

    find_first_of,find_last_of

    find_first_of,首次出现第1个参数中的字符串中任意一个字符的index,第2个参数表示从这个index开始搜索。

    find_last_of,最后出现第1个参数中的字符串中任意一个字符的index,第2个参数表示从末尾逆向移动的位置数,此位置作为开始index。

    没找到返回string:npos

    #include <iostream>
    #include <cstdio>
    #include <string>
    using namespace std;
    int main(void) {
        string s1{"123456abc34567"};
        cout << s1.find_first_of("cba") << endl;
        cout << s1.find_last_of("cba") << endl;
        cout << s1.find_first_of("cba",10) << endl;
        cout << s1.find_last_of("cba",5) << endl;
        return 0;
    18446744073709551615
    18446744073709551615
    

    find_first_not_of,find_last_not_of

    #include <iostream>
    #include <cstdio>
    #include <string>
    using namespace std;
    int main(void) {
        string s1{"123456abc34567"};
        cout << s1.find_first_not_of("cba") << endl;
        cout << s1.find_last_not_of("cba") << endl;
        cout << s1.find_first_not_of("cba",10) << endl;
        cout << s1.find_last_not_of("cba",5) << endl;
        return 0;
    

    empty,判断空字符串

    empty接口用来判断字符串是否为一个空串,是true,不是false:

    #include <cstdio>
    #include <string>
    int main(void) {
        std::string ss = "";
        std::string sy = ".";
        printf("ss: %s\n", ss.empty()?"true":"false");
        printf("sy: %s\n", sy.empty()?"true":"false");
        return 0;
    
    ss: true
    sy: false
    

    clear,清空字符串对象

    clear接口的作用,将字符串对象清空,就是将其变成一个空串。

    #include <cstdio>
    #include <string>
    int main(void) {
        std::string ss = "abcdef";
        std::string sy = ".....";
        ss.clear();
        sy.clear();
        printf("ss: %s\n", ss.empty()?"true":"false");
        printf("sy: %s\n", sy.empty()?"true":"false");
        return 0;
    
    
    
    
        
    
    
    ss: true
    sy: true
    

    replace,替换

    replace接口有好几套不同的参数风格。

  • 第1组replace
  • string& replace(size_t index, size_t len, const string& str);
    string& replace(size_t index, size_t len, const char* str);
    

    从index指定的位置开始,用str替换len这么长的子串。当len==0的时候,是插入的效果。len的值可以大于string对象的长度,但是index必须是合法值,支持传统C字符串,不支持单个字符。下面是测试代码:

    #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
        string ss = "abcdefg";
        string rr = "123";
        ss.replace(0,1,rr);
        cout << ss << endl;
        ss.replace(1,100,rr);
        cout << ss << endl;
        ss.replace(2, 0, rr);
        cout << ss << endl;
        ss.replace(3, 0, "abc");
        cout << ss << endl;
        ss.replace(4, 3, "d");
        cout << ss << endl;
        try {
            ss.replace(100, 0, rr);
        } catch(exception& e) {
            cout << e.what() << endl;
        return 0;
    
    123bcdefg
    1112323
    111abc2323
    111ad323
    basic_string::replace: __pos (which is 100) > this->size() (which is 8)
    
  • 第2组replace
  • string& replace(size_t index, size_t len, const string& str,
                         size_t str_index, size_t str_len);
    string& replace(size_t index, size_t len, const string& str, size_t str_index);
    string& replace(size_t index, size_t len, const char* str,
                         size_t str_index, size_t str_len);
    string& replace(size_t index, size_t len, const char* str, size_t str_len);
    

    与上一组接口相比,这一组replace增加一到两个参数,用于指定用于替换的str的index和len,即只把str的一部分用来进行替换。当使用string对象时,可以只指定index。当使用传统C字符串时,可以指定从指针位置开始的长度(为什么这两者要设计成不一样?)。下面是测试代码:

    #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
        string rr = "123";
            string ss = "abcdefg";
            ss.replace(0,1,rr,0,2);
            cout << ss << endl;
            ss.replace(4,0,rr,1,2);
            cout << ss << endl;
            string ss = "abcdefg";
            ss.replace(0,1,"123",0,2);
            cout << ss << endl;
            ss.replace(4,0,"123",1,2);
            cout << ss << endl;
            string ss = "abcdefg";
            ss.replace(0,1,rr,2);
            cout << ss << endl;
            ss.replace(4,0,"123",2);
            cout << ss << endl;
        return 0;
    
    12bcdefg
    12bc23defg
    12bcdefg
    12bc23defg
    3bcdefg
    3bcd12efg
    
  • 第3组replace
  • string& replace(const_iterator i1, const_iterator i2, const string& str);
    string& replace(const_iterator i1, const_iterator i2, const char* str);
    string& replace(const_iterator i1, const_iterator i2, const char* str, size_t str_len);
    

    这一组是迭代器(迭代器也是一种类型),i1到i2表达了一个范围,对应前面接口的index和len。很奇怪有一些接口在类型为string对象的时候,反而没有实现,是不是很少人会用到?!

    #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
        string rr = "123";
            string ss = "abcdefghijklmn";
            ss.replace(ss.begin()+3, ss.end()-2, rr);
            cout << ss << endl;
            string ss = "abcdefghijklmn";
            ss.replace(ss.begin()+3, ss.end()-2, "123");
            cout << ss << endl;
            string ss = "abcdefghijklmn";
            ss.replace(ss.begin()+3, ss.end()-2, "123", 2);
            cout << ss << endl;
        return 0;
    
    abc123mn
    abc123mn
    abc12mn
    
  • 第4组replace
  • string& replace(const_iterator i1, const_iterator i2, size_t n, char c);
    string& replace(size_t index, size_t len, size_t n, char c);
    

    这一组使用字符char了,当要用char来替换的时候,需要指定重复的次数。

    #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
            string ss = "abcdefghijklmn";
            ss.replace(ss.begin()+3, ss.end()-2, 3, 'x');
            cout << ss << endl;
            string ss = "abcdefghijklmn";
            ss.replace(0, 8, 3, 'x');
            cout << ss << endl;
        return 0;
    
    abcxxxmn
    xxxijklmn
    

    Raw String

    R来表示raw string,必须要在最外层使用一组括号()

    #include <iostream>
    #include <string>
    using namespace std;
    int main(void) {
        string s1{R"(abc\n123\n789)"};
        cout << s1 << endl;
        string s4{"abc\\n123\\n789"}; // same with above
        cout << s4 << endl;
        // special marco NOT_PRINT_FLAG, no use
        string s2{R"NOT_PRINT_FLAG(1234567890)NOT_PRINT_FLAG"};
        cout << s2 << " " << s2.size() << endl;
        // real \n in raw string
        string s3{R"(first
    second
    third)"};
        cout << s3 << endl;
        return 0;
    

    如何做trim

    C++标准string对象不提供trim接口,不知道这是什么考虑?自己造一个吧:

    void trim(string& s) {
        string whitespaces{"\t\n\r\f\v "};