string的模拟实现

news2024/10/7 11:31:59

目录

​一、模拟实现中类的组织

二、默认成员函数

1.默认构造函数

2.拷贝构造函数

(1)传统写法——循规蹈矩

(2)现代写法——偷天换日

3.析构函数

4.赋值运算符重载

二、元素访问

三、容量操作

1.容量与有效数据

2.改变空间容量

(1)reserve函数

(2)resize函数

四、迭代器

五、数据尾插

1.尾插一个字符

2.尾插一个字符串

3.+=的重载

六、字符串的比较

七、在特定位置插入数据

1.pos位置插入一个字符

2.pos位置插入一个字符串

八、清除数据

1.erase函数

2.清除数据

九、其他成员函数

1.返回string对象转C语言字符串

2.查找字符和子串

十、非成员函数

1.<<流插入重载

2.>>流提取重载

十一、测试代码


一、模拟实现中类的组织

 我们使用两个文件,一个是string_class.h,另一个是test.c,头文件用于定义string类,cpp文件编写测试代码。

string类的底层是顺序表,顺序表的内容可以看这里:

string_class.h

#pragma once
#include<iostream>
#include<assert.h>


namespace my_string//用一个命名空间包起来
{
    class string//我们实现的string类    
    {
    public:
        //这里定义成员函数,包括下面第二至九部分
    private:
        char* _str;//存放字符的顺序表
        size_t _size;
        size_t _capacity;
        const static size_t npos = -1;//用于缺省值,-1对size_t是无符号整形最大值
    };
    //这里定义非成员函数,包括下面的第十部分
}

test.c

#include"string_class"
void test()
{
    //测试代码
}
int main()
{
    test();
    return 0;
}

二、默认成员函数

1.默认构造函数

用缺省参数输入参数就构造对应字符串,不输入参数就构造空字符串

string(const char* str = "")
{
    _size = strlen(str);
    _capacity = _size;//容量与元素量一致
    _str = new char[_capacity + 1];//留个位置给\0
    strcpy(_str, str);//复制过来
}

2.拷贝构造函数

(1)传统写法——循规蹈矩

开同样的空间,赋同样的值,拷数据

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

(2)现代写法——偷天换日

这个写法我愿称之为挂路灯写法。(群资本家,时不时来群里看看剩余价值.jpg)

string(const string& s)
{
    string temp(s._str);//先通过构造函数构造一个与str一样的temp对象
    std::swap(_str, temp._str);//swap是std中提供的一个交换函数,可以交换任意类型的数据
    std::swap(_size, temp._size);
    std::swap(_capacity, temp._capacity);//然后将所有的内部数据全部交换,temp的内容就直接给了*this对象
    //出了作用域temp被销毁,但是*this的换来的temp构造的内容被保存了下来
}

3.析构函数

释放内存,指针和变量置零

~string()
{
    delete[] _str;
    _str = nullptr;//指针必须置空,否则可能存在野指针
    _size = 0;
    _capacity = 0;
}

4.赋值运算符重载

string& operator=(const string& s)
{
    if (&s != this)//避免给自己赋值(s = s),我等于我自己
    {
        char* p = new char[s._capacity + 1];//开新空间
        strcpy(p, s._str);//拷数据
        delete[] _str;//将原数据的空间释放掉
        _str = p;//指向新空间
        _size = s._size;
        _capacity = s._capacity;//将变量拷过去
    }
    return *this;
}

二、元素访问

[]的重载分为可读可写和只读的两个,其实at的实现也很类似,不写实现了。

//不可修改,返回的const char和const修饰的this是不可修改的
const char operator[](size_t pos) const
{
    assert(pos < _size);//加上越界检查
    return _str[pos];
}

//可修改,不加const修饰就行了
char operator[](size_t pos)
{
    assert(pos < _size);//加上越界检查
    return _str[pos];
}

三、容量操作

1.容量与有效数据

size_t size() const;——————返回string有效数据的长度

size_t length() const;——————返回string有效数据的长度

size_t capacity() const;——————返回开辟空间的大小

bool empty() const;——————检测当前string是否是空字符串

每一个都给this加上const最好,保证不会修改数据

//返回有效数据个数
size_t size() const
{
    return _size;
}

//返回容量大小
size_t capacity() const
{
    return _capacity;
}
//判断是否为空

bool empty() const
{
    if (_str[0] == '\0')
        return true;
    else
        return false;
}

2.改变空间容量

(1)reserve函数

由于硬件设计的一些特征,我们在内存管理时用指针开空间时并不能缩容。所以在我们设计程序时,是要尽力避免缩容的。

在这个函数中,我们可以分为两种情况:

reserve的空间大于_capacity:扩容到新空间大小

reserve的空间小于等于_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)resize函数

在这个函数中,我们也可以分为两种情况:

reserve的空间大于_capacity:扩容到新空间大小,再在后面的空位置补上对应字符

reserve的空间小于等于_capacity:将对应的下标位变为\0

在大于_capaciity时,先扩容,然后补全空位,扩大_size的大小,最后在后面加上\0;小于等于_capacity时,也分为两种情况,一种是小于_size,我们直接在_str[n]处(数组元素下标由0开始)加上\0,另一种是大于_size,小于_capacity,我们在_str[n]加上\0对整体数据也没有影响,就不再多加一个判断了。同样,函数也只用异地扩容,不缩容。

//改变空间大小,若新空间大于旧空间插入对应字符,小于则直接截断
void resize(size_t n, char ch = '\0')
{
    if (n > _capacity)
    {
        reserve(n);
        for (size_t i = _size; i < n; i++)
        {
            _str[i] = ch;
        }
        _size = n;
        _str[n] = '\0';
    }
    else
    {
        _str[n] = '\0';
        _size = n;
    }
}

四、迭代器

我这里只实现了正向迭代器,string的迭代器直接使用指针就可以了,反向是一样的底层组织,具体的迭代器实现还需要我们以后去学习。

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

五、数据尾插

1.尾插一个字符

和顺序表一样,先检查容量,不够就扩容,够就直接插数据

//插入字符
void push_back(char c)
{
    if (_size == _capacity)//容量等于有效字符个数需要扩容
    {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        //用一个三目操作符,设置新空间大小,如果容量位0设置新容量为4,反之新容量在原基础上乘2
        reserve(newcapacity);//开新的空间
    }
    _str[_size++] = c;//尾插数据同时加一
    _str[_size] = '\0';//补上\0
}

2.尾插一个字符串

这个和顺序表的扩容就有一些区别了,因为我们不知道需要插入的字符串的长度,所以不能用原来的2倍或1.5倍扩容,而是直接扩容到能直接容纳新字符串。

//插入字符串
void append(const char* str)
{
    size_t len = strlen(str);
    if (len + _size > _capacity)//不够长度
    {
        reserve(len + _size);//扩容
    }
    strcpy(_str + _size, str);//在尾部将数据拷贝过去
    _size += len;//有效数据长度也需要加上
}

3.+=的重载

我之前说过,+=可以尾插字符和字符串,其实就是通过这上面两个函数实现的

//尾插字符
string& operator+=(char c)
{
    push_back(c);
    return *this;
}
//尾插字符串
string& operator+=(const char* str)
{
    append(str);
    return *this;
}

六、字符串的比较

底层使用C语言的strcmp就可以了,而且只需要实现小于和等于就可以了,其他的大于、大于等于、小于等于和不等于都可以通过逻辑操作符实现

bool operator<(const string& s)
{
    if (strcmp(c_str(), s.c_str())>0)    
        return true;

    else
        return false;
}
bool operator==(const string& s)
{
    if (strcmp(c_str(), s.c_str()) == 0)

        return true;

    else
        return false;
}

bool operator<=(const string& s)
{
    if (!(*this > s))
        return true;
    else
        return false;
}

bool operator>(const string& s)
{
    if (!(*this < s && *this != s))
        return true;
    else
        return false;
}

bool operator>=(const string& s)
{
    if (!(*this < s))
        return true;
    else
        return false;
}


bool operator!=(const string& s)
{
    if (!(*this == s))
        return true;
    else
        return false;
}

七、在特定位置插入数据

1.pos位置插入一个字符

与之前尾插的扩容原则一致,不过需要将包括pos后的数据向后挪动一位然后插入数据,有效数据加一。

string& insert(size_t pos, char c)
{
    assert(pos<=_size);//断言输入的pos不能越界
    if (_size == _capacity)
    {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    }
    size_t end = _size;
    while (end > pos)//挪动数据
    {
        _str[end] = _str[end-1];
        --end;
    }
    _str[pos] = c;
    ++_size;
    return *this;
}

2.pos位置插入一个字符串

直接扩容到能直接容纳新字符串,同样需要一定数据不过这次是每一次移动字符串长度位,然后插入数据增加有效数据数。

string& insert(size_t pos, const char* str)
{
    assert(pos <= _size);//断言输入的pos不能越界
    size_t len = strlen(str);
    if (_size+len > _capacity)//计算剩余空间够不够用
    {
        reserve(_size + len);
    }
    _size += len;
    size_t end = _size;
    while (end > pos)//挪动数据
    {
        _str[end] = _str[end - len];
        --end;
    }
    strncpy(_str + pos, str, len);//用strncpy可以避免拷贝\0
    return *this;
}

八、清除数据

1.erase函数

erase用于从对应位置清除后面定量长度的数据,同样需要移动数据。

有两种情况:

pos后有效数据的个数小于要删除的数据长度:直接将pos放上\0

pos后有效数据的个数大于等于要删除的数据长度:按字符串长度向前挪动数据,我们也可以使用strcpy,这样就不用关注后续字符串的长度了

//清除特定位置后面的有限个字符(包括pos)
string& erase(size_t pos = 0, size_t len = npos)//pos和len都用缺省值
{
    assert(pos < _size);//断言输入的pos不能越界
    if (len == npos || pos + len >= _size)
    //不传len参数时len默认是npos,由于||中前面的表达式为真后面也就不计算的特点,可以单独放前面判断len==npos
    {
        _str[pos] = '\0';
        _size = pos;//长度直接就是pos
    }
    else
    {
        strcpy(_str + pos, _str + pos + len);//直接拷到\0停止,不需要关心循环的次数
        _size -= len;//减掉相应长度
    }
    return *this;
}

2.清除数据

这里的清除数据将string变为空字符串,但不会释放空间,在开始加个\0,再将size置零。

//清空但不释放空间
void clear()
{
    _str[0] = '\0';
    _size = 0;
}

九、其他成员函数

1.返回string对象转C语言字符串

//把一个string类型的变量返回它的字符串首元素地址
const char *c_str() const
{
    return _str;
}

2.查找字符和子串

find函数的两个重载函数,一个通过一个一个比较找到字符,另一个通过strstr找到子串,它们都返回对应的下标位,找不到则返回npos

// 返回某个字符在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
    assert(pos < _size);

    for (size_t i = 0; i < _size; i++)
    {
        if (_str[i] == c)
        return i;//直接返回下标
    }
    return npos;//没找到,返回npos
}

// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
    assert(pos < _size);
    char* pi = strstr(_str, s);
    if (pi == nullptr)
        return npos;//没找到,返回npos
    else
        return pi - _str;//找到了,通过指针减指针找下标
}

十、非成员函数

1.<<流插入重载

返回类型循环遍历即可,返回类型为ostream保证连续输出

std::ostream& operator<<(std::ostream& out, const my_string::string& s)//返回类型为cout的类型,可以实现连续赋值
{
    for (int i = 0; i < s.size(); i++)
    {
        std::cout << s[i];
    }
    return out;
}

2.>>流提取重载

std::istream& operator>>(std::istream& in, my_string::string& s)//返回类型为cin的类型,可以实现连续输入
{
    s.clear();//把原来的内容清除掉
    char buffer[128];//定义一个数组,存储临时数据
    size_t i = 0;
    char ch = in.get();//cin也是一个类变量,内部有一个函数get专门从输入缓冲区拿取一个字符
    while (ch != ' ' && ch != '\n')//读取到空字符就结束了
    {
        if (i == 127)//如果buffer满了,就尾插整个字符串
        {
            s += buffer;
            i = 0;//更新i
        }
        buffer[i++] = ch;//临时储存字符
        ch = in.get();//再次获取
    }
    if (i > 0)//将剩余没满的部分插入
    {
        buffer[i] = '\0';
        s += buffer;
    }
    return in;
}

十一、测试代码

我写了一段代码,尽量包括了所有的接口,定义在test.c里

void test()
{
    string s1 = "hello world";
    std::cout << s1 << std::endl;
    string s2;
    s2 += 'b';
    std::cout << s2 << std::endl;
    s2 += "onjour";
    std::cout << s2 << std::endl;
    string s3;
    std::cin >> s3;
    std::cout << s3 << std::endl;
    s1.erase(1);
    std::cout << s1 << std::endl;
    s1 = s2;
    std::cout << s1 << std::endl;
    printf("%d\n",s1.find('o'));
    printf("%d\n", s1.find('o', 2));
    s1.insert(1, "def");
    std::cout << s1 << std::endl;
    s1.insert(6, 'x');
    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
    std::cout << (s1 < s2) << std::endl;
    std::cout << (s1 <= s2) << std::endl;
    std::cout << (s1 > s2) << std::endl;
    std::cout << (s1 >= s2) << std::endl;
    std::cout << (s1 == s2) << std::endl;
    std::cout << (s1 != s2) << std::endl;
    s1.clear();
    std::cout << s1 << std::endl;
    string::iterator it1 = s2.begin();
    while (it1 != s2.end())
    {
        std::cout << *it1;
        it1++;
    }
    std::cout << std::endl;
    string s4;
    std::cin >> s4;
    std::cout << s4;
}

到这里string的主要接口就都实现了。

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

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

相关文章

SpringBootStarter技术:生产就绪与环境配置、实现自定义Starter

● Spring 官 方 Starter &#xff1a; 命 名 应 遵 循 spring-boot-starter-{name} 的 格 式 &#xff0c; 如 spring-boot-starter-web 作 为 SpringBoot Web模块的官方artifactId。 ● Spring 非 官 方 Starter &#xff1a; 命 名 应 遵 循 {name}-spring-bootstarter的格…

ModBus_RTU-上位机经RS485接口与PLC通信

目录&#xff1a; 一、预备知识 二、上位机经RS485接口与PLC通信 ---------------------------------------------------------------------------------------------------------------------- 一、预备知识 电力-ModBus_RTU通讯规约1 电力-ModBus_RTU通讯规约2 通信-R…

Java基于springboot+vue足球联赛管理系统

本足球联赛管理系统是针对目前足球联赛管理的实际需求&#xff0c;从实际工作出发&#xff0c;对过去的足球联赛管理系统存在的问题进行分析&#xff0c;完善用户的使用体会。采用计算机系统来管理信息&#xff0c;取代人工管理模式&#xff0c;查询便利&#xff0c;信息准确率…

字符串处理【后缀数组】 - 原理2 后缀数组

字符串处理【后缀数组】 - 原理2 后缀数组 在字符串处理中&#xff0c;后缀树和后缀数组&#xff08;Suffix Array&#xff09;都是非常有力的工具。 后缀数组是后缀树的一个非常精巧的替代品&#xff0c;比后缀树容易实现&#xff0c;可以实现后缀树的很多功能&#xff0c;时…

Jenkins

目录 一、什么是Jenkins 二、为什么需要使用持续集成工具 三、如何搭建jenkins服务 四、jenkins集成服务器上的JDK 五、jenkins集成git 5.1 jenkins所在的服务安装git 5.2 jenkins集成git 5.3 jenkins创建一个任务项 5.4 创建远程仓库 5.5 执行任务 六、jenkins集成maven…

[附源码]Python计算机毕业设计SSM基于的校园商城(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

人工智能写代码 !ChatGPT VScode 插件 真正打败AI人工智能的只能是AI人工智能自己。

前言&#xff1a; 真正打败你的只能是你自己。 真正打败程序员的只能是程序员自己。 真正打败AI人工智能的只能是AI人工智能自己。 人工智能写代码 &#xff01;上线3天&#xff0c;下载4万&#xff0c;ChatGPT中文版VSCode插件来了,程序员要失业了吗&#xff1f; 还没开始就遇…

[附源码]JAVA毕业设计医院挂号系统(系统+LW)

[附源码]JAVA毕业设计医院挂号系统&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

VoIP通话-基于SIP协议的Asterisk(零)-Demo

文章首发及后续更新&#xff1a;https://mwhls.top/4116.html&#xff0c;无图/无目录/格式错误/更多相关请至首发页查看。 新的更新内容请到mwhls.top查看。 欢迎提出任何疑问及批评&#xff0c;非常感谢&#xff01; VoIP通话-基于SIP协议的Asterisk这篇为 Demo&#xff0c;是…

LabVIEW如何实现高性能串口助手-附代码

LabVIEW基本上还是在Windows系统下进行开发。在Windows下&#xff0c;提供了非常多的系统接口&#xff0c;供应用开发时调用来实现各种功能&#xff0c;在LabVIEW里面也是可以通过对这些系统接口进行封装&#xff0c;形成LabVIEW里面的控件&#xff0c;供LabVIEW程序开发时调用…

mongo实时导入到clickhouse案例(包含复杂嵌套json的解析)

(一)案例介绍 本案例是把Mongo数据库的数据通过FlinkCDC实时导入到Kafka&#xff0c;消费Kafka数据把维表数据写入到MySQL。读取MySQL维表数据和消费Kafka的数据通过Flink SQL Join后导入到ClickHouse。 (二) maven依赖 <?xml version"1.0" encoding"UTF-…

数字人的生死疲劳

你看好数字人吗&#xff1f;这个问题在今天似乎颇难回答。如果从宏观趋势上看&#xff0c;数字人的利好要素似乎已经达到了一个空前的高度。比如有市场分析机构预测&#xff0c;到2026年中国AI数字人的市场规模将突破100亿人民币&#xff0c;整体市场呈现高速增长态势。又比如今…

git基础之三|初始化本地库、新建文件、提交代码、版本回退、穿梭等使用命令集合【2022最全版】

Git作为版本管理的软件&#xff0c;在我们的协同工作中非常重要。因此&#xff0c;对于Git的常见命令&#xff0c;如新建、编辑文件、提交版本、版本回退等操作必须要熟悉。 Git常用命令一、初始化本地库1、创建项目文件夹2、右击选择Git bash3、在bash中输入4、查看创建的结果…

mysql索引中最左前缀原则

最左前缀原则 最先匹配最左边的索引&#xff0c;匹配上就继续&#xff0c;如果匹配不上就检索不到 (a,b,c是索引) where后面的条件有没有给a对应的条件 不给定a等于几&#xff0c;是没法儿查询出结果的&#xff0c; 因为辅助聚簇索引是把索引按照组合索引的顺序存到一起的&…

IoTDB 可实现的基本操作 —— 数据写入、删除、导出、元数据管理、时区设置 | 小白教程文档(四)...

前言上篇教程介绍了 Apache IoTDB 处理时序数据时&#xff0c;能够实现的部分具体功能和具体的操作命令&#xff0c;包括数据导入、基本查询、和聚合查询。本篇将继续介绍 Apache IoTDB 可实现的其他功能和相关 SQL 语句命令&#xff0c;包括数据的写入、删除、导出、元数据操作…

python 解析库Beautiful Soup的安装

Beautiful Soup的安装一、Beautiful Soup的安装1.1 安装lxml库1.2 安装beautifulsoup41.3 验证beautifulsoup4能否运行一、Beautiful Soup的安装 Beautiful Soup是Python的一个HTML或XML的解析库&#xff0c;使用它可以很方便地从网页中提取数据。它的解析器是依赖于lxml库的&…

数组元素循环右移问题

目录 1008 数组元素循环右移问题 输入格式: 输出格式: 输入样例: 输出样例: 代码长度限制 时间限制 内存限制 思路: 1.右移函数 1.2函数代码: 2.main函数中 2.2main函数代码: 完整代码: 时间复杂度: 总结: 题目的链接: 1008 数组元素循环右移问题 一个数组A中存有…

MongoDB(一)【概念介绍安装】

MongoDB 概念以及安装 官方文档&#xff1a;https://www.mongodb.com/docs/manual/ 简介 官方介绍 MongoDB是一个文档数据库&#xff0c;旨在方便应用开发和扩展 百度百科 MongoDB是一个基于分布式文件存储的数据库。由C语言编写。旨在为WEB应用提供可扩展的高性能数据存储解…

【web前端期末大作业】html在线网上书店 基于html制作我的书屋(23页面)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

反序列化漏洞原理

序列化及其出现场景 ●远程和进程间通信&#xff08;RPC/IPC&#xff09; ●连线协议、Web服务、消息代理 ●缓存/持久性存储区 ●数据库、缓存服务器、文件系统 ●HTTP cookie、HTML参数、API身份验证令牌 序列化 (serialize)是将对象的状态信息转换为可以存储或传输的形…