🔥个人主页: Forcible Bug Maker
🔥专栏: STL || C++
目录
前言
本篇博客主要内容:实现string类的基本功能。
string使用很快就讲完了,但是光会用string还不够,在面试中,面试官总喜欢让我们自己来模拟实现string类,包括string类的构造、拷贝构造、赋值运算符重载以及析构函数等等内容。所以,我认为string类的自实现还是有必要讲一下的。
🔥string类的接口函数
我们本次并不会将string类的所有接口函数逐一讲解,讲一些常用的和重点的。本次string的实现分成了两个文件,一份是
string.h
,一份
string.cpp
。
看看string.h的内容:
#define_CRT_SECURE_NO_WARNINGS1#pragmaonce#include<iostream>#include<cstring>#include<cassert>using std::cout;using std::cin;using std::endl;namespace ForcibleBugMaker
{classstring{public:// 交换,非成员函数friendvoidswap(string& s1, string s2);// 定义迭代器typedefchar* iterator;typedefconstchar* const_iterator;//迭代器获取
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;// string默认成员函数string(constchar* str ="");string(const string& s);
string&operator=(string tmp);~string();// 获取只读字符串constchar*c_str()const;// 获取容量
size_t size()const;
size_t capaity()const;// []获取元素重载char&operator[](size_t pos);constchar&operator[](size_t pos)const;// 开辟空间voidreserve(size_t n);// 尾插字符或字符串voidpush_back(char ch);voidappend(constchar* str);
string&operator+=(char ch);
string&operator+=(constchar* str);// 插入字符或字符串voidinsert(size_t pos,char ch);voidinsert(size_t pos,constchar* str);//删除字符串voiderase(size_t pos =0, size_t len = npos);// 查找字符或字串
size_t find(char ch, size_t pos =0);
size_t find(constchar* str, size_t pos =0);// string对象比较booloperator>(const string& str)const;booloperator==(const string& str)const;booloperator>=(const string& str)const;booloperator<(const string& str)const;booloperator<=(const string& str)const;booloperator!=(const string& str)const;// 获取string对象字串
string substr(size_t pos, size_t len);// 交换函数,成员函数voidswap(string& str);// 清除串中内容voidclear();private:char* _str =nullptr;
size_t _size =0;
size_t _capacity =0;// 常量成员conststatic size_t npos;};// 交换函数,非成员函数voidswap(string& s1, string s2);// 流插入和流提取重载,非成员函数
std::ostream&operator<<(std::ostream& so,const string& str);
std::istream&operator>>(std::istream& is, string& str);}
在实现string类添加和拷贝的一些函数中,使用了C语言中的一些库函数,以此方便实现,这些函数在C++中统一存放在
<cstring>
。
如果对C语言的一些字符串函数不太了解,可以看看我之前写的一篇博客:C语言-字符串函数,相信会对你有所帮助。
🔥string类的模拟实现
接下来进入主要内容,按照string.h的接口开始实现。
以下接口都是放在命名空间里的,不同文件的相同命名空间在编译时会自动合并。
swap交换
如果你仔细观察,会发现存在两个swap交换函数,一个string中的成员函数,另一个是非成员函数。
在std的默认swap当中,是这样的:
template<classT>voidswap( T& a, T& b ){
T c(a); a=b; b=c;}
当我们使用这个成员函数进行交换时,会造成拷贝消耗,我们提供对应的非成员函数重载是为了防止C++程序员掉坑。
成员函数的swap:
void string::swap(string& str){
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);}
非成员函数的swap:
voidswap(string& s1, string& s2){
std::swap(s1._str, s2._str);
std::swap(s1._size, s2._size);
std::swap(s1._capacity, s2._capacity);}
默认成员函数
默认成员函数,就是不提供编译器自动会生成的一些函数。我们这里实现构造函数,析构函数和赋值运算符重载。
构造函数:
string::string(constchar* str):_size(strlen(str)){
_capacity = _size +1;
_str =newchar[_capacity +1];strcpy(_str, str);}
string::string(const string& s){
_str =newchar[s._capacity +1];strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;}
构造函数提供了两个,分别支持了字符串构造和string对象构造。_str指向通过new开辟空间,这个空间需要比实际的capacity大,因为需要在字符串末尾多存放一个
'\0'
。
析构函数:
string::~string(){delete[] _str;
_str =nullptr;
_capacity =0;
_size =0;}
这个没什么好说,释放空间,指针置空,_size和_capacity置0。
赋值运算符重载:
string的赋值属于深拷贝。
拷贝分为深拷贝和浅拷贝:
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
浅拷贝:
深拷贝:
如果中规中矩的来写(传统版赋值重载):
String&operator=(const String& s){if(this!=&s){char* pStr =newchar[strlen(s._str)+1];strcpy(pStr, s._str);delete[] _str;
_str = pStr;}return*this;}
但如果你能对之前实现的代码进行复用,你会发现这个过程可以简化非常多(现代版赋值重载):
string& string::operator=(string tmp){// 调用成员函数的swap,交换*this和tmpswap(tmp);return*this;}
在C++的编写中,学会复很有必要。
迭代器
之前讲过,迭代器不一定是指针,但是你可以把它想象成指针,可以通过正常的运算操作来控制其指向的元素。在string中,我们使用指针来模拟实现迭代器。
迭代器在不同编译器下的实现方式有所不同,比如VS下的迭代器就不是一个指针,而是一个类模板。
在类中需要typedef一下,可以看到我们所实现的string迭代器的本质:
// 定义迭代器typedefchar* iterator;typedefconstchar* const_iterator;
获取迭代器的接口:
string::iterator string::begin(){return _str;}
string::iterator string::end(){return _str + _size;}
string::const_iterator string::begin()const{return _str;}
string::const_iterator string::end()const{return _str + _size;}
这时,我们已经可以使用我们自己的迭代器了:
string str("hello world!");
string::iterator it = str.begin();while(it != str.end()){
cout <<*it <<" ";++it;}
cout << endl;
获取容量和内容信息
这些都是返回str类型状态和内容的函数。
constchar* string::c_str()const{return _str;}
size_t string::size()const{return _size;}
size_t string::capaity()const{return _capacity;}// 运算符重载char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}constchar& string::operator[](size_t pos)const{assert(pos < _size);return _str[pos];}
都比较简单易懂。
reserve预留空间
在开始字符串增删查改之前,有必要介绍这个函数,使用它可以更好的控制我们string对象的内存管理。
void string::reserve(size_t n){if(_capacity < n){char* tmp =newchar[n +1];strcpy(tmp, _str);delete[] _str;
_str = tmp;
_capacity = n;}}
当n大于当前容量_capacity的时候,进行空间的开辟,将_str中的内容拷贝到新空间中去,同时delete释放旧空间。
尾插字符和字符串
接口函数,一个是push_back,一个是append。
void string::push_back(char ch){if(_size >= _capacity){reserve(_capacity ==0?4: _capacity *2);}
_str[_size]= ch;++_size;
_str[_size]='\0';}void string::append(constchar* str){
size_t len =strlen(str);if(_capacity < _size + len){reserve(_size + len);}strcpy(_str + _size, str);
_size += len;}
这两个成员函数都使用了reserve来预留空间。
同时还需要有 运算符重载+= 来实现尾插,用重载的运算符执行这样的操作才是最爽的。
string& string::operator+=(char ch){push_back(ch);return*this;}
string& string::operator+=(constchar* str){append(str);return*this;}
其实不用再实现一遍了,直接复用就行。
可以简单看一下使用的效果:
string str("hello world!");char ch ='T';constchar* s ="hhhhhh";
cout << str << endl;
str += ch;
cout << str << endl;
str += s;
cout << str << endl;
字符或字符串的插入和删除
这里的逻辑稍微有些复杂,而且还有几个比较容易掉的坑。
// 字符的插入void string::insert(size_t pos,char ch){assert(pos <= _size);if(_size >= _capacity){reserve(_capacity ==0?4: _capacity *2);}// 注意这里为什么要+1,size_t是无符号整型,没有负值//当有符号和无符号比较时,统一会被转成无符号
size_t end = _size +1;while(end > pos){
_str[end]= _str[end -1];--end;}
_str[pos]= ch;++_size;}// 字符串的插入void string::insert(size_t pos,constchar* str){assert(pos <= _size);
size_t len =strlen(str);if(len ==0)return;if(_size + len > _capacity){reserve(_size + len);}// 注意这里为什么要+len,size_t是无符号整型,没有负值//当有符号和无符号比较时,统一会被转成无符号
size_t end = _size + len;while(end > pos + len -1){
_str[end]= _str[end - len];--end;}memcpy(_str + pos, str, len);
_size += len;}// 字符串的删除void string::erase(size_t pos, size_t len){assert(pos < _size);// 当pos+len过大,超过_size,则取到末尾if(len >= _size - pos){
_str[pos]='\0';
_size = pos;}else{strcpy(_str + pos, _str + pos + len);
_size -= len;}}
使用案例:
string str("hello world!");char ch ='T';constchar* s ="hhhhhh";
cout << str << endl;
str.insert(3, ch);
cout << str << endl;
str.insert(3, s);
cout << str << endl;
str.erase(3,strlen(s));
cout << str << endl;
find查找
查找C语言查找字符串的库也有提供,可以直接使用。
size_t string::find(char ch, size_t pos){for(size_t i = pos; i < _size; i++){if(_str[i]== ch)return i;}return npos;}
size_t string::find(constchar* sub, size_t pos){constchar* ptr =strstr(_str, sub);return ptr - _str;}
使用案例:
string str("hello world!");
cout << str.find('w')<< endl;
cout << str.find("wor")<< endl;
缺省参数在函数声明那里。
比大小运算符重载
C语言中,有一个按能按字典序将字符串比大小的函数——
strcmp
。
bool string::operator>(const string& str)const{returnstrcmp(_str, str._str)>0;}bool string::operator==(const string& str)const{returnstrcmp(_str, str._str)==0;}bool string::operator>=(const string& str)const{return*this> str ||*this== str;}bool string::operator<(const string& str)const{return!(*this>= str);}bool string::operator<=(const string& str)const{return!(*this> str);}bool string::operator!=(const string& str)const{return!(*this== str);}
只需要实现前两个,后面的复用就行。
获取子串
使用substr可以获取所需对象字串。
string string::substr(size_t pos, size_t len){if(pos + len >= _size){
string sub(_str + pos);return sub;}else{
string sub;
sub.reserve(len);for(size_t i =0; i < len; i++){
sub += _str[pos + i];}return sub;}}
清除clear
可以将对象内容都删除,但_capacity保持不变。
void string::clear(){
_str[0]='\0';
_size =0;}
流插入和流提取
这两个函数之前在Date类部分实现的时候,定义为了Date类的友元。但是,流插入和流提取其实可以不定义为string类的友元。
// 流插入
std::ostream&operator<<(std::ostream& os,const string& str){for(int i =0; i < str.size(); i++)
os << str[i];return os;}// 流提取
std::istream&operator>>(std::istream& is, string& str){
str.clear();char ch = is.get();while(ch !=' '&& ch !='\n'){
str += ch;
ch = is.get();}return is;}
string对象读取的截断是空格和换行,但是
cin
对象默认是读不到这两个字符的,这就需要使用另一种读取方式:
cin.get()
。
使用案例:
string str;
cin >> str;
cout << str << endl;
常量成员npos
常量成员的定义需要实现在类的外部,像这样:
const size_t string::npos =-1;
当常量的内容是整型(浮点型和字符类型等都不行)时,也可以作为缺省参数进行定义。
结语
本篇博客主要介绍了string类常用接口的实现,包括默认成员函数,迭代器,字符和字符串的插入删除等等内容。
后续博主还会继续分享与STL相关的内容,感谢大家的支持。♥
版权归原作者 Forcible Bug Maker 所有, 如有侵权,请联系我们删除。