当前位置: 首页 > news >正文

C++:模拟实现string

目录

命名空间的使用

成员变量

构造函数

析构函数

拷贝构造函数

string里的swap函数

赋值运算符重载

迭代器

数组容量

size和capacity

empty

reserve

resize

增删查改

push_back

append

+=运算符重载

clear

insert

插入字符

插入字符串

erase

substr

[]运算符重载

find

c_str

关系运算符

重载流插入、流提取


命名空间的使用

为了防止和标准库中的string出现冲突,我们可以在一个命名空间模拟实现string。

​
namespace zh
{class string{};
}​

成员变量


​namespace zh
{class string{private:char* _str;size_t _size;size_t _capacity;static const size_t npos;};const  size_t string::npos = -1;
}​​​​

string本质上就是一个可以动态扩容字符数组,所以我们用一个char*类型的指针来代表该数组_size代表有效字符的个数_capacity代表数组最多容纳有效字符的个数,npos代表一个很大的数(用法等下会提及)。

两个需要注意的点:

1.关于_cpacity的理解,_cpacity != 字符数组的大小。

我们都知道字符串是以’\0‘为结束标记的,也要占用空间,而_capacity代表数组最多容纳有效字符的个数,’\0‘不是有效字符,所以字符数组的大小是要比_capacity大(字符数组大小 == _capacity + 1),我们在写构造函数动态申请空间的时候,要开辟(_capacity + 1)个字节的空间。

2.有关nops的注意事项。

nops的类型是const size_t,是一个静态成员变量。我们都知道静态成员变量是不能在类成员变量声明里给缺省值的,因为静态成员变量不走初始化列表,要走声明定义分离

但是对于对于静态的const size_t,是可以在类里给缺省值的,可以看做定义,如下。

class string
{
private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;static const size_t npos = -1;  //static const可以这样干  ->编译器特殊处理,看作定义,double都不可以,int才可以.//static const size_t npos;  //静态的不能在类里面给缺省值,静态的值不走初始化列表,走声明定义分离
};

构造函数

构造函数我们实现无参构造和带参字符串构造,这两个构造函数可以合在一起。

//无参和有参数合并写成全缺省
string(const char* str = "") //"" 代表空字符串
{_size = strlen(str);//capacoty不包含\0_capacity = _size;_str = new char[_capacity + 1];  //空间要多开一个strcpy(_str, str);               //strcpy \0也会拷上 先拷贝再判断
}

析构函数

因为是动态开辟的资源,所以析构函数需要我们自己写,手动释放动态开辟的资源。

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

拷贝构造函数

对于成员变量含有指针的,编译器自动生成的拷贝构造是浅拷贝(值的拷贝),也就是说两个string的_str的值是一样的,都指向同一块空间,那当我们析构这两个对象时就会发生报错,因为对同一块空间析构了两次。

string里的swap函数

所以我们要手动写拷贝构造函数来实现深拷贝,但是在此之前,我们先实现一个交换函数,用于交换两个string。

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

那为什么不能用标准库里面的交换函数,而要自己写?

我们看库里交换函数的具体代码:

标准库里的交换函数涉及拷贝构造和赋值重载,损耗大,不建议使用。

有了这个交换函数,我们的拷贝构造就有了两种写法。

传统写法

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

先开辟和str一样大的空间,然后将str拷贝到_str,最后处理_size和_capacity。

新颖写法

//拷贝构造
string(const string& str)
{//新颖写法string tmp(str._str);swap(tmp);
}

先用str对象的_str指针构造一个临时对象tmp(调用构造函数),然后调用交换函数交换待初始化的对象this和tmp对象,这样就完成了拷贝构造,tmp对象也不用我们多做处理,函数结束后会自动调用析构函数将其清理。

最后析构tmp的时候,由于交换后tmp._str指针可能是随机值从而导致程序崩溃,所以我们在成员变量声明时,要给上缺省值,让它们走初始化列表,确保程序不会崩溃

赋值运算符重载

赋值重载也涉及深浅拷贝问题,编译器自动生成的赋值重载不能满足需求,所以我们要手动实现赋值重载

赋值重载也有两种写法。

传统写法

​
//赋值重载 注意自己给自己赋值的情况
string& operator=(string& str)
{//传统写法if (this != &str)                     {delete[] _str;_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}return *this;
}​

先把delete掉原来的资源,然后开辟跟str一样大的空间将str拷贝到_str处理_size和_capacity,最后返回*this。

注意:一定要释放原来的资源先,不然会造成内存泄漏。

新颖写法

​
//赋值重载 注意自己给自己赋值的情况
string& operator=(string str)
{//新颖写法swap(str);return *this;
}​

相比较于传统写法,我们传入的参数是string对象,不是对象的引用,所以我们传参的时候是传值调用,传值调用会调用拷贝构造,所以函数里的str是一个临时string对象,然后我们交换this对象和str对象完成了赋值重载

函数结束后str对象会调用析构函数完成清理工作。

迭代器

迭代器(Iterator)是一种设计模式,它提供了一种访问容器中元素的方法,而不需要暴露容器的内部结构。

string迭代器的使用

string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{cout << *it << " ";it++;
}
cout << endl;

 (begin()返回首字符的迭代器,end()返回字符数组最后一个有效字符的下一位置的迭代器,类比指针即可)

迭代器也是一种在string的类型,使用迭代器的方式类似于指针,所以我们实现的string类只需要对指针进行封装即可。

标准库里有正向迭代器和反向迭代器,我们这里只模拟实现正向迭代器。

我们模拟实现的迭代器也有两个版本,普通版本和const版本,类比于普通指针和const 指针,普通迭代器指向的元素允许读也允许写,而const迭代器指向的元素只允许读不允许写

两个版本的迭代器最好根据对象性质匹配使用,不然有可能触发权限放大导致编译不通过

权限可以缩小,但不能放大。也就是const迭代器既可以接收const对象可以接收普通对象,但是普通迭代器只允许接收普通对象接收const对象就是权限放大

迭代器具体代码如下:

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

注意对于const迭代器的begin()和end()必须在函数后面加const,否则不构成函数重载。而且普通this指针接收const对象触发权限放大。

数组容量

size和capacity

获取当前有效字符的个数最大容纳有效字符的个数。

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

empty

判断字符数组是否为空(就是判断是否是空字符)。

        bool empty() const//不仅要被我们的普通对象调用,也要被我们的const对象调用,所以加const{return _size == 0;}

reserve

用于空间不足时的扩容函数。

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

开辟一块大小为n+1的空间,将字符串拷贝到新空间,释放原来的资源,处理_str和_capacity。

resize

控制有效数据个数

  1. 若n<_size,直接删除数据,将有效位置的下一位置赋值成'\0',改变_size。
  2. 若_size<n<capacity,从原来的_size依次填写直至达到n个,最后一位置为'\0',改变_size。
  3. 若n>capacity,我们只需要扩容+初始化即可。

情况2 3可以放在reserve函数中。 

void resize(size_t n, char ch = '\0')
{if (n < _size){_str[n] = '\0';_size = n;}else{reserve(n);for (int i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}
}

增删查改

push_back

在字符串后面尾插一个字符,插入之前检查容量是否已满满了的话就调用reserve函数扩容,最后记得在数组末尾补上一个'\0'

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

append

在字符串后面添加一段新字符串添加之前要判满,满了就扩容,记得在数组末尾补'\0'。

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

先计算出新字符串的长度len,若_size + len大于2倍_capacity,扩成_size + len大小,若小于则扩成2倍_capacity大小。

这种方式的可以在一定程度上减少扩容的次数。

+=运算符重载

对于string类,我们可以+=一个字符或者一段字符串,所以这个函数会构成函数重载。

+=可以复用push_back函数和append函数。

string& string::operator+=(char ch) 
{push_back(ch);return *this; //注意我们的+=需要返回+=后的值
}string& string::operator+=(const char* str)
{append(str);return *this;
}

clear

请空字符串,将首元素位置赋值为'\0',将_size置为0即可。

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

insert

insert分为两种,一种是在pos位置插入字符ch,一种是在pos插入一段新字符串

插入字符

先来个错误示范

当pos为0时,程序就会挂掉,因为end是int类型pos是size_t类型在C/C++中,两个类型不一致的数发生比较会发生隐式类型转换,有符号的会被转换成无符号的,我们希望end到-1时结束循环,但是end被转换成无符号的,-1就是INT_MAX循环永远结束不了

有两种解决方法,一是把pos强转成int,二是在实参那里修改pos的类型为int。

 也可以改成下面的写法:

void string::insert(size_t pos, char ch)
{assert(pos <= _size);    //判断pos是否越界if (_size == _capacity)  //插入前先判断容量大小,满了就扩容{reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;                   //从'\0'开始挪动,这里的end是'\0'的下一位置while (end > pos)            //pos必须强转,防止end类型转换成size_t,循环条件就永远成立了{_str[end] = _str[end - 1];--end;}_str[pos] = ch;_size++;
}

插入完毕后记得让_size++; 

插入字符串

计算出新插入字符串的长度len判断_size + _len是否大于_capacity大于则扩容

挪动数据,插入新字符串,最后_size += len。

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

erase

删除pos置开始的len长个字符

删除需要分情况讨论:

  1. 当删除的长度len >= 剩下元素_size - pos时删除包括pos和之后的所有元素直接将pos位置的元素置为'\0'将_size更新为pos。当传入len时,函数就会调用缺省值npos,npos为INT_MAX,也是删除包括pos和之后的所有元素,npos就是在这里发挥作用
  2. 当len < _size - pos时,挪动数据删除即可。
​
void string::erase(size_t pos, size_t len = npos)
{assert(pos < _size);if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; ++i){_str[i - len] = _str[i];}}_size -= len;
}​

删除完成更新_size。

substr

截取从pos位置开始的len长度的字符串,返回一个string对象。

如果len >=  _size - pos,也就是说从pos位置截取的长度大于剩余元素的长度,要将len更新为剩余元素的长度_size - pos,因为你不能没有元素了还截取。

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

[]运算符重载

string本质上是对字符数组的封装,所以应该对[]进行重载从而支持随机访问。

我们也应该实现普通版本const版本

char& operator[](size_t pos)    //&为了支持修改 
{assert(pos >= 0 && pos < _size);return _str[pos];
}const char& operator[](size_t pos) const
{assert(pos >= 0 && pos < _size);return _str[pos];
}

find

find也实现两个版本,一是从pos位置查找单个字符,二是从pos位置查找字符串(用C语言的strstr函数解决即可)。

size_t string::find(char ch, size_t pos)
{assert(pos < _size);for (size_t i = pos; i < _size; ++i){if (_str[i] == ch) return i;}return npos;
}size_t string::find(const char* str, size_t pos)
{assert(pos < _size); const char* ptr = strstr(_str + pos, str);if (ptr == nullptr)return npos;	return ptr - _str;       //返回下标
}

c_str

返回_str这个字符指针

​const char* c_str() const{return _str;}​

关系运算符

重载> < ==这些关系运算符。

这些函数写在类外面,配合刚刚的c_str使用

bool operator<(const string& s1, const string& s2)
{return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{return !(s1 < s2);
}
bool operator==(const string& s1, const string& s2)
{return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}

重载流插入、流提取

这两个函数也是要写在类外面的。

流插入

实现类似cout << string对象的效果。

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

流提取

实现类似于cin >> string对象的效果

istream& operator>>(istream& in, string& str)  //cin scanf提取任何类型的值,默认空格或者换行都是分隔符
{                                            //空格换行读到了,被忽略了str.clear();char ch;//in >> ch;		ch = in.get();                           //get一个字符一个字符的读,跟getc一样的道理const int N = 256;char buff[N];int index = 0;while (ch != ' '&& ch != '\n'){//str += ch;buff[index++] = ch;if (index == N - 1){buff[index] = '\0';str += buff;	index = 0;}ch = in.get();}if (index > 0){buff[index] = '\0';str += buff;}return in;
}

为了扩容不那么频繁,我们使用一个buff数组来缓解这个问题。

注意:cin 和 scanf是读不到空格和换行符的,必须用get函数才能一个字符一个字符的读。


拜拜,下期再见😏

摸鱼ing😴✨🎞

相关文章:

  • 如何使用 WebRTC 获取摄像头视频
  • 用Promise实现前端并发请求
  • 老古董Lisp实用主义入门教程(12):白日梦先生的白日梦
  • C++11标准模板(STL)- 常用数学函数 - 计算一个数的给定次幂 (xy)(std::pow, std::powf, std::powl)
  • Autosar EcuM学习笔记-上电初始化执行函数及下电前执行函数
  • 逆变器控制技术
  • 数据结构与算法——Java实现 24.中缀表达式转后缀
  • Python | 第八章 | 数据容器
  • 爬虫入门 Selenium使用
  • 906. 超级回文数
  • 算法复杂度-空间
  • JAVA红娘婚恋相亲交友系统源码全面解析
  • Java语法-类和对象之抽象类和接口
  • 【软件测试】详解软件测试中的测试级别
  • Stable Diffusion 优秀博客转载
  • Java 多线程编程之:notify 和 wait 用法
  • Perseus-BERT——业内性能极致优化的BERT训练方案
  • Sequelize 中文文档 v4 - Getting started - 入门
  • Windows Containers 大冒险: 容器网络
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 构建工具 - 收藏集 - 掘金
  • 入门到放弃node系列之Hello Word篇
  • 用 vue 组件自定义 v-model, 实现一个 Tab 组件。
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • hi-nginx-1.3.4编译安装
  • 如何正确理解,内页权重高于首页?
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​MySQL主从复制一致性检测
  • #我与Java虚拟机的故事#连载06:收获颇多的经典之作
  • (02)Hive SQL编译成MapReduce任务的过程
  • (55)MOS管专题--->(10)MOS管的封装
  • (php伪随机数生成)[GWCTF 2019]枯燥的抽奖
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (算法)Game
  • (转)母版页和相对路径
  • ./configure,make,make install的作用
  • .NET Core 成都线下面基会拉开序幕
  • .NET 将混合了多个不同平台(Windows Mac Linux)的文件 目录的路径格式化成同一个平台下的路径
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • .net开发时的诡异问题,button的onclick事件无效
  • .ui文件相关
  • [20140403]查询是否产生日志
  • [2024-06]-[大模型]-[Ollama] 0-相关命令
  • [28期] lamp兄弟连28期学员手册,请大家务必看一下
  • [④ADRV902x]: Digital Filter Configuration(发射端)
  • [Android Pro] android 混淆文件project.properties和proguard-project.txt
  • [Android] Implementation vs API dependency
  • [Android]Tool-Systrace
  • [ASP.NET 控件实作 Day7] 设定工具箱的控件图标
  • [BJDCTF2020]Easy MD51
  • [BZOJ 1032][JSOI2007]祖码Zuma(区间Dp)
  • [bzoj1006]: [HNOI2008]神奇的国度(最大势算法)
  • [C++ 从入门到精通] 12.重载运算符、赋值运算符重载、析构函数
  • [dfs搜索寻找矩阵中最长递减序列]魔法森林的秘密路径
  • [Django 0-1] Core.Checks 模块