初识C++ · 模拟实现string
目录
1 构造 析构 拷贝构造 赋值
1.1 构造
1.2 析构
1.3 拷贝构造
1.4 赋值
2 迭代器
3 capacity部分
3.1 size capacity clear
3.2 resize reserve
4 [] const[] c_str的实现
4.1 push_back
4.2 append
4.3 +=
4.4 insert
4.5 erase
5 string operations部分
5.1 find
5.2 substr
6 6个全局函数->比较
7 几个非成员函数
7.1 swap
7.2 流插入
7.3 流提取
8 现代写法
继上文介绍了string的函数实现之后,本文介绍模拟实现string的大部分函数。
1 构造 析构 拷贝构造 赋值
1.1 构造
构造函数需要考虑的是无参和有参的构造不一样,所以我们可以使用函数重载实现两个构造函数,一个用来写无参构造,一个用来写const char*的:
string():_str(new char[1]),_size(0),_capacity(0){_str[0] = '\0';}
对于无参的构造,即是一个空串,所以_size _capacity都要给0,但是为什么还要给一个空间呢?为什么不给指针呢?因为即便是空串,里面也是有'\0'的,所以需要一个空间。
string(const char* s):_size(strlen(s)),_capacity(_size)//注意声明次序问题,_str(new char[strlen(s) + 1])//保证斜杠0{strcpy(_str, s);}
对于有参的构造,注意的地方应该是声明次序以及_str指向的空间,因为_capacity是等于_size,所以声明的时候是_size在前,不然给_capacity的值就是随机值了,然后_str指向的空间应该要给斜杠0预留一个位置,所以是strlen(s) + 1:
class strin
{
public://...
private:size_t _size;size_t _capacity;char* _str;
};
但是实际上,我们不用写上两个函数,可以将两个函数合二为一:
string(const char* str = "")//合二为一来个缺省值:_size(strlen(str)),_capacity(_size), _str(new char[strlen(str) + 1])
{strcpy(_str, str);
}
_size和_capacity的赋值问题不用多说,const char* str = ""可能是比较容易出错的点,当我们想要构造一个空串的时候,给_str的值就是一个"",部分同学会写的五花八门,比如const char* str = '\0',又或者是const char* str = "\0”,一个是char一个是char*不能赋值,一个是两个斜杠0,这里最好的写法就是这样,直接给一个空串就行了,然后strcpy即可。
1.2 析构
析构函数就简单了,没什么要特别注意的:
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
1.3 拷贝构造
使用拷贝构造的时候,就容易引出一个问题,即深浅拷贝的问题,如果我们只是简单的浅拷贝的一下,即两个_str指向的空间是一样的,那么带来的后果是很严重的,比如:
void Test_string(){string s1("123456");string s2(s1);s1[2] = 'x';cout << s1 << endl;cout << s2 << endl;}
他们指向的空间是一样的,那么修改的时候就会一并修改了,并且同一块空间都会析构两次,所以会报错:
所以我们要执行深拷贝,那么就是,给s2新开一块空间,再把s1的值赋值给它:
string(const string& s){_str = new char[s.capacity() + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
这样,就是深拷贝。
1.4 赋值
赋值问题,如果被赋值的串是空串,其实和拷贝构造函数没有什么太大的差别,但是大多数时候被赋值的串都是有值的,所以我们需要做的是拷贝构造一个临时变量,赋值给临时变量,释放被赋值的对象的原来的空间,再进行赋值。
string& operator=(const string& s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;return *this;}
目前赋值和拷贝构造都是比较传统的写法,后面介绍了swap,你就知道现代写法写起来有多舒服了。
2 迭代器
在string这里,实现的迭代器我们可以理解为指针,所以实现起来很简单,这里就实现begin end和cbegin cend:
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;
}
这个命名也是比较关键的,比如模拟实现顺序表的时候,重命名的又是另外的名字了,这也是为什么迭代器类型如此吓人的原因。
这里的实现是超乎你想象的简单的,所以不做过多介绍。
3 capacity部分
这里模拟实现size,resize,capacity,reserve,clear部分。
3.1 size capacity clear
size_t capacity() const
{return _capacity;
}
size_t size() const
{return _size;
}
void clear()
{_str[_size] = '\0';_size = 0;
}
其中clear的模拟实现我们不用想的太多复杂,直接给第一个字符置为斜杠0,也就达到了我们想要的效果
3.2 resize reserve
reserve的实现是影响的capacity,但是其他的不会变,这里也不用考虑缩容,考虑扩容即可,容易出错的点在于如何理解扩容,部分同学理解的可能是直接给原来的空间后面接上一段空间,实际上不是,大部分都是异地扩容的,所以需要我们重新开空间:
void reserve(size_t new_capacity)
{if (new_capacity > _capacity){char* tmp = new char[new_capacity + 1];_capacity = new_capacity;strcpy(tmp, _str);delete[] _str;_str = tmp;}//缩容一般不考虑
}
resize的实现是影响的size,再是间接的影响capacity,如果_size比原来的_size小的话是比较简单的,直接点就是一刀砍死,即在新_size的位置上给一个'\0'就可以了,那么如果是扩大,就在扩大之后的_size里面补上字符就行了,同时考虑需不需要扩容,最后不要忘记补个'\0':
void resize(size_t new_size,char ch = '\0'){//new_size < _size直接砍死if (new_size < _size){_str[new_size] = '\0';_size = new_size;}else{reserve(new_size);for (int i = _size; i < new_size; i++){_str[i] = ch;}_str[new_size] = '\0';//估摸着得忘}}
4 [] const[] c_str的实现
const char* c_str() const
{return _str;
}
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
const char& operator[](size_t pos)const
{assert(pos < _size);return _str[pos];
}
常用,并且简单,很容易实现。
5 Modifiers部分
这里介绍push_back,append,insert,erase,+=,swap函数。
4.1 push_back
push_back即尾插,需要考虑size == capacity,那么涉及扩容就扩容二倍就行,因为有时候是空串,所以扩容的时候可以使用三木操作符,当然,补'\0'也是很重要的,不能忘记。
void push_back(char ch)
{考虑扩容 二倍扩容即可if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;_str[_size] = '\0';
}
4.2 append
append即在末尾插入一个字符串,这里有两个思路,第一种是一直尾插,即一直调用push_back,第二种是判断出所需要空间,然后扩容,再使用字符函数strcpy直接进行插入:
void append(const char* s)
{size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, s);_size += len;
}
第一种思路就留给读者进行实现了,也是比较容易的,注意,这里的第二种思路是不可以只考虑二倍的,会出错,思维不能固化。
4.3 +=
+=有了前面两个函数的基础就很简单了:
string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* s){append(s);return *this;}
是的,没有了。
4.4 insert
insert有两个版本,一个是任意位置插入字符,一个是任意位置之前插入字符串,这里比较容易出现的问题是循环问题及斜杠0的问题,扩容问题同push_back就可以了,插入一个字符的时候,我们应该将该位置之后的字符全部往后移动一个单位,同理,插入一个字符串的时候,就应该移动字符串个的长度个单位,先来看第一种:
void insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t end = _size;while (end >= pos){_str[end + 1] = _str[end];--end;}_str[pos] = ch;++_size;
}
代码逻辑是没有什么大问题的,这里要注意的就是循环体,在平时,我们循环的元素类型都是int一类的,但是在string里面,size_t是很常见的,size_t不用多介绍吧,是无符号整型,即最小都是0,那么这个部分,除了头部的插入都是没有问题的,一旦是头插,那么在最后一步就会出岔子,因为pos是0,所以想要结束循环end就应该为-1,但是end不可能为-1,end = 0的下一步,减减的时候就会出问题,end会直接到42亿多(32位平台下),因为类型,好了,这里有人说把end改为int类型的不就好了,改了仍然会出问题,因为pos也是size_t类型的时候,比较的时候就会发生转换,即end还是会变为size_t类型的,那么这里的第一个解决方案就是把pos强转为int,为什么不在参数位置就设置为int呢?因为下标不可能为负数,所以:
int end = _size;
while (end >= (int)pos)
{_str[end + 1] = _str[end];--end;
}
这是第一种解决方式。
第二种解决方法是在end上面操作,即让end在0的时候结束就行了,这里思维稍微跳跃一点点:
size_t end = _size + 1;
while (end > pos)
{_str[end] = _str[end - 1];--end;
}
慢慢体会~
Tips:有了insert之后,前面的部分函数就可以进行复用了,比如,push_back ,append,所以说insert要放到最后实现呢:
void push_back(char ch)
{insert(_size, ch);
}
void append(const char* s)
{insert(_size, s);
}
4.5 erase
erase即删除,删除一段区间,当然这个区间长度也可以是1,文档记录的是如果没有给上参数,那么就是从该位置删完,所以要给上缺省值:
void erase(size_t pos, size_t len = npos)//删除一段区间
{assert(pos < _size);//可能存在溢出//npos - 1 + 未知数if (len == npos || len >= _size - pos){_str[pos] = '\0';}else{//while循环可以控制,但是有点麻烦strcpy(_str + pos, _str + pos + len);}_size -= len;
}
可以看到这里需要注意的问题有,向上溢出的问题,如果代码是len + pos >= _size,当pos是npos - 1,那么随便加个数就会导致向上溢出,这里的解决方法也是很简单,移项的事。
删除之后赋值,可以选择两种方法,一是直接使用strcpy,二是使用循环控制,将每个字符挨个挨个的存进去,当然比较麻烦,不太推荐。
当然,npos这里也是要自己声明,自己定义的。
5 string operations部分
上面实现了c_str,这里就实现find,substr即可。
5.1 find
find的重载可以找字符,也可以找字符串,所以需要模拟实现两个,模拟实现字符的函数简单,即匹配然后返回对应下标就可以了:
size_t find(char ch,size_t pos = 0)const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch)return i;}return npos;}
模拟实现字符串的返回下标有一点点难度,这里需要用到指针的运算,即指针相减 / 指针指向的类型是个数,这里也要复用到C语言的函数,strstr,使用字符串匹配,要是模拟实现strstr,就麻烦许多了:
size_t find(const char* sub, size_t pos = 0)const
{assert(pos < _size);//函数strstrconst char* p = strstr(_str + pos, sub);if (p){return p - _str;}else{return npos;}
}
5.2 substr
substr即是从pos位置开始,返回后面的字符串,返回字符串我们的第一个想法可能是返回该位置的指针,但是文档里面substr返回类型是string,所以我们也要返回string,这里就可以单独创建一个string变量,再复用+=,最后返回即可:
string substr(size_t pos = 0, size_t len = npos)
{string sub;//len + pos 向上溢出!!擦if (len >= _size - pos){for (size_t i = pos; i < _size; i++){sub += _str[i];}}else{for (size_t i = pos; i < pos + len; i++){sub += _str[i];}}return sub;
}
6 6个全局函数->比较
这里就和日期类是一样的,比较的时候我们只用实现一个,后面的所有比较函数复用前面的函数就可以了,这里的比较函数有 > < == != >= <=:
//几个全局函数
bool operator==(const string& s1,const string& s2)
{return !strcmp(s1.c_str(),s2.c_str());
}
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) || (s1 == 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);
}
比较的时候是按照字典序比较的,所以使用的函数是strcmp,
7 几个非成员函数
这里实现流插入和流重载以及swap。
7.1 swap
swap的使用是有讲究的,如果我们平时使用的时候使用std库里面的swap函数,那代价可就大了去了:
库里面的函数模板,使用的使用会自动识别类型,所以会发生拷贝构造,也会有析构的调用,一共是三次拷贝一次析构,但是对于内置类型的交换是很容易的,所以我们模拟实现string里面的交换的时候,就可以使用这个函数:
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
交换自定义类型无非就是交换空间,交换类里面的内置类型。
7.2 流插入
流插入很好实现,流提取的坑就比较多了:
ostream& operator<<(ostream& out, const string& s){out << s.c_str();return out;}
这里实现的流插入和日期类实现的流插入基本上是一样的。
7.3 流提取
代码先直接给上:
istream& operator>>(istream& in,string& s){s.clear();char ch = in.get();//保证空格能被读取char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
有如下要注意的点,
一是clear的调用,流提取本身是覆盖,所以我们需要把字符串原来的值给清除了,再进行提取。
二是提取是使用cin还是scanf还是?如果我们使用cin或者是scanf,就会导致空格读取不了,因为cin和scanf都是默认空格或换行都是字符串的结束标志,但是它们都是默认不读取的,所以这里使用的读取是io流里面的一个函数->get(),这个函数是可以读取空格或换行的,所以,使用这个函数。
三是为什么使用buff数组,因为我们不知道读取多少个字符,如果频繁调用insert或者其他函数会对效率影响挺大,插入扩容都是会消耗效率的,所以这里使用一个很奇妙的思路,使用一个数组,存满了清0重新存入即可,最后再给上斜杠0。
8 现代写法
string& operator=(string ss)
{swap(ss);return *this;
}
//现代写法 详细
string& operator=(const string& s)
{string ss(s);swap(ss);return *this;
}
string(const string& s)
{string tmp(s._str);swap(tmp);
}
赋值,使用swap,将我们需要的值给上一个临时变量,再把这临时变量的值交换给我们被赋值的变量,代码十分的简洁,并且nb。
拷贝构造同理可得,效率方面其实差别不大,但是这个思路很厉害,可以借鉴一下。
感谢阅读!