一. 简单string类设计
主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数等资源管理功能。
1. private成员
就是一个C语言中的字符串指针
class string { public: private: char* _str; };
2. 构造函数
我们设计一个全缺省的默认构造函数,如果不传参时就默认存储\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; };
3. 析构函数
使用delete[ ]释放我们开辟的空间,再把_str置为nullptr(防止野指针)
class string { public: //析构函数 ~string() { delete[] _str; _str = nullptr; } private: char* _str; };
4. 拷贝构造和赋值重载
这里一定要显示定义拷贝构造和赋值重载,如果用默认的会造成浅拷贝问题(多次释放同一块空间)
4.1 什么是浅拷贝?
默认的拷贝构造和赋值重载都是通过浅拷贝实现的。浅拷贝就是一个字节一个字节的拷,浅拷贝也叫值拷贝
4.2 浅拷贝带来的问题
4.3 深拷贝完成拷贝构造和赋值重载
既然是指向同一块空间带来的问题,那我们就重新开辟一块同样大小的空间,利用strcpy把另一块空间的内容拷贝到新开辟空间上,这就是深拷贝。
浅拷贝:空间相同,内容相同
深拷贝:空间不同,内容相同
拷贝构造
传统写法:
c
lass 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; };
赋值重载的几点说明:
返回值:为了支持连等,返回值为string又因为出了这个函数之后*this(也就是左值)依然存在所以返回左值的引用(少一次拷贝构造)。
参数值:对于右值我们只是读它的值用来拷贝给左值,并不修改它的内容,所以加上const修饰
二. string类的模拟实现
private成员
除了字符数组(_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; };
接下来我们介绍几个较复杂的接口
1. string类对象容量操作接口
1.1 reserve
原型: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.更新容量 } }
1.2 resize
原型: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
通过计算器也可以佐证我们的结果
按照一个字节一个字节来设置的就只适用于给字符数组来设置字符,因为一个字符的大小就是一个字节
2. string类对象字符串操作接口
2.1 c_str
原型:const char* c_str() const;
作用:返回C格式字符串,只可读不可写
就是直接返回成员变量_str,它的类型是char*
const char* c_str() const { return _str; }
C格式字符串和string对象还是不同的,C格式字符串看’ \0 ‘,用cout输出时遇到’ \0 ‘就结束了;而string对象看的是它的有效字符个数(也就是_size),不管中间有没有’ \0 ’
2.2 substr
原型: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类对象 }
3. string类对象修改操作接口
3.1 insert
原型: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; }
关于insert的几点说明
3.2 erase
原型: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; }
关于erase的几点说明
4. string类的非成员函数
4.1 operator<<
原型:ostream& operator<< (ostream& out, const string& s);
作用:string类的<<运算符重载
o
stream& 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; }
4.2 operator>>
原型: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; }
4.3 getline
原型: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; }
5. string类的iterator
对于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; }
其实auto也是的底层也是迭代器,auto最终会被编译器转化成迭代器