引言

string大概是我们最早接触的标准库类型,但正因为它太常用,很多细节反而被忽略了:它其实就是存字符的容器,vector实验里的那一套大小、容量、增删方法在它身上几乎都适用,同时它又有大量为文本量身定做的独有能力。

于是我写了一个string方法的示例实验,按初始化、访问、修改三个部分逐个验证。

在此篇文章中,我将记录下自己对string的理解,以供思考,如有不足之处欢迎指正。

正文

与vector共通的部分

先把共通的一笔带过:string的size()/length()、capacity()/reserve()/shrink_to_fit()、push_back()/pop_back()、front()/back(),以及以迭代器为参数的insert()/erase(),语义与vector完全一致,这里不再展开。

比较运算也是共通的设计思路,只是string额外提供了成员函数版本:

cout << s.compare("my string") << endl;//按字典序比较,返回-1/0/1
cout << (s == "my string") << endl;//运算符重载版
cout << s.ends_with("ing") << endl;//c++20,判断后缀

初始化的细节

string的构造方式很多,有一个必须注意的点:

string s6(4, '6');//直接初始化,得到"6666"
//string s6{4, '6'};//列表初始化会把4解析为字符,行为不同

必须使用直接初始化,列表初始化会以字符形式解析数字。这和vector实验里v1(6)与v2{6}的区别是同一个道理,花括号优先走列表构造。

按位置构造和按迭代器构造也很实用:

string s4{s3, 3};              //从下标3取到结尾
string s5{s3, 3, 3};           //从下标3取3个字符
string s7{s2.begin(), s2.end()};//迭代器范围构造

与c风格字符串的边界

string作为“带管理的字符容器”,与c风格字符串的转换是它独有的领域:

cout << s.c_str() << endl;//返回const char*,以'\0'结尾
cout << s.data() << endl; //c++17后data()也可以返回可变指针
char p[3];
s.copy(p, 2, 0);//只能将自身copy到c风格字符数组中,且不自动加'\0'
p[2] = '\0';    //必须手动补结束符

copy()更加危险,需要自己管理目标数组和结束符,能避免就避免。我认为c_str()与data()才是安全的出接口,copy()只在和c接口打交道时万不得已使用。

数字转字符串

实验里记录了一个to_string()的坑:

string s8{std::to_string(666)};
cout << s8 << endl;//某些情况下和cout的显示不一样

//更推荐下列写法:
std::ostringstream ss;
ss << 7777;
string s9{ss.str()};//走流的格式化,与cout显示一致

to_string()内部不走iostream的格式化逻辑,浮点数等类型的输出可能和cout不一致,用ostringstream则能保证所见即所得。

查找与修改

查找方面,string除了find()还提供反向查找:

auto it = s.find("str");//返回下标,找不到返回string::npos
it = s1.rfind("str");//从后往前找

修改方面,append()是string特有的拼接入口,功能比operator+=更细:

s1.append("!");
s1.append(s, 0, 4);//拼接s的前4个字符
s1.append(6, '6'); //拼接6个'6'
s1.replace(pos, count, s);//替换指定范围的子串

insert()/erase()同样既能按位置也能按迭代器操作,比vector多出了“按子串位置”的版本,毕竟对文本来说,下标往往比迭代器更直观。

string_view


void view(string &s)
{
    cout << "string_view" << endl;
    //string_view创建一个string的视图,拥有和string相同的只读函数,
    //但是无法修改,生命周期绑定至string,可以看作是类似指针的用法。
    std::string_view sv{s};

    using namespace std::string_literals; // access the s suffix
    using namespace std::string_view_literals; // access the sv suffix

    std::cout << "foo\n"; // no suffix is a C-style string literal
    std::cout << "goo\n"s; // s suffix is a std::string literal
    std::cout << "moo\n"sv; // sv suffix is a std::string_view literal

    std::string_view s1{"my string"}; //安全,s1指向常量字符串
    std::string_view s2{"my string"s}; //危险,s2指向一个被转换成string的临时对象,s2在该语句结束后悬空
    //收缩视图,不可逆操作
    sv.remove_prefix(1);
    sv.remove_suffix(1);
    cout << sv << endl;
}

小结

string的定位可以理解为"专门存字符的vector外加一整套文本操作":容器部分与vector共通,文本部分才是它的独有价值。而它的独有特性基本都围绕两个边界展开——与c风格字符串的互转边界,以及与数字格式化的边界,这两处恰好也是最容易出错的地方。

一个看似最简单的类型,把它的方法逐个过一遍之后,反而暴露了不少平时被“用惯了”掩盖的细节。

本文所用的测试代码收录于Learncpp,如有不妥之处欢迎指正。