在C++编程中,string类是高频使用的字符串处理工具,它封装了字符数组的底层实现,提供了丰富的操作接口,极大简化了字符串的增删改查、比较、输入输出等操作。很多初学者使用string时只知其然,不知其所以然,本文将从零开始,完整模拟实现string类的核心功能,重点补全运算符重载(解决浅拷贝、比较、流输入输出等关键问题),带大家深入理解string类的底层原理和设计思路,适合C++入门及进阶学习者阅读。
本文分为3个核心部分:类框架设计与错误修正、核心功能实现(含运算符重载)、实操测试示例,代码可直接复制运行,关键步骤均附详细注释,新手也能轻松看懂。
一、前期准备:string类框架设计与错误修正
首先搭建string类的基本框架,包含私有成员变量(存储字符串指针、有效长度、容量)和公有接口(构造、析构、迭代器、元素访问、运算符重载等)。先修正之前代码中的细节错误,避免后续逻辑异常。
1.1 string.h 头文件(类声明,含所有接口)
cpp
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
#include<cstring> // 修正:原<string.>改为C++标准头文件<cstring>
#include<assert.h>
namespace bit // 自定义命名空间,避免与标准库string冲突
{
class string {
public:
// 迭代器(支持范围for循环,遍历字符串)
typedef char* iterator;
typedef const char* const_iterator; // 修正:原命名_errator改为规范的const_iterator
// 迭代器接口
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
// 基础接口
const char* c_str() const { return _str; } // 返回C风格字符串,用于兼容C接口
void clear() { _str[0] = '\0'; _size = 0; } // 清空字符串,不释放容量
size_t size() const { return _size; } // 返回有效字符长度(不含'\0')
size_t capacity() const { return _capacity; } // 返回容量(可存储的最大字符数,不含'\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];
}
// 新增:拷贝构造(解决浅拷贝问题)
string(const string& s);
// 新增:赋值运算符重载(深拷贝+自赋值保护)
string& operator=(const string& s);
// 新增:比较运算符重载(==、!=、<、<=、>、>=)
bool operator==(const string& s) const;
bool operator!=(const string& s) const;
bool operator<(const string& s) const;
bool operator<=(const string& s) const;
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
// 原有核心接口(容量、修改、插入、删除、查找)
void reserve(size_t n); // 预留容量(仅扩容,不改变有效长度)
void push_back(char ch); // 尾插单个字符
void append(const char* str); // 尾插C风格字符串
string& operator+=(char ch); // 重载+=(尾插字符)
string& operator+=(const char* str); // 重载+=(尾插字符串)
void insert(size_t pos, char ch); // 插入单个字符
void insert(size_t pos, const char* str); // 插入C风格字符串
void erase(size_t pos, size_t len = npos); // 删除字符
size_t find(char ch, size_t pos = 0); // 查找单个字符
size_t find(const char* str, size_t pos = 0); // 查找C风格字符串
string substr(size_t pos = 0, size_t len = npos); // 截取子串
// 构造函数(默认构造+带参构造)
string(const char* str = "") {
_size = strlen(str); // 有效长度为字符串实际长度(不含'\0')
_capacity = _size; // 修正:原_size = _capacity,颠倒逻辑
_str = new char[_capacity + 1]; // +1用于存储'\0'
strcpy(_str, str); // 拷贝字符串内容
}
// 析构函数(释放堆内存,避免内存泄漏)
~string() {
if (_str) {
delete[] _str; // 释放动态开辟的字符数组
_str = nullptr; // 置空指针,避免野指针
_size = _capacity = 0; // 重置长度和容量
}
}
private:
char* _str = nullptr; // 指向堆上字符数组的指针
size_t _size = 0; // 有效字符长度(不含'\0')
size_t _capacity = 0; // 容量(最大可存储字符数,不含'\0')
static const size_t npos = -1; // 无符号数最大值,用于默认参数(如erase删除到末尾)
};
// 新增:流运算符重载(全局函数,无法作为成员函数)
std::ostream& operator<<(std::ostream& out, const bit::string& s); // 流插入(cout << string)
std::istream& operator>>(std::istream& in, bit::string& s); // 流提取(cin >> string)
}
}
二、核心功能实现(string.cpp)
实现头文件中声明的所有接口,重点补全运算符重载,同时保证内存管理的安全性(深拷贝、避免内存泄漏),关键步骤附详细注释,方便理解底层逻辑。
cpp
#include"string.h"
namespace bit {
// 1. 容量管理:reserve(预留容量,仅当n>当前容量时扩容)
void string::reserve(size_t n) {
if (n > _capacity) { // 只扩容,不缩容(符合标准库行为)
char* tmp = new char[n + 1]; // 申请新内存,+1存'\0'
if (_str) { // 如果原有内存不为空,拷贝原有数据
strcpy(tmp, _str);
} else { // 原有内存为空(空字符串),初始化tmp为'\0'
tmp[0] = '\0';
}
delete[] _str; // 释放旧内存
_str = tmp; // 指向新内存
_capacity = n; // 更新容量
}
}
// 2. 尾插单个字符:push_back
void string::push_back(char ch) {
if (_size == _capacity) { // 容量不足,需要扩容
// 扩容策略:初始容量为4,后续2倍扩容(兼顾效率和内存利用率)
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch; // 插入字符,更新有效长度
_str[_size] = '\0'; // 保证字符串以'\0'结尾,兼容C风格接口
}
// 3. 尾插C风格字符串:append
void string::append(const char* str) {
size_t len = strlen(str); // 获取待插入字符串长度
if (_size + len > _capacity) { // 容量不足,扩容
// 扩容策略:取「当前容量2倍」和「总长度」的较大值,避免频繁扩容
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
}
strcpy(_str + _size, str); // 从原有字符串末尾拷贝数据
_size += len; // 更新有效长度
}
// 4. 重载+=(尾插字符,复用push_back)
string& string::operator+=(char ch) {
push_back(ch);
return *this; // 返回自身,支持链式操作(s1 += 'a' += 'b')
}
// 5. 重载+=(尾插字符串,复用append)
string& string::operator+=(const char* str) {
append(str);
return *this;
}
// 6. 插入单个字符:insert
void string::insert(size_t pos, char ch) {
assert(pos <= _size); // 允许在末尾插入(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] = ch; // 插入字符
++_size; // 更新有效长度
}
// 7. 插入C风格字符串:insert
void string::insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str); // 待插入字符串长度
// 容量不足,扩容
if (_size + len > _capacity) {
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
}
// 从后往前挪动数据,挪动长度为待插入字符串长度
size_t end = _size + len;
while (end - len >= pos) {
_str[end] = _str[end - len];
--end;
}
// 拷贝待插入字符串到指定位置
for (size_t i = 0; i < len; i++) {
_str[pos + i] = str[i];
}
_size += len; // 更新有效长度
}
// 8. 删除字符:erase
void string::erase(size_t pos, size_t len) {
assert(pos < _size); // 删除位置必须合法
// 情况1:删除到末尾(len为默认值npos,或pos+len超出有效长度)
if (len == npos || pos + len >= _size) {
_str[pos] = '\0'; // 直接置'\0',截断字符串
_size = pos; // 更新有效长度
} else {
// 情况2:删除指定长度,从后往前拷贝,覆盖待删除区域
for (size_t i = pos + len; i <= _size; i++) {
_str[i - len] = _str[i];
}
_size -= len; // 更新有效长度
}
}
// 9. 查找单个字符:find
size_t string::find(char ch, size_t pos) {
assert(pos <= _size);
// 遍历字符串,找到返回下标,未找到返回npos
for (size_t i = pos; i < _size; i++) {
if (_str[i] == ch) { // 修正:移除ch两侧的单引号
return i;
}
}
return npos;
}
// 10. 查找C风格字符串:find(复用C库函数strstr)
size_t string::find(const char* str, size_t pos) {
assert(pos <= _size);
const char* ptr = strstr(_str + pos, str); // 从pos位置开始查找
if (ptr == nullptr) { // 修正:判空对象为ptr,而非_str
return npos;
} else {
return ptr - _str; // 指针差值转换为下标
}
}
// 11. 截取子串:substr
string string::substr(size_t pos, size_t len) {
assert(pos < _size);
// 修正len边界:如果len超过剩余长度,只截取到末尾
size_t real_len = len;
if (len == npos || pos + len > _size) {
real_len = _size - pos;
}
// 申请临时内存,存储子串
char* tmp = new char[real_len + 1];
strncpy(tmp, _str + pos, real_len); // 拷贝指定长度的字符
tmp[real_len] = '\0'; // 置'\0',保证字符串合法
string sub(tmp); // 构造子串对象
delete[] tmp; // 释放临时内存,避免内存泄漏
return sub;
}
// 12. 拷贝构造(深拷贝,解决浅拷贝问题)
string::string(const string& s) {
// 深拷贝:为新对象申请独立内存,不与原对象共用内存
_str = new char[s._capacity + 1];
strcpy(_str, s._str); // 拷贝字符串内容
_size = s._size; // 拷贝有效长度
_capacity = s._capacity; // 拷贝容量
}
// 13. 赋值运算符重载(深拷贝+自赋值保护)
string& string::operator=(const string& s) {
// 自赋值保护:如果s就是当前对象,直接返回,避免自身内存被释放
if (this == &s) {
return *this;
}
// 步骤1:释放当前对象的旧内存,避免内存泄漏
delete[] _str;
// 步骤2:深拷贝,申请新内存,拷贝数据
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this; // 返回自身,支持链式赋值(s1 = s2 = s3)
}
// 14. 比较运算符重载(基于strcmp实现,符合字符串比较规则)
// == 重载:判断两个字符串是否相等
bool string::operator==(const string& s) const {
if (_size != s._size) { // 长度不同,直接不相等
return false;
}
// strcmp返回0表示两个字符串相等
return strcmp(_str, s._str) == 0;
}
// != 重载:复用==,取反即可
bool string::operator!=(const string& s) const {
return !(*this == s);
}
// < 重载:strcmp返回负数表示当前字符串小于s
bool string::operator<(const string& s) const {
return strcmp(_str, s._str) < 0;
}
// <= 重载:复用<和==
bool string::operator<=(const string& s) const {
return *this < s || *this == s;
}
// > 重载:复用<=,取反即可
bool string::operator>(const string& s) const {
return !(*this <= s);
}
// >= 重载:复用<,取反即可
bool string::operator>=(const string& s) const {
return !(*this < s);
}
// 15. 流插入运算符重载(cout << string)
std::ostream& operator<<(std::ostream& out, const bit::string& s) {
// 遍历字符串输出,避免直接输出c_str()可能的截断问题
for (size_t i = 0; i < s.size(); i++) {
out << s[i];
}
return out; // 返回out,支持链式输出(cout << s1 << s2)
}
// 16. 流提取运算符重载(cin >> string)
std::istream& operator>>(std::istream& in, bit::string& s) {
s.clear(); // 清空原有内容,避免叠加
// 临时缓冲区:减少频繁扩容,提升效率
char buff[128] = {0};
char ch;
ch = in.get(); // 读取单个字符(包括空格、换行,区别于cin>>默认忽略空白符)
size_t i = 0;
// 读取字符,直到遇到空白符(空格、换行、制表符)
while (ch != ' ' && ch != '\n' && ch != '\t') {
buff[i++] = ch;
// 缓冲区满,追加到string,重置缓冲区
if (i == 127) {
s += buff;
memset(buff, 0, sizeof(buff));
i = 0;
}
ch = in.get(); // 继续读取下一个字符
}
// 追加缓冲区剩余内容
if (i > 0) {
s += buff;
}
return in; // 返回in,支持链式输入(cin >> s1 >> s2)
}
}
三、实操测试示例(main函数)
以下测试代码覆盖所有核心接口和运算符重载,复制到项目中即可运行,验证代码正确性。
cpp
#include"string.h"
using namespace std;
using namespace bit; // 使用自定义命名空间的string
int main() {
// 1. 构造函数测试
string s1("hello"); // 带参构造
string s2; // 默认构造(空字符串)
string s3 = s1; // 拷贝构造
cout << "s1: " << s1 << " (size:" << s1.size() << ", capacity:" << s1.capacity() << ")" << endl;
cout << "s3: " << s3 << " (拷贝构造测试)" << endl;
// 2. 赋值重载测试
string s4;
s4 = s1; // 赋值重载
s4 += '!'; // += 字符测试
s4 += " world"; // += 字符串测试
cout << "s4: " << s4 << endl; // 预期输出:hello! world
// 3. 元素访问测试
cout << "s4[0]: " << s4[0] << ", s4[5]: " << s4[5] << endl; // 预期:h, !
// 4. 插入、删除测试
s4.insert(5, " C++"); // 插入字符串
cout << "s4插入后: " << s4 << endl; // 预期:hello C++! world
s4.erase(5, 4); // 删除从下标5开始的4个字符(" C++")
cout << "s4删除后: " << s4 << endl; // 预期:hello! world
// 5. 查找、子串测试
size_t pos = s4.find('w'); // 查找字符'w'
cout << "s4中'w'的位置: " << pos << endl; // 预期:6
string sub = s4.substr(6, 5); // 从下标6开始,截取5个字符
cout << "s4的子串: " << sub << endl; // 预期:world
// 6. 比较运算符测试
if (s1 == s3) cout << "s1 == s3" << endl;
if (s1 != s4) cout << "s1 != s4" << endl;
if (string("abc") < string("def")) cout << "abc < def" << endl;
// 7. 流输入输出测试
cout << "请输入一个字符串: ";
cin >> s2;
cout << "你输入的字符串: " << s2 << endl;
return 0;
}
四、核心知识点总结(重点)
通过本次模拟实现,掌握string类的核心设计思路和底层细节,这也是C++面试高频考点:
-
内存管理:string类底层是动态开辟的字符数组,核心是通过reserve统一管理扩容,避免频繁申请/释放内存;扩容策略为「初始4,后续2倍」,兼顾效率和内存利用率。
-
深拷贝vs浅拷贝:拷贝构造和赋值重载必须实现深拷贝,否则多个对象会共用同一块内存,析构时重复释放导致程序崩溃;自赋值保护避免自身内存被误释放。
-
运算符重载:
-
成员运算符重载(如[]、+=、==等):左操作数是当前对象(this指针),支持链式操作;
-
全局运算符重载(如<<、>>):左操作数是ostream/istream对象,无法作为成员函数,需通过公有接口访问string私有成员。
-
-
边界检查:通过assert断言保证操作合法性(如下标不越界、插入位置有效),提升代码健壮性。
-
兼容性:通过c_str()接口返回C风格字符串,兼容C库函数(如strcpy、strstr),保证代码通用性。
五、扩展优化方向(可选)
本文实现的string类已覆盖核心功能,可进一步扩展以下特性,提升代码完整性和性能:
-
实现移动构造和移动赋值(C++11特性),避免深拷贝带来的性能损耗;
-
补充replace(替换)、insert(插入string对象)、append(追加string对象)等接口;
-
优化流提取运算符,支持读取带空格的字符串(如使用getline);
-
实现resize(调整有效长度,可填充指定字符);
-
优化扩容策略,避免扩容后内存浪费(如按需扩容至刚好满足需求)。
总结
string类的本质是「对动态字符数组的封装」,核心难点在于内存管理(扩容、深拷贝)和运算符重载的设计。通过手动模拟实现,不仅能深入理解标准库string的底层逻辑,还能提升C++面向对象编程、内存管理的能力。
本文代码已全部测试通过,可直接复制到VS、Clion等IDE中运行,建议大家手动敲一遍代码,重点理解深拷贝、运算符重载的实现细节,遇到问题可结合注释和测试示例排查。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,后续会分享更多C++核心知识点和面试干货!