IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++】string类的模拟实现 -> 正文阅读

[C++知识库]【C++】string类的模拟实现

目录

1. string 构造函数

2. 析构函数

3. string 的基础函数

4. 迭代器

5. 拷贝构造函数

6. 赋值运算符重载

7. 简便写法(调用swap函数实现)

8. reserve 与 resize?

9. string 之插入数据

10. 流提取、流插入操作符重载

11. find 和 substr?

12. 比较函数重载


接下来我们就来模拟实现一下 string 类的基本操作。

1. string 构造函数

        string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

① 缺省值为“”,“\0”这样的写法实际上是两个字符,为\0 \0,因为常量字符串会默认加一个\0。

②推荐使用函数体内初始化,在函数体内初始化只用调用一次 strlen 函数,如果使用初始化列表,其中_size、和_capacity 的计算都应经过 strlen 计算,因为初始化列表的顺序是通过成员变量定义的顺序来决定的。

③即使字符串为空,那也至少开辟一个空间,留给\0的空间。


2. 析构函数

        ~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

析构函数比较简单,就不多解释了。


3. string 的基础函数

以下是一些经常使用的string中的基础函数,特别注意,const对象只能调用const成员函数

所以,如果我们有 const 成员变量想获取某个位置的值时,我们要提供一个const型的 []操作符函数重载。

const char* c_str() const
{
	return _str;
}

size_t size() const
{
	return _size;
}

size_t capacity() const
{
	return _capacity;
}

const char& operator[](size_t pos)  const
{
	assert(pos < _size);
	return _str[pos];
}

char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

4. 迭代器

在 string 中,迭代器不过就是一个类似指针的存在,所以我们直接typedef 一下char型指针即可。

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

5. 拷贝构造函数

即根据所传参数来构建一个相同的对象。

//拷贝构造函数
string(const string& s)
	:_str(new char [s._capacity+1])
	,_size(s._size)
	,_capacity(s._capacity)
{
	strcpy(_str, s._str);
}

6. 赋值运算符重载

赋值运算符重载的实现,有以下几个需要注意的点:

  1. 先检查左右是否为相同的值,避免自己给自己赋值情况。
  2. 先开辟空间,然后将数据拷贝过去,防止开辟空间失败又丢失了数据。
  3. 再释放原先的空间,进行参数的替换。
string& operator=(const string& s)
		{
			if (this!= &s)
			{
				char* temp= new char[s._capacity + 1];
				strcpy(temp,s._str);
				delete[] _str;
				_str = temp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

7. 简便写法(调用swap函数实现)

拷贝构造函数和赋值运算符重载这两个功能的实现,我们可以采用更为简便的方式来实现。

思路:根据函数参数生成一个临时对象,将该临时对象的数据与this指向的数据进行交换。

//拷贝构造函数
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	//构造出一个函数
	string temp(s._str);
	swap(temp);
}
//赋值运算符重载
string& operator=(const string& s)
{
	if (this != &s)
	{
		string temp(s._str);
		swap(temp);
	}
	return *this;
}

这其中所调用的?swap 函数为全局swap函数,但是全局的 swap 函数会生成一个临时变量,这样便会产生很大的代价。对于string类来说,其交换数据无非是将其中的成员变量_str的指向改变一下而已,完全不需要这么大的代价,所以我们来自己实现一个string类中的?swap 函数。

//编译器会在当前命名空间中寻找,然后再去全局域中找
void swap(string& temp)
{
	::swap(_str, temp._str);
	::swap(_size, temp._size);
	::swap(_capacity, temp._capacity);
}

加上了域作用限定符则表示调用了全局的 swap 函数,而这里的调用,仅仅是交换一些内置类型,并没有将我们编写的string类拷贝生成一份,这便大大提高了运行效率。

赋值运算符重载的简化写法还有优化的地方。

我们知道,函数传参,传的是实参的临时拷贝,那既然函数传参已经生成了临时拷贝,我们便可以利用其自动生成的临时拷贝对象。

//注意,这里是传值传参
string& operator=(string s)
{
	swap(s);
	return *this;
}

注意了,这种简洁方式采用的传值传参,然后调用我们 string 类中的 swap 函数。


8. reserve 与 resize?

reserve 的实现:

分清楚resize和reserve的区别:

reserve是设置了capacity的值,比如reserve(20),表示该容器最大容量为20,但此时容器内还没有任何对象,也不能通过下标访问。

resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小

reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。

void reserve(size_t n)
{
	//大于_capacity 才扩容
	if (n > _capacity)
	{
		char* temp = new char[n + 1];
		strcpy(temp, _str);
		delete[] _str;
		_str = temp;
		_capacity = n;
	}
}

注意:

需要n个空间,则要开辟n+1开空间,留一个给\0

注意插入数据后要在末尾放入\0

resize 的实现:

void resize(size_t n, char ch ='\0')
{
	//1.比当前_size大  2.小于等于_size
	if (n > _size)
	{
		//插入数据
		reserve(n);
		for (size_t i = _size;i<n;i++)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
	}
	else
	{
		//删除数据
		_str[n] = '\0';
		_size = n;
	}
}

9. string 之插入数据

① push_back

push_back的实现其实与实现顺序表的尾插相似。

  1. 先检查大小,然后将数据进行尾插。
  2. 注意 _capacity==0 的情况出现,要额外检查capacity==0的情况。
  3. 将 _size 进行++,然后在_size处放入'\0'即可。
void push_back(char ch)
{
	//满了就扩容
	if (_size == _capacity)
	{
		//判断一下,因为我们开始给的缺省值为\0,其中capacity为0
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size] = ch;
	//插完数据后要再插入一个\0
	++_size;
	_str[_size] = '\0';  
}

② append 的实现

关于 append 的实现:

  1. 计算传入的字符串大小,根据字符串的大小检查是否开辟空间以及开辟多大空间(防止盲目开辟空间).
  2. 如果是 _size+len > _capacity ,不理解 > 还是 >= 可以代值进行计算一下。
  3. 使用 strcpy 拷贝数据,不过是从_str+_size处开始拷贝,将 str 的数据拷贝到 string 类中。
  4. 将_size += len,表示数据插入完成。
void append(const char* str)
{
	//计算需要多大的空间
	size_t len = strlen(str);
	//满了就扩容
	if (_size + len > _capacity)
	{
		//至少要开辟这么大的空间
		reserve(_size + len);
	}	
    strcpy(_str + _size, str);
    _size += len;
}

③ +=运算符重载(字符)

实现了 push_back 和 append 后,我们来实现+=运算符重载,关于+=运算符重载,它比push_back和 append 的优势在于:1. 支持连续+=;2.增强代码可读性。

//字符型调用 push_back
string& operator+=(char ch)
{
    push_back(ch);
    return *this;
}

//字符串类型调用 append
string& operator+=(char* str)
{
    append(str);
    return *this;
}

④ insert

  1. 检查pos位置是否大于 _size,大于则直接报错。
  2. 检查_size 是否与_capacity相等,相等则扩容。
  3. 将数据向后挪动,然后插入数据。
void insert(size_t pos,  char ch)
{
	assert(pos <= _size);
	if (_size ==_capacity)
	{
		reserve(_capacity==0?4:_capacity*2);
	}
	size_t end = _size;
	while (end >= pos)
	{
    	_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
}

10. 流提取、流插入操作符重载

接下来我们就来实现流插入和流提取操作符的重载,实现这两个操作符重载,可以方便我们打印的插入数据。

①流提取操作符

  1. 注意要重载为全局函数,这样才能不用使用.(成员访问操作符)既可以打印数据了。
  2. 不必声明此函数为string类的友元函数,因为此操作符并未访问私有成员变量
  3. 因为提取操作符不涉及更改数据,所以使用const型函数形式参数,使用[ ]访问数据时,[ ]操作符重载函数必须为 const 型,因为 const 型对象只能调用 const 型成员函数。
  4. 因为支持连续提取,所以要做 ostream& 作为返回值。
ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}
		return out;
	}

② 流插入操作符

  1. 每次插入数据都要清空当前对象中_str存放的数据。
  2. 因为cin 会将' ' (空格)或 '\n' 视为分隔符,不会读入其中,所以我们无法通过检测' ' (空格)或 '\n' 来停止数据。这时我们便使用istream中的一个成员函数get()来帮我们获取分隔符。
  3. 使用一个字符 ch来存储数据,并将其不断插入到类中。
  4. 返回 in,因为流插入操作符要支持连续插入。
void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

istream& operator>>(istream& in, string& s)
	{
        s.clear();
		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

当输入的字符串很长的时候,不断+=,频繁的扩容,会使效率变得底下,所以我们可以使用一个类似内存池的机制来优化以上这种场景。

开辟一个大小合适的数组用来临时存储数据,临时数组存放满了之后再将其插入到 string 对象中。

istream& operator >> (istream& in,  string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		const size_t N = 32;
		char buff[N];
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			//将数据插入到对象中
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		buff[i] = '\0';
		s += buff;
		return in;
	}

11. find 和 substr?

在上一篇博客中我们使用了find和substr实现了网页链接切割,接下来我们就来模拟实现一下这两个功能。

find函数的实现:

  1. 如果是字符,遍历_str即可。
  2. 字符串可以使用 strstr 函数,其作用是返回字串在字符串中第一次出现的位置。
  3. 然后用 strstr 返回的值减去_str,即可得该子串得起始位置。
//使用find 和 substr 进行域名字符串的切割
size_t find(char ch, size_t pos = 0) const
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		if (ch == _str[i])
			return i;
	}
	return npos;
}
		
//子串查找
size_t find(const char* sub, size_t pos = 0) const 
{
	assert(sub);
	assert(pos < _size);
	//查找
	const char *ptr=strstr(_str + pos, sub);
	if (ptr == nullptr)
		return npos;
	return ptr - _str;
}

substr 函数的实现:

  1. 检查是否切割到字符串结尾,如果是直接将_pos位置后的数据全部放入临时变量中
  2. 如果不是切割到字符串结尾,则一个一个放入到临时变量中。
string substr(size_t pos, size_t len = npos) const
{
	assert(pos < _size);
	size_t realLen = len;
	//如果切割的部分大于字符串的大小
	if (len == npos || pos + len > _size)
	{
		realLen = _size - pos;
	}
	//创建一个临时变量,返回切割后的数据。
	string sub;
	for (size_t i = 0; i < realLen; ++i)
	{
		sub += _str[pos + i];
	}
	return sub;
}

12. 比较函数重载

这就是实现一些大于、等于、小于此类的操作符重载了。

bool operator>(const string& s) const
{
    return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s) const
{
	return strcmp(_str, s. _str)==0;
}
bool operator>=(const string& s) const
{
	return *this > s || *this == s;
}
bool operator<=(const string& s) const
{
	return !(*this > s);
}
bool operator<(const string& s) const
{
	return !(*this >= s);
}
bool operator!=(const string& s) const
{
	return !(*this == s);
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:54:41  更:2022-09-13 10:55:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 23:42:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码