一、成员变量
private:
char *_str;
size_t _size;
size_t _capacity;
public:
static size_t npos = -1; //编译报错,不能在类中初始化
const static size_t npos = -1; //[1]
const char* c_str() const{ //[2]
return _str;
}
size_t size() const{
return _size;
}
size_t capacity() const{
return _capacity;
}
解释:
- [1] static静态变量需要在类中声明,类外定义
- [1] 特殊语法:const static成员可以在类中直接定义初始化
- [1] npos的值是无符号整形的最大值;1.在删除、截取等操作中表示取到字符串末尾;2.find中表示找不到该字符、字符串
- [2] const修饰成员函数,即修饰this指针。使const对象也可以调用该成员函数。
- [2] 指针、引用做函数参数,如果不在函数内修改引用对象的值最好要加const修饰。使函数兼容const对象。
二、四个默认成员函数
2.1 构造
Mystring(const char *str = "") //[1]
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity+1]; //[2]
strcpy(_str, str);
}
解释:
- [1] 空串""后有隐式的 ‘\0’ 结尾,空串做缺省值strlen值为0,开1字符空间用于存储’\0’,符合缺省要求。
- [2] _capacity表示最多容纳的有效字符个数(不包括’\0’),多开1字符空间用于存储’\0’。
2.2 拷贝构造
//传统写法:传统做法直接进行数据拷贝。
Mystring(const Mystring &str){
_size = str._size;
_capacity = str._capacity;
_str = new char[_capacity+1];
memcpy(_str, str._str, str._size+1); //[1]
}
//现代写法:(有问题)
//复用构造函数;在函数内用目标对象的字符串构造临时对象,与调用对象做浅交换,
//函数返回前析构临时对象(原调用对象数据),而调用对象得到拷贝数据。
Mystring(const Mystring &str)
:_str(nullptr), //[2]
_size(0),
_capacity(0)
{
Mystring tmp(str._str); //[3]
swap(tmp);
}
void swap(Mystring &str){
::swap(_str, str._str); //[4]
::swap(_size, str._size);
::swap(_capacity, str._capacity);
}
解释:
- [1] 此处不能用strcpy,因为string字符串的拷贝不以’\0’结束。
- [1] 拷贝字节数为_size+1,包括结尾的’\0’。
- [2] 必须将_str初始化为nullptr;否则交换后,析构临时对象时会释放随机指针而导致程序崩溃。
- [3] 这里使用目标对象的字符串构造临时对象的过程中遇到’\0’结束拷贝,实际上是存在问题的。
- [3] 但这种现代写法用在其他容器的实现中会使代码更加简洁。
- [4] 这里调用的是库里的全局swap函数,只对指针_str做浅交换。
2.3 operator=
//传统写法:传统做法直接进行数据拷贝。
Mystring& operator=(const Mystring &str){
if(this != &str) //[1]
{
char *tmp = new char[str._capacity+1]; //[2]
memcpy(tmp, str._str, str._size+1);
delete[] _str; //[3]
_str = tmp;
_size = str._size;
_capacity = str._capacity;
}
return *this; //[4]
}
//现代写法1:(有问题)
//复用构造函数;在函数内用目标对象的字符串构造临时对象,与调用对象做浅交换,
//函数返回前析构临时对象(原调用对象数据),而调用对象得到拷贝数据。
Mystring& operator=(const Mystring &str)
{
if(this != &str)
{
Mystring tmp(str._str); //[5]
swap(tmp);
}
return *this;
}
//现代写法2:(正确)
//复用拷贝构造函数;在函数内用目标对象拷贝构造临时对象,与调用对象做浅交换,
//函数返回前析构临时对象(原调用对象数据),而调用对象得到拷贝数据。
Mystring& operator=(Mystring str){ //[6]
swap(str);
return *this;
}
解释:
- [1] 比较两个对象的指针,避免出现自己给自己赋值而出错的情况
- [2] 先开新空间拷贝数据,再释放旧空间;防止出现开空间失败而造成原数据丢失的情况。
- [3] 必须释放旧空间,防止内存泄漏;匹配使用new T[N]和delete[]
- [4] 返回调用对象的引用,使运算符支持连续赋值。
- [5] 同上,调构造函数遇到’\0’结束拷贝,有问题。
- [6] 参数是一个string对象,调用函数时先用实参拷贝构造形参。因此现代写法2复用拷贝构造函数,正确。
2.4 析构
~Mystring(){
delete[] _str;
}
注意:匹配使用new T[N]和delete[],否则程序运行崩溃。
三、访问遍历
3.1 operator[ ]
char& operator[](size_t pos){
assert(pos<_size); //[1]
return _str[pos];
}
const char& operator[](size_t pos) const{ //[2]
assert(pos<_size);
return _str[pos];
}
解释:
- 一定要检查pos的范围,防止越界访问。
- const对象也要提供下标访问方法,const对象返回const引用。
3.2 iterator
typedef char *iterator; //[1]
typedef const char *const_iterator; //[2]
iterator begin(){
return _str;
}
const_iterator begin() const{
return _str;
}
iterator end(){
return _str+_size; //[3]
}
const_iterator end() const{
return _str+_size;
}
解释:
- [1] 对于string类,迭代器实际就是原生指针。
- [2] const对象也要提供迭代器访问方法,const对象返回const指针。
- [3] begin()返回首元素的地址;end()返回尾元素下一个位置的地址。
四、容量操作
reserve
void reserve(size_t newcapacity){
if(newcapacity > _capacity) //[1]
{
char *tmp = new char[newcapacity+1]; //[2]
memcpy(tmp, _str, _size+1); //[3]
delete[] _str;
_str = tmp;
_capacity = newcapacity;
}
}
解释:
- [1] 只有指定的新容量>旧容量时才会进行扩容
- [2] 要多开1字符空间,用于存储’\0’;newcapacity的大小不包括’\0’。
- [3] 不能使用strcpy,string对象不以’\0’为结束标志。拷贝字节数为_size+1,包括结尾的’\0’。
resize
void resize(size_t newsize, char ch = '\0'){ //[1]
if(newsize > _size) //[2]
{
reserve(newsize); //[3]
for(size_t i = _size; i<newsize; ++i) //[4]
{
_str[i] = ch;
}
}
_str[newsize] = '\0'; //[5]
_size = newsize;
}
解释:
- [1] 第二个参数缺省,默认以’\0’初始化字符串。
- [2] 只有当newsize>_size时才会可能进行扩容和初始化赋值操作。
- [3] 只有当newsize>_capacity时才会进行扩容。
- [4] 遍历从_size开始,保证字符串原有的数据保持不变;新增的字符用指定的字符或’\0’进行初始化。
- [5] 字符串最后一个位置以’\0’结尾。如果newsize<_size则截断原字符串。
clear
void clear(){
_str[0] = '\0';
_size = 0;
}
五、增删查改
push_back;
void push_back(char ch){
if(_size == _capacity) //[1]
{
size_t newcapacity = _capacity == 0? 5:2*_capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0'; //[2]
}
解释:
- [1] 判满扩容
- [2] 字符串以’\0’结尾,多开的1字符空间用于存储’\0’
append
void append(const char *str){
size_t newsize = _size+strlen(str);
reserve(newsize);
strcpy(_str+_size, str);
_size=newsize;
}
void append(const Mystring &str){
size_t newsize = _size+str._size;
reserve(newsize);
memcpy(_str+_size, str._str, str._size+1);
_size=newsize;
}
void append(size_t n, char ch){
reserve(_size+n);
for(size_t i = 0; i<n; ++i)
{
push_back(ch);
}
}
解释:
- 对比尾插常量字符串和string对象可以明显看出二者的区别:常量字符串以’\0’为结束标志;而string字符串以_size为结束标志。
operator+=
Mystring& operator+=(char ch){
push_back(ch);
return *this;
}
Mystring& operator+=(const char *str){
append(str);
return *this;
}
Mystring& operator+=(const Mystring &str){
append(str);
return *this;
}
解释:
- operator+= 复用 push_back 和 append函数。
insert
Mystring& insert(size_t pos, char ch){
assert(pos <= _size); //[1]
if(_size == _capacity)
{
size_t newcapacity = _capacity == 0? 5:_capacity*2;
reserve(newcapacity);
}
size_t end = _size+1; //[2]
while(end > pos)
{
_str[end] = _str[end-1];
end--;
}
_str[pos] = ch;
++_size;
return *this;
}
Mystring& insert(size_t pos, const char* str){
assert(pos <= _size);
size_t len = strlen(str);
if(len == 0) //[3]
{
return *this;
}
reserve(len + _size);
size_t end = _size+len; //[4]
while(end >= pos+len)
{
_str[end] = _str[end-len];
end--;
}
strncpy(_str+pos,str,len); //[5]
_size += len;
return *this;
}
解释:
-
[1] 检查pos的范围,防止越界。注意:pos==_size表示尾插
-
[2] 从后往前挪数据,包括’\0’;
-
[2] end的移动范围是(pos, size+1] 等于pos结束,不能是[pos,size]小于pos结束
-
[2] 由于pos和end是size_t无符号整形,所以当pos==0时(end<0结束)会造成死循环,越界访问。
-
[3] 如果插入的是空串不做任何处理直接返回。否则会造成资源的浪费,且当pos == 0时程序崩溃。
-
[4] 如图,在字符a后插入b,c,d:
其中红色表示end的移动范围;蓝色表示待插入的位置;
-
[5] 不能使用strcpy,因为strcpy会将结尾的’\0’也拷贝进去。
-
[5] 使用strncpy,从pos位置向后拷贝len个字符。
erase
void erase(size_t pos, size_t len = npos){
assert(pos < _size);
if(len == npos || pos+len >= _size) //[1]
{
_str[pos] = '\0';
_size = pos;
}
else{
strcpy(_str+pos, _str+pos+len); //[2]
_size-=len;
}
}
解释:
- 如果缺省第二个参数或者给定的长度大于字符串剩余长度,默认将字符串截断到pos
- 起始位置+长度 = 结束位置的下一个位置。
find
size_t find(char ch, size_t pos = 0) const{
assert(pos < _size);
for(size_t i = pos; i<_size; ++i) //[1]
{
if(_str[i] == ch)
{
return i;
}
}
return npos; //[2]
}
size_t find(const char *sub, size_t pos = 0) const{
assert(sub!=nullptr);
assert(pos < _size);
size_t len = strlen(sub);
for(size_t i = pos; i < _size; ++i)
{
if(_str[i] == sub[0])
{
size_t j = 1;
for(; j<len && i+j<_size; ++j) //[3]
{
if(_str[i+j] != sub[j])
{
break;
}
}
if(j == len) //[4]
{
return i;
}
}
}
return npos;
}
解释:
- [1] 从pos位置开始向后查找。如果pos缺省,默认从0开始。
- [2] 如果找不到,返回npos
- [3] 内循环的过程中,不仅要保证j在sub的范围内,还要保证i+j在_str的范围内,防止越界访问。
- [4] 内循环结束后,如果j == len表示sub子串连续完全匹配,即可返回起始位置i,否则++i继续查找。
substr
string substr(size_t pos, size_t len = npos) const{
assert(pos < _size);
if(len == npos || pos+len > _size)
{
len = _size-pos; //[1]
}
string sub;
for(size_t i = 0; i<len; ++i)
{
sub+=_str[pos+i];
}
return sub;
}
解释:
- [1] 如果缺省第二个参数或者给定的长度大于字符串剩余长度,默认取到字符串结尾。
- [1] 万能公式:起始位置+长度 = 结束位置的下一个位置。
六、字符串比较
operator==
bool operator==(const Mystring &str) const{
if(_size != str._size)
return false;
for(size_t i = 0; i<_size; ++i)
{
if(_str[i] != str._str[i])
{
return false;
}
}
return true;
}
operator>
bool operator>(const Mystring &str) const{
size_t i = 0;
while(i < _size && i < str._size)
{
if(_str[i] != str._str[i]) //[1]
{
return _str[i] > str._str[i];
}
++i;
}
if(i < _size) //[2]
{
return true;
}
else{
return false;
}
}
解释:
- [1] 字符串比较大小即比较第一个不同字符的ASCII码的大小
- [2] 如果所含字符全都相同,哪个字符串长哪个就比较大。
其他关系运算符重载
bool operator!=(const Mystring &str) const{
return !(*this==str);
}
bool operator>=(const Mystring &str) const{
return (*this>str) || (*this==str);
}
bool operator<(const Mystring &str) const{
return !(*this>=str);
}
bool operator<=(const Mystring &str) const{
return !(*this>str);
}
提示:只需要自己实现出>, == 或 <, ==的运算符重载,其他的关系运算符根据逻辑关系复用即可。
七、输入输出
operator<<
ostream& operator<<(ostream& out, const Mystring &str){
for(size_t i = 0; i<str.size(); ++i)
{
out << str[i];
}
return out;
}
注意:string字符串不以’\0’为字符串结束的标志,以_size为结束的标志。
operator>>
//优化前:
istream& operator>>(istream& in, Mystring &str){
str.clear(); //[1]
char ch;
ch = in.get(); //[2]
while(ch != ' ' && ch != '\n')
{
str+=ch;
ch = in.get();
}
return in; //[3]
}
解释:
- [1] 输入是赋值操作,要覆盖原数据:即清空字符串从头插入字符。
- [2] 不能使用cint >> ch,因为普通的输入会将空格和换行当做输入分割,不会录入到变量中。
- [3] 返回标准输入流对象的引用,使符号>>支持连续输入。
//优化后:
istream& operator>>(istream& in, Mystring &str){
str.clear();
const size_t N = 32;
char buff[N+1]; //[1]
size_t i = 0;
char ch;
ch = in.get();
while(ch != ' ' && ch != '\n')
{
buff[i++] = ch; //[2]
if(i == N)
{
buff[i] = '\0'; //最后一个位置存储'\0'
str+=buff;
i = 0; //刷新缓冲区,从头开始存储数据
}
ch = in.get();
}
if(i!=0)
{
buff[i] = '\0'; //[3]
str+=buff;
}
return in;
}
解释:
- [1] 在栈上开辟可以容纳N个有效字符的缓冲区,多开辟1字符空间用于存储’\0’。
- [2] 先将输入的字符插入到缓冲区中,待缓冲区满再一次性插入到string对象中,这样可以避免频繁的扩容操作。
- [3] 如果输入的字符数量不是N的整数倍,就需要在最后将缓冲区中的数据插入到string对象中。