1. 从零手写C++ string类:底层原理与实现全解析
在C++面试和实际开发中,string类的底层实现一直是考察重点。很多开发者虽然能熟练使用标准库的string,但当被问到"如何自己实现一个简易string类"时却无从下手。本文将带你从零开始,逐步构建一个功能完整的string类,深入理解其底层设计原理。
2. 核心数据结构与基础框架
2.1 string类的三大支柱成员
一个基础的string类需要三个核心成员变量:
char* _str:指向动态分配的字符数组,存储实际字符串内容size_t _size:记录当前字符串的有效长度(不包括结尾的'\0')size_t _capacity:记录当前分配的内存容量
cpp复制class string {
private:
char* _str;
size_t _size;
size_t _capacity;
public:
// 接口声明...
};
这种设计与标准库的常见实现思路一致,既保证了灵活性(动态内存分配),又通过_size和_capacity的维护避免了频繁计算字符串长度。
2.2 基础构造与析构实现
2.2.1 构造函数:从C风格字符串初始化
构造函数需要处理空字符串的默认情况,并正确分配内存:
cpp复制string::string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1]; // 多分配1字节存放'\0'
strcpy(_str, str);
}
关键细节:new分配的是_capacity+1的空间,因为需要额外存储字符串终止符'\0'。strcpy会连同'\0'一起拷贝。
2.2.2 拷贝构造:实现深拷贝
拷贝构造必须实现深拷贝,避免多个对象共享同一块内存:
cpp复制string::string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1); // 包含'\0'
_size = s._size;
_capacity = s._capacity;
}
这里使用memcpy而非strcpy,因为strcpy遇到中间'\0'会提前终止,而memcpy可以完整复制内存块。
2.2.3 赋值运算符:安全的自赋值处理
赋值运算符需要处理自赋值情况,并遵循"申请新内存→拷贝→释放旧内存"的顺序:
cpp复制string& 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;
}
2.2.4 析构函数:资源释放
析构函数负责释放动态分配的内存:
cpp复制string::~string()
{
delete[] _str; // 数组形式删除
_str = nullptr; // 避免野指针
_size = _capacity = 0;
}
3. 容量管理与内存分配
3.1 reserve:预分配内存
reserve用于提前分配足够内存,避免后续操作频繁扩容:
cpp复制void string::reserve(size_t n)
{
if(n > _capacity) {
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1); // 复制原有内容
delete[] _str;
_str = tmp;
_capacity = n;
}
}
性能提示:在知道最终字符串大致长度时,提前reserve可以显著提升性能,避免多次扩容。
3.2 resize:调整字符串长度
resize可以增大或缩小字符串,增大时可指定填充字符:
cpp复制void string::resize(size_t n, char ch = '\0')
{
if(n <= _size) {
// 缩小字符串
_size = n;
_str[_size] = '\0';
} else {
// 扩大字符串
reserve(n); // 可能需要扩容
for(size_t i = _size; i < n; ++i) {
_str[i] = ch; // 填充指定字符
}
_size = n;
_str[_size] = '\0';
}
}
4. 元素访问与迭代器
4.1 下标访问操作符
提供const和非const两个版本的下标访问:
cpp复制char& string::operator[](size_t pos)
{
assert(pos < _size); // 越界检查
return _str[pos];
}
const char& string::operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
4.2 迭代器实现
通过typedef和简单指针封装实现迭代器:
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; }
这使得我们的string类可以支持范围for循环:
cpp复制string s = "hello";
for(char ch : s) {
cout << ch;
}
5. 字符串修改操作
5.1 push_back:尾部添加字符
cpp复制void string::push_back(char ch)
{
if(_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2); // 扩容策略
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
5.2 append:追加字符串
cpp复制void string::append(const char* str)
{
size_t len = strlen(str);
if(_size + len > _capacity) {
reserve(_size + len); // 精确扩容
}
memcpy(_str + _size, str, len + 1); // 包含'\0'
_size += len;
}
5.3 operator+=:便捷拼接
通过重载+=提供更直观的拼接操作:
cpp复制string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
6. 插入与删除操作
6.1 insert:指定位置插入
cpp复制void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if(_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 移动后续字符
for(size_t i = _size; i > pos; --i) {
_str[i] = _str[i-1];
}
_str[pos] = ch;
_size++;
_str[_size] = '\0';
}
6.2 erase:删除字符
cpp复制void string::erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if(len == npos || pos + len >= _size) {
_str[pos] = '\0';
_size = pos;
} else {
memmove(_str + pos, _str + pos + len, _size - pos - len + 1);
_size -= len;
}
}
7. 字符串查找与比较
7.1 find:查找字符或子串
cpp复制size_t string::find(char ch, size_t pos = 0) const
{
for(size_t i = pos; i < _size; ++i) {
if(_str[i] == ch) {
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos = 0) const
{
const char* p = strstr(_str + pos, str);
return p ? p - _str : npos;
}
7.2 compare:字符串比较
cpp复制int string::compare(const string& s) const
{
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
if(ret == 0) {
if(_size < s._size) ret = -1;
else if(_size > s._size) ret = 1;
}
return ret;
}
8. 流操作与友元函数
8.1 输出流操作符重载
cpp复制friend ostream& operator<<(ostream& out, const string& s)
{
for(size_t i = 0; i < s._size; ++i) {
out << s._str[i];
}
return out;
}
8.2 输入流操作符重载
cpp复制friend istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
while(in.get(ch) && !isspace(ch)) {
s.push_back(ch);
}
return in;
}
9. 性能优化与异常安全
9.1 移动语义支持
现代C++应支持移动构造和移动赋值:
cpp复制string(string&& s) noexcept
:_str(s._str), _size(s._size), _capacity(s._capacity)
{
s._str = nullptr;
s._size = s._capacity = 0;
}
string& operator=(string&& s) noexcept
{
if(this != &s) {
delete[] _str;
_str = s._str;
_size = s._size;
_capacity = s._capacity;
s._str = nullptr;
s._size = s._capacity = 0;
}
return *this;
}
9.2 SSO优化考虑
小型字符串优化(SSO)是标准库常见技术,可以在栈上存储短字符串:
cpp复制class string {
private:
static const size_t SSO_SIZE = 15;
union {
struct {
char* _str;
size_t _size;
size_t _capacity;
} _long;
char _short[SSO_SIZE + 1];
};
bool _is_long() const { /*...*/ }
public:
// 接口需要根据存储方式调整...
};
10. 测试与验证
完整的测试应该覆盖所有边界情况:
cpp复制void test_string()
{
// 默认构造
string s1;
assert(s1.size() == 0);
// C字符串构造
string s2 = "hello";
assert(s2.size() == 5);
// 拷贝构造
string s3 = s2;
assert(s3 == "hello");
// 赋值
s1 = s3;
assert(s1 == "hello");
// 修改操作
s1 += " world";
assert(s1 == "hello world");
// 查找
assert(s1.find('w') == 6);
// 插入删除
s1.insert(5, " beautiful");
assert(s1 == "hello beautiful world");
s1.erase(5, 10);
assert(s1 == "hello world");
// 流操作
string s4;
istringstream iss("test");
iss >> s4;
assert(s4 == "test");
}
通过自己实现string类,不仅能深入理解其底层原理,还能掌握C++类设计、内存管理、运算符重载等核心概念。这种实现虽然简化,但涵盖了标准库string的主要设计思想。在实际面试中,能够清晰解释这些实现细节,会显著提升你的竞争力。
