本博客基于C++官方文档当中给出的string类当中的主要功能实现,来作为参照,简单模拟实现 My-string 。
对于C++当中的string类的介绍,在之前的几篇博客当中有说明,如有问题,请参照一下两个博客文章进行参考:
(2条消息) C++ string类-2_chihiro1122的博客-CSDN博客
(2条消息) C++ string类 迭代器 范围for_string类型迭代器_chihiro1122的博客-CSDN博客
string类
为了与官方当中的string类做区分,在写的时候,My-string类在自定义的命名空间当中进行实现。
基础功能实现
My-string 类的成员变量
不想成员变量在类外部被修改,所以用 protected 关键字来修饰:
protected:
char* _str;
size_t _size;
size_t _capacity;
构造函数和析构函数
参考官方文档当中的构造函数有很多,但是没有必要都实现,只需要实现常用的两种,之间传入字符串和构建空对象两个方式的构造函数,实现如下所示:
//构造函数
string()
:_str(new char[1]),
_size(0),
_capacity(0)
{
_str[0] = '\0';
}
string(const char* str)
: _str(new char[strlen(str) + 1]),
_size(strlen(str)),
_capacity(strlen(str))
{
strcpy(_str, str);
}
注意:上述使用的参数列表来对string类当中的成员变量进行赋值和开空间等操作,在使用参数列表的时候,参数列表当中参数的顺序是要和本类当中的成员变量的声明顺序要保持一致。因为在使用参数列表定义成员变量的时候,不是按照参数列表当中参数的顺序来定义的,是按照本类当中对成员的声明顺序来进行定义的。
如下例子,在使用参数列表的时候就会出错:
class string
{
public:
string(const char* str = "")
:
_size(strlen(str)),
_capacity(strlen(str)),
_str(new char[_capacity + 1])
{
strcpy(_str, str);
}
protected:
char* _str;
size_t _size;
size_t _capacity;
};
我们发现上述定义 _str 这个字符串数组的时候,本类当中的声明是第一声明的,而在构造函数的参数列表当中的位置是在最后的。但是在定义变量的时候不是先定义 _size 这个变量,而是按照本类当中对变量的声明顺序来进行定义,所以会先对 _str 这个字符串数组进行定义;又因为在上述构造函数的参数列表当中,定义的 _str 这个数组的大小是按照 _capacity 这个成员变量来进行计算的,但是此时 _capacity 这个成员变量没有定义,不是我们想要的值,所以这个程序可能出现问题。
所以,我们在使用参数列表的时候,记住一定要保证成员变量在类当中的声明和 列表当中定义的顺序保持一致。
像上述我们想实现的两种构造函数,其实可以用一个 带缺省参数的构造函数实现:
//string(const char* str = '\0') // 错误写法
//string(const char* str = nullptr) // 错误写法
//string(const char* str = "\0") // 可以但是没有必要下面更好
string(const char* str = "")
: _str(new char[strlen(str) + 1]),
_size(strlen(str)),
_capacity(strlen(str))
{
strcpy(_str, str);
}
拷贝构造函数实现:
// 拷贝构造函数(深拷贝)
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
析构函数实现:
// 析构函数
~string()
{
delete _str;
_str = nullptr;
_size = _capacity = 0;
}
一些简单的功能实现
// 返回c语言形式的字符串
const char* c_str() const
{
return _str;
}
// 返回有效的字符个数
size_t Size() const
{
return _size;
}
operator []
我们在使用官方的string类的时候,这个[] 下标访问操作符的运算符重载函数非常好用,所以这里也实现一下,其实这个函数实现不难,但是需要注意两个接口,一个接口是可读可写的函数,也就是针对的是非const 对象;另一个是只可读的 const 对象:
可读可写(非const对象):
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
只可读(const对象):
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
上述的只可读接口函数当中使用的 const 修饰的是 函数参数当中所隐含的 this 指针,这个指针指向的是这个函数作用的对象,如果这个对象 是 const 的对象,且使用的函数是可读可写的函数,也就是用没有用 const 修饰的函数,那么此处就发生了权限的放大,就会编译报错。
像上述就是函数参数的类型不同,构成了函数的重载,在使用这个 operator [] 这个函数的时候,使用的是普通对象,那么就调用第一个函数;如果使用的是 const对象,就调用第二个函数。
iterator 迭代器 和 范围for 的简单实现
迭代器
其实在string当中的迭代器就是一个 typedef 的实现,具体操作看如下对 string 迭代器 当中的 begin() 和 end() 函数的实现就会明白。
begin()和 end()在数组当中的关系:
那么其实实现也很简单:
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
其实 iterator 就是对char* 类型的一个 重命名。
上述迭代器适用的是普通的对象,这个迭代器是可读可写的;对于 const 的对象,就要单独创建只可读的迭代器:
typedef const char* const_iterator;
iterator begin() const
{
return _str;
}
iterator end() const
{
return _str + size();
}
使用迭代器
string_begin::string::iterator it = str.begin();
while (it != str.end())
{
cout << *it << endl;
it++;
}
注意:使用很简单,需要注意的是,在使用的使用,我们要想找到迭代器的位置,应该像上述一样先声明命名空间然后再声明命名空间的当中的类空间,然后编译器才能找到 iterator 。因为编译器默认只在全局进行搜索。
或者是用 auto 自动推导类型。
范围for
当我们实现了上述迭代器之后,我们像官方string类当中使用范围for一样来使在 My-string 当中使用范围for,发现已经可以使用了。
这是因为,范围for 的底层实际就是迭代器,所以我们在实现迭代器之后,就可以支持范围for了。
在我们看来 范围for 很智能,自动开始,自动判断结束等等,其实这都是编译器做的,在编译的时候会把范围for 傻瓜式的全部替换为迭代器,所以在我们看来很神奇,智能,其实都是编译器的功劳。
使用:
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
输出:
怎么证明呢?其实很简单,因为范围for 是编译器的 傻瓜式替换,所以名字不对也不能使用 范围 for:
现在我们把 end()这个函数给注释掉,发现直接报错了:
范围for 在使用的时候,编译器不能找到end()这个函数。
我们把end()函数的名字改成 Myend()之后也不行,也是直接报错,找不到end()函数:
总结,范围for 是傻瓜式的替换,不仅对迭代器的功能有要求,对迭代器的命名都是有要求的。
如果我们查看范围for 的底层汇编,发现他也是在调用 begin(),end()这样的函数,和迭代器差不多的。
增删查改
增
对于字符串的增,实现两个函数,一个是 push_back() 一个是 append(),push_back()是尾差一个字符,append ()是尾差一个字符串。
当然,增,可能会有一个扩容的问题,对于 push_back()我们可以直接用 _capacity 的 2 倍的形式扩容;但是append()因为尾差的是字符串,我们不敢直接 扩容2倍,可能会有添加的字符串之后的有效字符数,大于 _capacity 的情况。所以,我们这里使用 strlen(str)计算要插入的字符串的有效字符 + _size 原本字符串数组当中的有效字符作为 扩容的条件和 扩容的大小。
在官方的string类当中,有一个 reserve ()扩容函数,在这这里我们就直接实现这个函数,那么在上述的 push_back() 一个是 append() 函数中我们就使用这个函数来进行扩容:
reserve():
这个函数我们遵循的是 c 当中 realloc 函数的扩容规则之一,直接开辟另一个新的大空间,再把原本空间的当中的内容拷贝到 新空间当中:
// 扩容函数
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
扩容之后就可以实现上述两个函数了:
// 增
void push_back(char ch)
{
// 如果有效字符个数超过了 容量
if (_size >= _capacity)
{
// 2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
// 扩容
reserve(len + _size);
}
strcpy(_str + _size, str);
_size += len;
}
当然上述两个函数都不是我们最常用的,最常用的是 operator += 这个运算符重载函数,当然这个函数的底层和上述差不多,只不过在使用的时候更加方便:
他同样有两个接口,一个是 尾插字符,一个是尾差字符串:
// += 尾差字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
对于插入,还有就是在 指定位置(pos)位置插入字符或字符串(insert())函数的实现:
// 指定位置插入一个或多个字符
void insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n >= _capacity)
{
// 扩容
reserve(_size + n);
}
//往后挪动数据
size_t end = _size;
while (pos <= end && end != npos)
{
_str[end + n] = _str[end];
--end;
}
// 覆盖值
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
// 指定位置插入一个字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len >= _capacity)
{
// 扩容
reserve(_size + len);
}
//往后挪动数据
size_t end = _size;
while (pos <= end && end != npos)
{
_str[end + len] = _str[end];
--end;
}
// 覆盖值
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = *(str + i);
}
_size += len;
}
在实现的时候遇到的问题,如下图:
end是int类型的,pos是size_t类型的,按照上述的调试,本次循环应该退出循环,但是实际上是进入了循环;
其原因是因为,在c语言的语法当中规定,在一个运算符的两边,如果左右两边的操作数的类型不相同,就会发生整形提升,通常是小类型像大类型发生转换,比如如果是 int类型和 double类型,那么int类型会转换为 double类型。
所以,看似上述end到了-1,按道理应该退出循环,但是int类型的 end 发生的整形提升,提升到了 size_t 无符号整形,所以,了解类型的值域的循环的小伙伴就知道,此时end就不会是-1,从无符号整形的视角来看,是全1,就是整形的最大值。自然就不会跳出循环。
解决上述问题的方式有很多,可以强制类型转换:
或者像官方string当中一样设置一个 npoe :
如上设置之后,就可以在while循环当中多增加一个条件,当 end 走到 -1 的之后就停止:
删
erase函数,从pos位置删除一个或多个字符:
// 从pos位置删除一个或多个字符
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
clear()函数,删除字符串当中所有的有效字符:
// 清除所有有效字符
void clear()
{
_str[0] = '\0';
_size = 0;
}
查
find()函数,在pos位置查找一个字符:
// 从pos位置往后查找一个字符
size_t find(char ch, size_t pos = 0)
{
for (size_t i = pos; i <= _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
find()函数,从pos位置开始查找一个字符串:
// 从pos位置往后查找一个字符
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i <= _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
// 从pos位置开始查找一个字符串
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
const char* ptr = strstr(_str, str);
if (ptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
substr()函数,从字符串数组的pos位置开始取出len个字符大小的字符串:
// 从pos位置开始,从字符串数组当中取出len个字符的字符串,返回string类
string substr(size_t pos, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || len + pos >= _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (int i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
其他
resize()函数,在官方string类当中的resize()函数可以扩容也可以缩容(删除元素),同样我们MyString当中模拟实现的 resize()也应该具备上述功能,我们考虑出以下三种情况(如下图所示):
- 对于 5 的情况,就是小于 _size 的情况,这时候就需要缩容,相当于是删除元素,直接修改 _size 即可;
- 对于 15 的情况,就是 在 _size 和 _capacity 之间的情况,这时候空间是够的,只需要填写初始化覆盖的字符即可;
- 对于 25 的情况,就是 超出了 _capacitt 容量,这时候需要扩容,然后初始化字符;
- 对于上述 15 和 25 的情况,我们统一进行处理,我们上述实现的 reserve()扩容函数是会检测给定的空间是否超出 _capacity 原本的空间大小的,所以上述两种情况直接先进行 reserve()检测扩容,然后在进行初始化赋值操作;
代码实现:
// resize 删除或扩容添加函数
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
// 先检测 扩容
reserve(n);
for (int i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
输入输出(流插入和流提取)(operator<< 和 operator>>)
注意1:上述两个函数当中的 返回的 ostream 返回值和 ostream 参数一定要用引用,如果不用引用的话,在使用的时候就会报错;因为 ostream 做了一个防拷贝操作:
在定义的时候让 ostream 的拷贝构造函数 = delete 就做到了防拷贝操作;上述函数如果不做为引用返回的话,单独用 ostream 返回,就会产生拷贝,生成临时对象,所以就会报错!!!
例子:
ostream operator<< (ostream out, string& str)
{
方式一
//for (int i = 0; i < str.size(); i++)
//{
// out << str[i];
//}
// 方式二
for (auto ch : str)
{
out << ch;
}
return out;
}
上述这个例子没有进行引用就会报错:
注意2:这个函数最好是定义在全局,而不建议定义为成员函数,因为成员函数的第一个参数被固定为当前对象的this指针,这样的话,实现出来的流输出等等在格式上不符合标准的我们经常使用的格式,所以,我们期望 类似 ostream 这样的参数作为函数的第一参数,这样的格式比较符合。
注意3:上述也提到了要定义在全局,但是,我们上述是使用了命名空间来和官方的String类做区别的,所以这个全局函数应该定义在命名空间当中,String类之外,也就是在命名空间的全局域当中;如果你不小心把这个全局函数定义来命名空间之外的最大的那个全局当中,那么编译器会认为这个是STL当中的string。
operator<<(流插入)
可以用有元来解决私有成员的问题,但是一般不建议使用有元,如下使用 operator[] 函数,或迭代器来访问:
ostream& operator<< (ostream& out, string& str)
{
方式一
//for (int i = 0; i < str.size(); i++)
//{
// out << str[i];
//}
// 方式二
for (auto ch : str)
{
out << ch;
}
return out;
}
此处的 打印和 使用 c_str()函数来打印,两者其实有差别的,大多数情况下两者打印的结果都一样,但是有些特殊情况下就不一样了,如下例子所示:
c_str()函数打印的是一个字符串,是一个内置类型,打印这个字符串是以 '\0' 作为终止符的,而流输入是插入多少就打印多少,所以遇见 '\0' 是不会停止的;而上述没有打印出 '\0' 是VS版本的问题,上述使用的是VS2019,如果是VS2013就会在 "hello world" 和 “!!!!!!!!!”之间打印一个空格。
所以这也就引发出一个问题,在C库函数当中的strcpy函数也是按照 '\0" 作为停止符号的,如果字符串的有效字符当中包含了 '\0' 那么这个函数在拷贝的时候就会出现问题,所以我们应该使用 memcpy()这个函数。
operator>>(流提取)
这里的流提取(输入)不考虑空格的情况,如果需要像一个句子一样输入有空格的字符串的话,应该使用getline()函数,这个函数在下面会实现。
所以,只需要一次从缓冲区当中提取一个字符然后 += 到string类当中就行了。
而且,我们使用 流提取来对string类对象当中的字符串数组进行写入数据的话,我们希望的是覆盖,而不是像 operator+= 函数一样尾插,我们我们考虑先用 clear()函数对字符串进行清除。
问题:
istream& operator>> (istream& in, string& str)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
str += ch;
in >> ch;
}
return in;
}
上述代码会陷入死循环,这是因为流提取在识别不同字符或字符串的时候,使用 空格 或 换行(\n)来进行识别的,也就是说 istream 这个流提取本身就不会读到 空格 和 换行(\n),所以上面的ch就不能被赋值为 空格 和 换行(\n),所以循环就不会停止。
解决方法,在 istream 这个流提取当中有一个接口 get(),它默认每次只读取一个字符,不管这个字符是 空格 还是 换行。
代码如下:
istream& operator>> (istream& in, string& str)
{
str.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
str += ch;
ch = in.get();
}
return in;
}
上述代码还可以进行优化,我们发现我们给 str 对象当中插入字符,使用的是 operator+= 这个函数来实现的,而上述是一个一个字符的形式来进行插入的,这样就会导致一个问题,当我们输入的字符串很长的时候, 使用 operator+= 函数会进行很多次扩容,虽然影响不大,但是对于就这个代码来看,还是不太好,所以我们进行以下优化:
- 法1:我们可以先 resercve()预先开辟一段空间,这样就可以一定程度上解决问题,但是这样解决一小部分问题,假设我们先开辟 1024 个空间,如果我们现在只需要10个字符串,那么之后的 1014个空间就浪费了,如果我们需要的空间是 1024的好几倍,那么 1024也不够用,还是需要多开辟几次空间,所以这个方案我们不采取,不适用很多场景。
- 法2:开辟一个临时数组,这个数组的大小可以自己规定,这里我们规定大小为 128 个字符;相当于是把我们输入的字符串,以一组127个有效字符,分割成很多组,当一组的字符填满之后,在对str对象当中的字符串数组进行填写,这样就避免了 在 operator+= 当中很多次的扩容操作。
还需要优化的是,上述我们的实现的流提取,遇到换行或者空格就会停止,那么如果我们在输入有效字符之前,有空格或者换行,那么就会直接停止,但是在官方的string当中的流提取是会把前面的空格和换行清除的,所以我们这加一个循环,把有效字符之前的空格和换行给删除了。
最终代码实现:
istream& operator>> (istream& in, string& str)
{
str.clear();
char ch = in.get();
while (ch == ' ' || ch == '\0')
{
ch = in.get();
}
char Buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
Buff[i++] = ch;
if (i == 127)
{
Buff[i] = '\0';
str += Buff;
// 重置i
i = 0;
}
ch = in.get();
}
// 如果此时 i 不是0,说明Buff 当中还有字符没有 += 完
if (i != 0)
{
Buff[i] = '\0';
str += Buff;
}
return in;
}
比较大小
operator<
string的比较大小不按照长度来比,按照ascll码来比,比如 str1 = "bb" ; str2 = "aaa";那么要是 str2 > str1。
在这里实现可以直接使用 C 当中高度strcmp()这个库函数来实现,但是还是会出现和上述一样的问题,如果有效字符当中有 '\0' ,那么就会出现问题,所以我们应该使用 memcmp()。
但是,memcmp()还是有问题,如下两种情况,两个字符串字符个数不相等,还是比较麻烦的:
所以还是要自己来实现,其实自己实现也不难:
- 两个字符串一起走,如果当前哪一个字符串的字符的ascll值大,那么他就大,反之;如果当前两个字符串的字符相等,那么就继续往后走。
- 之后到最后,有两种情况,一种是两个字符串字符相等,字符个数也相等,那么这两个字符串就相等;另一种就是上述说的两种情况,短的字符串走完了,长的字符串没有走完,那么长的字符串就是大的那一个字符串;
代码实现:
int operator< (const string& str)
{
//return strcmp(_str, str._str) < 0;
size_t i1 = 0;
size_t i2 = 0;
while (i1 < _size && i2 < str._size)
{
if (_str[i1] < str._str[i2])
{
return true;
}
else if (_str[i1] > str._str[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
/*if (i1 == _size && i2 != str._size)
{
return true;
}
else
{
return false;
}*/
// 或者
// return _size < str._size;
// 或者
return i1 == _size && i2 != str._size;
}
复用 memcmp()函数实现的代码如下:
int operator< (const string& str)
{
int Mybool = memcmp(_str, str._str, _size < str._size ? _size : str._size);
return Mybool == 0 ? _size < str._size : Mybool < 0;
}
operator== / <= / > / >= / !=
写好一个之后,后面的就简单了,可以直接复用:
bool operator== (const string& str) const
{
return _size == str._size &&
memcmp(_str, str._str, _size) == 0;
}
bool operator<= (const string& str) const
{
return *this < str || *this == str;
}
bool operator> (const string& str) const
{
return !(*this <= str);
}
bool operator>= (const string& str) const
{
return !(*this < str);
}
bool operator!= (const string& str) const
{
return !(*this == s);
}
operator=
复制拷贝有两种,一种是浅拷贝,一种是深拷贝:
- 浅拷贝就是值拷贝,是直接复制,这样的话如果只是一些内置类型是没有问题的,但是,如果是一块空间的指针进行浅拷贝,那么这个指针只是指向了新的一块空间,那么原本的指针指向的空间就找不到了,就会发生内存泄漏;
- 深拷贝就是上述的例子,假设要拷贝另一块空间,就新开辟一块空间,这块空间的大小和另一块空间的大小一样,然后再把另一快空间当中的值拷贝到新空间当中,然后再让指针指向这条新的空间,这就是深拷贝。
- 对于深拷贝,上述描写的只是一种情况,就是当要拷贝的空间比原空间大;其实还有两种情况,一种拷贝的空间比原空间小,那么就直接进行拷贝,但是为了优化,多余的空间需要释放,所以还是要重新开一个更小的空间然后进行赋值;另一种是拷贝的空间和原空间相等,那么就直接对空间进行赋值。
所以综上所述,建议 处了空间大小相等的情况下 都直接释放原空间,然后再进行赋值。
string& operator= (string& str)
{
if (this != &str)
{
char* tmp = new char[str._capacity + 1];
memcpy(tmp, str._str , str._size + 1);
delete[] _str;
_str = tmp;
_size = str._size;
_capacity = str._capacity;
}
return *this;
}
上述就是深拷贝的赋值操作符重载函数。
其实还有一个更好的写法,先来看下面这个代码:
string& operator= (const string& str)
{
if (this != &str)
{
string tmp(str);
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
return *this;
}
如上,新创建一个和 str 一样的对象(调用了拷贝构造函数)-tmp,然后把tmp和 this对象的数组指针和 所以成员交换一下,然后就可以实现生拷贝了,这是一个很妙的写法,看下图:
如上所示,tmp新开辟了一块空间,s1想着,反正你tmp 的生命周期 就在这个函数当中,出了这个函数,tmp就需要调用析构函数,释放空间,那么s1就把他的空间给tmp,tmp就把新开辟的,和s3赋值好的空间给s1,然后tmp在最后释放空间的时候就释放的是s1原本的空间,相当于是tmp为s1把原空间给释放掉了。
注意:在赋值操作符重载函数当中不能像如下一样写:
string& operator= (const string& str)
{
string tmp(str);
std::swap(tmp , *this);
return *this;
}
像上述一样写会造成递归死循环。
这时候,swap()这个函数在两个参数都是对象的时候,它其中调用的就是 operator= 赋值操作符重载函数,如下所示:
那像上述代码就会在 swap 和 operator= 之间来回跳,造成递归式的死循环。
所以在像上述一样使用swap 的时候,还是需要自己实现swap()函数:
就如上述string类的swap如下所示实现:
class string
{
`````````````
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
````````````
};
根据上述实现的swap()函数,上述的 operator= 函数可以如下优化:
string& operator= (string& str)
{
swap(str);
return *this;
}
上述相当于把两个string对象的 所有成员 都给交换了。
string& operator= (string str)
{
swap(str);
return *this;
}
而上述才是和之前一样,是传值拷贝,需要创建临时对象,也就是这个str就是局部变量,局部变量出了这个函数作用域就结束了生命周期,相当于是 str 帮s1 (*this)把s1原本的空间释放掉了。
基于上述方法,对拷贝构造函数的优化
基于上述的优化,我们可以优化拷贝构造函数,我们也可以在拷贝构造函数使用和上述一样的方式来,让编译器来帮我们释放空间:
string(const string& s)
{
string tmp(s._str);
// 让tmp来帮s对象释放掉原来的空间
swap(tmp);
}
上述是对拷贝构造函数的优化,但是上述还是有问题:
上述代码在一般的编译器当中就会报错,编译器对于类当中的内置类型,一般来说是不会自动做初始化的,如果你在一些环境当中看见初始化的,都属于是这个编译器的优化,但是不是所以的编译器都会这样做,所以我们不敢依靠编译器来自动初始化,我们需要手动初始化。
上述的 this 对象没有手动初始化,对于其中的内置类型(_size 和 _capacity )来说,是随机值,那么tmp交换的是一个(_size 和 _capacity )是随机值的对象,在这个函数结束之后tmp调用 析构函数 delete 释放空间的时候就会出现问题。
所以我们应该这样写:
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
// 让tmp来帮s对象释放掉原来的空间
swap(tmp);
}
需要注意的是,上述是直接用s._str 来构造的tmp对象,那么对于 下述情况就会出现问题:
"hello\0worle"
对于上述这个字符串,他只能拷贝 hello。所以如果是有上述情况使用的话,建议使用之前使用的拷贝构造函数来实现。
string类的完整代码
#pragma once
#include<assert.h>
namespace string_begin
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
iterator begin() const
{
return _str;
}
iterator end() const
{
return _str + size();
}
// 构造函数
//string()
// :_str(new char[1]),
// _size(0),
// _capacity(0)
//{
// _str[0] = '\0';
//}
//string(const char* str)
// : _str(new char[strlen(str) + 1]),
// _size(strlen(str)),
// _capacity(strlen(str))
//{
// strcpy(_str, str);
//}
//string(const char* str = '\0') // 错误写法
//string(const char* str = nullptr) // 错误写法
//string(const char* str = "\0") // 可以但是没有必要下面更好
string(const char* str = "")
: _str(new char[strlen(str) + 1]),
_size(strlen(str)),
_capacity(strlen(str))
{
memcpy(_str, str, _size + 1);
}
// 拷贝构造函数(深拷贝)
//string(const string& s)
//{
// _str = new char[s._capacity + 1];
// //strcpy(_str, s._str);
// memcpy(_str, s._str, s._size + 1);
// _capacity = s._capacity;
// _size = s._size;
//}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
// 让tmp来帮s对象释放掉原来的空间
swap(tmp);
}
// 析构函数
~string()
{
delete _str;
_str = nullptr;
_size = _capacity = 0;
}
// 返回c语言形式的字符串
const char* c_str() const
{
return _str;
}
// 返回有效的字符个数
size_t size() const
{
return _size;
}
// 下标+引用返回的运算符重载函数
// 要提供两个版本,一个是非const对象的,一个是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];
}
// 扩容函数
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// 增
void push_back(char ch)
{
// 如果有效字符个数超过了 容量
if (_size >= _capacity)
{
// 2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
// 扩容
reserve(len + _size);
}
//strcpy(_str + _size, str);
memcpy(_str + _size, str, len + 1);
_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, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n >= _capacity)
{
// 扩容
reserve(_size + n);
}
//往后挪动数据
size_t end = _size;
while (pos <= end && end != npos)
{
_str[end + n] = _str[end];
--end;
}
// 覆盖值
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
// 指定位置插入一个字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len >= _capacity)
{
// 扩容
reserve(_size + len);
}
//往后挪动数据
size_t end = _size;
while (pos <= end && end != npos)
{
_str[end + len] = _str[end];
--end;
}
// 覆盖值
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = *(str + i);
}
_size += len;
}
// 从pos位置删除一个或多个字符
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
// 从pos位置往后查找一个字符
size_t find(char ch, size_t pos = 0)
{
assert(pos <= _size);
for (size_t i = pos; i <= _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
// 从pos位置开始查找一个字符串
size_t find(const char* str, size_t pos = 0)
{
assert(pos <= _size);
const char* ptr = strstr(_str, str);
if (ptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
// 从pos位置开始,从字符串数组当中取出len个字符的字符串,返回string类
string substr(size_t pos, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || len + pos >= _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (int i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
// resize 删除或扩容添加函数
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
// 先检测 扩容
reserve(n);
for (int i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
// 清除所有有效字符
void clear()
{
_str[0] = '\0';
_size = 0;
}
// 比较string大小
//int operator< (const string& str) const
//{
// //return strcmp(_str, str._str) < 0;
// size_t i1 = 0;
// size_t i2 = 0;
// while (i1 < _size && i2 < str._size)
// {
// if (_str[i1] < str._str[i2])
// {
// return true;
// }
// else if (_str[i1] > str._str[i2])
// {
// return false;
// }
// else
// {
// ++i1;
// ++i2;
// }
// }
// /*if (i1 == _size && i2 != str._size)
// {
// return true;
// }
// else
// {
// return false;
// }*/
// // 或者
//
// // return _size < str._size;
// // 或者
// return i1 == _size && i2 != str._size;
//}
// < 当中复用 memcmp
bool operator< (const string& str) const
{
int Mybool = memcmp(_str, str._str, _size < str._size ? _size : str._size);
return Mybool == 0 ? _size < str._size : Mybool < 0;
}
bool operator== (const string& str) const
{
return _size == str._size &&
memcmp(_str, str._str, _size) == 0;
}
bool operator<= (const string& str) const
{
return *this < str || *this == str;
}
bool operator> (const string& str) const
{
return !(*this <= str);
}
bool operator>= (const string& str) const
{
return !(*this < str);
}
bool operator!= (const string& str) const
{
return !(*this == str);
}
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
// 复制操作符重载函数
//string& operator= (string& str)
//{
// if (this != &str)
// {
// char* tmp = new char[str._capacity + 1];
// memcpy(tmp, str._str , str._size + 1);
// delete[] _str;
// _str = tmp;
// _size = str._size;
// _capacity = str._capacity;
// }
// return *this;
//}
//string& operator= (const string& str)
//{
// if (this != &str)
// {
// string tmp(str);
// //std::swap(_str, tmp._str);
// //std::swap(_size, tmp._size);
// //std::swap(_capacity, tmp._capacity);
// swap(tmp);
// }
// return *this;
//}
string& operator= (string str)
{
swap(str);
return *this;
}
protected:
char* _str;
size_t _size;
size_t _capacity;
size_t static npos;
};
size_t string::npos = -1;
ostream& operator<< (ostream& out, string& str)
{
方式一
//for (int i = 0; i < str.size(); i++)
//{
// out << str[i];
//}
// 方式二
for (auto ch : str)
{
out << ch;
}
return out;
}
istream& operator>> (istream& in, string& str)
{
str.clear();
char ch = in.get();
while (ch == ' ' || ch == '\0')
{
ch = in.get();
}
char Buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
Buff[i++] = ch;
if (i == 127)
{
Buff[i] = '\0';
str += Buff;
// 重置i
i = 0;
}
ch = in.get();
}
// 如果此时 i 不是0,说明Buff 当中还有字符没有 += 完
if (i != 0)
{
Buff[i] = '\0';
str += Buff;
}
return in;
}
}
写时拷贝(延迟拷贝)
我们上述对深拷贝和浅拷贝都实现和说明,假设现在有一个s1对象,根据s1拷贝构造了 s2 出来,根据以上实现的拷贝构造函数是深拷贝实现的,但是如果现在对s2的使用其实没有那么多,只是想简单的拷贝出来,当s1析构的时候,我期望s2也跟着析构了;那对于这种情况我为什么不使用浅拷贝呢?
其实原因很简单,也浅拷贝有诸多的问题,上述也进行描述了,浅拷贝只是单纯的进行值拷贝,比如像上述的两个对象,其中的 _str 数组指针是指向同一块空间的,那么在两个对象析构的时候,就会对这个空间析构两次;而且如果其中一个对象对数组进行修改,那么另一个对象也会受到影响;
所以这个时候,有人就发明了写时拷贝;
写时拷贝:多用一个引用计数,这个引用计数的数字代表着当前有多少引用来管理这块空间,比如上述的s1 和 s2 ,此时s1 和 s2 共同指向的空间的引用计数就是2,在s2 拷贝构造出来之前,引用计数就是1;
用这个引用计数来对这块空间有多少引用管理来进行计数,当某一个对象要进行析构的时候,先对引用计数进行 " -- " 的操作,如果这个引用计数不为0,那么就不进行释放空间的操作;当最后一个引用进行析构的时候,这时候引用计数就会 " -- " 到0,那么就对这块空间进行释放;简单理解就是:最后一个走的人关灯;
上述解决了析构两次的问题,还有一个问题就是对于空间修改会影响多个对象的问题,也是根据引用计数来进行操作的;
当某一个对象想对空间进行修改的时候,先看引用计数是否为1,如果为1 ,说明这块空间是这个对象独占的,那么就可以直接进行修改;如果不为1,说明这空间不是这个对象独占的,那么就进行深拷贝,在堆深拷贝出来的空间就修改,此时引用计数-1;
上述进行深拷贝出来的空间也要有引用计数。
上述的写实拷贝,在g++ 当中有使用。但是不是所以的编译器使用的string都是写时拷贝,在VS2019 当中就没有使用写时拷贝,因为写时拷贝并不是C++标准的规范。
在VS2019 当中,还进行了一些优化,当我们使用官方string存储的字符 比较小的时候,他是存储在 一个buf 数组当中,并没有在堆上开空间,他认为是比较小的 字符串,没有必要在堆上开空间,直接存储到一个数组当中;当字符串比较大的时候才会在堆上去开空间:
虽然效率提高了,但是在存储大的字符串时,就会浪费掉 buf 这一个数组空间。
写实拷贝的读取缺陷:
https://coolshell.cn/articles/1443.htmlhttps://coolshell.cn/articles/1443.html
关于string类的好文章
STL 的string类怎么啦?_haoel的博客-CSDN博客https://blog.csdn.net/haoel/article/details/1491219
C++面试中string类的一种正确写法 | 酷 壳 - CoolShellhttps://coolshell.cn/articles/10478.html