C++的string你可能会用,但是你模拟实现过了吗?带你实现以下string的重要接口!

模拟实现string


​ 为了不与库里面的string相冲突,我们可以将自己的string写在自己的命名空间(namespace)里面。

以下是程序需要要的头文件。

#pragma once
#include<iostream>
#include<string.h>
#include<assert.h>
using namespace std;

#pragma once是防止头文件被再次引用,是一个C语言的知识点,开始实现。


注意以下写的函数,如果没有特殊表明,都是成员函数的意思,成员函数里面是默认有this指针的

string类的是写在一个文件,测试函数写在另一个文件(main()函数)

一.四大默认成员函数

​ 首先我们得想想,string类的成员变量都有谁,string是一个字符数组。

  1. 维护动态内存的指针(_str)
  2. 指向最后位置的下标(_size)
  3. 字符数组容量(_capacity)。
namespace kcc//kcc是博主的名字
{
public:
    //成员函数…………
    
private:
    char* _str;
    size_t _size;
    size_t _capacity;
    
}
构造函数
string(const char* str = "")//动态内存--要解决深拷贝的问题
{
	_size = strlen(str);
	_capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);//这里就很好的复用了c库里面的库函数
}

​ 要申请_capacity+1长的空间的原因是,我们要将\0也拷贝进来。

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

​ 析构函数主要解决对象的清理工作,1.要完成对空间的释放 2.将维护空间的指针置为空指针,避免野指针的存在

3._size_capacity也要变为0。


拷贝构造函数
传统写法–自己生火,自己做饭

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TckUWpDR-1663384266179)(D:\gitee仓库\博客使用的表情包\自己做饭.jpg)]

​ 还记得我们之前说的,编译器默认生成的拷贝构造函数,只能完成简单的值拷贝,而像这种动态内存的,需要深拷贝,这次可不能靠编译器来帮我们了。

​ 所以我们要实现的功能是,重新申请一块动态空间,再将其内容拷贝过来。

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

现代写法(别人烧火做饭,再给我)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpF4FgO6-1663384266181)(D:\gitee仓库\博客使用的表情包\点外卖.jpg)]

​ 为了实现别人的东西给我们,我们先实现一个交换函数。(也是我们的string的成员函数)

void swap(string& s)
{
    ::swap(_str, s._str);//加了两个点是因为swap是用库里面的交换函数
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}

​ 库里面的交换函数是这样的,是一个模板函数。如果我们用库里面的,将会多次调用构造函数和拷贝构造函数,这样子效率其实比较低,所以我们可以自己实现一个对string的交换函数。

string(const string& s)
    :_str(nullptr)
    {			
        string tmp(s._str);
		swap(tmp);//在类中是有this指针的
    }


赋值运算符重载operaor=
传统写法
string& operator= (const string& s)
{
    //考虑自己给自己赋值的问题
    if (this != &s)
    {
        delete[]_str;
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capacity = s._capacity;
    }
    return *this;
}

​ 如果不考虑自赋值,那么delete[]_str;,这句代码就会让后面的程序崩掉了。

现代写法
string& operator=(string s)
{    
    swap(s);
    return *this;
}

​ 虽然现代写法的拷贝构造觉得没有什么太大的优势,但是现代写法的赋值运算符重载,显得代码的妙处很高!很灵活。

但是现代写法没有考虑自赋值的问题,自赋值会使地址发生该改变。我的理解是:自赋值本来就是一个错误的写法,现代的编程素养不断提高,也不会在程序中写出自赋值的情况。当然了,如果你非要去实现自赋值,那还是用传统写法吧。或者把现代写法改一改。

其实面试时,面试官叫你实现一个string主要也是想看看,你会如何去实现这几个默认成员函数。


二、三种遍历方式

下标遍历法

​ 下标遍历法首先我们要实现一个成员函数size(),其可以返回成员变量_size。

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

温馨建议:如果一个成员函数不会改变对象内容,实现时加上const修饰,不然const对象是无法调用该成员函数的(权限放大)

关于权限的放大,缩小大家可以参考这篇文章:

(337条消息) 类与对象(下)_龟龟不断向前的博客-CSDN博客


​ 我们想像内置类型一样,欢快的使用,我们还得实现一个运算符[]重载

operator[]
 char& operator[]( size_t pos) //返回引用才可修改
 {
     return _str[pos];//访问对应下表的内容
 }

温馨提示:如果一个成员函数既可以修改对象的内容,又可以不修改对象的内容。即:可读可写,与仅可读,那么我们要实现两个成员函数

const char& operator[](size_t pos) const
{
    return _str[pos];
}

这样子,const与非const对象均可调用。

下标遍历的代码实现:

int main()
{
    kcc::string s = "hello world";
    for(int i = 0;i< s.size();++i)
    {
        cout<<s[i]<<" "
    }
    cout<<endl;
    return 0;
}

迭代器遍历法

​ string的迭代器其实就是一个char*指针。

iterator与const_iterator

​ 大家可能会说:不是说string迭代器是指针吗,那iterator是啥呀,我也不认识啊,其实iterator的原理很简单,就是使用了typedef

typedef char* iterator;
typedef const char* const_iterator
begin()
char* begin() 
{
    return _str;
}

返回首元素的地址,根据咱们的温馨提示,如果有可读可写和仅可读功能的,成员函数要实现两个。

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

迭代器遍历法的代码实现:

int main()
{
    kcc::string s1 = "hello world";
    iterator it1 = s1.begin();
    while(it1 != s1.end())
    {
        *it1+=1;
        cout<<*it1<<" ";//可读可写
        ++it1;
    }
    
    const kcc::string s2 = "hello world";
    iterator it2 = s2.begin();
    while(it2 != s2.end())
    {
        cout<<*it2<<" ";//仅可读
        ++it2;
    }
    
    return 0;
}

范围for遍历法

代码实现:

int main()
{
    string s = "hello world";
    for(auto& e: s)
    {
        e+=1;
        cout<<e<<" ";
    }
    cout<<endl;
    return 0;
}

其实范围for的原理也很简单,就是将代码替换成了迭代器的代码,不信你可以将上面的begin()的成员函数写成Begin(),范围for就遍历不起来的。


三、string的增删查改

​ 由于增要考虑到增容的问题,为了增强代码的复用,所以咱们先要实现一个reserve(),它可以改变_capacity的值。

reserve–改变容量(异地增容)
void reserve(size_t n)
{
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[]_str;     
        _capacity = n; 
        _str = tmp;//让_str来维护这块空间
    }
}

我们顺便也把resize给实现了把

resize()–改变_size的值
void resize(size_t n,char c = '\0')
{
    if (n < _size)
    {
        _str[n] = '\0';
        _size = n;
    }
    else
    {
        if (_size + n > _capacity)
        {
           
            reserve(_capacity + n);       
        }

        for (int i = _size; i < n; i++)       
        {
            _str[i] = c;
        }
        _str[n] = '\0';              
        _size = n;
    }
}

push_back()–尾插字符
void push_back(char c)
{
    //增容情况
    //这里没有考虑如果_capacity是0话还是会增容失败
    if (_size == _capacity)
    {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    }
    _str[_size] = c;
    _str[_size+1] = '\0';
    _size++;
}

size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;

这句代码的意义是很重要的,因为我们实现的string的空串的_capacity是0,如果_capacity再怎么2倍增容,还是会增容失败。


append–尾插字符串
void append(const char* str)
{
    //这个增容情况就不像push_back,因为增容2倍可能也不够
    //所以建议增容一个字符串的长度
    size_t len = strlen(str);
    if (_size == _capacity)
    {
        reserve(len + _capacity);
    }
    strcpy(_str + _size, str );//将str的内容拷贝进来
    _size += len;
}
operator+=–既可以尾插字符,又可以尾插字符串
string& operator+=(const char ch)
{
    push_back(ch);
    return *this;
}
string& operator+=(const char* str)
{
    append(str);
    return *this;
}
string& operator+=(const char* str)
{
    append(str);
    return *this;
}

​ 很多同学可能会说,库里面为什么还要设计push_back和append啊,明明一个operator+=就够了,但是我们从实现的角度理解,

operator是复用了push_back和append。


insert–中间插,头插字符或字符串
string& insert(size_t pos,const char c)//在pos位置,插入字符
{
    assert(pos <= _size);
    if (_size == _capacity)//增容问题
    {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    }
    size_t end = _size + 1;//防止头插时,end越界了,因为是无符号数
    //1.挪动数据
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }
    //2.插入字符
    _str[pos] = c;
    _size++;//不要忘记调正_size的值了
    return *this;
}

特别注意,上面的end是无符号数,end如果减到-1了,其实是一个很大的数,并不是一个小于0的数。

string& insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    int len = strlen(str);
    int newcapacity = (_size + len >= _capacity) ? _capacity + len : _capacity;
    reserve(newcapacity);
    //2.挪动数据-这次我们使用指针
    char* end = _str + _size;
    while (end >= _str + pos)//pos的位置上的数据也要挪
    {
        *(end + len) = *end;
        --end;
    }
    //3.填数据
    strncpy(_str + pos, str, len);
    _size += len;
    return *this;
}

strncpy(_str + pos, str, len);这句代码我们得讨论以下,可千万不敢用strcpy,因为strcpy会把\0也拷贝过来,与我们的需要不一致,所以我们要设置一个拷贝的最大长度,避免\0被拷贝过来了。


string的超大专属数npos
namespace kcc//kcc是博主的名字
{
public:
    //成员函数…………
    
private:
    char* _str;
    size_t _size;
    size_t _capacity;
    static const size_t npos;
}

const size_t npos = -1;
erase–从指定位置pos开始,删除n个字符
string& erase(size_t pos = 0,size_t n = npos)
{
    //将数据向前挪动即可
    assert(pos <= _size);
    if (n < _size)
    {
        char* start = _str + pos + n;
        while (start <= _str + _size)
        {
            *(start - n) = *start;
            ++start;
        }
        _size -= n;
    }   
    else
    {
        _str[0] = '\0';
        _size = 0;   
    }
    return *this;
}

删除数据并不是说是真的将数据删除,我们上面的代码,仅仅只是挪动后面的数据,将前面的数据覆盖了。

温馨提示:insert和erase都需要挪动数据,极端情况下的时间复杂度是O[N^2],所以尽量少用insert和erase


find–从指定位置,找指定字符和字符串的下标(从前向后找)

找到了返回下标,找不到返回npos

size_t find(const char ch, size_t pos = 0) const
{
    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)const
{
    assert(pos < _size);  
    char* ret = strstr(_str, str);//在_str中找str,返回第一个找到的下标
    if (ret)
    {
        return (ret - _str);
    }
    else
    {
        return npos;
    }
}

reverse–逆置字符串

想必大家在c语言时期已经写过很多遍了吧。这个逆置是使用迭代器逆置的

什么是迭代器逆置,c语言时期我只学过用指针逆置啊,或者下标逆置啊。

不要慌张,上面不是讲了string的迭代器本质不就是指针嘛)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QRlGYQfx-1663384266182)(D:\gitee仓库\博客使用的表情包\冷静.jpg)]

string& reverse(iterator left,iterator right)
{
    while (left < right)
    {
        char tmp = *left;
        *left = *right;
        *right = tmp;
        ++left;
        --right;
    }
    return *this;
}

rfind–从后向前找
size_t rfind(const char ch, size_t pos = npos)const
{
    char* end = nullptr;
    if (pos < _size)
    {
        end = _str + pos;
    }
    else
    {
        end = _str + _size;
    }
    while (end >= _str)
    {
        if (*end == ch)
        {
            return end - _str;
        }
        --end;
    }
    return npos;
}
size_t rfind(const char* ch, size_t pos = npos)const//ch是c串的首元素地址
{
    size_t len = strlen(ch);
    char* end = nullptr;
    if (pos > _size)
    {
        end = _str + _size;
    }
    else
    {
        end = _str + pos;
    }
    while (end >= _str)
    {
        int ret = strncmp(end, ch, len);
        if (ret == 0)//找到了就会返回0
        {
            return (end - _str);
        }
        --end;
    }
    return npos;
}

不过这样子实现rfind的复用性很差,大家可以思考一下,如果用find和reverse来实现rfind。

欢迎在评论区留下你的代码。


改其实咱们遍历的使用就讲了,三种遍历里面可以修改对象的数据。

四、字符串比较

为了可以更好地体现c++的字符串和c串的兼容性,我们可以写一个c_str()函数,库里面也写了

const char* c_str(const string& s)
{
    return s._str;
}

####> , < ,>= ,<= ,== ,!=(可以不设置为成员函数)

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 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 !(s1 == s2);
}

如果以上函数在未写声明的时候定义,编译器找对应的函数只会从下向上找,所以先后顺序要清楚。

五、operator<<,operator>>,getline

以前说过,operator<<,operator>>为了占位是要定义在类的外面的,getline也定义在类外面

关于运算符重载的占位可以看看博主的另一篇文章:

(337条消息) 类与对象(下)_龟龟不断向前的博客-CSDN博客

由于我们已经可以通过成员函数得到串的内容,所以不再需要将这些函数定义成类的友元了


operator<<
ostream& operator<<(ostream& out, string& s)//注意字符串输出可不是遇到\0结束,而是输出所有内容
{
    for (int i = 0; i < s.size(); ++i)
    {
        out << s[i];
    }
    return out;
}

看到什么差别了吗,有些同学总是会说,C语言的字符串跟C++的字符串有什么区别啊,不都是字符数组吗,但是你输出一个C的串,它遇到\0就会结束不再输出,而C++的string则是输出至_size结束。

图解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjPmhfb6-1663384266183)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220917104257023.png)]

大家思考一下,C语言的输出上述串,输出的结果是hello

而c++输出的是helloworld


operator>>

如果思考一下:如果对一个已初始化的对象进行输出,那么他原先的内容将被覆盖,所以咱们还需要一个成员函数clear()来清空其内容

clear()–成员函数
void clear()
{
    _size = 0;
}

不要不可思议这段代码,还是那个思想,删除并不一定就是删除

istream& operator>>(istream& in, string& s)//这个是遇到空格或者回车就会结束了
{
    s.clear();//我们得先将s的内容清空
    char ch;
    ch = getchar();
    while ((ch != ' ') && (ch != '\n'))//用in会自动省略\n,所以咱们用scanf或者getchar
    {        
        s += ch;//用+=就不用考虑增容了
        ch = getchar();
    }
    return in;
}
getline()
istream& getline(istream& in, string& s)
{
    s.clear();//我们得先将s的内容清空
    char ch;
    ch = getchar();
    while (ch != '\n')//用in会自动省略\n,所以咱们用scanf或者getchar
    {
        s += ch;//用+=就不用考虑增容了
        ch = getchar();
    }
    return in;
}

六、相对应的string的作业

  1. 917. 仅仅反转字母 - 力扣(LeetCode)
  2. 387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
  3. 字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)
  4. 125. 验证回文串 - 力扣(LeetCode)
  5. 415. 字符串相加 - 力扣(LeetCode)
  6. 541. 反转字符串 II - 力扣(LeetCode)
  7. 557. 反转字符串中的单词 III - 力扣(LeetCode)
  8. 43. 字符串相乘 - 力扣(LeetCode)
  9. 找出字符串中第一个只出现一次的字符_牛客题霸_牛客网 (nowcoder.com)
  10. 剑指 Offer 05. 替换空格 - 力扣(LeetCode)

七、聊一聊心得吧

​ 其实这个模拟实现string的程序写的时候真的很痛苦,写代码一时爽,测试起来真的要了我的老命,各种BUG层出不穷。

说来这些BUG也很哭笑不得。

  1. new int[4]我把[]写成(),导致一调用析构函数就报错
  2. const关键词不知道什么时候加,导致对象调用成员函数老报错(总结在上面文章的温馨提示)
  3. 输出字符串显示很多乱码,因为后面的\0没有处理
  4. …………

BUG之大,一锅炖不下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvk8PxP6-1663384266184)(D:\gitee仓库\博客使用的表情包\一锅炖不下.jpg)]

​ 但是编程不就是这样的吗,程序员的大部分时间都在调式代码,微软搞了这么多年的操作系统不也还是会蓝屏

还记得我的一个导师说过:编程带给你的快乐往往是让你经理了多重痛苦,给你一份敞开心扉的爽快,在我实操完了string

的模拟实现,测试完了一般程序,真的是很欣慰开心,当然了上面的代码还有很多的不足之处,还有很多可以改良的地方。

欢迎大家在评论区留言指出,多谢了。

string的模拟实现的的代码也已经上传到了gitee上面:
5.2模拟实现string/5.2模拟实现string · small_sheep/cplusplus0study - 码云 - 开源中国 (gitee.com)


如果这篇文章有帮到你,请点歌免费的赞再走吧,这是对我最大的鼓励!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMfY5qNE-1663384266184)(D:\gitee仓库\博客使用的表情包\给点赞吧.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SCEzktGt-1663384266185)(D:\gitee仓库\博客使用的表情包\要赞.jpg)]

  • 35
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 37
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值