一、前言
在我们对STL中的string类有了一个基本的认识后,今天我们手动的从0到1去模拟实现一个STL库中的string类中的一些常用接口。
二、string类的模拟实现
为了不和库里的string类产生冲突,我们可以建立一个名为其他的命名空间,此时因为作用域不同,就不会产生冲突。即namespace xxx.
下面是对string类的声明:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<assert.h>
using namespace std;namespace zihan//命名空间
{class string//定义类{public://构造string(const char* str = " ")//全缺省{_size = strlen(str);_capacity = _size;//_capacity中不包含\0,为了存放字符 \0 需要长度加一_str = new char[_capacity + 1];strcpy(_str, str);//拷贝字符串}//显示调用拷贝构造//S2(S1)string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}//赋值 S1=S2string& 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(){delete[]_str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const{return _str;}size_t size()const{return _size;}size_t capacity()const{return _capacity;}//迭代器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;}//返回pos位置的字符const char& operator[](size_t pos)const{return _str[pos];}//清掉所有的数据但不动空间void clear(){_str[0] = '\0';_size = 0;}//预留空间void reserve(size_t n);//尾插字符void push_back(char ch);//尾插字符串void append(const char* str);//+=string& operator+=(char ch);string& operator+=(const char* str);//头插void insert(size_t pos,char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len = npos);//查找字符、字符串size_t find(char ch, size_t pos = 0);size_t fing(const char* str, size_t pos = 0);//截取字符串string substr(size_t pos, size_t len);//比较bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;private:char* _str = nullptr;size_t _capacity = 0;size_t _size = 0;static const size_t npos;};//流插入 流提取ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& out, string& s);
}
2.1成员函数
2.1.1构造函数
string(const char* str = " ")
string(const char* str = " ")//全缺省{_size = strlen(str);_capacity = _size;//_capacity中不包含\0,为了存放字符 \0 需要长度加一_str = new char[_capacity + 1];strcpy(_str, str);//拷贝字符串}
2.1.2拷贝构造函数
string(const string& s)
我们知道在拷贝构造中,若是一个类在没有显示定义拷贝构造,对于内置类型不做处理,对于自定义类型会去调用类中默认提供的拷贝构造函数,此时就会造成浅拷贝问题。
浅拷贝:编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一对象还不知该资源已被释放,以为还有效,若继续对资源进行操作时,就会发生访问违规。
深拷贝:源对象和拷贝对象相互独立,其中一个对象的改动不会对另一个对象造成影响
若想进行深拷贝,必须显示定义拷贝构造
1.传统写法
//显示调用拷贝构造//S2(S1)string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
传统写法的主要思想是开辟一块足够的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也拷贝过去即可。
2.现代写法
void swap(string& s)
{std::swap(_str , s._str);//调用标准库中的swap函数将对象的成员变量进行交换成员变量std::swap(_size , s._size);std::swap(_capacity , s._capacity);
}
//现代写法
string(const string& s)
{string tmp(s._str);//带参构造swap(tmp);//通过交换去掠夺
}
现代写法的主要思想是先根据原字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象进行数据交换。
2.1.3赋值运算符重载函数
string& operator=(const string& s)
1.传统写法
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;}
2.现代写法
//S1=S2
//现代写法
string& operator=(const string& s)
{//不能自己赋值自己if (this != &s){string tmp(s._str);swap(tmp);}return *this;
}
这里通过用源对象字符串构造一个tmp对象, tmp对象与s1交换之后,出了作用域也会销毁,可以达到将s2赋值给s1的效果。
2.1.4析构函数
~string()
~string(){delete[]_str;_str = nullptr;_size = _capacity = 0;}
2.2元素访问
2.2.1迭代器
本质上是封装思想,可以类比指针,可以指向容器中的某个元素,并可以通过操作迭代器来访问和修改容器中的元素 。
//迭代器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;}
2.2.2 返回pos位置的字符
const char& operator[](size_t pos)const
//返回pos位置的字符const char& operator[](size_t pos)const{return _str[pos];}
2.3字符串修改
2.3.1push_back 尾插字符
//尾插字符
void string::push_back(char ch)
{//可能要扩容if (_capacity == _size){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;//不要忘记\0_str[_size] = '\0';
}
2.3.2append尾插字符串
//尾插字符串
void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){//扩容 如果长度大于2倍 则该多大就开多大 否则就开2倍的空间reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);}strcpy(_str + _size, str);_size += len;
}
2.3.3operator+=(char ch)
复用push_back即可
string& string::operator+=(char ch)
{push_back(ch);return *this;
}
2.3.4operator+=(const char* str)
复用append即可
string& string::operator+=(const char* str)
{append(str);return *this;
}
2.3.5在pos位置插入字符insert
void string::insert(size_t pos, char ch)
{assert(pos <= _size);//万一要扩容if (_capacity == _size){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size;//进行挪动 end 挪向end+1while (end >= (int)pos){_str[end + 1] = _str[end];--end;}_str[pos] = ch;++_size;}
注意这里挪动数据时的while循环的判断条件end>=pos,因为end和pos都是size_t无符号类型,所以当_size=0时,end无论怎么— —都不会小于_size,也就会陷入死循环 。
这里有解决方法:1.把无符号类型强转为有符号类型,进行整型提升
2.令end=_size+1,循环条件变成>
2.3.6在pos位置插入字符串insert
void string::insert(size_t pos, const char* str)
{assert(pos <=_size);size_t len = strlen(str);if (_size + len > _capacity){//扩容 如果长度大于2倍 则该多大就开多大 否则就开2倍的空间reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);}size_t end = pos + len;while (end >= pos+len){_str[end] = _str[end - len];end--;}//再依次把字符串插进去for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}
2.3.7删除pos之后的n个字符
void string::erase(size_t pos, size_t len )
void string::erase(size_t pos, size_t len )
{if (len > _size - pos){_str[pos] = '\0';}else{for (size_t i = pos + len; i <= _size; i++){_str[i - len] = _str[i];}}_size -= len;
}
这里分两种情况:
1.如果len>_size-pos,说明pos之后的全删除,只需把pos位置置为'\0'
2.否则需要将不删除的字符一个个往前挪动
2.4比较运算符
2.4.1大于>
bool string::operator>(const string& s)const
{return strcmp(_str, s._str) > 0;
}
2.4.2小于<
bool string::operator<(const string& s)const
{return strcmp(_str, s._str) < 0;
}
2.4.3等于==
bool string::operator==(const string& s)const
{return strcmp(_str, s._str) == 0;
}
2.4.4大于等于>=
这里可以复用>和=,也可以对<取反
bool string::operator>=(const string& s)const
{return !(*this < s);
}
2.4.5小于等于<=
同理,这里可以复用<和=,也可以对>取反
bool string::operator<=(const string& s)const
{return !(*this > s);
}
2.4.6不等于!=
bool string::operator!=(const string& s)const
{return !(*this == s);
}
2.5查找和截取
2.5.1find,寻找一个字符
size_t find(char ch, size_t pos = 0);
size_t string::find(char ch, size_t pos)
{for (size_t i = 0; i < _size; i++){if (_str[i] == ch){return i;}return npos;}}
2.5.2find,寻找一个字符串
size_t fing(const char* str, size_t pos = 0);
这里直接使用C语言中的库函数strstr函数
size_t string::fing(const char* str, size_t pos)
{const char* ptr = strstr(_str + pos, str);//str在_str+pos中的位置if (ptr){return ptr - _str;//找到了,返回下标,所以指针相减}return npos;//没找到}
2.5.3substr,截取字符串
string string::substr(size_t pos, size_t len)
//截取字符串
string string::substr(size_t pos, size_t len)
{if (len > _size - pos)//大于有效字符个数{ //更新lenlen = _size - pos;}//最后需要返回字符串string sub;sub.reserve(len);for (size_t i = 0; i < len; i++){//把要截取的字符尾插到subsub += _str[pos + i];}return sub;
}
在更新好有效字符个数后,从pos位置取n个字符追加到临时的string对象中去, 最后将其返回即可,这里我们返回了一个出作用域就销毁的临时变量,只能使用传值返回,不能使用传引用返回。
2.6空间操作
2.6.1size
获取当前字符串的有效长度,不包含'\0'
size_t size()const
{return _size;
}
2.6.2capacity
获取字符串的当前的容量
size_t capacity()const
{return _capacity;
}
2.6.3clear清空数据
清掉所有的数据但不动空间
//清掉所有的数据但不动空间
void clear()
{_str[0] = '\0';_size = 0;}
2.6.4reserve预留空间,扩容
void string::reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//开出一块新的空间,永远要多开一个空间 存\0strcpy(tmp, _str);//将原本的数据拷贝过来delete[]_str;//释放旧空间_str = tmp;//指向新空间_capacity = n;}
}
2.7流插入,流提取
2.7.1operator<< 流插入
ostream& operator<<(ostream& out, const string& s)
{for (auto ch: s)//范围for遍历{out << ch;}return out;
}
注意这里不用加友元,因为并没有访问私有成员变量
2.7.2operator>> 流提取
istream& operator>>(istream& in, string& s)
{s.clear();//清理字符串char ch = in.get();//一个一个字符获取while (ch != ' ' && ch != '\n'){s += ch;//将读取到的字符插到字符串后面ch = in.get();//继续读取字符串}return in;}
可以进行优化,遇到长度比较大的字符串时可以减少扩容次数
istream& operator>>(istream& in, string& s){s.clear();const int n = 256;char buff[n];char ch = in.get();int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i = n - 1)//如果字符存满了{buff[n - 1] = '\0';s += buff;i = 0;//继续循环 直到读完}if (i > 0)//如果没有读满数组 还有剩余空间{buff[i] = '\0';s += buff;}return in;}if (i > 0){buff[i] = '\0';s += buff;}return in;}
}
样例测试:
以上就是string类基本接口的模拟实现,有不足的地方还请大家指正,共同交流!