【STL学习之路】string的模拟实现

一、string的结构

class string
{
  public:
    //迭代器
    typedef char* iterator;
    typedef const char* const_iterator;
  public:
    
    string(const char* str = "")//构造
    string(const string& s)//拷贝构造
    string& operator=(const string &s)//赋值重载
    ~string()//析构

    // iterator
    iterator begin();
    iterator end();
	const_iterator begin() const;
    const_iterator end() const/

    // modify

    void push_back(char c);//尾插一个字符
    string& operator+=(char c);//+=字符
    void append(char c);//拼接一个字符
    void append(const char* str);//拼接一个字符串std
    void append(const string& s);//拼接一个string
    string& operator+=(const char* str);//+=字符串
    void clear();//清理
    void swap(string& s);//交换
    const char* c_str()const;//返回C形式字符串
    /

    // capacity

    size_t size()const;//string 大小
    size_t capacity()const;//string 容量
    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;
    /

    //字符串比较

    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);
    
    //返回子串 - 返回从pos位置向后的len个字符
    string substr(size_t pos = 0, size_t len = npos);
  private:
    char* _str;
    size_t _capacity;
    size_t _size;
  }
	
private:
    char* _str;	
	size_t _size;
	size_t _capacity;
public:
	static size_t npos;//npos 类外定义为-1 
};
size_t npos = -1;
ostream& operator<<(ostream& out, const string& s);//重载cout
istream& operator>>(istream& in, string& s);//重载cin

string的接口

1. 构造函数

注意点:

  1. 默认构造给空字符串 “”,_str不能给nullptr,因为 _str为char*类型,cout输出时,实际上是解引用输出字符串,直到\0, _str为空会发生空指针的解引用
  2. capacity为有效数据容量,开空间应多开一个空间给’\0’

写法1

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

缺点:strlen()是O(N)的接口,调用多次,效率太低!

写法2(优化): 只调用一次strlen

string(const char* s = "") // 也可赋值为"\0"
		{
			_size = strlen(s);
			_capacity = _size;
			_str = new char[_capacity+1]; //空间大小是有效容量 + 1('\0')
			strcpy(_str, s);//拷贝
		}

错误写法

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

//错误原因:
//虽然strlen只用一次,但是初始化列表的初始化顺序是根据成员函数的声明来的,所以会先走_str的初始化,此时_capacity是随机值,会出错! 如果要使用这种方法,要保证成员声明的顺序并且当增加新的成员的时候,还要维护声明的顺序!

2. 析构函数

//直接释放资源即可
~string()
{
	_size = _capacity = 0;
	delete[] _str;
	_str = nullptr;
}

3. swap函数

void swap(string& tmp)		//当前对象和tmp交换的函数
{
    //调用库中的swap来交换基本类型
	::swap(_str, tmp._str);	
	::swap(_capacity, tmp._capacity);
	::swap(_size, tmp._size);
}

4. 拷贝构造

注意问题:深浅拷贝问题

_str是一个指针,指向动态申请的数组,如果不显示写一个深拷贝,那么默认生成的拷贝构造就会采取值拷贝的形式进行,

这样拷贝的对象中的_str指针和原对象的 _str指针指向同一块空间,存在问题:

  1. 拷贝对象的修改会影响原对象
  2. 析构的时候,会析构两次,导致崩溃

所以:拷贝的时候需要另外开辟一段新空间,把原对象的空间的内容拷贝到新空间

1) 传统写法

string(const string& s)
			:_size(s._size)
			,_capacity(s._capacity)
			,_str(new char[s._capacity+1]) //新开空间	
		{
			strcpy(_str, s._str);//把数组中的内容进行拷贝
		}

2) 现代写法

//现代写法就是让一个临时对象帮this去做开空间和拷贝 最后再交换
//需要先把this的成员初始化,都是随机值,_str就是野指针,野指针delete会崩溃!
string(const string& s)
			:_size(0)
			,_capacity(0)
			,_str(nullptr)
		{
			string tmp(s._str);	//利用s的_str构造一个临时对象tmp
			swap(tmp);//this指向的对象和tmp交换所有成员变量
		}				

5. 赋值重载(operator=)

1) 传统写法

	string& operator=(const string& s)
		{
			//自己给自己赋值 直接返回  如果不处理,自己给自己释放了就变成了随机值
			if (this == &s) {
				return *this;
			}
       		 //开空间成功才会删除原来的 .这样的好处是如果new失败 原对象不会被破坏
			char* tmp = new char[s._capacity + 1];
			//失败的话抛异常就直接跳到catch了
			strcpy(tmp, s._str);
			delete[] _str;
			_str = tmp;		//然后再把tmp赋值给_str
			_size = s._size;
			_capacity = s._capacity;
			return *this;
		}

2) 现代写法

	//形参不传递引用,直接用实参拷贝构造一个形参
	//然后形参与this交换
	//形参最后会销毁
	string& operator=(string s)
	{
		swap(s);			//s作为形参,调用结束也会销毁
		return *this;
	}

6. 返回C形式字符串(c_str)

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

7. 返回长度(size)

size_t size() const
{
    return _size;
}

8. 判空(empty)

bool empty() const
{
	return _size == 0;
}

9. 容量(capacity)

size_t capcacity() const
{
	return _capacity;
}

10. operator[]

//operator[]  -> 返回pos位置的元素的引用
char& operator[](size_t pos)
{
    assert(pos < _size);
    return _str[pos];
}
//operator[] const   ---  const对象调用 返回const引用
const char& operator[](size_t pos) const
{
    assert(pos < _size);
    return _str[pos];
}

11. 迭代器

string的底层是数组,迭代器就是原生指针

typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
    return _str;
}
iterator end()
{
    return _str + _size;	//end()返回最后一个位置的下一个位置的迭代器
}
const_iterator begin() const
{
    return _str;
}
const_iterator end() const
{
    return _str + _size;
}

12. 设置容量(reserve)

//扩容 -> 大于当前容量就扩容
// 注意要进行深拷贝 -> 开新空间
void reserve(size_t n)
{
    //如果n大于当前的capacity 才会进行预留空间
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];
        //把原空间的内容拷贝进去
        strcpy(tmp, _str);
        //释放原空间
        delete[] _str;
        _str = tmp;
        _capacity = n;
    }
}

13. 设置大小(resize)

//改变大小 
void resize(size_t n,char c = '\0')
{
    //如果n大于_size 把size到n位置的元素 置为c 并改变size
    if (n > _size)
    {
        reserve(n);//扩容一下,因为可能n大于_capacity
        for (size_t i = _size; i < n; ++i) //n==size时不会进入循环
        {
            _str[i] = c;
        }
        _str[n] = '\0';	//不要忘了末尾加'\0'
        _size = n;
    }
    //如果n<size,直接保留前n个字符,后面的删除即可
    else
    {
        _size = n;
        _str[n] = '\0';
    }
}

14. 尾插字符(push_back)

void push_back(char c)
{
    //如果满了
    if (_size == _capacity)
    {
        //扩容复用reserve
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
    _str[_size] = c;
    ++_size;
    //最后一个位置保证是'\0'
    _str[_size] = '\0';
}

15. 拼接字符串(append)

//拼接一个字符-> 复用push_back
void append(char c)
{
    push_back(c);
}
//拼接n个字符
void append(size_t n, char c)
{
    reserve(n + _size);// 减少扩容(如果n+_size 大于此时的容量)就会扩容
    for (size_t i = 0; i < n; ++i)
    {
        push_back(c);
    }
}
//拼接C字符串
void append(const char* s)
{
    size_t len = strlen(s);
    //如果 追加之后的字符串大于此时的容量 就要扩容
    if (len + _size > _capacity)
    {
        //扩容  复用reserve
        reserve(len + _size);
    }
    strcpy(_str + _size, s);//拷贝到从_size开始往后的位置
    //如果用strcat(_str,s)追加,那么每次都要找'\0' 效率很低
    _size += len;//更新size
}

//拼接string
void append(const string& s)
{
    //直接复用重载即可
    append(s._str);
}

16. operator+=

// += 一个字符
string& operator+=(char c)
{
    //复用push_back即可
    push_back(c);
    return *this;
}
//+=字符串
string& operator+=(const char* s)
{
    //复用append
    append(s);
    return *this;
}

17. 插入字符/字符串(insert)

//pos位置插入字符
string& insert(size_t pos, char c)
{
    assert(pos <= _size); // =:可以尾插
    //如果满了  扩容
    if (_size == _capacity)
    {
        //扩容复用reserve
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }//另一种写法 end表示后面的位置 而不是前面的位置
    //向后挪动
    size_t end = _size + 1;
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }
    _str[pos] = c;//插入字符c
    ++_size;//更新size
    return *this;
}

/

//pos位置插入字符串
string& insert(size_t pos, const char* s)
{
    assert(pos <= _size);//检查pos合法
    size_t len = strlen(s);
    if (len + _size > _capacity)
    {
        reserve(_capacity + len);//扩容
    }
    //向后挪动
    size_t end = _size + len;
    while (end>=pos+len)	//最后一次是_str[pos+len] = _str[pos]
    {
        _str[end] = _str[end - len];//要插入的字符串的长度为len pos位置之后的向后挪动len个位置
        --end;
    }
    //把数据拷贝进去 -- 拷贝n个
    strncpy(_str + pos, s,len);
    _size += len;//更新_size
    return *this;
}

18. 删除字符(erase)

//从pos位置删除n个字符
//默认是从0位置,删除所有字符,因为npos为整数最大值
void erase(size_t pos = 0,size_t n = npos)
{
    assert(pos < _size);
    //如果 n >= _size-pos   即要删除的个数 大于等于 pos之后的字符的个数  直接全删了就可以
    if (n == npos || n >= _size - pos)
    {
        _str[pos] = '\0';
        _size = pos; //更新长度
    }
    else
    {
        //hello worldxxx 否则就直接向前覆盖
        strcpy(_str + pos, _str + pos + n);//覆盖pos后面的n个位置
        _size -= n; //更新长度
    }
}

19. 查找字符/子串(find)

//从pos位置查找字符  - 返回第一次出现字符c的位置
size_t find(char c, size_t pos = 0) const
{
    assert(pos < _size);
    for (size_t i = pos; i < _size; ++i)
    {
        if (c == _str[i])
        {
            return i;
        }
    }
    return npos;//找不到返回npos
}
//从pos位置查找字符串 - 返回第一次出现字符串的位置
size_t find(const char* s, size_t pos = 0) const
{
    //复用C语言中的 strstr
    assert(s);
    assert(pos < _size);
    char* start = strstr(_str+pos,s);//从_str + pos位置开始查找字符串s
    //如果找到了 -> 返回npos
    if (start == nullptr)
    {
        return npos;
    }
    else
    {
        return start - _str;	//start是目标子串的首地址,指针相减得到start的坐标
    }
}

20. 返回子串(substr)

// 返回从pos位置向后的len个字符
string substr(size_t pos = 0, size_t len = npos)
{
    assert(pos < _size);
    string sub;	//临时对象
    size_t reallen = len;	//正常情况下真实长度就是len
    if (len == npos || len > _size - pos)	//如果len很大,或者len超过从pos到结尾的字符个数
    {
        reallen = _size - pos;		//就吧pos位置后面的全删除即可
    }
    for (size_t i = 0; i < reallen; ++i)
    {
        sub += _str[pos + i];
    }
    return sub;
}

21 字符串比较

//compare
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 !(*this <= s);
}
bool operator>=(const string& s) const
{
    return *this == s || *this > s;
}
bool operator!=(const string& s) const
{
    return !(*this == s);
}

22. 重载cout

误区:认为cout和cin的重载必须声明为友元!

其实不是的,因为可以通过接口来实现容器内成员的更改

ostream& operator<<(ostream& out, const string& s)
{
    //遍历string对象,利用重载的[]依次访问即可
    for (size_t i = 0; i < s.size(); ++i)
    {
        out << s[i];	//用接口访问数据
    }
    return out;
}

23. 重载cin

1) 初始版本

istream& operator>>(istream& in, string& s)
{
    //首先清理原字符串
    if (s.size() != 0)
    {
        s.clear();
    }
    //一个字符一个字符的读入		//get()是istream中的一个成员函数,用istream对象cin调用
    //get就是单纯的读字符,不会考虑不同元素之间的间隔也是空格的问题
    char ch = in.get();		// 用cin>>ch 不可以,因为cin自动忽略空格和换行 不会读到缓冲区 因为读的时候他认为一个一个输入之间的空格就是间隔!,所以空格根本不会读入 所以下面循环不会跳出!
    while (ch != ' ' && ch != '\n') // 如果不是空格也不是换行,就继续输入
    {
        //尾插到结尾
        /*s.push_back(ch);*/					//cin也完全不用直接访问成员,通过接口可以完成
        s += ch;
        ch = in.get();//继续读
    }
    return in;
}

2) 优化

优化问题:如果输入的字符串很长,初始版本中读入字符就会不断+= 可能会频繁扩容,效率很低

优化的思想: 利用一个临时数组,先把读到的数据放到临时数组中
如果临时数组满了,就接到字符串的后面

istream& operator>>(istream& in, string& s) 
{
    //清理
    if (s.size() != 0)
    {
        s.clear();
    }
    const size_t N = 32;//设定临时数组的大小
    char tmp[N];
    char c = in.get();
    size_t i = 0;
    while (c!=' ' && c!= '\n')
    {
        tmp[i++] = c;	//从缓冲区读入的数据放到临时数组
        if (i == N - 1)		//如果是最后一个数据,那么放入\0  然后 接到string的后面
        {
            tmp[i] = '\0';
            s += tmp;
            //然后i回到最开始
            i = 0;
        }
        c = in.get();
    }

    //如果某一次没有读到 N-1 ,在中途就遇到 '\n' 或者' '而跳出循环,所以再加一次
    tmp[i] = '\0';
    s += tmp;

    return in;
}

,这样有两个好处

  1. 如果频繁输入并且输入的字符串较小的话,减少扩容 ,每有输入N个才进行一次扩容
  2. 如果直接reserve来解决频繁扩容问题的话,若输入的字符串比较少,可能会浪费空间,用临时数组扩容的话,扩容后的容量就是输入字符串的长度(因为调用的是 +=)
    并且一般不会输入这么多!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2021狮子歌歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值