C++_String增删查改模拟实现

C++_String增删查改模拟实现

  • 前言
  • 一、string默认构造、析构函数、拷贝构造、赋值重载
    • 1.1 默认构造
    • 1.2 析构函数
    • 1.3 拷贝构造
    • 1.4 赋值重载
  • 二、迭代器和范围for
  • 三、元素相关:operator[ ]
  • 四、容量相关:size、resize、capacity、reserve
    • 4.1 size、capacity
    • 4.2 reserve
    • 4.3 resize
  • 五、数据相关:push_bach、append、operator+=、insert、erase
    • 5.1 尾插:push_back
    • 5.2 append尾部插入字符串
    • 5.3 operator+=()字符、字符串
    • 5.4 insert插入字符、字符串
      • 5.4.1 insert插入字符(在这提醒下,博主是所有的拷贝数据都是从'\0'开始,这样就不需要单独对'\0'做处理)
        • 初学者最容易范的一个错误
      • 5.4.2 insert插入字符串
    • 5.5 erase
  • 六、 关系操作符重载:< 、 ==、 <=、 >、>=、!=
  • 七、find查找字符、字符串、substr
    • 7.1 find查找字符
    • 7.2 find查找字符串
    • 7.3 strsub( ) 模拟实现
  • 八、流插入和流提取(<<、>>)(实现在string类外)
    • 8.1 流插入<<
    • 8.1 流提取>>
      • 优化
  • 九、所有代码

前言

本篇博客仅仅实现存储字符的string。同时由于C++string库设计的不合理,博主仅实现一些最常见的增删查改接口!
接下来给出的接口都是基于以下框架:

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

一、string默认构造、析构函数、拷贝构造、赋值重载

1.1 默认构造

博主在这仅仅提供如无参和带参默认构造接口:

//无参默认构造
string():_str(new char[1]{'\0'}),_capacity(0),_size(0)
{ }
//带参默认构造
string(const char* str = ""):_capacity(strlen(str)),_size(_capacity)
{_str = new char[_capacity + 1];strcpy(_str, str);
}

小tips:

  1. C++string标准库中,无参构造并不是空间为0,直接置为空指针。而是开一个字节,并存放‘\0’。(C++中支持无参构造一个对象后,直接在后面插入数据,也从侧面说明了这点)
  2. 由于C++构造函数不管写不写都会走初始化列表的特性,所以这里博主也走初始化列表。
  3. string中,_capacity和_size都不包含空指针,所以带参构造要多开一个空间,用来存储’\0’。

1.2 析构函数

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

1.3 拷贝构造

传统写法:

string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}

现代写法:现代写法的核心在于:将拷贝数据的工作交给别人来做,最后将成果交换一样即可。

//交换
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)
{string tmp(s._str);swap(tmp);
}

tips:现代写法中,拷贝构造是数据需初始化为空。原因在于C++中,编译器对内置类型不会做处理(个别如vs2019等编译器会做处理的),这也就意味这_str是一个随机值,指向任意一块空间。调用析构函数时会报错。

1.4 赋值重载

赋值重载同样分为传统写法和现代写法。
传统写法:

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

现代写法:

//现代写法
//法一
/*string& operator=(const string& s)
{if (this != &s){string tmp(s._str);swap(tmp);}return *this;
}*///法二
string& operator=(string tmp)
{swap(tmp);return *this;
}

二、迭代器和范围for

在C++中,范围for在底层是通过迭代器来实现的。所以只要实现了迭代器,就支持范围for。
而迭代器类似于指针,迭代器可以被看作是指针的一种泛化,它提供了类似指针的功能,可以进行解引用操作、指针运算等。
 
以下提供了const迭代器和非const迭代器:

typedef char* iterator;
const typedef 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;}

三、元素相关:operator[ ]

这里我们和库中一样,提供以下两个版本

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

四、容量相关:size、resize、capacity、reserve

4.1 size、capacity

size_t size()const
{return _size;
}
size_t capacity()const
{return _capacity;
}

4.2 reserve

在C++中,我们一般不缩容。
所以实现reserve时(容量调整到n),首先判断目标容量n是否大于当前容量。如果小于就不做处理,否则先开辟n+1个内存空间(多出来的一个用于存储‘\0’),然后将原有数据拷贝到新空间(strcpy会将’\0’一并拷贝过去)。然后释放就空间,并让_str指向新空间,同时更新_capacity。

void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}

4.3 resize

resize到目标大小分为以下3中情况:
在这里插入图片描述

  1. 当n<_size时,只需将下标为n的地址处的数据改为’\0’。
  2. 其他情况,我们直接统一处理。直接复用reserve()函数将_capacity扩到n。然后用将[_size, n)中的数据全部初始化为ch。(这里博主给ch一个初始值’\0’,但ch不一定为’\0’,所以要将下标为n处的地址初始化为’\0’)
void resize(size_t n, char ch='\0')
{if (n <= _size){_str[n] = '\0';_size = n;}else{reserve(n);while (_size < n){_str[_size] = ch;_size++;}_str[_size] = '\0';}
}

五、数据相关:push_bach、append、operator+=、insert、erase

5.1 尾插:push_back

尾插首先检查扩容,在插入数据

void push_back(char ch)
{//扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//插入数据_str[_size] = ch;_size++;_str[_size] = '\0';}

5.2 append尾部插入字符串

void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity)//扩容{reserve(_size + len);}strcpy(_str + _size, str);_size += len;
}

5.3 operator+=()字符、字符串

operator+=()字符、字符串可以直接复用push_back和append。

string& operator+=(char ch)
{push_back(ch);return *this;
}string& operator+=(const char* str)
{append(str);return *this;
}

5.4 insert插入字符、字符串

5.4.1 insert插入字符(在这提醒下,博主是所有的拷贝数据都是从’\0’开始,这样就不需要单独对’\0’做处理)

insert插入字符逻辑上还是很简单的。
首先判断插入字符时是否需要扩容。然后从下标为pos开始,所有数据依次往后挪动。最后将待插入字符给到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+1] = _str[end];end--;}_str[pos] = ch;_size++
}

这样对吗?答案是错误的。

假设是在头插字符,end理论上和pos(即0)比较完后就减到-1,在下一次循环条件比较时失败,退出循环。
遗憾的是end是size_t类型,始终>=0, 会导致死循环。

博主在这给出两种解决方法:

  1. 将pos强转为整型。
void insert(size_t pos, char ch)
{assert(pos <= _size);//扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//挪动数据int end = _size;while (end >= (int)pos){_str[end+1] = _str[end];end--;}_str[pos] = ch;_size++
}

2.从end从最后数据的后一位开始,每次将前一个数据移到当前位置。最后条件判断就转化为end>pos,不会出现死循环这种情况。
在这里插入图片描述

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--;}//插入数据,更新_size_str[pos] = ch;_size++;
}

5.4.2 insert插入字符串

insert同样存在相同问题,并且思路一样。博主就直接给出代码了。
法一:

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

法二:

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

5.5 erase

erase分两种情况:

  1. 从pos开始,要删的数据个数超过的字符串,即将pos后序所有数据全部情况。(直接将pos处数据置为’\0’即可)
  2. 从pos开始,要删的数据个数没有超出的字符串。所以只需要从pos+len位置后的所有数据向前移动从pos位置覆盖原数据即可。
void erase(size_t pos, size_t len = npos)
{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;}
}

六、 关系操作符重载:< 、 ==、 <=、 >、>=、!=

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

七、find查找字符、字符串、substr

7.1 find查找字符

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

7.2 find查找字符串

size_t find(const char* sub, size_t pos = 0)
{const char* p = strstr(_str + pos, sub);if (p){return p - _str;}else{return npos;}
}

7.3 strsub( ) 模拟实现

strsub目标长度可能越界string,也可能还有没有。但不管是那种情况,最后都需要拷贝数据。所以这里我们可以先将len真实长度计算出来,在拷贝数据。

string substr(size_t pos, size_t len = npos)const
{string s;size_t end = pos + len;//目标字符越界string,更新lenif (len == npos || end >= _size){len = _size - pos;end = _size;}//拷贝数据s.reserve(len);for (size_t i = pos; i < end; i++){s += _str[i];}return s;
}

八、流插入和流提取(<<、>>)(实现在string类外)

8.1 流插入<<

由于前面我们实现了迭代器,所以最简单的方式就是范围for

ostream& operator<<(ostream& out, const string& s)
{/*for (size_t i = 0; i < s.size(); i++){out << s[i];}*/for (auto ch : s)out << ch;return out;
}

8.1 流提取>>

流提取比较特殊。在流提取前需要将原有数据全部清空。同时由于>>无法获取空字符和换行符()(都是作为多个值之间的间隔),直接流提取到ostream对象中,没法结束。(类似于C语言中scanf, 换行符和空字符仅仅只是起到判断结束的作用,但scanf无法获取到它们)
所以这里博主直接调用istream对象中的get()函数。(类似于C语言中的getchar()函数)
get详细文档
在这里插入图片描述

class string
{void clear(){_str[0] = '\0';_size = 0;}
private:char* _str;size_t _capacity;size_t _size;
};istream& operator>>(istream& in, string& s){s.clear();char ch;//in >> ch;ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;//in >> ch;ch = in.get();}return in;}

上面这种方法虽然可以达到目的。但还有一个问题,每次插入数据都面临可扩容问题。那如何优化呢?

优化

其中一种办法就是调用reserve()提前开好空间,但这样面临这另一个问题:开大了浪费空间;开小了,同样面临这扩容的问题。
所以在这博主采用和vs底层实现的思路:首先开好一段数组(包含’\0’,以16为例)。当数据个数小于16时,字符串存在数组中;当数据个数大于等于16时,将数据存在_str指向的空间。
这是一种以空间换时间的思路,同时也能很好的减少内存碎片的问题。

class string
{void clear(){_str[0] = '\0';_size = 0;}
private:char* _str;size_t _capacity;size_t _size;
};istream& operator>>(istream& in, string& s)
{s.clear();char buff[16];size_t i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 16){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;
}

九、所有代码

namespace achieveString
{class string{public:typedef char* iterator;const typedef 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;}//构造函数/*string():_str(new char[1]{'\0'}),_capacity(0),_size(0){ }*/string(const char* str = ""):_capacity(strlen(str)), _size(_capacity){_str = new char[_capacity + 1];strcpy(_str, str);}const char* c_str() const{return _str;}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//拷贝构造/*string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}*///交换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){string tmp(s._str);swap(tmp);}// 赋值重载/*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;}*///现代写法//法一/*string& operator=(const string& s){if (this != &s){string tmp(s._str);swap(tmp);}return *this;}*///法二string& operator=(string tmp){swap(tmp);return *this;}//可读可写char operator[](size_t pos){assert(pos < _size);return _str[pos];}//只读const char operator[](size_t pos)const{assert(pos < _size);return _str[pos];}size_t size()const{return _size;}size_t capacity()const{return _capacity;}bool empty()const{return _size == 0;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char ch='\0'){if (n <= _size){_str[n] = '\0';_size = n;}else{reserve(n);while (_size < n){_str[_size] = ch;_size++;}_str[_size] = '\0';}}void push_back(char ch){//扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//插入数据_str[_size] = ch;_size++;_str[_size] = '\0';}void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}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--;}//插入数据,更新_size_str[pos] = ch;_size++;}void insert(size_t pos, const char* str){int len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//法一/*int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];end--;}strncpy(_str + pos, str, len);_size += len;*///法二size_t end = _size+1;while (end > pos){_str[end + len-1] = _str[end-1];end--;}strncpy(_str + pos, str, len);_size += len;}void erase(size_t pos, size_t len = npos){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;}}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);}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;}size_t find(const char* sub, size_t pos = 0){const char* p = strstr(_str + pos, sub);if (p){return p - _str;}else{return npos;}}string substr(size_t pos, size_t len = npos){string s;size_t end = pos + len;if (len == npos || end >= _size){len = _size - pos;end = _size;}s.reserve(len);for (size_t i = pos; i < end; i++){s += _str[i];}return s;}void clear(){_str[0] = '\0';_size = 0;}private:char* _str;size_t _capacity;size_t _size;//const static size_t npos = -1;  // C++支持const整型静态变量在声明时给值初始化,但不建议//const static double npos = 1.1;  // 不支持const static size_t npos;};const size_t string::npos = -1;ostream& operator<<(ostream& out, const string& s){/*for (size_t i = 0; i < s.size(); i++){out << s[i];}*/for (auto ch : s)out << ch;return out;}//istream& operator>>(istream& in, string& s)//{//	s.clear();//	char ch;//	//in >> ch;//	ch = in.get();//	while (ch != ' ' && ch != '\n')//	{//		s += ch;//		//in >> ch;//		ch = in.get();//	}//	return in;//}istream& operator>>(istream& in, string& s){s.clear();char buff[16];size_t i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 16){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
}

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

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

相关文章

斯坦福NLP课程来了

生成式AI&#xff0c;尤其是以ChatGPT为首的大语言模型正在改变人们的生活方式&#xff0c;我想一定有小伙伴想加入NLP这个行列。 微软重磅发布4个适合初学者的机器学习资料 我在前一篇文章中分享了微软人工智能初学者课程&#xff0c;其中的【生成式AI】非常适合初学者&…

五大匹配算法

五大匹配算法 五大匹配算法 BF 匹配、RK 匹配、KMP 匹配、BM 匹配、Sunday 匹配。 1、BF 匹配 // BF 匹配&#xff08;暴力匹配&#xff09; public static int bfMatch(String text, String pattern) {char[] t text.toCharArray();char[] p pattern.toCharArray();int i …

CCF CSP认证 历年题目自练Day48

题目 试题编号&#xff1a; 201803-3 试题名称&#xff1a; URL映射 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 256.0MB 问题描述&#xff1a; 问题描述   URL 映射是诸如 Django、Ruby on Rails 等网页框架 (web frameworks) 的一个重要组件。对于从浏览器发来的 H…

渗透测试高级技巧(二):对抗前端动态密钥与非对称加密防护

在前文的技术分享中&#xff0c;我们描述了验签和静态对称加密&#xff08;静态密钥 AES&#xff09;的常见场景&#xff0c;大家我相信遇到类似的加解密清醒&#xff0c;基本都可以通过热加载的基本使用获得破解前端加密解密的方法&#xff0c;达到一个比较好的测试状态。 在…

定时器的使用

目录 前言 正文 1.方法 schedule(TimerTask task, Date time) 的测试 &#xff08;1&#xff09;执行任务的时间晚于当前时间(在未来执行)的效果 &#xff08;2&#xff09;线程TimerThread不销毁的原因 &#xff08;3&#xff09;使用 public void cancel() 方法实现 T…

阿里云高效计划学生和老师免费代金券申请认证方法

阿里云高校计划学生和教师均可参与&#xff0c;完成学生认证和教师验证后学生可以免费领取300元无门槛代金券和3折优惠折扣&#xff0c;适用于云服务器等全量公共云产品&#xff0c;订单原价金额封顶5000元/年&#xff0c;阿里云百科aliyunbaike.com分享阿里云高校计划入口及学…

不是说人工智能是风口吗,那为什么工作还那么难找?

最近确实有很多媒体、机构渲染人工智能可以拿高薪&#xff0c;这在行业内也是事实&#xff0c;但前提是你有足够的竞争力&#xff0c;真的懂人工智能。 首先&#xff0c;人工智能岗位技能要求高&#xff0c;人工智能是一个涵盖了多个学科领域的综合性学科&#xff0c;包括数学、…

LiteOS同步实验(实现生产者-消费者问题)

效果如下图&#xff1a; 给大家解释一下上述效果&#xff1a;在左侧&#xff08;顶格&#xff09;的是生产者&#xff08;Producer&#xff09;&#xff1b;在右侧&#xff08;空格&#xff09;的是消费者&#xff08;Consumer&#xff09;。生产者有1个&#xff0c;代号为“0”…

Scala如何写一个通用的游戏数据爬虫程序

以前想要获取一些网站数据的时候&#xff0c;都是通过人工手动复制粘贴&#xff0c;这样的效率及其低下。数据少无所谓&#xff0c;如果需要采集大量数据&#xff0c;手动就显得乏力了。半夜睡不着&#xff0c;爬起来写一段有关游戏商品数据的爬虫通用模板&#xff0c;希望能帮…

Git——感谢尚硅谷官方文档

Git——尚硅谷学习笔记 第1章 Git 概述1.1 何为版本控制1.2 为什么需要版本控制1.3 版本控制工具1.4 Git 简史1.5 Git 工作机制1.6 Git 和代码托管中心 第2章 Git 安装第 3 章 Git 常用命令3.1 设置用户签名3.2 初始化本地库3.3 查看本地库状态3.4 添加暂存区3.4.1 将工作区的文…

JavaDS —— 初识集合框架 + 时间/空间复杂度

目录 1. 初识集合框架 1.1 集合框架的初识 1.2 什么是数据结构&#xff1f; 2. 时间与空间复杂度 2.1 时间复杂度 2.2 大O的渐进表示法 2.3 常见时间复杂度计算举例 2.4 空间复杂度 1. 初识集合框架 1.1 集合框架的初识 什么叫集合&#xff1f;什么叫框架&#xff1f;什么又叫集…

React函数组件渲染两次

渲染两次是因为react默认开启了严格模式 React.StrictMode标签作用&#xff1a; 1、识别不安全的生命周期 2、关于使用过时字符串 ref API 的警告 3、关于使用废弃的 findDOMNode 方法的警告 4、检测意外的副作用 5、检测过时的 context API 注释掉React.StrictMode即为关闭严…