前言
各位读者朋友们大家好!上期我们讲解了string类的基础用法,这期我们来模拟实现一下string类。
目录
- 前言
- 一. string类的构造函数
- 1. 1 无参构造
- 2.2 带参构造
- 1.3 无参和带参构造结合
- 1.4 拷贝构造
- 1.5 c_str
- 二. string类的析构函数
- 三. 字符串的遍历
- 3.1 size接口
- 3.2 operator[]
- 四. 字符串的增删查改接口
- 4.1 push_back
- 4.2 operator +=
- 4.3 append
- 4.4 insert
- 4.5 erase
- 4.6 find
- 4.7 substr
- 五. string类的其他接口
- 5.1 赋值运算符重载
- 5.2 比较大小
- 5.2 流插入和流提取重载
- 六. string类的深拷贝的现代写法
- 结语
string类的基本结构:
namespace Yuey
{class string{public:// 成员函数private:char* _str;size_t _size;size_t _capacity;};
}
一. string类的构造函数
1. 1 无参构造
string():_str(nullptr),_size(0),_capacity(0)
{}
通过初始化列表,将_str初始化为空指针, _size和 _capacity初始化为0。但是这样写是有bug的,在我们访问string的时候会对空指针解引用,正确的是
string():_str(new char[1] {'\0'}), _size(0), _capacity(0)
{}
2.2 带参构造
string(const char* str)
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];// 给\0strcpy(_str, str);
}
如果我们通过初始化列表对string初始化,我们需要先对_str初始化,但是开空间又需要用到_size,就很麻烦,而且这些成员变量不是必须在初始化列表初始化的(引用成员变量、const成员变量、没有默认构造的类类型变量),所以我们在构造函数中对其初始化。
1.3 无参和带参构造结合
string(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];// 给\0strcpy(_str, str);
}
1.4 拷贝构造
string(const string& s)
{_str = new char[s._capacity + 1]; // 给\0strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
1.5 c_str
由于我们没有写流提取和流插入的重载函数,为了方便打印查看,我们模拟实现c_str函数,来返回初始化好的string类的字符串:
const char* c_str() const
{return _str;
}
二. string类的析构函数
~string()
{if (_str){delete[] _str;_str = nullptr;_size = _capacity = 0;}
}
将数组在堆上申请的空间释放,size和capacity置为0。
三. 字符串的遍历
3.1 size接口
size_t size()
{return _size;
}
返回字符串的字符个数。
3.2 operator[]
char& operator[](int pos)
{assert(pos < _size);return _str[pos];
}const char& operator[](int pos) const
{assert(pos < _size);return _str[pos];
}
这样就完成了字符串的遍历
这样支不支持范围for呢?
答案是不支持的,因为范围for的底层是迭代器,这里我们简单的实现一下:
typedef char* iterator;
iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}
这样就能使用范围for遍历字符串了,也能使用迭代器访问:
string::iterator it = s.begin();
while (it != s.end())
{cout << *it << " ";++it;
}
在这里用指针模拟迭代器是因为string类的底层是数组
四. 字符串的增删查改接口
4.1 push_back
void string::reserve(size_t n)// 预留空间
{if (n > _capacity){char* tmp = new char[n + 1]; // 给\0strcpy(tmp, _str);delete[] _str;// 释放原数组的空间_str = tmp;_capacity = n;}
}void string::push_back(char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;_str[_size] = '\0';
}
4.2 operator +=
- string& string::operator += (char ch)
string& string::operator += (char ch)
{push_back(ch);return *this;
}
- string& string::operator += (const char * str)
string& string::operator += (const char* str)
{append(str);return *this;
}
4.3 append
void append(const char * str);
void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);// 如果_size + len > 2*_capacity要多少给多少,// 否则二倍扩容}strcpy(_str + _size, str);_size += len;
}
4.4 insert
- void string::insert(size_t pos, char ch)
void string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity); }size_t end = _size + 1;// 直接挪动\0while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;
}
- void string::insert(size_t pos, const char * str)
void string::insert(size_t pos, const char* str)
{assert(pos < _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len > 2 * _capacity ? _size + len :2 * _capacity);}int end = _size + len;while (end > pos + len - 1) // pos + 3 - 1{_str[end] = _str[end - len];--end;}for (int i = pos; i < len; ++i){_str[i] = str[i];}_size += len;
}
4.5 erase
void string::erase(size_t pos, size_t len)// 缺省值给到声明{if (len >= _size - pos)// 长度多于剩余字符的长度,就全删{_str[pos] = '\0';_size = pos;}else // 从后往前依次拷贝{for (int i = pos + len; i < _size; ++i){_str[i - len] = _str[i];}_size -= len;}}
4.6 find
- size_t string::find(char ch, const size_t pos)
size_t string::find(char ch, const size_t pos)
{for (int i = pos; i < _size; ++i){if (_str[i] == ch){return i;}}return npos;
}
找单个字符仅需遍历字符串对比即可。
- size_t string::find(const char * str, const size_t pos)
size_t string::find(const char* str, const size_t pos){assert(pos < _size);const char* rptr = strstr(_str + pos, str);// 返回的是str在_str中第一次出现的地址if (rptr) // 如果找到了,即rprt非空{return rptr - _str; // 指针 - 指针是元素个数}else{return npos;}}
4.7 substr
string string::substr(size_t pos, size_t len)
{string tmp;if (len > _size - pos){len = _size - pos;}tmp.reserve(len);for (int i = 0; i < len; ++i){tmp += _str[pos + i];}return tmp;
}
如果len大于剩余的字符的个数,那就将len置为剩余的字符总个数,将剩余的字符全部来构造新的字符串,给新的字符串预留出len长度的空间,然后对其加等原字符串的pos后的len个字符。
五. string类的其他接口
5.1 赋值运算符重载
string& operator=(const string& s)
{if (&s != this)// 避免自己给自己赋值将空间释放{delete[] _str;_str = new char[s._capacity + 1]; // 给\0strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;
}
几乎相当于拷贝构造
5.2 比较大小
bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;// 在全局无法访问成员变量}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}
在比较的时候,也可以进行字符串和string类的比较,因为字符串会走隐式类型转换,构造一个临时对象,但是不能两个字符串比较,因为运算符重载至少有一个类类型的对象。
5.2 流插入和流提取重载
- 流插入
ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}
- 流提取
void string::clear(){_str[0] = '\0';_size = 0;}istream& operator>>(istream& in, string& s){s.clear();const int N = 256;char buff[N];int i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
这里使用get是因为流提取操作符不能提取空格和换行操作符。开一个buff数组是为了提高效率。
六. string类的深拷贝的现代写法
void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s){string tmp(s._str);// 拷贝构造一个_strswap(tmp);}
这段程序,用s1拷贝构造s2,对s2拷贝构造时,先走初始化列表,由于我们没有显示的对s2的成员变量初始化,并且编译器对于没有初始化的内置类型的成员变量的赋值是未定义的,因此我们需要给缺省值,防止释放野指针。
走完初始化列表之后,s2的_str是空指针, _size和 _capacity都是,这时候与用s构造出来的tmp进行成员变量的交换,就相当于拷贝构造得到了s1。
这样,赋值运算符重载也可以使用现代写法了
string& operator=(const string& s){if (this != &s){string tmp(s);swap(tmp);}return *this;}
用s2拷贝构造一个tmp,然后s1与tmp交换,s1就跟s2一样了,而且将s1给tmp之后,作用域结束,tmp会释放,也就不需要我们手动释放s1了。
究极写法:
string& operator = (string tmp)
{swap(tmp);return *this;
}
我们将s2传过来是传值传参会调用拷贝构造,构造出和s2相同的对象,然后我们将s1和tmp对象的内容进行交换,就相当于将s2赋值给s1了,由于将s1与tmp交换,所以出作用域后,tmp将s1释放。并且在这种写法下,也不用考虑s1和s2是否相同,即便相同我们也是传的s2的拷贝,对形参的改变也不会影响实参。
结语
以上我们就讲完了string类的模拟实现,希望对大家有所帮助,感谢大家阅读,欢迎批评指正!