1. 从零实现一个简易string类
在C++开发中,string是最基础也是最常用的数据结构之一。作为C++标准模板库(STL)的重要组成部分,string类封装了字符串的各种操作,极大简化了开发工作。但你是否想过,如果自己动手实现一个string类,会遇到哪些问题?本文将带你从零开始,实现一个功能完整的简易string类。
这个简易string类将包含以下核心功能:
- 基础构造与析构
- 拷贝控制(拷贝构造和赋值运算符)
- 迭代器支持
- 常用字符串操作(插入、删除、查找等)
- 运算符重载
- 流操作支持
通过这个实现过程,你不仅能深入理解string类的内部工作原理,还能掌握C++类设计的关键技术点。
2. 基础结构设计与实现
2.1 类成员变量设计
我们的简易string类需要三个核心成员变量:
cpp复制private:
char* _str; // 存储字符串内容的指针
size_t _size; // 当前字符串长度(不包括'\0')
size_t _capacity; // 当前分配的存储空间大小
这种设计与标准库的string类似,通过分离_size和_capacity实现高效的内存管理。_capacity总是大于等于_size,预留空间可以减少频繁的内存分配。
2.2 构造函数与析构函数
构造函数需要考虑多种情况:
cpp复制// 全缺省构造函数,默认构造空字符串
string(const char* str = "") : _size(strlen(str)) {
_str = new char[_size + 1]; // 多分配1字节存放'\0'
strcpy(_str, str);
_capacity = _size; // 初始时容量等于大小
}
这里使用了全缺省参数的设计技巧,将无参构造和有参构造合并为一个实现。默认参数设为空字符串"",因为它自带'\0',处理起来最方便。
析构函数需要正确释放内存:
cpp复制~string() {
delete[] _str; // 注意使用delete[]释放数组
_str = nullptr; // 避免野指针
_size = _capacity = 0;
}
注意:new[]和delete[]必须配对使用,否则会导致内存泄漏或未定义行为。
2.3 拷贝控制:拷贝构造与赋值运算符
拷贝构造的传统写法:
cpp复制string(const string& str) {
_size = str._size;
_capacity = str._capacity;
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
现代写法利用swap实现,更加简洁高效:
cpp复制string(const string& str) {
string tmp(str._str); // 先构造临时对象
swap(tmp); // 交换资源
}
赋值运算符也有传统和现代两种写法。现代写法利用"拷贝-交换"惯用法:
cpp复制string& operator=(string str) { // 注意这里是传值,会调用拷贝构造
swap(str); // 交换资源,原str会在函数结束时析构
return *this;
}
这种写法不仅简洁,还天然具备自赋值安全性和异常安全性。
3. 迭代器与元素访问
3.1 简易迭代器实现
为了让我们的string支持范围for循环,需要实现迭代器:
cpp复制typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
这里我们直接使用char*作为迭代器类型,虽然简单但功能完整。注意const版本和非const版本都需要提供。
3.2 下标访问运算符
重载[]运算符提供类似数组的访问方式:
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对象,返回const引用防止修改。
4. 字符串操作实现
4.1 内存管理:reserve函数
reserve用于预分配内存,避免频繁扩容:
cpp复制void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1]; // 多分配1字节给'\0'
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
扩容策略通常采用几何增长(如每次翻倍),这样能保证多次插入的平摊时间复杂度为O(1)。
4.2 插入操作
插入单个字符:
cpp复制void insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
}
这里有个细节需要注意:当pos为0时,如果使用size_t类型的end变量,循环条件不能写成end >= pos,因为size_t是无符号类型,end减到0后再减会变成最大值。我们采用从_size+1开始移动的策略避免了这个问题。
插入字符串:
cpp复制void insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (pos + len > _capacity) {
reserve(pos + len);
}
size_t end = _size + len;
while (end > pos + len - 1) {
_str[end] = _str[end - len];
end--;
}
memcpy(_str + pos, str, len);
_size += len;
}
这里使用memcpy而不是strcpy,因为要插入的字符串可能包含'\0'。
4.3 删除操作
删除指定位置开始的len个字符:
cpp复制void erase(size_t pos, size_t len) {
if (pos + len > _size) {
_str[pos] = '\0';
_size = pos;
} else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
4.4 其他常用操作
尾插字符和字符串:
cpp复制void push_back(char c) { insert(_size, c); }
void append(const char* str) { insert(_size, str); }
string& operator+=(char c) {
push_back(c);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
查找操作:
cpp复制size_t find(char ch, size_t pos = 0) const {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_str[i] == ch) {
return i;
}
}
return npos;
}
size_t find(const char* sub, size_t pos = 0) const {
assert(pos < _size);
const char* p = strstr(_str + pos, sub);
return p ? p - _str : npos;
}
子串操作:
cpp复制string substr(size_t pos, size_t len) {
if (len > _size - pos) {
return string(_str + pos);
} else {
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++) {
sub += _str[pos + i];
}
return sub;
}
}
5. 运算符重载与流操作
5.1 比较运算符
实现字符串的各种比较操作:
cpp复制bool operator<(const string& s) const {
return strcmp(_str, s._str) < 0;
}
bool operator==(const string& s) const {
return strcmp(_str, s._str) == 0;
}
// 其他比较运算符可以基于上面两个实现
bool operator<=(const string& s) const {
return *this < s || *this == s;
}
5.2 流输入输出
输出运算符:
cpp复制std::ostream& operator<<(std::ostream& os, const string& str) {
for (size_t i = 0; i < str.size(); i++) {
os << str[i];
}
return os;
}
输入运算符需要更复杂的处理:
cpp复制std::istream& operator>>(std::istream& is, string& str) {
str.clear();
char ch = is.get();
char buffer[128];
int i = 0;
while (ch != ' ' && ch != '\n') {
buffer[i++] = ch;
if (i == 127) {
buffer[i] = '\0';
str += buffer;
i = 0;
}
ch = is.get();
}
if (i != 0) {
buffer[i] = '\0';
str += buffer;
}
return is;
}
这里使用缓冲区减少频繁的内存分配,提高了性能。
6. 实现中的关键问题与解决方案
6.1 内存管理陷阱
-
浅拷贝问题:默认的拷贝构造和赋值运算符会导致多个对象共享同一块内存,析构时多次delete。必须实现深拷贝。
-
自赋值安全:赋值运算符必须正确处理自我赋值的情况,传统写法需要显式检查,现代写法天然具备这个特性。
-
异常安全:现代写法在发生异常时不会破坏原对象状态,而传统写法可能在new失败时留下部分修改的对象。
6.2 边界条件处理
-
无符号整数回绕:size_t是无符号类型,在循环条件中要特别注意不能出现负数比较。
-
空字符串处理:各种操作都要考虑空字符串(_size=0)的特殊情况。
-
'\0'字符处理:字符串操作要正确处理包含'\0'的情况,不能盲目使用strcpy。
6.3 性能优化点
-
扩容策略:几何增长(如每次翻倍)比线性增长性能更好,平摊时间复杂度为O(1)。
-
缓冲区使用:如流输入中使用缓冲区减少内存分配次数。
-
移动语义:C++11后可以添加移动构造和移动赋值进一步优化性能。
7. 完整代码实现
以下是完整实现的头文件:
cpp复制#ifndef MY_STRING_H
#define MY_STRING_H
#include <cstring>
#include <iostream>
#include <cassert>
namespace MyString {
class string {
public:
// 构造与析构
string(const char* str = "") : _size(strlen(str)) {
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
// 拷贝控制
string(const string& str) : _size(str._size), _capacity(str._capacity) {
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
string& operator=(string str) {
swap(str);
return *this;
}
void swap(string& str) {
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
// 迭代器
typedef char* iterator;
typedef const char* const_iterator;
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 n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char c = '\0') {
if (n > _size) {
if (n > _capacity) {
reserve(n);
}
memset(_str + _size, c, n - _size);
}
_str[n] = '\0';
_size = n;
}
// 元素访问
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 char* c_str() const { return _str; }
// 修改操作
void push_back(char c) { insert(_size, c); }
void append(const char* str) { insert(_size, str); }
string& operator+=(char c) {
push_back(c);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
void insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
}
void insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos + len - 1) {
_str[end] = _str[end - len];
end--;
}
memcpy(_str + pos, str, len);
_size += len;
}
void erase(size_t pos, size_t len = npos) {
if (pos >= _size) return;
if (len == npos || pos + len > _size) {
_str[pos] = '\0';
_size = pos;
} else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
void clear() {
_str[0] = '\0';
_size = 0;
}
// 字符串操作
size_t find(char c, size_t pos = 0) const {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_str[i] == c) {
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const {
assert(pos < _size);
const char* p = strstr(_str + pos, str);
return p ? p - _str : npos;
}
string substr(size_t pos, size_t len = npos) const {
if (pos >= _size) return string();
if (len == npos || pos + len > _size) {
return string(_str + pos);
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++) {
sub += _str[pos + i];
}
return sub;
}
// 比较运算符
bool operator<(const string& rhs) const {
return strcmp(_str, rhs._str) < 0;
}
bool operator==(const string& rhs) const {
return strcmp(_str, rhs._str) == 0;
}
bool operator<=(const string& rhs) const {
return *this < rhs || *this == rhs;
}
bool operator>(const string& rhs) const {
return !(*this <= rhs);
}
bool operator>=(const string& rhs) const {
return !(*this < rhs);
}
bool operator!=(const string& rhs) const {
return !(*this == rhs);
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos = -1;
};
// 流操作
std::ostream& operator<<(std::ostream& os, const string& str) {
for (size_t i = 0; i < str.size(); i++) {
os << str[i];
}
return os;
}
std::istream& operator>>(std::istream& is, string& str) {
str.clear();
char ch = is.get();
char buffer[128];
int i = 0;
while (ch != ' ' && ch != '\n') {
buffer[i++] = ch;
if (i == 127) {
buffer[i] = '\0';
str += buffer;
i = 0;
}
ch = is.get();
}
if (i != 0) {
buffer[i] = '\0';
str += buffer;
}
return is;
}
} // namespace MyString
#endif // MY_STRING_H
这个实现涵盖了string类的大部分核心功能,通过这个练习,你应该对C++的类设计、内存管理和字符串处理有了更深入的理解。在实际项目中,建议直接使用标准库的string,但了解其实现原理对提升编程能力大有裨益。