1 实现一个简单的string
首先,我们实现一个最简单的 string 类,它只包含一个指向动态字符数组的指针 _str
。
1.1 简单 string 类框架
namespace practice_string {
class string {
public:
// 构造函数:使用C字符串初始化
string(const char* str = "")
: _str(new char[strlen(str) + 1]) // 多分配1个字节存放'\0'
{
strcpy(_str, str);
}
// 析构函数:释放动态分配的内存
~string() {
delete[] _str;
_str = nullptr;
}
// 获取字符串长度(不包含'\0')
size_t size() const {
return strlen(_str);
}
// 重载[]运算符,支持像数组一样访问字符
char& operator[](size_t i) {
return _str[i];
}
// 获取C风格字符串(只读)
const char* c_str() const {
return _str;
}
private:
char* _str; // 核心:指向动态字符数组的指针
};
}
关键点:
-
构造函数:参数使用
const char*
而不是char*
,char*
无法传入常量字符串,一旦传入常量字符串,就相当于char* _str = const char* str
。这是访问权限的放大,会导致报错。(访问权限的缩放规则对指针和引用生效) -
内存管理:在堆上(
new[]
)分配内存,并在析构时释放(delete[]
),这是管理动态资源的类的典型特征。 -
默认参数:
const char* str = ""
使得默认构造函数和带参构造函数合二为一。
1.2 拷贝构造
假设我们自己不写拷贝函数,编译器会调用自动生成的拷贝函数(浅拷贝)。
编译器默认生成的「拷贝构造」和「赋值重载」是浅拷贝(仅拷贝指针值,不拷贝指针指向的内容),会导致严重问题:两个对象的 _str
指向同一块内存,析构时会「重复释放同一块空间」,触发程序崩溃。程序如下:
void test_string() {
string s1("hello");
string s2(s1); // 编译器默认浅拷贝:s2._str = s1._str(同一块内存)
}
// 函数结束时:s2先析构(释放内存),s1再析构(释放已释放的内存→崩溃)
详细解析:
浅拷贝也称之为“值拷贝”,会把一块空间中的数据直接拷贝到另一块空间。也就是说,s1中 _str 指针的值被直接赋给了s2中的 _str 指针,这两个 string 对象的 _str 指针指向的是同一块空间,当函数退出时,s1 和 s2 结束生命周期,调用析构函数,就会将 _str 指向的空间释放 2 次,而一块空间是不能被重复释放的,所以导致了报错。
而与“浅拷贝”对应的,“深拷贝”可以解决这个问题。
深拷贝的核心逻辑:为新对象重新分配一块独立的内存,再拷贝原对象的内容,让两个对象的 _str
指向不同内存,彼此独立。
深拷贝版拷贝构造:
string(const string& s)
: _str(new char[strlen(s._str) + 1]) { // 为s2新分配内存
strcpy(_str, s._str); // 拷贝s1的内容到新内存
}
1.3 深拷贝版赋值重载
需注意两个细节:
① 防止「自赋值」(如 s1 = s1
);
② 先释放原内存,再指向新内存(避免内存泄漏)。
string& operator=(const string& s)
{
// 1. 防止自赋值:若自己赋值给自己,直接返回(避免释放自身内存后拷贝)
if (this != &s) {
// 2. 先分配新内存,拷贝内容(若new失败,原内存不会被破坏)
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
// 3. 释放原内存,指向新内存
delete[] _str;
_str = tmp;
}
return *this; // 支持链式赋值(如 s1 = s2 = s3)
}
2. 支持增删查改的 string 类
基础版 string 仅满足简单需求,实际使用需「动态扩容、尾插、插入、删除、查找」等功能。此时需新增两个成员变量:
-
_size
:有效字符个数(不包含'\0'
),替代strlen
(避免每次调用都遍历字符串,提高效率); -
_capacity
:当前内存可容纳的最大有效字符数(不包含'\0'
),用于动态扩容管理。
2.1 框架与核心接口
namespace practice_string {
class string {
public:
// 基础接口(复用+优化)
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
char& operator[](size_t i) { assert(i < _size); return _str[i]; } // 越界检查
const char* c_str() const { return _str; }
// 核心功能:扩容、尾插、插入、删除、查找
void reserve(size_t n); // 扩容(仅扩大容量,不改变有效字符)
void resize(size_t n, char ch = '\0'); // 调整size(可补字符)
void push_back(char ch); // 尾插单个字符
void append(const char* str); // 尾插字符串
string& insert(size_t pos, const char ch); // 插入字符
string& erase(size_t pos, size_t len); // 删除字符
size_t find(const char ch, size_t pos = 0); // 查找字符
private:
char* _str;
size_t _size; // 有效字符数
size_t _capacity; // 容量(最大有效字符数)
static const size_t npos; // 静态常量:表示“未找到”(值为-1,size_t最大值)
};
// 静态成员初始化(类外)
const size_t string::npos = -1;
}
2.2 构造函数与析构函数
/* 默认构造函数 */
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // 多加一个空间给'\0'
strcpy(_str, str);
}
/* 拷贝构造函数 */
string(const string& s)
{
_size = s._size;
_capacity = _size;
_str = new char[_capacity + 1]; // 多加一个空间给'\0'
strcpy(_str, s._str);
}
/* 析构函数 */
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
2.3 扩容(reserve)
-
作用:仅当
n > _capacity
时,扩大内存容量(避免频繁扩容,提高效率); -
细节:扩容后需拷贝原字符串内容,释放原内存。
void practice_string::reserve(size_t n) {
if (n > _capacity) { // 仅当需要的容量大于当前容量时才扩容
char* tmp = new char[n + 1]; // 多1字节存'\0'
strcpy(tmp, _str); // 拷贝原内容
delete[] _str; // 释放原内存
_str = tmp; // 指向新内存
_capacity = n; // 更新容量
}
}
2.4 赋值重载
/* 赋值重载函数 */
string& operator=(const string& s)
{
if (this != &s)
{
// 开辟一块新的空间,拷贝数据过去
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
// 释放原来的空间防止内存泄露,指向新空间
delete[] _str;
_str = tmp;
}
return *this;
}
string& operator=(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // 多加一个空间给'\0'
strcpy(_str, str);
return *this;
}
2.5 迭代器
/* 迭代器 */
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
2.6 resize
-
作用:同时管理「容量」和「有效字符数」:
-
若
n < _size
:截断字符串(仅修改_size
,不释放内存); -
若
n > _size
:先扩容(若需),再用ch
补全新增的位置。
-
void practice_string::resize(size_t n, char ch) {
if (n < _size) // 截断:直接在n位置放'\0',修改size
{
_str[n] = '\0';
_size = n;
}
else // 扩容+补字符
{
if (n > _capacity) reserve(n); // 容量不足则先扩容
// 补字符(从原size到n)
for (size_t i = _size; i < n; ++i) {
_str[i] = ch;
}
_size = n; // 更新size
_str[_size] = '\0'; // 确保字符串结束符
}
}
2.7 尾插字符/字符串
-
尾插单个字符(push_back):先检查容量(满则扩容,默认扩为 2 倍或初始 2),再插入字符并更新
_size
。
/* 尾插单个字符 */
void push_back(char ch)
{
// 空间不足,扩容 hello xxxxxxx &tmp xxxxxxxxxxxx
if (_size == _capacity)
{
size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(new_capacity);
}
// 放入字符
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
/* s1 += 'a' */
string& operator+=(const char ch)
{
this->push_back(ch);
return *this;
}
-
尾插字符串(append):计算字符串长度,若
_size + 长度 > _capacity
则扩容,再拷贝字符串到末尾。
/* 尾插字符串 */
void append(const char* str)
{
size_t len = strlen(str);
// 如果空间不足,则增容
if (_size + len > _capacity)
{
size_t new_capacity = _size + len;
reserve(new_capacity);
}
// 拷入新的字符串到原字符串后面
strcpy(_str + _size, str);
_size += len;
}
/* s1 += "abc" */
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
2.8 insert
-
逻辑:先检查越界(
pos
需小于_size
),再扩容(若需),然后「挪动数据」(从后往前挪,避免覆盖),最后插入字符 / 字符串。 -
在pos下标位置插入字符:
/* 在pos下标位置插入字符 */
string& insert(size_t pos, const char ch)
{
assert(pos < _size);
// 空间不够就增容
if (_size == _capacity)
{
size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(new_capacity);
}
// 挪动数据,从'\0'开始挪
int end = _size;
while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,
// 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转
{
_str[end + 1] = _str[end];
--end;
}
// 插入数据
_str[end] = ch;
++_size;
return *this;
}
-
在pos下标位置插入字符串
两种方式:
/* 在pos下标位置插入字符串 */
string& insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
// 空间不足则扩容
if (_size + len > _capacity)
{
reserve(_size + len); // 函数内会自动多开一个空间给'\0'
}
// 挪动数据
int end = _size;
while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,
// 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转
{
_str[end + len] = _str[end];
--end;
}
// 放字符串数据(或者使用memcpy也行)
for (size_t i = 0; i < len; ++i)
{
_str[pos] = str[i];
++pos;
}
_size += len;
return *this;
}
-
memcpy搬数据的方法:
/* 在pos下标位置插入字符串 */
/* memcpy搬数据的方法 */
void insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
// 空间不够就增容
if (_capacity < _size + len)
{
size_t new_capacity = _capacity + len;
reserve(new_capacity);
}
// 得到要搬运有效数据的长度
size_t memcpy_len = strlen(_str + pos);
// 将数据先存在另一个空间,之后再搬回来
char* tmp = new char[memcpy_len + 1];
memcpy(tmp, _str + pos, memcpy_len + 1); // 加上的1是'\0'的一个字节
_size -= memcpy_len;
// pos位置加上字符串str
memcpy(_str + _size, str, len + 1); // 加上的1是'\0'的一个字节
_size += len;
// 把另一个空间中的数据搬回来
memcpy(_str + _size, tmp, memcpy_len + 1); // 加上的1是'\0'的一个字节
delete[] tmp;
tmp = nullptr;
_size += memcpy_len;
}
2.9 erase
/* 从pos位置开始删除len个字符 */
string& erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos) // pos后面的都被删了
{
_str[pos] = '\0';
_size = pos;
}
else // 删除后pos后面还有数据存留
{
size_t i = pos + len;
while (i <= _size) // 移动数据
{
_str[i - len] = _str[i];
++i;
}
_size -= len;
}
return *this;
}
2.10 find
/* 从pos下标位置开始查找字符的下标位置 */
size_t find(const char ch, size_t pos = 0)
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i; // 找到返回下标
}
}
return npos; // 没找到返回-1(无符号的-1,size_t的最大值)
}
/* 从pos下标位置开始查找字符串的下标位置 */
size_t find(const char* str, size_t pos = 0)
{
char* p = strstr(_str, str);
if (p == NULL)
{
return npos;
}
else
{
return p - _str; // p - _str刚好是中间相差的元素个数,即 p 指向元素的下标
}
}
2.11 关系运算符重载
/* 字符串比较大小 */
bool operator<(const string& s)
{
int ret = strcmp(_str, s._str);
return ret < 0;
}
bool operator==(const string& s)
{
int ret = strcmp(_str, s._str);
return ret == 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s; // 尽量提高对代码的复用度,方便后续修改
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
2.12 输入输出重载
/* 输入重载 */
istream& operator>>(istream& in, string& s)
{
while (1)
{
char ch;
//in >> ch; 不能使用>>,因为ch是一个char类型的变量,>>默认无法接收' '和'\n'
// 导致下面的if判断无法成功,陷入死循环
ch = in.get(); // get函数可以直接获取字符,不会跳过
if (ch == ' ' || ch == '\n') // getline()函数和>>重载唯一的区别就是getline这里的判断没有ch == ' '
{
break;
}
else
{
s += ch;
}
}
return in;
}
/* 输出重载 */
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i];
}
return out;
}
2.13 模拟实现的string功能测试代码
// 遍历与迭代器test
void test_string1()
{
string s1;
string s2("hello");
cout << s1 << endl;
cout << s2 << endl;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
// 三种遍历方式
// 1.[]
for (size_t i = 0; i < s2.size(); ++i)
{
s2[i] += 1;
cout << s2[i] << " ";
}
cout << endl;
// 2.迭代器
string::iterator it2 = s2.begin();
while (it2 != s2.end())
{
*it2 -= 1;
cout << *it2 << " ";
++it2;
}
cout << endl;
// 3.范围for
// 范围for是由迭代器支持的,也就是说这段代码最终会被编译器替换成迭代器
// 想要支持范围for,需要先支持 iterator begin() end()
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
}
// pushback&insert_test
void test_string2()
{
string s1("hello");
s1.push_back(' ');
s1.push_back('w');
// char* arr = {0};
// strcpy(arr, s1.c_str());
cout << s1.c_str() << endl;
// cout << arr << endl;
s1.append("orld");
cout << s1 << endl;
string s2;
s2 += "abcd";
cout << s2 << endl;
s2 += 'e';
cout << s2 << endl;
s2.insert(1, "XYZ");
cout << s2 << endl;
s2.insert(1, "XYZ");
cout << s2 << endl;
}
// resize_test
void test_string3()
{
string s1("hello");
s1.resize(1);
cout << s1 << endl;
s1.resize(11,'x');
cout << s1.size() << endl;
cout << s1 << endl;
//for (int i = 0; i < s1.size(); ++i)
//{
// cout << (int)s1[i] << endl;
//}
}
// erase_test
void test_string4()
{
string s1("hello world");
s1.erase(2, 3);
cout << s1 << endl;
}
// find_test
void test_string5()
{
string s1("abcdefghijklmn");
cout << s1.find('c') << endl;
cout << s1.find("defg") << endl;
cout << s1.find("asdgf") << endl;
}
// cin >> s 和 cout << s
void test_string6()
{
string s1;
cin >> s1;
cout << s1;
}
3. 深浅拷贝问题(传统vs现代)
C++ 的一个常见面试提示让你实现一个 string 类。
限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源,也就是完成 默认构造 + 析构 + 拷贝 + operator=()
下面是默认构造+析构的简单代码:
class string
{
public:
string(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
~string()
{
delete[] _str;
_str = nullptr;
}
size_t size()
{
return strlen(_str);
}
char& operator[](size_t i)
{
return _str[i];
}
private:
char* _str;
};
3.1 传统写法(深拷贝)
手动分配新内存并复制数据。
// 拷贝构造函数(传统写法)
string(const string& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
// 赋值运算符重载(传统写法)
string& operator=(const string& s) {
if (this != &s) { // 1. 防止自我赋值
char* tmp = new char[strlen(s._str) + 1]; // 2. 分配新空间
strcpy(tmp, s._str); // 3. 拷贝数据
delete[] _str; // 4. 释放旧空间
_str = tmp; // 5. 指向新空间
}
return *this; // 6. 返回自身引用以支持连续赋值
}
关键点:
-
自我赋值判断:
if (this != &s)
至关重要,否则delete[] _str
会先释放自身资源,导致后续操作出错。 -
异常安全:先分配新空间和拷贝数据,成功后再释放旧空间。如果
new
失败抛出异常,旧数据依然完好。
3.2 现代写法
利用“拷贝-交换” ,更简洁且异常安全。
// 深拷贝 - 现代写法(更简洁)
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str); // tmp._str变成nullptr,析构时也不会出错
}
// 赋值
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(_str, tmp._str); // tmp是一个临时局部变量,出函数时就会析构,把_str的值给tmp._str,tmp析构会自动释放原_str的空间
}
return *this;
}
// 赋值更简洁的写法
// 这里最巧的是用了传值操作,相当于一次拷贝构造,此时的s就是我想要的东西
// 直接把_str和s._str一换,大功告成
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
关键点:
-
巧妙利用传值:
operator=(string s)
的参数s
是通过拷贝构造生成的实参副本。函数体内只需交换this
和s
的资源。 -
自动管理:函数结束时,形参
s
析构,自动释放了this
对象原来的资源。代码极其简洁且安全。 -
自我赋值:现代写法天然处理了自我赋值。如果是
s1 = s1
,传参时s
是s1
的副本,交换后,副本s
带着s1
原来的资源被析构,s1
的资源没变。
3.3 支持增删查改的string的拷贝构造和赋值
同样的,复杂一点的string同样可以使用现代写法来写拷贝构造和赋值函数。
传统写法:
/* 拷贝构造函数 */
string(const string& s)
{
_size = s._size;
_capacity = _size;
_str = new char[_capacity + 1]; // 多加一个空间给'\0'
strcpy(_str, s._str);
}
/* 赋值重载函数 */
string& operator=(const string& s)
{
if (this != &s)
{
// 开辟一块新的空间,拷贝数据过去
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
// 释放原来的空间防止内存泄露,指向新空间
delete[] _str;
_str = tmp;
}
return *this;
}
string& operator=(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // 多加一个空间给'\0'
strcpy(_str, str);
return *this;
}
现代写法:
void swap(string& s)
{
// ::的意思是我调用的不是这里的这个swap,而是全局域的swap
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
/* 拷贝构造现代写法 */
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
this->swap(tmp);
}
/* 赋值现代写法 */
string& operator=(string s)
{
this->swap(s);
return *this;
}