目录
一、构造函数

首先要明确一点string最基本要有三个成员变量:str, size , capacity
string类具有很多构造函数,string类的很多构造函数都是没有用的
我们具体实现一个缺省的构造函数
- string(const char* str = "")
- {
- _size = strlen(str);
- _capacity = _size;
- _str = new char[_capacity + 1];
- strcpy(_str, str);
- }
缺省值是“” 它是一个常量字符串,默认后面带一个'\0'
然后依次将_size,_capacity赋值
_size和_capacity就是str的长度,如果我们没有传字符串就用缺省值。
传入字符串就用,传入的 与库中的string类用法保持一致
有人可能会是这样写构造函数
- string(const char* str = "")
- :_size(strlen(str))
- ,_capacity(_size)
- ,_str(new char[_capacity + 1])
- {
- strcpy(_str, str);
- }
这是一个极其隐蔽的错误,首先是初始化列表的初始化顺序,是由声明的顺序来决定的
就算我们修改了声明顺序,也是相当的不推荐这样写,因为在后期维护时,有人可能觉得你的声明顺序不顺眼,而修改声明顺序,从而导致了程序崩溃,因此我们不要使用初始化列表,不要将声明顺序与初始化顺序绑定。
拷贝构造会涉及深浅拷贝的问题
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,浅拷贝会带来数据安全方面的隐患。在进行赋值之前,为指针类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝 [1] 。这种拷贝称为深拷贝。深拷贝有两种方式实现:层层clone的方法和利用串行化来做深拷贝。层层clone的方法:在浅拷贝的基础上实现,给引用类型的属性添加克隆方法,并且在拷贝的时候也实现引用类型的拷贝。此种方法由于要在多个地方实现拷贝方法,可能会造成混论。利用串行化来做深拷贝:为避免复杂对象中使用clone方法可能带来的换乱,可以使用串化来实现深拷贝。先将对象写到流里,然后再从流里读出来
默认拷贝构造,会对内置类型进行浅拷贝或者值拷贝
因此我们要使用深拷贝,就要我们自己实现
- string(const string& s)
- {
- _size = s._size;
- _capacity = s._capacity;
- _str = new char[_capacity + 1];
- strcpy(_str, s._str);
- }
我们使用strcpy会将'\0'拷贝过去
这里使用的是传统写法
下面使用的是现代写法
现代写法是调用构造函数,创建一个局部对象,然后使用swap函数进行深拷贝的交换
局部对象出了作用域就销毁了,不用担心内存泄漏问题。
- void swap(string& s)
- {
- ::swap(_str, s._str);
- ::swap(_size, s._size);
- ::swap(_capacity, s._capacity);
- }
-
- //拷贝构造现代写法
- string(const string& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- string tmp(s.c_str());
- swap(tmp);
- }
我们要写一个swap函数,该函数我们可以调用全局的swap函数

我们使用swap函数将类的每个成员的地址与tmp对象的地址交换,就算是拷贝构造了一个新对象。
但是我们要先对要进行构造的对象,简易初始化,因为delete[]无法处理随机数据。
=操作符重载也涉及深拷贝问题,同时也有传统写法和现代写法.
同时我们要避免自己给自己赋值。或者是看上去是自己给自己赋值。
首先自己给自己赋值毫无意义,同时由于自定义类型通常较大,自己给自己赋值 是没有意义的消耗。
其次我们以深拷贝的思路来写,为了避免内存泄漏,会将原空间先释放,释放之后_str成为nullptr
会将原本的值改变,同时这个类也是无法使用的,会出现野指针问题。
我们还是先写传统写法
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- char* tmp = new char[s._capacity + 1];
- strcpy(tmp, s._str);
-
- delete[] _str;
- _size = s._size;
- _capacity = s._capacity;
- _str = tmp;
- }
- return *this;
- }
先定义一个tmp保存原数据,然后处理完成之后,再让_str指向tmp
这是为了避免new失败,从而导致的数据丢失。
接下来我们看一下现代写法
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- string tmp(s);
- swap(tmp);
- }
- return *this;
- }
=操作符重载的现代写法与拷贝构造现代写法类似,都是调用了构造函数
构造出一个对象然后交换即可,它最大限度的复用了代码。
迭代器是C++ STL容器的统一遍历方式
iterator是像指针一样的类型,有可能是指针也有可能不是指针,但是它的用法像指针一样。
string不喜欢用iterator,因为【】更好用
vector同理
list/map/set……只能用迭代器。
我们这里实现迭代器是为了使用范围for,范围for的底层就是迭代器,编译器会进行傻瓜式的替换,将
- for (auto& e : str)
- {
- cout << e << " ";
- }
直接替换成
- string::itreator it = str.begin();
-
- while (it != str.end())
- {
- cout << *it << endl;
- it++;
- }
类似这样的形式
string的空间是连续的,所以它的迭代器是原生指针
我们目前只能实现iterator和const_iterator这两种迭代器,反向迭代器暂时无法实现。
- typedef char* itreator;
- typedef const char* const_iterator;
需要先将iterator typedef一下,原生指针就相对简单了,将头指针和尾指针返回就可以了

这三个函数比较简单,同时方法类似,就是将三个成员变量返回
注意,要返回const变量,防止在外部修改
- const char* c_str() const
- {
- return _str;
- }
-
- const size_t size()const
- {
- return _size;
- }
-
- const size_t capacity()const
- {
- return _capacity;
- }
我们要先想一想,[ ] 的功能,它的[ ] 类似数组的,可以直接进行修改,也就是要接收要访问的位置,并且为了支持修改我们要返回引用
无论是string类还是vector等其它容器的模拟实现,但凡涉及到位置的函数都要对该位置进行合法性检验,防止越界,[ ] 是用的assert 而at(与[ ] 功能类似)使用的是抛异常。
- char& operator[](size_t pos)
- {
- assert(pos < _size);
- return _str[pos];
- }
-
- const char& operator[](size_t pos)const
- {
- assert(pos < _size);
- return _str[pos];
- }
我们实现了两个[ ] 操作符重载,为了能够使const对象也能够调用[ ]
库中也实现了两种

reserve就是扩容函数,扩大capacity,size不变
扩容在后面的很多函数都有调用
所以先实现出来
扩容也是深拷贝,既然是扩容,因此只能够扩大空间,而不能缩小空间,如果传入的参数比原空间小就不会进行任何处理
注意要释放原空间
- void rersize(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;
- }
push_back就是尾插,我们的成员变量_size指向的是有效数据的下一个位置
我们先扩容,然后在_size位置插入新数据,_size++
插入完之后,很容易忘记一件事,将_size指向的位置再次置为'\0',因为字符串是以'\0'为结尾。
- void push_back(char ch)
- {
- if (_size == _capacity)
- {
- reserve(_capacity == 0 ? 0 : _capacity * 2);
- }
-
- _str[_size] = ch;
- _size++;
- //不要忘记\0
- _str[_size] = '\0';
- }
append与push_back类似,不过append有更多的尾插方式可以选择

我们只重点实现几个函数
- void append(const char* str)
- {
- size_t len = strlen(str);
-
- if (_size + len > _capacity)
- {
- reserve(_size + len);
- }
-
- strcpy(_str + _size, str);
- _size += len;
-
- }
尾插一个字符串,首先还是计算要插入的字符串的长度
判断是否需要进行扩容,之后进行插入数据,我们可以借助strcpy来插入
尾插一个string对象,这个也比较简单
我们复用前面的append就可以
- void append(const string& s)
- {
- append(s.c_str());
- }
尾插n个字符
这个也比较简单
- void append(size_t n, char ch)
- {
- if (n + _size > _capacity)
- {
- reserve(n + _size);
- }
- for (size_t i = 0; i < n; i++)
- {
- push_back(ch);
- }
- }
+=操作符重载也分为两种,一种是尾插一个字符,另一个是尾插字符串
我们直接调用push_back和append即可
- string& operator+=(char ch)
- {
- push_back(ch);
- return *this;
- }
-
- string& operator+=(const char* str)
- {
- append(str);
- return *this;
- }
insert也是老套路,先检查位置合法性
其次是扩容
接下来就是挪动数据
- string& insert(size_t pos, char ch)
- {
- assert(pos <= _size);
-
- //扩容
- if (_size == _capacity)
- {
- reserve(_capacity == 0 ? 4 : _capacity * 2);
- }
-
- //移动数据
- size_t end = _size + 1;
- while (end > pos)
- {
- _str[end] = _str[end - 1];
- --end;
- }
-
- _str[pos] = ch;
- ++_size;
-
- return *this;
- }
挪动数据有一个注意点size_t是无符号整型,它永远大于等于0,所以挪动数据要小心,控制边界。最后插入数据。
erase就是删除指定位置的字符或者字符串

我们会发现len是有缺省值npos的
npos是一个无符号的整型,赋值为-1,也就是说npos是一个42亿左右的字节
STL认为没有长度为42亿左右的字符串
如果不传len也就是说从pos位置删除数据一直到字符串结尾。
如果len+pos大于_size也会删到结尾。
删除也比较简单还是用strcpy将pos位置后面的数据往前覆盖。
- void erase(size_t pos, size_t len = npos)
- {
- assert(pos < _size);
- //判断原字符串是否大于len+pos
- if (len == npos || pos + len > _size)
- {
- _str[pos] = '\0';
- _size = pos;
- }
- else
- {
- strcpy(_str + pos, _str + pos + len);
- _size -= len;
- }
- }
clear函数十分的简单,它就是清空字符串,它并不会涉及到缩容。
- void clear()
- {
- _str[0] = '\0';
- _size = 0;
- }
流提取和流插入重点说流插入操作符重载
- istream& operator>>(istream& in, string& s)
- {
- char ch;
- ch = in.get();
-
- const size_t N = 32;
- char buff[N];
- size_t i = 0;
-
- while (ch != ' ' && ch != '\n')
- {
- buff[i++] = ch;
- if (i == N - 1)
- {
- buff[i] = '\0';
- s += buff;
- i = 0;
- }
-
- ch = in.get();
- }
-
- buff[i] = '\0';
- s += buff;
-
- return in;
- }
-
- ostream& operator<<(ostream& out, const string& s)
- {
- for (size_t i = 0; i < s.size(); i++)
- {
- out << s[i];
- }
- return out;
- }
流提取操作符重载并没有声明为友元,因为我们有[ ] 可以直接访问字符串
流插入要注意的问题有很多,为了仿照库中功能
首先要清空空字符串,其次是cin输入到库中的string时,它并不会读取空格和回车
还有最后一点是,我们每次尾插数据string会频繁的检查是否需要扩容,开辟空间
消耗较大,因此我们可以使用一个buff数组先存储输入的数据,然后一组一组的插入到字符串中
find重点实现两个重载
一个是在字符串中查找字符,另一个是在字符串中查找子串
两者都是找到返回下标,找不到返回npos
在查找子串时可以使用strstr来找,找到返回地址,找不到返回nullptr
可是我们是要返回下标,可以使用找到的元素的地址减去首元素的地址,也就减去是_size
- size_t find(char ch, size_t pos = 0)const
- {
- assert(pos < _size);
- for (size_t i = pos; i < _size; i++)
- {
- if (ch == _str[i])
- {
- return i;
- }
- }
- return npos;
- }
-
- size_t find(const char* sub, size_t pos = 0)const
- {
- assert(pos < _size);
- assert(sub);
-
- char* ptr = strstr(_str + pos, sub);
- if (ptr == nullptr)
- {
- return npos;
- }
- else
- {
- return ptr - _str;
- }
- }
这个函数有点多余但是也实现一下。
首先还是检查pos位置,计算出字符串真正的长度,然后定义一个临时对象将数据尾插到对象中,然后返回即可。
- string substr(size_t pos, size_t len = npos)const
- {
- assert(pos < _size);
-
- size_t reallyLen = len;
- if (len == npos || pos + len > _size)
- {
- reallyLen = _size - pos;
- }
-
- string sub;
- for (size_t i = 0; i < reallyLen; i++)
- {
- sub += _str[pos + i];
- }
-
- return sub;
- }
这些操作符我们只需要实现==和>剩下的直接服用即可
>和=可以借助strcmp来解决


通过它的返回值不同就可以判断出是否大于
- bool operator==(const string& s)const
- {
- return strcmp(_str, s._str) == 0;
- }
-
- bool operator>(const string& s)const
- {
- return strcmp(_str, s._str) > 0;
- }
-
- bool operator>=(const string& s)const
- {
- return (*this > s) || (*this == s);
- }
-
- bool operator<(const string& s)const
- {
- return strcmp(_str, s._str) < 0;
- }
-
- bool operator<=(const string& s)const
- {
- return !(*this > s);
- }
- bool operator!=(const string& s)const
- {
- return !(*this == s);
- }
resize与reserve很容易混淆,当我们实现它的底层之后就自然而然地理解它们两者的不同了
resize也会开空间,它不但会将_capacity改变,同时也会改变_size,因为resize会进行初始化
默认是用'\0'来进行初始化。
同时resize支持缩容
- void rersize(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;
- }
扩容后之后要用传入的参数进行初始化,直接尾插就可以了。
而缩绒的操作就简单多了,直接在容量的下标位置加上'\0'就可以,别忘了将size变成n
例如:以上就是今天要讲的内容,本文仅仅简单介绍了string的模拟实现。string中其它的函数实现可参考STL的源码。