【C++】string类的模拟实现

目录

  • 1.默认成员函数
    • 1.1 构造函数
    • 1.2 拷贝构造
      • 1.2.1 传统写法
      • 1.2.2 现代写法
    • 1.3 赋值构造
      • 1.3.1 传统写法
      • 1.3.2 现代写法
    • 1.4 析构函数
  • 2.其他成员函数
    • 2.1 迭代器
    • 2.2 容量操作
      • 2.2.1 size()
      • 2.2.2 capacity()
      • 2.2.3 reserve()
      • 2.2.4 resize()
      • 2.2.5 clear()
    • 2.3 访问操作(operator[]())
    • 2.4 修改操作
      • 2.4.1 push_back()
      • 2.4.2 append()
      • 2.4.3 operator+=()
      • 2.4.4 insert()
      • 2.4.5 erase
      • 2.4.6 find()
    • 2.5 c_str()
  • 3.非成员函数
    • 3.1 operator<<和operator>>
    • 3.2 getline()
    • 3.3 字符串比较
  • 4.整体代码

1.默认成员函数

1.1 构造函数

STL库中实现了多种版本的构造函数,我们这里实现最常用的参数为const char*的一种。还需要提供一个无参的构造函数,这里采用缺省参数来代替。

_str代表用来存放数据的数组
_size代表数组有效字符的个数
_capacity在vs中代表有效字符的容量,在g++中包含了\0。我们采用vs的实现方式,底层开辟空间时多开一个空间存放\0。

string(const char* str = "")	//采用缺省参数来代替无参构造: _size(strlen(str))		//_str(str)	不能这样初始化,会导致权限放大
{_capacity = _size == 0 ? 3 : _size;_str = new char[_capacity + 1];	//多开一个空间用来存放\0strcpy(_str, str);
}

1.2 拷贝构造

1.2.1 传统写法

手动开辟空间,并且拷贝数据到新空间里。

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

1.2.2 现代写法

我们可以利用构造函数来创建一个中间变量tmp,然后再用swap函数将this和tmp交换。
这里swap函数分为两种:一个是std里的swap,一个是string类里的swap。
std中的:

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

string类中的:

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

上述两种swap在使用中有什么区别吗?
std中的参数是自定义类型,在交换时需要进行连续三次深拷贝,效率很低。
string中的参数则是内置类型,无需深拷贝。
所以我们在这里选用string类中的swap函数。

string(const string& s)
// 初始化,防止交换后tmp中的_str成为野指针,在析构时崩溃: _str(nullptr), _size(0), _capacity(0)
{string tmp(s._str);// 要使用tmp作为中间值,再让this与tmp交换,不能改变s的内容swap(tmp);
}

1.3 赋值构造

1.3.1 传统写法

与拷贝构造类似,需要手动开辟空间拷贝数据。但要考虑到同一对象可能被多次赋值,所以我们要使用delete[]将原有的资源释放。

string& operator=(const string& s)
{// 给出条件this != &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;
}

1.3.2 现代写法

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

1.4 析构函数

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

2.其他成员函数

2.1 迭代器

目前我们无法完全理解迭代器,但我们暂时可以把它理解为指针,现在用typedef把iterator定义为char*来模拟实现迭代器。
begin()返回首元素的地址
end()返回末尾\0对应的地址

typedef char* iterator;iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}

与上面同理,我们还可以写出const迭代器

typedef const char* const_iterator;
const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}

范围for本质上就是迭代器,只要实现了begin()和end()就可以使用范围for,不过begin和end的名字是固定的,也就是说不能用其他的功能相同但名字不同的函数替代。

void test4()
{string s("hello world!");string::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;for (auto ch : s){cout << ch << " ";}cout << endl;
}

在这里插入图片描述

2.2 容量操作

2.2.1 size()

size_t size() const
{return _size;
}

2.2.2 capacity()

size_t capacity() const
{return _capacity;
}

2.2.3 reserve()

reserve的参数是有效字符所占的字符,不包括\0,所以底层开空间时要开n+1个。
这里的扩容是异地扩容,要将原有的内容拷贝到新的空间。

//reserve不能缩容,如果n<capacity,则什么都不干
void reserve(size_t n)
{if (n > capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}

2.2.4 resize()

resize根据n的大小可分为3种操作:
1.n<_size:删除数据
2.n>_size && n<=_capacity:插入数据
3.n>_size && n>_capacity:扩容并插入数据

void resize(size_t n, char ch = '\0')
{// 删除数据if (n < _size){_size = n;_str[_size] = '\0';}// 插入数据else if (n > _size){// 扩容if (n > _capacity){reserve(n);}size_t i = _size;while (i < n){_str[i] = ch;++i;}_size = n;_str[_size] = '\0';}
}

2.2.5 clear()

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

2.3 访问操作(operator

operator需要实现两个版本分别用来适应const和非const对象。

const char& operator[] (size_t pos) const
{// 越界访问直接报错assert(pos < _size);return _str[pos];
}
char& operator[] (size_t pos)
{assert(pos < _size);return _str[pos];
}

2.4 修改操作

2.4.1 push_back()

void push_back(char ch)
{// 因为在构造函数中我们已经设定好capacity一定不会为0,所以这里不用担心capacity为0的情况if (_size + 1 > _capacity){reserve(_capacity * 2);}_str[_size] = ch;++_size;// 最后别忘了在末尾补上\0_str[_size] = '\0';
}

2.4.2 append()

void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}// 将str追加到_str的尾部strcpy(_str + _size, str);_size += len;
}

2.4.3 operator+=()

使用函数重载分别实现尾插字符和尾插字符串即可

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

2.4.4 insert()

insert的实现有一些需要注意的地方,先来简单实现一下:

string& insert(size_t pos, char ch)
{assert(pos <= _size);if (_size + 1 > _capacity){reserve(_capacity * 2);}size_t end = _size;// 挪动数据while (end >= pos){_str[end + 1] = _str[end];--end;}_str[pos] = ch;++_size;return *this;
}

上述代码适用于大多数场景,但是当pos=0时,程序会崩溃,通过调试发现,当end=0的时候,–end没有变成-1,而是会变成很大的数,导致无限循环。原因是end的类型是无符号整数,那么如何解决呢?
我们可以用int类型的end,再将pos强制转换为int类型,这样确实可以解决问题,但是代码显得杂乱,所以还有另一种方法,在while循环中加入一个判断:当end=0时跳出循环。

while (end >= pos)
{_str[end + 1] = _str[end];if (end == 0)break;--end;
}

或者,我们将end初始化为_size+1,这样end就不会在等于0时减一了

string& insert(size_t pos, char ch)
{assert(pos <= _size);if (_size + 1 > _capacity){reserve(_capacity * 2);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;return *this;
}

插入字符串和上面也类似,需要注意的点是拷贝时要用strncpy,如果用strcpy会把\0也拷贝进去不符合要求。

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);size_t end = _size + len;if (_size + len > _capacity){reserve(_capacity + len);}while (end-len+1 > pos){_str[end] = _str[end - len];--end;}strncpy(_str + pos, str, len);_size += len;return *this;
}

实现了insert之后,push_back和append也可以通过调用insert来实现了:

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

2.4.5 erase

erase的实现可分为两种情况:
1.len=npos或者pos+len=_size:此时只需要把pos位置的数据改为\0即可
2.pos+len<_size:用strcpy来移动数据
在string中npos是一个静态成员变量,静态成员变量需要在类内声明,类外定义。不过对于const修饰的静态成员变量,但是只有整型可以,其他类型就不可以了。

class string
{
private:char* _str;size_t _size;size_t _capacity;static const size_t npos;// 可以//static const size_t npos = -1;/*static const int N = 10;int a[N];*/// 不可以//static const double dpos = -1;
};const size_t string::npos = -1;
string& erase(size_t pos, size_t len = npos)
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size -= pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}return *this;
}

2.4.6 find()

寻找字符:遍历一遍,找到相同字符就返回索引,找不到返回npos
寻找字符串:使用strstr来解决,找到返回p-_str,否则返回npos

size_t find(char ch, size_t pos = 0)
{assert(pos < _size);for (size_t i = pos; i < _size; ++i){if (_str[i] == ch){return i;}}return npos;
}
size_t find(const char* str, size_t pos = 0)
{assert(pos < _size);char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}return p - _str;
}

2.5 c_str()

const char* c_str()
{return _str;
}

3.非成员函数

3.1 operator<<和operator>>

operator<<实现很容易,一个范围for即可完成

operator>>的实现:
关于流提取,我们知道用cin或者scanf无法拿到空格或者\n,但是我们在循环插入数据时又需要用空格和\n来作为循环结束的标志,所以我们需要用in中的get()函数来读取输入的数据,它能够获取到空格和换行符。
我们知道operator+=在容量满了之后需要扩容,如果我们输入的字符串很大,就会频繁的扩容,效率很低,所以我们用buff数组作为缓冲来解决问题:当数据正常时先都装进buff中,如果buff满了,再将buff中的数据一并转入string里。

ostream& operator<<(ostream& out, string& s)
{for (auto ch : s){out << ch;}return out;
}
istream& operator>>(istream& in, string& s)
{s.clear();char 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();}// 如果buff中还有数据if (i != 0){buff[i] = '\0';s += buff;}return in; 
}

3.2 getline()

getline()遇见空格时不会停下,只有遇见\n才会停下。

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

3.3 字符串比较

都加上const,为的是让const对象也能使用。

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 || s == *this;
}
bool operator<(const string& s) const
{return !(*this >= s);
}
bool operator<= (const string & s) const
{return *this < s || s == *this;
}
bool operator!=(const string& s) const
{return !(s == *this);
}

4.整体代码

如是我们就实现了一个比较完整的string类,整体代码如下:

namespace lgr
{class string{public:typedef char* iterator;typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""): _size(strlen(str)){_capacity = _size == 0 ? 3 : _size;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& s): _str(nullptr), _size(0), _capacity(0){string tmp(s._str);swap(tmp);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}const char* c_str(){return _str;}const char& operator[] (size_t pos) const{assert(pos < _size);return _str[pos];}char& operator[] (size_t pos){assert(pos < _size);return _str[pos];}string& operator=(string s){swap(s);return *this;}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 || s == *this;}bool operator<(const string& s) const{return !(*this >= s);}bool operator<= (const string & s) const{return *this < s || s == *this;}bool operator!=(const string& s) const{return !(s == *this);}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){_size = n;_str[_size] = '\0';}else if (n > _size){if (n > _capacity){reserve(n);}size_t i = _size;while (i < n){_str[i] = ch;++i;}_size = n;_str[_size] = '\0';}}void push_back(char ch){insert(_size, ch);}void append(const char* str){insert(_size, str); }string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}string& insert(size_t pos, char ch){assert(pos <= _size);if (_size + 1 > _capacity){reserve(_capacity * 2);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;return *this;}string& insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);size_t end = _size + len;if (_size + len > _capacity){reserve(_capacity + len);}while (end-len+1 > pos){_str[end] = _str[end - len];--end;}strncpy(_str + pos, str, len);_size += len;return *this;}string& erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size -= pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}return *this;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}size_t find(char ch, size_t pos = 0){assert(pos < _size);for (size_t i = pos; i < _size; ++i){if (_str[i] == ch){return i;}}return npos;}size_t find(const char* str, size_t pos = 0){assert(pos < _size);char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}return p - _str;}void clear(){_str[0] = '\0';_size = 0;}private:char* _str;size_t _size;size_t _capacity;static const size_t npos;};const size_t string::npos = -1;ostream& operator<<(ostream& out, string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();char 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; }istream& getline(istream& in, string& s){char ch = in.get();while (ch != '\n'){s += ch;ch = in.get();}return in;}
}

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

一文读懂TSC时钟: (x86_64/arm64)实现介绍和编程使用

Linux(16)之Time Stamp Counter Author&#xff1a;Once Day Date&#xff1a;2023年5月30日 参考文档: 4. Environment Abstraction Layer — Data Plane Development Kit 23.03.0 documentation (dpdk.org)DPDK: lib/eal/include/generic/rte_cycles.h File Reference测量…

反射之使用自定义注解并处理自定义注解

注解&#xff1a;说起注解初学者可能不太明白&#xff0c;annotation是jdk1.5 引入的新特性&#xff0c;中文名叫注解&#xff0c;它提供了一种安全的类似注释的机制&#xff0c;用来将任何的信息或元数据&#xff08;metadata&#xff09;与程序元素&#xff08;类、方法、成员…

java自定义注解解析及自定义注解

jdk1.5之后提供了注解&#xff08;Annotation&#xff09;这一种语法。其主要作用是编译检查&#xff08;比如override&#xff09;和代码分析&#xff08;通过代码中添加注解&#xff0c;利用注解解析器对添加了注解的代码进行分析&#xff0c;获取想要的结果&#xff0c;一般…

自定义注解开发

自定义注解的语法要求&#xff1a; Target({ElementType.METHOD,ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Inherited Documented public interface Description {String desc();String author();int age() default 18; } 首先我们要明确这不是一个接口&#x…

自定义注解(上)自定义注解的定义和检查

什么是注解&#xff1f; 注解是接口的一种变型&#xff0c;定义一组属性&#xff0c;让类、方法或属性用标记的方式调用这些属性。不仅是带有一些属性和值&#xff0c;某些注解带有一些特殊的功能。 如单元测试Test&#xff0c;可以让方法不依赖主函数单独运行&#xff0c;右…

自定义注解详解

文章引用 深入理解Java&#xff1a;注解&#xff08;Annotation&#xff09;自定义注解入门 自定义注解详细介绍 说在最前 文章忽略特性的一些基本概念 注解的本质是反射 1. 自定义注解 注解其实就是一种标记&#xff0c;可以在程序代码中的关键节点&#xff08;类、方法、…

查看 HTTP 请求的数据.

文章结构 如果是 GET 请求如果是 POST 请求方法1&#xff1a;DEBUG 窗口&#xff08;**爽、超级爽、吴迪爽**&#xff09;&#xff1a;方法2&#xff1a;写方法读取流中数据&#xff08;繁琐&#xff0c;难用&#xff09;&#xff1a; 我们可能会碰到 MVC 拿不到前端的参数&…

基于Html5的在线资料库的设计与实现(asp.NET,SQLServer)

在线资料库系统采用.NET开发平台进行开发&#xff0c;开发工具采用Microsoft Visual Studio 2010集成开发环境&#xff0c;后台编程语言采用C#编程语言来进行编程开发&#xff0c;数据库我们采用当下流行的SQL Server 2008数据库管理系统来存放平台中的数据信息&#xff0c;整个…

【软硬件测试】测试经验:软硬件结合测试要点

目录 一、应用行业 二、测试要点 三、硬件测试 &#xff08;1&#xff09;测试含义 &#xff08;2&#xff09;测试方法 &#xff08;3&#xff09;相关链接 四、结合测试 &#xff08;1&#xff09;测试含义 &#xff08;2&#xff09;测试工具 &#xff08;3&am…

【ros/ros2】LCN及ros2节点的LCN改写

文章目录 序言1. ros2两种节点类型2. LCN是什么3. LCN状态转换4. LCN状态转换要做的事5. LCN节点功能划分6. ros2节点的LCN改写 序言 背景&#xff1a;ros2节点改写为lifecycle node节点 1. ros2两种节点类型 Node&#xff1a;和ros1中一样的节点基类LifecycleNode&#xff…

桶排序 — 计数排序和基数排序

计数排序 int类型数组&#xff0c;其中存的是员工的年龄。比如说16 - 150。对于这样的数据来讲&#xff0c;数据状况是受限的。此时如果将数组从小到大进行排序&#xff0c;该如果实现&#xff1f; 这个实现很简单&#xff0c;实现一个统计数组范围从 0 ~ 150&#xff0c;遍历原…

816墨盒计算机无法与,816墨盒怎么加墨 816墨盒加墨方法及注意问题【详解】

导语&#xff1a;随着时代的快速发展&#xff0c;人们生活水平的不断提高&#xff0c;打印机在我们日常生活中的应用也变得非常广泛&#xff0c;利用打印机打印文件&#xff0c;还有一些重要的材料&#xff0c;方便了人们的生活&#xff0c;给人们的生活提供了很大的便利&#…

打印机 检测到用过的耗材或者赝品耗材

检测到用过的耗材或者赝品耗材 大家好&#xff0c;今天续着给大家分享下惠普的803/805墨盒加墨应该注意的事项&#xff0c;先预习&#xff0c;加墨就没那么多困惑了~ 加墨后打印白线条、溅墨怎么办&#xff1f; ①先用温水浸泡打印头约30秒&#xff08;注意不要泡到芯片&…

打印机墨盒问题

因为打印机墨盒属于耗材&#xff0c;容易损坏&#xff0c;从而造成打印机没法打印。对于家用打印机来说&#xff0c;一个打印机也就三四百块钱&#xff0c;然后换一个新墨盒就得花掉一百左右&#xff0c;心里感觉贼不爽&#xff0c;墨盒那么小一个&#xff0c;居然要那么贵&…

墨盒 连供漏墨恒压问题

你提出了一个连供压力平衡原理的问题。 连供形状各式各样&#xff0c;但基本原理都是相同的。 以红色为例&#xff1a; 如上图。打印机静止时&#xff0c;墨水室的墨水重力&#xff0c;等于墨水室上方因为空气变稀薄后产生的负压。墨水不会流动。实现了压力的静平衡。 打印机工…

【Java 并发编程】深入理解 AQS - AbstractQueuedSynchronizer

深入理解 AQS - AbstractQueuedSynchronizer 1. AQS1.1 什么是 AQS1.2 AQS 具备的特性 2. AQS 原理解析2.1 AQS 原理概述2.1.1 什么是 CLH 锁2.1.2 AQS 中的队列 2.2 AQS 共享资源的方式&#xff1a;独占式和共享式2.2.1 Exclusive&#xff08;独占式&#xff09;2.2.2 Share&a…

JVM学习笔记(中)

1、垃圾回收算法 标记清除法 特点&#xff1a; 速度较快会产生内存碎片 注意&#xff1a;这里的清除并不是真正意义上的清除&#xff0c;即每个字节都清0&#xff0c;而是记录一下被清除的对象的起始和结束的地址&#xff0c;当下一次分配给一个新对象时&#xff0c;新对象…

《Java并发编程实战》课程笔记(四)

互斥锁 原子性问题到底该如何解决呢&#xff1f; “同一时刻只有一个线程执行”这个条件非常重要&#xff0c;我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的&#xff0c;那么&#xff0c;无论是单核 CPU 还是多核 CPU&#xff0c;就都能保证原子性了。 锁模型 …

RK3588平台开发系列讲解(驱动基础篇)设备树常用 of 函数

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、查找节点的 of 函数二、获取属性值的 of 函数三、实验示例3.1、查找的节点代码3.2、获取属性内容代码沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 设备树描述了设备的详细信息,这些信息包括数字类型的…

chatgpt赋能python:Python中-1的用法介绍

Python中-1的用法介绍 什么是-1&#xff1f; 在Python中&#xff0c;-1是一个特殊的索引值&#xff0c;它表示从序列的末尾开始向前数1个元素。这在对于列表、字符串、元组等序列类型进行操作时非常有用。 如何使用-1&#xff1f; 假设我们有一个列表&#xff1a; l [1, …