1. String类实现概述
在C++开发中,字符串处理是最基础也是最频繁的操作之一。标准库虽然提供了std::string,但自己实现一个String类仍然是理解内存管理、拷贝控制和运算符重载等核心概念的绝佳练习。本文将详细解析一个完整的String类实现,涵盖从基础构造到高级优化的方方面面。
这个String类实现了现代C++的最佳实践:
- 完善的RAII内存管理
- 高效的拷贝与移动语义
- 完整的迭代器支持
- C++20的三向比较运算符
- 异常安全的设计
- 性能优化技巧
通过这个实现,你不仅能掌握字符串类的核心机制,还能学到许多可应用于其他资源管理类的通用技术。下面我们就从最基础的内存管理开始,逐步拆解这个String类的实现细节。
2. 内存管理设计
2.1 核心成员变量
String类的内存管理围绕三个核心成员变量展开:
cpp复制size_t size_; // 字符串长度(不含 '\0')
size_t capacity_; // 分配的总空间(含 '\0')
char *s_; // 永远以 '\0' 结尾
这种设计与std::vector类似,但有一个关键区别:字符串必须以空字符'\0'结尾,这是为了兼容C风格的字符串函数。
2.2 容量管理要点
容量计算规则:
capacity_必须至少为size_ + 1,因为要存储结尾的'\0'- 扩容时使用
std::max(2 * capacity_, required)策略,既避免频繁分配又保证足够空间
典型扩容场景:
cpp复制void push_back(char ch) {
ensure_capacity(size_ + 2); // 为新字符和'\0'预留空间
s_[size_] = ch;
s_[++size_] = '\0';
}
注意:确保容量时总是比当前需求多预留1字节给'\0',这是字符串实现中最容易出错的地方之一。
2.3 内存分配策略
ensure_capacity是内存管理的核心方法:
cpp复制void ensure_capacity(size_t required) {
if (required <= capacity_) return;
size_t new_capacity = std::max(2 * capacity_, required);
char *p = new char[new_capacity];
std::memcpy(p, s_, size_ + 1); // 复制原内容包括'\0'
delete[] s_;
s_ = p;
capacity_ = new_capacity;
}
关键点:
- 指数增长策略减少分配次数
- 总是复制原字符串的完整内容(包括'\0')
- 先分配新内存再释放旧内存,保证异常安全
3. 构造与赋值操作
3.1 构造函数族
String类提供了一组丰富的构造函数:
cpp复制// 默认构造:空字符串
String() : size_(0), capacity_(1), s_(new char[1]{'\0'}) {}
// C风格字符串构造
String(const char *str)
: size_(std::strlen(str)), capacity_(size_ + 1) {
s_ = new char[capacity_];
std::memcpy(s_, str, size_ + 1);
}
// 子串构造
String(const char *str, size_t len)
: size_(len), capacity_(len + 1) {
s_ = new char[capacity_];
std::memcpy(s_, str, len);
s_[size_] = '\0'; // 手动添加结尾
}
// 重复字符构造
String(size_t count, char ch)
: size_(count), capacity_(count + 1) {
s_ = new char[capacity_];
std::fill_n(s_, count, ch);
s_[size_] = '\0';
}
// 单字符构造(explicit避免意外转换)
explicit String(char ch)
: size_(1), capacity_(2), s_(new char[2]{ch, '\0'}) {}
3.2 拷贝控制:拷贝与移动
现代C++中,拷贝控制是类设计的核心。String类实现了完整的拷贝构造函数、移动构造函数和拷贝赋值运算符。
拷贝构造:
cpp复制String(const String &other)
: size_(other.size_), capacity_(other.capacity_) {
s_ = new char[capacity_];
std::memcpy(s_, other.s_, size_ + 1);
}
移动构造:
cpp复制String(String &&other) noexcept
: size_(other.size_), capacity_(other.capacity_), s_(other.s_) {
other.s_ = nullptr; // 关键!防止双重释放
other.size_ = 0;
other.capacity_ = 0;
}
移动后必须使源对象处于有效但可析构状态,这是移动语义的基本要求。
3.3 拷贝赋值与copy-and-swap惯用法
String类使用copy-and-swap技术实现了异常安全的赋值操作:
cpp复制String &operator=(String other) noexcept { // 按值传参!
swap(other);
return *this;
}
void swap(String &other) noexcept {
std::swap(size_, other.size_);
std::swap(capacity_, other.capacity_);
std::swap(s_, other.s_);
}
这种实现有几个优点:
- 自动处理自赋值情况
- 同时支持拷贝赋值和移动赋值
- 异常安全(所有可能抛异常的操作发生在参数构造阶段)
4. 迭代器实现
4.1 迭代器模板设计
String类通过模板统一实现了普通迭代器和const迭代器:
cpp复制template <typename CharT>
class IteratorBase {
public:
using iterator_category = std::random_access_iterator_tag;
using value_type = char;
// 其他必要的typedef...
private:
CharT *ptr_;
public:
// 迭代器操作...
operator IteratorBase<const char>() const { // 转换操作
return IteratorBase<const char>(ptr_);
}
};
4.2 迭代器类型定义
基于模板实例化各种迭代器类型:
cpp复制using Iterator = IteratorBase<char>;
using ConstIterator = IteratorBase<const char>;
using ReverseIterator = std::reverse_iterator<Iterator>;
using ConstReverseIterator = std::reverse_iterator<ConstIterator>;
4.3 迭代器获取方法
提供完整的迭代器接口:
cpp复制Iterator begin() noexcept { return Iterator(s_); }
Iterator end() noexcept { return Iterator(s_ + size_); }
ConstIterator begin() const noexcept { return ConstIterator(s_); }
// 其他begin/end变体...
这种设计使得String类可以无缝配合标准算法使用,如:
cpp复制String str = "hello";
std::sort(str.begin(), str.end());
5. 元素访问与修改
5.1 下标访问操作
提供const和非const版本的下标运算符:
cpp复制char &operator[](size_t idx) { return s_[idx]; }
const char &operator[](size_t idx) const { return s_[idx]; }
5.2 边界检查访问
对于需要边界检查的访问,提供at()方法:
cpp复制char &at(size_t idx) {
if (idx >= size_)
throw std::out_of_range("String::at: index out of range");
return s_[idx];
}
5.3 字符串操作接口
提供完整的字符串操作接口:
cpp复制// 获取C风格字符串
const char *c_str() const noexcept { return s_; }
// 获取原始数据指针
const char *data() const noexcept { return s_; }
char *data() noexcept { return s_; }
// 首尾字符访问
char &front() { return s_[0]; }
char &back() { return s_[size_ - 1]; }
6. 字符串修改操作
6.1 基本修改操作
cpp复制void clear() noexcept {
size_ = 0;
s_[0] = '\0';
}
void push_back(char ch) {
ensure_capacity(size_ + 2);
s_[size_] = ch;
s_[++size_] = '\0';
}
void pop_back() noexcept {
assert(size_ > 0);
s_[--size_] = '\0';
}
6.2 插入与删除
cpp复制String &insert(size_t pos, const String &other) {
if (pos > size_) throw std::out_of_range(...);
ensure_capacity(size_ + other.size_ + 1);
std::memmove(s_ + pos + other.size_, s_ + pos, size_ - pos + 1);
std::memcpy(s_ + pos, other.s_, other.size_);
size_ += other.size_;
return *this;
}
String &erase(size_t pos = 0, size_t count = npos) {
if (pos > size_) throw std::out_of_range(...);
count = std::min(count, size_ - pos);
std::memmove(s_ + pos, s_ + pos + count, size_ - pos - count + 1);
size_ -= count;
return *this;
}
注意:插入和删除操作中必须使用memmove而非memcpy,因为源和目标区域可能重叠。
7. 字符串查找与比较
7.1 查找操作
实现各种查找方法:
cpp复制size_t find(char ch, size_t pos = 0) const noexcept {
for (size_t i = pos; i < size_; ++i)
if (s_[i] == ch) return i;
return npos;
}
size_t find(const String &other, size_t pos = 0) const noexcept {
if (other.size_ == 0) return pos <= size_ ? pos : npos;
// 使用memcmp进行快速比较
// ...
}
7.2 C++20三向比较
利用C++20的<=>运算符简化比较操作:
cpp复制std::strong_ordering operator<=>(const String &other) const noexcept {
return compare(other) <=> 0;
}
bool operator==(const String &other) const noexcept {
return size_ == other.size_ && compare(other) == 0;
}
这种实现让编译器自动生成所有比较运算符,大大减少了样板代码。
8. 字符串连接优化
8.1 基本连接操作
cpp复制String operator+(const String &lhs, const String &rhs) {
String result;
result.reserve(lhs.size_ + rhs.size_ + 1);
result.append(lhs);
result.append(rhs);
return result;
}
8.2 移动优化连接
对于右值操作数,可以优化避免不必要的拷贝:
cpp复制String operator+(String &&lhs, const String &rhs) {
lhs.append(rhs);
return std::move(lhs); // 直接复用lhs的内存
}
这种优化可以显著提升字符串连接的效率,特别是在链式连接时。
9. 流操作与辅助功能
9.1 输入输出流支持
cpp复制friend std::ostream &operator<<(std::ostream &os, const String &str) {
return os << str.s_;
}
friend std::istream &operator>>(std::istream &is, String &str) {
str.clear();
is >> std::ws;
char ch;
while (is.get(ch) && !std::isspace(static_cast<unsigned char>(ch)))
str.push_back(ch);
return is;
}
9.2 getline实现
cpp复制inline std::istream &getline(std::istream &is, String &str, char delim = '\n') {
str.clear();
char ch;
while (is.get(ch) && ch != delim)
str.push_back(ch);
return is;
}
9.3 哈希支持
为支持unordered容器,提供特化的hash:
cpp复制namespace std {
template <> struct hash<String> {
size_t operator()(const String &s) const noexcept {
size_t h = 0;
for (size_t i = 0; i < s.size(); ++i)
h = h * 31 + static_cast<unsigned char>(s[i]);
return h;
}
};
} // namespace std
10. 实现中的关键技巧与陷阱
10.1 空字符串处理
空字符串必须分配至少1字节存储'\0':
cpp复制String() : size_(0), capacity_(1), s_(new char[1]{'\0'}) {}
绝不能将s_设为nullptr,否则c_str()将无法正常工作。
10.2 异常安全保证
所有可能抛异常的操作(如内存分配)应该:
- 要么在对象构造完成前完成
- 要么保证操作失败时对象状态不变
例如,赋值操作中的内存分配发生在参数构造阶段,不影响原对象。
10.3 noexcept的正确使用
正确标记noexcept对性能很重要:
cpp复制~String() noexcept { delete[] s_; }
void swap(String &other) noexcept;
size_t size() const noexcept;
但可能抛异常的函数不要标记:
cpp复制void reserve(size_t n); // new可能失败
char &at(size_t idx); // 可能抛out_of_range
10.4 自赋值安全
copy-and-swap惯用法自动处理自赋值:
cpp复制String s = "hello";
s = s; // 安全
10.5 memcpy与memmove的选择
- 区域不重叠时用memcpy(更快)
- 区域可能重叠时用memmove(安全)
cpp复制std::memcpy(s_, other.s_, size_ + 1); // 拷贝构造
std::memmove(s_ + pos + len, s_ + pos, ...); // insert/erase
实现一个完整的String类是掌握C++核心概念的绝佳练习。通过这个实现,我们不仅学习了字符串处理的具体技术,还深入理解了现代C++的资源管理、异常安全和性能优化等通用技术。这些经验可以直接应用于其他资源管理类的设计。