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

news2024/11/27 2:28:07

文章目录

  • 前言
  • 一、写实拷贝(了解)
  • 二、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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四川芸鹰蓬飞商务信息咨询有限公司电商带货可信吗

今天&#xff0c;我们要向大家介绍的是四川芸鹰蓬飞商务信息咨询有限公司的电商带货服务&#xff0c;一个在电商领域独树一帜的服务项目。它的出现&#xff0c;不仅为电商行业注入了新的活力&#xff0c;也引领了行业发展的新趋势。 一、背景介绍 四川芸鹰蓬飞商务信息咨询有限…

mac 安装使用svn教程

mac 安装使用svn教程 一、安装Homebrew 要在Mac OS上安装SVN&#xff0c;首先需要安装Homebrew。Homebrew是一个流行的包管理器&#xff0c;因此我们将使用它来安装SVN。 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"…

电子印章怎么弄?三步教你电子印章在线生成免费教程!

在这个数字化快速发展的时代&#xff0c;电子印章已经成为日常商务活动中不可或缺的一部分。相对于传统的实体印章&#xff0c;电子印章具有更高的便捷性和安全性&#xff0c;更是无纸化办公中必不可少的一环。那么&#xff0c;电子印章怎么弄呢&#xff1f;跟着下面这三步来操…

Map and Set

map and set 文章目录 map and set前言搜索树<1> 操作-查找<2> 操作-插入<3> 操作-删除<4> 代码展示<5> 性能分析 Map 和 Set 概念及应用场景Map 和 Set 模型分析Map 的使用<1> Map常用方法说明<3> TreeMap 演示<2> Entry 内部…

[git] cherry pick 将某个分支的某次提交应用到当前分支

功能&#xff1a;将某个分支的某次提交应用到当前分支 应用场景&#xff1a; 在合并分支时&#xff0c;是将源分支的所有内容都合并到目标分支上&#xff0c;有的时候我们可能只需要合并源分支的某次或某几次的提交&#xff0c;这个时候我们就需要使用到git的cherry-pick操作…

超级APP,All in one APP

在信息化时代&#xff0c;企业需要处理的数据和使用的各种系统繁多复杂。然而&#xff0c;传统的应用往往孤立存在&#xff0c;导致数据无法流动和系统无法高效对接。WorkPlus作为一款超级APP&#xff0c;以其全面的功能和强大的集成能力&#xff0c;实现了数据到系统的全方位集…

GitHub Copilot Chat将于12月全面推出;DeepLearning.AI免费新课

&#x1f989; AI新闻 &#x1f680; GitHub Copilot Chat将于12月全面推出&#xff0c;提升开发者的生产力 摘要&#xff1a;GitHub宣布将于12月全面推出GitHub Copilot Chat&#xff0c;这是GitHub Copilot的一个新功能&#xff0c;旨在帮助开发者编写代码。它能够集成到开…

乐优商城(三)品牌管理

1. 品牌的新增 1.1 url 异步请求 点击品牌管理下的新增品牌&#xff0c;填写品牌信息后提交 2.打开浏览器控制台 由此可以得知&#xff1a; 请求方式&#xff1a;POST请求路径&#xff1a;/item/brand请求参数&#xff1a;{name: “测试品牌”, image: “”, cids: “76,32…

【Cocos新手进阶】父级预制体中的数据列表,在子预制体中的控制方法!

本篇文章主要讲解&#xff0c;cocos中在预制体操作过程中&#xff0c;父级预制体生成的数据列表中&#xff0c;绑定了子预制体中的事件&#xff0c;在子预制体的时间中如何控制上级列表的具体操作教程。 日期&#xff1a;2023年11月10日 作者&#xff1a;任聪聪 一、实际效果情…

矩阵起源加入 OpenCloudOS 操作系统开源社区,完成技术兼容互认证

近日&#xff0c;超融合异构云原生数据库 MatrixOne企业版软件 V1.0 完成了与 OpenCloudOS 的相互兼容认证&#xff0c;测试期间&#xff0c;整体运行稳定&#xff0c;在功能、性能及兼容性方面表现良好。 一、产品简介 矩阵起源 MatrixOrigin 致力于建设开放的技术开源社区和…

牛客网:NC69 链表中倒数最后k个结点

一、题目 函数原型&#xff1a; struct ListNode* FindKthToTail(struct ListNode* pHead, int k ) 二、思路 本题需要找到链表中的倒数第k个结点&#xff0c;有两种方法&#xff1a; 1.暴力解法&#xff1a;先遍历一次链表&#xff0c;计算出链表的长度&#xff1b;再遍历一次…

大厂面试题-MySQL索引的优点和缺点?

(图片)索引&#xff0c;是一种能够帮助MySQL高效从磁盘上检索数据的一种数据结构。在MySQL中的InnoDB引擎中&#xff0c;采用了B树的结构来实现索引和数据的存储 MySQL里面的索引的优点有很多&#xff1a; 1. 通过B树的结构来存储数据&#xff0c;可以大大减少数据检索时的磁…

【docker:容器提交成镜像】

容器创建部分请看&#xff1a;点击此处查看我的另一篇文章 容器提交为镜像 docker commit -a "sinwa lee" -m "首页变化" mynginx lxhnginx:1.0docker run -d -p 88:80 --name lxhnginx lxhnginx:1.0为啥没有变啊&#xff0c;首页&#xff1f; 镜像打包 …

CAN总线记录诊断助手 CAN记录仪

随着CAN总线的应用市场越来越多&#xff0c;不仅局限于汽车行业&#xff0c;工程车、特种车、消防、医疗等多行业都是以CAN总线通讯为主。总线的调试诊断也成为技术日常工作&#xff0c;有个好的工具能有效帮助发现问题、解决问题。 来可电子的CANLog-VCI是一款即插即用的CAN数…

从零开始:集成视频直播美颜SDK的步骤及注意事项

如果你是一位开发者&#xff0c;想要为你的视频直播应用添加美颜功能&#xff0c;那么你来对地方了。本文将从零开始&#xff0c;介绍集成视频直播美颜SDK的步骤及需要注意的事项&#xff0c;帮助你顺利实现这一技术目标。 步骤一&#xff1a;选择合适的美颜SDK 在开始之前&…

带有密码的Excel只读模式,如何取消?

Excel文件打开之后发现是只读模式&#xff0c;想要退出只读模式&#xff0c;但是只读模式是带有密码的&#xff0c;该如何取消带有密码的excel只读文件呢&#xff1f; 带有密码的只读模式&#xff0c;是设置了excel文件的修改权限&#xff0c;取消修改权限&#xff0c;我们需要…

银河麒麟操作系统安装_V4/V10系统详细使用教程

银河麒麟桌面操作系统V10是一款简单易用、稳定高效、安全创新的新一代图形化桌面操作系统产品。现已适配国产主流软硬件产品&#xff0c;同源支持飞腾、鲲鹏、海思麒麟、龙芯、申威、海光、兆芯等国产CPU和Intel、AMD平台&#xff0c;通过功耗管理、内核锁及页拷贝、网络、VFS、…

Python使用腾讯云SDK实现对象存储(上传文件、创建桶)

文章目录 1. 开通服务2. 创建存储桶3. 手动上传文件并查看4. python上传文件4.1 找到sdk文档4.2 初始化代码4.3 region获取4.4 secret_id和secret_key获取4.5 上传对象代码4.6 python实现上传文件 5 python创建桶 首先来到腾讯云官网 https://cloud.tencent.com/1. 开通服务 来…

Python 实践

文章目录 一、HttpRequests 一、Http Requests python——Request模块

家庭安全计划 挑战赛| 溺水预防

溺水预防 从了解到行动 家庭安全计划 | 少年急救官 地震避险逃生该怎么做&#xff1f; 起火了该如何应对&#xff1f; 哪些行为容易导致溺水&#xff1f; 家庭风险隐患有哪些&#xff1f; 家庭逃生演练四步骤你会吗&#xff1f; 国际救助儿童会&#xff08;英国&#xff…