🥳个人主页: 起名字真南
🥳个人专栏:【数据结构初阶】 【C语言】 【C++】
目录
- 引言
- 1 模拟实现string类基本框架
- 2 实现string类中的主要成员函数
- 2.1 Push_Back 函数
- 2.2 reserve 函数
- 2.3 append 函数
- 2.4 c_str 函数
- 2.5 begin ,end 函数
- 2.5 operator= 函数
- 2.6 operator+= 函数
- 2.7 insert 函数
- 2.8 erase 函数
- 2.9 find 函数
- 2.10 substr 函数
- 3 实现string类中的非成员函数
- 3.1 双目运算符的重载
- 3.2 输出流
- 3.3 输入流
引言
C++中的std::string类是标准库提供的高级字符串处理工具,支持动态内存管理和丰富的操作函数。为了加深对字符串类的理解,我们将从零开始模拟实现一个简化版的String类,涵盖字符串的创建、拷贝、连接、查找等基本功能。本篇文章的主要目的是展示如何设计和实现一个类似于std::string的类,并探讨其中涉及的内存管理和操作细节。
1 模拟实现string类基本框架
为了和库函数中的 string
做出区分,所以模拟实现的类名是 String
。
用一个字符串构造一个函数时,我们在初始化的时候给一个默认参数,默认值为 ""
(空字符串),不能是单引号。这样可以让我们即使不传入任何参数的情况下,也可以构造一个空的 String
对象。
#include<iostream>
#include<assert.h>
using namespace std;
namespace wzr
{
class String
{
public:
String(const char* str = "")
{
size_t len = strlen(str);
//实际上要存储的数据是len个因为是字符数组所以最后一位留给'\0'
//char* tmp = new char[len + 1]
//_arr = tmp;
//上面的写法是错误写法因为他只是开辟了空间并没有将内容复制过去
_arr = new char[len + 1];
//使用strcpy可以将str的内容(包括'\0'都统一复制到_arr里面)
strcpy(_arr,str);
//都没有包含\0
_size = _capacity = len;
}
//拷贝构造函数 要求新构造的函数与原函数一样
String(const String& str)
{
//首先获取str中的元素个数
_size = str._size;
//开辟空间
//_arr = new char[_size + 1]; 实际空间大小不是原函数的空间大小
_arr = new char[str._capacity + 1];
//复制字符串的内容到新构造的字符串中
strcpy(_arr,str._arr)
//同步capacity
//不同于使用字符串构造函数 _capacity 需要与原函数保持一致
//_capacity = _size;
_capacity = str._capacity;
}
//我们现在已经初步实现了构造函数接下来实现析构函数
~String()
{
//delete [] _arr;
//delete和[]之间没有空格
delete[] _arr;
_arr = nullptr;
_capacity = _size = 0;
}
private:
char* _arr;
size_t _size;
size_t _capacity;
const static size_t npos;
}
}
2 实现string类中的主要成员函数
2.1 Push_Back 函数
内存不够需要开辟空间,这时候就需要用到 reserve
函数来开辟空间。在进行分配内存大小的时候,我们一般按照原来大小的二倍扩容 reserve(2 * _capacity)
,但是这个时候出现了一个问题:如果我们的字符串是一个空字符串,空间大小是 0,那么二倍以后依旧是 0。所以我们在这里需要用到三目操作符,并且给一个初始值是 4。
void String::push_back(const char ch)
{
//在进行尾插之前我们首先要检查内存大小是否足够
if(_capacity == _size)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity)
}
// 内存空间足够可以尾插
_arr[_size] = ch;
_size++;
//尾插过后因为是字符数组所以需要将最后一位置为0
_arr[_size] = '\0';
}
2.2 reserve 函数
不管是尾插还是插入字符串,只要是涉及到增加数据都需要扩容,所以我们实现 reserve
函数。在进行扩容之前,首先要判断 n
和原内存空间的大小:
- 如果
n
大于原内存空间,那么我们可以直接扩容。 - 如果
n
小于原内存空间,就需要判断是否小于数据空间。如果小于数据空间,直接减少内存会导致数据泄露。 - 如果
n
在这之间,则进行缩小到n
。
//不管是尾插还是插入字符串只要是涉及到增加数据都都需要扩容
//所以我们实现reserve函数
void String::reserve(size_t n)
{
if(n > _capacity)
{
//开辟空间大小为n
//新建一个临时空间用于拷贝
char* tmp = new char[n + 1];
//将原函数arr的数据拷贝到tmp变量里
strcpy(tmp,_arr);
//释放原空间的大小
delete[] _arr;
_arr = tmp;
_capacity = n;
//_size不发生变化
//进行缩小
}else if(n < _capacity && n >= _size)
{
char* tmp = new char[n + 1];
//我们在进行数据拷贝的时候一定要注意当我们在进行拷贝的时候
//_arr的空间大小是大于tmp的所以我们只需要拷贝我们需要的字符数
strncpy(tmp, _arr, _size);
//strcpy(tmp, _arr);
tmp[_size] = '\0';
delete[] _arr;
_arr = tmp;
_capacity = n;
//缩小_size不会发生变化
}
}
2.3 append 函数
void String::append(const char* str)
{
//断言追加的字符串不能回空
assert(str);
//首先进行判断内存是否足够
//使用len来记录添加字符串的字符数
size_t len = strlen(str);
if (_size + len > _capacity)
{
//如果原来的数据加上新的字符串的个数大于原空间内存的大小
//我们需要进行扩容
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
//扩容完成现在要考试移动数据
strcpy(_arr + _size, str);
_size += len;
//由于strcpy会把'\0'自动拷贝过来所以我们不需要手动添加
}
2.4 c_str 函数
因为我们设置的 _arr
是私有变量,所以为了方便访问以及保护私有变量的安全,我们模拟实现了 c_str
函数。实现这个函数的同时还有一个目的,就是可以在 test.cpp
函数内输出 String
类型对象数组的内容。
并且像这种短小但是调用频繁的函数,我们直接写在头文件中,因为在类中定义的函数默认为内联函数,可以减少函数的调用,从而提高程序的运行效率。内联函数在编译器的编译阶段就会被替换,所以函数体积不宜过大。
char* c_str()
{
return _arr;
}
//我们不仅需要返回普通数据还要返回常量
const char* c_str() const
{
return _arr;
}
2.5 begin ,end 函数
//其实迭代器的底层逻辑就是通过typedef来定义的所以我们这里的数据类型都是char类型
//所以在定义的时候只需要定义char* 和 const char*
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _arr;
}
iterator end()
{
return _arr + _size;
}
const_iterator begin() const
{
return _arr;
}
const_iterator end() const
{
return _arr + _size;
}
2.5 operator= 函数
重载赋值操作符的时候,把它放在 String
类的里面作为成员函数有以下好处:
- 可以直接访问私有成员变量。
- 固定左操作数(即当前对象),因为赋值操作符的左操作数总是当前对象。
- 同时返回
*this
可以支持链式赋值(例如a = b = c
)。
并且 C++ 也规定了赋值操作符必须是成员函数。
String& operator=(const String& str)
{
_arr = new char[_capacity + 1];
strcpy(_arr, str._arr);
_size = str._size;
_capacity = str._capacity;
return *this;
}
2.6 operator+= 函数
在进行模拟实现的时候,因为 +
、+=
和 Push_Back
、append
函数的功能相同,
所以我们可以直接调用已经实现好的两个函数。
String& String::operator+=(char ch)
{
push_back(ch);
return *this;
}
String& String::operator+=(const char* ch)
{
append(ch);
return *this;
}
2.7 insert 函数
//insert 函数在指定位置增加添加数据
void String::insert(size_t pos, char ch)
{
//判断pos指针是否有效
assert(pos >= 0 && pos <= _size);
//判断内存空间大小
if (_size + 1 >= _capacity)
{
//注意原内存可能空间为0的时候
reserve(_capacity ==0 ? 1 : 2 * _capacity);
}
//将pos以及pos以后的位置向后移位
//定义一个end变量用来记录最后一个位置
size_t end = _size;
while (end >= pos)
{
_arr[end + 1] = _arr[end];
end--;
}
//因为我们在移动数据的时候已经将\0向后移位所以不需要在进行赋值
_arr[pos] = ch;
_size += 1;
//当参数为一个字符串的时候
void String::insert(size_t pos, const char* ch)
{
assert(pos >= 0 && pos <= _size);
//判断内存空间大小
size_t len = strlen(ch);
if (_size + len >= _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
//将pos以及pos以后len个长度的位置向后移位
size_t end = _size;
while (end >= pos)
{
_arr[end + len] = _arr[end];
end--;
}
//插入数据
for (size_t i = 0; i < len; i++)
{
_arr[pos + i] = ch[i];
}
_size += len;
}
}
2.8 erase 函数
void String::erase(size_t pos)
{
assert(this->_arr);
assert(pos >= 0 && pos <= _size);
//移除某一位置的元素只需要将该位置后面的元素直接向前移位
//因为是删除数据所以不涉及到开辟空间内存的问题
for (size_t i = 0; i < _size - pos; i++)
{
//遍历从pos位置一直到最后一个字符
//将pos位置后面的字符向前移动一位
_arr[pos + i] = _arr[pos + 1 + i];
}
_size -= 1;
}
2.9 find 函数
使用 npos
的前提是在 String
类中声明 npos
变量为静态变量,这样所有该类的对象都可以共享这个变量,并且可以通过类名直接访问。同时,静态成员变量的定义要在类的外面进行。
// 在 String 类内部声明静态变量
class String {
public:
static const size_t npos; // 声明静态常量成员变量
// 其他成员函数和变量...
};
// 在类的外部定义静态变量
const size_t String::npos = -1; // 赋值为 -1,表示未找到
这样,npos
可以被所有 String
对象共享,并通过 String::npos
访问。
const size_t String::npos = -1;
size_t String::find(char c)
{
assert(this->_arr);
for (size_t i = 0; i < _size; i++)
{
if (c == _arr[i])
{
return i;
//这里是找到了返回下标
}
}
//C++中npos代表无,所以这里没有找到返回的时无
return npos;
}
size_t String::find(const char* ch, size_t pos)
{
//从pos位置寻找一个字符串并返回首字符的坐标
size_t len = strlen(ch);
//调用strstr来寻找
const char* ptr = strstr(_arr + pos, ch);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _arr;
}
}
2.10 substr 函数
String String::substr(size_t pos, size_t len)
{
//从当前字符的n位置出提取len个字符形成一个新的String类对象
//判断如果len过大可能会多开空间
if (len > _size - pos)
{
len = _size - pos;
}
String sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _arr[pos + i];
}
return sub;
}
3 实现string类中的非成员函数
3.1 双目运算符的重载
bool operator==(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator>(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
bool operator>=(const String& s1, const String& s2)
{
return (s1 > s2 || s1 == s2);
}
bool operator<(const String& s1, const String& s2)
{
return !(s1 >= s2);
}
bool operator<=(const String& s1, const String& s2)
{
return !(s1 > s2);
}
bool operator!=(const String& s1, const String& s2)
{
return !(s1 == s2);
}
3.2 输出流
ostream& operator<<(ostream& out, const String& s)
{
out << s.c_str() << endl;
return out;
}
3.3 输入流
istream& operator >> (istream& in, String& s1)
{
//首先清空s1
s1.clear();
//提前开辟一块空间用来存储输入的数据
const size_t N = 256;
char buff[N];
//获取字符
char ch;
ch = in.get();
//作为buff的指针用来存储数据
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
//+=会找0的位置
buff[i] = '\0';
s1 += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s1 += buff;
}
return in;
}