前言
本文将模拟实现string的一些常见功能,目的在于加深理解string与回顾类与对象的相关知识。
一、前置知识
- string是表示可变长的字符序列的类
- string的底层是使用动态顺序表存储的
- string对象不以’\0’字符为终止算长度,而是以size有效字符的个数算长度
- 为了兼容C,所以string对象在最后追加的一个’\0’字符,但是这个’\0’字符不属于string对象的有效字符
- 建议在模拟实现之前熟悉string的常用接口,并且查看文档。
二、string常用接口的模拟实现
1、string的成员变量
//我们模拟实现的string,将其封装在wjs的命名空间中,与库中的string区别开
namespace wjs
{class string{//成员变量private:size_t _size;//有效字符个数size_t _capacity;//存储有效字符的空间容量,注不包含'\0'char* _str;//指向堆申请的连续空间//静态成员变量public:const static size_t npos;//了解:const整数类型的静态成员可以在类内部初始化//static const size_t npos = -1;};//静态成员变量在类外部定义const size_t string::npos = -1;
}
tip:
- 命名空间
- 作用:使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突或命名污染
- 定义:namespace后面跟命名空间名字,然后接一对{}即可,{}中即为命名空间的成员
- 静态成员变量:
- 静态成员属于类为所有类对象共享,不属于某个具体的对象,存放在静态区
- 静态成员也是类的成员,受访问限定符的限制
- 一般静态成员变量不能在声明时给缺省值,因为缺省值是给初始化列表使用的,初始化列表是初始化对象的成员变量,而静态成员变量不属于类的任何一个对象
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。常与静态成员变量配套使用
- 在类外部定义静态成员
- 静态成员只要能突破类域和访问限定符就可以访问
- 了解:const整数类型的静态成员变量可以在类内部初始化,但是不建议。
- string的npos:
- size_t npos = -1,表示该类型的最大值
- 当string成员函数的参数的缺省值为npos时,表示“直到字符串结束”
- 当npos作为返回值时,一般表示没有匹配(例如:find)
2、构造函数
string类常用的构造函数有:
- string():默认构造函数,构造一个空的string对象,即空字符串。
- string(const char* str):用C格式字符串来构造一个string对象。
//默认构造函数——即可以传参构造,也可以使用缺省值构造
string(const char* str = "")//注意:初始化列表按照类中声明次序初始化,建议不要修改顺序,易错点!:_size(strlen(str)),_capacity(_size),_str(new char[_capacity + 1])//C字符串后默认以'\0'结束,为了兼容C所以要多开一个空间,保存'\0'
{//上面的new只是开了空间,所以需要把str拷贝到_str中//注意:strcpy拷贝到'\0'才结束strcpy(_str, str);
}
tip:
- 默认构造函数:
- 三种默认构造函数:无参构造函数、全缺省构造函数、编译器默认生成的构造函数
- 注意:默认构造函数只能存在一个——虽然语法上可以同时存在,但是无参调用时存在歧义,所以默认构造函数只能有一个
- 推荐使用全缺省默认构造函数——优点:即可以不传参使用缺省值初始化对象,也可以传参自己初始化对象
- 不传参就可以调用的就是默认构造函数
- 给成员变量赋初值的方式有:
- 使用初始化列表
- 使用构造函数的函数体
- 建议给成员变量赋初值都使用初始化列表,因为初始化列表是成员变量定义的地方
- 构造函数体与初始化列表结合使用,因为总有一些事情是初始化列表不能完成的。
- 初始化列表:
- 初始化列表是成员变量定义的地方,所以每一个成员变量在初始化列表中最多只能出现一次
- 引用成员变量、const成员变量、没有默认构造函数的自定义成员,必须在初始化列表初始化(因为这三种成员变量有一个共同特征,在定义时就必须初始化)
- 注意: 成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- string的底层存储是使用动态顺序表实现
- 在C++中使用new和delete操作符进行动态内存管理
- 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:配合使用
- C字符串后默认以’\0’结束,为了兼容C所以string要多开一个空间,保存’\0’
3、析构函数
//析构函数——有动态申请资源,需要显示实现析构释放资源
~string()
{delete[] _str;_str = nullptr;//delete与free一样释放完了,不会改变_str,所以将其置为空_size = _capacity = 0;
}
tip:
- 一般当对象涉及动态申请资源,就需要显示实现析构函数
- delete释放完空间之后与free一样,不会改变指向动态空间的指针变量,有危险,建议释放完之后将其置为空
4、拷贝构造函数
拷贝构造的浅拷贝
: 按字节序完成拷贝。
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成两个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象。
所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。
拷贝构造深拷贝
: 有自己的空间。
深拷贝是把值拷贝到自己的空间。
深浅拷贝就像我们平时考试时抄别人的作业,浅拷贝——名字、学号都抄别人的,深拷贝——知道修改名字、学号。
编译器默认生成的拷贝构造只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。
拷贝构造的深拷贝的两种实现:
- 传统写法:自己开空间,自己拷贝
- 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们。
传统写法:
//传统写法——自己开空间,自己拷贝
string(const string& s)//参数只有一个且必须是类类型对象的引用
{//自己开空间_str = new char[s._capacity + 1];//自己拷贝_size = s._size;_capacity = s._capacity;//不能使用strcmp拷贝,因为strcmp到'\0'结束,但是string不是以'\0'结束的//例如:hello\0xxxmemcpy(_str, s._str, s._size + 1);
}
现代写法:
//方式2:现代写法——先叫别人帮我们开好空间,拷贝好了,再把数据给我们。
//拷贝构造的现代写法对于hello\0xxxx的string有bug,所以我们使用传统写法
string(const string& s)//参数只有一个且必须是类类型对象的引用//注意:C++并没有规定对内置类型进行初始化,所以我们需要将其初始化:_size(0),_capacity(0),_str(nullptr)
{string tmp(s._str);//缺点:string不以'\0'字符结束swap(tmp);
}
tip:
- string不以’\0’字符为终止,所以拷贝构造的现代写法有bug
- 所以string的拷贝构造,我们不使用现代写法,使用传统写法
5、operator=赋值重载
赋值重载的浅拷贝
: 按字节序完成拷贝。
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成三个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象;③旧空间没释放,造成内存泄漏。
所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。
赋值重载的深拷贝
: 有自己的空间且要释放旧空间。
深拷贝是把值拷贝到自己的空间,并且将旧空间释放。
编译器默认生成的赋值重载只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。
赋值重载的深拷贝的两种实现:
- 传统写法:自己开空间,自己拷贝,自己释放旧空间。
- 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们,最后的旧空间也由别人帮我们释放。
传统写法:
//传统写法:自己开空间,自己拷贝,自己释放旧空间。
string& operator=(const string& s)
{//避免自己给自己赋值if (this != &s){//自己开空间char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);//自己释放旧空间delete[] _str;//自己拷贝_str = tmp;_capacity = s._capacity;_size = s._size;}return *this;
}
现代写法:
//现代写法:全部叫别人做,然后交给自己
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// string tmp(s);
// swap(tmp);
// //std::swap(tmp, *this);//swap的内部实现调用了operator=,所以会造成无限递归
// }
// return *this;
//}
//现代写法:进一步叫别人做,直接从参数开始
string& operator=(string tmp)
{swap(tmp);return *this;
}void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
tip:
- 现代写法:就是别人做好了,我们交换拿结果就可以了。
6、c_str成员函数获取C格式字符串
//获取C格式字符串
const char* c_str()const//内部不改变成员变量,建议加上const
{return _str;
}
tip:
- const成员
- 将const修饰的成员函数称为const成员函数
- 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用
- C格式字符串与string:
- C格式字符串不是一种类型,string是表示字符序列的一个类
- C字符串以’\0’字符为终止算长度,string不以’\0’字符为终止,以size为终止算长度(说明:C++为了兼容C,所以string对象的最后都追加了一个’\0’字符,但是这个’\0’不属于string对象的元素)
7、size成员函数获取string的长度
//获取string的长度
size_t size()const//内部不改变成员变量,建议加上const
{return _size;
}
tip:
- string的长度,即有效字符个数。
- 注意:string不以’\0’字符为终止算长度,是以有效字符的个数算长度。
8、capacity成员函数获取string当前空间容量
//获取string的当前空间容量
size_t capacity()const//内部不改变成员变量,建议加上const
{return _capacity;
}
tip:
- 说明:capacity不一定等于string的长度。他可以大于或等于。
9、reserve成员函数申请n个字符的空间容量
//申请n个字符的空间容量——只会改变容量,不会改变长度
void reserve(size_t n = 0)
{//如果n大于当前string容量,则按需申请n个字符的空间容量if (n > _capacity){//避免申请失败,先使用一个临时变量保存char* tmp = new char[n + 1];//不能使用strcmp拷贝,因为strcmp到'\0'结束,但是string不是以'\0'结束的//例如:hello\0xxxmemcpy(tmp, _str, _size + 1);//成功之后,将申请的新空间给string对象,旧空间释放delete[] _str;_str = tmp;tmp = nullptr;//注:reserve只改变容量,不改变长度_capacity = n;}
}
tip:
- reserve申请n个字符的空间容量:
- 如果n大于当前string容量,则申请扩容到n或比n大。
- 如果n小于当前string容量,则申请缩容,但是该申请是不具约束力的。
- 我们模拟实现的reserve,只有n大于当前string容量时,按需申请n个字符的空间容量,n小于当前string容量时不做处理。
- 注意:reserve只是单纯的开空间,所以reserve只会改变容量,不改变长度
10、resize成员函数调整string的长度
//调整string的长度——不仅会改变长度,还会改变容量
void resize(size_t n, char ch = '\0')//ch用于填值赋值,不传参使用缺省值
{//1、如果n小于string长度,删除n之后的字符,但不会缩容if (n < _size){//即只改变长度_size = n;//为了兼容C,string对象的最后都追加了一个'\0'_str[_size] = '\0';}else//如果n大于string长度,则扩容+填值赋值{//1、扩容——改变容量reserve(n);//2、填值赋值——改变长度for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;//为了兼容C,string对象的最后都追加了一个'\0'_str[_size] = '\0';}
}
tip:
- resize是将string的长度调整为n个字符的长度:
- 如果n大于string长度,则扩容+填值赋值(默认填’\0’)
- 如果n小于string长度,删除n之后的字符,但不会缩容
- 注意:当n大于当前string的长度时,resize既影响size也影响capacity。
11、clear成员函数清理string的有效字符
//清空string的有效字符——注:不会影响容量
void clear()
{_size = 0;_str[_size] = '\0';
}
tip:
- clear:清空string的有效字符,使之成为空字符串。
- 注意:clear不会影响容量。
12、empty成员函数判断字符串是否为空
//判断string是否为空
bool empty()const//内部不改变成员变量,建议加上const
{return _size == 0;
}
tip:
- 有效字符个数为空,string即为空。
13、operator[]成员函数返回pos位置字符的引用
//operator[]
//版本1:能读能写
char& operator[](size_t pos)
{//注意:operator[]越界断言assert(pos < _size);return _str[pos];
}
//版本2:只能读不能写
const char& operator[](size_t pos)const
{//注意:operator[]越界断言assert(pos < _size);return _str[pos];
}
tip:
- operator[]:
- operator[]越界是断言处理
- operator[]必须是成员函数
- operator[]通常定义两个版本:一个返回普通引用能读能写,一个返回常量引用只能读不能写
- 重载函数调用时,会走最匹配的,普通对象调用普通的,const对象调用const的
14、迭代器
//迭代器
// 在string中迭代器就是字符指针
//版本1:能读能写
typedef char* iterator;
iterator begin()
{//返回指向string第一个字符的指针return _str;
}
iterator end()
{//返回指向string尾字符的下一个位置的指针return _str + _size;
}
//版本2:只能读不能写
typedef const char* const_iterator;
const_iterator begin()const
{//返回指向string第一个字符的指针return _str;
}
const_iterator end()const
{//返回指向string尾字符的下一个位置的指针return _str + _size;
}
tip:
- 迭代器类似于指针类型,提供了对对象的间接访问,可以读写对象。在string中迭代器就是字符指针
- begin成员返回指向第一个字符的迭代器
- end成员返回指向尾字符的下一个位置的迭代器
- begin和end也有普通版本和const版本
- 有了迭代器,就可以使用范围for,因为范围for底层就是end和begin实现的(C++11)
15、insert成员函数在stringpos位置字符之前插入字符或字符串
//insert
//1、在string中pos指向的字符前插入n个字符ch
string& insert(size_t pos, size_t n, char ch)
{assert(pos <= _size);//1、插入之前判断是否扩容if (_size + n > _capacity){//至少扩容到_size + nreserve(_size + n);}//2、pos指向的字符前插入n个字符ch//①往后挪动size_t end = _size;//从'\0'开始while (end >= pos && end != npos){//向后挪动n个字符_str[end + n] = _str[end];//迭代end--;//特殊:当pos为0时,end=npos时,结束循环}//②插入for (size_t i = 0; i < n; i++){_str[i + pos] = ch;}_size += n;return *this;
}
//2、在string中pos指向的字符前插入C字符串
string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);//1、插入之前判断是否扩容if (_size + len > _capacity){//至少扩容到_size + lenreserve(_size + len);}//2、pos指向的字符前插入C字符串//①往后挪动size_t end = _size;while (end >= pos && end != npos){//向后挪动C字符串的长度_str[end + len] = _str[end];//迭代end--;//特殊:当pos为0时,end=npos时,结束循环}//②插入for (size_t i = 0; i < len; i++){_str[i + pos] = str[i];}_size += len;return *this;
}
tip:
- 首先断言pos位置是否合理
- 判断是否需要扩容
- 把[pos,size]区间的字符都往后挪动len
- len:len是插入字符的有效个数
- size:size位置是’\0’,‘\0’也挪动保证插入之后的string对象最后也有’\0’
- 插入:注意是从pos位置开始插入len个字符
- 注意:当循环变量的类型是size_t时,一定要注意边界0
- 当insert不是尾插时,需要挪动数据,效率低,所以一般很少使用
16、push_back成员函数在string后尾插一个字符
//push_back
//在string对象后尾插一个字符
void push_back(char ch)
{方式1:自己实现1、插入之前判断是否扩容//if (_size == _capacity)//{// //按2倍扩容// reserve(_capacity == 0 ? 4 : 2 * _capacity);//注意为空串的情况//}2、尾插ch//_str[_size] = ch;//_size++;//_str[_size] = '\0';// 方式2:复用insertinsert(_size, 1, ch);
}
tip:
- 方式1:自己实现
- 插入之前判断是否需要扩容
- 插入:直接尾插,插入之后需要注意:①有效字符个数+1;②string对象为了兼容C最后要加’\0’
- 方式2:复用insert
- string对象的尾插时间复杂度为O(1),效率高
17、append成员函数在string后追加C字符串或string对象
//append
//1、在string对象后追加C字符串
void append(const char* str)
{//方式1:自己实现//size_t len = strlen(str);1、插入之前判断是否扩容//if (_size + len > _capacity)//{// //至少扩容到_size + len// reserve(_size + len);//}2、尾插str//memcpy(_str + _size, str, len + 1);//_size += len;//方式2:复用insertinsert(_size, str);
}
//2、在string对象后追加string对象
void append(const string& str)
{size_t len = str._size;//1、插入之前判断是否扩容if (_size + len > _capacity){//至少扩容到_size + lenreserve(_size + len);}//2、尾插strmemcpy(_str + _size, str._str, len + 1);_size += len;
}
tip:
- 插入之间判断是否需要扩容
- 插入:使用memcpy拷贝即可,memcpy由我们自己控制拷贝多少字节,不是拷贝到’\0’就结束
- 插入之后记得更新string的有效字符个数
18、operator+=成员函数追加字符或字符串
//operator+=
string& operator+=(const string& str)
{append(str);return *this;
}
string& operator+=(const char* str)
{append(str);return *this;
}
string& operator+=(char ch)
{push_back(ch);return *this;
}
- string的operator+=的实现就是复用append和push_back
- 所以operator+=不仅可以追加单个字符,还可以追加字符串
- operator+=的使用相比push_buck和append更加人性化,所以一般我们更加喜欢使用operator+=
- operator+=
- 一般将复合赋值运算符重载定义为类的成员函数
- 为与内置类型的复合赋值一致,类的复合赋值运算符也要返回其左侧运算对象的引用
- 复合运算符都会影响左侧操作数,因为他们都会返回左侧操作数
- 自定义类型做函数返回值时,为了提高程序效率,能引用返回尽量引用返回
- 引用做返回值:
- 优点:①减少拷贝提高效率;②可以读写返回值
- 注意:当返回值出了函数体,不存在了,就不能用引用返回
19、erase成员函数从pos位置删除len个字符
//erase
//从pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{assert(pos < _size);//如果len=npos或pos+len>=size时,则从pos删除到string末尾if (len == npos || pos + len >= _size){_size = pos;_str[_size] = '\0';}else{//向前挪动数据size_t begin = pos + len;while (begin <= _size)//=size是为了把'\0'也挪动了{_str[pos++] = _str[begin++];}_size -= len;}return *this;
}
tip:
- 断言pos是否合理
- 如果当len为缺省值npos或len太大时,则把pos后的所有字符删掉
- 反之len+pos<size时,删除len个字符,即从pos+len向前挪动数据
- 删除之后记得更新有效字符个数
20、find成员函数从stringpos位置开始查找字符或字符串
//find
//1、从pos位置开始往后找字符ch
size_t find(char ch, size_t pos = 0)const//内部不改变成员变量,建议加上const
{assert(pos < _size);for (size_t i = pos; i < _size; i++){//找到返回该字符的位置if (_str[i] == ch){return i;}}//找不到,返回nposreturn npos;
}
//2、从pos位置开始往后找字符串str
size_t find(const char* str, size_t pos = 0)const//内部不改变成员变量,建议加上const
{assert(pos < _size);//strstr找到返回子串位置,找不到返回nullconst char* ret = strstr(_str, str);if (ret){return ret - _str;//后一个元素的下标等于前面的元素个数}else{return npos;}
}
tip:
- find:从string对象pos位置开始往后找字符或字符串,找到则返回该字符或字符串在string中第一次出现的位置,找不到返回npos
- 注意断言pos位置是否合理
- 指针-指针:
- 前提:两个指针要指向同一块空间
- 作用:得到两个指针之间的元素个数
21、substr成员函数获取string的子串
//substr
//获取string对象的子串,子串从pos开始,截取len个字符
string substr(size_t pos = 0, size_t len = npos)const//内部不改变成员变量,建议加上const
{assert(pos < _size);//避免多次扩容,算出子串大小,一次reservesize_t n = len;if (len == npos || len >= _size){n = _size - len;}string tmp;tmp.reserve(n);//拷贝子串for (size_t i = pos; i < pos + n; i++)//注意:结束条件为pos+n{tmp += _str[i];}return tmp;
}
tip:
- 断言pos位置是否合理
- 避免多次扩容,算出子串的大小,一次reserve
- 从pos位置拷贝子串
22、operator<<非成员函数输出string对象
//operator<<
//1、ostream必须引用
//2、必须在类外定义
ostream& operator<<(ostream& out, const string& s)
{for (auto ch : s){out << ch;}return out;
}
- operator<<必须是非成员函数:
- 因为成员函数的左操作数是隐含的this,所以operator<<必须是非成员函数
- ostream不允许拷贝构造,所以ostream对象必须引用
- 范围for的底层是迭代器,所以只要实现了迭代器就可以直接使用
- <<运算符从左向右结合,可以连续打印,所以要返回ostream
23、operator>>非成员函数输入string对象
//operator>>
istream& operator>>(istream& in, string& s)
{//每次输入前清空字符串,避免追加s.clear();//多个数值用换行或空格分割,所以cin不会读取换行和空格//所以istream提供了一个成员函数get,读取每一个字符char ch = in.get();//处理前缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}//读取数据char buff[128];//避免多次扩容,先把数据读到buff数组,数组满了和读取结束了,将数组中的数据给string对象size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;//数组满了if (i == 127){buff[i] = '\0';s += buff;//追加字符串i = 0;}ch = in.get();}//读取结束了if (i != 0){buff[i] = '\0';s += buff;}return in;
}
- operator>>必须是非成员函数:
- 因为成员函数的左操作数是隐含的this,所以operator>>必须是非成员函数
- istream不允许拷贝构造,所以istream对象必须引用
- string的operator<<实现:
- 读取之前,需要清空string对象
- 处理前缓存区的空格和换行
- 避免多次扩容,创建一个局部数组,先保存读取的数据,再将数组的数据追加到string对象
- <<运算符也是可以连续读取的,所以需要返回istream
24、关系运算重载非成员函数比较string对象
//关系比较
//①先实现operator==和operator<
//②其余利用他们之间的互斥关系复用
//operator<的方式1:自己实现
//bool operator<(const string& s1, const string& s2)
//{
// //对应位置的字符比较
// size_t i = 0;
// size_t j = 0;
// size_t len1 = s1.size();
// size_t len2 = s2.size();
// while (i < len1 && j < len2)
// {
// if (s1[i] > s2[j])
// {
// return false;
// }
// else if (s1[i] < s2[j])
// {
// return true;
// }
// else
// {
// //迭代
// i++;
// j++;
// }
// }
// // "hello" "hello" false
// // "helloxx" "hello" false
// // "hello" "helloxx" true
// //即只有s1的长度小于s2时,才为真
// return len1 < len2;
//}
//operator<的方式2:复用memcmp
bool operator<(const string& s1, const string& s2)
{//对应位置的字符比较size_t len1 = s1.size();size_t len2 = s2.size();int ret = memcmp(s1.c_str(), s2.c_str(), len1 < len2 ? len1 : len2);//ret<0为真// "hello" "hello" false// "helloxx" "hello" false// "hello" "helloxx" true//当ret = 0时,s1的长度小于s2时也为真return ret == 0 ? len1 < len2 : ret < 0;
}bool operator==(const string& s1, const string& s2)
{size_t len1 = s1.size();size_t len2 = s2.size();return len1 == len2&& memcmp(s1.c_str(), s2.c_str(), len1) == 0;
}bool operator<=(const string& s1, const string& s2)
{return s1 < s2 || s1 == s2;
}bool operator>(const string& s1, const string& 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);
}
- string对象的关系运算:
- 比较对应字符的ASCII码值,如果相等,则继续比较,直到出现不同的字符
- 特殊:当其中一个string对象比较完之后都相等,这个时候比较两个string对象的长度
- 关系运算重载的实现:
- 先实现<和==( 或>和==)
- 其余利用他们之间的互斥直接复用