我们首先看看几个典型的错误。

这里使用初始化列表进行构造函数的初始化,str本身是const类型,而初始化列表又将str赋值给了_str,所以此时就无法对str进行接下来string类的增删查改操作(只有查可以)。

这里错误原因是初始化顺序跟声明顺序有关,先声明_str,所以第一步先初始化_str,而我们为了不用每次都调用strlen函数,而是调用了_size,而我们先调用了_str,此时的_size还没有初始化,所以会报错!这是一个大坑!!!改个顺序就能报错的大坑!
综上我们可以看出string的构造不适合用初始化列表,因此我们改用普通构造函数,大不了我们定义的时候不初始化,其实对于string这个类是没有问题的。
一般内置类型我们使用初始化列表和构造函数没有多少差别。
典型错误:


这里在打印空字符串时,会报错,原因不是析构函数中delete/free对空指针的解引用,因为delete或者free函数内部会有对空指针的特殊检查,如果是空指针,delete和free不做处理。
原因是cout函数会自动识别s2.c_str,,因为c_str是一个char*类型,cout就会自动识别他是字符串类型,所以此时会造成经典的cout对空指针的解引用!
string(const char* str = "\0")//构造函数 全缺省
{
_size = strlen(str);
_capacity = _size;
_str = new char[_size + 1];
//strcpy(_str, str);
}
~string()//析构函数
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}对于空的string要多开辟一个空间,用来存储' /0 '。
我们没有必要写两个构造函数,我们使用缺省参数来接受,注意这里是"\0",而不是’\0‘,也可以直接"",空字符串默认含有斜杠0.第二种参数是char类型,与char*类型的参数都不匹配。这里用"",也是可以的,因为常量字符串默认含有\0。
注意这里必须要使用引用返回!!!
引用有两大作用:
1、减少拷贝
2、修改返回值
这里我们想要 [] 来修改字符串的值,返回值不用引用,就无法实现修改,因此引用在某些特定条件下不可替代!!!
这里有两种 [ ] 实现方式,第一种就是普通版本(可读可写),第二种就是const版本(只准读)。
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}注意:const修饰迭代器,迭代器本身可以修改,但是指向的内容不能修改
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//多开辟一个留给'\0'
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
} void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while (end >= (int)pos)//注意隐式类型转换,容易造成死循环!
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
}注意while循环的判断条件,如果不强制类型转换,容易造成隐式类型转换,因为在C语言比较大小有一个规则小的会向大的转换,end是int类型为-1会转换成无符号整形的pos,因此需要强制类型转换!
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)//条件必须要有len == npos
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}分为两种情况,
第一种就是需要erase的长度大于等于本身字符串的长度,那么直接在pos位置放置' \0 ' 即可完成。
假如我们if判断条件只写pos+len >= _size,是会报错的!因为假设len == npos,注意这里的npos( size_t npos = -1)表示无符号整型的最大值,再加上len会造成数据的溢出!
第二种就是erase的长度小于字符串本身的长度,这时候直接用strcpy,注意这里strcpy也将字符串最后一个位置的\0拷贝过来!
举个生动形象的例子:
假如我们要搬砖去挣钱买显卡爪刀,我们有两种方式:
第一种传统方法:自己去搬砖,用自己搬砖挣的钱去买。
第二种现代方法:我们可以推荐老乡去搬砖挣钱,我们自己不用搬砖,我们在其中抽取老乡搬砖的钱。
上面是帮助我们理解,但到了真正代码这块,我们就发现传统写法的拷贝构造函数和复制重载函数都是需要我们自己去创建一块新的空间,然后赋值。
但是到了现代写法,我们用构造函数创建一个新的对象(老乡),然后将内容交给老乡去做。我们最后自己使用swap函数将新创建的对象和自身交换即可!

但是这个写法有一个问题,就是tmp是临时变量,出函数作用域,会调用析构函数,假如s2不是空,是随机值,接着与tmp交换,然后析构tmp,就会导致内存泄漏,因此我们在使用这个方法的时候,要将构造函数有缺省值为空!
原理与上一个类似。都是不用自己干活,交给别人干。
注意这里参数不能引用传参!!!
我们不用引用传参的目的就是去调用拷贝构造函数,然后让拷贝构造产生的s和我们的*this进行交换!
下面是原码实例
//传统写法
//string::string(const string& s)
//{
// _str = new char[s._capacity];
// _size = s._size;
// _capacity = s._capacity;
// strcpy(_str, s._str);
//}
//现代写法
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//传统写法
//string& string::operator=(const string& s)
//{
// _str = new char[s._capacity];
// _size = s._size;
// _capacity = s._capacity;
// strcpy(_str, s._str);
// return *this;
//}
//现代写法
string& string::operator=(string s)
{
swap(s);
return *this;
}我们默认都是将这两个函数重载在类的外部,所以不是类的成员函数,因为使用上的方便。
问题:流插入和流提取的重载必须要用友元函数吗 答案是不一定,因为是否用到友元,看我们是否调用到类的私有成员,如果没有,那就不用友元函数!
注意流提取不能这么书写:

因为in >> ch 这段代码不能识别出 ’ ‘ 和 ’\0‘,cin和scanf都是默认将他们当作分隔符!!!
原码:
istream& operator>>(istream& in, string& s)
{
s.clear();
char tmp = in.get();
while (tmp != ' ' && tmp != '\n')//注意换行是'\n'
{
s += tmp;
tmp = in.get();
}
return in;
}
ostream& operator<<(ostream& out, string& s)
{
for (auto i : s)
{
out << i;
}
return out;
}