C++ STL -->模拟实现string

在之前的文章中C++ STL-->String类详解,介绍了一下STL中string类的基本使用 这篇文章将模拟实现string类的常用函数

string模拟实现

模拟实现的目的就是为了更好的使用STL

模拟实现string类函数接口

cpp 复制代码
namespace ding
{
   class string
   {
   public:
        string(const char* str = "");
        string(const string& s);
        string& operator=(const string& s);
        ~string();
        //=======================================================
        // iterator
        typedef char* iterator;
        iterator begin();
        iterator end();
        //=======================================================
        // modify
        void push_back(char c);
        void append(const char* str);
        string& operator+=(char c);
        string& operator+=(const char* str);
        void clear();
        void swap(string& s);
        const char* c_str()const;
        //=======================================================
        // capacity
        size_t size()const;
        size_t capacity()const;
        bool empty()const;
        void resize(size_t n, char c = '\0');
        void reserve(size_t n);
        //=======================================================
        // access
        char& operator[](size_t index);
        const char& operator[](size_t index)const;
        //=======================================================
        //relational operators
        bool operator<(const string& s);
        bool operator<=(const string& s);
        bool operator>(const string& s);
        bool operator>=(const string& s);
        bool operator==(const string& s);
        bool operator!=(const string& s);
        // 返回c在string中第一次出现的位置
        size_t find(char c, size_t pos = 0) const;
        // 返回子串s在string中第一次出现的位置
        size_t find(const char* s, size_t pos = 0) const;
        // 在pos位置上插入字符c/字符串str,并返回该字符的位置
        string& insert(size_t pos, char c);
        string& insert(size_t pos, const char* str);
        // 删除pos位置上的元素,并返回该元素的下一个位置
        string& erase(size_t pos, size_t len);
    private:
        char* _str;       //存储字符串
        size_t _capacity; //记录字符串当前的容量
        size_t _size;     //记录字符串当前的有效长度
   };
   ostream& operator<<(ostream& _cout, const ding::string& s);
   stream& operator>>(istream& _cin, ding::string& s);
};

为了解决和标准库中string命名发生冲突,这里使用自己的命名空间解决

默认成员函数

构造函数

无参构造函数:构造空字符串 空字符串里面是有一个\0的 并不是什么都没有

cpp 复制代码
string::string()
	:_str(new char [1])
	,_capacity(0)
	,_size(0)
{
	_str[0] = '\0';
}

带参构造函数

cpp 复制代码
string::string(const char* str)
	:_size(strlen(str))
{
	_capacity = _size;
	_str = new char[_capacity + 1];//多开辟一块空间存放\0
	strcpy(_str, str);
}

省值可以将无参的和带参的构造函数写成一个函数

函数声明:string(const char* str = "");

注意 这里缺省值只能给函数声明,定义的时候不能再使用缺省值

实现:

cpp 复制代码
string::string(const char* str )
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

析构函数

这里的构造函数用new从堆区申请空间,所以析构函数需要自己实现去主动释放资源,编译器默认生成的无法满足需求

cpp 复制代码
string::~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

拷贝构造函数

这里如果使用编译器默认生成的拷贝构造函数去初始化对象 在对象生命周期结束时 c++编译器自动调用析构函数会出错 如下图

原因 :这里是浅拷贝 s1和s2共用同一块地址空间,在对象s1,s2生命周期结束时,调用析构函数。对s1进行析构时,已经将资源释放掉了,而s2还不知道资源已经被释放,再次对其资源进行释放时,就会出现访问违规。 关于深浅拷贝

浅拷贝:编译器只会将对象中的值拷贝过来。拷贝的对象和源对象共用同一块地址空间,对其中一个对象的修改会影响到另一个对象

深拷贝:拷贝的对象与源对象使用不同的地址空间,二者互不干扰。

在这里,我们应该实现的是深拷贝,编译器默认生成的不能完成。需要自己实现一个拷贝构造函数 代码实现

cpp 复制代码
string::string(const string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

赋值运算符重载函数

赋值时,同样应该采用深拷贝的方式进行赋值

首先将原来的空间释放掉,然后申请新空间进行拷贝
注意:如果自己给自己赋值,应该进行判断。否则将自己释放后,在进行赋值,会出错

cpp 复制代码
string& 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;
}

这里申请临时对象是为了防止申请空间失败,对原来的数据造成破坏

迭代器相关函数(iterator)

迭代器在string类中的实现很简单 就是给字符指针取了个高大上的名字
typedef char* iterator;
typedef const char* const_iterator;

begin()

返回字符串的起始位置的地址

cpp 复制代码
string::iterator string::begin()
{
        return _str;
}
string::const_iterator string::begin()const
{
        return _str;
}    

end()

返回字符串结束位置的地址 即\0位置的地址

cpp 复制代码
string::iterator string::end()
{
        return _str + _size;
}
string::const_iterator string::end()const
{
        return _str + _size;
}

利用迭代器遍历字符串

在底层 用迭代器遍历字符串,实际上就是用指针的方式去遍历

cpp 复制代码
string s1("Hello world");
auto it = s1.begin();
while (it != s1.end())
{
        cout << *it << endl;
        it++;
}

capacity(容量和大小函数)

size函数

返回字符串的元素个数不包含\0;

cpp 复制代码
size_t string::size()const
{
        return _size;
}

capacity函数

返回字符串的有效空间大小

cpp 复制代码
size_t string::capacity()const
{
        return _capacity;
}

reserve函数

扩容

reserve函数规则:

  • 大于当前对象的capacity时,将capacity扩大到n或者大于n
  • 当n小于当前对象的capacity时,什么也不做。(不会缩容)
cpp 复制代码
void string::reserve(size_t n)
{
        _capacity = n + _capacity;
        char* tmp = new char[_capacity];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        //_size 不用处理
        //_size = strlen(_str);
}

resize函数

扩容+初始化

resize函数的规则:

  • 当n大于当前对象的size时 将size扩大到n 若未指定字符 默认为'\0'
  • 当n小于当前对象的size时 将size缩小到n
cpp 复制代码
void string::resize(size_t n, char c = '\0')
	{
		if (n <= _size)
		{
			_size = n;
			_str[_size] = '\0';
		}
		else
		{
			if (n > _capacity)
			{
				reserve(n);
			}
			//加数据
			size_t i = _size;
			while (i < n)
			{
				_str[_size] = c;
				i++;
			}
			_size = n;
			_str[_size] = '\0';
		}
	}

modfiy(修改字符串函数)

push_back(尾插)

在字符串结尾插入一个字符

插入之前首先要考虑一下容量是否足够 不够需要扩容

cpp 复制代码
void string::push_back(char c)
{
        //扩容
        if (_size >= _capacity)
        {
                _capacity = _capacity == 0 ? 2 : _capacity;//处理空串
                reserve(_capacity * 2);
        }
        //push_back
        _str[_size] = c;
        ++_size;
        //处理\0
        _str[_size] = '\0';
}

注意:插入一个字符后需要自己处理一下字符串结束标志\0

append(尾插一个字符串)

在字符串尾部插入一个字符串 在插入之前还是首先要检查一下容量是否足够

cpp 复制代码
void string::append(const char* str)
{
    //扩容
    size_t len = strlen(str);
    if (_size + len >= _capacity)
    {
        _capacity = _capacity == 0 ? len : _capacity;//处理空串
        reserve(_capacity + len);
    }

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

注意:这里使用strcat也可以完成。但是strcat需要遍历找尾,效率太低,使用strcpy即可。strcpy会处理字符串结束标志\0,不用自己手动处理

+=赋值运算符重载

+=运算符重载实现很简单了 直接函数复用即可

cpp 复制代码
string& string::operator+=(char c)
{
    push_back(c);
    return *this;
}
string& string::operator+=(const char* str)
{
    append(str);
    return *this;
}
string& string::operator+= (const string& str)
{
    append(str.c_str());
    return *this;
}

这里的c_str()函数就体现出价值了。append函数的形参是const char* 类型 而str是string类型 直接传参会导致参数类型不匹配错误。使用c_str转换一下类型即可

cpp 复制代码
const char* string::c_str()const
{
    return _str;
}

注意:这里的c_str()实现一个const成员函数即可,cosnt和非const对象都可以调用。如果只实现非cosnt版本的话,const对象去调用c_str函数则会出错。

clear

清空字符串中的有效字符,不会改变底层空间的大小(不会影响容量的大小)

cpp 复制代码
void string::clear()
{
    _size = 0;
    _str[0] = '\0';
}

find

默认从0(下标)位置开始查找,返回第一个匹配项的下标位置,找不到返回npos
nposstatic const size_t npos = -1;

cpp 复制代码
size_t string::find(char c, size_t pos) const
{
    for (size_t i = 0; i < _size; i++)
    {
        if (_str[i] == c)
        {
            return i;
        }
    }
    //没找到
    return npos;
}
cpp 复制代码
size_t string::find(const char* s, size_t pos) const
{
    const char* ret = strstr(_str + pos, s);
    if (ret)
    {
        return ret - _str;
    }
    else
    {
        return npos;
    }

}

insert

在指定位置插入n个字符

cpp 复制代码
string& string::insert(size_t pos, size_t n,char c)
{
        //扩容
        if (_size + n >= _capacity)
        {
                reserve(_capacity+n);
        }
        //挪动数据
        int end = _size;//会把\0挪动
        while (end >= (int)pos)
        {
                _str[end + n ] = _str[end];
                --end;
        }
        while (n--)
        {
                _str[pos] = c;
                pos++;
        }
        _size += n;
        return *this;
}

在指定位置插入字符串

cpp 复制代码
string& string::insert(size_t pos, const char* str)
{
        assert(pos <= _size);
        size_t len = strlen(str);
        //扩容
        if (_size + len >= _capacity)
        {
                reserve(_capacity + len);
        }
        //挪动数据
        int end = _size;
        while (end >= (int)pos)
        {
                _str[end + len] = _str[end];
                --end;
        }
        strncpy(_str + pos, str, len);//不会处理\0
        return *this;
}

pos可以通过find函数查找指定 比如

cpp 复制代码
string s2("Hello");
size_t pos2 = s2.find('e');
s2.insert(pos2, 5, 'x');//在e的前面插入5个x
cout << s2.c_str() << endl;

erase

删除指定位置指定长度的字符(不指定长度全部删除)

  • 当长度大于字符串长度时,直接在pos位置赋值\0 即可
  • 其他情况挪动数据删除即可。
cpp 复制代码
string& string::erase(size_t pos, size_t len)
{
        assert(pos < _size);
        if (pos+len >= _size || len == npos)
        {
                _str[pos] = '\0';
                _size = pos;
        }
        else
        {
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
        }
        return *this;
}

访问字符串相关函数(access)

[]运算符重载

[]运算符重载函数目的是为了能让string对象像字符数组一样,使用[]+下标的方式进行访问 通过[]+下标,可以访问对应下标位置的元素。模拟实现时把对应位置的引用返回即可 这样可读可写

cpp 复制代码
char& string::operator[](size_t pos)
{
        assert(pos < _size);
        return _str[pos];
}
const char& string::operator[](size_t pos)const
{
        assert(pos < _size);
        return _str[pos];
}

front && back函数

获取字符串有效元素第一个元素和最后一个元素(\0之前的)

cpp 复制代码
char& string::front()
{
        return _str[0];
}
const char& string::front() const
{
        return _str[0];
}
char& string::back()
{
        return _str[_size - 1];
}
const char& string::back() const
{
        return _str[_size - 1];
}

关系运算符重载函数(relational operators)

关系运算符有 == 、!=、 <、 <=、 >、 >= 六个

实现其中一个==和> 或者< 函数复用一下,其他的就全部能实现了

cpp 复制代码
bool string::operator<(const string& s)
{
        return strcmp(_str, s.c_str()) < 0;
}
bool string::operator==(const string& s)
{
        return strcmp(_str, s.c_str()) == 0;
}
bool string::operator<=(const string& s)
{
        return *this < s || *this == s;
}
bool string::operator>(const string& s)
{
        return !(*this <= s);
}
bool string::operator>=(const string& s)
{
        return  *this > s || *this == s;
}
bool string::operator!=(const string& s)
{
        return !(*this == s);
}

>>和<<的重载

<<重载

<< 的重载是为了让string对象 像内置类型一样,直接使用<<进行输出

重载时就是遍历一遍string对象,string对象存的是char类型,在使用<<输出即可

cpp 复制代码
ostream& operator<<(ostream& out, const string& s)
{
    for (auto ch : s)
    {
        out << ch;
    }
    return out;
}

注意: cout的类型是ostream类类型;返回ostream的引用是为了连续输出

>>重载

<<的重载是为了让string对象可以像内置类型一样,直接使用cin进行输入。

输入时应该注意,先清空对象中的数据在进行输入。

cpp 复制代码
//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
    s.clear(); 
    char ch = in.get(); 
    while (ch != ' '&&ch != '\n') 
    {
            s += ch; 
            ch = in.get(); 
    }
    return in; 
}

注意 这里不能使用>>直接输入char类型 要用cin对象的成员函数get()

因为>> 会将空格或者\n当成默认的分隔符 ,ch不会向缓冲区中拿空格或者\n。 导致循环一直无法终止,显然不符合要求。C++ cin对象有一个成员函数get(),会忽略空格和\n。都会从缓冲区中读取。

相关推荐
思麟呀3 小时前
在C语言基础上的C++第二章(类和对象)
c语言·开发语言·c++·学习
hjxxlsx3 小时前
备战 C++ 编程区赛 指南
开发语言·数据结构·c++·算法·贪心算法·区赛
酷酷的崽7983 小时前
【递归与回溯深度解析:经典题解精讲(中篇)】—— LeetCode
c++·算法·leetcode
励志成为大佬的小杨4 小时前
初始c语言第一个c语言项目
c语言·c++·算法
和小潘一起学AI4 小时前
c++基于过程
开发语言·c++
凌盛羽5 小时前
Bitmap(BMP)图像信息分析主要说明带压缩的形式
c++·stm32·单片机·物联网·proteus·嵌入式实时数据库
SunkingYang5 小时前
C/C++应该如何使用NI-488.2库?
c语言·c++·协议·ni·使用方法·设备·gpib
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之循环结构(for循环语句)(三)
数据结构·c++·算法
计科土狗6 小时前
巧妙算法之位运算的应用
c++
姚先生976 小时前
LeetCode 349. 两个数组的交集 (C++实现)
c++·算法·leetcode