【C++心愿便利店】No.12---C++之探索string底层实现

文章目录

  • 前言
  • 一、写实拷贝(了解)
  • 二、string类常用接口实现
    • 2.1 成员变量
    • 2.2 默认构造函数
    • 2.3 拷贝构造函数
    • 2.4 operator==
    • 2.5 operator[]
    • 2.6 c_str
    • 2.7 size()
    • 2.8 capacity()
  • 三、迭代器的实现
    • 3.1 begin()和end()
    • 3.2 范围for
  • 四、string类增删查改
    • 4.1 reserve():增容函数
    • 4.2 push_back():尾插字符
    • 4.3 append():追加字符串
    • 4.4 operator+=
    • 4.5 insert
    • 4.6 erase
    • 4.7 resize
    • 4.8 find
    • 4.9 substr
  • 五、string类运算符重载
    • 5.1 operator< == <= > >= !=
    • 5.2 operator<<
    • 5.3 operator>>


前言

在这里插入图片描述

👧个人主页:@小沈YO.
😚小编介绍:欢迎来到我的乱七八糟小星球🌝
📋专栏:C++ 心愿便利店
🔑本章内容:探索string底层实现
记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~


提示:以下是本篇文章正文内容,下面案例可供参考

一、写实拷贝(了解)

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

二、string类常用接口实现

2.1 成员变量

class string
{
public:

private:
	char* _str;
	size_t _size;
	size_t _capacity;

	const static size_t npos;
	——————————————————————————
	const static size_t npos=-1;//静态的成员变量是不可以给缺省值,必须在类外面进行初始化
	但是const静态的整形可以(特例)
};
const size_t string::npos = -1;//支持在类外面初始化

在这里插入图片描述

2.2 默认构造函数

string()
	:_str (new char[1]{'\0'})
	,_size(strlen(0))
	,_capacity(0)
{

}
//常量字符串规定后面默认就有\0
//strlen 计算的是字符串中有效字符的个数,不算 '\0',而常量字符串的结尾默认有一个 '\0',用 new开辟空间的时候需要多开一个用来存储结尾的 \0
string(const char* str="")
	:_size(strlen(str))//0
	,_capacity(_size)//0
{
	_str = new char[_capacity + 1];//1
	strcpy(_str, str);
}

对于上述代码中形参上必须加 const 修饰,这样才能用 C 语言中的常量字符串来初始化 string 类对象,上面两种初始化的方式都可以一个是无参的一个是有缺省值的,形参的的缺省值直接给一个空字符串即可如上述,要注意初始化列表是按照声明的顺序来初始化的。_capacity表示的是可以存储有效字符的容量,而字符串结尾默认的 ‘\0’ 并不算作有效字符,因此最初的 _capacity 就是形参 str 的长度。
🌟对于为什么不可以用 ‘\0’ “\0” 和nullptr当缺省值?
答案:

  • 首先对于’\0’:str是一个char*类型,而’\0’是一个char类型的 类型不匹配
  • 其次对于给缺省值nullptr:strlen是不会去检查空的,它是一直找到 \0为止的,也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。
  • 最后对于"\0":它表示该字符串有一个字符 ‘\0’ ,它的结尾还有一个默认的 ‘\0’,因此有两个 ‘\0’

2.3 拷贝构造函数

//传统写法
string(const string& s)//拷贝构造
{
	_str = new char[s._size+1];
	strcpy(_str,s._str);
	_size = s._size;
	_capacity = s._capacity;
}

上述我们称为传统写法下面这种我们称为现代写法,对于现代写法不需要亲自去申请空间初始化,而是调用构造函数去完成。最后再将初始化好的 tmp 交换过来
还要注意:一定要通过初始化列表对 *this 进行初始化,不然交换给 tmp 后,里面都是随机值(不同编译器有的会处理有的不会),最终出了作用域 tmp 去销毁的时候就会出问题。

//现代写法:下面代码是两种不同的现代写法
不过要注意如果 string 对象中有 '\0',只会把 '\0' 前面的字符拷贝过去
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string(const string& s)
	:_str(nullptr)//拷贝构造
	,_size(0)
	,_capacity(0)//不处理tmp里面是随机值析构就会发合适呢个错误
{
	string tmp(s._str);//构造
	swap(tmp);
}
___________________________________________________________________________________________
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string(const string& s)//拷贝构造
{
	string tmp(s._str);
	swap(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);
}

2.4 operator==

//传统写法
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;
}

对比于上述代码写法,下述这种写法通过调用拷贝构造来申请空间,在利用局部对象出了作用就会被销毁的特点,将需要释放的资源通过 swap 交换给这个局部变量,让这个局部变量帮我们销毁。

//现代写法:这里不能直接用 swap 交换两个 string 类对象,会导致栈溢出
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
	if (this != &s)
	{
		//string tmp(s);//调用拷贝构造
		string tmp(s._str);//调用构造
		swap(tmp);//这里s2换给了tmp本来s2要析构现在tmp出了作用域调用析构也就意味着原始的s2的空间释放了
	}
	return *this;
}

下述这种写法不用我们去调用构造或者拷贝构造,直接通过形参去调用,传值传参会调用拷贝构造,tmp是它的实参调用拷贝构造构造的一个一摸一样的空间。

//现代写法的优化版本
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{
	swap(tmp);
	return *this;
}

2.5 operator[]

这两个运算符重载函数构成函数重载,对象在调用的时候会找最匹配的,非const对象会调用非const版本,const 对象会调用const版本。

//可读可写版本
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}
//只可以读版本
const char& operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

2.6 c_str

返回的是一个const char*的数组指针,只读不写,这个数组包含的字符序列与string对象的值相同,另外还包含一个以空字符(‘\0’)结尾的字符串,加上 const,这样普通的 string 类对象可以调用,const 类型的 string 类对象也可以调用,普通对象来调用就是权限的缩小

const char* c_str()
{
	return _str;
}

2.7 size()

size_t size()const
{
	return _size;
}

2.8 capacity()

size_t capacity()const
{
	return _capacity;
}

三、迭代器的实现

iterator 是 string 类的内嵌类型,也可以说是在 string 类里面定义的类型,在一个类里面定义类型有两种方法,typedef 和 内部类。

3.1 begin()和end()

//非const调用
typedef char* iterator;//string 类的 iterator 是通过typedef来实现的
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;
}

3.2 范围for

支持范围for写法,范围for的底层是迭代器实现的,但是范围for不是万能的,范围for遇上const类型的对象,会报错,因此要提供const迭代器:typedef const char* const_iterator;

string s1("hello world");
for (auto ch : s1)
{
	ch++;
	cout << ch << " ";
}
cout << endl;

四、string类增删查改

4.1 reserve():增容函数

reserve 函数不会进行缩容,因此在扩容前要先进程判断,只有当形参 n 大于当前容量的时候才扩容。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//开n+1个是因为n个有效字符,另一个是'\0'
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

4.2 push_back():尾插字符

对于尾插首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数来进行扩容(选择2倍扩容),
扩容后将ch加到str上,然后 _size++ 最后手动添加一个新的 \0 。

void push_back(char ch)
{
	if (_size == _capacity)
	{
		//reserve(_capacity * 2);
		//对于这里可以采用三目来判断_capacity是否为0,若不进行判断空串的_capacity是0,进行扩容0*0=0就会发生越界访问
		reserve(_capacity==0?4:_capacity * 2);
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

4.3 append():追加字符串

void append(const char * str)
{
	size_t len = strlen(str);
	if (_size +len> _capacity)
	{
		reserve(_size+len);//这里就不需要担心_capacity为0的情况
	}
	strcpy(_str + _size, str);//strcpy会把'\0'也拷贝过去
	size += len;
}

4.4 operator+=

+=要有返回值返回*this

//追加一个字符复用push_back()
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

//追加一个字符串复用append()
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

4.5 insert

在pos位置插入字符

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] = _str[end-1];
		--end;
	}
	_str[pos] = ch;
	_size++;
}

注意:上述代码挪动数据时的判断条件中,end 和 pos 都是 sizt_t 类型,例如当 pos = 0 的时候 end >= pos,end–,一直减到end=-1但是end是一个无符号整形,所以循环条件一直成立还可以进入循环, 所以下面有两种修改方式:

为了防止整型提升有以下两种写法:
void insert(size_t pos, char 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] = ch;
	_size++;
}
——————————————————————————————————————————————————————————————————————————————————
void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	int end = _size;
	换成有符号也会报错因为操作符的两边两个数据的类型不同时会发生类型提升,
	end变成有符号类型也会被提升到无符号类型因为pos是无符号类型,所以可以强转pos->(int)pos
	while (end >=(int) pos)
	{
		_str[end] = _str[end-1];
		--end;
	}
	_str[pos] = ch;
	_size++;
}

在pos位置插入字符串

void insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	size_t end = _size+len;
	while (end >= pos+len)
	{
		_str[end] = _str[end-len];
		--end;
	}
	strncpy(_str + pos, str,len);
	_size += len;}
}

4.6 erase

void erase(size_t pos, size_t len=npos)
{
	assert(pos < _size);
	if (len == npos||pos+len>=_size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		size_t begin = pos + len;
		while (begin <= _size)
		{
			_str[begin - len] = _str[begin];
			begin++;
		}
		_size -= len;
	 }
}

4.7 resize

size_t find(char ch,size_t pos=0)//这里给了一个半缺省
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}

4.8 find

size_t find(const char* str, size_t pos = 0)//半缺省
{
	const char* p = strstr(_str+pos, str);//strstr找到返回所在位置指针否则返回空
	if (p)
	{
		return p - _str;//返回下标
	}
	else
	{
		return npos;
	}
}

4.9 substr

这里就表明我们一定要手写一个拷贝构造,编译器默认生成的拷贝构造是一个浅拷贝,会发生调用两次析构的问题所以要手写一个深拷贝

string substr(size_t pos, size_t len = npos)
{
	string s;
	size_t end = pos + len;
	if (len == npos || pos + len >= _size)
	{
		len = _size - pos;
		end = _size;
	}
	s.reserve(len);
	for (size_t i = pos; i < pos + len; i++)
	{
		s += _str[i];
	}
	return s;//s是一个浅拷贝出了作用域s销毁
}

五、string类运算符重载

5.1 operator< == <= > >= !=

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

5.2 operator<<

因为类函数有this指针传参数容易发生错误匹配原因,>>和<<运算符重载要写在类外面
无论是形参还是返回值,只要涉及到 ostream 或 istream 都必须要用引用

//有以下两种写法:
ostream& operator<<(ostream& out,const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}
_______________________________________________________________________
范围for(这里使用范围for要调用const迭代器)
ostream& operator<<(ostream& out,const string& s)
{
	for (auto ch : s)//s是一个const对象要用const迭代器
		out << ch;
	return out;
}

5.3 operator>>

空格符 ’ ’ 和换行符 \n不能直接用 istream 对象来读取的,in >> ch 是读不到空格符和换行符。需要借助 get() 成员函数才能读取到空格符和换行符。

istream& operator>>(istream& in, string& s)
{
	s.clear();//清掉原始数据不然就变成尾插了
	char ch;
	//in >> ch;//拿不到空格或者换行例如sacnf拿不到空格所以出现了getchar
	ch = in.get();
	while (ch!=' '&&ch!='\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

对于上面这种写法,在输入的字符串很长的情况下会多次调用 reserve 进行扩容,所以可以采用下述优化版本来实现:先开辟一个数组,将输入的字符存储到数组中,然后从数组中拷贝到string对象中,数组出了作用域就会销毁

istream& operator>>(istream& in, string& s)
{
	s.clear();//清掉数据不然就变成尾插了
	char buff[128];
	size_t i = 0;
	char ch;
	ch = in.get();
	while (ch!=' '&&ch!='\n')
	{
		buff[i++] = ch;
		if (i == 128)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
	}
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/127978.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开发知识点-Mybatis

MybatisPlus [项目实战]Spring Boot 2.x 实现《百思不得姐》2-APP数据抓包3-基础环境搭建4-抓取原始数据5-json序列化与反序列化6-URL爬虫实现7-MyBatis保存原始数据8-数据建模19-配置优化与项目上线电商秒杀系统实战&&集成环境mybatis&springboot在 application.p…

关于有源电力滤波器在地铁站低压配电系统中的应用分析

安科瑞 崔丽洁 摘要&#xff1a;作为国家提出的绿色电网、节能降耗已成为现代化企业努力的目标&#xff0c;也是企业急需解决的问题。作为地铁车站这类市政公共交通建筑的着重系统——配电系统。实现绿色电网实质上是解决电网中存在的各种电能问题&#xff0c;主要是涉及到谐波…

【科研绘图】MacOS上的LaTeX公式插入工具——LaTeXiT

在Mac上经常用OmniGraffle绘图&#xff0c;但是有个致命缺点是没办法插入LaTeX公式&#xff0c;很头疼。之前有尝试用Pages文稿插入公式&#xff0c;但是调字体和颜色很麻烦。并且&#xff0c;PPT中的公式插入感觉也不太好看。 偶然机会了解到了LaTeXiT这个工具&#xff0c;可…

NVM安装node后提示没有对应npm包(即:无法将“npm”项识别为 cmdlet、函数、脚本文件)

背景 windows11 node版本降低到v12.22.12后&#xff0c;执行&#xff1a;nvm -v npm -v npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果 包括路径&#xff0c;请确保路径正确&#xff0c;然后再试一次。 所在位置 …

XOR Construction

思路&#xff1a; 通过题目可以得出结论 b1^b2a1 b2^b3a2 ....... bn-1^bnan-1 所以就可以得出 (b1^b2)^(b2^b3)a1^a2 b1^b3a1^a2 有因为当确定一个数的时候就可以通过异或得到其他所有的数&#xff0c;且题目所求的是一个n-1的全排列 那么求出a的前缀异或和arr之后…

Zeitgeist ZTG Token以及其预测市场加入Moonbeam生态

波卡上的首选多链开发平台Moonbeam宣布与Zeitgeist达成XCM集成&#xff0c;将ZTG Token引入Moonbeam。此集成将使波卡内的Moonbeam和Zeitgeist网络之间的流动性得以流动&#xff0c;并通过Moonbeam的互连合约实现远程链集成。 Zeitgeist是一个基于波卡的Substrate区块链框架构…

使用微信小程序控制蓝牙小车(微信小程序端)

目录 使用接口界面效果界面设计界面逻辑设计 使用接口 微信小程序官方开发文档 接口说明wx.openBluetoothAdapter初始化蓝牙模块wx.closeBluetoothAdapter关闭蓝牙模块(调用该方法将断开所有已建立的连接并释放系统资源)wx.startBluetoothDevicesDiscovery开始搜寻附近的蓝牙…

【ATTCK】MITRE ATTCK 设计与哲学

MITRE ATT&CK™:设计与哲学 来源&#xff1a;MITRE ATT&CK™: Design and Philosophy 摘要 MITRE ATT&CK知识库描述了网络对手的行为&#xff0c;并为攻击和防御提供了一个通用的分类。它已成为跨许多网络安全领域的一个有用工具&#xff0c;用于传递威胁情报&…

纵行科技LPWAN2.0芯片产品ZT1826获“2023年度硬核芯”大奖

2023年10月30日&#xff0c;由深圳市芯师爷科技有限公司主办、慕尼黑华南电子展协办、深圳市半导体行业协会支持的“第五届硬核芯生态大会暨2023汽车芯片技术创新与应用论坛”在深圳国际会展中心1号馆圆满落幕。当晚&#xff0c;“2023年度硬核芯评选”获奖榜单同步揭晓并进行颁…

网络的相关概念介绍

客户端和服务器通常运行在不同的主机上&#xff0c;通过计算机网络的硬件和软件资源来通信。网络是个复杂的系统&#xff0c;这里我们从程序员的角度来介绍一下网络相关的概念。 对主机而言&#xff0c;网络只是一种I/O设备&#xff0c;是数据源和数据接收方。 一个插到I/O总线…

补偿 FIR 滤波器引入的延迟

补偿 FIR 滤波器引入的延迟 对信号进行滤波会引入延迟。这意味着相对于输入&#xff0c;输出信号在时间上有所偏移。此示例向您说明如何抵消这种影响。 有限冲激响应滤波器经常将所有频率分量延迟相同的时间量。这样&#xff0c;我们就很容易通过对信号进行时移处理来针对延迟…

降维·预测·救命:PCA、随机森林与乳腺癌

一、引言 乳腺癌作为女性健康领域的一大挑战&#xff0c;对全球范围内的女性健康产生了深远影响。据世界卫生组织&#xff08;WHO&#xff09;统计&#xff0c;乳腺癌已成为全球女性恶性肿瘤发病率的最高者&#xff0c;且呈现逐年上升的趋势。在中国&#xff0c;乳腺癌也是女性…

实用技巧:嵌入式人员使用http服务模拟工具模拟http服务器测试客户端get和post请求

文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/134305752 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结…

隧道施工工艺流程vr线上虚拟展示成为产品3D说明书

行业内都知道&#xff0c;汽车生产的大部分都需要冲压加工来完成&#xff0c;因此汽车冲压工艺是汽车制造过程中的重要环节&#xff0c;传统的展示方式往往局限于二维图纸和实地操作&#xff0c;难以充分展现工艺的细节和流程。然而&#xff0c;随着技术的进步&#xff0c;汽车…

HashMap存值、取值及哈希碰撞原理分析

HashMap中的put()和get()的实现原理&#xff1a; map.put(k,v)实现原理 首先将k,v封装到Node对象当中&#xff08;节点&#xff09;。 然后它的底层会调用K的hashCode()方法得出hash值。 通过哈希表函数/哈希算法&#xff0c;将hash值转换成数组的下标&#xff0c;下标位置上…

关于c++中数据sqrt() 精度问题

情景介绍 今天在做一个算法题目的时候&#xff0c;发现&#xff0c;当使用sqrt()方法进行开方的时候&#xff0c;一直存在提交不通过的情况。 问题分析 对数据不断分析后&#xff0c;发现对35进行开方后&#xff0c;仍然满足条件&#xff0c;这就存在问题。 sqrt(35) 5.9160…

jsp 的div表格示例

<%page contentType"text/html;charsetgbk" pageEncoding"UTF-8"%> <!DOCTYPE html> <html><head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"><title>jsp div 表格示例 &…

十六、W5100S/W5500+RP2040树莓派Pico<HTTP Client上传数据到OneNET>

文章目录 1 前言2 简介2 .1 什么是HTTP&#xff1f;2.2 HTTP Client的优点2.3 HTTP Client工作原理2.4 HTTP Client应用场景 3 WIZnet以太网芯片4 HTTP Client网络设置示例概述以及使用4.1 流程图4.2 准备工作核心4.3 连接方式4.4 主要代码概述4.5 结果演示 5 注意事项6 相关链…

微软surface laptop禁用触摸屏(win10、设备管理器)

参考链接&#xff1a; 在屏幕中启用和禁用触摸屏Windows 设置如下

处理uniapp打包后有广告的问题

1、登录平台&#xff08;开发者中心&#xff09; 2、 3、 4、 5、