一. 构造 & 拷贝构造 & 赋值重载 & 析构 & 赋值重载
🎨传统写法
【面试题】实现一个简洁的string类,即只考虑_str这个成员,着重考察深浅拷贝
💢1.构造函数
构造函数,我们一般是带参/无参两种构造方式。其中文档可查到无参时默认构造空串"" (隐含了’\0’),给一个缺省值即可,注意不是给空指针nullptr( c_str会出错 )
string(const char* str = "") { _size = strlen(str);//复用 —— 且不用关心初始化顺序 _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); }
ps:capacity是指能存储有效字符的个数,因此开空间时要注意给\0预留空间,string类要特别注意\0的存在
💢2.拷贝构造
我们知道,拷贝构造&赋值重载这两个特殊的成员函数,如果自己不写编译器会自动生成。这份默认的拷贝构造(赋值重载)对于内置类型完成浅拷贝;对于自定义类型会调用它的拷贝构造(赋值重载)
如果使用默认生成的默认构造,完成的是浅拷贝,析构时会崩溃~~ 看图
对此我们要实现深拷贝,动态开辟一个空间,实现数据独立
//s2(s1) 拷贝构造 string(const string& s) :_str(new char[s._capacity+1]) ,_size(s._size) ,_capacity(s._capacity) { strcpy(_str, s._str); }
💢3.赋值重载
如果使用默认生成的赋值重载 ,会导致以下问题 ——
我们还是要实现深拷贝,释放旧空间
有些点要注意:
上来最好不要直接销毁,有可能自己给自己赋值s3=s3,导致把自身释放了,所以要判断一下地址是否相同
string& operator=(const string& s) { if (this != &s) { delete[] _str; _str = new char[strlen(s._str)]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; return *this; } }
还有没有可能会new失败呢 ?还没有new成功我们就把_str给释放了,为此我先开一个中转的空间,并且给给tmp,若开辟成功才释放_str
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; } }
💢4.析构函数
前面都是 new[] 开的空间,也要delete[]释放
~string { delete[] _str; _str = nullptr; _size = _capacity = 0; }
以上均为传统写法,但现在都什么年代?还在用传统写法??
🎨现代写法(资本家)
传统写法就是老老实实,一步一个脚印的开空间、拷贝,现代写法更注重灵活,投机取巧,要对前面比较熟悉才能掌握
🌍1.构造函数 & 析构函数
现代写法多是复用的思维,构造函数 & 析构函数只能老老实实
🌍2.拷贝构造
复用含参构造出tmp(打工人),把tmp和s2身体互换
注意s2的_str必须给nullptr,如果不置空,会把随机值传给tmp,tmp是局部变量出作用域时调用析构函数,会崩溃
//现代写法 —— 复用构造 —— 找替死鬼 string(const string& s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); swap(tmp);//this -> swap(tmp) }
🌍3.赋值重载
同样复用拷贝构造进行深拷贝,
string& operator=(const string& s) { if (this != &s) { string tmp(s._str); swap(tmp); //this ->swap(tmp) return *this; } }
更加剥削的版本:形参s同样是局部变量,出作用域也会调用析构函数 ——
// s1 = s3; //s传参时顶替tmp当打工人,s就是s3的深拷贝 string& operator=(string s) { swap(s); return *this; }
🎨swap的区别
string的标准库中提供了一个swap,全局也有一个swap的模板函数(适用于内置类型),底层都是对两个对象的成员进行交换,结果相同,那为什么还要有呢string类中的?
std::string s1("hello"); std::string s2("one and only"); s1.swap(s2); swap(s1, s2);
注意:二者不构成函数重载!!都不在一个域(函数重载的前提)
string类的中的成员函数仅仅是对对成员函数进行交换,效率高,而全局的swap则进行了三次深拷贝(一次拷贝+两次赋值),空间损耗很大
我们要交换三个成员,于是封装了一个swap,为了避免命名冲突,swap中的swap要指定库中的作用域,不然是先从局部开始找,发现参数不匹配就会报错
void swap(string& tmp) { std:: swap(_str, tmp._str); std::swap(_size, tmp._size); std::swap(_capacity, tmp._capacity); }
如果在模拟赋值重载时用全局的swap,会发生栈溢出
二. 基本接口
一些常用的接口,实现不难,但要注意小细节
🌈size & capacity
size_t size() const { return _size; } size_t capacity() const { return _capacity; }
🌈c_str
const char* c_str() const { return _str;//解引用找到'\0' }
🌈[]
//可读可写 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迭代器:const对象优先调用最匹配的const成员函数,可读不可写
typedef char* iterator; typedef const char* const _iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } const _iterator begin() const { return _str; } const _iterator end() const { return _str + _size; }
迭代器实现出来了,范围for🍬还会远吗??
三. 增
⚡reserve & resize
插入字符串要考虑扩容
💦reserve
开新的空间,拷贝过去,释放旧空间
void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1];//考虑'\0' strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } }
💦resize
要考虑多种情况
插入数据:如果是将元素个数增多,默认用\0来填充多出的元素空间,也可指定字符来填充
删除数据:如果是将元素个数减少,要把多出size的字符抹去
//"helloxxxxxxxxx" void resize(size_t n, char ch = '\0') { if (n > _size) { //插入数据 reserve(n); for (size_t i = _size; i < n; ++i) { _str[i] = ch; } _str[n] = '\0'; _size = n; } else { //删除数据 _str[n] = '\0'; _size = n; } }