string 的增删查改:insert/erase/replace,与 find + substr 的组合技
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
string 最后一块:增删查改 。原则只有一条:"增删查改加遍历 "------这七个字就是 string 的全部。但接口看着几十个,真正高频的就那几个,这篇按"该用哪个"来理,顺带把 erase 那个容易写错的下标约定、以及 find + substr 的经典组合讲透。
一、修改类:到底该用哪个
string 的修改接口一大堆:push_back、append、operator+=、insert、erase、replace、assign、swap。按场景选,别纠结:
| 场景 | 用什么 |
|---|---|
| 尾插单个字符 | push_back / += |
| 尾插字符串 | append / += |
| 中间插入 | insert |
| 头插 | insert(0, ...)(要挪数据) |
| 删除 | erase |
| 替换区间 | replace |
选择顺序很简单:尾插用 +=(最直观),中间/头插用 insert,删除一律 erase。
append 的重载很多
append 的重载五花八门:支持字符串、字符、迭代器区间、n 个相同字符......这么多的重载是为了让它在任何手头的数据类型下都能直接用。但日常写:
当真正要尾插一些元素时,一般用
+=operator 函数。
可读性最好,也不需要去翻那个重载表。
头插为什么贵
笔记里的那句话很到位:
在第几个字符插入就要挪动第几个字符------这就是头插的成本。
尾插是追加,不用动已有数据;头插要把整个字符串往后挪一位 ,O(n)。所以高频插入场景应该考虑反向写入或者 list------这也是后面学容器选型时的第一个分水岭。
二、erase:下标本身也算一个
erase 的签名是 erase(pos, len),从 pos 下标开始,往后删 len 个字符。
容易错的是 pos 的语义,记牢这句:
erase 从下标 pos 开始,往后删除 npos 个字符;下标本身也算一个。
也就是:
cpp
s.erase(0, 1); // 删下标 0 的字符(pos=0,删 1 个)
s.erase(5, 1); // 删下标 5 的字符本身
s.erase(0); // len 缺省为 npos → 删到结尾
len 的缺省值是 npos (即 size_t 的最大值,42 亿)------所以就算想删除所有字符,直接 erase() 传个 pos 就够了,它会一路删到底。
replace 的参数排布也是这个套路:replace(pos, len, str),不同之处是它第三个参数必须是字符串,用新内容替换掉 [pos, pos+len) 这一段。
三、find 与 substr:取文件的真实后缀
这部分是 string 最高频的组合技。
find
find一般用于查找字符与字符串 ,只有两个参数:(找哪个字符,从哪里开始找 = 0)。
cpp
size_t pos = s.find('x'); // 从头找字符
size_t pos = s.find("abc", 5); // 从下标 5 开始找子串
if (pos == string::npos) { /* 没找到 */ }
找不到返回 npos。用返回值和 npos 比较来判断是否成功,别拿 -1 去比(npos 是无符号的 42 亿)。
substr
substr 函数:从 string 的下标位置开始,截取拷贝一部分出来返回。
签名 substr(pos, len):从 pos 开始,取 len 个字符。返回的是新构造出来的 string(涉及构造 + 拷贝的开销,传返回值时注意)。
那条通用的区间公式
左闭右开就是个数。
cpp
// subs 里 j - i 就是长度
s.substr(i, j - i); // 取 [i, j) 这一段
j - i 直接就是数组长度,这个公式在任何"左闭右开"区间的地方都能用------迭代器、[begin, end)、数组切片,全都一样。
实战:取一个多后缀文件的真实后缀
有些文件名有多个点,比如 test.tar.gz,想要最后那个 gz:
cpp
string filename = "test.tar.gz";
size_t pos = filename.rfind('.'); // 反向找最后一个点
string suffix = filename.substr(pos); // 从这里截到结尾
cout << suffix; // ".gz"
find 找第一个、rfind 找最后一个;再配 substr(pos)(len 缺省到结尾),一行搞定。
四、一个意外的收获:c_str 与 fopen
笔记里有个小问题挺有意思:为什么 fopen 可以直接用 .c_str() 的返回值?
cpp
string filename = "data.txt";
FILE* fp = fopen(filename.c_str(), "r");
答案就一句:
因为 fopen 函数就是要文件名,也就是字符串;而 string 底层数组固定有
\0符。
c_str() 返回的是兼容 C 的以 \0 结尾的字符指针 ,因为 string 内部始终在末尾维持一个 \0(这就是第一篇说的"capacity 不计算 \0"的那个 \0)。它存在的全部意义,就是让 C++ 的 string 能随时交还给 C 的那一套 API。
顺带:strcmp、printf("%s")、exec 系列这类只要 const char* 的地方,走的都是这条路。
五、其他接口的边角料
- 字符串指针与 string 也能比较 :
"abc" == s、s < "abc"都可以,因为有非成员的运算符重载。有了 string 之后基本不需要再用 strcmp。 getline(cin, s):读一整行,遇到空格不停------这是cin >> s做不到的事。第三个参数还能指定分隔符。swap:两个 string 交换,底层只交换指针,O(1)。比自己写临时变量强得多。
至此 string 全部讲完:增删查改加遍历 七个字概括全部接口。下一篇把 string 这一个月收个尾(string 的底层模仿实现),然后进 vector。