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

【C++】—— string 模拟实现

【C++】—— string模拟实现

  • 0 前言
  • 1 string的底层结构
  • 2 默认成员函数的实现
    • 2.1 构造函数
      • 2.1.1 无参构造
      • 2.1.2 带参构造
      • 2.1.2 合并
    • 2.2 析构函数
    • 2.3 拷贝构造函数
      • 2.3.1 传统写法
      • 2.3.2 现代写法
    • 2.3 赋值重载
      • 2.3.1 传统写法
      • 2.3.2 现代写法
      • 2.3.3 传统写法与现代写法的优劣
  • 3 size、capacity、operator[] 的实现
  • 4 正向迭代器的模拟实现
    • 4.1 问题引出
    • 4.2 模拟实现
    • 4.3 const 迭代器
    • 4.4 进一步理解封装
  • 5 string 增删查改的模拟实现
    • 5.1 reserve 的模拟实现
    • 5.2 push_back 的模拟实现
    • 5.3 append 的模拟实现
    • 5.4 operator+= 的模拟实现
    • 5.5 insert 的模拟实现
      • 5.5.1 插入一个字符
      • 5.5.2 插入字符串
    • 5.6 erase 的模拟实现
      • 5.6.1 npos 的定义和声明
      • 5.6.2 实现 erase
  • 6 string 查找的模拟实现
    • 6.1 find 的模拟实现
      • 6.1.1 查找字符
      • 6.1.2 查找字符串
    • 6.2 substr 的模拟实现
  • 7 非成员函数的模拟实现
    • 7.1 operator 比较系列
    • 7.2 operator<<
    • 7.3 operator>>
  • 10 完整代码
    • 10.1 string.h
    • 10.2 string.cpp

0 前言

  为了让我们更加深入理解 string,接下来我们将模拟实现一个简易版的 s t r i n g string string。而为了和STL库中的 s t r i n g string string 以示区分,我们将使用命名空间namespace对其封装。
  
  

1 string的底层结构

   s t r i n g string string 简单来说就是一个被封装的可动态增长的数组,其的底层结构我们可以类比顺序表来实现

namespace my_string
{class string{public://···private:char* _str;//指向字符串的指针size_t _size;//有效字符个数(不包括'\0')size_t _capacity;//有效字符个数(不包括'\0')};
}

  
  

2 默认成员函数的实现

2.1 构造函数

  构造函数,我们可以写一个无参构造的和一个带参构造
  

2.1.1 无参构造

  在学习类与对象时(【C++】—— 类与对象(二)),我们曾讲过成员变量尽量走初始化列表

	string():_str(nullptr),_size(0),_capacity(0){}

  
  但是,库中的 s t r i n g string string,无参时并不是空指针,而是放着一个空字符,我们进行以下修改:

string():_str(new char('')), _size(0), _capacity(0)
{}

  这里,我们开辟一个字节空间放空字符串,也就是 ‘\0’
  因为 _capacity 显示的空间大小是 不包括 ‘\0’ 的,因此 _ c a p a c i t y capacity capacity 显示的空间大小永远比实际开辟的空间小一个。这里开辟了一个空间放 '/0',而 _ c a p a c i t y capacity capacity0
  
  

2.1.2 带参构造

再写个带参的:

string(const char* str):_str(new char[strlen(str) + 1]), _size(strlen(str)), _capacity(_size)
{strcpy(_str, str);//将str中的内容拷贝
}

  上述代码中,要多开辟一个空间,因为 s t r l e n strlen strlen 是不计算 ‘\0’ 的。

  不难发现带参的构造函数用初始化列表初始化并不太好,因为要调用 s t r l e n strlen strlen函数 2 次。即使初始化列表中的 _ s t r str str 和 _ s i z e size size 位置互换也没用,因为初始化列表的初始化顺序是根据成员变量在类中声明的顺序进行初始化的

  这时,我们可以使用函数体初始化,虽然默认也会走初始化列表,但都是内置类型,并不影响。

  初始化列表只是推荐使用,但并不是所有情况都适合

string(const char* str)
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//实际开辟空间比_capacity多一strcpy(_str, str);//将str中的内容拷贝
}

  这里再多提一句,_ c a p a c i t y capacity capacity 不计算 '\0',因此虽然实际开的空间比 _ s i z e size size 多一个,但 _capacity 等于_size
  
  

2.1.2 合并

  其实我们可以将上述无参和带参的构造函数合二为一
  怎么做呢?用一个缺省参数就好啦

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

  
  

2.2 析构函数

  析构函数的实现很简单,只需要把资源释放就行

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

  

2.3 拷贝构造函数

  在学习了前面类与对象中(【C++】—— 类与对象(三)),我们知道:对于有指向资源的类,我们必须要自己实现拷贝构造,实现深拷贝
  

2.3.1 传统写法

  拷贝构造的传统写法很简单,直接调用 s t r c p y strcpy strcpy 函数拷贝就行了

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

  

2.3.2 现代写法

  拷贝构造除了传统的写法,还有一种现代写法
  现代写法的通俗来说就是窃取别人的劳动成果,什么意思呢?我们先来看代码

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

  

原理如下:

  • 先用传入的 s s s 对象的字符串构造 t m p tmp tmp 对象(此时调用的是构造函数
  • 再将 * t h i s this this t m p tmp tmp 交换
  • 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦
  • 最后出了函数作用域,交换后的 t m p tmp tmp 调用析构后销毁

s w a p swap swap 交换函数在上一章已讲解,有疑惑的小伙伴可移步至:【C++】—— string 类的了解与使用
  

  可以说,现代写法的活全是构造函数干的,拷贝构造其实是 s w a p swap swap 窃取构造函数的劳动成果,从而达到拷贝构造的目的。

:上述代码还是有缺陷的。因为在交换前 (*this)._str 仅仅是在建立栈帧时开了空间,其本身随机值。交换给 t m p tmp tmp t m p tmp tmp 出了函数作用域会调用析构函数此时 t m p tmp tmp 释放的是一个随机的地址,是个野指针。因此,最好在声明处给 _ s t r str str 一个默认值(缺省值): n u l l p t r nullptr nullptr

namespace my_string
{class string{public://···private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}

  
  

2.3 赋值重载

  赋值重载与拷贝构造相似,我们一起来看看
  

2.3.1 传统写法

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

  我们知道,赋值会将原来的数据覆盖掉 s t r i n g string string 也应如此
  赋值重载要先将旧空间释放掉,再开辟新空间进行拷贝

:存在自己给自己赋值的情况要单独处理
  
  

2.3.2 现代写法

  与拷贝构造一样,赋值重载也有现代写法,且原理类似

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._str);swap(tmp);}return *this}

  

  • 先用传入的 s 对象的字符串构造 tmp 对象(此时调用的是构造函数
  • 再将 * t h i s this this t m p tmp tmp 交换
  • 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦

  这种写法不仅仅是交换那么简单,还 t h i s this this 原来的资源交给了 t m p tmp tmp,这样 t m p tmp tmp 出函数作用域还能顺便将 t h i s this this 旧的资源释放掉

  赋值的现代写法还有一个简化版本

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

  这里用了传值传参,程序调用拷贝构造产生形参 tmp t h i s this this 再与 t m p tmp tmp 交换,出了作用域 t m p tmp tmp 销毁
  这里不再判断是否自己给自己赋值是因为:当走入函数体时,拷贝已经完成了,此时再判断已经没意义了。而且即使是自己给自己赋值也不会出问题,只是效率低了点。
  
  

2.3.3 传统写法与现代写法的优劣

  其实,传统写法与现代写法在效率上并没有区别,他们都要去完成拷贝,只是是谁来完成这份工作的问题而已
  
  

3 size、capacity、operator[] 的实现

   s i z e size size c a p a c i t y capacity capacity 的实现很简单,我们直接上代码

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

  
  operator[] 的实现也同样很简单,但需要注意的是 o p e r a t o r operator operator[ ] 的实现要加断言以判断所给的下标是否合法

//普通版本
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}//const 版本
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}

  
  

4 正向迭代器的模拟实现

4.1 问题引出

  在【C++】—— string 类的了解与使用中,我们知道 s t r i n g string string 的遍历有三种方式:下标访问迭代器访问范围 for

  那范围for那么好用,我们可不可以直接使用呢?

	void test1(){string s1 = "hello world";for (auto i : s1){cout << i << " ";}}

在这里插入图片描述

  可以看到,报错了。开始报错的内容很奇怪,说没有 b e g i n begin begin 函数,为什么呢
  我们曾说过,范围for并没有什么神奇的,其底层其实就迭代器
  范围 f o r for for 与迭代器是一种傻瓜式的替换,并不看你实现了什么,只要名字相同就行
  
  下面我们就一起来模拟实现 s t r i n g string string迭代器
  

4.2 模拟实现

  我们知道,迭代器模拟的是指针的行为,而 s t r i n g string string 类的指针 c h a r char char* 完美满足迭代器的各种需求,因此我们直接用指针作为迭代器
  我们直接用原生指针 t y p e d e f typedef typedef 一个 i t e r a t o r iterator iterator 出来

class string
{
public://···typedef char* iterator;private://···
};

  
  其实,所有的迭代器 i t e r a t o r iterator iterator 都是 t y p e d e f typedef typedef 出来的,只是其他容器不像 s t r i n g string string 这么简单除暴直接用指针,他们可能是自定义类型,经过封装后再 t y p e d e f typedef typedef i t e r a t o r iterator iterator
  
  我们再把 b e g i n begin begin e n d end end 实现出来

class string
{
public://···iterator begin(){return _str;}iterator end(){return _str + _size;}private://···
};

  
我们来测试一下:

void test2()
{string s1 = "hello world";for (auto i : s1){cout << i << " ";}cout << endl;string s2 = "ni hao";string::iterator it = s2.begin();while (it != s2.end()){cout << *it << " ";++it;}cout << endl;
}

运行结果:

在这里插入图片描述

  可以看到,不仅迭代器访问好了,范围for访问也好了,可见范围 f o r for for 的底层其实就是迭代器
  
  

4.3 const 迭代器

  当然,还有 const迭代器

class string
{
public://···typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}private://···
};

  
  

4.4 进一步理解封装

  迭代器的设计本质是一种 封装的体现

  我们之前理解的封装是这样的:
  我把数据方法都放到类里面,都封装起来,不能让你随便修改。想让你访问的设置成公有,不想让你访问的设置成私有

  而迭代器的设计也是一种封装
  有各种各样的容器,它们底层可能是顺序表、链表甚至可能是树。现在我不管你底层结构是什么,差别有多大,我都用一个叫 i t e r a t o r iterator iterator 的东西来进行访问
  iterator 属于类域,它是个什么类型我不知道。现在你封装以后给我提供一个 b e g i n begin begin,那我就知道无论你是什么容器,这就是指向第一个有效数据位置 e n d end end 就是最后一个数据的下一个位置。我要访问就解引用,我要访问下一个位置就 ++
  这样,我们就不需要再关心这个数据结构到底是顺序表还是链表还是树,iterator 屏蔽了底层的结构和实现细节,它提供了统一的接口,我们可以对不同的数据结果用类似的方式进行访问
  
  

5 string 增删查改的模拟实现

  上面所实现的函数,大多都是一些比较短小的函数,又或是需要频繁调用的函数,我们将他们放在类中,以成内联函数提高程序的效率。下面的大部分函数,我们可以对其进行进行声明和定义的分离
  

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

  我们这里实现的 reserve 只扩容,不缩容,因此要先判断 n 是否 > _capacity

   r e s e r v e reserve reserve 的整体实现思路如下:开辟更大的新空间 -> 将就空间数据拷贝至新空间 -> 释放就空间

  • char* tmp = new char[n + 1];这里要记住,实际开空间要多开一个空间,为 ‘\0’ 留位置
  • strcpy(tmp, _str);拷贝数据
  • delete[] _str;释放旧空间
  • _str = tmp;指向新空间
  • _capacity = n;更新 _ c a p a c i t y capacity capacity

  
  

5.2 push_back 的模拟实现

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

  既然是插入数据,那么一定要判断是否需要扩容
  这里,我们调用 r e s e r v e reserve reserve 来完成扩容:如果 _ c a p a c i t y capacity capacity 为 0,则开 4 个空间,否则扩容两倍
  括完容就简单啦,在最后的位置插入 c h ch ch,并将 _ s i z e size size++,最后,千万别忘了要在最后补上 ‘\0’

  
  

5.3 append 的模拟实现

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

   a p p e n d append append 同样需要判断是否需要扩容:当 _size + 新字符串长度 > _capacity 则表示需要扩容

  这里有一个问题:括多大呢?如果_size + len 比扩容 2 倍后的空间还大怎么办呢
  因此,这里要进行一下判断,如果比 2 倍扩容后的空间小,则正常 2 倍扩容,反之扩容_capacity + len 的大小

  
  

5.4 operator+= 的模拟实现

  上面实现了 p u s h push push_ b a c k back back a p p e n d append append,那么 o p e r a t o r operator operator+= 的实现就很简单啦,只需要直接复用他们就行了

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

  
  

5.5 insert 的模拟实现

5.5.1 插入一个字符

void string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){size_t n = _capacity == 0 ? 4 : 2 * _capacity;reserve(n);}size_t i = _size;while (i >= pos){_str[i + 1] = _str[i];--i;}_str[pos] = ch;++_size;
}
  • assert(pos <= _size);首先要判断下标合不合法
  • if (_size == _capacity)既然要插入数据,那么一定要判断扩不扩容
  • 再接着便是挪动数据,这里我们选择从后往前挪动。如果是从前往后,会造先挪动的数据会覆盖后挪动的数据

  那么上述代码有问题吗?
  有!头插的时候会造成死循环!

  为什么呢?因为 i i i s i z e size size_ t t t 类型,当头插时, p o s pos pos 为 0 。挪动最后一个数据后: i i i 从 0 减1;可是 i i i s i z e size size_ t t t 类型,再 -1 得到的是一个很大的数,后继续不断减,但永远大于等于0,程序死循环。
  
  那把 i i i 改为 i n t int int 类型可不可以呢?

int i = _size;
while (i >= pos)
{_str[i + 1] = _str[i];--i;
}

  还是不可以,因为while (i >= pos)语句发生了类型转换
  C语言规定,当操作符两边操作数类型不同时,类型小的向范围大的去提升。(详情请看:【C语言】——详解操作符(下))
  上述情况就是 i n t int int 类型的 i i i 提升成了 s i z e size size_ t t t 类型,因此还是无法解决问题。
  
解决方法有多种,这里简单列举几个

//法一:将pos强转成int
int i = _size;
while (i >= (int)pos)
{_str[i + 1] = _str[i];--i;
}//法三:单独判断 i == npos(-1) 的情况
size_t i = _size;
while (i >= pos && i != npos)
{_str[i + 1] = _str[i];--i;
}//法三:修改 i 的起始位置
size_t i = _size + 1;
while(i > pos)
{_str[i] = _str[i - 1];--i;
}

  
  

5.5.2 插入字符串

void string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){size_t n = len > _capacity ? _capacity + len : 2 * _capacity;reserve(n);}size_t i = _size;while (i >= pos && i != npos){_str[i + len] = _str[i];--i;}memcpy(_str + pos, str, sizeof(char) * len);_size += len;
}

  插入字符串与前面的情况很类似,前面步骤都是断言->扩容->挪数据

  不同的是,挪动数据是向后挪动 len 个位置
  最后是拷贝数据,拷贝数据可以直接 m e m c p y memcpy memcpy 拷贝。注意,这里不能用 s t r c p y strcpy strcpy,因为 s t r c p y strcpy strcpy 会额外多拷贝一个 '\0',这样不仅会覆盖掉一个数据,并且插入字符串之后的数据也不可见
  
  当然,我们也可以自己实现拷贝

for (size_t i = 0; i < len; i++)
{_str[pos + i] = s[i];
}

  
  

5.6 erase 的模拟实现

5.6.1 npos 的定义和声明

  不了解 n o p s nops nops 的小伙伴可以移步:【C++】—— string 类的了解与使用 来学习

  我们知道, n o p s nops nops s t r i n g string string 中的一个静态常量成员
  它在 s t r i n g string string 类中的声明如下

class string
{
public://···private://···static const size_t npos;
};

  
  但需要注意的是,它不能string.h文件中进行定义,像这样:

string.hclass string
{//···
};const size_t string::npos = -1;

  因为 . h h h 文件会被多个 . c p p cpp cpp 文件包含 ,被几个 .cpp文件 包含就意味着 npos 被定义了几次,而一个变量规定只能定义一次

  因此, n p o s npos npos 应在 string.cpp文件 中定义

string.cppnamespace my_string
{const size_t string::npos = -1;void erase(size_t pos, size_t n){//···}//···
};

  
  但是,有一个特殊情况:对于 n p o s npos npos,可以在声明时直接给缺省值

class string
{
public://···private://···static const size_t npos = -1;
};

  虽然但是,还是不建议这种写法。
  因为静态变量是不走初始化列表的,因此理论上是不能在声明时给缺省值的
  只有 static + const + 整型才可以, d o u b l e double double 都不行,我们可以认为编译器特殊处理了。

  
  

5.6.2 实现 erase

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

   e r a s e erase erase 的数据删除分为两种情况:一种是 p o s pos pos 之后的数据全部删除;一种是删除部分数据
  当len == npospos + len > _size,则表明之后的数据全部删除。此时只需在 p o s pos pos 位置改为 ‘\0’ 并调整 _ s i z e size size 即可
  另一种情况就是挪动数据,这次是从前往后挪,并调整 _ s i z e size size
  
  

6 string 查找的模拟实现

6.1 find 的模拟实现

6.1.1 查找字符

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

  这个比较简单,大家直接看代码即可,需要注意的是如果没找到记得返回 n p o s npos npos
  
  

6.1.2 查找字符串

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

  找子串的问题可以用暴力匹配的算法,也可以用 kmp算法,更好的有 BM算法,大家可以自行去学习。这里就不过多介绍,我们这里直接复用 C语言 中的 s t r s t r strstr strstr 函数。( s t r s t r strstr strstr 的模拟实现可以移步:【C语言】——字符串函数的使用与模拟实现(下))

  这里需要注意,当找不到目标字符串时, s t r s t r strstr strstr 返回的是 n u l l p t r nullptr nullptr,这种情况我们要单独处理
  
  

6.2 substr 的模拟实现

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

  首先,我们先把 l e n len len 更新一下,如果 pos + len > _size,那么 len 就是 _size - pos
  剩下的就非常简单啦,创建一个新的对象,并提前预留空间,最后再一个一个字符 += ( p u s h push push_ b a c k back back)
  

:这里一定要先写好拷贝构造,当我们没有自己实现拷贝构造时,编译器默认生成的是浅拷贝,会出大问题。

string s1 = "hello world";
string s2 = s1.substr(6);
cout << s2.c_str() << endl;

  上述 s u b s t r substr substr 用的是传值返回,传值返回会调用拷贝构造来创建一个临时对象,临时对象再调用一次拷贝构造来拷贝给上 s 2 s2 s2.

  如果我们没实现拷贝构造,那么编译器实现的是浅拷贝。 s 2 s2 s2 s u b sub sub 指向的是同一块空间 s u b sub sub 出了作用域后会销毁,它所开辟的空间也跟着释放 s 2 s2 s2 指向一块已经被释放的空间,要是再调用析构函数,自然会出问题。

在这里插入图片描述

  不过在新一点的编译器可能不会出现问题,如:VS2022。
  编译器一般都会进行优化,在进行传值拷贝构造时,编译器可能不会进行 s u b sub sub 对象的创建,直接用临时对象拷贝构造 s 2 s2 s2
  而在 VS2022 等较新的编译器,优化更为激进,直接合三为一 s u b sub sub 和临时对象都不创建了,直接拷贝构造 s 2 s2 s2!所以也就不存在同一块空间被多次析构的问题了!关于编译器的优化,感兴趣的小伙伴可以移步:【C++】—— 类与对象(五)
  
  

7 非成员函数的模拟实现

7.1 operator 比较系列

  为什么 s t r i n g string string类中, o p e r a t o r operator operator 比较系列要实现成全局函数呢?

  因为在类中实现,那第一个参数必须是 this指针,那么 s t r i n g string string 永远只能放在左边。库中想 string与字符串比较字符串与string都实现,因此将 operator比较系列放在类外

   o p e r a t o r operator operator 比较系列比较简单,我们直接看代码就好

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

  6 个比较函数,只需任意实现其中两个,另外4个函数都可以通过服用得到
  
  

7.2 operator<<

  流插入的实现和简单,遍历一遍就好了

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

:这里ostream& out不能加 c o n s t const const,因为这是流插入,要往 o u t out out 里面去写,out是要进行修改的,加上 c o n s t const const 就不能修改了。
  
  

7.3 operator>>

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

:这里string& s前不能加 c o n s t const const,因为是流提取提取出来的字符是放到 s s s 中,是要对 s 进行改变的

  上述代码先将string s清空,后不断提取缓冲区的字符到变量 c h ch ch中,直到出现 ' ''\n'
  但我们检验一下就知道,这个代码是有问题的:因为流提取读到' ''\n'默认将他们跳过(流提取将他们当做是字符串与字符串之间的分隔符,因此跳过他们) s c a n f scanf scanf 也是如此,因此程序会一直让你输入,无法停止
  
  我们可以用 g e t get get 函数 g e t get get 函数不管你是什么字符,都会给你读进来,与 C语言 中的 g e t c getc getc函数 类似

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

  但上述代码还有个缺点,因为是一个字符一个字符地插入,因此需要频繁的扩容

  怎么解决呢?
  我们可以给一个大小合适的 b u f f buff buff 数组,现在每次来的字符先不往 s t r i n g string string 进行 +=,先放到 buff 数组里面, b u f f buff buff 满了,再一次性放入 s t r i n g string string 里面。当遇到' ''\0',再将buff数组中剩下的字符放入 s t r i n g string string 中。

istream& operator>>(istream& in, string& s)
{s.clear();char ch;ch = in.get();const int N = 256;char buff[N];int i = 0;while (ch != ' ' && ch != '\0'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;
}

  
  

10 完整代码

10.1 string.h

#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;namespace my_string
{class string{public:friend ostream& operator<<(ostream& out, const string& s);friend istream& operator>>(const istream& in, string& s);string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);//将str中的内容拷贝}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(tmp);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const{return _str;}string& operator=(string tmp){swap(tmp);return *this;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos) const{assert(pos < _size);return _str[pos];}typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}void clear() {_str[0] = '\0';_size = 0;}void reserve(size_t n);void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);string substr(size_t pos = 0, size_t len = npos);static const size_t npos;private:char* _str = nullptr;//指向字符串的指针size_t _size = 0;//有效字符个数(不包括'\0')size_t _capacity = 0;//有效字符个数(不包括'\0')};bool operator<(const string& s1, const string& s2);bool operator<=(const string& s1, const string& s2);bool operator>(const string& s1, const string& s2);bool operator>=(const string& s1, const string& s2);bool operator==(const string& s1, const string& s2);bool operator!=(const string& s1, const string& s2);ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);}

10.2 string.cpp

#include"string.h"namespace my_string
{const size_t string::npos = -1;void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void string::push_back(char ch){if (_size == _capacity){size_t n = _capacity == 0 ? 4 : 2 * _capacity;reserve(n);}_str[_size++] = ch;_str[_size] = '\0';}void string::append(const char* str){size_t len = strlen(str);if (len + _size > _capacity){size_t n = len > _capacity ? _capacity + len : 2 * _capacity;reserve(n);}strcpy(_str + _size, str);_size += len;}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}void string::insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){size_t n = _capacity == 0 ? 4 : 2 * _capacity;reserve(n);}size_t i = _size;while (i >= pos && i != npos){_str[i + 1] = _str[i];--i;}_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 n = len > _capacity ? _capacity + len : 2 * _capacity;reserve(n);}size_t i = _size;while (i >= pos && i != npos){_str[i + len] = _str[i];--i;}memcpy(_str + pos, str, sizeof(char) * len);_size += len;}void string::erase(size_t pos, size_t len){assert(pos < _size);if (npos == len || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; ++i){_str[i - len] = _str[i];}_size -= len;}}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);char* p = strstr(_str + pos, str);if (nullptr == p){return npos;}return p - _str;}string string::substr(size_t pos, size_t len){assert(pos < _size);if (pos + len > _size || len == npos){len = _size - pos;}string sub;sub.reserve(len);for (size_t i = pos; i < pos + len; ++i){sub += _str[i];}return sub;}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);}ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();char ch;ch = in.get();const int N = 256;char buff[N];int i = 0;while (ch != ' ' && ch != '\0'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
}

相关文章:

【C++】—— string 模拟实现

【C】—— string模拟实现 0 前言1 string的底层结构2 默认成员函数的实现2.1 构造函数2.1.1 无参构造2.1.2 带参构造2.1.2 合并 2.2 析构函数2.3 拷贝构造函数2.3.1 传统写法2.3.2 现代写法 2.3 赋值重载2.3.1 传统写法2.3.2 现代写法2.3.3 传统写法与现代写法的优劣 3 size、…...

详解TensorRT的C++高性能部署以及C++部署Yolo实践

详解TensorRT的C高性能部署 一. ONNX1. ONNX的定位2. ONNX模型格式3. ONNX代码使用实例 二、TensorRT1 引言 三、C部署Yolo模型实例 一. ONNX 1. ONNX的定位 ONNX是一种中间文件格式&#xff0c;用于解决部署的硬件与不同的训练框架特定的模型格式的兼容性问题。 ONNX本身其…...

手机如何切换网络IP地址:‌方法详解与操作指南‌

在当今的数字化时代&#xff0c;‌网络IP地址作为设备在网络中的唯一标识&#xff0c;‌扮演着至关重要的角色。‌对于手机用户而言&#xff0c;‌了解如何切换网络IP地址不仅有助于提升网络体验&#xff0c;‌还能在一定程度上保护个人隐私。‌本文将详细介绍手机切换网络IP地…...

南通网站建设手机版网页

随着移动互联网的迅猛发展&#xff0c;越来越多的人通过手机浏览网页&#xff0c;进行在线购物、信息查询和社交互动。因此&#xff0c;建立一个适合移动端访问的网站已成为企业和个人不可忽视的重要任务。在南通&#xff0c;网站建设手机版网页的需求逐渐增加&#xff0c;如何…...

macos系统内置php文件列表 系统自带php卸载方法

在macos系统中, 自带已经安装了php, 根据不同的macos版本php的版本号可能不同, 我们可以通过 which php 命令来查看mac自带的默认php安装路径, 不过注意这个只是php的执行文件路径. 系统自带php文件列表 一下就是macos默认安装的php文件列表. macos 10.15内置PHP文件列表配置…...

微信小程序认证和备案

小程序备案的流程一般包括以下步骤‌&#xff1a; 准备备案所需材料‌&#xff1a;通常需要提供‌营业执照、法人的‌身份证、两个‌手机号和一个邮箱等资料。 ‌1 ‌登录‌微信公众平台‌&#xff1a;作为第一次开发微信小程序的服务商&#xff0c;需要通过微信公众平台申请…...

C++复习day05

类和对象 1. 面向对象和面向过程的区别是什么&#xff1f;&#xff08;开放性问题&#xff09; 1. **抽象级别**&#xff1a;- **面向对象**&#xff1a;以对象&#xff08;数据和方法的集合&#xff09;为中心&#xff0c;强调的是数据和行为的封装。- **面向过程**&#xf…...

python数值误差

最近在用fenics框架跑有限元代码&#xff0c;其中有一个部分是把在矩阵里定义的初始值&#xff0c;赋值到有限元空间里&#xff0c;这就涉及到了初始矩阵和有限元空间坐标的转化&#xff0c;部分代码如下 for i in range(len(dof_coordinates)):# x, y dof_coordinates[i…...

基于FPGA的OV5640摄像头图像采集

1.OV5640简介 OV5640是OV&#xff08;OmniVision&#xff09;公司推出的一款CMOS图像传感器&#xff0c;实际感光阵列为&#xff1a;2592 x 1944&#xff08;即500w像素&#xff09;&#xff0c;该传感器内部集成了图像出炉的电路&#xff0c;包括自动曝光控制&#xff08;AEC…...

CDN ❀ Http协议标准缓存字段梳理

文章目录 1. 背景介绍2. 测试环境搭建3. 缓存字段3.1 Expires3.2 Cache-Control3.3 协商缓存 1. 背景介绍 Http协议标准有RFC定义好的请求和响应头部字段用于进行缓存设置&#xff0c;本文主要进行介绍缓存功能相关的头部字段及其使用方法。在使用CDN功能是&#xff0c;协议标…...

浅谈NODE的NPM命令和合约测试开发工具HARDHAT

$ npm install yarn -g # 将模块yarn全局安装 $ npm install moduleName # 安装模块到项目目录下 默认跟加参数 --save 一样 会在package文件的dependencies节点写入依赖。 $ npm install -g moduleName # -g 的意思是将模块安装到全局&#xff0c;具体安装到磁盘哪个位置&…...

k8s-pod 实战六 (如何在不同的部署环境中调整startupprobe的参数?)

在不同的部署环境中(如开发、测试、生产环境),你可能希望对 startupProbe 的参数进行调整,以适应不同的需求和条件。以下是几种常见的方法和实践: 方法一:使用 Kustomize 1. 目录结构 假设你的项目目录结构如下: my-app/ ├── base/ │ └── deployment.yaml …...

和服务端系统的通信

首先web网站 前端浏览器 和 后端系统 是通过HTTP协议进行通信的 同步请求&异步请求&#xff1a; 同步请求&#xff1a;可以从浏览器中直接获取的&#xff08;HTML/CSS/JS这样的静态文件资源)&#xff0c;这种获取请求的http称为同步请求 异步请求&#xff1a;js代码需要到服…...

python 实现perfect square完全平方数算法

python 实现perfect square完全平方数算法介绍 完全平方数&#xff08;Perfect Square&#xff09;是一个整数&#xff0c;它可以表示为某个整数的平方。例如&#xff0c;1,4,9,16,25,… 都是完全平方数&#xff0c;因为 1 1 2 , 4 2 2 , 9 3 2 11^2,42^2,93^2 112,422,93…...

【漏洞复现】某客圈子社区小程序审计(0day)

0x00 前言 █ 纸上得来终觉浅,绝知此事要躬行 █ Fofa:"/static/index/js/jweixin-1.2.0.js"该程序使用ThinkPHP 6.0.12作为框架,所以直接审计控制器即可.其Thinkphp版本较高,SQL注入不太可能,所以直接寻找其他洞. 0x01 前台任意文件读取+SSRF 在 /app/api/c…...

信息安全数学基础(1)整除的概念

前言 在信息安全数学基础中&#xff0c;整除是一个基础且重要的概念。它涉及整数之间的特定关系&#xff0c;对于理解数论、密码学等领域至关重要。以下是对整除概念的详细阐述&#xff1a; 一、定义 设a, b是任意两个整数&#xff0c;其中b ≠ 0。如果存在一个整数q&#xff0…...

SearchGPT与谷歌:早期分析及用户反馈

光年AI系统&#xff0c;作为先进AI技术的成果&#xff0c;推出了一个AI驱动搜素引擎的原型&#xff0c;类似于SearchGPT。 该发布引起了广泛的关注&#xff0c;并引发了关于其是否有能力与Google竞争的讨论。 然而&#xff0c;早期的研究和用户反馈表明&#xff0c;虽然Searc…...

VUE饿了么UPload组件自定义上传

代码&#xff1a; 1.视图&#xff1a; <el-dialog :title"dialogTitle" width"30%" :visible.sync"dialogFormVisible" :destroy-on-close"true"><el-form ref"fileForm" class"items-align" ><e…...

2.1概率统计的世界

欢迎来到概率统计的世界&#xff01;在量化交易中&#xff0c;概率统计是至关重要的工具。通过理解概率&#xff0c;我们可以用数学的方法来描述市场行为&#xff0c;预测未来走势&#xff0c;并制定交易策略。让我们一起从基础概念开始&#xff0c;逐步深入&#xff0c;揭开概…...

SpringBoot使用QQ邮箱发送邮件

1.开启POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 设置 -> 账号 -> POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 获取授权码 SpringBoot依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter&l…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

拉力测试cuda pytorch 把 4070显卡拉满

import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试&#xff0c;通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小&#xff0c;增大可提高计算复杂度duration: 测试持续时间&#xff08;秒&…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...

CMake控制VS2022项目文件分组

我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...