---
title: "C++中string的一些思考"
date: 2026-09-04
category: "C++"
tags: ["C++", "STL", "string"]
summary: "引言\\n\\nstring大概是我们最早接触的标准库类型，但正因为它太常用，很多细节反而被忽略了：它其实就是存字符的容器，vector实验里的那一套大小、容量、增删方法在它身上几乎都适用，同时它又有大量为文本量身定做的独有能力。\\n\\n在此篇文章中，我将记录下自己通过一个方法示例实验对string的理解，以供思考，如有不足之处欢迎指正。…"
series:
  name: "C++STL容器及附属"
  order: 0
---

## 引言

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

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

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

## 正文

### 与vector共通的部分

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

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

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

### 初始化的细节

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

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

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

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

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

### 与c风格字符串的边界

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

```cpp
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()的坑：

```cpp
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()还提供反向查找：

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

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

```cpp
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

```cpp

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](https://github.com/ManJin03/Learncpp)，如有不妥之处欢迎指正。
