【C++】string的底层原理及实现

文章目录

  • string类的存储结构
  • 默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 赋值重载
  • 容量操作
    • size()
    • capacity()
    • reserve()
    • resize()
    • clear()
  • 遍历与访问
    • operator[ ]
    • 迭代器范围与for
  • 增删查改
    • push_back()
    • pop_back()
    • append()
    • operator+=
    • insert()
    • erase()
    • c_str()
    • find()
    • substr()
  • 非成员函数
    • operator+
    • 关系运算符
    • 流插入<<和流提取>>
    • getline()

本篇参考C++string类参考手册,实现一些string的常用接口,接口原型在该网站查阅。

string类的存储结构

string类的底层实际上是char类型的顺序表,所以结构上也比较相似。

namespace lw
{class string{public:static const size_t npos;private:size_t _size;//有效数据个数size_t _capacity;//可存储的容量char* _str;//指向字符串起始位置的地址};const size_t string::npos = -1;
};

STL源码中,许多整型变量类型都是size_t无符号整型(没有负值);npos是string类常用到的一个值,有些函数的参数或者返回值是npos,并且这个值设为-1(表示232-1),参考标准库中的定义。
在这里插入图片描述

为什么string定义在一个命名空间中?
我们如果展开了标准库using namespace std; 那string默认使用的就是标准库中的。如果不展开标准库,那么所有的cin和cout都需要加上std:: 比较繁琐。

用一个命名空间对我们自己实现的string类进行封装,可以方便我们随时测试代码,与库里的string类进行测试对比,只需要改::前面的作用域即可。

#include <iostream>
using namespace std;
int main()
{string s("hello world");//默认标准库std::string s("hello world");//标准库lw::string s("hello world");//自己定义的类cout << s << endl;return 0;
}

string与C语言中的char类型字符串的一个较大的区别就是:char类型的字符串是以\0为结束符,遇到\0就停止;而string是以_size来判断是否结束,遇到\0并不会停止。 这点很重要,一定要弄清楚!


默认成员函数

常用的四个默认成员函数,一般情况我们不需要显示定义,使用编译成生成的即可;但string类涉及到资源申请,所以我们必须自己显示定义。

构造函数

官网的参考手册中给的string标准库有很多构造函数重载,我们只实现常用的两个构造。这两个我们可以合并成一个实现。

string();
string(const char* s);

缺省值不能给nullptr,一是因为strlen无法计算空指针,二是因为无参标准库默认给的就是空串。
这里strcpy和memcpy都可以,因为是对char类型字符串进行拷贝,拷贝str在\0之前的内容。用memcpy只是为了和后面的写法统一。

string(const char* str = ""): _size(strlen(str)), _capacity(strlen(str)), _str(new char[strlen(str) + 1])//多一个空间给'\0'
{//strcpy(_str, str);//strcpy也可以,只是为了与后面统一,换成memcpymemcpy(_str, str, _size + 1);//'\0'也要拷贝
}

我们在new新空间时,每次多new一个空间给\0留位置。 memcpy在拷贝时,多拷贝一个字节把末尾\0也拷贝过去。


析构函数

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

拷贝构造函数

深浅拷贝问题
编译器默认生成的是浅拷贝,就是只将s._str的指针地址拷贝给了新对象的_str,两个对象指向同一块地址! 那么就会出问题:

1.一块空间最后会析构两次,程序必然崩溃;
2.一个对象修改,另一个对象也会修改;

所以我们需要进行深拷贝,重新申请一块空间,把s._str指向地址的内容拷贝过来。

string(const string& s)
{_str = new char[s.capacity() + 1];//多一个空间给'\0'//strcpy(_str, s._str);//遇到'\0'终止,后面内容无法拷贝memcpy(_str, s._str, s._size + 1);//末尾'\0'也拷贝过来_size = s._size;_capacity = s._capacity;
}

注意:这里不能用strcpy来拷贝数据,因为strcpy的拷贝结束条件是遇到\0终止,而string对象是可以存储\0的,strcpy不会将\0后面内容拷贝过来,所以要用memcpy或者memmove按照字节进行拷贝,多拷贝一个字节是将末尾的\0也拷贝过来。


赋值重载

与拷贝构造原理类似,但要先将申请的空间存放在临时变量里,防止空间开辟失败丢失原数据。原始空间别忘了释放!

string& operator=(const string& s)
{if (this != &s){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;//释放原始空间_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;//支持连续赋值
}

第二种写法:我们可以利用库里的swap函数将两个对象_str指向的地址和数据个数、容量完全交换。顺便将swap接口也实现了。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
//现代写法
string& operator=(string s)//不能传引用也不能加const
{swap(s);return *this;
}

注意:不能影响右操作数实参的值,所以右操作数传参只能以传值方式,且不能加const,否则不能交换改变。并且我们不需要进行自己给自己赋值的判断,因为s是实参的拷贝,一定与原对象地址不同。


容量操作

size()

size_t size() const
{return _size;
}

capacity()

size_t capacity() const
{return _capacity;
}

reserve()

只扩容不缩容,指定n大于原始容量则进行扩容,否则不进行操作。
拷贝时还是同样要注意\0问题,不能用strcpy。

void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//strcpy(tmp, _str);//错误,无法拷贝'\0'后面的内容memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}
}

resize()

resize()只改变有效数据个数,不会改变容量。
n > _size:缩小长度为n,多余内容删掉。
n < _szie:增加长度到n,剩下空间用给定的字符参数c填充,无参默认补充\0

void resize (size_t n);
void resize (size_t n, char c);

库里的resize()函数有两个版本,同样我们可以合成一个版本,第二个参数给缺省值\0即可。

void resize(size_t n, char ch = '\0')
{assert(n >= 0);if (n < _size)//缩小长度,后面删掉{_size = n;_str[_size] = '\0';}else{reserve(n);//reserve会检查容量for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}
}

clear()

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

遍历与访问

operator[ ]

需要实现两个版本:普通对象调用[ ]可读可写,const对象调用[ ]只可读不可写。

char& operator[](size_t pos)//可读可写
{assert(pos < _size);return _str[pos];
}
const& char operator[](size_t pos) const//可写
{assert(pos < _size);return _str[pos];
}

迭代器范围与for

迭代器不一定是指针,而string的迭代器我们可以用指针来模拟实现。将指针重命名为迭代器。

typedef char* iterator;//普通迭代器 可读可写
iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}typedef const char* const_iterator;//const迭代器 只可读
const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}

左闭右开区间,所以begin()返回字符串起始位置,end()返回末尾字符的下一个位置\0

范围for
实现迭代器后,我们就可以使用范围for了。范围for是C++11的新语法,它的底层是傻瓜式地替换成迭代器,支持迭代器就支持范围for。

int main()
{lw::string s1("hello world");lw::string::iterator it = s1.begin();while (it != s1.end()){*it += 1;cout << *it << ' ';it++;}cout << endl;for (auto ch : s1){cout << ch << ' ';}cout << endl;
}

增删查改

push_back()

末尾要放\0

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

pop_back()

void pop_back()
{assert(_size > 0);_size--;_str[_size] = '\0';
}

append()

append()在末尾追加字符串,空间不够则扩容,如果每次将_capacity扩2倍,一方面可能造成空间浪费,另一方面可能原始空间太小导致频繁扩容,所以我们最好提前计算好扩容的空间。

append的接口也有很多,下面给出最常用的两种。

string& append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//strcpy(_str + _size, str);//strcat效率O(n+m) strcpy效率O(m)memcpy(_str + _size, str, len + 1);//末尾的'\0'也要拷贝_size += len;return *this;
}
string& append(const string& s)
{if (_size + s._size> _capacity){reserve(_size + s._size);}memcpy(_str + _size, s._str, s._size + 1);//末尾的'\0'也要拷贝_size += s._size;return *this;
}

operator+=

+=是string经常用到的操作符,使用非常方便。
operator+=总共有三个重载版本,我们可以直接复用push_back()和append()

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

insert()

任意位置插入字符ch

string& insert(size_t pos, size_t n, char ch)
{assert(pos <= _size);if (_size + n > _capacity){reserve(_size + n);}for (size_t end = _size; end >= pos; end--){//pos==0时end会减到-1//无符号整型 end=-1时表示42亿多 不加判断会无限循环if (end == npos){break;}_str[end + n] = _str[end];}for (size_t i = 0; i < n; i++){_str[i + pos] = ch;}_size += n;return *this;
}

任意位置插入字符串str

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}for (size_t end = _size; end >= pos; end--){if (end == npos){break;}_str[end + len] = _str[end];}memcpy(_str + pos, str, len);//不能多拷贝一个字节_size += len;return *this;
}

注意:这里的memcpy不能跟之前一样多拷贝一个字节,否则会将原对象pos+1的字符替换成\0,会出错。


erase()

string& erase(size_t pos, size_t len = npos)
{assert(pos < _size);//没给参数 或者 要删除的个数>=pos后面剩下的个数 则后面包括pos位置全部删掉if (len == npos || pos + len >= _size){//pos及pos后面所有字符都删掉_size = pos;_str[_size] = '\0';}else{memcpy(_str + pos, _str + pos + len, len);_size -= len;}return *this;
}

c_str()

返回字符串首地址,以\0结束。这个接口主要是用来兼容C语言的。

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

find()

find查找失败会返回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);const char* p = strstr(_str + pos, str);if (p){return p - _str;}else{return npos;}
}

substr()

返回子串

string substr(size_t pos = 0, size_t len = npos) const
{assert(pos < _size);size_t n = len;//子串长度if (len == npos || pos + len > _size){n = _size - pos;}string tmp;for (size_t i = 0; i < n; i++){tmp += _str[i + pos];//复用+=}return tmp;
}

非成员函数

operator+

实际上+操作符很少用,直接用+=更方便省事;这里只实现了一个接口,重载为友元函数。

string operator+(const string& s1, const string& s2)
{string tmp(s1);tmp += s2;return tmp;
}

关系运算符

C++官方手册中,每种运算符都有3种重载版本,这里每个只实现了一种,重要的是理解本质,加深对string的理解。

为什么用C语言的memcmp函数进行字节上的比较,不用strcmp?
因为strcmp遇到\0终止,而string不看\0,以_size为终止。
当然也可以不用memcmp,遍历每个字符进行比较。

bool operator==(const string& s1, const string& s2)
{return s1._size == s2._size && memcmp(s1._str, s2._str, min(s1._size, s2._size)) == 0;
}
bool operator<(const string& s1, const string& s2)
{int ret = memcmp(s1._str, s2._str, min(s1._size, s2._size));return ret == 0 ? s1._size < s2._size : ret < 0;
}//直接复用
bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}
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);
}

流插入<<和流提取>>

流插入的实现可以借助范围for,本质是遍历整个字符串打印每个字符。

ostream& operator<<(ostream& out, const string& s)
{//out << s._str;//'\0'后面的内容无法打印for (auto ch : s){out << ch;}return out;//带返回值 支持连续输出
}

流提取需要注意几种情况:
1.每次读取之前需要先清空string对象,否则会叠加之前的内容。
2.为什么用get()读取而不用>>?
流提取>>默认是跳过空格和换行的,所以>>永远无法读取空格和换行,程序会一直运行一直可以输入。
3.如果字符串前面有空格或者换行,标准库的>>会默认清理空格和换行。所以要预先处理前面的空格和换行。

istream& operator>>(istream& in, string& s)
{s.clear();//清空之前的内容char ch = in.get();//可以读取空格和换行//处理掉缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

getline()

getline可以自定义读取的分隔符,遇到分隔符就不再读取,字符参数默认为\n,换行截断。

istream& getline(istream& in, string& s, char delim = '\n')
{s.clear();//清空之前的内容char ch = in.get();while (ch != delim){s += ch;ch = in.get();}return in;
}

整体代码->:string模拟实现代码

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣考研经典题 反转链表

核心思想 头插法&#xff1a; 不断的将cur指针所指向的节点放到头节点之前&#xff0c;然后头节点指向cur节点&#xff0c;因为最后返回的是head.next 。 解题思路 1.如果头节点是空的&#xff0c;或者是只有一个节点&#xff0c;只需要返回head节点即可。 if (head null …

Echarts中的热力图和漏斗图(在Vue中使用热力图和漏斗图)

热力图 (Heatmap) Echarts的热力图用于展示两个维度数据矩阵中的值分布情况。它通过在平面上划分成多个矩形区域&#xff0c;并用不同的颜色填充这些区域来表示数据的大小或强度。颜色渐变从浅到深通常映射着数值从小到大&#xff0c;从而直观展示数据的集中程度和分布模式。热…

半同步主从复制

半同步主从复制的概念 半同步主从复制&#xff08;Semisynchronous Replication, SBR&#xff09;是MySQL数据库中的一种数据复制方式&#xff0c;它在异步复制的基础上增加了一定程度的同步性&#xff0c;旨在提高数据安全性&#xff0c;减少数据丢失的风险。 半同步主从复制…

阶段三:项目开发---大数据开发运行环境搭建:任务8:安装配置Redis

任务描述 知识点&#xff1a;安装配置Redis 重 点&#xff1a; 安装配置Redis 难 点&#xff1a;无 内 容&#xff1a; Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务&#xff0c;是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可…

电路基础知识汇总

1.0 串连&#xff0c;并联&#xff0c;混连 串联的定义 电路串联是一种电路元件的连接方式&#xff0c;其中各个元件沿着单一路径互相连接&#xff0c;形成一个连续的链。在串联电路中&#xff0c;每个节点最多只连接两个元件&#xff0c;这意味着电流只有一条路径可以通过整个…

Apache Seata Mac下的Seata Demo环境搭建

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Mac下的Seata Demo环境搭建&#xff08;AT模式&#xff09; 前言 最近因为工作需要&#xf…

Git教程

文章目录 Git分布式版本控制工具版本控制器的方式常用命令远程仓库Tip Git分布式版本控制工具 ​ Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。 ​ Git是分布式的&#xff0c;Git不需要有中心服务器&#xff0c;我们每…

【感谢告知】本账号内容调整,聚焦于Google账号和产品的使用经验和问题案例分析

亲爱的各位朋友&#xff1a; 感谢您对本账号的关注和支持&#xff01; 基于对朋友们需求的分析和个人兴趣的转变&#xff0c;该账号从今天将对内容做一些调整&#xff0c;有原来的内容改为Google&#xff08;谷歌&#xff09;账号和产品的使用经验&#xff0c;以及相关问题的…

24西安电子科技大学经济与管理学院—考研录取情况

24西安电子科技大学—经理与管理学院—考研录取统计 01、经理与管理学院各个方向 02、24经济与管理近三年复试分数线对比 1、经管院24年院线相对于23年院线普遍下降2-15分&#xff0c;个别专业上涨4-10分。 2、经管院应用经济学2024年院线350分&#xff1b;管理科学与工程院线…

java join与yield方法

join() join() 方法的主要作用是使当前线程&#xff08;调用 join() 方法的线程&#xff09;等待目标线程完成执行。当目标线程执行完毕后&#xff0c;当前线程才会继续执行。 代码示例&#xff1a; public class JoinExample {public static void main(String[] args) {Thr…

保研复习 | 数据结构

目录 CH1 绪论☆ 数据项、数据元素、数据结构☆ 逻辑结构和存储结构的区别☆ 顺序存储结构和链式存储结构的比较☆ 算法的重要特性☆ 算法的复杂度 CH2 线性表☆ 单链表 CH3 栈、队列和数组☆ 栈和堆是什么&#xff1f;☆ 栈在括号匹配中的应用☆ 栈在表达式求值中的应用☆ …

Linux|信号

Linux|信号 信号的概念信号处理的三种方式捕捉信号的System Call -- signal 1.产生信号的5种方式2.信号的保存2.1 core 标志位 2.信号的保存2.1 对pending 表 和 block 表操作2.2 阻塞SIGINT信号 并打印pending表例子 捕捉信号sigaction 函数验证当前正在处理某信号&#xff0c…

数据库SQL Server常用字符串函数

文章目录 字符串函数 字符串函数 CONCAT:拼接字符串 CONCAT(COLUMN1,_,COLUMN2) AS COLCONVERT&#xff1a;转换数据类型 CONVERT(data_type(length),data_to_be_converted,style)例如&#xff1a;CONVERT(VARCHAR(10),GETDATE(),110) SUBSTRING()&#xff1a;从字符串中返回…

java项目总结5

1.单列集合顶层接口Collction 集合体系结构 注意&#xff1a;因为Collection定义的方法是共性的&#xff0c;使用不能通过搜引来删除&#xff0c;只能通过元素的对象进行删除&#xff0c;返回值是boolean类型。例如我添加了"aaa"进List集合&#xff0c;删除则要对象…

STM32-01 推挽输出-点亮LED

本文以STM32中点亮LED为例&#xff0c;解读推挽输出的原理 推挽输出介绍 所谓的推挽输出&#xff0c;就是通过控制输出控制模块&#xff0c;打开或者关闭P-MOS或者N-MOS。 ─ 推挽模式下&#xff1a;输出寄存器上的’0’激活N-MOS&#xff0c;而输出寄存器上的’1’将激活P-M…

局部静态变量实现的单例存在多个对象

文章目录 背景测试代码运行测试尝试打开编译器优化进一步分析 背景 业务中出现日志打印失效&#xff0c;发现是因为管理日志对象的单例在运行过程中存在了多例的情况。下面通过还原业务场景来分析该问题。 测试代码 /* A.h */ #ifndef CALSS_A #define CALSS_A#include <…

打造属于自己的脚手架工具并发布到npm仓库

一、创建项目 使用 npm init -y 创建项目创建项目入口文件 index.js在 package.json 中添加 bin 字段使用 npm link 命令将文件映射至全局&#xff0c;使可以在本地测试 zp 命令 // "zp" 为用于全局执行脚手架的命令&#xff0c;vue-cli中使用的是vue命令 "bi…

基于java+springboot+vue实现的旅游管理系统(文末源码+lw+ppt)23-402

研究的内容 当下流行的WPS、Word等办公软件成为了人们耳熟能详的系统&#xff0c;但一些更加专业性、性能更加强大的网络信息工具被人们“埋没”在互联网的大海中。甘肃旅游管理系统是一个便于用户查看热门景点、酒店信息、推荐线路、旅游攻略、景点资讯等&#xff0c;管理员进…

【Python基础篇】你了解python中运算符吗

文章目录 1. 算数运算符1.1 //整除1.2 %取模1.3 **幂 2. 赋值运算符3. 位运算符3.1 &&#xff08;按位与&#xff09;3.2 |&#xff08;按位或&#xff09;3.3 ^&#xff08;按位异或&#xff09;3.4 ~&#xff08;按位取反&#xff09;3.5 <<&#xff08;左移&#…

HTML 【实用教程】(2024最新版)

核心思想 —— 语义化 【面试题】如何理解 HTML 语义化 ?仅通过标签便能判断内容的类型&#xff0c;特别是区分标题、段落、图片和表格 增加代码可读性&#xff0c;让人更容易读懂对SEO更加友好&#xff0c;让搜索引擎更容易读懂 html 文件的基本结构 html 文件的文件后缀为 …