CD37.【C++ Dev】string类的模拟实现(上)

目录

1.string基本知识的回顾

2.简单的模拟实现

准备操作

代码实现

成员变量

构造函数

C风格构造的函数

无参构造函数

C风格构造的函数与无参构造函数合二为一

析构函数

c_str()

size()

[operator[ ]](#operator[ ])

可读可写

只可读

iterator(指针版)

begin()和end()

​编辑

[push_back(char c)](#push_back(char c))

[reserve(size_t n = 0)](#reserve(size_t n = 0))

[结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止](#结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止)


1.string基本知识的回顾

参见文章:

CC12.【C++ Cont】string类字符串的创建、输入、访问和size函数

CC13.【C++ Cont】初识string类字符串的迭代器

CC14.【C++ Cont】string类字符串的push_back、pop_back、字符串+=与+运算和insert

CC15.【C++ Cont】string类字符串的find和substr函数

CC16.【C++ Cont】string类字符串的关系运算和与string有关的函数


CD34.【C++ Dev】STL库的string的使用 (上)

CD35.【C++ Dev】STL库的string的使用 (中)

CD36.【C++ Dev】STL库的string的使用 (下)

2.简单的模拟实现

由于STL库实现的string比较复杂,这里实现一个"微缩"版本的string

准备操作

自制一个string.h(不是C语言的头文件),包含到main.cpp中,来模拟正常使用STL库的string需要包含对于头文件<string>的操作

cpp 复制代码
//string.h
#pragma once
namespace mystl
{
	class string
	{

	};
}
cpp 复制代码
//main.cpp
#include "string.h"
using namespace mystl;
int main()
{
	return 0;
}

代码实现

成员变量

一个指针指向字符串,一个变量size存储string的大小,另一个变量capacity存储string的空间

粗略按照STL的标准:

cpp 复制代码
class string
{
	//......
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

构造函数

C风格构造的函数

按照声明的顺序来写初始化成员列表,可修改声明的顺序

cpp 复制代码
string(const char* str)
	:_size(strlen(str))
	, _capacity(_size+10)
	, _str(new char[_capacity+1])
{
	strcpy(_str, str);
}

注意参数初始化的值:

由于_size不计入字符串结尾的\0,因此_size的值等于strlen(str)

而capacity是最多能存储有效字符的个数,这里_capacity初始化为_size+10,多了10个字符的有效空间

注意_str(new char[_capacity+1])的_capacity**+1** ,这里一定要写+1!+1是给字符串结尾的\0,而\0不计入有效字符

(注strcpy函数的讲解在51.【C语言】字符函数和字符串函数(strcpy函数)文章)

初始化成员的顺序_size-->_capacity-->_str

如果new申请失败,在构造函数外捕获即可:

测试代码:

cpp 复制代码
void test1()
{
	try
	{
		string s("teststring");
		return;
	}
	catch (const std::exception& e)
	{
		std::cout << e.what() << std::endl;
	}
}

下断点到return,查看string的构造情况:

正好是10个cd,是多开辟了10个有效字符的空间

无参构造函数
cpp 复制代码
string()
	:_size(0)
	, _capacity(0)
	, _str(new char[1])
{
    _str = '\0';
}

这里的**_str不能初始化为nullptr**,否则析构函数会出现问题,只开辟一个字节的空间,填上'\0'即可

C风格构造的函数与无参构造函数合二为一

对C风格构造的函数进行改造,带缺省参数即可

cpp 复制代码
string(const char* str="")//""表示一个\0
	:_size(strlen(str))
	, _capacity(_size+10)
	, _str(new char[_capacity+1])
{
	strcpy(_str, str);
}

测试代码:

cpp 复制代码
void test2()
{
	string s;
	return;
}

监视窗口:

析构函数

cpp 复制代码
~string()
{
	delete _str;
	_str = nullptr;
	_size = _capacity = 0;
}

注意:delete不会自动将_str置空,需要手动对_str置空

c_str()

标准库的c_str的返回类型为const char*

cpp 复制代码
const char* c_str() const
{
	return _str;
}

函数名后要写const,因为c_str成员函数没有对_str进行修改

size()

cpp 复制代码
size_t size() const
{
	return _size;
}

函数名后要写const,因为c_str成员函数没有对_size进行修改

operator[ ]

cplusplus网上给了两个版本:

返回类型为char&可读可写,而返回类型为const char&只可读

可读可写

提供的pos要合法,需要小于_size才可以,因此可以先断言,参数和返回值类型和STL保持一致

cpp 复制代码
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}
只可读

提供的pos要合法,需要小于_size才可以,因此可以先断言,参数和返回值类型和STL保持一致

注意像下面这样写是有问题的:

会导致两个operator[ ]的参数列表相同,因此需要使用const修饰隐藏的this指针

cpp 复制代码
const char& operator[ ](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

测试代码:

cpp 复制代码
void test3()
{
	string s1("teststring");
	const string s2("stringtest");
	for (size_t pos1 = 0, pos1 < s1.size(); pos1++)
	{
		s1[pos1]++;
		std::cout << s1[pos1];
	}
	std::cout << std::endl;
	for (size_t pos2 = 0, pos2 < s1.size(); pos2++)
	{
		std::cout << s1[pos2];
	}
}

int main()
{
	test3();
	return 0;
}

运行结果:

cpp 复制代码
void test3()
{
	string s1("teststring");
	const string s2("stringtest");
	for (size_t pos1 = 0; pos1 < s1.size(); pos1++)
	{
		s1[pos1]++;
		std::cout << s1[pos1];
	}
	std::cout << std::endl;
	for (size_t pos2 = 0; pos2 < s1.size(); pos2++)
	{
		std::cout << s2[pos2];
	}
}

int main()
{
	test3();
	return 0;
}

运行结果:

iterator(指针版)

CD35.【C++ Dev】STL库的string的使用 (中)文章提到的:

迭代器即iterator,是像指针一样的类型,它有可能 指针,也有可能不是 指针

为了简单起见,现按指针方式实现,使用typedef重定义即可:

cpp 复制代码
typedef char* iterator;
typedef const char* const_iterator;
begin()和end()

和STL保持一致,各自实现const修饰和没有用const修饰的,和STL的要求保持一致:

注: past是过去的的意思,那么past-the-end character指的是最后一个字符之后的字符

cpp 复制代码
iterator begin()
{
	return _str;
}

const_iterator begin() const
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator end() const
{
	return _str + _size;
}

测试代码1:范围for

范围for会自动调用迭代器begin()和end()并解引用迭代器:

cpp 复制代码
void test4()
{
	string s1("teststring");
	const string s2("stringtest");
	for (char it1 : s1)
		std::cout << it1;
	std::cout << std::endl;
	for (char it2 : s2)
		std::cout << it2;
}

反汇编代码:

运行结果:

测试代码2:手动使用迭代器

cpp 复制代码
void test5()
{
	string s1("teststring");
	const string s2("stringtest");
	for (string::iterator it = s1.begin(); it < s1.end(); it++)
		std::cout << *it;
	std::cout << std::endl;
	for (string::const_iterator it = s2.begin(); it < s2.end(); it++)
		std::cout << *it;
}

运行结果:

push_back(char c)

依照STL标准:

尾插前先考虑需不需要扩容,C++扩容没有像realloc这样的函数,需要手动操作,当_size==_capacity时扩容,不同STL的扩容方案不同,这里展示二倍扩容:使用reserve函数对开辟新空间,修改capacity即可:

reserve(size_t n = 0)

为了简单起见,只考虑n>_capacity的情况:

步骤:开辟新空间-->拷贝旧空间的数据到新空间-->释放旧空间-->修改_str指针和_capacity

cpp 复制代码
void reserve(size_t n = 0)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//+1是为\0准备的
		strcpy(tmp, _str);
		delete[ ] _str;
		_str = tmp;
		_capacity = n;
	}
}

提问: 这样写有没有问题?

答:有隐患,因为strcpy只拷贝\0以前的字符,但标准库实现的reserve函数停止拷贝的条件是复制位置达到_size

对比STL的测试代码:

cpp 复制代码
void test6()
{
	std::string str("abc");
	str.push_back('\0');
	str.push_back('1');
	std::string copy_str = str;//调用拷贝构造函数
	return;
}

下断点到return;,监视窗口查看:

结论: C语言的字符数组,以\0算中止长度(strlen),但string对象的拷贝不以\0来中止,只看size来中止

那么在实现reserve函数时就不能使用strcpy,应该用内存拷贝函数:memcpy函数或memove函数,但之前在58.【C语言】内存函数(memcpy函数)文章讲过:

为了避免溢出,由destination和source指针指向的数组应该至少为num个字节,而且两者不能重叠(对于重叠的内存块,memmove是一种更安全的方法)

则使用memove来修改reserve函数:

(注:reserve只是扩容,而resize是扩容+填值初始化(默认填\0) resize会让size和capacity都变(size变的原因是填值初始化),这在CD36.【C++ Dev】STL库的string的使用 (下)文章提到过)

cpp 复制代码
void reserve(size_t n = 0)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//+1是为\0准备的
		memmove(tmp, _str,_size+1);//+1是为\0准备的
		delete[ ] _str;
		_str = tmp;
		_capacity = n;
	}
}

继续写push_back函数:

直接写下面的代码会有问题:

cpp 复制代码
void push_back(char c)
{
	if (_size == _capacity)
	{
		reserve(_capacity * 2);
	}
	_str[_size++] = c;
	_str[_size] = '\0';
}

*注: 如果是空字符串,_size==0,尾插不会出现问题,因为_capacity最小为10,见之前的代码

测试代码:

cpp 复制代码
void test7()
{
	string str;
	str.push_back('a');
	str.push_back('\0');
	str.push_back('1');
	str.push_back('3');
	str.push_back('\0');
	str.push_back('5');
	return;
}

下断点到return,内存窗口查看:

相关推荐
byte轻骑兵几秒前
【Bluedroid】蓝牙HID DEVICE断开连接流程源码分析
android·c++·蓝牙·hid·bluedroid
Edward Nygma16 分钟前
springboot3+vue3融合项目实战-大事件文章管理系统-更新用户密码
android·开发语言·javascript
焜昱错眩..28 分钟前
代码随想录训练营第二十一天 |589.N叉数的前序遍历 590.N叉树的后序遍历
数据结构·算法
菲兹园长1 小时前
MyBatis-Plus
java·开发语言·mybatis
Tisfy1 小时前
LeetCode 1550.存在连续三个奇数的数组:遍历
算法·leetcode·题解·数组·遍历
wang__123001 小时前
力扣70题解
算法·leetcode·职场和发展
菜鸟破茧计划1 小时前
滑动窗口:穿越数据的时光机
java·数据结构·算法
修修修也1 小时前
【C++】特殊类设计
开发语言·c++·特殊类·类与对象
Cloud Traveler1 小时前
Java并发编程常见问题与陷阱解析
java·开发语言·python
虾球xz2 小时前
游戏引擎学习第274天:基于弹簧的动态动画
c++·学习·游戏引擎