主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数等资源管理功能。
就是一个C语言中的字符串指针
class string
{
public:
private:
char* _str;
};
我们设计一个全缺省的默认构造函数,如果不传参时就默认存储\0,就是空串
class string
{
public:
//构造函数(错误写法)
string(char* str = '\0')
{
_str = str;
}
private:
char* _str;
};
上面的那个写法其实是错误的,当我们显示地给string对象初始值(字符串)时,这个字符串是存储在代码段的常量,只读不可写。不能进行修改操作的话这个string对象也就没意义了。
既然不能传存在代码段的常量字符串,那么我们传存储在栈上的字符串行不行?也不行,栈上的字符串是存储在字符数组里的,我们虽然可以修改但是不能扩容,因为数组定义出来时空间就是定死的,这样不方便我们对字符串进行资源管理。
既想要修改字符串内容又想随时扩容,那么把string对象的值放在堆上是最合适的。此时存储在堆空间上的字符串既可以像栈上的字符串一样修改,又可以随时通过new[ ]来开辟你想要大小的空间。
class string
{
public:
//构造函数(正确写法)
string(const char* str = '\0')//const char* str="" 两种写法一样
{
// 1.让_str指向我们在堆上开辟的空间(多开一个为了存储\0)
_str = new char [strlen(str) + 1];
// 2.把 str 内容拷贝到 _str(也就是把代码段的内容(str)拷贝到堆空间上(_str))
strcpy(_str, str);
}
private:
char* _str;
};
使用delete[ ]释放我们开辟的空间,再把_str置为nullptr(防止野指针)
class string
{
public:
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
这里一定要显示定义拷贝构造和赋值重载,如果用默认的会造成浅拷贝问题(多次释放同一块空间)
默认的拷贝构造和赋值重载都是通过浅拷贝实现的。浅拷贝就是一个字节一个字节的拷,浅拷贝也叫值拷贝
既然是指向同一块空间带来的问题,那我们就重新开辟一块同样大小的空间,利用strcpy把另一块空间的内容拷贝到新开辟空间上,这就是深拷贝。
拷贝构造
传统写法:
class string
{
public:
//拷贝构造
string(const string& s)
{
// 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
_str = new char[strlen(_str) + 1];
// 2.拷贝str空间的内容到_str指向的空间里
strcpy(_str, s._str);
}
private:
char* _str;
};
现代写法(更加简洁):
class string
{
public:
string(const string& s)
{
string tmp(s._str);
//这里的swap是c++提供的
//交换_str和tmp.str指向的空间
//出了函数tmp生命周期结束,自动调用析构函数释放tmp的空间(也就是原来s的空间)
swap(_str, tmp._str);
}
private:
char* _str;
};
赋值重载
赋值重载的两个对象都已经初始化过了,所以在把右值拷贝给左值前要先把左值的旧空间释放,在让它指向新空间
传统写法:
class string
{
public:
//赋值重载
string& operator=(const string& s)
{
if(this!=&s)
{
// 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
char* newstr = new char[strlen(s._str) + 1];
// 2.拷贝str空间的内容到newstr指向的空间里
strcpy(newstr, s._str);
// 3.释放旧的空间
delete[] _str;
// 4.让_str指向新开辟并且已经拷贝了值的空间
_str = newstr;
//返回
return *this
}
}
private:
char* _str;
};
现代写法:
class string
{
public:
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);//拷贝构造s
swap(_str, tmp._str);
}
return *this;
}
private:
char* _str;
};
赋值重载的几点说明:
除了字符数组(_str)外还加了 _size(记录当前有效字符个数),_capacity(记录可以存储多少个有效字符)和static常量npos(npos就是size_t类型的-1)
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
接下来我们介绍几个较复杂的接口
原型:void reserve (size_t n = 0);
作用:给字符串对象扩容,若n小于等于当前容量(_capacity)啥事没有;n大于当前容量就扩容
void reserve(size_t n=0)
{
if (n > _capacity)
{
char* newstr = new char[n+1];// 1.开新空间
strcpy(newstr, _str); // 2.拷贝旧空间
delete[] _str; // 3.释放旧空间
_str = newstr; // 4.指向新空间
_capacity = n; // 5.更新容量
}
}
原型:void resize (size_t n, char c=’\0’);
作用:将有效字符的个数改成n个,如果大于原size多出的有效空间用字符c填充,如果没有传字符c,那就默认是字符’ \0 ';如果小于原size就会截断多出来的有效字符。
void resize(size_t n, char c = '\0')
{
if (n > _size)
{
//检查是否需要扩容
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, c, n - _size);
_size = n;
_str[_size] = '\0';
}
else if(n<_size)
{
_size = n;
_str[_size] = '\0';
}
}
//简化后可以这样写
void resize(size_t n, char c = '\0')
{
if (n>_size)
{
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, c, n - _size);
}
_size = n;
_str[_size] = '\0';
}
如果要求的有效字符个数大于原来的size那么我们用memset来设置后面多出的有效空间,要注意的是memset是一个字节一个字节地拷贝,一般只在设置字符的时候才会用这个。
下面举个例子来说明这个问题:我们要把10容量的整形数组arr内容用memset设置为3
通过计算器也可以佐证我们的结果
按照一个字节一个字节来设置的就只适用于给字符数组来设置字符,因为一个字符的大小就是一个字节
原型:const char* c_str() const;
作用:返回C格式字符串,只可读不可写
就是直接返回成员变量_str,它的类型是char*
const char* c_str() const
{
return _str;
}
C格式字符串和string对象还是不同的,C格式字符串看’ \0 ‘,用cout输出时遇到’ \0 ‘就结束了;而string对象看的是它的有效字符个数(也就是_size),不管中间有没有’ \0 ’
原型:string substr (size_t pos = 0, size_t len = npos) const;
作用:在str中从pos下标开始,截取n个字符,然后将其返回
string substr(size_t pos = 0, size_t len = npos) const
{
//既然是子串,那下标必须合法
assert(pos < _size);
if (len > _size)
{
len = _size-pos;
}
char* tmp = new char[len + 1];// 1.开新空间(多开一个为了存储\0)
strncpy(tmp, _str + pos, len);// 2.拷贝子串到新空间
tmp[len] = '\0'; // 3.处理末尾的\0
string s_tmp(tmp); // 4.利用前面开的子串空间拷贝构造一个string对象
delete[] tmp; // 5.释放前面开的新空间
return s_tmp; // 6.返回拷贝构造的string类对象
}
原型:string& insert (size_t pos, const char* s);
作用:在pos位置插入一个字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
// 1.判断容量是否足够,不够的话需要增容
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 2.保证空间足够了,就开始挪动数据(一个字符一个字符的挪)
size_t end = _size;
while ((int)pos <= (int)end)
{
_str[end + len] = _str[end];
end--;
}
// 3.挪好之后,开始放数据
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
原型:string& erase (size_t pos = 0, size_t len = npos);
作用:删除 pos 下标后的 len 长度字符串
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
// 1.如果要求的长度大于等于后面的有效字符的长度,就是删除pos后面的所有有效字符
if (len >= _size - pos)
{
len = _size - pos;
resize(pos);
}
else// 2.删除的是中间一段的话,那就直接把前后拼接起来
{
strncpy(_str + pos, _str + pos + len, _size - pos - len + 1);
_size -= len;
}
return *this;
}
原型:ostream& operator<< (ostream& out, const string& s);
作用:string类的<<运算符重载
ostream& operator<<(ostream& out, const string& s)
{
int len = s.size();
// 把字符串的字符一个一个的输出,共输出size个
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
// 最后还要返回out,为了支持连续的<<操作
return out;
}
原型:istream& operator>> (istream& , string& s);
作用:重载string类的<<运算符
该运算符读取和C语言里的scanf一样,在读取字符串时不能读取到空格和回车,都是输入回车时算输入完毕。
istream& operator>>(istream& in, string& s)
{
while (1)
{
char c = in.get();// 从缓冲区接收数据,一个字符一个字符的接收
//如果遇到空格或者回车算接收完毕
if (c == ' ' || c == '\n')
{
break;
}
else// 否则把字符尾插到对象
{
s += c;
}
}
return in;
}
原型:istream& getline (istream& is, string& str);
作用:string类对象接收一行的数据(空格也可以接收)
相当于C语言的gets(),可以接收一行的数据
istream& getline(istream& in, string& s)
{
while (1)
{
char c = in.get();
if (c == '\n')// 从缓冲区接收数据,遇到回车才停止
{
break;
}
else
{
s += c;
}
}
return in;
}
对于string类而言,它的typedef就是char*(iterator要在类内的pubulic内声明),既然是char*那么就可以像C语言里面的指针一样使用了。
class string
{
public:
//string类的iterator
typedef char* iterator;
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
我们可以用iterator来遍历string类对象
void test_string()
{
string s("hello");
my_string::string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}