前言:
在之前的学习中,我们已经对string类进行了简单的介绍,大家只要能够正常使用即可。但是在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。因此,接下来我将带领大家手动模拟实现一下。
(一)成员函数
1、构造函数
刚开始时,如果我们要实现构造函数,可能就需要分别实现带参的构造函数和无参的构造函数,但是有没有简单方法可以做到一步到位呢?
💨 因此,为了更加的灵活方便,我们直接把带参的构造函数和无参构造函数集合,形成全缺省的构造函数,这样就省得再去写两个构造函数。
代码如下:
//全缺省的构造函数 //string(const char* str = nullptr) //不可以,对其解引用如果遇到空指针就报错 //string(const char* str = '\0') //类型不匹配,char 不能匹配为指针 //string(const char* str = "\0") //可以 string(const char* str = "") :_size(strlen(str)) { _capacity = _size == 0 ? 5 : _size; _str = new char[_capacity + 1]; strcpy(_str, str); }
2、拷贝构造
编译器默认的实现的是浅拷贝,但是浅拷贝存在问题:
- 如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
因此为了解决上述的问题,可以采用深拷贝解决浅拷贝问题:
- 每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
代码如下:
//深拷贝 // str3(str2) string(const string& STR) :_size(STR._size) , _capacity(STR._capacity) { _str = new char[STR._capacity + 1]; strcpy(_str, STR._str); }
3、赋值重载
注意:
- 当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;
- 当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。
代码如下:
string& operator=(const string& STR) { if (this != &STR) { char* tmp = new char[STR._capacity + 1]; strcpy(tmp, STR._str); delete[] _str; _str = tmp; _size = STR._size; _capacity = STR._capacity; } return *this; }
4、析构函数
析构函数的实现就比较简单,只需将指针所指的空间进行释放并把置空即可(防止野指针) ,最后把剩余的两个成员置为0即可。
代码如下:
//析构函数 ~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; }
(二)容量
1、size()
顾名思义返回字符串的长度(以字符数为单位)
代码如下:
size_t size() const { return _size; }
2、capacity()
返回当前为basic_string分配的存储空间的大小,以字符表示。
代码如下:
size_t capacity() const { return _capacity; }
3、reserve()
表示请求更改容量,使字符串容量适应计划的大小更改为最多 n 个字符。
注意是有效字符,不包含标识字符,而在具体实现的时候,我们在底层多开一个空间给\0。
代码如下:
//扩容操作 void reserve(size_t N) { if (N > _capacity) { char* tmp = new char[N + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = N; } }
4、resize()
其实它的情况大体上可以分为插入数据和删除数据两种情况。
- 1.对于插入数据来说直接调用【reserve】提前预留好空间,然后搞一个for循环将字符ch尾插到数组里面去,最后再在数组末尾插入一个\0标识字符;
- 2.对于删除数据就比较简单了,如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符以外的字符,然后重置一下_size的大小为n即可。
代码如下:
//扩容+初始化 void resize(size_t n, char STR = '\0') { if (n < _size) { // 删除数据--保留前n个 _size = n; _str[_size] = '\0'; } else if (n > _size) { if (n > _capacity) { reserve(n); } size_t end = _size; while (end < n) { _str[end] = STR; end++; } _size = n; _str[_size] = '\0'; } }
5、clear()
顾名思义就是清除字符串,擦除basic_string的内容,该内容变为空字符串(长度为 0 个字符)。
代码如下:
void clear() { _str[0] = '\0'; _size = 0; }
(三)元素访问
1、 operator[]
元素访问操作相对来说用的最多的就是operator[] ;
- 对它进行调用时可能进行的是写操作,也可能进行读操作,所以为了适应const和非const对象,operator[]应该实现两个版本的函数;
- 并且这个函数处理越界访问的态度就是assert直接断言,而at对于越界访问的态度是抛异常。
代码如下:
const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; }
(四)修改
1、 operator+=
追加到字符串,通过在当前值的末尾附加其他字符来扩展
在这里我们只实现添加字符和字符串的操作;
我们可以直接复用【push_back】的操作来实现。
代码如下:
//+= string& operator+=(char STR_1) { push_back(STR_1); return *this; } string& operator+=(const char* STR_2) { append(STR_2); return *this; }
2、append()
追加到字符串, 通过在当前值的末尾附加其他字符来扩展。
- 我们可以直接调用strcpy接口来进行字符串的尾插,但是需要注意一点,那就是【string】类的字符串函数是不会进行自动扩容的,所以我们需要判断一下是否需要进行扩容,在空间预留好的情况下进行字符串的尾插即可实现;
- 其次,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。
代码如下:
//追加字符串 void append(const char* STR) { size_t len = strlen(STR); if (len + _size > _capacity) { reserve(_size + len); } strcpy(_str + _size, STR); _size += len; //insert(_size, STR); }
3、push_back()
注意:
- 首先对于【push_back】有一个特别需要注意的地方就是当容量不够时的扩容操作。如果是一个空对象进行push_back的话,这时如果我们采取的二倍扩容就有问题,因为0*2还是0,所以对于空对象的情况我们应该给他一个初始的capacity值,所以上述构造函数的时候我给成了【5】,其他情况下进行二倍扩容即可;
- 其次,就是在尾插字符之后,要记得进行补【\0】操作,,否则在打印的时候就会有麻烦了。
- 最后跟【append】一样,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。
代码如下:
//尾插操作 void push_back(char STR) { if (_size + 1 > _capacity) { reserve(_capacity * 2); } _str[_size] = STR; ++_size; _str[_size] = '\0'; //insert(_size, STR); }
4、insert()
插入到字符串中,在 pos(或 p)指示的字符之前将其他字符插入
注意:
- 对于【insert】函数,有经常会引出错误的地方,那就是对于while循环里面的操作;
可能很多的小伙伴在while循环里面都是这样写的:_str[end + 1] = _str[end] ,那么这样写有没有问题呢?答案是会出问题的;
- 我们的end是size_t定义的,因为size_t是无符号数,那么-1会被认为是无符号整数,进行隐式类型转换,由于-1的补码是全1,此时就是恒大于0,程序会陷入死循环。所以我们可以不用size_t来定义end,防止发生隐式类型转换;
- 那么是不是只要把【size_t end = _size + len;】中的【end】用 int 定义就可以解决了呢?答案当然不是的 (是不是觉得很坑了呀!!!);
- 因为-1在和size_t定义的pos进行比较时,又会发生隐式类型转换。这是因为比较运算符也是运算符,只要进行运算就有可能出现隐式类型转换,因此此时又可能出现上述那样的情况,-1就又会被转为无符号整型,程序就又陷入死循环;
- 那么有没有解决方法呢?当然是有的,我们只需在比较时将【size_t】的pos强转为【int】类型,此时再去比较就没得问题了;
- 但当我们就想使用size_t类型,通过把【end-1】位置的元素挪到【end】位置上去,在while循环条件的判断位置,我们用end来和pos位置进行比较,end应该大于pos的位置,一旦end=pos我们就跳出循环,这样就可以了。
代码如下:
//插入字符操作 string& insert(size_t pos, char STR_1) { assert(pos < _size); if (_size + 1 > _capacity) { reserve(2 * _capacity); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = STR_1; ++_size; return *this; } //插入字符串 string& insert(size_t pos, const char* STR_2) { assert(pos < _size); size_t len = strlen(STR_2); if (_size + len > _capacity){ reserve(_size + len); } // 挪动数据 size_t end = _size + len; while (end > pos + len - 1) { _str[end] = _str[end - len]; --end; } // 拷贝插入 strncpy(_str + pos, STR_2, len); _size += len; return *this; }
5、erase()
意思很简单,就是从字符串中删除字符
对于删除,思路很简单,分为两种情况下的删除:
- 1.如果当前位置加上要删除的长度大于字符串的长度,即【 pos + len >= _size】,此时的意思即为删除pos之后的所有元素;
- 2.除了上述情况,就是在字符串内正常删除操作。我们只需利用strcpy来进行,将pos+len之后的字符串直接覆盖到pos位置,这样实际上就完成了删除的工作。
注意:
- 对于【npos】这个参数,首先我们知道对于静态成员变量,它的规则是在类外定义,类里面声明,定义时不加static关键字;
- 但如果静态成员变量有const修饰,这时它可以在类内直接进行定义,这样的特性只针对于整型,对于其他类型则是不适用的;
- npos就是const static修饰的成员变量,可以直接在类内进行定义。
代码如下:
//删除操作 string& erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; }
6、swap()
至于交换,这个就没有必要再多说什么了很简单,我相信大家肯定也会这个。
代码如下:
//交换 void swap(string& STR) { std::swap(_str, STR._str); std::swap(_capacity, STR._capacity); std::swap(_size, STR._size); }
(五)字符串操作
1、c_str()
获取等效的 C 字符串,返回指向一个数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示basic_string对象的当前值
代码展示:
const char* c_str() { return _str; }
2、find()
查找字符串中的第一个匹配项, 在basic_string中搜索由其参数指定的序列的第一个匹配项。
对于这个函数不用多说,就是对其进行遍历查找即可。
代码展示:
//查找 size_t find(char STR, size_t pos = 0) { assert(pos < _size); for (size_t i = pos; i < _size; ++i) { if (_str[i] == STR) { return i; } } return npos; } size_t find(const char* STR, size_t pos = 0) { assert(pos < _size); char* p = strstr(_str + pos, STR); if (p == nullptr) { return npos; } else { return p - _str; } }
(六)非成员函数重载
1、relational operators()
basic_string的关系运算符,以ascll码的方式比较大小
这个实现的过程,跟之前日期类的时间如出一辙,基本上都是一样的。
代码如下:
//比较大小 bool operator >(const string& STR) const { return strcmp(_str, STR._str) > 0; } bool operator == (const string & STR)const { return strcmp(_str, STR._str) == 0; } bool operator >= (const string & STR)const { return *this > STR || *this == STR; } bool operator < (const string & STR)const { return !(*this >= STR); } bool operator <= (const string& STR)const { return !(*this > STR); } bool operator!=(const string& STR) const { return !(*this == STR); }
2、operator<<
将字符串插入流, 将符合 str 值的字符序列插入到 os 中。
代码如下:
//operator<< ostream& operator<<(ostream& out, const string& STR) { for (auto e : STR) { out << e; } return out; }
3、operator>>
从流中提取字符串, 从输入流中提取字符串,将序列存储在 str 中,该序列被覆盖(替换 str 的先前值)
注意:
- 流提取是以空格和\n作为间隔标志的 ,而【getline】则是以【\0】就停止。
代码如下:
//operator>> istream& operator>>(istream& in, string& STR) { STR.clear(); char ch = in.get(); //如果输入到缓冲区里的字符串非常非常的长,那么+=就需要频繁的扩容,则效率就会降低 //因此,在这里可以使用开辟一个数组,先将有效数据放入数组中,在进行操作,可有效提高效率 char buff[128]; size_t i = 0; while (ch != ' ' && ch != '\0') { buff[i++] = ch; if (i == 127) //最后得留一个位置给\0 { buff[127] = '\0'; STR += buff; i = 0; } ch = in.get(); } if (i != 0) { buff[i] = '\0'; STR += buff; } return in; }
(七)代码汇总
代码汇总如下:
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; } //全缺省的构造函数 //string(const char* str = nullptr) //不可以,对其解引用如果遇到空指针就报错 //string(const char* str = '\0') //类型不匹配,char 不能匹配为指针 //string(const char* str = "\0") //可以 string(const char* str = "") :_size(strlen(str)) { _capacity = _size == 0 ? 5 : _size; _str = new char[_capacity + 1]; strcpy(_str, str); } //深拷贝 // str3(str2) string(const string& STR) :_size(STR._size) , _capacity(STR._capacity) { _str = new char[STR._capacity + 1]; strcpy(_str, STR._str); } //赋值操作 string& operator=(const string& STR) { if (this != &STR) { // str1 = str1 的情况不满足 /*delete[] _str; _str = new char[s._capaicty + 1]; strcpy(_str, s._str); _size = s._size; _capaicty = s._capaicty;*/ char* tmp = new char[STR._capacity + 1]; strcpy(tmp, STR._str); delete[] _str; _str = tmp; _size = STR._size; _capacity = STR._capacity; } return *this; } //析构函数 ~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; } const char* c_str() { return _str; } const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } size_t size() const { return _size; } size_t capacity() const { return _capacity; } //比较大小 bool operator >(const string& STR) const { return strcmp(_str, STR._str) > 0; } bool operator == (const string & STR)const { return strcmp(_str, STR._str) == 0; } bool operator >= (const string & STR)const { return *this > STR || *this == STR; } bool operator < (const string & STR)const { return !(*this >= STR); } bool operator <= (const string& STR)const { return !(*this > STR); } bool operator!=(const string& STR) const { return !(*this == STR); } //扩容+初始化 void resize(size_t n, char STR = '\0') { if (n < _size) { // 删除数据--保留前n个 _size = n; _str[_size] = '\0'; } else if (n > _size) { if (n > _capacity) { reserve(n); } size_t end = _size; while (end < n) { _str[end] = STR; end++; } _size = n; _str[_size] = '\0'; } } //扩容操作 void reserve(size_t N) { if (N > _capacity) { char* tmp = new char[N + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = N; } } //尾插操作 void push_back(char STR) { if (_size + 1 > _capacity) { reserve(_capacity * 2); } _str[_size] = STR; ++_size; _str[_size] = '\0'; //insert(_size, STR); } //追加字符串 void append(const char* STR) { size_t len = strlen(STR); if (len + _size > _capacity) { reserve(_size + len); } strcpy(_str + _size, STR); _size += len; //insert(_size, STR); } //+= string& operator+=(char STR_1) { push_back(STR_1); return *this; } string& operator+=(const char* STR_2) { append(STR_2); return *this; } //插入字符操作 string& insert(size_t pos, char STR_1) { assert(pos < _size); if (_size + 1 > _capacity) { reserve(2 * _capacity); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = STR_1; ++_size; return *this; } //插入字符串 string& insert(size_t pos, const char* STR_2) { assert(pos < _size); size_t len = strlen(STR_2); if (_size + len > _capacity){ reserve(_size + len); } // 挪动数据 size_t end = _size + len; while (end > pos + len - 1) { _str[end] = _str[end - len]; --end; } // 拷贝插入 strncpy(_str + pos, STR_2, len); _size += len; return *this; } //删除操作 string& erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; } //交换 void swap(string& STR) { std::swap(_str, STR._str); std::swap(_capacity, STR._capacity); std::swap(_size, STR._size); } //查找 size_t find(char STR, size_t pos = 0) { assert(pos < _size); for (size_t i = pos; i < _size; ++i) { if (_str[i] == STR) { return i; } } return npos; } size_t find(const char* STR, size_t pos = 0) { assert(pos < _size); char* p = strstr(_str + pos, STR); if (p == nullptr) { return npos; } else { return p - _str; } } void clear() { _str[0] = '\0'; _size = 0; } //operator<< ostream& operator<<(ostream& out, const string& STR) { for (auto e : STR) { out << e; } return out; } //operator>> istream& operator>>(istream& in, string& STR) { STR.clear(); char ch = in.get(); char buff[128]; size_t i = 0; while (ch != ' ' && ch != '\0') { buff[i++] = ch; if (i == 127) { buff[127] = '\0'; STR += buff; i = 0; } ch = in.get(); } if (i != 0) { buff[i] = '\0'; STR += buff; } return in; }
(八)总结
到此,关于string的模拟实现,在这里我们主要实现的是经常用得到的,对于其他的,我们并没有一一列举。如果后面有机会再给大家展示。
接下来,我们简单总结一下本文:
- 我们从文档的先后顺序入手,依次对各个板块的常用接口进行了模拟实现;
- 大家在上手操作的时候,一定要想明白为什么,做到真正的掌握string类它是非常重要的。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
到此,便于string类的模拟实现便讲解完毕了。希望本文对大家有所帮助,感谢各位的观看!!!