这篇文章我们会深入讲解string类的实现,但是我们首先要对string类有一定的了解,所以推荐可以先看看string类的使用,推荐:【C++】模板编程入门指南:零基础掌握泛型编程核心(初阶)
string类的结构非常简单,它实际就是一个char类型的顺序表,它的结构和顺序表一致,都是一个指针再加size和capacity,如下:
namespace TL
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
在这里我们说明一下,string类的实现我们将采用声明和定义分离的方式实现,之后我们给出的函数实现都是cpp定义文件的实现,头文件的声明就不具体给出了
string类的构造我们就实现三个就可以了,三个接口就够我们了解string类的构造了,这三个接口分别如下:
这三个接口还可以进行合并,比如将字符串的构造和默认构造进行合并,方法就是让这个字符串的缺省值为空串,这样不传参数就默认生成一个空串“”,我们可以通过strlen函数得到字符串的长度,然后再开空间,使用strcpy初始化我们的_str
这里要注意的是开空间要始终比_capacity多开一个,因为字符串的特点是以\0为结尾,如果string类对象的容量为10,也就是可以存放10个字符,那么就必须多开一个空间来存放\0,接下来我们把默认构造和字符串构造结合的构造写出,如下:
//这里是cpp文件,缺省值在声明时给的空串“”
string::string(const char* s)
:_size(strlen(s))
{
_capacity = _size;
//注意要多开一个空间存放\0
_str = new char[_capacity + 1];
//strcpy会拷贝\0
strcpy(_str, s);
}
接下来我们来实现另一个填充n个字符c的构造,这个构造也相对比较简单,只需要开好空间,然后通过循环填充字符即可,如下:
string::string(size_t n, char c)
:_size(n)
{
_capacity = _size;
//注意要多开一个空间存放\0
_str = new char[_capacity + 1];
for (size_t i = 0; i < _size; i++)
{
_str[i] = c;
}
//这里不会自动给\0,需要我们手动给出
_str[_size] = '\0';
}
析构非常简单,相当于就是之前顺序表的Destroy函数,进行资源空间的释放,如下:
string::~string()
{
if (_str)
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
等下在讲拷贝构造和赋值重载时我们会用到string类的swap函数,所以我们这里先简单讲解一下swap的实现,想要交换两个string类对象其实很简单,只需要把对应的指针、size以及capacity进行交换即可,如下:
void string::swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
string类的拷贝构造其实很简单,我们会讲两个写法,讲到现代写法一定会让你眼前一亮,我们先来想想如果按之前我们学习的深拷贝,我们是不是需要给当前对象开辟和被拷贝对象一样的空间,然后将对应的capacity和size也修改了即可,这就是普通写法,如下:
string::string(const string& str)
{
//以前的写法
if(_str)
delete[] _str;
_str = new char[str._capacity + 1];
_size = str._size;
_capacity = str._capacity;
strcpy(_str, str._str);
}
这样写是一定没有问题的,但是我们其实还有一个更妙的方法,就是借助编译器帮我们做这些事情,具体思路是在函数中创建一个局部对象,这个对象是str的拷贝,然后我们直接将这个局部对象的资源拿过来,也就是进行交换
这个时候我们目标对象已经得到对应的资源了,也就是拷贝成功了,而之前的资源被我们交换到了这个局部对象上,出了作用域这个局部对象就会调用析构函数将之前的资源销毁,也就成功让编译器帮我们做事了,具体实现如下:
string::string(const string& str)
{
//现代写法(利用构造)
string tmp(str._str);
swap(tmp);
//出了作用域tmp调用析构后销毁之前的资源
}
赋值重载的普通写法以及现代写法和拷贝构造差不多,这里直接给出实现:
string& string::operator=(const string& str)
{
//注意判断,不要自己给自己赋值
if (this != &str)
{
//普通写法
/*if (_str)
delete[] _str;
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;*/
//利用拷贝构造,也可以利用构造
string tmp(str);
swap(tmp);
}
return *this;
}
在我们实现修改接口之前我们要先来实现一下扩容接口reserve,因为在插入数据时数据可能满了,我们需要进行扩容,至于扩容逻辑我们还是按照两倍去扩容数组,同时注意判断扩容参数,如果小于当前的容量我们就不进行扩容,因为这个接口还要开放给用户使用,万一用户传错误的值就不行了,实现如下:
void string::reserve(size_t n)
{
//如果参数小于等于容量就不予处理
if (n <= _capacity)
{
return;
}
//2倍扩容
size_t newCapacity = _capacity * 2;
//如果2倍扩容不够就直接扩容到n
if (newCapacity < n)
newCapacity = n;
//开新空间,注意还是要多开一个空间以后放\0用
char* tmp = new char[newCapacity + 1];
//数据的拷贝
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = newCapacity;
}
尾插一个字符非常简单,跟我们顺序表的尾插差不多,但是唯一要注意的是我们的string类的底层是一个字符串,字符串以\0为结尾,而_size位置就存放的是\0,我们将其用来填充了要插入的字符,那么这个字符串就没有\0作为结尾了,在我们插入字符后要将\0添上,如下:
void string::push_back(char c)
{
//注意查看空间是否满了,如果满了就扩容
if (_size == _capacity)
{
reserve(2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
尾删我们已经写过很多次了,实际上就是让size–就可以了,但是要注意这是删除接口,所以要保证数组中有数据,可以进行断言,同时这是一个字符串,所以还要把\0补上,如下:
void string::pop_back()
{
//确保数组不为空
assert(_size > 0);
//size--相当于就把它删除了
_size--;
//补上\0
_str[_size] = '\0';
}
我们来看看append我们实现哪些接口,如下:
我们会实现string对象和n个字符c的追加,字符串可以通过隐式类型转换为string类对象,所以可以不用专门实现,接下来我们一个一个来讲解
追加一个string类对象实际上就是将这个string类对象的字符串拷贝到当前对象的末尾,但是要注意算出这个字符串的长度以便于看看是否需要扩容,如下:
void string::append(const string& str)
{
size_t len = strlen(str._str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
//将str的字符串拷贝到当前对象
//注意\0会跟着一起拷贝,不需要专门给
strcpy(_str + _size, str._str);
_size += len;
}
接下来我们来实现n个c字符的append,和上面插入字符串不同的就是要使用循环插入n个字符c,并且要自己手动设置\0,其它逻辑几乎相同,如下:
void string::append(size_t n, char c)
{
if (n + _size > _capacity)
{
reserve(n + _size);
}
for (size_t i = _size; i < n + _size; i++)
{
_str[i] = c;
}
_size += n;
_str[_size] = '\0';
}
我们来看看insert我们实现哪些接口,如下:
insert接口和append接口最大的不同就是insert是往任意位置插入元素,而append是追加,所以这里涉及到元素的挪动,至于往后挪动几个位置就要看我们要插入的字符串的长度是多少,我们要将刚好留这个字符串的长度,我们画个图分析分析:
接下来有了上面图中的示意,我们实现insert就很简单了,如下:
void string::insert(size_t pos, const string& str)
{
assert(pos >= 0 && pos <= _size);
size_t len = strlen(str._str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t src = _size - 1;
size_t dest = src + len;
//这里如果不强转为int就会导致死循环和越界访问
//因为src类型是size_t,pos为0再--会变成非常大的数
while ((int)src >= (int)pos)
{
_str[dest--] = _str[src--];
}
//下面这句strcpy也能实现上面的拷贝功能
//strcpy(_str + pos + len, _str + pos);
//接下来将str中的字符串拷贝过来,注意不要拷贝\0
strncpy(_str + pos, str._str, strlen(str._str));
_size += len;
_str[_size] = '\0';
}
接下来我们来实现第二个接口,也就是在指定位置插入n个c字符,和上面的过程都差不多,只是变成了字符,如下:
void string::insert(size_t pos, size_t n, char c)
{
assert(pos >= 0 && pos <= _size);
if (_size + n > _capacity)
{
reserve(_size + n);
}
size_t src = _size - 1;
size_t dest = src + n;
while ((int)src >= (int)pos)
{
_str[dest--] = _str[src--];
}
//循环填充字符
for (size_t i = pos; i < pos + n; i++)
{
_str[i] = c;
}
_size += n;
_str[_size] = '\0';
}
我们来看看erase的接口:
erase接口我们就实现第一个就够用了,从pos位置开始删除len个字符,要注意它们的默认含义,就是从0位置开始删除npos个字符,由于npos非常大,所以默认相当于就是删完所有元素,在实现这个接口之前我们要把npos这个成员变量添上
之前我们也介绍过npos,它其实是一个size_t类型的静态成员变量,并且是一个常量,它的值非常大,这个npos可以给用户使用,所以它的声明要放在类的public的限制下,那么我们怎么给它一个非常大的值呢?很简单,它是无符号整型,我们给一个-1就是size_t的最大值了,如下:
const size_t string::npos = -1;
接下来我们就来实现erase函数,它的实现也涉及到挪动数据,只不过erase是从后往前挪动,把要删除的数据覆盖,而insert是从前往后挪动,给插入的数据留出位置,它们的原理差不多,这里我们直接给出实现,如下:
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
len = _size - pos;
size_t src = pos + len;
size_t dest = pos;
for (size_t i = pos + len; i < _size; i++)
{
_str[dest--] = _str[src--];
}
//下面这句strcpy也可以实现数据的覆盖拷贝,达到删除的目的
//strcpy(_str + pos, _str + pos + len);
_size -= len;
_str[_size] = '\0';
}
我们来看看+=重载有哪些接口,如下:
我们可以发现+=的重载实际上就是append和push_back的结合体,我们直接复用代码就好了,如下:
string& string::operator+=(const string& str)
{
append(str);
return *this;
}
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
我们来看看+重载有哪些接口:
C++没有将+重载写成成员函数的原因就是,可能是字符+string类对象,或者是字符串+string类对象,这两种情况用成员函数就实现不出来,所以没有将它写成成员函数
这些接口也很好实现其实,因为我们上面大部分接口都写了,我们还是直接复用就好了,接下来我们就简单写几个,没有写到的可以自己尝试一下,复用接口就行了,如下:
//不是成员函数
string operator+(char c, const string& str)
{
//之前实现的n个c字符的构造
string tmp(1, c);
//直接复用+=
return tmp += str;
}
string operator+(const char* s, const string& str)
{
//字符串的构造
string tmp(s);
//复用+=
return tmp += str;
}
string operator+(const string& str, char c)
{
string tmp(str);
//利用匿名对象
return tmp += string(1, c);
}
string operator+(const string& str, const char* s)
{
string tmp(str);
//利用隐式类型转换,将s转换为string然后+=
return tmp += s;
}
那么今天string类的实现(上)就讲到这里了,今天讲的都是string类的核心接口,希望大家都能掌握,敬请期待string实现的下篇吧 bye~