• 【CPP】string 类的模拟实现


    👉前言👈

    因为 string 类的函数接口组合在一起才好玩,所以我们模拟实现 string 类就一组一组函数来模拟实现。为了避免我们模拟实现的 string 类和库里的 string 类冲突,所以我们将自己实现的 string 类封装在命名空间里。

    👉访问和遍历 string 类👈

    首先,string 类是一个可以动态增长的字符数组,其实就相当于是存储字符的顺序表。那么 string 类的成员变量如下方代码:

    namespace Joy
    {
    	class string
    	{
    	public:
    		// 成员函数
    	private:
    		char* _str; // 指向动态开辟的数组
    		size_t _size; // 数组的有效字符个数
    		size_t _capacity; // 数组的容量
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那现在我们就来模拟实现 string 类了。string 类最重要的就是构造函数了,所以我们先实现构造函数。

    string 类的构造函数

    string(const char* str = "")
    {
    	_size = strlen(str);
    	_capacity = _size;
    	_str = new char[_capacity + 1];
    
    	strcpy(_str, str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注:string类的构造函数最好提供空串缺省参数,然后还有多开一个空间来存储\0标识字符,标识字符不算是 string 类的有效字符,给 _size 和 _capacity 都赋好初值后,就借助 strcpy 函数来拷贝字符串了。

    string 类的析构函数

    ~string()
    {
    	delete[] _str;
    	_str = nullptr;
    	_size = _capacity = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    为了 string 类能用起来像数组一样,我们需要提供以下函数接口sizec_str[]运算符重载。

    const char* c_str() const
    {
    	return _str; // 返回数组首元素的地址
    }
    
    size_t size() const
    {
    	return _size;
    }
    
    // 普通对象:可读可写
    char& operator[](size_t pos)
    {
    	assert(pos < _size);
    	return _str[pos];
    }
    
    // const对象:只读
    const char& operator[](size_t pos) const
    {
    	assert(pos < _size);
    	return _str[pos];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    以上实现的函数接口,足以帮我构造一个 string 类对象和访问 string 类对象了,那么现在我们就来测试一下。

    在这里插入图片描述
    这样,访问和遍历 string 类对象的第一种方式就搞定了。那我们现在来实现一下遍历 string 类对象的第二种方式:迭代器iterator

    迭代器可能是指针,也有可能不是指针,所以我们实现 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    那现在我们来测试一下功能。

    在这里插入图片描述
    注:以上的 string 类迭代器是使用指针实现的,但是库里的 string 类的迭代器就有可能不是用指针实现了,比如 string 类的反向迭代器就不是使用指针来实现的了。因为reverse_iterator rit = s.rbegin() rit++会走到s.rend(),而如果使用指针实现,应该it--才能走到s.end(),使用 string 类的反向迭代器不是使用指针来实现的。

    现在我们已经实现了 string 类的正向迭代器,其实范围 for 也实现好了。因为范围 for 的底层原理就是迭代器,编译器会自动将范围 for 替换成迭代器。

    在这里插入图片描述
    如果我们将正向迭代器的代码屏蔽掉,那范围 for 的代码就无法编译通过了。

    👉capacity、resize 和 reserve👈

    size_t capacity() const
    {
    	return _capacity;
    }
    
    void reserve(size_t n)
    {
    	if (n > _capacity)
    	{
    		char* tmp = new char[n + 1];
    		strcpy(tmp, _str);	
    		delete[] _str;
    		_str = tmp;
    
    		_capacity = n;
    	}
    }
    
    void resize(size_t n, char ch = '\0')
    {
    	if (n > _size)
    	{
    		reserve(n);
    		for (size_t i = _size; i < n; i++)
    		{
    			_str[i] = ch;
    		}
    
    		_size = n;
    		_str[_size] = '\0';
    	}
    	else
    	{
    		_str[n] = '\0';
    		_size = n;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    reserve 函数接口说明

    • 如果 n > _capacity,那么就先申请一块 n + 1的空间tmp,然后将数据拷贝到这块空间,_str = tmp,更新容量的大小_capacity = n
    • 如果n <= _capacity,那么就什么都不用做。如果缩容的话,就会造成效率低下。

    resize 函数接口说明

    • n > _size时,调用 reserve 函数调整容量的大小,然后插入字符 ch。
    • n <= _size时,直接_str[_size] = '\0' _size = n,注意:这个过程也不要缩容。

    在这里插入图片描述

    👉string 类插入字符或字符串👈

    我们主要模拟实现最常用的几个插入的接口,目的不是造更好的轮子。那我们现在开干!

    string& push_back(char ch)
    {
    	if (_size == _capacity)
    	{
    		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    		reserve(newCapacity);
    	}
    
    	_str[_size] = ch;
    	++_size;
    	_str[_size] = '\0';
    
    	return *this;
    }
    
    string& append(const char* str)
    {
    	size_t len = strlen(str);
    	if (_size + len > _capacity)
    	{
    		reserve(_size + len);
    	}
    
    	strcpy(_str + _size, str);
    	_size += len;
    
    	return *this;
    }
    
    string& operator+=(char ch)
    {
    	push_back(ch);
    	return *this;
    }
    
    string& operator+=(const char* str)
    {
    	append(str);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    注:不管是 push_back 函数,还是 append 函数,都要考虑是否需要扩容(reverse 函数会多开一个空间存储标识字符'\0'),然后再插入数据,最后更新_size和加上标识字符'\0'。对于operator +=运算符重载就可以赋用 push_back 函数和 append 函数了。

    在这里插入图片描述

    除了以上在尾部的插入,还有在任意位置的插入。

    string& insert(size_t pos, char ch)
    {
    	assert(pos <= _size);
    
    	if (_size == _capacity)
    	{
    		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    		reserve(newCapacity);
    	}
    
    	// 挪动数据
    	/*
    	int end = _size;
    	while (end >= (int)pos)
    	{
    		_str[end + 1] = _str[end];
    		--end;
    	}
    	_str[pos] = ch;
    	++_size;
    	return *this;
    	*/
    
    	size_t end = _size + 1;
    	while (end > pos)
    	{
    		_str[end] = _str[end - 1];
    		--end;
    	}
    	_str[pos] = ch;
    	++_size;
    	return *this;
    }
    
    string& insert(size_t pos, const char* str)
    {
    	assert(pos <= _size);
    
    	int len = strlen(str);
    	if (_size + len > _capacity)
    	{
    		reserve(_size + len);
    	}
    
    	// 挪动数据
    	/*
    	int end = _size;
    	while (end >= (int)pos)
    	{
    		_str[end + len] = _str[end];
    		--end;
    	}
    
    	strncpy(_str + pos, str, len);
    	_size += len;
    	return *this;
    	*/
    
    	size_t end = _size + len;
    	while (end > pos + len - 1)
    	{
    		_str[end] = _str[end - len];
    		--end;
    	}
    
    	strncpy(_str + pos, str, len);
    	_size += len;
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    注:挪动数据就要注意对边界条件的控制,如果控制不会就会造成死循环和越界访问的问题。还有拷贝数据,不能使用 strcpy 函数,strcpy 函数会将\0'拷贝过去。如果是 >=,要将 pos 强转为 int。

    在这里插入图片描述

    👉erase👈

    bool empty() const
    {
    	return _size == 0;
    }
    
    string& erase(size_t pos, size_t len = npos)
    {
    	assert(!empty());
    	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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    注:const static size_t npos = -1npos是 string 类的静态成员变量,其值为 -1。

    erase 函数接口说明

    • len == nps || pos + len >= _size时,就相当于将pos位置及其之后的字符都删掉,所以此时只需要将pos位置的字符改为'\0'并更新_size的值为pos
    • 除此之外,用 strcpy 函数将后面的字符向前覆盖删除,然后_size -= len就行了。

    在这里插入图片描述

    👉find👈

    size_t find(char ch, size_t pos) const
    {
    	assert(pos < _size);
    	while (pos < _size)
    	{
    		if (_str[pos] == ch)
    		{
    			return pos;
    		}
    		++pos;
    	}
    
    	return npos;
    }
    
    size_t find(const char* str, size_t pos) const
    {
    	assert(pos < _size);
    	const char* ptr = strstr(_str + pos, str);
    	if (ptr == nullptr)
    	{
    		return npos;
    	}
    	else
    	{
    		return ptr - _str;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注:strstr 函数是暴力匹配的查找算法,除了这种算法外,还有 KMP 算法。如果想要了解 KMP 算法的话,可以看一下这篇文章。需要注意的是,strstr 函数的返回值是指针,而 find 函数的返回值为下标,所以我们要将指针进行相减转换成下标。

    在这里插入图片描述

    在这里插入图片描述

    👉流插入和流提取重载👈

    << 运算符重载

    ostream& operator<<(ostream& out, const string& s)
    {
    	for (size_t i = 0; i < s.size(); i++)
    	{
    		out << s[i];
    	}
    	return cout;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    << 运算符是将类对象里的字符一个个打印出来的,所以它跟 c.str() 有一点区别。

    在这里插入图片描述

    在这里插入图片描述

    clear 函数清空类对象的数据

    void clear()
    {
    	_size = 0;
    	_str[0] = '\0';
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    >> 运算符重载

    istream& operator>>(istream& in, string& s)
    {
    	s.clear();
    	
    	// 第一种方式
    	/*char ch = in.get();
    	while (ch != ' ' && ch != '\n')
    	{
    		s += ch;
    		//in >> ch;
    		ch = in.get();
    	}
    	return in;*/
    
    	// 第二种方式
    	char buff[128] = { '\0' };
    	size_t i = 0;
    	char ch = in.get();
    	while (ch != ' ' && ch != '\n')
    	{
    		if (i == 127)
    		{
    			s += buff;
    			i = 0;
    		}
    
    		buff[i++] = ch;
    		ch = in.get();
    	}
    
    	if (i > 0)
    	{
    		buff[i] = '\0';
    		s += buff;
    	}
    
    	return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    以上两种方式,都能实现流插入。但是第二种方式相较于第一种方式不需要频繁地扩容。注:流提取是以空格或者换行结束的。

    在这里插入图片描述

    注:以上的流插入和流提取重载可以用友元实现,友元的话就可以直接访问类对象的数据了,而我实现的方式是通过函数接口来访问类对象的数据。

    👉拷贝构造和赋值运算符重载的传统写法👈

    拷贝构造

    //传统拷贝构造写法 s2(s1)
    string(const string& s)
    {
    	_str = new char[s._capacity + 1];
    	_capacity = s._capacity;
    	_size = s._size;
    
    	strcpy(_str, s._str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    赋值运算符重载

    // 传统赋值运算符重载 s2 = s1
    string& operator=(const string& s)
    {
    	if (this != &s)
    	{
    		char* tmp = new char[s._capacity + 1];
    		strcpy(tmp, s._str);
    
    		delete[] _str;
    		_str = tmp;
    
    		_size = s._size;
    		_capacity = s._capacity;
    	}
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    注:拷贝构造和赋值运算符重载都是先申请或释放空间,然后再把数据拷贝到对象中去。

    👉拷贝构造和赋值运算符重载的现代写法👈

    拷贝构造

    在这里插入图片描述

    交换类对象的数据

    void swap(string& s)
    {
    	std::swap(_str, s._str);
    	std::swap(_size, s._size);
    	std::swap(_capacity, s._capacity);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    库里面也有交换的函数模板,但是这个函数接口会涉及三次深拷贝,效率不高。所以我们就自己实现一个交换类对象数据的函数接口,string 类中的交换函数也是这样实现的,效率较高。

    在这里插入图片描述

    // 现代拷贝构造写法 s2(s1)
    string(const string& s)
    	: _str(nullptr)
    	, _size(0)
    	, _capacity(0)
    {
    	string tmp(s._str); // 构造函数
    	//this->swap(tmp);
    	swap(tmp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    拷贝构造现代写法说明

    • 先初始化列表,初始化对象 s2。
    • 然后调用构造函数string tmp(s._str),构造一个对象出来
    • 最后将 tmp 的数据和 s2 的数据进行交换。
    • 注:s2 一定要走初始化列表,如果不走初始化列表,s2 的数据将会是随机值,随机指向一块空间。将 tmp 和 s2 的数据交换后,tmp 会被销毁,那么随机指向的空间将会被delete掉,程序会崩溃。

    未写初始化列表

    在这里插入图片描述

    赋值运算符重载

    交换类对象的数据

    void swap(string& s)
    {
    	std::swap(_str, s._str);
    	std::swap(_size, s._size);
    	std::swap(_capacity, s._capacity);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // 现代赋值运算符重载 s2 = s1
    /*string& operator=(const string& s)
    {
    	if (this != &s)
    	{
    		//string tmp(s._str);
    		string tmp(s);
    		swap(tmp);
    	}
    	return *this;
    }*/
    
    // s2 = s1
    string& operator=(string s)
    {
    	swap(s);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    赋值运算符重载现代写法有两种:第一种现代赋值运算符重载的参数是类对象的引用,可以减少拷贝构造。当 s2 != s1 时,才进行调用拷贝构造函数构造对象 tmp,再将 tmp 的数据和 s2 的数据进行交换。第二种现代赋值运算符重载的参数类对象,传参需要调用拷贝构造函数构造 s,然后将 s 的数据和 s2 的数据进行交换。

    在这里插入图片描述

    注:拷贝构造和赋值运算符重载的现代写法并不是追求效率,而是追求简洁。深拷贝不要求 _capacity 相同。

    有了模板之后,内置类型也需要有构造函数和析构函数。见下图代码:

    在这里插入图片描述
    为什么要支持内置类型的构造函数和析构函数呢?因为有了模板,要支持泛型编程。比如下图:如果 T1 和 T2 为内置类型,就要去调用构造函数和析构函数了。

    在这里插入图片描述

    👉浅拷贝和深拷贝👈

    浅拷贝

    浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

    就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就 你争我夺,玩具损坏。

    在这里插入图片描述
    在这里插入图片描述
    可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

    深拷贝

    如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

    在这里插入图片描述

  • 相关阅读:
    CSS伪类大全!4大类伪类详解
    Blazor组件自做十二 : Blazor Pdf Reader PDF阅读器 组件(更新)
    UE4 C++设计模式:适配器模式(Adapter Pattern)
    100+经典Java面试题及答案解析
    [附源码]计算机毕业设计JAVAjsp毕业设计管理系统
    vue课程79 介绍并安装vue-cli
    排序算法--插入排序
    C语言堆排序
    城市旅游景点信息交流平台的设计与实现 毕业设计-附源码290915
    EN 14509自支撑双层金属面保温绝热夹芯板—CE认证
  • 原文地址:https://blog.csdn.net/m0_63639164/article/details/127932700