7. 字符串操作
7.1 c_str() 和 data()
const char* c_str() const;
该函数返回 string 对象底层字符数组的指针,且通过返回的指针不能修改字符串内容。
实现非常简单,直接返回内部指针即可:
const char* string::c_str() const {
return _str;
}
这里有个关键点需要注意:如果你获取了返回值并存储下来,后续对 string 对象进行了可能导致内存重新分配的操作(如追加、插入),那么之前存储的指针就会失效,变成野指针。这个接口主要是为了兼容那些只接受 C 风格字符串的旧接口。
data() 接口的效果与 c_str() 基本一致,但在某些标准版本中行为略有差异,通常建议优先使用 c_str() 以确保兼容性。
7.2 find()
find() 提供了多种重载形式:
size_t find(const string& str, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
size_t find(const char* s, size_t pos, size_t n) const;
size_t find(char c, size_t pos = 0) const;
虽然重载很多,但核心逻辑一致:从 pos 位置开始向后查找,直到字符串尾部。找到第一个匹配项则返回起始位置索引,否则返回 npos。
我们重点模拟第 2 个和第 4 个重载,其他思路类似:
size_t string::find(char c, size_t pos) const {
assert(pos <= _size); // 防止越界
for (size_t i = pos; i < _size; ++i) {
if (_str[i] == c)
return i;
}
return npos;
}
size_t string::find(const char* s, size_t pos) const {
assert(pos <= _size);
// 利用 C 库函数 strstr 进行子串匹配
char* tmp = strstr(_str + pos, s);
if (tmp == nullptr)
return npos;
return tmp - _str;
}
对于带长度参数 n 的重载,可以先截取前 n 个字符再执行相同逻辑。
此外,还有几个功能相似的函数:
rfind():从后往前查找。find_first_of()/find_last_of():查找包含指定字符集合中的任意字符。find_first_not_of()/find_last_not_of():查找不包含指定字符集合的字符。
这些实现都与 find() 大同小异,读者可以尝试自行实现以加深理解。
8. 迭代器相关
在之前的章节中提到过,可以直接用 typedef char* iterator; 来模拟迭代器。这里做进一步补充,特别是关于 const 迭代器的处理。
8.1 迭代器实现
普通迭代器允许修改数据,但对于 const string 对象,如果返回普通迭代器会导致权限放大。因此需要区分普通迭代器和常量迭代器:
typedef char* iterator;
typedef const char* const_iterator;
注意不要随意修改类型名称。普通迭代器解引用后可修改数据,而 const_iterator 只能读取,不能修改。
8.2 begin()
返回指向首元素的迭代器。需要提供两个版本:
iterator string::begin() {
return _str;
}
const_iterator string::begin() const {
return _str;
}
8.3 end()
返回指向最后一个元素下一个位置的迭代器,即区间右边界。同样提供两个版本:
iterator string::end() {
return _str + _size;
}
const_iterator string::end() const {
return _str + _size;
}
记住迭代器区间是左闭右开的,所以 end() 不指向有效数据。rbegin() 和 rend() 的实现相对复杂,涉及反向迭代器逻辑,此处暂不展开。
9. 运算符重载
9.1 流插入和流提取
流操作符必须重载为全局函数或友元函数。
std::ostream& operator<<(std::ostream& out, const string& s);
std::istream& operator>>(std::istream& in, string& s);
- 流插入 (
<<):将_str的内容写入流中。
ostream& operator<<(ostream& out, const string& s) {
for (size_t i = 0; i < s.size(); ++i) {
out << s[i];
}
return out;
}
- 流提取 (
>>):由于不知道输入长度,不能直接用cin >> _str。策略是逐个读取字符,遇到空格或换行停止。为了性能,可以引入临时缓冲区,避免频繁扩容。
istream& operator>>(istream& in, string& s) {
char ch = '\0';
s.clear(); // 清空原有数据
// 先读一个字符,避免空输入问题
in.get(ch);
char tmp[256];
int times = 0;
while (ch != '\n' && ch != ' ') {
if (times == 255) {
tmp[times] = '\0';
s += tmp;
times = 0;
}
tmp[times++] = ch;
in.get(ch);
}
// 处理剩余字符
if (times != 0) {
tmp[times] = '\0';
s += tmp;
}
return in;
}
9.2 比较运算符
需要实现 <, <=, >, >=, ==, !=。实际上只需实现 == 和 >,其余可复用:
bool operator>(const string& s1, const string& s2) {
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
bool operator==(const string& s1, const string& s2) {
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<(const string& s1, const string& s2) {
return !(s1 == s2 || s1 > s2);
}
bool operator<=(const string& s1, const string& s2) {
return s1 < s2 || s1 == s2;
}
bool operator>=(const string& s1, const string& s2) {
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2) {
return !(s1 == s2);
}
10. string 模拟实现的现代写法
赋值运算符有两种常见写法:
// 写法一:基于交换(Move Semantics 思想)
String& operator=(String s) {
swap(_str, s._str);
return *this;
}
// 写法二:传统深拷贝
string& string::operator=(const string& s) {
string tmp(*this);
clear();
if (s._size > _capacity) {
size_t newcapacity = 2 * _capacity > s._size ? 2 * _capacity : s._size;
reserve(newcapacity);
}
strcpy(_str, s._str);
return *this;
}
从性能角度看,写法一更优,因为它避免了不必要的深拷贝。但在面试快速搭建框架时,写法二逻辑更直观。现代 C++ 开发倾向于复用思想,减少 Bug 风险,除非对性能有极致要求,否则推荐采用类似写法一的优化策略。
本文多处已应用了这种现代写法,例如比较运算符和赋值操作。
11. 补充阅读:写时拷贝
在某些编译器环境(如 g++)中,string 可能采用写时拷贝(Copy-On-Write, CoW)机制。
原理如下:
- 每个
string对象维护一个共享计数器time。 - 发生拷贝时,执行浅拷贝,计数器
time++。 - 析构时
time--,仅当time为 0 时才释放空间。 - 若修改数据前发现
time > 1,则触发深拷贝,当前对象独占新空间。
这种方式在多个对象共享同一块内存且不修改的场景下效率极高。但也存在缺陷,例如多线程环境下计数器的同步开销等问题,实际应用中需权衡利弊。


