1.为什么手撕string类
在面试或者一些学习场景中,手撕 string
类不仅仅是对字符串操作的考察,更多的是考察程序员对 C++ 内存管理的理解。例如,深拷贝与浅拷贝的实现,如何正确重载赋值运算符,如何避免内存泄漏,这些都是需要掌握的核心技能,本文章最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
2.string类的构造
//成员变量 private: char* _str; size_t _size; size_t _capacity;
string::string - C++ Reference 文档
2.1 简单实现一个string类
#include<cstring>
namespace A
{
class string
{
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
#include"string.h"
using namespace std;
int main()
{
A::string s1("hello string!");
return 0;
}
2.1.1 解释:
- 构造函数:为字符串动态分配内存,并将传入的字符串内容复制到新分配的空间中。
- 析构函数:使用
delete[]
释放动态分配的内存,以避免内存泄漏。
补充:
#include<cstring>
namespace A
{
class string
{
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
#include"string.h"
using namespace std;
int main()
{
A::string s1("hello string!");
A::string s2(s1);
return 0;
}
解释:
未显示拷贝构造函数,默认生成拷贝构造函数会完成浅拷贝/值拷贝,导致一块内存空间被多个对象使用,析构函数会对这个空间析构多次,程序崩溃!
如下图:s1,s2同时指向同一块空间。
2.1.2 解决办法:
为了避免浅拷贝带来的问题,我们需要在拷贝构造函数中实现深拷贝。深拷贝确保每个对象都有自己独立的内存空间,不会与其他对象共享内存。
核心代码如下:
//深拷贝构造函数
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._size];
strcpy(_str, s._str);
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
A::string s2(s1);
return 0;
}
内存图如下:
可以看到s1,s2对象指向不同的空间,完成深拷贝,程序正常运行。
3.赋值运算符重载与深拷贝
3.1 为什么需要重载赋值运算符?
在C++中,当我们将一个对象赋值给另一个对象时,默认情况下,编译器会为我们生成一个浅拷贝的赋值运算符。这意味着赋值后的对象和原对象会共享同一个内存空间,这会导致和浅拷贝相同的潜在问题,特别是在一个对象被销毁时,另一个对象继续使用该内存区域会引发错误。
3.2 实现赋值运算符重载
在赋值运算符重载中,我们需要考虑以下几点:
- 自我赋值:对象是否会被赋值给自己,避免不必要的内存释放和分配。
- 释放原有资源:在赋值前,我们需要释放被赋值对象原有的内存资源,避免内存泄漏。
- 深拷贝:为目标对象分配新的内存,并复制内容。
3.2.1 示例:
#include<cstring>
namespace A
{
class string
{
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
//深拷贝构造函数
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._size];
strcpy(_str, s._str);
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
A::string s2("hello C++");
s1 = s2;
return 0;
}
执行到语句 : s1 = s2后的监视图如下:可以看到和上诉的情况一样,只完成浅拷贝/值拷贝,程序结束时,自动调用析构函数,同一块内存被析构多次,导致程序崩溃。
解决方法:
显示深拷贝的赋值运算符重载
// 赋值运算符重载
string& operator=(const string& s){
if (this != &s) { // 避免自我赋值
delete[] _str; // 释放原有内存
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1]; // 分配新内存
strcpy(_str, s._str); // 复制内容
}
return *this;
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
A::string s2("hello C++");
s1 = s2;
return 0;
}
完成深拷贝后程序,s1,s2 指向不同的空间,如下图:
3.2.2 解读代码
自我赋值检查:自我赋值是指对象在赋值时被赋值给自己,例如 s1 = s1。在这种情况下,如果我们没有进行检查,就会先删除对象的内存,然后再试图复制同一个对象的内容,这样会导致程序崩溃。因此,重载赋值运算符时,自我赋值检查是非常必要的。
释放原有内存:在分配新内存之前,我们必须先释放旧的内存,以防止内存泄漏。
深拷贝:通过分配新的内存,确保目标对象不会与源对象共享内存,避免浅拷贝带来的问题。
4.迭代器与字符串操作
4.1 迭代器的实现
迭代器是一种用于遍历容器(如数组、
string
等)的工具,它允许我们在不直接访问容器内部数据结构的情况下遍历容器。通过迭代器,可以使用范围for
循环等简便的方式遍历string
对象中的字符。
4.1.1 string类迭代器模拟
namespace A
{
class string{
//迭代器
itreator begin()
{
return _str;
}
itreator end()
{
return _str + _size;
}
//const迭代器
const_itreator begin()const
{
return _str;
}
const_itreator end()const
{
return _str + _size;
}
}
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
for (auto ch : s1)
cout << ch;
cout << endl;
A::string const s2("hello C++!");
for (auto ch : s2)
cout << ch;
cout << endl;
return 0;
}
5.字符串的常见操作
在 C++ 标准库
string
类中,提供了很多方便的字符串操作接口,如查找字符或子字符串、插入字符、删除字符等。我们也需要在自定义的string
类中实现这些操作。接下来,我们将逐步模拟实现这些功能,并进行测试。
5.1 查找操作
C++ 中 string
类的 find
()
函数用于查找字符串或字符在当前字符串中的位置。如果找到了字符或子字符串,find
()
会返回其位置;如果找不到,则返回 string::npos
。
我们将在自定义的 string
类中实现类似的功能。
#include<string.h>
#include<assert.h>
namespace A
{
class string
{
//typedef char* itreator;
using itreator = char*;
using const_itreator = const char*;
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
//迭代器
itreator begin()
{
return _str;
}
itreator end()
{
return _str + _size;
}
//const迭代器
const_itreator begin()const
{
return _str;
}
const_itreator end()const
{
return _str + _size;
}
//深拷贝构造函数
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._size];
strcpy(_str, s._str);
}
// 赋值运算符重载
string& operator=(const string& s) {
if (this != &s) { // 避免自我赋值
delete[] _str; // 释放原有内存
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1]; // 分配新内存
strcpy(_str, s._str); // 复制内容
}
return *this;
}
size_t find(const string& str, size_t pos = 0) const;
// 查找子字符串在字符串中的第一次出现位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, s);
if (ptr == nullptr)
{
return npos;
}
else {
return ptr-_str;
}
}
// 查找字符在字符串中的第一次出现位置
size_t find(char c, size_t pos = 0) const
{
assert(pos<_size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static const size_t npos = -1; // 定义 npos 为 -1,表示未找到
};
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
for (auto ch : s1)
cout << ch;
cout << endl;
A::string const s2("hello C++!");
for (auto ch : s2)
cout << ch;
cout << endl;
size_t ret=s1.find('g', 0);
cout << ret << endl;
ret = s1.find("str", 0);
cout << ret << endl;
return 0;
}
输出结果:
hello string!
hello C++!
11
6
5.1.1 为什么 static const size_t npos = -1 可以在类内初始化?
size_t 是一种整型类型,尽管其大小和符号位取决于平台,但它仍然是整型常量的一种。因此,npos 的初始化类似于前面提到的整型静态成员变量。由于 -1 可以表示为 size_t 的最大值,这个值在编译时就可以确定,因此它符合类内初始化的条件。
class String {
public:
static const size_t npos = -1; // 可以在类内初始化
};
总结:因为 npos 是整型常量,并且编译器可以在编译时确定其值,(只要是在编译时可以确定为常量就可以在类内初始化,无任何限制)符合在类内部初始化的条件。
5.2 插入操作
C++ 中的 string
类允许我们在字符串的任意位置插入字符或子字符串。接下来,我们将在自定义的 string
类中实现类似的插入功能。
5.2.1 示例代码:实现字符串插入
#include<string.h>
#include<assert.h>
namespace A
{
class string
{
//typedef char* itreator;
using itreator = char*;
using const_itreator = const char*;
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
// 在指定位置插入字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity;
//扩2倍不够,需要多少扩多少
if (newcapacity < _size + len)
newcapacity = _size + len;
reserve(newcapacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos++] = str[i];
}
_size += len;
}
// 在指定位置插入字符
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity * 2); // 如果容量不够,扩展容量
}
for (size_t i = _size; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = c;
_size++;
_str[_size] = '\0';
return *this;
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static const size_t npos = -1; // 定义 npos 为 -1,表示未找到
};
}
#include"string.h"
#include<iostream>
using namespace std;
int main()
{
A::string s1("hello string!");
s1.insert(11, "hello C++");
return 0;
}
5.3 删除操作
string
类允许我们删除指定位置的字符或子字符串。接下来,我们实现字符串的删除功能。
5.3.1 示例代码:实现字符串删除
#include<string.h>
#include<assert.h>
namespace A
{
class string
{
//typedef char* itreator;
using itreator = char*;
using const_itreator = const char*;
public:
//默认构造函数
string(const char* str = " ")
:_size(strlen(str))//字符有效个数
{
_capacity = _size;//容量大小
_str = new char[_size + 1];//申请内存空间
strcpy(_str, str);//拷贝数据
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
// 在指定位置插入字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity;
//扩2倍不够,需要多少扩多少
if (newcapacity < _size + len)
newcapacity = _size + len;
reserve(newcapacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos++] = str[i];
}
_size += len;
}
// 在指定位置插入字符
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity * 2); // 如果容量不够,扩展容量
}
for (size_t i = _size; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = c;
_size++;
_str[_size] = '\0';
return *this;
}
//删除字符或字符串
void erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - len)
{
_str[pos] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[end - len] = _str[end];
++end;
}
_size -= len;
}
}
//析构函数
~string()
{
delete[]_str;//释放空间
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static const size_t npos = -1; // 定义 npos 为 -1,表示未找到
};
}
相信通过这篇文章你对string类的有了初步的了解。如果此篇文章对你学习C++有帮助,期待你的三连,你的支持就是我创作的动力!!!