C++关于string类的模拟实现

一、string类的模拟实现

1.成员变量

string类的成员变量分别是存储字符串的一段空间_str,表示字符串的有效字符个数_size和表示存储有效字符空间的_capacity。

private:
    char *_str;
    size_t _size;// 有效字符的个数
    size_t _capacity;// 存储有效字符的空间

2.构造函数

(1)无参构造函数

string类的无参构造函数非常简单,_size和_capacity都设置为0,但是_str不能设置为nullptr,因为根据标准库里的string设计,无参构造函数里的_str设置为空串。

string()
    :_size(0)
    ,_capacity(0)
{
    // 按照标准库里的string设计无参构造函数
    // _str存放一个空串,而不是直接设置为nullptr
    _str = new char[1];
    _str[0] = '\0';
}

(2)有参构造函数

首先,string类的有参构造函数其实可以设计为全缺省函数,缺省值设置为空串,当没有传入参数时使用缺省值,将_str设置为空串,这样就可以不需要定义无参构造函数了。其次,如果传入了参数,就将_size和_capacity设置为形参的长度,然后开一段大小和形参相同的空间给_str,最后将值拷贝过去即可。

string(const char *str = "")
    :_size(strlen(str))
    ,_capacity(_size)
{
    // strcpy函数会将\0也拷贝过去
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

3.c_str函数

string类的c_str函数是为了方便string字符串配合C语言的字符串函数接口使用而设计的,它可以返回string对象的char * 类型的指针,这个函数实现起来非常简单,直接返回我们成员变量_str即可,它就是一个char * 类型的指针。需要注意的是,c_str函数的返回类型是 const char*,要加上const的原因调用该函数只能获取指针,并不能对指针进行修改。

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

4.operator[]

string类中对运算符[]的重载是很重要的,提供这个运算符重载可以方便我们对string对象进行下标访问,也可以修改该下标对应的值。函数的返回类型是char&,一般引用返回是为了减少拷贝,但这里我们只需要返回一个字符,拷贝成本并不大。这里使用引用返回的目的是允许修改,因为引用就是返回值的一个别名,我们就可以对返回值进行修改。

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

// 再提供一个const版本,让const对象也能调用,就不会出现权限放大的问题
const char &operator[](size_t pos) const
{
    return _str[pos];
}

5.深浅拷贝问题

(1)浅拷贝

在C++的类设计中,如果我们没有写拷贝构造函数,那么会使用默认的拷贝构造函数,默认的拷贝构造函数是浅拷贝,也就是简单地将一个对象的值拷贝给另一个对象。如果对于内置类型的话浅拷贝没什么太大的影响,但如果是自定义类型,就比如我们的string类,将一个string对象的值拷贝给另一个string对象本质上是将指针内容进行拷贝,这样就会导致两个对象指向同一块空间。

在这里插入图片描述

这样浅拷贝的话会引发一些问题,比如析构函数的时候会被析构两次,或者一个对象改变自己字符串的值会影响另一个对象字符串的值。

(2)深拷贝

深拷贝和浅拷贝不同的是深拷贝是另外开一块同样的空间,然后将字符串的内容拷贝下来,这样就让两个对象指向不同的空间,但这两个不同空间的字符串值是相同的,这样就解决了自定义类型浅拷贝带来的问题。

在这里插入图片描述

要完成深拷贝的话就需要我们自己写拷贝构造函数了,不能使用默认的拷贝构造函数:

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

赋值运算符重载和拷贝构造一样,如果我们没有写赋值运算符重载,默认使用的是浅拷贝,所以我们也要自己写赋值运算符重载。
赋值运算符重载的写法和拷贝构造函数的写法不同,不能直接复制过来,因为赋值运算符重载面对的是两个都已经存在了的开好空间了的对象,有可能需要被赋值的对象的空间比较小,会存在越界访问的问题;也有可能需要被赋值的对象的空间比较大,虽然不会出现越界访问但会造成空间的浪费。所以简单粗暴的就是先释放原有的空间,再进行复制。

string& operator=(const string& s)
{
    // 防止出现自己给自己赋值导致的错误
    if (this != &s)
    {
        delete[] _str;
        _str = new char[s._capacity + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capacity = s._capacity;
    }

    return *this;
}

6.size函数和capacity函数

这两个函数很简单,分别返回_size的值和_capacity的值即可。

size_t size() const
{
    return _size;
}

size_t capacity() const
{
    return _capacity;
}

7.reserve函数

reserve函数是扩容函数,当容量不足时,会扩大到指定的容量。这个函数实现起来比较简单,当指定容量大于当前容量时说明要扩容,我们定义一个新的空间,把这块新空间的大小设置为指定的新容量,然后将原来字符串的值拷贝到新开的空间上,再将原来的空间释放,让_str指针指向新的空间,最后更新_capacity的值即可。

void reserve(size_t n)
{
    // 如果容量不够,就要扩容
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];// 保留一个位置给\0
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
    }
}

8.resize函数

resize函数也是扩容函数,但是它改变的是_size的值,这个函数的实现分三种情况讨论(假设新的size值为newsize):

  1. 当newsize > _capacity时:说明容量不够了,首先要进行扩容,可以服用reserve函数进行扩容操作,并将字符串填充满。
  2. 当_size < newsize <= capacity时:说明容量是够的,所以只需要增大_size的值,并且将字符串填满即可。
  3. 当newsize <= _size时:只需要缩小_size的值即可。
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[_size] = '\0';
}

9.string的插入函数

(1)push_back函数

push_back函数是在字符串末尾插入一个字符,首先需要考虑容量是否满了,如果容量满了就需要先扩容。扩容的时候还需要特别关注一下如果字符串是空串,那么_capacity的值是0,需要特殊处理一下。扩容完以后就是简单的尾插操作即可。

void push_back(char ch)
{
    // 说明容量满了,需要先扩容
    if (_size == _capacity)
    {
        // 需要考虑到_capacity是不是等于0这个情况
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    _str[_size] = ch;
    _size++;
    _str[_size] = '\0';
}

(2)append函数

我们只实现append插入一个常量字符串的函数,首先是要计算新插入的字符串加上原来的字符串一共有多长,然后要判断这个长度是否大于_capacity,如果是的话就需要扩容。最后直接用strcpy函数拼接起来即可。

void append(const char* str)
{
    // 先计算插入字符串以后的长度
    int len = _size + strlen(str);
    // 如果容量不大需要扩容
    if (len > _capacity)
    {
        reserve(len);
    }

    strcpy(_str + _size, str);
    _size = len;
    _str[_size] = '\0';
}

(3)operator+=函数

这个函数只需要复用push_back函数和append函数即可。

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

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

(4)insert函数

insert函数我们实现两个版本,一个是插入一个字符,一个是插入一个常量字符串。这个函数实现的逻辑也比较简单,就是将pos位置到_size位置的字符往后挪动,然后在空位处插入新的字符或者字符串。

string& insert(size_t pos, char ch)
{
    // 断言防止pos出现非法范围,当pos=_size时就是push_back
    // 所以push_back可以复用这个insert
    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;
}

string& insert(size_t pos, const char* str)
{
    assert(pos <= _size);

    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;
}

10.字符串比较函数

字符串比较函数需要实现各种比较符号的运算符重载函数,字符串比较的原理是根据字符的ASCII码值的大小进行比较,这可以利用C语言的strcmp函数进行比较。其实我们只需要实现其中个别几个函数,剩下的直接复用即可。

    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 strcmp(s1.c_str(), s2.c_str()) == 0;
    }

    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);
    }

11.迭代器

string类的迭代器非常简单,其实就是 char* 类型的指针,与迭代器配合使用的begin函数和end函数实现起来也非常简单,begin函数只要返回字符串第一个字符的地址即可,end函数只要返回字符串最后一个字符的下一个位置的地址。同时,我们也要实现一个const的迭代器和const修饰的begin函数和end函数,让const对象也可以调用。

typedef char* iterator;
typedef const char* const_iterator;

iterator begin()
{
    return _str;
}

iterator end()
{
    return _str + _size;
}

const_iterator begin() const
{
    return _str;
}

const_ iterator end() const
{
    return _str + _size;
}

string的遍历方式有三种,分别是通过下标遍历、通过迭代器遍历和通过范围for来遍历,通过下标遍历借助operator[]函数即可,通过迭代器遍历只要实现迭代器即可,范围for遍历其实底层也是通过迭代器来遍历的,例如下面的例子:ch是迭代器解引用的一份拷贝,假设迭代器变量为it,等价于 ch = *it;

void test_string1()
{
    JJP::string s("hello");

    for (auto ch : s)
    {
        cout << ch;
    }
    cout << endl;
}

所以我们就能意识到一个问题,如果我们对ch的值进行改变的话,原来的字符串的值并不会发生改变,因为改变的是ch这个变量的值,并没有改变*it的值,因此,如果想要改变原来字符串的值,需要带上引用。

void test_string1()
{
    JJP::string s("hello");

    // 只改变ch的值,不能改变原字符串的值
    for (auto ch : s)
    {
        ch -= 1;
        cout << ch;
    }
    cout << endl;
    cout << s.c_str() << endl;

    // 加上引用就可以改变原来字符串的值
    for (auto& ch : s)
    {
        ch -= 1;
        cout << ch;
    }
    cout << endl;
    cout << s.c_str() << endl;
}

12.erase函数

string的删除函数与插入函数相反,挪动数据覆盖即可。

string& erase(size_t pos, size_t len = npos)
{
    assert(pos <= _size);

    if (len == npos || pos + len >= _size)
    {
        _str[pos] = '\0';
        _size = pos;
    }
    else
    {
        size_t begin = pos + len;
        while (begin <= _size)
        {
            _str[begin - len] = _str[begin];
            begin++;
        }
        _size -= len;
    }
}

13.find函数

find函数实现起来也非常简单,只需要遍历查找即可。我们实现一个查找字符函数和一个查找字符串函数,查找字符函数挨着遍历去查找即可,查找字符串函数可以使用C语言的库函数strstr去查找子串。

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

    return npos;
}

size_t find(const char* str, size_t pos = 0)
{
    const char* p = strstr(_str + pos, str);
    if (p == nullptr)
    {
        return npos;
    }
    else
    {
        return p-_str;
    }
}

14.string类的流插入和流提取函数

我们还需要实现以下流插入和流提取函数,这样方便我们输入字符串和输出字符串。

    ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

    istream& operator>>(istream& in, string& s)
	{
	    // 先将字符串清空
        s.resize(0, '\0');
        // 这种方法可能存在多次扩容的情况,效率较低
		//char ch;
		in >> ch;
		//ch = in.get();
		//while (ch != ' ' && ch != '\n')
		//{
		//	s += ch;
		//	//in >> ch;
		//	ch = in.get();
		//}

		//return in;

		char ch;
		ch = in.get();
		char buff[128] = {'\0'};
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				s += buff;
				memset(buff, '\0', 128);
				i = 0;
			}

			ch = in.get();
		}

		s += buff;
		return in;
	}

15.swap函数

string类的swap函数是在底层直接交换两个对象的指针,所以实现起来非常简单。

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

16.拷贝构造函数和赋值函数的现代写法

我们上面写的拷贝构造函数方法太麻烦了,有一种更加方便的写法,就是定义一个局部对象tmp,让tmp去调用常量字符串构造函数从而完成深拷贝,最后将tmp与this交换即可。需要注意的是一开始的时候this的_str、_size和_capacity需要初始化一下,因为如果没有初始化最后交换给tmp的就是随机值,tmp在最后析构的时候可能会报错。

// 现代写法,让tmp去完成深拷贝,复用常量字符串构造函数
string(const string &s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{
    string tmp(s._str);
    swap(tmp);// 这个调用的是string类的swap函数
}

除了拷贝构造函数有现代写法,赋值运算符重载函数也有现代写法。第一种现代写法和拷贝构造函数的写法相似,也是借助局部对象tmp完成深拷贝,最后交换即可。

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

    return *this;
}

第二种写法更加的简洁粗暴,我们可以直接将形参的对象与this交换即可。由于我们参数传递是传值传参,不是引用传参,而是实参的一份临时拷贝,所以交换以后并不会影响实参的值。

// 现代写法更简洁的版本
string& operator=(string s)
{
    swap(s);
    return *this;
}

二、代码

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

// 使用命名空间为了不让库的string和我们自己定义的string冲突
namespace JJP
{
    class string
    {
    public:
        typedef char* iterator;
        typedef const char* const_iterator;

        iterator begin()
        {
            return _str;
        }

        iterator end()
        {
            return _str + _size;
        }

        const_iterator begin() const
        {
            return _str;
        }

        const_iterator end() const
        {
            return _str + _size;
        }

        // 可以不给无参构造函数,直接将有参构造函数设计成全缺省
        // 可以达到一样的效果
        // string()
        //     :_size(0)
        //     ,_capacity(0)
        // {
        //     // 按照标准库里的string设计无参构造函数
        //     // _str存放一个空串,而不是直接设置为nullptr
        //     _str = new char[1];
        //     _str[0] = '\0';
        // }
        string(const char *str = "")
            :_size(strlen(str))
            ,_capacity(_size)
        {
            // strcpy函数会将\0也拷贝过去
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

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

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

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

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

        // // 原始写法,代码比较多
        // string(const string &s)
        //     :_size(strlen(s._str))
        //     ,_capacity(_size)
        // {
        //     _str = new char[_capacity + 1];
        //     strcpy(_str, s._str);
        // }

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

        // 现代写法,让tmp去完成深拷贝,复用常量字符串构造函数
        string(const string &s)
            :_str(nullptr)
            ,_size(0)
            ,_capacity(0)
        {
            string tmp(s._str);
            swap(tmp);// 这个调用的是string类的swap函数
        }

        // 原始写法
        // string &operator=(const string &s)
        // {
        //     // 防止出现自己给自己赋值导致的错误
        //     if (this != &s)
        //     {
        //         delete[] _str;
        //         _str = new char[s._capacity + 1];
        //         strcpy(_str, s._str);
        //         _size = s._size;
        //         _capacity = s._capacity;
        //     }

        //     return *this;
        // }

        // 现代写法
        // string& operator=(const string& s)
		// {
		// 	if (this != &s)
		// 	{
		// 		string tmp(s._str);
		// 		swap(tmp);
		// 	}

		// 	return *this;
		// }

        // 现代写法更简洁的版本
        string &operator=(const string& s)
        {
            string tmp(s);
            swap(tmp);

            return *this;
        }

        size_t size() const
        {
            return _size;
        }

        size_t capacity() const
        {
            return _capacity;
        }

        void push_back(char ch)
        {
            // 说明容量满了,需要先扩容
            if (_size == _capacity)
            {
                // 需要考虑到_capacity是不是等于0这个情况
                reserve(_capacity == 0 ? 4 : _capacity * 2);
            }
            _str[_size] = ch;
            _size++;
            _str[_size] = '\0';
        }

        void append(const char* str)
        {
            // 先计算插入字符串以后的长度
            int len = _size + strlen(str);
            // 如果容量不大需要扩容
            if (len > _capacity)
            {
                reserve(len);
            }

            strcpy(_str + _size, str);
            _size = len;
            _str[_size] = '\0';
        }

        void reserve(size_t n)
        {
            // 如果容量不够,就要扩容
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];// 保留一个位置给\0
                strcpy(tmp, _str);
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }

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

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

        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[_size] = '\0';
        }

        string& insert(size_t pos, char ch)
        {
            // 断言防止pos出现非法范围,当pos=_size时就是push_back
            // 所以push_back可以复用这个insert
            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;
        }

        string& insert(size_t pos, const char* str)
        {
            assert(pos <= _size);

            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;
        }

        string& erase(size_t pos, size_t len = npos)
        {
            assert(pos <= _size);

            if (len == npos || pos + len >= _size)
            {
                _str[pos] = '\0';
                _size = pos;
            }
            else
            {
                size_t begin = pos + len;
                while (begin <= _size)
                {
                    _str[begin - len] = _str[begin];
                    begin++;
                }
                _size -= len;
            }

            return *this;
        }

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

            return npos;
        }

        size_t find(const char* str, size_t pos = 0)
        {
            const char* p = strstr(_str + pos, str);
            if (p == nullptr)
            {
                return npos;
            }
            else
            {
                return p-_str;
            }
        }

    private:
        char *_str;
        size_t _size;// 有效字符的个数
        size_t _capacity;// 存储有效字符的空间

        const static size_t npos;
    };

    const size_t string::npos = -1;

    std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

    std::istream& operator>>(std::istream& in, string& s)
	{
        // 先将字符串清空
        s.resize(0, '\0');
        // 这种方法可能存在多次扩容的情况,效率较低
		//char ch;
		in >> ch;
		//ch = in.get();
		//while (ch != ' ' && ch != '\n')
		//{
		//	s += ch;
		//	//in >> ch;
		//	ch = in.get();
		//}

		//return in;

		char ch;
		ch = in.get();
		char buff[128] = {'\0'};
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				s += buff;
				memset(buff, '\0', 128);
				i = 0;
			}

			ch = in.get();
		}

		s += buff;
		return in;
	}

    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 strcmp(s1.c_str(), s2.c_str()) == 0;
    }

    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);
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值