string的使用和模拟实现| 细致到strcpy ,strstr,strcmp都不放过

2024-03-19 01:20

本文主要是介绍string的使用和模拟实现| 细致到strcpy ,strstr,strcmp都不放过,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们模拟实现一个string 不是为了造一个比库里面更好的,而是熟悉语法,学习底层原理,复习数据结构等。

string的成员变量

string 就是有\0的顺序表,所以和顺序表的成员一样有,size,capacity,和一个指针

  1. 因为string的底层是字符串数组
    所以我们需要一个char*的指针指向数组
  2. size记录存放字符的个数不包括/0
  3. capacity作用是反应现在的容量以便后续扩容

string的构造方法

常用的就是第一个和第四个所以我们模拟实现也是这两个

在这里插入图片描述

用法

std::string s1; // 无参
std::string s2("hello word"); //字符串构造

无参的构造方法的实现

在这里插入图片描述

这样初始化可以吗?不可以 因为用空指针初始化_str则_str无法解引用,打印_str数组时会空指针异常。
怎么解决这个问题呢?可以给str开一个空间放/0

 string():_str(new char[1]),_size(0),_capacity(0){_str[0] = '/0';}

我们如果想传一个c字符串来构造字符串怎么做呢?

全缺省的构造参数的实现

就是用c字符串str的长度给_str开空间,再把str拷贝_str

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

可能有的同学已经忘了strcpy的原理了我简单复习下

strcpy的模拟实现

就是挨个拷贝,连同反斜杠/0一起,因为没有/0就不能称为一个c串了。

char* my_strcpy(char* dest, const char* src) 
{assert(dest != nullptr && src != nullptr);char* ret = dest;while (*src != '\0') {*(dest++ ) = *(src++);}*dest = *src;return ret;
}

为什么这里的_size要+1?

因为strlen算出的字符串大小不包括/0,所以开辟的空间的大小要_size+1

为什么这里是默认传空字符串?

如果传空指针解strlen(str)没有/0作为字符串结束标志则会崩溃,不知道在哪结束所以不能传空指针,而空字符串默认是有一个/0的。

赋值运算符重载

 void  operator=(const string& s1)
{_size = s1._size;_capacity = s1._capacity;char* tmp = new char[_capacity + 1];strcpy(tmp, s1._str);delete[]_str;_str = tmp;
}                     

析构函数

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

遍历字符串

operator[]

这个有个非常进步的地方,就是它会越界检查了,c字符串越界的读不会检查而越界的写是抽查。

使用

s1[2]  // 非常的方便

传统c语言字符串下标遍历的缺点

越界读不报错
在这里插入图片描述
越界写报错但是是抽查的,在边界检查严格些

模拟实现

 char& operator[](size_t pos) {assert(pos< _size);return _str[pos];}

有了断言这样不管是读还是写都会检查了

迭代器

使用

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

注意这个end是\0的那个位置,而不是最后一个字符,因为是最后一个字符的话 it!=s2.end()就不打印最后一个字符了

不同类的迭代器可能不一样,我们用指针简单的实现一个string的迭代器

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

范围for

当我们把迭代器写好后,范围for自动就成了。
因为范围for底层就是调用迭代器
在这里插入图片描述

成员函数

reserve

在这里插入图片描述
功能是给字符串的容量增长到n个字符的空间,当n<_capacity时不起作用。

reserve的使用

reserve(100);

reserve的模拟实现

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

push_back

在这里插入图片描述
功能追加一个字符给字符串

push_back的使用

string s1("hello word");
s1.push_back('a');

push_back的模拟实现

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

append

appen的功能是把字符串变长通过在现存的字符串的末尾后面添加额外的字符串。
!

apeend的模拟实现

 void append(const string& s) {int len = strlen(s._str);if (_size > _capacity - len){reserve(_size + len);}strcpy(_str + _size, s._str);}

insert

在这里插入图片描述
功能是在pos位置之前插入其他字符/字符串

insert的使用

 std::string s("hello");std::string b(" word");s.insert(0, b);cout << s;

insert的模拟实现

插入字符
 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--;}_str[pos] = ch;_size++;}

版本二

void insert(size_t pos, char c) 
{assert(pos <= _size);if (_size >= _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}int end = _size;while (end >= (int)pos) //不强转pos end变成无符号数 一直>=0 {_str[end+1] = _str[end];end--;}_str[pos] = c;_size++;}

注意这里的pos一定要强制类型转换不然end会整型提升 当头插时pos = 0 end被提升为无符号数了 一直大于pos 0 所以会出错

插入字符串

 void  insert(size_t pos, string& s) {int len = s._size;assert(pos <= _size);if (_size+len  >= _capacity){reserve(_size + len);}size_t end = _size + len;while (end >= pos + len){_str[end] = _str[end - len];end--;}strncpy(_str + pos, s.c_string(),len);_size += len;}

注意这里要有strncp目的是不让\0拷贝下来

erase

功能:擦除pos位置起len长度的字符串
在这里插入图片描述
第一个用得很多我们模拟实现第一个

erase的使用

   std::string s1("hello word");s1.erase(12);

erase的模拟实现

首先判断pos位置是否合法 pose小于size才行 因为等于size就把\0擦除了。
判断 npos = -1 || len + pos > size 成功就全部擦除把o位置的值赋值成\0
或者把pos位置赋值成\0
再把 pos+len的位置拷贝到 pos处
size-= len

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

resize

功能是改变字符串的长度
如果当前的size小于n,就缩短到n
若n大于size,如果指定了字符就在后面插入指定的字符否则插入\0以达到n的长度
在这里插入图片描述

resize的使用

  std::string s1("hello word");s1.resize(100,'a');cout << s1;

rise的模拟实现

首先判断size和n的关系
n小于size 则把n位置的字符赋值\0
否则 扩容到n的长度
把下标size到下标n-1的字符全部赋值成ch
size 改为 n

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

substr

在这里插入图片描述

功能:返回从主串截取的从pos位置长度为len的子串

substr的使用

std::string s1("hello word");
cout << s1.substr(0, 5);

substr的模拟实现

因为要返回一个新的字符串,所以我们先定义个新串
然后一个+=循环 ,循环条件从pos到下标pos+len-1

string substr(size_t pos = 0, size_t len = npos)
{string sub;//if (len == npos || len >= _size-pos)if (len >= _size - pos){for (size_t i = pos; i < _size; i++){sub += _str[i];}}else{for (size_t i = pos; i < pos + len; i++){sub += _str[i];}}return sub;

find

寻找子串在主从中从pos位置开始第一个出现的位置
或者找一个字符在字符串pos后的第一个位置
没有找到则返回npos

使用

     string s1("hello word");int ret1 = s1.find("word", 3);int ret2 = s1.find("w",3);

模拟实现

size_t find(const string& str, size_t pos = 0) const 
{assert(pos < _size);char * ret= strstr(_str + pos, str.c_string());if(ret)return ret - _str;return npos;
}size_t 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;
}

这里我们直接用的库函数strstr 可能有的同学忘了ststr的功能
我简单复习一下 就是在主串中在子串第一个出现的位置然后返回匹配到的第一个位置的指针。没有找到则返回空指针

strstr的模拟实现

这里我们使用暴力查找的方法简单的模拟一下

     char* my_strstr(const char* str, const char* substr) {int len = strlen(substr);int i = 0;while (i < strlen(str)) {size_t j = 0;for (; j < len; j++){if (substr[j] == str[i]){i++;}else{break;}}if (j == len) {return (char*) str+i - len;}i++;}return nullptr;}

swap

交换两个字符串

使用

string s1("hello word");
string s2;
s2.swap(s1);

改写

在这里插入图片描述
我们直接把T变为 string string c(a); a =b ;b = c;
这里会走一次拷贝构造 两次赋值运算符重载 总共三次构造 还外加一次析构 c 代价太大了 所以必须重写

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

为了防止别人调用库里面的swap(T a,T b)我们需要在类外面写一个相同参数的swap,这样调用的时候就优先调用我们写了的。
因为函数模板有现成的吃现成的。

	void swap(string& x, string& y){x.swap(y);}

string的赋值运算符重载

==

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

可能有的同学忘了strcmp的原理
就是两个字符串从头开始比较 相等就继续走 如果遇到串1的第一个字符大于串2的第一个字符 return 1 小于则return -1 全部走完了则return 0

strcmp的模拟实现

    int my_strcmp(const char* a, const char* b) {while (*a == *b && *a != '\0' && *b != '\0'){a++;b++;}if (*a > *b)return 1;if (*a < *b)return -1;return 0;}

<

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

>>

这里不能用 scanf 和 cin 因为 cin 和scanf 都把空格和 回车作为分隔符 读不到 则程序永远不会结束。

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

改进

我们这个+=需要频繁扩容不太好。
如果我们用reserve 开个很大的空间 则输入很小的时候又浪费了
所以我们用一个数组暂存字符 然后根据数组的大小一次性开辟好空间,就不用频繁扩容了。

 istream& operator>>(istream& in,  string& s) {char ch;s.clear();char buff[128];ch = in.get();int i = 0;while (ch != ' ' && ch != '\n') {//s += ch;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;}

什么叫现代写法呢?就是假借他人的手完成相应的功能

拷贝构造函数的现代写法

在这里插入图片描述
借构造函数的手 构造一个和s1一样的字符串 然后和 this 交换

         /* string(const string &s1) {_size = s1._size;_capacity = s1._capacity;char* tmp = new char[_size + 1];strcpy(tmp, s1._str);this->_str = tmp;}*/string(const string& s1) {string tmp(s1.c_string());swap(tmp);}

赋值运算符重载的现代写法

第三个版本是由第二个版本而进过来的
第二个版本借助 s 拷贝构造 字符串ss
第三个版本 我们没有用引用编译器帮我们调用了拷贝构造

// version 1/*string& operator=(const string& s){string ss(s);swap(ss);return *this;}*/// version 2/*string& operator=(const string& s){string ss(s);swap(ss);return *this;}*/// version 3string& operator=( string ss){swap(ss);return *this;}

谢谢观看

这篇关于string的使用和模拟实现| 细致到strcpy ,strstr,strcmp都不放过的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/824349

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置