【C++】string类的模拟实现

目录

一、构造函数

二、拷贝构造

三、=操作符重载

四、迭代器

五、c_str 、capacity、size

六、[ ]操作符重载

七、reserve

八、push_back

九、append

十、+=操作符重载

十一、insert

十二、erase

十三、clear

十四、<<操作符重载 >>操作符重载

十五、find

十六、substr

十七、> 、>=、<、<=、==、!=

十八、resize

总结


一、构造函数

【C++】string类的模拟实现_第1张图片

首先要明确一点string最基本要有三个成员变量:str, size , capacity

string类具有很多构造函数,string类的很多构造函数都是没有用的

我们具体实现一个缺省的构造函数

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

缺省值是“” 它是一个常量字符串,默认后面带一个'\0'

然后依次将_size,_capacity赋值

_size和_capacity就是str的长度,如果我们没有传字符串就用缺省值。

传入字符串就用,传入的 与库中的string类用法保持一致

有人可能会是这样写构造函数

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

这是一个极其隐蔽的错误,首先是初始化列表的初始化顺序,是由声明的顺序来决定的

就算我们修改了声明顺序,也是相当的不推荐这样写,因为在后期维护时,有人可能觉得你的声明顺序不顺眼,而修改声明顺序,从而导致了程序崩溃,因此我们不要使用初始化列表,不要将声明顺序与初始化顺序绑定。

二、拷贝构造

拷贝构造会涉及深浅拷贝的问题

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,浅拷贝会带来数据安全方面的隐患。在进行赋值之前,为指针类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝 [1]  。这种拷贝称为深拷贝。深拷贝有两种方式实现:层层clone的方法和利用串行化来做深拷贝。层层clone的方法:在浅拷贝的基础上实现,给引用类型的属性添加克隆方法,并且在拷贝的时候也实现引用类型的拷贝。此种方法由于要在多个地方实现拷贝方法,可能会造成混论。利用串行化来做深拷贝:为避免复杂对象中使用clone方法可能带来的换乱,可以使用串化来实现深拷贝。先将对象写到流里,然后再从流里读出来

默认拷贝构造,会对内置类型进行浅拷贝或者值拷贝

因此我们要使用深拷贝,就要我们自己实现

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

我们使用strcpy会将'\0'拷贝过去

这里使用的是传统写法

下面使用的是现代写法

现代写法是调用构造函数,创建一个局部对象,然后使用swap函数进行深拷贝的交换

局部对象出了作用域就销毁了,不用担心内存泄漏问题。

        void swap(string& s)
        {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }

        //拷贝构造现代写法
        string(const string& s)
            :_str(nullptr)
            , _size(0)
            , _capacity(0)
        {
            string tmp(s.c_str());
            swap(tmp);
        }

我们要写一个swap函数,该函数我们可以调用全局的swap函数

【C++】string类的模拟实现_第2张图片

我们使用swap函数将类的每个成员的地址与tmp对象的地址交换,就算是拷贝构造了一个新对象。

但是我们要先对要进行构造的对象,简易初始化,因为delete[]无法处理随机数据。

三、=操作符重载

=操作符重载也涉及深拷贝问题,同时也有传统写法和现代写法.

同时我们要避免自己给自己赋值。或者是看上去是自己给自己赋值。

首先自己给自己赋值毫无意义,同时由于自定义类型通常较大,自己给自己赋值 是没有意义的消耗。

其次我们以深拷贝的思路来写,为了避免内存泄漏,会将原空间先释放,释放之后_str成为nullptr

会将原本的值改变,同时这个类也是无法使用的,会出现野指针问题。

我们还是先写传统写法

       string& operator=(const string& s)
        {
            if (this != &s)
            {
                char* tmp = new char[s._capacity + 1];
                strcpy(tmp, s._str);

                delete[] _str;
                _size = s._size;
                _capacity = s._capacity;
                _str = tmp;
            }
            return *this;
        }

先定义一个tmp保存原数据,然后处理完成之后,再让_str指向tmp

这是为了避免new失败,从而导致的数据丢失。

接下来我们看一下现代写法

        string& operator=(const string& s)
        {
            if (this != &s)
            {
                string tmp(s);
                swap(tmp);
            }
            return *this;
        }

=操作符重载的现代写法与拷贝构造现代写法类似,都是调用了构造函数

构造出一个对象然后交换即可,它最大限度的复用了代码。

四、迭代器

迭代器是C++ STL容器的统一遍历方式

iterator是像指针一样的类型,有可能是指针也有可能不是指针,但是它的用法像指针一样。

string不喜欢用iterator,因为【】更好用

vector同理

list/map/set……只能用迭代器。

我们这里实现迭代器是为了使用范围for,范围for的底层就是迭代器,编译器会进行傻瓜式的替换,将

        for (auto& e : str)
        {
            cout << e << " ";
        }

直接替换成

        string::itreator it = str.begin();

        while (it != str.end())
        {
            cout << *it << endl;
            it++;
        }

类似这样的形式

string的空间是连续的,所以它的迭代器是原生指针

我们目前只能实现iterator和const_iterator这两种迭代器,反向迭代器暂时无法实现。

        typedef char* itreator;
        typedef const char* const_iterator;

需要先将iterator typedef一下,原生指针就相对简单了,将头指针和尾指针返回就可以了

五、c_str 、capacity、size

【C++】string类的模拟实现_第3张图片

这三个函数比较简单,同时方法类似,就是将三个成员变量返回

注意,要返回const变量,防止在外部修改

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

        const size_t size()const
        {
            return _size;
        }

        const size_t capacity()const
        {
            return _capacity;
        }

六、[ ]操作符重载

我们要先想一想,[ ] 的功能,它的[ ] 类似数组的,可以直接进行修改,也就是要接收要访问的位置,并且为了支持修改我们要返回引用

无论是string类还是vector等其它容器的模拟实现,但凡涉及到位置的函数都要对该位置进行合法性检验,防止越界,[ ] 是用的assert 而at(与[ ] 功能类似)使用的是抛异常。

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

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

我们实现了两个[ ] 操作符重载,为了能够使const对象也能够调用[ ] 

库中也实现了两种

【C++】string类的模拟实现_第4张图片

七、reserve

reserve就是扩容函数,扩大capacity,size不变

扩容在后面的很多函数都有调用

所以先实现出来

扩容也是深拷贝,既然是扩容,因此只能够扩大空间,而不能缩小空间,如果传入的参数比原空间小就不会进行任何处理

注意要释放原空间

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

八、push_back

push_back就是尾插,我们的成员变量_size指向的是有效数据的下一个位置

我们先扩容,然后在_size位置插入新数据,_size++

插入完之后,很容易忘记一件事,将_size指向的位置再次置为'\0',因为字符串是以'\0'为结尾。

        void push_back(char ch)
        {
            if (_size == _capacity)
            {
                reserve(_capacity == 0 ? 0 : _capacity * 2);
            }

            _str[_size] = ch;
            _size++;
            //不要忘记\0
            _str[_size] = '\0';
        }

九、append

append与push_back类似,不过append有更多的尾插方式可以选择

【C++】string类的模拟实现_第5张图片

我们只重点实现几个函数

        void append(const char* str)
        {
            size_t len = strlen(str);

            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }

            strcpy(_str + _size, str);
            _size += len;

        }

尾插一个字符串,首先还是计算要插入的字符串的长度

判断是否需要进行扩容,之后进行插入数据,我们可以借助strcpy来插入


尾插一个string对象,这个也比较简单

我们复用前面的append就可以

        void append(const string& s)
        {
            append(s.c_str());
        }

尾插n个字符

这个也比较简单

        void append(size_t n, char ch)
        {
            if (n + _size > _capacity)
            {
                reserve(n + _size);
            }
            for (size_t i = 0; i < n; i++)
            {
                push_back(ch);
            }
        }

十、+=操作符重载

+=操作符重载也分为两种,一种是尾插一个字符,另一个是尾插字符串

我们直接调用push_back和append即可

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

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

十一、insert

insert也是老套路,先检查位置合法性

其次是扩容

接下来就是挪动数据

        string& 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;

            return *this;
        }

挪动数据有一个注意点size_t是无符号整型,它永远大于等于0,所以挪动数据要小心,控制边界。最后插入数据。

十二、erase

erase就是删除指定位置的字符或者字符串

【C++】string类的模拟实现_第6张图片

我们会发现len是有缺省值npos的

【C++】string类的模拟实现_第7张图片 

npos是一个无符号的整型,赋值为-1,也就是说npos是一个42亿左右的字节

STL认为没有长度为42亿左右的字符串

如果不传len也就是说从pos位置删除数据一直到字符串结尾。

如果len+pos大于_size也会删到结尾。

删除也比较简单还是用strcpy将pos位置后面的数据往前覆盖。

        void erase(size_t pos, size_t len = npos)
        {
            assert(pos < _size);
            //判断原字符串是否大于len+pos
            if (len == npos || pos + len > _size)
            {
                _str[pos] = '\0';
                _size = pos;
            }
            else
            {
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
            }
        }

 

十三、clear

clear函数十分的简单,它就是清空字符串,它并不会涉及到缩容。

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

十四、<<操作符重载 >>操作符重载

流提取和流插入重点说流插入操作符重载

istream& operator>>(istream& in, string& s)
    {
        char ch;
        ch = in.get();

        const size_t N = 32;
        char buff[N];
        size_t i = 0;

        while (ch != ' ' && ch != '\n')
        {
            buff[i++] = ch;
            if (i == N - 1)
            {
                buff[i] = '\0';
                s += buff;
                i = 0;
            }

            ch = in.get();
        }

        buff[i] = '\0';
        s += buff;

        return in;
    }

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

流提取操作符重载并没有声明为友元,因为我们有[ ] 可以直接访问字符串

流插入要注意的问题有很多,为了仿照库中功能

首先要清空空字符串,其次是cin输入到库中的string时,它并不会读取空格和回车

还有最后一点是,我们每次尾插数据string会频繁的检查是否需要扩容,开辟空间

消耗较大,因此我们可以使用一个buff数组先存储输入的数据,然后一组一组的插入到字符串中

十五、find

find重点实现两个重载

一个是在字符串中查找字符,另一个是在字符串中查找子串

两者都是找到返回下标,找不到返回npos

在查找子串时可以使用strstr来找,找到返回地址,找不到返回nullptr

可是我们是要返回下标,可以使用找到的元素的地址减去首元素的地址,也就减去是_size

        size_t find(char ch, size_t pos = 0)const
        {
            assert(pos < _size);
            for (size_t i = pos; i < _size; i++)
            {
                if (ch == _str[i])
                {
                    return i;
                }
            }
            return npos;
        }

        size_t find(const char* sub, size_t pos = 0)const
        {
            assert(pos < _size);
            assert(sub);

            char* ptr = strstr(_str + pos, sub);
            if (ptr == nullptr)
            {
                return npos;
            }
            else
            {
                return ptr - _str;
            }
        }

十六、substr

这个函数有点多余但是也实现一下。

首先还是检查pos位置,计算出字符串真正的长度,然后定义一个临时对象将数据尾插到对象中,然后返回即可。

        string substr(size_t pos, size_t len = npos)const
        {
            assert(pos < _size);

            size_t reallyLen = len;
            if (len == npos || pos + len > _size)
            {
                reallyLen = _size - pos;
            }

            string sub;
            for (size_t i = 0; i < reallyLen; i++)
            {
                sub += _str[pos + i];
            }

            return sub;
        }

十七、> 、>=、<、<=、==、!=

这些操作符我们只需要实现==和>剩下的直接服用即可

>和=可以借助strcmp来解决

 【C++】string类的模拟实现_第8张图片

通过它的返回值不同就可以判断出是否大于

        bool operator==(const string& s)const
        {
            return strcmp(_str, s._str) == 0;
        }

        bool operator>(const string& s)const
        {
            return strcmp(_str, s._str) > 0;
        }

        bool operator>=(const string& s)const
        {
            return (*this > s) || (*this == s);
        }

        bool operator<(const string& s)const
        {
            return strcmp(_str, s._str) < 0;
        }

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

 

十八、resize

resize与reserve很容易混淆,当我们实现它的底层之后就自然而然地理解它们两者的不同了

resize也会开空间,它不但会将_capacity改变,同时也会改变_size,因为resize会进行初始化

默认是用'\0'来进行初始化。

同时resize支持缩容

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

扩容后之后要用传入的参数进行初始化,直接尾插就可以了。

而缩绒的操作就简单多了,直接在容量的下标位置加上'\0'就可以,别忘了将size变成n


总结


例如:以上就是今天要讲的内容,本文仅仅简单介绍了string的模拟实现。string中其它的函数实现可参考STL的源码。

你可能感兴趣的:(C++,c++,开发语言)