在学习了类和对象、模板等前期的C++基础知识之后,我们可以尝试根据C++标准库中所提供的接口类型,来搭建我们自己的string类型。这个过程有助于初学者掌握C++的基础语法及底层逻辑。
框架的搭建
首先搭建模型的基础框架,需要建立my_string.h和my_string.cpp文件,头文件中是函数的声明,.cpp文件中是函数的实现。再创建main.cpp文件主要用于对my_string的功能测试。
- 在头文件中使用命名空间,以免和标准库中的string发生冲突,在测试时指明我们自己的命名空间即可。
- 在源文件中包含上我们创建的my_string.h文件,以保证项目正常运行。
1、成员属性
在C++的标准库文件中可以看到,string的底层逻辑其实是一个顺序表,成员属性包括顺序表的首地址、顺序表的容量以及当前情况下的大小。我们可以定义my_string的成员属性为:
//my_string.h
#include <iostream>
using std::cout;
using std::cin;namespace ltq //命名空间名称可以更改
{class string {public:private: char* _str;//顺序表的地址size_t _size;//当前大小size_t _capacity;//容量大小};
}
2、构造函数和析构函数
string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_size + 1];memcpy(_str, str, _size + 1);}~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}
这里需要说明的是:在构造函数中建议不要把_str写入初始化列表中,如果初学者避免多次调用strlen(),在这里复用_size的话就会发生错误;因为初始化列表的顺序并不是程序进行初始化的顺序,初始化的顺序是和成员属性的顺序保持一致的。也就是说_str会先进行初始化,而那时的_size还是随机值,这样就是导致发生错误。
为了能够有效的进行测试,这里需要提前写上c_str()成员方法,配合cout 可以进行程序的打印测试。
char* c_str(){return _str;}
下面就可以进行测试,来验证一下构造函数和析构函数的功能是否正常。
3、迭代器和范围for
C++11中的范围for是十分简洁的,它其实是用迭代器来进行实现的,string的迭代器事实上就是char* 类型的指针。
typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}
此时的my_string就支持使用迭代器和范围for来进行访问了。测试如下:
但是需要注意一个问题,上述代码正常的my_string对象是没有问题的,但是被const修饰对象就不能正常使用,因为有this类型不匹配的问题。所以上述的代码我们也需要加上const版本,尽可能和库中的string功能保持一致。所以需要做出以下更新:
typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}
此时const修饰的对象仍然可以正常调用。
4、reserve和resize函数
string在容量不够的情况下可以进行主动和被动的扩容,reserve函数可以直接被用户调用实现扩容功能。也可以嵌在拷贝构造函数中,应对各种容量不足的被动扩容功能。resevse函数一般只会往大扩容。
void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];memcpy(tmp, _str, _size * sizeof(char));delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n){if (n < _size){_str[n] = '\0';}else{reserve(n);while (_size < n){_str[_size++] = '\0';}}}
5、拷贝构造函数
我们知道拷贝构造函数在传参时要传入拷贝目标的引用,这是为了避免无限递归。如果在传参时是传值传参,实参传递给形参这一过程会触发一次拷贝操作,就会引发无限递归。所以要传引用传参。拷贝构造函数有几种不同的写法:
第一种:最传统的写法,在函数内部自己开空间,再进行内存拷贝,修改成员属性;
string(const string& str){_str = new char[str._size + 1];memcpy(_str, str._str, str._size + 1);_size = str._size;_capacity = str._capacity;}
但是上述的new一旦失败就是抛出异常,后面的内存拷贝函数就会出现异常。所以,这里采用一种全新的写法:调用构造函数构造中间对象,再将this对象与中间对象的内容进行交换。当中间对象出作用域时,还会调用析构函数释放原来的空间。但是这里需要注意的是:需要在这里进行对 成员属性进行初始化列表初始化,因为一旦编译器不对内置类型进行初始化的情况下,this._str就是随机值也就是野指针,当交换tmp对象和this._str之后,出作用域tmp调用析构函数,析构函数对野指针指向的空间进行释放时就是产生错误。所以,务必在这里加上初始化列表。
string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_size + 1];memcpy(_str, str, _size + 1);}//string(const string& str)//{// _str = new char[str._size + 1];// memcpy(_str, str._str, str._size + 1);// _size = str._size;// _capacity = str._capacity;//}string(const string& str):_str(nullptr),_size(0),_capacity(0){string tmp(str._str);swap(tmp);}void swap(string& tmp){std::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);}
6、赋值重载函数
赋值重载函数需要返回的是string& 类型,主要原因是返回引用就可以实现连续的赋值,另外就是传引用更高效,不用进行拷贝直接返回。
string& operator=(const string& s){if (this != &s){delete[] _str;_str = new char[s._size + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}return *this;}
有了上面的思路,赋值重载函数也不用自己去开空间自已进行内存拷贝。而是可以直接进行传值传参,传值传参时实参传递给形参这一过程会调用拷贝构造函数,拷贝完成之后,交换即可。所以复制重载函数可以改进成下面的形式:
string& operator=(string tmp){swap(tmp);return *this;}
7、运算符重载
1、string中支持使用方括号来访问string中的字符,其实内部的逻辑十分简单;
//读写形式char& operator[](size_t n){return _str[n];}//只读形式const char& operator[](size_t n)const{return _str[n];}
2、string支持+=运算符,返回+=之后的对象;
string& operator+=(const char* str){size_t len = strlen(str);reserve(_size+len);memcpy(_str + _size, str, len + 1);/* for (size_t i = 0; i < len; i++){_str[_size + i] = str[i];}*/_size += len;/*_str[_size] = '\0';*/return *this;}
3、append() 可以直接复用operator+=();
string& append(const char* str){operator+=(str);return *this;}
4、push_back(),在尾部插入一个字符就更容易了,在_size位置直接写入,_size++.不过事先要检查容量。
void push_back(char ch){reserve(_size + 1);_str[_size++] = ch;}