C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++实现string

C++模拟实现string的方法详解

作者:bite-ccc

标准库类型string表示可变长的字符序列,使用string类型必须首先包含string的头文件。本文将利用C++模拟实现string,需要的可以参考一下

1.string 成员变量

首先需要一个动态开辟的指针指向这个字符串,然后还需要容量和存储的个数,并且我们不能和标准库的string进行冲突所以我们需要写在我们自己的类域中,并且我们库中还有一个静态的变量是npos,就是无符号的-1,代表整形的最大值:

namespace cyf
{
    class string
    {
    public:
        //成员函数
    private:
        char *_str;
        size_t size;
        size_t capaticy;
        const static size_t npos = -1;
    };
}

这里有一个特例:static成员变量一般在类中声明在类外定义,但是const static int型的变量可以直接在类中定义。

2.构造函数

strlen求出的是\0之前的字符个数,所以_size和_capacity标识的是实际存储的字符个数,在开辟空间时多开辟一个字符用来存储'\0'。

string(const char* s = "")  
        {
            _size = strlen(s);  
            _capacity = _size;
            _str = new char[_capacity + 1];   
 
            strcpy(_str, s);  //开辟好空间后将s的内容拷贝至_str
        }

3.拷贝构造、赋值重载和析构函数

1.拷贝构造

_str 维护的是一块空间,所以不能简单的将s._str的值赋值给_str (浅拷贝),而是单独开辟一块空间,让_str指向这一块空间,再将s._str空间中的值拷贝至新开辟的空间,新开辟的空间比_capacity多开一个字节用来存储'\0',作为字符串的结束标志。

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

2.赋值重载

首先开辟一块空间,将字符串的内容拷贝至这个空间,将_str原来指向的空间释放,_str再指向这个新开辟的空间,size和capacity还是原来的大小。

//s2=s1
        string& operator=(const string& 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;
        }

3.析构函数

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

4.访问成员变量

提供接口可以在类的外边查看字符串的内容,存储字符串的元素个数和容量

        const char* c_str()
        {
            return _str;
        }
 
        size_t size()
        {
            return _size;
        }
        size_t capacity()
        {
            return _capacity;
        }

配合之前的构造函数和这里的接口,我们进行验证:

运行结果:

5.遍历

遍历有三种模式:

1.下标+【】

_str是一个指针,那么我们可以通过数组的方式来访问,只需要重载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];
}

2.迭代器(iterator)

在string中,迭代器就是一个指针,只不过我们进行了封装,typedef一下就可以啦,同样我们也要实现两个版本的,const和非const的。

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

3.范围for

我们范围for的底层就是迭代器,所以我们不用实现,只要实现了迭代器,那么我们就可以直接使用范围for,范围for在执行的时候实际还是通过迭代器实现的,上例子:

运行结果:

6.空间的申请

1.reserve

一般是我们原空间容量满了,需要申请空间扩容,我们的扩容函数还是要先申请空间,然后在进行拷贝,接着我们delete原来的空间,把申请的空间的指针和 容量 赋值过去即可。 

void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];    //多开一个字节留给'\0';
                strcpy(tmp, _str);
                delete[] _str;
                _str = tmp;
 
 
                _capacity = n;
            }
 
 
        }

2.resize

1. 如果我们是传入一个正整数大于_size的值,那么我们可以使用传入的字符(或者缺省值)把我们申请的空间进行初始化,也就是从_size到n-1置为我们传入的字符,n置为' \0 ',最后把_size置为n。

2.如果传入一个小于_size正整数,那么我们把0~_size-1进行初始化为传入的字符(或者缺省值),把n位置置为' \0 ',接着我们会把_size置为n,而_capaticy不变。

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

7.增删查改

1.push_back   尾插一个字符

上来就检查容量,_size==_capacity时就说明没有容量了,得分类讨论:1.原来的字符串有元素2.原来的是空字符串,如果字符串为空,就给4个字节大小的容量 。如果原来的字符串不为空但是需要扩容就调用reserve函数进行扩容,容量扩为2倍,2倍比较合适,避免给的小了频繁的扩容,但是也不能给的太大了,太大了会造成空间的浪费。在_size的位置插入字符ch ,_size++,插入的字符ch 将原来的'\0'给覆盖了,最后再补上'\0',作为字符串的结束标志。

void push_back(char ch)
        {
            if (_size == _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            _str[_size] = ch;   
            ++(_size);
            _str[_size] = '\0'; //记得处理 \0
        }

2.append 尾部插入一个字符串

插入字符串的大小不确定,就需要确定是否需要扩容。当插入的字符串的长度加上当前字符串的有效元素个数大于容量_capacity时,就需要扩容,扩后的容量大小为_size+len ,这里给reverse函数传的是有效元素的个数,在reverse函数内部为我们多开了一个字符的大小用来存储'\0'。再进行拷贝工作,这里记得插入元素后_size要进行变换。

void append(const char* str)
        {
            size_t len = strlen(str);
            if (len + _size > _capacity)
            {
                reserve(_size + len);   
 
            }
            strcpy(_str + _size, str);
            _size += len;
        }

3.insert  在指定位置插入一个字符

上来首先检查要插入的位置是否合理,_size 代表的位置是有效元素的下一个位置即'\0'的位置,pos的范围【0,_size】string字符串的头到尾部之间 ,插入元素要检查容量,记得考虑原string是否为空串的情况。插入数据需要挪动数据,从前往后挪动数据,将end位置确定到'\0'的下一个位置,这样方便头插。最终将pos位置腾出来,插入字符ch 插入数据后_size++。

string& insert(size_t pos, char ch)
        {
            assert(pos <= _size);  //等于size 时候相当于尾部插入
            if (_size == _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            size_t end = _size + 1;  //这里是把\0往  \0的下一个位置挪动 方便头插   
            while (end > pos)
            {
                _str[end] = _str[end - 1];
                --end;
            }
            _str[pos] = ch;
            _size++;
            return *this;
        }

4.insert 在指定位置插入一个字符串

string& insert(size_t pos, const char* str),我们先进行断言pos不能超过_size,接着我们开辟空间,这次就不考虑空串的问题了,因为我们要指定开辟的字节数,和上面一样的我们也要进行挪动数据,我们只不过是由每次挪动一个步,变为了挪动 len 步了,最后使用strncpy插入字符串,把_szie +=len 即可。

画图理解:

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

实现了上述的接口当然最好用的还是下面的接口,对push_back和append进行封装实现string+=

一个字符 和 string+=一个字符串

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

5.删除接口:erase

指定字符串从开始位置到指定位置删除元素。得保证删除的位置在string的内部,当只给定了删除的起始位置没有给结束的位置那么就触法我们的缺省值,即从pos位置开始直到将字符串删完,或者说给定的结束位置大于了字符串的本身长度,那就从pos位置开始直到删除完字符串,实现的方法很简单,直接在pos位置添字符串的结束标志'\0'。  如果给定的两个值都在字符串的内部直接进行从len位置往前拷贝覆盖掉要删除的元素。

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

6.find字符 从某个位置开始查找字符,如果没有给定开始位置,就用缺省值,默认从开头寻找,找到了就返回元素的下标,没有找到就返回npos。

size_t find(char ch, size_t pos = 0)  ///默认从pos位置开始寻找,有缺省值0,从pos 位置开始往后寻找
        {                                     //对比找到了就返回下标,找不到返回npos
            assert(pos < _size);
            while (pos < _size)
            {
                if (_str[pos] == ch)  //遍历寻找
                {
                    return pos;
                }
                pos++;
            }
            return npos;
        }

7.find字符串  ,从某个位置开始往后寻找字符串,找到了就返回下标,找不到就返回npos

这里套用c语言的库函数strstr进行实现

size_t find(const char* str, size_t pos = 0)
        {
            assert(pos < _size);
            const char* ptr = strstr(_str + pos, str);   
            if (ptr == nullptr)  //strstr找不到返回空指针
            {
                return npos;    //转换至cpp找不到就返回npos
            }
            else
            {
                return ptr - _str;  //返回的是下标  指针-指针  ==下标
            }
 
        }

8.clear  清空字符串的内容

直接在第一个位置加入结束标志就将字符串清空了,将清空后_size就为0,

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

8.重载cin 和 cout

1.cout 

依次的输出string对象的内容即可

    ostream& operator<<(ostream& out,const string& s)
    {
        for (size_t i = 0; i < s.size(); i++)
        {
            out << s[i];
        }
 
        return out;
    }

2.cin

这里注意因为要改变字符串的内容,首先调用clear函数清空原来的内容,因为要改变字符串的内容所以不用const 直接引用改变的就是字符串的本身。因为我们的 in 会默认 '空格' 和 ' \n '是分割符,不进行读取,这样我们就没办法停止。需要使用下 in 的get函数,让我们的来读取 ‘  ’  和         ' \n ',我们看下代码:

void clear()
{
    _str[0] = '\0';
    _size = 0;
}
 
istream& operator>>(istream& in, string& s)
{
    s.clear();//要先进行清理,否则就会出现剩余的数据也被我们写入了。
    char ch;
    ch = in.get();
 
    char buff[32];
    size_t i = 0;
 
    while (ch != ' ' && ch != '\n')
    {
        buff[i++] = ch;
        if (i == 31)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }
 
    buff[i] = '\0';
    s += buff;
 
    return in;
}

cin和cout的重载不一定是类的友元函数,在类中提供接口,我们也可以直接访问类的成员变量!

到此这篇关于C++模拟实现string的方法详解的文章就介绍到这了,更多相关C++实现string内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文