C++入门小馆: 深入string类(二)

嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!

我的博客:yuanManGan

我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记  闲言碎语小记坊 题山采玉 领略算法真谛

目录

string的成员变量

成员变量

c_str和size( ),capacity( )

默认成员函数:

string的默认构造

无参构造:

带参构造: 

string的析构函数:

string 的拷贝构造:

赋值运算符重载:

尾插相关操作

string 的reserve(扩容)

string的push_back

string的append(追加字符串)

string重载运算符+=

string的遍历:

重载[ ]:

迭代器:

范围for:

string 在任意位置插入删除 

insert

​编辑erase 

​编辑 string中的查找和裁剪

find:

 substr:

 补充拷贝构造和赋值运算符重载的现代写法:

swap

拷贝构造

迭代区间构造

重载赋值运算符

流插入流提取操作符的重载

cout

​编辑

cin

clear


本章来模拟实现一下string类,不是按照模板实现,而是按照容易理解的实现。

string的成员变量

我们的string类本质还是字符数组,但我们可以动态开辟,用_str字符指针来指向数组,我们还需要知道数组的空间大小,以及有效字符个数,跟之前实现的顺序表有点类似,但这里用类来实现。

我们将string放在一个命名空间里面,以防和库里的冲突。

namespace refrain
{class string{public:private:char* _str;size_t _size;size_t _capacity;};
}

成员变量

c_str和size( ),capacity( )

这里为了方便打印,我们先实现这个返回c类型的字符串,就是j将_str返回,随便也实现另外俩个成员变量的返回

我们将声明与定义分离,写在不同的文件里。

const char* string::c_str() const
{return _str;
}
size_t string::capacity() const
{return _capacity;
}
size_t string::size() const
{return _size;
}

默认成员函数:

string的默认构造

无参构造:

_size 和_capacity好处理,都是0但_str应该初始化为什么,是空指针还是什么,不如看看库里面是怎么实现的?

库里面是'\0'那我们就按照它的来实现吧!那就意味着我们一开始就得开一个'\0'的空间。但我们的capacity和size不要记录这个'\0'的空间。

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

带参构造: 

我们带参构造就将传入的参数直接拷贝过去就好了。

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

看看这种写法,用了三次strlen时间成本大大提高了,我们可不可以在初始化列表先将_size初始化,然后复用_size呢? 

我们试试:

这里为什么_str没有创建空间呢?我们回忆一下初始化列表是按照怎么顺序,对是按照变量声明的顺序,我们先声明的_str,但此时_size还未初始化,_size的值看编译器实现,这里vs将_size初始化为了0,所以只有一个空间。

那有同学就要说了,那我们将声明顺序改一下能不能实现呢,我们试试。

ok了,但这样真的好吗,你这不是自己给自己埋雷吗,万一别人不知道,在这里乱改一下,那怎么办?

我们可以考虑只在初始化列表初始化_size让_str和_capacity走初始化函数。

依旧ok。

还有个问题,我们可不可以给缺省值,就不用写默认无参构造了?

最终版本:

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

string的析构函数:

这个就简单了,但要判断一下,如果_str为空就不能析构

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

string 的拷贝构造:

//传统写法
string::string(const string& s)
{_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;
}

赋值运算符重载:

// s1 = s2
string& string::operator=(const string& s)
{if (this != &s){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;
}

尾插相关操作

string 的reserve(扩容)

扩容是一个会频繁调用的操作,所以我们先来实现一下这个操作。

这是库里实现的。

void reserve(size_t n);

 先在string.h写个声明,在string.cpp里实现这个函数。我们要将容量扩容到n,如果n>capacity,就直接新创建一块空间然后拷贝,有人说不能用relloc吗,我的建议是不要使用,因为relloc扩容扩的空间大了,也是重新创建一块空间进行扩容,然后拷贝。

那如果n <capacity呢,我们看编译器,可能缩容,但一般不缩容,我们就不实现这个了,缩容是典型的以时间换空间的案例。

void string::reserve(size_t n)
{if (n > _capacity){//注意这里是n+1给'\0'留一点空间char* tmp = new char[n + 1];//要判断_str是否为nullptr对空指针解引用要报错if (_str){memcpy(tmp, _str, _size + 1);delete[] _str;}_str = tmp;_capacity = n;}
}

string的push_back

加入函数得先判断一下是否需要扩容,当_size == _capacity 时需要扩容。然后将最后一个字符改成要加入的值,再将_size++最后将最后一个位置弄成'\0'

void string::push_back(char ch)
{if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}_str[_size++] = ch;_str[_size] = '\0';
}

string的append(追加字符串)

这里的扩容逻辑就得考虑一下了,如果我们插入的字符串的长度是len,如果len + _size > _capaticy时才会扩容,我们是扩二倍,还是len + _size 呢,如果给多少扩多少时,我们会面临一个问题:就是如果我们频繁扩小字符串,就会频繁扩容;如果我们扩二倍,如果我们扩容的字符串很大,len + _size > 2 * _capacity就出现了一个很严重的问题,我存的值不见了,就好比你去银行存了几百万,结果一查就省几十万了,谁还敢存钱在你们银行。

我们这里就得分类讨论一下了,如果len+_size > 2*capacity,我们就扩容到len  +_size,没有就扩到2倍。

void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;reserve(newcapacity);}memcpy(_str + _size, str, len + 1);_size += len;
}

string重载运算符+=

实现了push_back和append实现+=运算符就易如反掌了,只需要复用加重载就完成了。

string& string::operator+=(char ch)
{push_back(ch);return *this;
}
string& string::operator+=(const char* str)
{append(str);return *this;
}
char& string::operator[](size_t i)
{return _str[i];
}
const char& string::operator[](size_t i) const
{return _str[i];
}

我们再来实现一下string的遍历吧

string的遍历:

重载[ ]:

这个实现很简单。直接返回*(_size + i);

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

迭代器:

这里实现迭代器就使用原生指针了,但底层实现不一定是原生指针,可能是其他的主要看编译器想怎么实现。

string::iterator string::begin()
{return _str;
}
string::iterator string::end()
{return _str + _size;
}
string::const_iterator string::begin() const
{return _str;
}
string::const_iterator string::end() const
{return _str + _size;
}

范围for:

实现了迭代器就实现了范围for,范围for的实质就是替换为迭代器。

string 在任意位置插入删除 

insert

insert有很多个版本,我们就实现其中比较实用的两个吧,第二个就实现插入一个吧

在pos位置插入一个字符: 

就将pos位置之后的字符全部向后挪一步,然后将pos位置改成插入的值。

void string::insert(size_t pos, char ch)
{assert(pos < _size);//判断是否需要扩容if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}size_t end = _size + 1;while (pos < end){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;
}

在任意位置插入字符串

void string::insert(size_t pos, const char* str)
{assert(pos < _size);size_t len = strlen(str);if (_size + len > _capacity){size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;reserve(newcapacity);}size_t end = _size + len;while (end >= pos + len){_str[end] = _str[end - len];end--;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}

erase 

任意位置删除n个字符

这里得分类讨论一下,如果删的字符个数过多就等于把后面全删了,这种情况比较好处理,当我们第二个参数不传时,默认删完,那我们该咋实现呢?对给缺省值给你npos我们得先定义一下npos,定义成const静态成员变量

private:size_t _size;size_t _capacity;char* _str;static const size_t npos = -1;

在c++这里可以这样给缺省值哦。

 erase函数就成这样了。

void erase(size_t pos, size_t len = npos);

 注意声明和定义不能同时给缺省值。

当删不完的时候

我们可以使用c语言的库函数memmove来移动数据(也是懒得实现了)

void string::erase(size_t pos, size_t len)
{assert(pos < _size);//删完if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else {memmove(_str + pos, _str + pos + len, _size - (pos + len) + 1);_size -= len;}
}

 string中的查找和裁剪

find:

我们也是简单实现这两个哦

最后一个好实现直接遍历一遍即可:

size_t string::find(char ch, size_t pos) const
{for (size_t i = 0; i < _size; i++){if (_str[i] == ch) return i;}return npos;
}

从第pos位置开始查找字符串sub返回最先的找到的下标

这里我们直接调用库函数里面的strstr来查找。

size_t string::find(const char* sub, size_t pos) const
{assert(pos < _size);const char* p = strstr(_str + pos, sub);if (p == nullptr){return npos;}else{return p - _str;}
}

 substr:

创建一个string类型的ret,直接+=

string string::substr(size_t pos, size_t len)const
{assert(pos < _size);if (len > _size - pos){len = _size - pos;}string ret;ret.reserve(len);for (size_t i = 0; i < len; i++){ret += _str[pos + i];}return ret;
}

 补充拷贝构造和赋值运算符重载的现代写法:

swap

我们先来实现一下swap函数,有人就要问了,库里面不是有swap函数吗

看看库里面的swap

template <class T> void swap ( T& a, T& b )
{T c(a); a=b; b=c;
}

看看这里是创建了一个c对象拷贝a对象,然后再赋值交换,要付出的代价有点太大了 。我们在string这个类中仅仅需要交换一下指针和_size 和_capacity就行了。

void string::swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

注意这里的函数里面swap函数必须要制定std空间,不然会认为自己调用自己导致无限递归。

但我们学习C++的有两种人,一种是只了解怎么使用string的,一种是像我们这样深入学习string库,了解底层原理,他们并不知道那种更高效,为了避免这种情况发生,我们编译器会自动调用string库里面的swap函数,无论你是下面那种代码:

swap(s1, s2);
s1.swap(s2);

 然后我们来实现一下构造函数:

我们可以将传入的对象先默认构造一份然后交换给this

拷贝构造

string::string(const string& s)
{string tmp(s_str);swap(tmp);
}

但当我们实现以下操作时得到的不是我们想要的答案 

	void test_string01(){string s1("hello world");s1 += '\0';s1 += "xxxxxx";string s2(s1);cout << s1 << endl;cout << s2 << endl;}

为什么打印不了后面的呢?

问题出在了我们进入拷贝构造后,要将目标字符串默认构造一份,此时的默认构造除了问题,其中计算_size时只会计数到'\0',会导致出现问题。

那我们咋解决呢?

在string中可以用迭代区间构造,需要使用模版,这里为什么要使用模版呢?有人说直接用string里面的迭代器不就好了。我们不只是可以使用string的迭代器,还可以用其他容器的迭代器。

迭代区间构造

template <class InputIterator>
string(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}

 我们将拷贝构造改成这样就ok了。

string::string(const string& s)
{string tmp(s.begin(),s.end());swap(tmp);
}

重载赋值运算符

赋值运算符也是同样的思路

string& string::operator=(const string& s)
{string tmp(s.begin(), s.end());swap(s);return *this;
}

还有一种更简单的写法 

string& string::operator=(string tmp)
{swap(tmp);return *this;
}

我们这里自己传值传参,传值传参调用构造函数, 然后直接交换,返回*this,出作用于,tmp直接销毁。

流插入流提取操作符的重载

cout

要将该重载定义在string类外。

这个实现就很简单直接打印就行

ostream& operator<<(ostream& os,const string& s)
{for (auto& ch : s){os << ch;}return os;
}

看这种情况,我们打印s1的c_str( )时出现了我们不想要的结果,这是为什么呢,c_str()返回的是c类型的字符串,而c类型的字符串它以'\0'为结尾,只要发现的'\0'就返回。

cin

我们先来简单实现一个我们都爱犯的错误的代码

istream& operator>>(istream& is, string& s)
{char ch; is >> ch;s += ch;while (ch != '\0' && ch != '\n'){is >> ch;s += ch;}return is;
}

 

我们发现为什么一直得不到结果呢?因为我们的流输入操作,以空格或者换行为间隔,读取下一个,输入流(如键盘、文件)不会直接读取到 '\0''\0' 是字符串的结束符,不是输入字符)。

那我们该怎么解决呢?

c++io流中里面有一个get函数用来读取单个字符

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

 这里还存在一些问题就是,要把之前的数据清除掉。

这又得写个clear函数了

简单实现一下。

clear

void string::clear()
{_size = 0;_str[_size] = '\0';
}

这里就不实现缩容了,没必要。

 

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

最后一个小问题,我们如果频繁输入小的数据,就又会频繁扩容的问题出现,那又该怎么解决了,我们都不知道我们要输入多少的字符,也不能提前扩容。

我们可以实现一个内存池,比如开个255空间大小的内存池,当输入的小于255时就放在内存池中。

实现如下:

istream& operator>>(istream& is, string& s)
{s.clear();char buff[256];size_t i = 0;char ch = is.get();while (ch != '\0' && ch != '\n'){buff[i++] = ch;ch = is.get();if (i == 255){buff[i] = '\0';s += buff;i = 0;}}if (i > 0){buff[i] = '\0';s += buff;}return is;
}

over!感谢观看! 

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

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

相关文章

【nginx】服务的信号控制

目录 1. 说明2. 常用信号及作用3. 信号控制的具体操作3.1 获取 Nginx 主进程 PID3.2 发送信号 4. 应用场景4.1 重新加载配置文件4.2 日志切割 5. 平滑升级 Nginx6. 注意事项 1. 说明 1.Nginx 的信号控制是其管理服务的重要机制&#xff0c;通过向主进程发送特定信号&#xff0…

Ubuntu下展锐刷机工具spd_dump使用说明

spd_dump使用说明 源码地址&#xff1a;https://github.com/ilyakurdyukov/spreadtrum_flash 编译环境准备&#xff1a; sudo apt update sudo apt install git sudo apt install build-essential sudo apt install libusb-1.0-0-devIf you create /etc/udev/rules.d/80-spd…

鸿蒙NEXT开发LRUCache缓存工具类(单例模式)(ArkTs)

import { util } from kit.ArkTS;/*** LRUCache缓存工具类&#xff08;单例模式&#xff09;* author 鸿蒙布道师* since 2025/04/21*/ export class LRUCacheUtil {private static instance: LRUCacheUtil;private lruCache: util.LRUCache<string, any>;/*** 私有构造函…

笔记:react中 父组件怎么获取子组件中的属性或方法

在子组件中我们可以使用下面两个方法去暴露你所要放行的属性或方法&#x1f447; 1.useImperativeHandle 2.orwardRef 搭配使用例子 import React, { useState, forwardRef, useImperativeHandle } from "react"function Son(props, ref) {const [data] useStat…

《浔川代码编辑器v2.0内测(完整)报告》

一、测试概述 浔川代码编辑器v2.0经过为期五周的封闭内测&#xff0c;累计提交了186份测试报告。本次内测主要针对v2.0新增的多语言支持、AI辅助编码、性能优化等核心功能进行全面验证。 二、测试环境 - 硬件配置&#xff1a;i7-12700H/16GB RAM/512GB SSD - 操作系统&#xf…

ubuntu18.04安装QT问题汇总

1、Could not determine which ”make“ command to run. Check the ”make“ step in the build configuration.” sudo apt-get install clang sudo apt-get install build-essential sudo apt-get install libqt4-dev 2、fatal error: sqlite3.h: No such …

基于单片机的BMS热管理功能设计

标题:基于单片机的BMS热管理功能设计 内容:1.摘要 摘要&#xff1a;在电动汽车和储能系统中&#xff0c;电池管理系统&#xff08;BMS&#xff09;的热管理功能至关重要&#xff0c;它直接影响电池的性能、寿命和安全性。本文的目的是设计一种基于单片机的BMS热管理功能。采用…

CSS基础-即学即用 -- 笔记1

目录 前言CSS 基础1. 层叠样式表来源理解优先级源码顺序经验法则继承inherit 关键字initial 关键字 2. 相对单位em 和 rem响应式面板视口的相对单位使用vw定义字号使用calc()定义字号自定义属性&#xff08;即CSS变量&#xff09; 3. 盒模型调整盒模型 前言 只需一分钟就能学会…

Linux中服务器时间同步

简单介绍 在 redhat 8 之前&#xff0c;时间同步服务是使用 NTP&#xff08;网络时间协议&#xff09;来实现的&#xff0c;在 redhat 8 及之 后使用是 NTP 的实现工具 chrony 来实现时间同步。 在 redhat 8 及之后&#xff0c;默认情况下已经安装好 chrony 软件并已经开机启…

让SQL飞起来:搭建企业AI应用的SQL性能优化实战

我上一篇文章已经讲解过了如何使用公开的AI模型来优化SQL.但这个优化方法存在一定的局限性.因为公开的AI模型并不了解你的数据表结构是什么从而导致提供的优化建议不太准确.而sql表结构又是至关重要的安全问题,是不能泄露出去的.所以在此背景下我决定搭建一个自己的AI应用在内网…

小迪安全-112-yii反序列化链,某达oa,某商场,影响分析

yii是和tp一样的框架 入口文件 web目录下 相对tp比较简单一些&#xff0c;对比tp找一下他的url结构 对应的位置结构 这个contorllers文件的actionindex就是触发的方法 控制器&#xff0c;指向的index文件&#xff0c;就可以去视图模块看index文件 这就是前端展示的文件 自…

自定义多头注意力模型:从代码实现到训练优化

引言 在自然语言处理和序列生成任务中,自注意力机制(Self-Attention)是提升模型性能的关键技术。本文将通过一个自定义的PyTorch模型实现,展示如何构建一个结合多头注意力与前馈网络的序列生成模型(如文本或字符生成)。该模型通过创新的 MaxStateSuper 模块实现动态特征…

动态LOD策略细节层级控制:根据视角距离动态简化远距量子态渲染

动态LOD策略在量子计算可视化中的优化实现 1. 细节层级控制:动态简化远距量子态渲染 在量子计算的可视化中,量子态通常表现为高维数据(如布洛赫球面或多量子比特纠缠态)。动态LOD(Level of Detail)策略通过以下方式优化渲染性能: 距离驱动的几何简化: 远距离渲染:当…

Java 泛型使用教程

简介 Java 泛型是 JDK 5 引入的一项特性&#xff0c;它提供了编译时类型安全检测机制&#xff0c;允许在编译时检测出非法的类型。泛型的本质是参数化类型&#xff0c;也就是说所操作的数据类型被指定为一个参数。 泛型的好处&#xff1a; 编译期检查类型安全 避免强制类型转…

Leetcode - 周赛446

目录 一、3522. 执行指令后的得分二、3523. 非递减数组的最大长度三、3524. 求出数组的 X 值 I四、3525. 求出数组的 X 值 II 一、3522. 执行指令后的得分 题目链接 本题就是一道模拟题&#xff0c;代码如下&#xff1a; class Solution {public long calculateScore(String…

【更新完毕】2025泰迪杯数据挖掘竞赛A题数学建模思路代码文章教学:竞赛论文初步筛选系统

完整内容请看文末最后的推广群 基于自然语言处理的竞赛论文初步筛选系统 基于多模态分析的竞赛论文自动筛选与重复检测模型 摘要 随着大学生竞赛规模的不断扩大&#xff0c;参赛论文的数量激增&#xff0c;传统的人工筛选方法面临着工作量大、效率低且容易出错的问题。因此&…

计算机视觉与深度学习 | RNN原理,公式,代码,应用

RNN(循环神经网络)详解 一、原理 RNN(Recurrent Neural Network)是一种处理序列数据的神经网络,其核心思想是通过循环连接(隐藏状态)捕捉序列中的时序信息。每个时间步的隐藏状态 ( h_t ) 不仅依赖当前输入 ( x_t ),还依赖前一时间步的隐藏状态 ( h_{t-1} ),从而实现…

AI速读:解锁LLM下Game Agent的奇妙世界

在 AI 浪潮中&#xff0c;大语言模型&#xff08;LLMs&#xff09;正重塑游戏智能体格局。想知道基于 LLMs 的游戏智能体如何运作&#xff0c;在各类游戏中有何惊艳表现&#xff0c;未来又将走向何方&#xff1f; 大型语言模型&#xff08;LLMs&#xff09;的兴起为游戏智能体的…

【每日八股】复习计算机网络 Day3:TCP 协议的其他相关问题

文章目录 昨日内容复习TCP 的四次挥手&#xff1f;TCP 为什么要四次挥手&#xff1f;在客户端处于 FIN_WAIT_2 状态时&#xff0c;如果此时收到了乱序的来自服务端的 FIN 报文&#xff0c;客户端会如何处理&#xff1f;何时进入 TIME_WAIT 状态&#xff1f;TCP 四次挥手丢了怎么…

学习笔记十五——rust柯里化,看不懂 `fn add(x) -> impl Fn(y)` 的同学点进来!

&#x1f9e0; Rust 柯里化从零讲透&#xff1a;看不懂 fn add(x) -> impl Fn(y) 的同学点进来&#xff01; &#x1f354; 一、什么是柯里化&#xff1f;先用一个超好懂的生活比喻 假设你在点一个汉堡&#xff1a; 你说&#xff1a;我要点一个鸡腿汉堡&#xff01; 店员…