1. 从C字符串到string类的进化之路
在C语言中,我们处理字符串主要依赖于字符数组和一系列以str开头的库函数。这种处理方式存在几个明显的痛点:
- 需要手动管理内存,容易造成内存泄漏
- 字符串操作函数安全性不足(如strcpy可能导致缓冲区溢出)
- 缺乏统一的字符串抽象表示
- 功能有限,扩展性差
C++的string类完美解决了这些问题。它本质上是一个封装了字符序列的类模板特化实例,通过面向对象的方式提供了丰富的字符串操作接口。理解string类的实现原理,对于掌握C++标准库设计思想至关重要。
2. string类的底层结构剖析
2.1 核心成员变量
string类的典型实现包含三个核心成员:
cpp复制class string {
private:
char* _str; // 指向动态分配的字符数组
size_t _size; // 当前存储的字符数(不含'\0')
size_t _capacity; // 当前分配的存储容量
};
这种设计与vector类似,但有两个关键区别:
- 总是多分配一个字节存放'\0',保证与C字符串兼容
- _size表示有效字符数,不包括结尾的'\0'
2.2 内存布局示例
对于字符串"Hello":
code复制_str -> ['H']['e']['l']['l']['o']['\0'][未使用]...[未使用]
0 1 2 3 4 5 6 ... _capacity
_size = 5
_capacity ≥ 5(具体值取决于实现)
3. 构造函数实现详解
3.1 默认构造函数
cpp复制string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_size + 1])
{
memcpy(_str, str, _size + 1); // 拷贝包括'\0'
}
关键点:
- 缺省参数为空字符串,确保构造安全
- 使用memcpy而非strcpy,避免对源字符串的多次遍历
- 显式处理'\0'保证字符串完整性
3.2 拷贝构造函数
cpp复制string(const string& s)
: _str(new char[s._capacity + 1])
, _size(s._size)
, _capacity(s._capacity)
{
memcpy(_str, s._str, _size + 1);
}
特别注意:
- 深拷贝是必须的,避免多个对象共享同一内存
- 拷贝容量而非仅_size,提高后续操作效率
- 仍然使用memcpy保证包含中间可能存在的'\0'
4. 关键操作实现解析
4.1 赋值运算符的现代实现
传统实现方式:
cpp复制string& operator=(const string& s) {
if(this != &s) {
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代C++推荐实现(拷贝-交换惯用法):
cpp复制string& operator=(string tmp) {
swap(tmp);
return *this;
}
优势:
- 自动处理自赋值情况
- 利用RAII自动管理临时对象生命周期
- 代码更简洁,异常安全
4.2 高效的swap实现
cpp复制void swap(string& s) noexcept {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
关键点:
- 使用noexcept修饰,便于容器优化
- 直接交换指针而非逐个拷贝元素
- 交换所有状态保证一致性
5. 容量管理策略
5.1 reserve的实现艺术
cpp复制void reserve(size_t n) {
if(n > _capacity) {
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
扩容策略分析:
- VS2019采用1.5倍增长(初始15,之后31,47,...)
- GCC采用2倍增长
- 避免频繁扩容带来的性能损耗
5.2 push_back的完整实现
cpp复制void push_back(char c) {
if(_size == _capacity) {
reserve(_capacity ? _capacity * 2 : 4);
}
_str[_size++] = c;
_str[_size] = '\0';
}
优化点:
- 空对象初始分配4字节(可调整)
- 扩容时采用指数增长策略
- 始终维护'\0'终止符
6. 运算符重载的巧妙实现
6.1 下标运算符
cpp复制char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
设计要点:
- 提供const和非const版本
- 使用assert进行边界检查
- 返回引用支持修改操作
6.2 比较运算符
以operator<为例:
cpp复制bool operator<(const string& s) const {
size_t i = 0;
while(i < _size && i < s._size) {
if(_str[i] != s._str[i]) {
return _str[i] < s._str[i];
}
++i;
}
return _size < s._size;
}
比较规则:
- 逐个字符比较ASCII值
- 较短字符串视为较小
- 其他比较运算符可基于operator<实现
7. 迭代器设计与范围for支持
7.1 迭代器类型定义
cpp复制typedef char* iterator;
typedef const char* const_iterator;
string的迭代器本质就是字符指针,这是其连续内存布局决定的。
7.2 关键迭代器方法
cpp复制iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
这些实现使得string支持:
- 标准迭代器操作
- 基于范围的for循环
- STL算法直接应用
8. 实用成员函数实现
8.1 find的高效实现
cpp复制size_t find(const char* s, size_t pos = 0) const {
assert(pos < _size);
const char* p = strstr(_str + pos, s);
return p ? p - _str : npos;
}
优化技巧:
- 利用标准库strstr函数
- 支持从指定位置开始查找
- 正确处理查找失败情况
8.2 insert的边界处理
cpp复制string& insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
reserve(_size + len);
// 移动现有字符
for(size_t i = _size; i >= pos; --i) {
_str[i + len] = _str[i];
}
// 插入新内容
for(size_t i = 0; i < len; ++i) {
_str[pos + i] = str[i];
}
_size += len;
return *this;
}
注意事项:
- 预留足够空间避免重复分配
- 从后向前移动字符防止覆盖
- 更新_size保证一致性
9. 流操作的重载实现
9.1 输出流重载
cpp复制friend ostream& operator<<(ostream& os, const string& s) {
for(char ch : s) {
os << ch;
}
return os;
}
关键点:
- 使用范围for遍历字符
- 直接输出每个字符而非依赖c_str()
- 处理包含'\0'的特殊字符串
9.2 输入流重载的优化版本
cpp复制friend istream& operator>>(istream& is, string& s) {
s.clear();
char ch = is.get();
// 跳过前导空白
while(isspace(ch)) {
ch = is.get();
}
// 缓冲读取优化
char buffer[128];
size_t i = 0;
while(!isspace(ch) && !is.eof()) {
if(i == sizeof(buffer) - 1) {
buffer[i] = '\0';
s += buffer;
i = 0;
}
buffer[i++] = ch;
ch = is.get();
}
if(i > 0) {
buffer[i] = '\0';
s += buffer;
}
return is;
}
优化策略:
- 缓冲技术减少内存分配
- 批量追加提高性能
- 正确处理各种空白字符
10. 性能优化与异常安全
10.1 写时复制(Copy-On-Write)的考量
虽然现代string实现通常不再使用COW,但理解其思想很重要:
cpp复制class string {
struct SharedData {
size_t refcount;
char data[1]; // 柔性数组
};
SharedData* _shared;
// ...其他成员
};
优缺点:
- 优点:减少不必要的拷贝
- 缺点:多线程安全问题,原子操作开销
10.2 短字符串优化(SSO)
现代实现常采用的优化:
cpp复制class string {
union {
struct {
char* _ptr;
size_t _size;
size_t _capacity;
} _long;
char _short[16]; // 小字符串直接存储
};
bool _is_short;
};
当字符串较短时(如≤15字符),直接存储在对象内部,避免堆分配。
11. 完整实现代码
以下是整合了所有优化后的完整string类实现:
cpp复制#include <iostream>
#include <cstring>
#include <cassert>
#include <algorithm>
namespace my {
class string {
public:
// 类型定义
typedef char* iterator;
typedef const char* const_iterator;
static const size_t npos = -1;
// 构造/析构
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_capacity + 1])
{
memcpy(_str, str, _size + 1);
}
string(const string& other)
: _size(other._size)
, _capacity(other._capacity)
, _str(new char[_capacity + 1])
{
memcpy(_str, other._str, _size + 1);
}
~string() {
delete[] _str;
}
// 赋值操作
string& operator=(string other) {
swap(other);
return *this;
}
// 迭代器
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
// 容量操作
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
bool empty() const { return _size == 0; }
void reserve(size_t new_cap) {
if(new_cap > _capacity) {
char* new_str = new char[new_cap + 1];
memcpy(new_str, _str, _size + 1);
delete[] _str;
_str = new_str;
_capacity = new_cap;
}
}
void resize(size_t new_size, char c = '\0') {
if(new_size > _size) {
reserve(new_size);
memset(_str + _size, c, new_size - _size);
}
_size = new_size;
_str[_size] = '\0';
}
// 元素访问
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
// 修改操作
void push_back(char c) {
if(_size == _capacity) {
reserve(_capacity ? _capacity * 2 : 4);
}
_str[_size++] = c;
_str[_size] = '\0';
}
string& operator+=(char c) {
push_back(c);
return *this;
}
string& operator+=(const char* s) {
append(s);
return *this;
}
void append(const char* s) {
size_t len = strlen(s);
reserve(_size + len);
memcpy(_str + _size, s, len);
_size += len;
_str[_size] = '\0';
}
void clear() {
_size = 0;
_str[_size] = '\0';
}
void swap(string& other) noexcept {
std::swap(_str, other._str);
std::swap(_size, other._size);
std::swap(_capacity, other._capacity);
}
// 字符串操作
const char* c_str() const { return _str; }
size_t find(char c, size_t pos = 0) const {
for(; pos < _size; ++pos) {
if(_str[pos] == c) {
return pos;
}
}
return npos;
}
size_t find(const char* s, size_t pos = 0) const {
const char* p = strstr(_str + pos, s);
return p ? p - _str : npos;
}
string substr(size_t pos = 0, size_t len = npos) const {
if(pos > _size) {
throw std::out_of_range("string::substr");
}
size_t real_len = std::min(len, _size - pos);
string result;
result.reserve(real_len);
memcpy(result._str, _str + pos, real_len);
result._size = real_len;
result._str[result._size] = '\0';
return result;
}
private:
size_t _size;
size_t _capacity;
char* _str;
};
// 非成员函数
inline bool operator==(const string& lhs, const string& rhs) {
return lhs.size() == rhs.size() &&
memcmp(lhs.c_str(), rhs.c_str(), lhs.size()) == 0;
}
inline std::ostream& operator<<(std::ostream& os, const string& s) {
for(char c : s) {
os << c;
}
return os;
}
inline std::istream& operator>>(std::istream& is, string& s) {
s.clear();
char c;
while(is.get(c) && !isspace(c)) {
s += c;
}
return is;
}
} // namespace my
12. 测试用例设计
验证string实现的完整测试套件:
cpp复制void test_constructors() {
my::string s1;
assert(s1.size() == 0);
assert(strcmp(s1.c_str(), "") == 0);
my::string s2("hello");
assert(s2.size() == 5);
assert(strcmp(s2.c_str(), "hello") == 0);
my::string s3(s2);
assert(s3.size() == 5);
assert(strcmp(s3.c_str(), "hello") == 0);
}
void test_assignment() {
my::string s1("hello");
my::string s2;
s2 = s1;
assert(s2.size() == 5);
assert(strcmp(s2.c_str(), "hello") == 0);
s2 = "world";
assert(s2.size() == 5);
assert(strcmp(s2.c_str(), "world") == 0);
}
void test_modifiers() {
my::string s;
s.push_back('h');
s.push_back('i');
assert(s.size() == 2);
assert(strcmp(s.c_str(), "hi") == 0);
s += " there";
assert(s.size() == 8);
assert(strcmp(s.c_str(), "hi there") == 0);
s.clear();
assert(s.empty());
}
void test_operations() {
my::string s("hello world");
assert(s.find('w') == 6);
assert(s.find("wo") == 6);
assert(s.find('x') == my::string::npos);
my::string sub = s.substr(6, 5);
assert(sub.size() == 5);
assert(strcmp(sub.c_str(), "world") == 0);
}
void test_all() {
test_constructors();
test_assignment();
test_modifiers();
test_operations();
std::cout << "All tests passed!\n";
}
13. 实际应用中的性能考量
-
预分配策略:在已知最终大小时,提前reserve可避免多次重分配
cpp复制my::string build_string(const vector<string>& parts) { my::string result; size_t total = 0; for(const auto& part : parts) { total += part.size(); } result.reserve(total); for(const auto& part : parts) { result += part; } return result; } -
移动语义优化:C++11后可添加移动构造和移动赋值
cpp复制string(string&& other) noexcept : _str(other._str) , _size(other._size) , _capacity(other._capacity) { other._str = nullptr; other._size = other._capacity = 0; } -
短字符串优化:对于长度≤15的字符串,可存储在栈上避免堆分配
14. 与现代STL实现的差异
- 异常安全:标准库实现有更强的异常安全保证
- 分配器支持:标准库支持自定义分配器
- SSO实现:不同编译器的SSO策略不同(VS:15字符,GCC:22字符)
- 移动语义:C++11后添加的移动操作
- constexpr支持:C++20后部分操作可在编译期执行
理解这些差异有助于在实际项目中做出合理的设计选择。