了解过string常用接口后,接下来的任务就是模拟实现string类。
目录
VS下的string结构
默认成员函数和简单接口
string结构
c_str()、size()、capacity()、clear()、swap()
构造函数
拷贝构造函数
赋值重载
析构函数
访问及遍历
容量操作
reserve
resize
修改操作
push_back
append
operator+=
insert
earse
输入/输出运算符重载
VS下的string结构
■首先测试下string结构的大小(注:下述测试是在32位平台上进行的,指针占4个字节)
int main()
{
string s;
cout << sizeof(s) << endl;
return 0;
}
通过调试窗口理解string的结构:
int main()
{
string s("Hello");
return 0;
}
通过测试知道string总共占28个字节,调试发现结构中有一个联合体,用来定义string中字符串的存储空间: 1.当字符串长度小于16时,使用内部固定的字符数组来存放 。2.当字符串长度大于等于16时,从堆上开辟空间。
union _Bxty
{
char _Buf[16];
char* _Ptr;
char _Alias[16];
} _Bx;
除了联合体,还有一个size_t字段保存字符串长度,还有一个size_t字段记录容量!此外,还有一个做其它事情的指针。总大小:16+4+4+4 = 28 !!
默认成员函数和简单接口
下述接口的模拟实现仅仅是一种写法,可能不是最优的,也可能存在一些BUG,仅供参考!
string结构
c_str()、size()、capacity()、clear()、swap()
为了测试方便,先将上述几个常用且简单的接口进行实现:
//返回指向_str的指针 const char* c_str() { return _str; } //返回_size size_t size() const { return _size; } //返回_capacity size_t capacity() const { return _capacity; } //清空有效字符 void clear() { _size = 0; _str[0] = '\0'; }
复习时间:const修饰返回值的作用是避免返回值被修改,const写在成员函数后面修饰的是隐藏的this指针,不能对类的成员进行修改!
swap:string类中提供一个swap接口用来交换两个string对象。
void swap(string& s) { std::swap(_str, s._str); std::swap(_size,s._size); std::swap(_capacity, s._capacity); }
构造函数
分析:对于string s1(“Hello”)/ string s2 都能处理;
逻辑思路:通过strlen计算str的字符个数,确定size和capacity的值,空间一般多开一个,因为要算上\0。在将str 拷贝到开辟好的空间中。
//构造 string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity+1]; strcpy(_str,str); cout << "调用构造函数!" << endl; }
测试代码:
//构造函数测试 void TestString1() { string s1("zhang"); string s2; cout << s1.c_str() << endl; }
拷贝构造函数
分析:string s1(s2);
按照拷贝对象的容量申请空间,对拷贝对象进行拷贝。
传统写法:
//拷贝构造 string(const string& s) { _str = new char[s._capacity]; _size = s._size; _capacity = s._capacity; strcpy(_str,s._str); cout << "调用拷贝构造!" << endl; }
测试代码:
//拷贝构造测试 void TestString2() { string s2("Hi zxy!"); cout << s2.c_str() << endl; string s3(s2); cout << s3.c_str() << endl; }
现代写法:这里注意在初始化列表对s进行初始化,否则当tmp对象析构的时候,会出现析构野指针的问题。
//现代写法 string(const string& s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); //this->swap(tmp); swap(tmp); cout << "拷贝构造" << endl; }
赋值重载
传统写法:
注意:不要见到&就是引用,在下述判断代码中,&s取出的s的地址,和this指针进行比较,如果地址不相同则进行赋值工作!
赋值的过程需要将右操作数赋值给做左操作数,这时从新根据右操作数的容量申请一块空间,将左操作数空间释放。_str指向刚刚开好的空间,将右操作数中的字符串拷贝到_str中。
//赋值重载 string& operator=(const string& s) { //两操作数地址不相等 if (this != &s) { //申请一块能容纳右操作数的空间 char* tmp = new char[s._capacity]; delete[] _str; _str = tmp; strcpy(_str,s._str); _size = s._size; _capacity = s._capacity; } return *this; }
测试代码:
//赋值重载测试 void TestString3() { string str1("Hello World!"); string str2("Hi!"); cout <<"赋值前str1:"<< str1.c_str() << " str2:" << str2.c_str() << endl; str1 = str2; cout <<"赋值后str1:"<< str1.c_str() << " " << str2.c_str() << endl; }
测试结果:
现代写法:
第二种相比较第一种写法省去了一次拷贝构造,传值调用形参是实参的临时拷贝,所以第二种写法更加的简洁,第一种写法更容易理解。
//赋值重载现代写法1 string& operator=(string& s) { if (this != &s) { //string tmp(s._str); string tmp(s); swap(tmp); } return *this; }
//赋值重载现代写法2 string& operator=(string s) { swap(s); return *this; }
析构函数
//析构 ~string() { _capacity = _size = 0; delete[] _str; _str = nullptr; cout << "调用析构函数!" << endl; }
测试代码:
//析构函数测试 void TestString4() { string str("zxy"); cout << str.c_str() << endl; }
访问及遍历
operator[ ]
char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
begin/end
typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; }
迭代器:访问string对象、vector等对象元素的一种通用机制。迭代器类似于指针类型(不一定是指针),使用迭代器可以访问某个元素。范围for的支持就和迭代器有关:
void TestString2() { string s2("My name is zxy!"); string::iterator it1 = s2.begin(); while (it1 != s2.end()) { cout << *it1; it1++; } cout << endl; string::iterator it2 = s2.begin(); while (it2 != s2.end()) { (*it2)++; cout << *it2; it2++; } }
证明范围for是依赖于迭代器实现的:
1.屏蔽掉string中的begin或者end.
2.放开屏蔽,更改begin的命名为Begin。一样的报错
3.提供小写的begin和end
综上所述:便捷神秘的范围for,运用迭代器这种通用的机制来实现元素的遍历和访问。
容量操作
reserve
扩容:申请一块n大小的空间,将_str指向的字符串拷贝到tmp指向的空间中,清理_str指向的字符串并将_str指向新空间,更新容量的大小。
//扩容 void reserve(size_t n) { //新开一块空间 char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; }
void TestString6() { string ss("Hello World!"); cout << "size:" << ss.size() << endl; cout << "capacity:" << ss.capacity() << endl; cout << "str:" << ss.c_str() << endl; cout << "--------------reserve--------------" << endl; ss.reserve(100); cout << "size:" << ss.size() << endl; cout << "capacity:" << ss.capacity() << endl; cout << "str:" << ss.c_str() << endl; }
resize
改变字符串中有效字符的个数,当n>_size时,其余位置用ch占位。当n<_size时只有前n个字符是有效字符。
//resize,更改有效元素个数的大小 void resize(size_t n,char ch = '!') { if (n > _size) { reserve(n); for (size_t i = _size;i < n; i++) { _str[i] = ch; } _size = n; _str[n] = '\0'; } else { _size = n; _str[n] = '\0'; } }
上述代码中,当n小于_size时,在n位置填入\0,有效字符的个数正好是n,因为字符数组的下标是从0开始的。
void TestString7() { string ss("Hello"); cout << "str:" << ss.c_str() << endl; cout << "size:" << ss.size() << endl; //字符串有效字符个数增多 ss.resize(10,'x'); cout << "str:" << ss.c_str() << endl; cout << "size:" << ss.size() << endl; //字符串有效字符个数变少 ss.resize(3); cout << "str:" << ss.c_str() << endl; cout << "size:" << ss.size() << endl; }
修改操作
push_back
尾插操作,也就是在字符串末尾追加一个字符:
//追加字符 void push_back(char ch) { if (_size == _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; //扩容 reserve(newcapacity); } _str[_size++] = ch; _str[_size] = '\0'; }
append
在_str末尾追加一个字符串:
//追加字符串 void append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size+len+1); } strcpy(_str+_size,str); _size += len; }
operator+=
重载+=运算符,使+=既可以追加字符右可以追加字符串,实现思路和上述基本一样,这里直接调用上面的接口。
//运算符重载 string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; }
测试push_back/append/+=性能
void TestString3() { string ss("Hi"); cout << "str:" <<ss.c_str() << endl; ss.append("zxy"); cout << "append(zxy):" <<ss.c_str() << endl; ss.push_back('!'); cout << "push_back(!):" <<ss.c_str()<< endl; ss += '!'; cout << "+=字符(!):" <<ss.c_str() << endl; ss += "bd"; cout << "+=字符串(bd):" <<ss.c_str() << endl; }
insert
insert这里重载了两个接口,分别是用来插入字符和插入字符串:
●在pos位置插入字符
//插入字符 string& insert(size_t pos,char ch) { //检查pos是否越界 assert(pos <= _size); if (_capacity == _size) { //扩容 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } //挪动数据 size_t end = _size + 1; while (end > pos) { _str[end] = _str[end-1]; end--; } _str[pos] = ch; _size++; return *this; }
void TestString9() { string ss("Helloworld!"); ss.insert(6, ' '); cout << ss.c_str() << endl; }
●在pos位置插入字符串:
//插入字符串 string& insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _size) { //扩容 size_t newcapacity = _capacity == 0 ? 4 : _size+len; reserve(newcapacity); } //挪动数据 size_t end = _size + len; while (end > pos+len-1) { _str[end] = _str[end - len]; end--; } strncpy(_str+pos,str,len); _size += len; return *this; }
void TestString10() { string ss("Hi Lisi!"); ss.insert(3, "zxy I am "); cout << ss.c_str() << endl; }
earse
删除pos后的len个字符,如果len == npos,将pos后的数据全部删除!
//删除字符 string& earse(size_t pos,size_t len = npos) { assert(pos <= _size); //如果pos后面的数据不够删或者=npos,pos后数据全部删除 if (len == npos || len + pos >= _size-pos) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; }
输入/输出运算符重载
小提问:输入和输出的重载为了和我们的使用习惯保持一致一般会定义在全局,那么一定要在对应自定义类型中声明友元吗?
答:不一定,声明友元的原因是要访问类中的私有成员,如果在重载的过程中不涉及私有成员的访问,就不用写友元声明!
//流插入 ostream& operator<<(ostream& out,string& s) { for (size_t i = 0; i < s.size(); i++) { out << s[i]; } return out; }
流提取的实现使用了一个字符数组,in.get()用于读取字符,当数组满时追加到_str中,没满时将数据存放到buff数组中,最后输入结束时如果buff中还有数据的话将剩余数据追加到_str中,不过要注意在buff的数据末尾添加一个\0。
//流提取 istream& operator>>(istream& in, string& s) { s.clear(); char ch = in.get(); size_t i = 0; char buff[128] = {'\0'}; while (ch != ' ' && ch != '\n') { if (i == 127) { s += buff; i = 0; } buff[i++] = ch; ch = in.get(); } if (i > 0) { buff[i] = '\0'; s += buff; } return in; }