【本节目标】
- 1. string类的模拟实现
- 2.C++基本类型互转string类型
- 3.编码表 :值 --- 符号对应的表
- 4.扩展阅读
1. string类的模拟实现
1.1 经典的string类问题
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以 下string类的实现是否有问题?为了防止和库里面的string类发生冲突,我们在这里使用命名空间来限制我们写的string类。
构造函数
namespace yu { class string { public: string(const char* str) :_str(str) {} private: char* _str; size_t _size; size_t _capacity; }; }
上面的代码有什么问题吗?
我们之前提到权限可以缩小,可以平移,但是就是不能放大,那我们下面的写法还有错误吗?
namespace yu { class string { public: string(const char* str) :_str(str) {} private: const char* _str; size_t _size; size_t _capacity; }; void test() { string str("hello world"); } }
这里也是不可以的,因为常量字符串存在代码区,只能可读,不能写,那我们上面就只能完成一个打印输出的工作,不能完成扩容,修改等其他增删改操作。所以我们可以开辟一个同样的空间
namespace yu { class string { public: string(const char* str) //strlen求取'\0'之前字符的个数 :_str(new char[strlen(str)+1]) ,_size(strlen(str)) //capacity是存储有效字符的个数,不包括'\0' ,_capacity(strlen(str)) {} private: char* _str; size_t _size; size_t _capacity; }; void test() { string str("hello world"); } }
但是上面strlen这个需要计算3次,而且strlen的实践复杂度是O(N),所以我们写成下面的形式。
namespace yu { class string { public: string(const char* str) :_size(strlen(str)) //capacity是存储有效字符的个数,不包括'\0' ,_capacity(_size) //strlen求取'\0'之前字符的个数 ,_str(new char[_capacity + 1]) {} private: char* _str; size_t _size; size_t _capacity; }; void test() { string str("hello world"); } }
随后我们写一下c_str函数,看看是否打印输出成功。
namespace yu { class string { public: string(const char* str) :_size(strlen(str)) //capacity是存储有效字符的个数,不包括'\0' ,_capacity(_size) //strlen求取'\0'之前字符的个数 ,_str(new char[_size + 1]) { strcpy(_str, str);//拷贝 } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; }; void test() { string str("hello world"); cout << str.c_str() << endl; } } int main() { yu::test(); return 0; }
此时我们的程序发生了崩溃,因为初始化的顺序和声明的顺序一致,所以程序会先执行_str(new char[_capacity + 1]),但是此时_capacity还没有初始化,此时编译器可能给了随机值或者0。
那么此时开的空间就只有1个字符的空间,开空间小导致拷贝时程序报错。
那怎么解决呢?我们可以初始化的顺序和声明的顺序一致。
但是这样的写法不好,我们这里可以不使用初始化列表,可以使用函数体内初始化。
namespace yu { class string { public: string(const char* str) { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str);//拷贝 } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; }; void test() { string str("hello world"); cout << str.c_str() << endl; } } int main() { yu::test(); return 0; }
我们再来来实现一下析构函数
~string() { delete[] _str; _str = nullptr; _capacity = 0; _size = 0; }
string类里面还提供了无参的构造函数
namespace yu { class string { public: string() :_str(nullptr) ,_size(0) ,_capacity(0) {} string(const char* str) { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str);//拷贝 } ~string() { delete[] _str; _str = nullptr; _capacity = 0; _size = 0; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; }; void test() { string str; cout << str.c_str() << endl; } } int main() { yu::test(); return 0; }
我们这里程序又崩溃了,为什么?cout在识别到char *类型的时候,会认为当前输出的是字符串,会进行解引用行为,这里报错就是空指针解引用的原因。所以我们这里可以设置一个空间存储'\0'
string() :_str(new char[1]) ,_size(0) ,_capacity(0) { _str[0] = '\0'; }
但是实践上我们一般写成全缺省构造函数,不分别写有参和无参两种形式。
//string(const char* str = nullptr)//error:strlen(nullptr)会报错 //string(const char* str = '\0')//error:char不能给char* string(const char* str = "")//常量字符串默认结尾是\0 { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str);//拷贝 }
再来实现一下size和[ ]操作符重载,库中我们还实现了cosnt[ ]操作符重载形式,这种形式函数内部未对对象(*this)作出改变,所以可以加上const。
//不包括'\0' size_t size() const { return _size; } //返回pos位置值的引用 // 1.减少拷贝 // 2.修改返回值 char& operator[](size_t pos) { //这里可以=因为\0处也有空间 //hello world\0 //\0位置处的下标就是_size assert(pos <= _size); return _str[pos]; } const char& operator[](size_t pos) const { //这里可以=因为\0处也有空间 //hello world\0 //\0位置处的下标就是_size assert(pos <= _size); return _str[pos]; }
函数内部未对对象(*this)作出改变,所以可以加上const,我们可以验证一下。
void test() { string str("hello world"); for (size_t i = 0; i < str.size(); i++) { str[i]++; } cout << str.c_str() << endl; }
运行结果:
除了上面的[ ]可以遍历和修改,迭代器也可以修改,我们来模拟实现一下。
typedef char* iterater; iterater begin() { //第一个字符位置是begin return _str; } iterater end() { //\0位置就是end return _str + _size; }
我们来测试一下
void test() { string str("hello world"); string::iterater it = str.begin(); while (it != str.end()) { cout << *it; it++; } }
运行结果:
但是上面这种写法只适合底层空间连续,后面遇到不连续的我们就要修改写法,除了上面的打印工作,我们还有范围for。
for (auto ch : str) { cout << ch; }
其实范围for底层也是用的迭代器,通过反汇编我们可以看到。
如果我们上面把begin变成Begin,此时范围for就会报错,因为范围for是傻瓜式的替换成迭代器,只有我们自定义写的迭代器没有按照规则命名,范围for就不能使用。
我们再来实现一下打印输出的工作
void print_str(const string& s) { for (size_t i = 0; i < s.size(); i++) { s[i]++; } cout << s.c_str() << endl; }
但是我们发现我们的代码出现错误了,为什么?
因为我们上面的size和[ ]操作符重载传入的对象是非const类型的,而我们的打印输出是const类型的,这里会存在权限放大的方法,所里这里会报错,所以size和c_str函数内部未对对象(*this)作出改变,所以可以加上const。而[ ]操作符重载可以使用cosnt版本的。
void print_str(const string& s) { for (size_t i = 0; i < s.size(); i++) { //s[i]++;//此时是const,也就不能修改 cout << s[i]; } cout << endl; }
运行结果:
我们再将迭代器放入刚刚的输出打印函数,我们发现也出现了同样的问题。
所以这里要使用const迭代器,所以我们要实现一下。
typedef const char* const_iterater; const_iterater begin() const { return _str; } const_iterater end() const { return _str + _size; }
因此我们的程序就可以正常输出,但是此时指针指向的内容不可被改变。
我们再来实现一下string类的增删查改。字符串的增加操作必定都要开空间,对于字符串追加的函数,我们这里不能实现每次开2倍的空间操作,如果要追加的字符串的长度过长,开辟的空间必定不够,因此这里我们先实现reserve函数,解决空间开辟的问题。
void reserve(size_t n) { if (n > _capacity) { //扩容步骤 /* 1.开辟空间 2.拷贝数据 3.释放旧空间 4.指向新空间 */ char* tmp = new char[n + 1];//多开一个给'\0'的位置 strcpy(tmp, _str);//会拷贝'\0' delete[] _str; _str = tmp; _capacity = n; } //不缩容 return; }
现在预备条件已经写好了,我就可以开始写轮子了。
void append(const char* str) { size_t len = strlen(str); //这里都不包含'\0',因此可以不用处理 //而且我们开空间都给\0开好了位置 //空间永远都比capacity多一个 if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str); _size += len; //这里插入的str字符串已经拷贝过来\0,就不需要单独处理了 } void push_back(char ch) { if (_size == _capacity) { //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下 size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0';//处理\0 } //这里我们就可以复用上面的接口 string& operator+=(const char* str) { append(str); return *this; } string& operator+=(char ch) { push_back(ch); return *this; }
【字符串探秘:手工雕刻的String类模拟实现大揭秘】(中):https://developer.aliyun.com/article/1425677