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

string的增删改查模拟实现(简单版)【C++】

目录

1. string的私有成员

2. 构造函数和析构函数

2.1 构造函数

2.1.1 无参构造函数 和 带参构造函数

2.1.2 全缺省构造函数

2.2 析构函数

3. string遍历

 3.1 size()、length() 和 operator[ ]

3.2 迭代器遍历

3.3 遍历测试

4. string的增删改查

4.1 insert

4.2 push_back 和 append

4.3 operator+=

4.4 erase

4.5 resize

4.6 swap

4.7 find

4.8 substr

5. 深浅拷贝问题

6. 比较关系运算符重载

6.1 operator==

6.2 operator<

6.3 其他关系运算符重载

7. 流插入、流提取和getline

7.1 流插入

7.2 流提取

7.3 getline

8. 源码


string的底层就是一个动态增长字符数组。首先在自己创建的mystring的命名空间中创建一个头文件string.h,再定义一个string类,同时创建一个test.cpp文件来调用测试。

1. string的私有成员

namespace mystring {class string {public://成员函数private://string的底层就是动态增长的字符数组char* _str;	  //指向一个动态分配的字符数组size_t _size; //字符串的实际长度(不包括'\0')size_t _capacity;	//分配的内存容量};//测试函数void test_string1() {}
}

其中size_t是无符号整型。

2. 构造函数和析构函数

2.1 构造函数

使用构造函数来进行初始化,同时使用初始化列表

	class string {public://成员函数//2.1 构造函数//无参构造函数string():_str(nullptr),_size(0),_capacity(0){}//带参构造函数string(const char* str) :_str(str),_size(strlen(str)),_capacity(strlen(str)){}private://string的底层就是动态增长的字符数组//1 私有成员char* _str;	  //指向一个动态分配的字符数组size_t _size; //字符串的实际长度(不包括'\0')size_t _capacity;	//分配的内存容量};

这时会出现一个问题 如图: 

  • const char* 不能改成 char* 因为这样可能会导致参数字符串被改变,得不到原来想要的参数。
  • 也不能把 私有成员 char* 改成 const char* ,因为我们想去修改字符串的时候,发现无法修改。

所以应该去开辟一个新空间,拷贝数据。正确方法:

		//带参构造函数string(const char* str) :_str(new char[strlen(str)+1])//加1原因是为了存储'\0',与C语言风格字符串匹配,_size(strlen(str)),_capacity(strlen(str)){strcpy(_str,str);	//拷贝数据}

这里发现调用了三次strlen,优化版本

错误示例:

		//错误示例:string(const char* str):_size(strlen(str)),_str(new char[_size+1]), _capacity(strlen(str)){strcpy(_str, str);	//拷贝数据}

所以 在构造函数上的初始化列表上的初始化顺序 最好和私有成员定义的顺序一致。

正确写法:

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

2.1.1 无参构造函数 和 带参构造函数
		//无参构造函数string():_str(nullptr),_size(0),_capacity(0){}string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_capacity + 1];strcpy(_str,str);}

但是,当定义无参对象的时候仍有问题

string s1;

这里借助获取字符串的c_str()成员函数来检测

#include<iostream>namespace mystring {class string {public://成员函数//2.1 构造函数//无参构造函数string():_str(nullptr),_size(0),_capacity(0){}string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_capacity + 1];strcpy(_str,str);}const char* c_str() {return _str;}private://string的底层就是动态增长的字符数组//1 私有成员char* _str;	  //指向一个动态分配的字符数组size_t _size; //字符串的实际长度(不包括'\0')size_t _capacity;	//分配的内存容量};//测试函数void test_string1() {string s1;std::cout << s1.c_str() << std::endl;}
}

可以将无参构造函数带参构造函数合并为一个全缺省构造函数(即所有参数都有默认值的构造函数)

2.1.2 全缺省构造函数

在写全缺省构造函数的时候也要注意:

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

strlen(nullptr) 这会导致程序崩溃,strlen的参数必须指向合法的C字符串(以 \0 结尾)

正确写法:

		string(const char* str = "")	//也可以"\0":_size(strlen(str)){_capacity = _size;_str = new char[_capacity + 1];strcpy(_str,str);}

2.2 析构函数

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

3. string遍历

 3.1 size()、length() 和 operator[ ]

在成员函数中进行实现

		//遍历size_t size() const{return _size;}size_t length() const{return _size;}//返回pos位置的字符 并且要可读可写char& operator[](size_t pos) //char& 返回引用 说明,{//断言检查有没有越界assert(pos < _size);	//开销不大,因为会变成内联函数return _str[pos];//_str[pos]访问的是堆上面的空间,出了作用域还在,就说明可以使用引用返回// 返回值类型是引用,返回的不是拷贝,返回的是别名,就说明可读可写}

传统的c语言的越界检查是一种给抽查(结束位置进行,看变量有没有被修改)

而C++函数封装的时候,可以加上越界判断,使用assert(#include<assert.h>)

3.2 迭代器遍历

迭代器是像指针一样,有可能是指针,也有可能不是指针,这里借助指针。

		//成员函数//指针版本的迭代器typedef char* iterator;typedef const char* const_iterator;//普通对象版本iterator begin() {return _str;}iterator end(){return _str + _size;}//const对象版本const_iterator begin() const    //第二个const是防止修改_str{return _str;}const_iterator end() const {return _str + _size;}

3.3 遍历测试

	//测试访问void test_string1() {//string s1;//cout << s1.c_str() << endl;//测试遍历string s1 = "hello world";for (size_t i = 0; i < s1.size(); i++){cout << s1[i] << " ";}cout << "\n";for (size_t i = 0; i < s1.size(); i++){cout << ++s1[i] << " ";}cout << "\n";//普通对象迭代器遍历+修改string::iterator it1 = s1.begin();while (it1 != s1.end()) {cout << *it1 << " ";it1++;}cout << endl;it1 = s1.begin();while (it1 != s1.end()){cout << ++(*it1) << " ";it1++;}cout << endl;//const对象迭代器遍历const string s2("xxxx");string::const_iterator cit2 = s2.begin();while (cit2 != s2.end()) {cout <<*cit2<< " ";++cit2;}cout << "\n";//范围for,底层是替换成迭代器,从编译器角度只增加一个替换规则for (auto e : s1){cout << e << " ";}cout << '\n';//范围for根据对象类型 对应//在这里发现 当s是普通对象的时候会替换成普通迭代器,是const对象的时候就会替换成const迭代器const string s("xxxxx");for (auto e : s){cout << e << endl;}}

测试结果;

4. string的增删改查

在实现下方函数时,因为增加数据要开空间,所以先实现reserve。

		//预分配好内存void reserve(size_t n) {//当n > _capacity 需要单独开空间if (n > _capacity) {//开新空间char* tmp = new char[n + 1];//拷贝数据strcpy(tmp,_str);//释放旧空间delete[] _str;//修改指针指向_str = tmp;//修改容量_capacity = n;}}

4.1 insert

这里模拟实现在pos位置插入字符串。

		//在pos位置插入单个字符void insert(size_t pos , char ch) {//首先判断位置是否合法assert(pos <= _size);	//这里pos == _size 相当于尾插//判断是否需要扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 :2* _capacity);}//移动数据//先移动_size位置的'\0',之后在向前移动,直到移动完pos位置for (size_t i = 0; i != _size - pos + 1; i++){_str[_size + 1 - i] = _str[_size - i];}//插入数据_str[pos] = ch;//调整大小++_size;}//在pos位置插入字符串void insert(size_t pos , const char* str) {//先判断位置是否合法assert(pos <= _size);//判断是否需要进行扩容size_t len = strlen(str);if (len + _size >_capacity) {reserve(len + _size);}//移动数据// 错误例子//size_t end = _size;//while (end >= pos) //{//	_str[end+len] = _str[end];//	--end;//}//正确写法size_t end = _size + 1;while (end > pos) {_str[end-1 + len] = _str[end-1];--end;}//拷贝数据strncpy(_str+pos,str,len);_size += len;}

上述在移动插入单个字符的实现移动数据实现上 通过移动的数据个数来控制,不会出现头插的bug,而在 插入在字符串移动数据使用end指针来控制的时候,需要格外关注头插问题。

4.2 push_back 和 append

push_back 和 append都是在字符串的末尾进行追加。

		//追加单个字符void push_back(char ch){//尾插,首先要判断是否需要扩容if (_size == _capacity) {reserve(_capacity == 0 ? 4 : 2*_capacity);	//这样写是为了避免_capacity == 0的情况}//追加数据_str[_size] = ch;++_size;_str[_size] = '\0';}//追加字符串void append(const char* str) {size_t len = strlen(str);if (len + _size > _capacity) {reserve(len+_size);}//拷贝数据strcpy(_str+_size,str);_size += len;}

 优化:当然也可以借助insert函数来实现

void push_back(char ch){insert(_size,ch);}void append(const char* str){insert(_size,str);}

4.3 operator+=

对于增加字符串,operator+=重载是经常用的。

		string& operator+=(char ch){//这里可以直接调用push_backpush_back(ch);return *this;}string& operator+=(const char* str) {//直接调用appendappend(str);return *this;}

4.4 erase

这里简单实现一下erase常用的函数,在pos位置删除len个字符

这里的nops是一个共有的静态const成员变量,属于整个类,属于每个对象 ,类内声明,类外初始化。

注意在实现erase函数的时候要注意参数

如果 len = npos 或者 pos+ len >= _size的时候,直接在pos位置加 \0,否则直接删除部分串即可。

但是 pos + len 会有溢出风险,len = npos的时候,就会出现的。

所以最好改写成 _size - pos <= len 。

		//在pos位置删除len个字符void erase(size_t pos , size_t len = npos) {//判读删除位置是否合法assert(pos < _size);//pos位置后面全部删除if (len == npos || _size - pos <= len ) {_str[pos] = '\0';_size = pos;}else {//部分删除strcpy(_str+pos,_str+pos+len);_size -= len;}}

4.5 resize

调整字符长度,需要注意的就是调整小空间和调大的情况

  • 调小:直接在字符串 n 的位置给 \0 表示该串结束
  • 调大:先扩容,再进行填充字符ch,注意最后n位置要加\0表示结束
		void resize(size_t n,char ch = '\0'){//调小的情况if (n < _size){_str[n] = '\0';}else {//调大的情况//先扩容,再在原串的末尾填充chreserve(n);for (size_t i = _size; i < n;i++) {_str[i] = ch;}_str[n] = '\0';}_size = n;	//注意修改}

4.6 swap

swap具体有三种,算法库里面的swap,手动模拟实现的swap,string里面重载的swap。

标准库里面的swap:

		//库里面的swap对象交换的底层是/*template<class T> void swap(T&a,T&b) {T c(a);a = b; b = c;}这里的代价就是:三次拷贝+一次析构*/

这使用整个模板有一定的代价,三次拷贝+一次析构。

手动实现的swap:

为了避免上述的代价,最简单的方式就是把对象里面的成员进行交换。

		void swap(string & s) {//命名空间展开//using namespace std; 先去局部找,在类中和函数找,没有找到就去全局找//一般不会直接去命名空间找(除非 命名空间展开)//编译器只会向上找 , 先局部-> 全局,如果命名空间展开就是命名空间找std::swap(_str,s._str);std::swap(_size,s._size);std::swap(_capacity,s._capacity);}

有什么方式避免误用swap?

我们发现string里面 重载了一个 void swap(string& x,string&y){x.swap(y)}
有现成就用现成,没有就去调用模板的。

4.7 find

实现 查找在pos位置出现的字符或者字符串

  • 查找字符:先判断pos位置是否合法,然后从pos位置开始查找,找到返回下标,找不到返回npos。
  • 查找字符串:先判断pos位置是否合法,然后从pos位置开始查找( 这里使用strstr()函数进行查找。strstr( ) 找不到就会返回空,找到了就会返回指向原字符串中第一次出现的子串的指针找到。)找到返回下标(这里返回的下标通过 两个指针来实现),找不到返回npos。
		//find chsize_t find(char ch,size_t pos = 0) const{//判断位置是否合法assert(pos < _size);//查找字符,从pos位置开始查找for (size_t i = pos; i < _size; i++){if (_str[i] == ch) {return i;}}return npos;}//find substrsize_t find(const char* substr, size_t pos = 0) const{//判断位置是否合法assert(pos < _size);//查找字串const char* p = strstr(_str+pos,substr);if (p) {//不为空就返回下标//指针-指针返回的就是两指针之间的距离,也就是下标return  p - _str;}else {return npos;}}

4.8 substr

获取部分字串,根据标准库来模拟实现。也就是获取从pos位置后且长度为len的字串。

先判断pos位置是否合法。

当 len 为npos的时候或者 pos+len >=_size的时候,获取从pos位置开始所有字符。

否则获取 pos开始长度为len的字符串。

		//获取部分字串string substr(size_t pos = 0,size_t len = npos) const{string sub;assert(pos < _size);if (len == npos || _size - len <= pos) {获取pos位置后的全部for (size_t i = pos; i < _size; i++){sub += _str[i];}}else {//获取从pos位置开始的len个for (size_t i = pos; i < pos + len;i++) {sub += _str[i];}}return sub;}

优化的地方:可以去复用append函数 

5. 深浅拷贝问题

拷贝构造函数

在没有手动实现拷贝构造函数的情况下,进行拷贝构造,发现程序出现问题,从调试可以发现问题:

  • 在没有写拷贝构造的情况下,编译器会自动默认生成的拷贝构造是浅拷贝。
  • 浅拷贝会指向相同的空间
  • 而且会析构同一空间两次。
  • 当s1修改的时候,发现s也被修改了。

所以编译器生成的拷贝构造函数不合适,所以需要手动生成一个拷贝构造函数。

采用深拷贝的方式

		//拷贝构造函数//s1(s)string(const string& s) {//深拷贝//另开一个空间,再把数据拷贝过来_str = new char[s._capacity + 1];strcpy(_str,s._str);_size = s._size;_capacity = s._capacity;}

再次调试验证一下:

拷贝的对象不再指向相同空间了。

赋值运算符重载

当我们按照上图的方式赋值的时候,发现也会出现浅拷贝的问题。

所以需要手动实现赋值运算重载。

传统写法版本

		//赋值运算符重载//如果不手动实现就会出现浅拷贝的问题//传统写法string& operator=(const string & s) {//开新空间char* tmp = new char[s._capacity + 1];//拷贝数据strcpy(tmp,s._str);//释放旧空间delete[] _str;//指向tmp_str = tmp;_size = s._size;_capacity = s._capacity;return *this;}

现代写法

		//拷贝构造函数的现代写法string(const string& s) {string tmp(s._str);swap(tmp);}

6. 比较关系运算符重载

6.1 operator==

一般会直接在类中来实现该成员函数

		bool operator==(const string & s){int ret = strcmp(_str,s._str);//这里借助strcmp函数来实现return ret == 0;}

但是在比较的时候会出现问题:成员函数的左端必须是对象才可以。

 解决方法:可以重载为 全局的关系运算符。

	bool operator==(const string& s1,const string& s2){int ret = strcmp(s1.c_str(), s2.c_str());return ret == 0;}

6.2 operator<

这里借助strcmp函数来实现,strcmp(str1,str2) 当str1 < str2 的时候,会返回一个小于0的值。

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

6.3 其他关系运算符重载

可以复用上述的关系运算符,来实现其他关系运算符。

	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,string& s2) {return !(s1 == s2);}

7. 流插入、流提取和getline

7.1 流插入

当直接给对象输入字符串的时候和直接用对象输出字符串的时候,发现程序报错。需要我们手动来模拟实现。这里使用标准输出流类来实现。

	//流插入必须是全局的ostream& operator<<(ostream& out , const string& s) {//使用范围for拿到字符,在使用out来输出for (auto ch : s) {out << ch;}return out;	//这里返回希望连续cout<< s << s1...}

 发现这里没有使用友元,因为这里只是打印字符,不需要去访问私有成员,打印字符可以使用operator[ ]、范围for等来访问 。

所以,运算符流插入的重载 不一定 必须为是全局函数且为类的友元函数。

7.2 流提取

这里对一个对象输入一个新的字符串。

需要注意的是先提前清空缓冲区,或者提前清空原来的串,也就是新的字符串覆盖原来的串,这样才可以保证打印出来的是新的串,这里通过复用模拟实现的clear来实现。

		void clear(){//内容清空,空间不变_size = 0;_str[_size] = '\0';}

标准库中的cin提取不到空格和换行,但是这里string中的流提取需要读到。这里使用istream类中的重载函数get。

	//流提取istream& operator>>(istream& in , string& s) {s.clear();//把提取的字符放到s里面去char ch;ch = in.get();while (ch != ' ' && ch != '\n') {s += ch;ch = in.get();}return in;}

问题:

当输入比较长的字符串的时候,上述代码会频繁的进行扩容,字符换越长,扩容次数就越多,就会导致一系列的消耗。

如果使用reserve,虽然可以减少扩容,但是不确定要开多少的内存

上述如果给了128,但是提取字符串较少的时候,内存空间就开大了,有些浪费。

优化:

		s.clear()//为避免频发库容 和 reserve的不合适char ch;ch = in.get();char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n') {buff[i++] = ch;if (i == 127) {buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;

所以这里没有把提取的数据直接用string += 而是先放到一个buff数组里面,如果字符串比较短,例如3个字符遇到空格或者换行就结束,string对象直接+=buff 就可以。

 如果提取的字符串大 _capacity就大,值得注意的是,字符拆较大的时候,借助buff一段一段加。

 

7.3 getline

getline是获取一行的字符串,包括中间的空格。这里只需要把流提取的 ch !=  ‘ ’ 去掉就可以。

	istream& getline(istream& in,string& s) {s.clear();char ch;ch = in.get();while (ch != '\n'){s += ch;ch = in.get();}return in;}

getline同样也可以优化

	istream& getline(istream& in,string& s) {//优化//避免频繁扩容 和 reserve的不合适s.clear();char ch;char buff[128];ch = in.get();size_t i = 0;while (ch != '\n') {buff[i++] = ch;if (i == 127){s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}

8. 源码

详细源码 请猛戳 此处

http://www.dtcms.com/a/288443.html

相关文章:

  • 数据分析综合应用 30分钟精通计划
  • 使用UV管理FastAPI项目
  • 数独算法Python示例
  • 【HarmonyOS】Ability Kit - Stage模型
  • Redis数据库基础与持久化部署
  • Vue3的definePros和defineEmits
  • Nacos:微服务架构的核心引擎
  • xss-dom漏洞
  • Python 数据分析模板在工程实践中的问题诊断与系统性解决方案
  • 2025在线教育系统源码、平台开发新趋势:开源架构+AI赋能
  • FPGA自学——整体设计思路
  • MySQL练习3
  • 轻松上手:从零开始启动第一个 Solana 测试节点
  • 小架构step系列19:请求和响应
  • Redis字符串操作指南:从入门到实战应用
  • 81、【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例:压栈内容
  • MC0462最后一难
  • Redis进阶--集群
  • C study notes[1]
  • LVS技术知识详解(知识点+相关实验部署)
  • simulink系列之模型接口表生成及自动连线脚本
  • 消息队列:数字化通信的高效纽带
  • SQL Server和PostgreSQL填充因子
  • HCIA综合实验
  • string【下】- 内功修炼(搓底层)
  • C++入门--lesson4
  • CCF编程能力等级认证GESP—C++6级—20250628
  • ICT测试原理之--什么是假短
  • 基于opencv的人脸识别考勤系统
  • 人工智能与心理史学:从阿西莫夫的科幻预言到可计算社会模型>