【C++】string类--构造、拷贝、赋值及其模拟实现
目录
1. string出现的原因
1.1. C语言中的字符串处理
2. 标准库中的string类
2.1. string类
3.string类的常见构造及模拟实现
3.1. string类对象的常见构造
3.2. string类的构造函数
3.3. string的拷贝构造
3.4. string类的赋值拷贝
3.4.1. 常规构造
3.4.2. 考虑异常安全的解法
编辑
4. 源码
1. string出现的原因
1.1. C语言中的字符串处理
在C语言中,字符串都是以“\0”结尾的字符集合,为了操作这个字符集合,C语言提供了str系列的标准库,如:strlen、strcat、strcpy等。但是这些函数与字符串时分离的,也就是说,C语言中的字符串并不是以对象形式出现的,所以这就并不符合C++中的面向对象的思想,并且底层空间依然需要用户自己管理,而且可能出现越界访问的情况。
为了更加方便地使用字符串,C++设计了string类。
2. 标准库中的string类
2.1. string类
string官方文档
在使用string类时,一定要包含#include<string>,这里注意区分string和string.h!!!
字符串使用了表示字符序列的类(string就是将一堆字符按顺序存放的类)
标准字符串类提供了对此类对象的支持,其接口就类似于标准字符串容器的接口,只不过单独提供了对于单字节字符串的操作
string 类是使用 char 作为它的字符类型,使用它的默认 char_traits 和分配器类型(也就是string其实就是一个别名)
string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)
这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作
- string在底层实际是:basic_string模板类的别名(using string = std::basic_string<char>)
- 不能操作多字节或者变长字符的序列
3.string类的常见构造及模拟实现
3.1. string类对象的常见构造
int main() {string s1; //构造空的s1 等价于string s1("");string s2("hello");//用C格式字符串构造string s3(s2); //调用拷贝构造函数 return 0; }
3.2. string类的构造函数
接下来我们模拟实现一下string的构造函数。
class string { public://全缺省构造函数string(const char* str = ""): _size(strlen(str)), _capacity(_size){//实际上要多开一个 留给'\0'_str = new char[_capacity + 1];strcpy(_str, str);//会把'\0也拷过去'} private:char* _str;size_t _size;//有效字符数量,不包括"\0"size_t _capacity;// };
这段代码还是比较简单的,就是要注意,strcpy会将字符串末尾“\0”也拷贝过去。
3.3. string的拷贝构造
基本写法:
string(const string& s):_size(strlen(s._str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, s._str);}
现代写法:
void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//现代写法string(const string& s):_str(nullptr),_size(0),_capacity(0){string tmp(s._str);swap(tmp);}
这里的基本写法不能理解,但是这个现代写法似乎有点抽象,我们在后面的赋值拷贝来进行讲解。
3.4. string类的赋值拷贝
3.4.1. 常规构造
我们在写任何一个一个类型的赋值运算符重载类型的时候都要考虑下面这几个问题:
- 是否将返回值类型设置为该类型的引用,并在函数结束时返回该实例自身的引用。因为只有返回引用,才能支持连续赋值。如果是void,那么该重载的赋值运算符将不支持连续赋值。
- 是否将传入的参数声明为该类型的引用,因为如果传入的是实例,那么从形参到实参还需要调用拷贝构造函数,传引用就可以避免这样的无谓消耗,提升代码效率。同时我们在赋值运算符内部不会对实例产生修改,所以将在引用参数前面加上const关键字。
string& operator=(const string& s)
- 赋值前是否将就内存清理掉了,如果没有释放掉已有的内存,可能就会导致内存泄漏。
- 判断传入的参数与当前实例是不是同一个实例,如果是的话不进行赋值直接返回。如果现实没有做这个判断,传入参数与与当前实例是同一个实例,那么在释放就内存的时候,传入的参数的内存也被释放掉了,就会找不到要赋值的东西了。
结合上面四点我们写出下面的代码:
string& operator=(const string& s){if (this == &s)return *this;//自己赋值自己 直接返回delete[] _str;_str = nullptr;_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);return *this;}
这个代码已经能够解决我们上面提到的问题了,但是我们考虑一种更加极端的安全情况,那就是,如果在delete[ ] _str之后,系统没有足够的空间来new char[ ],那么此时就会抛出异常,那么此时_str就会处于既不是nullptr,也没有保持原来状态,这就违背了异常安全原则。
3.4.2. 考虑异常安全的解法
有两种方法来解决这个安全问题:
- 第一种简单的方法就是,先分配新内存,然后在delete旧内存,这样的话就算分配内存没有成功,也不会破坏掉原来的string实例
- 第二种方法就是我创建一个临时实例,这个临时实例是传入实例的副本,因为是调用的拷贝构造函数,所以抛出内存分配异常会在修改实例状态前。然后我们交换临时实例和原来实例,又因为临时实例是一个局部变量,出了函数的作用域会自动调用析构函数
string& operator=(const string& s){if (this != &s){string tmp(s._str);std::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);}return *this;}
然后我们来看看我们模拟string的效果:
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <cstring> namespace s {class string{public://全缺省的构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){//实际上要多开一个 留给'\0'_str = new char[_capacity + 1];strcpy(_str, str);//会把'\0也拷过去'}string(const string& s):_size(strlen(s._str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, s._str);}//string& operator=(const string& s)//{// if (this == &s)// return *this;//自己赋值自己 直接返回// delete[] _str;// _str = nullptr;// _str = new char[strlen(s._str) + 1];// strcpy(_str, s._str);// return *this;//}string& operator=(const string& s){if (this != &s){string tmp(s._str);std::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);}return *this;}~string(){if (_str){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}}private:char* _str;size_t _size;//有效字符个数 不算'/0';size_t _capacity;//实际存储有效字符的空间}; }int main() {s::string s1("Hello World");s::string s2;s::string s3;/*std::string s1("Hello World");std::string s2;std::string s3;*/s3 = s2 = s1;return 0; }
赋值前:
赋值后:
4. 源码
string模拟代码
(本篇完)