【字符串探秘:手工雕刻的String类模拟实现大揭秘】(上):https://developer.aliyun.com/article/1425676
验证一下
void test() { string str("hello world"); str += '!'; str += "!!"; print_str(str); }
运行结果:
我们再来实现一下插入函数
void insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下 size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } size_t end = _size; while (end >= pos) { //依次向后挪动 _str[end + 1] = _str[end]; --end; } _str[pos] = ch; ++_size; //挪动的时候挪动了'\0' //_str[_size] = '\0';这里不用处理 } void test() { string str("hello world"); str += '!'; str += "!!"; print_str(str); cout << endl; str.insert(5, '*'); str.insert(5, '*'); str.insert(5, '*'); print_str(str); }
运行结果:
我们看一下我们的代码有没有什么问题?我们试一下我们的头插:str.insert(0, '*');
头插的时候,end减到为0,下次减减的时候减到-1,但是此时end为size_t,会变成整型的最大值,因此程序会一直运行,进入死循环,那我们将上面的end变量类型变为int呢?我们来看一下下面的程序。
上面的结果为什么是yes!当无符号整型和有符号整型比较时,有符号整型会隐式提升为无符号整型,-1此时就能转化为整型的最大值。所以将上面的end变量类型变为int也不行,因为我们的pos也是无符号整型,不过我们可以通过强制类型转换解决。
void insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下 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; //挪动的时候挪动了'\0' //_str[_size] = '\0';这里不用处理 }
或者也可以这样写,这样end变量的值只能减到0,就不会出现上面的错误。
void insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下 size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } size_t end = _size + 1; while (end > pos) { //依次向后挪动 _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; //挪动的时候挪动了'\0' //_str[_size] = '\0';这里不用处理 }
我们再来实现一下字符串的插入。
void insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } int end = _size; while (end >= (int)pos) { _str[end + len] = _str[end]; --end; } strncpy(_str+pos,str,len); }
这里需要注意不能使用strcpy,因为它会拷贝'\0',会覆盖后面的'w'字符。接下来我们再实现一下删除,库里面接口为我们提供了缺省值npos,它属于静态成员变量,类里面定义,类外初始化。
private: char* _str; size_t _size; size_t _capacity; //不用给缺省值 static size_t npos;//不会初始化列表,属于整个类,属于所有对象 }; size_t yu::string::npos = -1;
但是我们发现库里面是这样写的。
我们发现当我们给双精度浮点型的时候程序报错了,error C2864: yu::string::x: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”。对于上面这种写法只能支持整型变量。上面的这种写法可以算是编译器的特殊处理,此时的npos只读,不能修改,不能加加减减。
void erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } }
测试一下:
void test() { string str("hello world"); print_str(str); cout << endl; str.erase(5, 1); print_str(str); cout << endl; str.erase(2); print_str(str); }
运行结果:
我们看一下上面判断的条件,第二个条件是否可以覆盖第一个条件。这里是不能的,npos已经是最大值了,如果再加上pos就会溢出,程序就会有问题。我们再来写来写一下交换的成员函数。
void swap(string& str) { //交换指向的内容即可 std::swap(str._str, _str); std::swap(str._size, _size); std::swap(str._capacity, _capacity); //std::swap(str, *this);效率较低,拷贝构造消耗大 }
我们来看一下库中这几个函数的区别,如果没有第二个swap函数,就只能调用第三个swap函数,第三个函数首先要拷贝,然后赋值,这里都是深拷贝,要开空间区拷贝,代价大。
我们再来实现一下find函数。
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; } size_t find(const char* str, size_t pos = 0) { //暴力匹配 const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) { return npos; } else { //指针相减是元素之间的个数 return ptr - _str; } }
测试一下:
void test1() { string str("hello world"); cout << str.find('h') << endl; cout << str.find("lo") << endl; }
运行结果:
我们再来实现一下substr函数。
string substr(size_t pos = 0, size_t len = npos) { assert(pos < _size); size_t end = pos + len; if (len == npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); //小于后面字符的个数 for (size_t i = pos; i < end; i++) { str += _str[i];//会自动处理'\0' } return str; }
我么们来看一下我们的代码有没有什么问题?
这里主要就是浅拷贝的问题,临时对象和str对象指向了同一块空间。
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构 造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
string(const string& s)//拷贝构造 - 深拷贝 { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; }
通过深拷贝就可以解决问题指向同一块空间。
那赋值的深拷贝呢?
string& operator=(const string& s)//赋值 - 深拷贝 { if (this != &s) { char* tmp = new char[s._capacity + 1]; strcpy(tmp, s._str); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } }
此时也能解决问题。
我们上面都是通过C语言的形式中print_str或者str.c_str()输出我们的字符串的,我们实现一下直接使用C++流插入操作符来输出字符串。
//流插入和流提取重载在全局 ostream& operator<<(ostream& out, const string& s) { for (auto ch :s) { out << ch; } return out; }
再来实现一下流提取。
// 这里会修改s,所以不用带上const istream& operator>>(iostream& in, string& s) { char ch; in >> ch; while (ch != ' ' && ch != '\n') { s += ch; in >> ch; } }
我们来测试一下
我们发现我们的程序无论是输入空格或者换行都不能结束。我们来测试一个数据,当什么输入"12345空格6",安装我们想要的逻辑,cin应该只会读取到”123345”。我们来一下我们的调式结果。
按照常理,输入到'5'字符后应该输入' ',但是上面的调式结果却跳过了空格,而是输出字符'6',ch没有拿到空格。
我们来看一下cin的特性,当我们连续往两个字符中写入'x',' '和'y',cin会将空格当为分隔符,会忽略这个空格,从而直接读取后续的字符,也就是读取'y','\n'也是如此,我们上面的程序是将字符串分为一个个字符,然后字符依次输入,此时就是cin输入单个字符,遇到空格或者换行就会被忽略掉,所以上面的程序无论输入什么都不会被结束,因为拿不到空格或者换行,我们可以使用getchar来获取单个字符的输入,但是我们今天学习的是C++语言,最好应用C++函数。
【字符串探秘:手工雕刻的String类模拟实现大揭秘】(下):https://developer.aliyun.com/article/1425679