C++要笑着学:模拟实现string类
🤣 爆笑教程 👉 《C++要笑着学》 👈 火速订阅 🔥
💭 写在前面
本章将正式介绍深浅拷贝,在模拟实现 string 的同时带着去理解深浅拷贝。我们模拟实现 string类不是为了造更好的轮子,而是为了去学习它,理解它的本质!你自己造一次,心里会更清楚,也有利于加深对 string 的理解。
Ⅰ. 深浅拷贝
0x00 引入:
我们先来实现 string 的构造和析构:
💬 string.h
namespace chaos { // 命名空间class string { public:string(const char* str) { // ...}~string() { // ...}private:char* _str;};}
这里为了和原有的 string 进行区分,我们搞一个命名空间给它们括起来。
❓ 思考一个问题,构造函数能不能这样初始化呢?
string(char* str): _str(str) {}
这是不行的,因为你初始化这个 string 时,比如我们通常情况会这么写:
void test_string1() {string s1("hello world");}
这是一个常量字符串,退一万步来讲,就算它不是常量字符串,它也是一个指针,
是不能被修改的,那我们后面要实现修改、插入删除,怎么扩容嘛?
你就只能对堆上的空间扩容了,所以是不能这么写的!那该怎么写呢?
💬 我们可以这么写:
string(const char* str) : _str(new char[strlen(str) + 1]) { // 开strlen大小的空间 strcpy(_str, str);}
值得注意的是,这里要 strlen(str) + 1,因为 strlen 算的是有效字符的长度,没算 \0 。
💬 然后我们实现析构,用 new[] 对应的 delete[] 来析构:
~string() { delete[] _str; // 释放空间_str = nullptr; // 置空}
我们来测试一下:
💬 string.h
#include using namespace std;namespace chaos {class string {public:string(const char* str): _str(new char[strlen(str) + 1]) {strcpy(_str, str);}~string() {delete[] _str;_str = nullptr;}private:char* _str;};void test_string1() {string s1("hello world");}}
💬 test.h
#include "string.h"int main(void){chaos::test_string1();return 0;}
🚩 运行结果如下:
💬 此时我们改一下测试用例 test_string1,如果我们要用 s1 拷贝构造一下 s2:
void test_string1() {string s1("hello world");string s2(s1);}
🚩 运行结果如下:
🔑 详细解析:
❓ 如何解决这样的问题呢?
我们 s2 拷贝构造你 s1,本意并不是想跟你指向一块空间!
我们的本意是想让 s2 有一块自己的空间,并且能内容是 s1 里的 hello world
所以这里就涉及到了深浅拷贝的问题,我们下面就来探讨一下深浅拷贝的问题。
0x01 深浅拷贝问题
举个最简单的例子 —— 拷贝就像是在抄作业!
浅拷贝:直接无脑照抄,连名字都不改。
(直接把内存无脑指过去)
深拷贝:聪明地抄,抄的像是我自己写的一样。
(开一块一样大的空间,再把数据拷贝下来,指向我自己开的空间)
浅拷贝就是原封不动地把成员变量按字节依次拷贝过去,
深拷贝就是进行深一个层次的拷贝,不是直接拷贝,而是拷贝你指向的空间。
0x02 拷贝构造的实现
我们之前实现日期类的时候,用自动生成的拷贝构造(浅拷贝)是可以的,
所以当时我们不用自己实现拷贝构造,让它默认生成就足够了。
但是像 string 这样的类,它的拷贝构造我们不得不亲自写。
💬 string 的拷贝构造:
/* s2(s1) */string(const string& s): _str(new char[strlen(s._str) + 1]) {strcpy(_str, s._str);}
🔍 我们监视看一下效果:
0x03 赋值的深拷贝
💬 现在有一个 s3,如果我们想把 s3 赋值给 s1:
void test_string1() {string s1("hello world");string s2(s1);string s3("pig");s1 = s3;}
如果你不自己实现赋值,就和之前一样,会是浅拷贝,也会造成崩溃:
所以,我们仍然需要自己实现一个 operator= ,实现思路如下:
💬 代码实现 operator=
/* s1 = s3 */string& operator=(const string& s) { if (this != &s) { // 防止自己给自己赋值delete[] _str; // 释放原有的空间_str = new char[strlen(s._str) + 1]; // 开辟新的空间strcpy(_str, s._str); // 把s3的值赋给s1}return *this;}
🔑 代码解析:
根据我们的实现思路,首先释放原有空间,然后开辟新的空间,
最后把 s3 的值赋值给 s1。为了防止自己给自己赋值,我们可以判断一下。
这时我们还要考虑一个难以发现的问题,如果 new 失败了怎么办?
抛异常!抛异常!抛异常!
失败了没问题,也不会走到 strcpy,但问题是我们已经把原有的空间释放掉了,
神不知鬼不觉地,走到析构那里二次释放可能会炸,所以我们得解决这个问题!
⚡ 我们可以试着把释放原有空间的步骤放到后面:
/* s1 = s3 */string& operator=(const string& s) {if (this != &s) { // 防止自己给自己赋值char* tmp = new char[strlen(s._str) + 1]; // 开辟新的空间到tmp中strcpy(tmp, s._str); // 把s3的值赋给 tmpdelete[] _str; // 释放原有的空间_str = tmp; // 把tmp的值赋给 s1}return *this;}
🔑 代码解析:
这样一来,就算是动态内存开辟失败了,我们也不用担心出问题了。
这是更标准的实现方式,我们先去开辟空间,放到临时变量 tmp 中,
tmp 没有翻车,再去释放原有的空间,最后再把 tmp 的值交付给 s1,
这是非常保险的,有效避免了空间没开成还把 s1 空间释放掉的 "偷鸡不成蚀把米" 的事发生。
Ⅱ. string 的实现
0x00 引入
刚才我们为了方便讲解深浅拷贝的问题,有些地方所以没有写全,
是没有考虑增删查改的问题的,所以我们现在要增加一些成员:
private:char* _str;size_t _size;size_t _capacity; // 有效字符的空间数,不算\0
0x01 成员函数 _size 和 _capacity
💬 加上 _size 和 _capacity 后,在刚才实现的 string 基础上修改完善:
#include using namespace std;namespace chaos {class string {public:string(const char* str) : _size(strlen(str)) , _capacity(_size) {_str = new char[_capacity + 1]; // 多开一个空间给\0strcpy(_str, str);}/* s2(s1) */string(const string& s): _size(s._size), _capacity(s._capacity) {_str = new char[_capacity + 1];strcpy(_str, s._str);}/* s1 = s3 */string& operator=(const string& s) {if (this != &s) { // 防止自己给自己赋值char* tmp = new char[s._capacity + 1]; // 开辟新的空间到tmp中strcpy(tmp, s._str); // 把s3的值赋给 tmpdelete[] _str; // 释放原有的空间_str = tmp; // 把tmp的值赋给 s1_size = s._size;_capacity = s._capacity;}return *this;}~string() {delete[] _str; _str = nullptr;_size = _capacity = 0;}private:char* _str;size_t _size;size_t _capacity; // 有效字符的空间数,不算\0};}
为了减少 strlen 的次数,我们在初始化列表里只处理 _size 和 _capacity。
0x02 c_str() 的实现
📚 c_str() 返回的是C语言字符串的指针常量,是可读不写的。
💬 c_str 的实现:
/* 返回C格式字符串:c_str */const char* c_str() const {return _str;}
const char*,因为是可读不可写的,所以我们需要用 const 修饰。
c_str 返回的是当前字符串的首字符地址,这里我们直接 return _str 即可实现。
我们来测试一下:
void test_string1() {string s1("hello world");string s2;cout << s1.c_str() << endl;}
🚩 运行结果如下:
(c_str 是认 \0 的,下面我们探讨不带参全缺省值给什么值的时候需要知道这个点)
0x03 全缺省构造函数
我们还要考虑不带参的情况,比如下面的 s2:
void test_string1() {string s1("hello world"); // 带参string s2; // 不带参}
💬 不带参初始化:
string(): _str(new char[1]), _size(0), _capacity(0) {_str[0] = '\0';}
这里我们开一个空间给 \0,既然都这么写了,我们不如直接在缺省值上动手脚:
string(const char* str = ""): _size(strlen(str)) , _capacity(_size) {_str = new char[_capacity + 1]; // 多开一个空间给\0strcpy(_str, str);}
一般的类都是提供全缺省的,值得注意的是,这里缺省值给的是 " "
有人看到指针 char* 就突发恶疾,这里缺省值就忍不住想给个空 nullptr:
string(const char* str = nullptr)
不能给!给了就崩。因为 strlen 是不会去检查空的,它是去找 \0 ,
void test_string2() {string s1("hello world");string s2;cout << s1.c_str() << endl;cout << s2.c_str() << endl;}
也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。
所以我们这里给的是一个空的字符串 " ",常量字符串默认就带有 \0,这样就不会出问题:
string(const char* str = "")
0x04 size() 和 operator[] 的实现
💬 size() 的实现:
size_t size() const {return _size;}
size() 只需要返回成员函数 _size 即可,考虑到不需要修改,我们加上 const。
💬 operator[] 的实现:
/* operator[] */char& operator[](size_t pos) {return _str[pos]; // 返回字符串对应下标位置的元素}
直接返回字符串对应下标位置的元素,
因为返回的是一个字符,所以我们这里引用返回 char。
我们来测试一下,遍历整个字符串,这样既可以测试到 size() 也可以测试到 operator[] :
void test_string1() {string s1("hello world");string s2;for (size_t i = 0; i < s1.size(); i++) {cout << s1[i] << " ";}cout << endl;}
🚩 运行结果如下:
我们再来测试一下 operator[] 的 "写" 功能:
void test_string1() {string s1("hello world");string s2;s1[0] = 'F';for (size_t i = 0; i < s1.size(); i++) {cout << s1[i] << " ";}cout << endl;}
普通对象可以调用,但是 const 对象呢?所以我们还要考虑一下 const 对象。
💬 我们写一个 const 对象的重载版本:
const char& operator[](size_t pos) const {return _str[pos];}
因为返回的是 pos 位置字符的 const 引用,所以可读但不可写。
💬 最后我们还需要考虑一下越界的问题,这里我们使用断言暴力处理一下:
#include ...char& operator[](size_t pos) {assert(pos < _size);return _str[pos];}const char& operator[](size_t pos) const {assert(pos < _size);return _str[pos];}
测试一下效果如何:
void test_string1() {string s1("hello world");s1[30];}
Ⅲ. 实现迭代器
0x00 引入 - 再探迭代器
在上一章中,我们首次讲解迭代器,为了方便理解,我们当时解释其为像指针一样的类型。
实际上,有没有一种可能,它就是一种指针呢?
遗憾的是,迭代器并非指针,而是类模板。 只是它表现地像指针,模拟了指针的部分功能。
0x01 迭代器的实现
实际上迭代器的实现非常简单,它就是一个 char* 的指针罢了(但也不一定)。
后面我们讲解 list 的时候它又™不是指针了,又是自定义类型了。如何评价?
我的评价是 —— 似是而非。
它是一个像指针的东西,有可能是指针有可能不是指针。
💬 实现迭代器的 begin() 和 end() :
typedef char* iterator;iterator begin() {return _str; // 返回第一个字符位置}iterator end() {return _str + _size; // 返回最后一个数据的下一个位置}
💬 我们来测试一下:
void test_string2() {string s1("hello world");// 迭代器写string::iterator it = s1.begin();while (it != s1.end()) {*it += 1;it++;}// 迭代器读it = s1.begin(); // 重置起点while (it != s1.end()) {cout << *it << " ";it++;}}
🚩 运行结果如下:
0x02 const 迭代器的实现
我们知道,const 迭代器就是可以读但是不可以写的迭代器。
💬 const 迭代器:
typedef const char* const_iterator;const_iterator begin() const {return _str; // 返回第一个字符位置}const_iterator end() const {return _str + _size; // 返回最后一个数据的下一个位置}
这里用 const 修饰,意味着解引用时可以读但不可以写。
0x03 再度思考迭代器
它的底层是连续地物理空间,给原生指针++解引用能正好贴合迭代器的行为,就能做到遍历。
但是对于链表和树型结构来说,迭代器的实现就没有这么简单了。
但是,强大的迭代器通过统一的封装,无论是树、链表还是数组……
它都能用统一的方式遍历,这就是迭代器的优势,也是它的强大之处。
0x04 再探范围 for
上一章讲 string 类对象的遍历时,我们讲的第三种方式就是范围 for,回忆一下 ——
(五毛特效)
我们上一章提到过,我们现在就来演示一下范围 for 的实现:
for (auto e : s1) {cout << e << " ";}cout << endl;
你会发现根本就不需要自己实现,你只要把迭代器实现好,范围 for 直接就可以用。
范围 for 的本质是由迭代器支持的,编译时范围 for 会被替换成迭代器。
这么一看,又是自动加加,又是自动判断结束的范围 for,好像也没那么回事儿。
📌 注意事项:
它的替换是认 begin 和 end 的,我们可以试着把我们实现的迭代器 begin 的 b 改成大写 B 试试:
typedef char* iterator;iterator Begin() {return _str; }iterator end() {return _str + _size; }
void test_string2() {string s1("hello world");string::iterator it = s1.Begin();while (it != s1.end()) {*it += 1;it++;}it = s1.Begin(); // 重置起点while (it != s1.end()) {cout << *it << " ";it++;} for (auto e : s1) {cout << e << " ";}cout << endl;}
迭代器是可以正常用的,但是范围 for 就寄了。
因为它是按迭代器固定的名称去替换的,begin 和 end,
如果你自己实现迭代器时没有按固定的规范去实现,
比如 begin 取名为 start,那范围 for 就不支持了。
Ⅳ. string的增删查改
0x00 reserve() 的实现
💬 我们先实现一下 reserve 增容:
/* 增容:reverse */void reserve(size_t new_capacity) {if (new_capacity > _capacity) {char* tmp = new char[new_capacity + 1]; // 开新空间strcpy(tmp, _str); // 搬运delete[] _str; // 释放原空间_str = tmp; // 没问题,递交给_str_capacity = new_capacity; // 更新容量}}
这里可以检查一下是否真的需要增容,万一接收的 new_capacity 比 _capacity 小,就不动。
这里我们之前讲数据结构用的是 realloc,现在我们熟悉熟悉用 new,
还是用申请新空间、原空间数据拷贝到新空间,再释放空间地方式去扩容。
我们的 _capacity 存储的是有效字符,没算 \0,所以这里还要 +1 为 \0 开一个空间。
0x01 push_back() 的实现
💬 push_back:
/* 字符串尾插:push_back */void push_back(char append_ch) {if (_size == _capacity) { // 检查是否需要增容reserve(_capacity == 0 ? 4 : _capacity * 2); }_str[_size] = append_ch;// 插入要追加的字符_size++;_str[_size] = '\0'; // 手动添加'\0'}
首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数,
参数传递可以用三目操作符,防止容量是0的情况,0乘任何数都是0从而引发问题的情况。
然后在 \0 处插入要追加的字符 append_ch,然后 _size++ 并手动添加一个新的 \0 即可。
我们来测试一下效果如何:
void test_string4() {string s1("hello world");cout << s1.c_str() << endl;s1.push_back('!');cout << s1.c_str() << endl;s1.push_back('A');cout << s1.c_str() << endl;}
🚩 运行结果如下:
0x02 append() 的实现
💬 append:
/* 字符串追加:append */void append(const char* append_str) {size_t len = strlen(append_str); // 计算要追加的字符串的长度if (_size + len > _capacity) {// 检查是否需要增容reserve(_size + len);}strcpy(_str + _size, append_str); // 首字符+大小就是\0位置_size += len; // 更新大小}
append 是追加字符串的,首先我们把要追加的字符串长度计算出来,
然后看容量够不够,不够我们就交给 reserve 去扩容,扩 _size + len,够用就行。
这里我们甚至都不需要用 strcat,因为它的位置我们很清楚,不就在 _str + _size 后面插入吗。
用 strcat 还需要遍历找到原来位置的 \0,太麻烦了。
0x03 operator+= 的实现
这就是我们一章说的 "用起来爽到飞起" 的 += ,因为字符和字符串都可以用 += 去操作。
所以我们需要两个重载版本,一个是字符的,一个是字符串的。
我们不需要自己实现了,直接复用 push_back 和 append 就好了。
💬 operator+=
/* operator+= */string& operator+=(char append_ch) {push_back(append_ch); // 复用push_backreturn *this;}string& operator+=(const char* append_str) {append(append_str); // 复用appendreturn *this;}
测试一下看看:
void test_string5() {string s1("hello world");cout << s1.c_str() << endl;s1 += '!';cout << s1.c_str() << endl;s1 += "this is new data";cout << s1.c_str() << endl;}
🚩 运行结果如下:
0x04 insert() 的实现
💬 insert:字符
/* 插入:insert */string& insert(size_t pos, char append_ch) {assert(pos = (int)pos) {//_str[end + 1] = _str[end];//end--;//}size_t end = _size + 1;while (end > pos) {_str[end] = _str[end - 1];end--;}// 插入_str[pos] = append_ch;_size++;return *this;}
💬 insert:字符串
string& insert(size_t pos, const char* append_str) {assert(pos _capacity) {reserve(_size + len);}// 向后挪动数据size_t end = _size + len;while (end > pos + len) {_str[end] = _str[end - len];end--;}// 插入strncpy(_str + pos, append_str, len);_size += len;return *this;}
测试一下:
void test_string6() {string s1("hello world");cout << s1.c_str() << endl;s1.insert(0, 'X');cout << s1.c_str() << endl;s1.insert(0, "hahahaha");cout << s1.c_str() << endl;}
🚩 运行结果如下:
insert 都实现了,那 push_back 和 append 直接复用,岂不美哉?
⚡ 修改 push_back 和 append:
/* 字符串尾插:push_back */void push_back(char append_ch) {//if (_size == _capacity) { // 检查是否需要增容//reserve(_capacity == 0 ? 4 : _capacity * 2); //}//_str[_size] = append_ch;// 插入要追加的字符//_size++;//_str[_size] = '\0'; // 手动添加'\0'insert(_size, append_ch);}/* 字符串追加:append */void append(const char* append_str) {//size_t len = strlen(append_str); // 计算要追加的字符串的长度//if (_size + len > _capacity) {// 检查是否需要增容//reserve(_size + len);//}//strcpy(_str + _size, append_str); // 首字符+大小就是\0位置//_size += len; // 更新大小insert(_size, append_str);}
测试一下 push_back 和 append,和复用它们两实现的 operator+= 有没有问题:
void test_string4() {string s1("hello world");cout << s1.c_str() << endl;s1.push_back('!');cout << s1.c_str() << endl;s1.push_back('A');cout << s1.c_str() << endl;s1.append("this is new data");} void test_string5() {string s1("hello world");cout << s1.c_str() << endl;s1 += "!";cout << s1.c_str() << endl;s1 += "this is new data";cout << s1.c_str() << endl;}
🚩 运行结果如下:
0x05 resize() 的实现
我们为了扩容,先实现了 reverse,现在我们再顺便实现一下 resize。
这里再提一下 reverse 和 resize 的区别:
resize 分给初始值和不给初始值的情况,所以有两种:
他们也是这么实现的。
但是我们上面讲构造函数的时候说过,我们可以使用全缺省的方式,这样就可以二合一了。
resize 实现的难点是要考虑种种情况,我们来举个例子分析一下:
如果欲增容量比 _size 小的情况:
因为标准库是没有缩容的,所以我们实现的时候也不考虑去缩容。我们可以加一个 \0 去截断。
如果预增容量比 _size 大的情况:
resize 是开空间 + 初始化,开空间的工作我们就可以交给已经实现好的 reserve,
然后再写 resize 的初始化的功能,我们这里可以使用 memset 函数。
💬 resize:
/* resize */void resize(size_t new_capacity, char init_ch = '\0') {// 如果欲增容量比_size小if (new_capacity _capacity) { reserve(new_capacity);}// 起始位置,初始化字符,初始化个数memset(_str + _size, init_ch, new_capacity - _size);_size = _capacity;_str[_size] = '\0';}}
0x06 find() 的实现
💬 find:查找字符
/* find */size_t find(char aim_ch) {for (size_t i = 0; i < _size; i++) {if (aim_ch == _str[i]) { // 找到了return i; // 返回下标}}// 找不到return npos;}
遍历整个字符串,找到了目标字符 aim_ch 就返回对应的下标。
如果遍历完整个字符串都没找到,就返回 npos(找到库的来)。
💬 这个 npos 我们可以在成员变量中定义:
... private:/* 成员变量 */char* _str;size_t _size;size_t _capacity; // 有效字符的空间数,不算\0 public:static const size_t npos;};/* 初始化npos */const size_t string::npos = -1; // 无符号整型的-1,即整型的最大值。...}
💬 find:查找字符串
size_t find(const char* aim_str, size_t pos = 0) {const char* ptr = strstr(_str + pos, aim_str);if (ptr == nullptr) {return npos;}else {return ptr - _str; // 减开头}}
这里我们可以用 strstr 去找子串,如果找到了,返回的是子串首次出现的地址。如果没找到,返回的是空。所以我们这里可以做判断,如果是 nullptr 就返回 npos。如果找到了,就返回对应下标,子串地址 - 开头,就是下标了。
0x07 erase() 的实现
💬 erase:
/* 删除:erase */string& erase(size_t pos, size_t len = npos) {assert(pos = _size) {_str[pos] = '\0'; // 放置\0截断_size = pos;}else {strcpy(_str + pos, _str + pos + len);_size -= len;}return *this;}
测试一下:
void test_string7() {string s1("hello world");cout << s1.c_str() << endl;s1.erase(5, 2); // 从第五个位置开始,删两个字符cout << s1.c_str() << endl;s1.erase(5, 20); // 从第五个位置开始,删完cout << s1.c_str() << endl;}
🚩 运行结果如下:
Ⅴ. 传统写法和现代写法
0x00 拷贝构造的传统写法
对于深拷贝,传统写法就是本本分分分地去完成深拷贝。
💬 我们刚才实现的方式,用的就是传统写法:
/* 拷贝构造函数:s2(s1) */string(const string& s) // 拷贝构造必须使用引用传参,一般用const修饰 : _size(s._size) // 将s1的size给给s2, _capacity(s._capacity) // 将s1的capacity给给s2{_str = new char[_capacity + 1]; // 开辟空间strcpy(_str, s._str); // 将s1字符串给给s2}
这就是传统写法,非常的老实。
0x01 拷贝构造的现代写法
现在我们来介绍一种现代写法,它和传统写法本质工作是一样的,即完成深拷贝。
现代写法的方式不是本本分分地去按着 Step 一步步干活,而是 "投机取巧" 地去完成深拷贝。
💬 直接看代码:(为了方便讲解,我们暂不考虑 _size 和 _capacity)
// 现代写法string(const string& s): _str(nullptr) // 为tmp置空做准备{string tmp(s._str); swap(_str, tmp._str); // 交换}
现代写法的本质就是复用了构造函数。
我想拷贝,但我又不想自己干,我把活交给工具人 swap 来帮我干。妙啊!资本家看了都说好!
❓ 我们为什么要在初始化列表中,给 _str 个空指针:
string(const string& s): _str(nullptr)
我们可以设想一下,如果我们不对他进行处理,那么它的默认指向会是个随机值。
这样交换看上去没啥问题,确实能完成深拷贝,但是会引发一个隐患!
tmp 是一个局部对象,我们把 s2 原来的指针和 tmp 交换了,那么 tmp 就成了个随机值了。
tmp 出了作用域要调用析构函数,对随机值指向的空间进行释放,怎么释放?
都不是你自己的 new / malloc 出来的,你还硬要对它释放,就可能会引发崩溃。
但是 delete / free 一个空,是不会报错的,因为会进行一个检查。
所以是可以 delete 一个空的,我们这里初始化列表中把 nullptr 给 _str,
是为了交换完之后, nullptr 能交到 tmp 手中,这样 tmp 出了作用域调用析构函数就不会翻车了。
🐞 我们来看看效果如何:
💬 如果还是不放心,我们还可以在析构函数那进行一个严格的检查:
/* 析构函数 */~string() {if (_str != nullptr) {delete[] _str;_str = nullptr;}_size = _capacity = 0;}
0x02 赋值重载的现代写法
💬 传统写法:
/* 赋值重载:s1 = s3 */string& operator=(const string& s) {if (this != &s) { // 防止自己给自己赋值 char* tmp = new char[s._capacity + 1]; // Step1:先在tmp上开辟新的空间strcpy(tmp, s._str); // Step2:把s3的值赋给tmpdelete[] _str; // Step3:释放原有的空间_str = tmp; // Step4:把tmp的值赋给s1// 把容量和大小赋过去_size = s._size; _capacity = s._capacity;}return *this; // 结果返回*this}
传统写法,全都自己干,自己开空间自己拷贝数据。
💬 现代写法:复用拷贝构造
/* 赋值重载:s1 = s3 */string& operator=(const string& s) {if (this != &s) {string tmp(s); // 复用拷贝构造swap(_str, tmp._str);}return *this;}
我们先通过 s3 拷贝构造出 tmp,这样 tmp 就是 _str 的工具人了。
tmp 里的 "pig" ,s1 看的简直是垂涎欲滴,我们让 tmp 和 s1 交换一下
交换完之后,正好让 tmp 出作用域调用析构函数,属实是一石二鸟的美事。
把 tmp 压榨的干干净净,还让 tmp 帮忙把屁股擦干净(释放空间)。
⚡ 还有更简洁的写法:
/* 赋值重载:s1 = s3 */string& operator=(string s) {swap(_str, s._str);return *this;}
和上面的写法本质是一样的。这种写法不用引用传参,它利用了拷贝构造。
这里的形参 s 就充当了 tmp,s 就是 s3 的拷贝,再把 s1 和 s 交换。简直是物尽其用!
📌 注意:但是这种写法也有小缺点,可能会导致自己给自己赋值时地址被换。
你会发现我们这里没有加个 if 去判断自己给自己赋值的问题了。
因为这里没办法判断自己给自己赋值了。之前 s 就是 s3,this 就是 s1。
现在 this 还是 s1,但是 s 已经不是 s3 了,所以判断不到自己
if (this != &s) ?????? 👆 👆 s1 s1
所以这里加上 if 判断也没用。但是其实也没太大问题,谁会自己给自己赋值啊。
0x03 整体代码改进
我们现在再去考虑 _size 和 _capacity,我们来把之前写的传统写法都改成现代写法。
💬 拷贝构造函数:s2(s1)
/* 拷贝构造函数:s2(s1) */string(const string& s): _str(nullptr) // 为tmp置空做准备, _size(0), _capacity(0){string tmp(s._str);swap(_str, tmp._str);swap(_size, tmp._size);swap(_capacity, tmp._capacity);}
💬 赋值重载函数:s1 = s3
/* 赋值重载:s1 = s3 */string& operator=(string s) {swap(_str, s._str);swap(_size, s._size);swap(_capacity, s._capacity);return *this;}
这里也是进行交换的,真是跟 tmp 交换改成了跟 s 交换。
我们不如写一个 Swap 函数:
void Swap(string& s) {swap(_str, s._str);swap(_size, s._size);swap(_capacity, s._capacity);}
这样就很简单了 ——
/* 拷贝构造函数:s2(s1) */string(const string& s): _str(nullptr) // 为tmp置空做准备, _size(0), _capacity(0){string tmp(s._str);Swap(tmp); // this->Swap(tmp);}/* 赋值重载:s1 = s3 */string& operator=(string s) {Swap(s);return *this;}
0x04 总结
现代写法在 string 中体现的优势还不够大,因为好像和传统写法差不多。
但是到后面我们实现 vector、list 的时候,你会发现现代写法的优势真的是太大了。
现代写法写起来会更简单些,比如如果是个链表,传统写法就不是 strcpy 这么简单的了,
你还要一个一个结点拷贝过去,但是现代写法只需要调用 swap 交换一下就可以了。
现代写法更加简洁,只是在 string 这里优势体现的不明显罢了,我们后面可以慢慢体会。
Ⅵ. operator 运算符重载
0x00 引入
学日期类的时候我们就说过,我们只需实现 < 和 ==,剩下的都可以复用解决。
0x00 operator<
💬 我们在全局实现:
/* s1 < s2*/bool operator<(const string& s1, const string& s2) {size_t i1 = 0, i2 = 0;while (i1 < s1.size() && i1 < s2.size()) {if (s1[i1] s2[i2]) {return false;}else {i1++;i2++;}}return i2 < s2.size() ? true : false;}
当然,我们还可以实现的更简单些,直接用 strcmp 偷个懒:
/* s1 < s2*/bool operator<(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) < 0;}
0x01 operator=
💬 全局作用域下:
/* s1 == s2 */bool operator==(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) == 0;}
0x02 剩下的直接复用
💬 operator<=
/* s1 <= s2 */bool operator<=(const string& s1, const string& s2) {return s1 < s2 || s1 == s2;}
💬 operator>
/* s1 > s2 */bool operator>(const string& s1, const string& s2) {return !(s1 <= s2);}
💬 operator>=
/* s1 >= s2 */bool operator>=(const string& s1, const string& s2) {return !(s1 < s2);}
💬 operator!=
/* s1 != s2 */bool operator!=(const string& s1, const string& s2) {return !(s1 == s2);}
Ⅶ. 流插入和流提取
0x00 引入
我们当时实现日期类的流插入和流提取时,也详细讲过这些,当时讲解了友元。
在友元那一章我们说过 "占参问题" ,这里就不再多做解释了。
如需复习猛戳 👇
【C++要笑着学】友元 | 初始化列表 | 关键字explicit | 静态成员static | 内部类
如果我们重载成成员函数,第一个位置就会被隐含的 this 指针占据。
这样实现出来的流插入必然会不符合我们的使用习惯,所以我们选择在全局实现。
在全局里不存在隐含的 this 指针了。
0x01 operator<< 的实现
💬 operator<<
// cout << s1 → operator<<(cout, s1)ostream& operator<<(ostream& out, const string& s) {//for (auto ch : s) {//out << ch;//}for (size_t i = 0; i < s.size(); i++) {out << s[i];}return out;}
0x02 operator>> 的实现
💬 operator>>
// cin >>istream& operator<<(istream& in, string& s) {char ch = in.get();while (ch == '\n') {s += ch;ch = in.get();}return in;}
Ⅷ. 完整代码
#include #include using namespace std;namespace chaos{class string {public:/* 构造函数 */string(const char* str = ""): _size(strlen(str)) // 计算出字符串str的大小, _capacity(_size) { // 初始容量等于字符串大小_str = new char[_capacity + 1]; // 开辟一块 "容量+1" 大小的空间 (_capacity存的是有效字符)strcpy(_str, str); // 将传入的字符串str复制到 _str中}void Swap(string& tmp) {swap(_str, tmp._str);swap(_size, tmp._size);swap(_capacity, tmp._capacity);}/* 拷贝构造函数:s2(s1) string(const string& src): _size(src._size) // 拷贝string大小, _capacity(src._capacity) { // 拷贝string容量// 拷贝string内容_str = new char[src._capacity + 1]; // 开辟一块和src相同容量的空间strcpy(_str, src._str); // 将src中的_str内容拷贝到自己的_str中}*/string(const string& src): _str(nullptr), _size(0), _capacity(0) {string tmp(src._str); // 拷贝构造一个srcSwap(tmp);// 现代写法:交换}/* 赋值重载:s1 = s3string& operator=(const string& src) {// 防止自己跟自己赋值if (this != &src) {// 1. 暂时用tmp开辟一块相同的空间char* tmp = new char[src._capacity + 1];// 2. 把src的值复制给tmpstrcpy(tmp, src._str);// 3. 释放this原空间delete[] _str;// 4. 没翻车,把tmp交付给_src_str = tmp;_size = src._size;_capacity = src._capacity;}return *this;}string& operator=(const string& src) {// 防止自己跟自己赋值if (this != &src) {string tmp(src); // 复用拷贝构造Swap(tmp);}return *this;}*/string& operator=(string src) {Swap(src); // 正好调用拷贝构造,不如让形参充当tmpreturn *this;}/* 返回C格式的字符串:c_str */const char* c_str() const {return _str;}/* 求字符串大小:size() */size_t size() const {return _size;}/* operator[] */char& operator[](size_t pos) {assert(pos < _size);return _str[pos]; // 返回字符串对应下标位置的元素}const char& operator[](size_t pos) const {assert(pos _capacity) { // 检查是否真的需要扩容char* tmp = new char[new_capacity + 1]; // 开空间strcpy(tmp, _str); // 先搬运数据到tmp_str = tmp; // 没翻车,递交给_str_capacity = new_capacity; // 更新容量}}/* 字符尾插:push_back() */void push_back(char append_ch) {/*if (_size == _capacity) { // 检查是否需要扩容reserve(_capacity == 0 ? 4 : _capacity * 2); // 首次给4,其他情况默认扩2倍}_str[_size] = append_ch; // 插入要追加的字符_size++; _str[_size] = '\0'; // 手动添加'\0'*/insert(_size, append_ch);}/* 字符串追加:append() */void append(const char* append_str) {/*size_t len = strlen(append_str); // 计算出要追加的字符串的长度if (_size + len > _capacity) { // 检查是否需要扩容reserve(_size + len);}strcpy(_str + _size, append_str); // 首字符+大小,就是'\0'位置_size += len; // 更新大小*/insert(_size, append_str);}/* operator+= */string& operator+=(char append_ch) {push_back(append_ch);return *this;}string& operator+=(const char* append_str) {append(append_str);return *this;}/* insert */string& insert(size_t pos, char append_ch) {assert(pos pos) {_str[end] = _str[end - 1];end--;}// 插入_str[pos] = append_ch;_size++;return *this;}string& insert(size_t pos, const char* append_str) {assert(pos _capacity) { // 检查是否需要增容reserve(_size + len);}// 向后挪动数据size_t end = _size + len;while (end > pos + len) {_str[end] = _str[end - len];end--;}// 插入strncpy(_str + pos, append_str, len);_size += len;return *this;}/* resize */void resize(size_t new_capacity, char init_ch = '\0') {// 如果欲增容量比_size小if (new_capacity _capacity) {reserve(new_capacity);}// 起始位置,初始化字符,初始化个数memset(_str + _size, init_ch, new_capacity - _size);_size = _capacity;_str[_size] = '\0';}}/* find */size_t find(char aim_ch) {for (size_t i = 0; i < _size; i++) {if (aim_ch == _str[i]) {// 找到了return i; // 返回下标}}// 找不到return npos;}size_t find(const char* aim_str, size_t pos = 0) {const char* ptr = strstr(_str + pos, aim_str);if (ptr == nullptr) {return npos;}else {return ptr - _str; // 减开头}}/* 删除:erase */string& erase(size_t pos, size_t len = npos) {assert(pos = _size) {_str[pos] = '\0'; // 放置\0截断_size = pos;}else {strcpy(_str + pos, _str + pos + len);_size -= len;}return *this;}/* 析构函数 */~string() {if (_str != nullptr) {delete[] _str;_str = nullptr;}_size = _capacity = 0;}private:/* 成员变量 */char* _str;size_t _size;size_t _capacity;public:static const size_t npos;};/* 初始化npos */const size_t string::npos = -1; // 无符号整型的-1,即整型最大值/* s1 < s2*/bool operator<(const string& s1, const string& s2) {/*size_t i1 = 0, i2 = 0;while (i1 < s1.size() && i1 < s2.size()) {if (s1[i1] s2[i2]) {return false;}else {i1++;i2++;}}return i2 < s2.size() ? true : false;*/return strcmp(s1.c_str(), s2.c_str()) < 0;}/* s1 == s2 */bool operator==(const string& s1, const string& s2) {return strcmp(s1.c_str(), s2.c_str()) == 0;}/* 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);}/* s1 != s2 */bool operator!=(const string& s1, const string& s2) {return !(s1 == s2);}// cout << s1 → operator<<(cout, s1)ostream& operator<<(ostream& out, const string& s) {/*for (auto ch : s) {out << ch;}*/for (size_t i = 0; i < s.size(); i++) {out <>istream& operator<<(istream& in, string& s) {char ch = in.get();while (ch == '\n') {s += ch;ch = in.get();}return in;}/* 测试用 */void test_string1() {string s1("hello world");string s2(s1);cout << s1.c_str() << endl;cout << s2.c_str() << endl;string s3("pig");cout << s3.c_str() << endl;s1 = s3;cout << s1.c_str() << endl;}void test_string2() {string s1("hello world");string s2;for (size_t i = 0; i < s1.size(); i++) {cout << s1[i] << " ";}cout << endl;}void test_string3() {string s1("hello world");string s2;s1[0] = 'F';for (size_t i = 0; i < s1.size(); i++) {cout << s1[i] << " ";}cout << endl;}void test_string4() {string s1("hello world");// 迭代器写string::iterator it = s1.begin();while (it != s1.end()) {*it += 1;it++;}// 迭代器读it = s1.begin(); // 重置起点while (it != s1.end()) {cout << *it << " ";it++;}}void test_string5() {string s1("hello world");cout << s1.c_str() << endl;s1.push_back('!');cout << s1.c_str() << endl;s1.push_back('A');cout << s1.c_str() << endl;}void test_string6() {string s1("hello world");cout << s1.c_str() << endl;s1 += '!';cout << s1.c_str() << endl;s1 += "this is new data";cout << s1.c_str() << endl;}void test_string7() {string s1("hello world");cout << s1.c_str() << endl;s1.insert(0, 'X');cout << s1.c_str() << endl;s1.insert(0, "hahahaha");cout << s1.c_str() << endl;}void test_string8() {string s1("hello world");cout << s1.c_str() << endl;s1.erase(5, 2); // 从第五个位置开始,删两个字符cout << s1.c_str() << endl;s1.erase(5, 20); // 从第五个位置开始,删完cout << s1.c_str() << endl;}}
📜 参考资料
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
. C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.
百度百科[EB/OL]. []. https://baike.baidu.com/.
比特科技. C++[EB/OL]. 2021[2021.8.31]. .
📌 [ 笔者 ] 王亦优
📃 [ 更新 ] 2022.5.6
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
本章完。