目录
string类的创建
string类的构造函数
itertor迭代器
[]操作符重载
字符串修改函数
1.尾插函数
2.append函数
3.+=运算符重载函数
4.clear函数
5.swap函数
容量检测或修改函数
resize函数
reserve函数
经过上一次的博客之后我们已经认识了string类,并且可以使用string类当中的相关的成员函数。为了巩固我们学习到的相关的知识,在本次的博客当中我们将自主实现一个string类来完成相应的功能。
首先我们想要创建string类就应该清楚他的存储的原理。实质上就跟我们的顺序表完全相同,申请一段连续的空间,进行数据的存储。所以我们在创建string类之前应该先定义三个变量。它们分别是:反应字符串长度的size,反应空间容量的capacity,为存储数据所开辟的空间str。这三个变量将会是我们string类作用的全部的私有变量。所示代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
};
}
在设计完所需要的私有成员变量之后,第二步需要做的就是设计构造函数,用于我们类对象的初始化操作。我们可以通过new操作符向堆区申请一段可以使用的空间。并将其他参数进行适当的初始化。
由于构造函数有很多重载类型,所以我们可以先设计一个简单一点的。比如将一个字符作为参数生成一个字符串对象。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
};
}
当我们只使用一个字符初始化对象的时候,我们只需要申请两个字符的空间即可,一个字节存放着我们的有效字符,一个字节存放着 ‘ \0 ’ 之后再将我们的 size 和 capacity 均设为1即可。需要注意的是:在设置capacity的时候我们并不将 ‘ \0 ’ 的长度计算在容量当中,所以 capacity 为1。
之后为了是我们的string类更加丰富,所以我们可以采用函数重载的形式,设计多个构造函数用于,初始化我们的对象。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
};
}
第二个构造函数我们使用一个字符串来初始话我们的对象。我们需要首先需要计算出字符串的长度,之后开辟出指定的空间,最后将字符串当中的数据复制到我们开辟好的空间当中即可。许哟啊注意的是:在复制的时候需要使用memcpy进行复制。因为对于我们的string类对象来说,所存储的数据可能存在 ‘ \0 ’ 如果使用strcpy及进行拷贝就可能造成数据缺失的弊端。
另一个十分重要的部分就是我们的拷贝构造。拷贝构造作为一种特殊的构造函数我们在初始话构造函数的同时也将其设计出来。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
};
}
在拷贝构造当中我们需要开辟一个和字符串对象相同的空间,之后将字符串对象的所有的特征都赋值给我们创造出来的新的对象即可。
上面三种构造函数是string类生成对象最常用的三个构造函数。我们先构建出最常用的功能以小见大即可。
同样的,说到构造函数自然而然就会想到我们的析构函数,在析构函数的部分当中我们要处理的功能为:释放我们开辟的堆区的空间,将我们私有变量的值进行适当的处理即可。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
};
}
之后为了检验我们的代码是否可以正常的使用,所以我们还可以提前完善出 c_str() 的功能, c_str()函数的作用就是返回我们堆区开辟的字符串的指针,让我们的cout不用重载也可以打印出来,(因为指针是内置类型,cout可以打印出内置类型)。同样的,我们还可以设置出一个返回size的接口,和capacity的接口,仅仅返回size和capacity的值即可,代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
};
}
之后我们就可以通过 c_str() 函数检测我们代码的正确性的,测试上述构造函数是否可以正常使用,测试结果如下:
我们会发现代码运行一切正常。
在完成构造函数之后我们就可以完善我们的迭代器了。之前我们说过,迭代器的本质就是一个指针,我们可以使用一个指针进行模拟实现迭代器。使用typedef重命名char*为itertor迭代器,之后返回首指针和尾指针即可。所示代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
};
}
因为我们迭代器的作用是遍历我们的字符串数组,对于字符串数组我们可能进行的操作分为查找和修改两种,因此我们的迭代器也应该分为两种,一种是不使用const修饰的迭代器,另一种是使用const修饰的迭代器。使用const修饰的迭代器只能读取数据不能进行数据的修改。
作为打印字符串指定元素并修改的操作,相信肯定离不开 [ ] 所以下一步我们要进行的操作就是重载 [ ] 运算符,进行指定元素的返回即可。这一部分的功能实现起来同样很简单,代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
};
}
在返回指定元素值的时候,我们最好先进行合法性检查,保证不会因为数组越界造成系统报错。同样的我们返回的内容也包括读写两种功能,因此我们将其分为两个函数完成。代码运行的结果:
功能一切正常。
尾插函数的作用就是向字符串当中插入一个字符的数据。对于这个函数我们第一步需要进行的操作就是检查容量是否已满,如果申请的容量满的话就需要重新申请空间,之后将数据拷贝到新申请的空间在释放原有的空间,最后在插入我们想要插入的数据即可。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
};
}
尝试插入一个元素测试上述代码的运行结果:
代码运行依旧正常。
既然有一个字符的插入就肯定有一个字符串的插入,使用append函数就可以向对象当中插入一个字符串。我们同样需要先检查容量是否已满,如果容量已经满的话就需要重新开辟一个数组,重复我们上面所进行的操作即可。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
};
}
我们尝试向字符串当中插入一个字符串,得到的结果如下:
但是在string类当中最常用的尾插操作并不是append函数,因为每一次都需要调用函数书写上会很麻烦,因此我们重载了一个+=操作符。但是实质上我们在函数当中调用的还是append函数,只不过使用起来会简单很多。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
string& operator+=(const char* str)
{
//复用append即可
append(str);
return *this;
}
};
}
就像是我们上述代码当中的,我们只需要在函数当中复用append函数即可,尝试调用+=函数插入一个字符串,运行结果如下:
clear函数的作用就是清空一个对象当中所存储的所有数据,这一个操作同样很简单。我们只需要想清楚字符串存在的原理即可。字符串存在的标志有两个,第一 ‘ \0 ’, 第二size。想要将字符串当中的内容清空,我们只需要将size置为0,并将 str [0] 当中存入 ‘ \0 ’ 即可。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
string& operator+=(const char* str)
{
//复用append即可
append(str);
return *this;
}
void clear()
{
//清空字符串对象当中的数据
_str[0] = '\0';
_size = 0;
}
};
}
同样测试上述代码运行的效果如下:
之后是我们的交换函数。想要交换两个字符串对象当中存储的数据,我们实质上仅仅需要的只是交换两个对象当中存储的size和capacity以及指向特定空间的指针而已。至于交换这些内置类型的函数我们也不需要自己写,直接调用std命名空间域当中的swap函数使用即可。代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
string& operator+=(const char* str)
{
//复用append即可
append(str);
return *this;
}
void clear()
{
//清空字符串对象当中的数据
_str[0] = '\0';
_size = 0;
}
void swap(string& s)
{
//只需要交换指针指向并修改size和capacity即可,无需开辟新的空间
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
};
}
字符串当中存储的数据也已经进行交换完毕了。代码运行一切正常。
在这一部分我们需要着重介绍的是resize和reserve函数。在之前我们向大家介绍过,resize函数的作用是调整字符串的长度,对字符串进行指定的修改。而reserve函数是针对容量进行修改,并不会对字符串造成影响。我们就来分别认识这两个函数。
对于resize函数我们需要分情况进行讨论。当我们调整之后的大小小于我们原有的空间的时候,我们需要舍弃指定位置后面的字符串的内容。如果调整的大小大于我们原有的空间的时候,我们需要针对于容量进行适当的扩容操作。并且为了符合库当中的函数,我们还应该允许对增加的字符串插入指定的字符。只要分好类,代码的编写含是很简单的,代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
string& operator+=(const char* str)
{
//复用append即可
append(str);
return *this;
}
void clear()
{
//清空字符串对象当中的数据
_str[0] = '\0';
_size = 0;
}
void swap(string& s)
{
//只需要交换指针指向并修改size和capacity即可,无需开辟新的空间
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
void resize(size_t n, char c = '\0')
{
//当n小于原有字符的个数的时候,舍弃n后面的字符
if (n < _size)
{
_size = n;
_str[n] = '\0';
}
//当n大于原有字符的个数的时候,判断是否容量需要进行改变,适当进行扩容
if (n > _size)
{
if (n > _capacity)
{
//需要进行扩容,直接调用reserve函数
reserve(n);
}
//将默认字符填入申请的空间当中
while (_size < n)
{
_str[_size] = c;
_size++;
}
_str[_size] = '\0';
}
}
};
}
运行结果如下:
空间缩小就舍弃原有字符串当中存储的数据的内容,空间扩大允许插入指定的字符。带么运行一切正常。
reserve函数主要针对于改变字符串对象的容量。实质上就是调用new函数重新申请一个空间,之后将我们的数据全部转移到新开辟的数组当中即可,同样分为两种条件。第一种就是对字符串扩容,那么我们原本字符串当中的数据就不做改变。另一种就是缩小容量,并且缩小之后的容量小于我们的字符串的长度,这一种情况编译器当中并没有明确的规定,所以我们可以修改原本的字符串的打印顺序也可以不做修改。(最好进行修改,因为如果不修改,就会造成数组越界的问题。)实现这一部分功能的代码如下:
#include
using namespace std;
//定义一个命名空间于,在其中创建一个字符串类
namespace test
{
class string
{
private:
int _size;
int _capacity;
char* _str;
public:
typedef char* itertor;
typedef const char* cosnt_itertor;
//使用函数重载,丰富构造函数,使我们可以使用多种类型进行对象的初始化
string(const char ch)
{
_size = 1;
_capacity = 1;
_str = new char[2];
_str[0] = ch;
_str[1] = '\0';
}
//写一个构造函数,用于初始化string类对象
string(const char*ch="")
{
_size = strlen(ch);
_capacity = _size;
_str = new char[_capacity+1];
memcpy(_str, ch, _size+1);
}
//定义一个拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
}
//定义一个析构函数,用于使用完毕的空间的释放
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
const char* c_str()const
{
return _str;
}
int size()const
{
return _size;
}
int capacity()const
{
return _capacity;
}
itertor begin()
{
return _str;
}
itertor end()
{
return (_str + _size);
}
const itertor begin() const
{
return _str;
}
const itertor end()const
{
return (_str + _size);
}
char& operator[](size_t index)
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
//检查开始的下标是否合法
assert(index < _size);
return _str[index];
}
//实现尾插函数
void push_back(char c)
{
//检验容量是否已满
if (_capacity == _size)
{
//开辟新的空间,每次根据原有空间的二倍进行开辟,并将原有数据转移至新的空间当中
//所开辟的空间需要考虑到'\0'
char* tmp = new char[_capacity == 0 ? 2 : _capacity * 2 + 1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity == 0 ? _capacity=1 : _capacity *= 2; //扩容完毕
}
//开始将数据插入到字符串数组当中
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
//实现尾插一个新的字符串
//检查数组的容量
int len=strlen(str);
if (_size + len > _capacity)
{
//开辟一段新的空间,转移数据后释放原有的空间
char* tmp = new char[_size+len+1];
memcpy(tmp, _str, _size);
delete[] _str;
_str = tmp;
_capacity = _size + len;
}
//将字符串当中的数据拷贝到对象当中
memcpy(&_str[_size], str, len);
_size += len;
_str[_size] = '\0';
}
string& operator+=(const char* str)
{
//复用append即可
append(str);
return *this;
}
void clear()
{
//清空字符串对象当中的数据
_str[0] = '\0';
_size = 0;
}
void swap(string& s)
{
//只需要交换指针指向并修改size和capacity即可,无需开辟新的空间
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
void resize(size_t n, char c = '\0')
{
//当n小于原有字符的个数的时候,舍弃n后面的字符
if (n < _size)
{
_size = n;
_str[n] = '\0';
}
//当n大于原有字符的个数的时候,判断是否容量需要进行改变,适当进行扩容
if (n > _size)
{
if (n > _capacity)
{
//需要进行扩容,直接调用reserve函数
reserve(n);
}
//将默认字符填入申请的空间当中
while (_size < n)
{
_str[_size] = c;
_size++;
}
_str[_size] = '\0';
}
}
void reserve(size_t n)
{
//调整capacity的大小,一般情况下,容量变小不做改变
if (_capacity < n)
{
char* tmp = new char[n + 1];
_capacity = n;
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
}
}
};
}