⚡【C++要笑着学】(17) 模拟实现 string:深浅拷贝 | string 模拟实现 | 传统写法与现代写法

🔥 订阅量破千的火热 C++ 教程
👉 火速订阅
《C++要笑着学》 

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

   本篇博客全站热榜排名:2 🔥

  • 💭 写在前面本章将正式介绍深浅拷贝,在模拟实现 string 的同时带着去理解深浅拷贝。我们模拟实现 string类不是为了造更好的轮子,而是为了去学习它,理解它的本质!你自己造一次,心里会更清楚,也有利于加深对 string 的理解。

Ⅰ.  深浅拷贝(Deep Copy & Shallow Copy)

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 <iostream>
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的值赋给 tmp
		delete[] _str;                             // 释放原有的空间
		_str = tmp;                                // 把tmp的值赋给 s1
	}

	return *this;
}

🔑 代码解析:

这样一来,就算是动态内存开辟失败了,我们也不用担心出问题了。

这是更标准的实现方式,我们先去开辟空间,放到临时变量 tmp 中,

 tmp 没有翻车,再去释放原有的空间,最后再把 tmp 的值交付给 s1

这是非常保险的,有效避免了空间没开成还把 s1 空间释放掉的 "偷鸡不成蚀把米" 的事发生。

Ⅱ.  模拟实现 string 库

0x00 引入:正式开始实现 string

 刚才我们为了方便讲解深浅拷贝的问题,有些地方所以没有写全,

 是没有考虑增删查改的问题的,所以我们现在要增加一些成员:

private:
	char*  _str;
	size_t _size;
	size_t _capacity;   // 有效字符的空间数,不算\0

0x01 成员函数 _size 和 _capacity

💬 加上 _size _capacity 后,在刚才实现的 string 基础上修改完善:

#include <iostream>
using namespace std;

namespace chaos 
{
	class string {
	public:
	string(const char* str) 
		: _size(strlen(str)) 
		, _capacity(_size) {
		_str = new char[_capacity + 1];     // 多开一个空间给\0
		strcpy(_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的值赋给 tmp
			delete[] _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];     // 多开一个空间给\0
	strcpy(_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 <assert.h>
...

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];
}

Ⅲ. 实现迭代器(Iterator)

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 就寄了。

因为它是按迭代器固定的名称去替换的,beginend

如果你自己实现迭代器时没有按固定的规范去实现,

比如 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_backappend 就好了。

💬 operator+= 

/* operator+= */
string& operator+=(char append_ch) {
	push_back(append_ch);    // 复用push_back
	return *this;
}
string& operator+=(const char* append_str) {
	append(append_str);      // 复用append
	return *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 <= _size);

	// 检查是否需要增容
	if (_size == _capacity) {     
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
			
	// 向后挪动数据
	//size_t end = _size;
	//while (end >= (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 <= _size);
	size_t len = strlen(append_str);

	// 检查是否需要增容
	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, 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_backappend 直接复用,岂不美哉?

⚡ 修改 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

这里再提一下 reverseresize 的区别:

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 <= _size) {
		_str[new_capacity] = '\0';      // 拿斜杠零去截断
		_size = new_capacity;           // 更新大小
	}
	// 欲增容量比_size大
	else {
		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);

	if (len == pos || pos + len >= _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的值赋给tmp
		delete[] _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 就充当了 tmps 就是 s3 的拷贝,再把 s1 s 交换。简直是物尽其用!

📌 注意:但是这种写法也有小缺点,可能会导致自己给自己赋值时地址被换。

你会发现我们这里没有加个 if 去判断自己给自己赋值的问题了。

因为这里没办法判断自己给自己赋值了。之前 s 就是 s3this 就是 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 true;
		} 
		else 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 <iostream>
#include <assert.h>
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);   // 拷贝构造一个src
			Swap(tmp);              // 现代写法:交换
		}
	

		/* 赋值重载:s1 = s3
		string& operator=(const string& src) {
			// 防止自己跟自己赋值
			if (this != &src) {
				// 1. 暂时用tmp开辟一块相同的空间
				char* tmp = new char[src._capacity + 1];
				// 2. 把src的值复制给tmp
				strcpy(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);    // 正好调用拷贝构造,不如让形参充当tmp
			return *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 < _size);
			return _str[pos];
		}

		/* 迭代器 */
		typedef char* iterator;
		iterator begin() { 
			return _str;            // 返回第一个字符的位置
		}
		iterator end() {
			return _str + _size;    // 返回最后一个字符的位置
		}

		/* const迭代器 */
		typedef const char* const_iterator;
		const_iterator begin() const {
			return _str;
		}
		const_iterator end() const {
			return _str + _size;
		}

		/* reserve() */
		void reserve(size_t new_capacity) {
			if (new_capacity > _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 <= _size);

			if (_size == _capacity) {		// 检查是否需要扩容
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			// 向后挪动数据
			size_t end = _size + 1;
			while (end > 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 <= _size);
			size_t len = strlen(append_str);

			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, append_str, len);
			_size += len;

			return *this;
		}

		/* resize */
		void resize(size_t new_capacity, char init_ch = '\0') {
			// 如果欲增容量比_size小
			if (new_capacity <= _size) {
				_str[new_capacity] = '\0';      // 拿斜杠零去截断
				_size = new_capacity;           // 更新大小
			}
			// 欲增容量比_size大
			else {
				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);

			if (len == pos || pos + len >= _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 true;
			}
			else 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 || s1 == s2;
	}

	/* 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);
	}


	/* 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 << s[i];
		}

		return out;
	}

	// cin >>
	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;

	}
}

​​

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.5.6 | 2023.10.8(重制)
❌ [ 勘误 ]   暂无
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]

评论 60
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王平渊

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值