详解C++的string对象

Last Updated: 2024-01-02 11:09:40 Tuesday

-- TOC --

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

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

std::basic_string<char>

string是类型为char的容器,因此具有C++容器的一些标准名称接口,同时它还具有方便处理字符串的接口。由于string本质上是存放char的容器,因此需要具备容器思维,C++保持了所有容器标准名称接口含义的一致性。比如reserve用来增加容器的capacity,但不改变size。这对于string对象也适用,此时就不能是C语言的思维,不能认为有了空间就可以任意赋值,对于string对象而言,要有了size才能任意赋值。比如:

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

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';

同时,C++的string对象也保持了与C一致的mental picture,访问string对象的任意位置,如使用[]时,得到的是char类型数据,而char类型数据如果参与计算,就是int。

创建string对象

// #include <string>
string s1 { "0123456" };
string s2 { s1 };
string s3;          // empty string
string s4 { 'a' };  // "a"
string s41(1, 'a'); // "a"
string s5(5, 'c');  // "ccccc"
string s6("0123456", 3); // "012",  [0:3]
string s7(s1, 3);        // "3456", [3:]
string s8 { s1+"789" };  // "0123456789"

从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.size() << endl;
    return 0;
}

输出:

01 2

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

加法拼接

检讨一下这一行错误代码:s8 = 'g' + "890" + s1,错误的原因是,这两个加法,谁先谁后是不确定的。C++中有一个术语,叫做evaluation order,说的就是这个事儿。In general, C++ has no clearly specified execution order for operands。

类似的错误还有b = ++a + a,b的值也是不确定的。

to_string,转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;
}

,to_string不支持char类型参数,char类型会被当成int处理。

to_string('a');     // "97"
string s41(1, 'a'); // "a"

string suffix "s"

"s"后缀,是定义在stdlib中的特质string对象的后缀。

#include <iostream>
#include <string>
#include <type_traits>
using namespace std;

int main(void) {
    auto a { "12345" };
    static_assert(is_same_v<decltype(a),const char*>);
    auto b { "abcde"s };
    static_assert(is_same_v<decltype(b),string>);
    cout << a << b << endl;
    return 0;
}

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;
}

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;
}

字符串比较

可直接使用==!=来比较两个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()

#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;
}

输出:

2
9
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;
}

输出:

6
8
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;
}

输出:

0
13
10
5

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接口有好几套不同的参数风格。

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
1123
1112323
111abc2323
111ad323
basic_string::replace: __pos (which is 100) > this->size() (which is 8)
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
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
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 "};
    s.erase(s.find_last_not_of(whitespaces)+1);
    s.erase(0, s.find_first_not_of(whitespaces));
}

当输入的s为空串时,两个find接口都返回string:npos,以上代码依然能够成功执行。string:npos+1 == 0

本文链接:https://cs.pynote.net/sf/c/cpp/202209031/

-- EOF --

-- MORE --