目录
一、构造函数
二、拷贝构造
三、=操作符重载
四、迭代器
五、c_str 、capacity、size
六、[ ]操作符重载
七、reserve
八、push_back
九、append
十、+=操作符重载
十一、insert
十二、erase
十三、clear
十四、<<操作符重载 >>操作符重载
十五、find
十六、substr
十七、> 、>=、<、<=、==、!=
十八、resize
总结
一、构造函数
首先要明确一点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的源码。