前言
👧个人主页:@小沈YO.
😚小编介绍:欢迎来到我的乱七八糟小星球🌝
📋专栏:C++ 心愿便利店
🔑本章内容:探索string底层实现
记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~
提示:以下是本篇文章正文内容,下面案例可供参考
一、写实拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
二、string类常用接口实现
2.1 成员变量
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
——————————————————————————
const static size_t npos=-1;//静态的成员变量是不可以给缺省值,必须在类外面进行初始化
但是const静态的整形可以(特例)
};
const size_t string::npos = -1;//支持在类外面初始化
2.2 默认构造函数
string()
:_str (new char[1]{'\0'})
,_size(strlen(0))
,_capacity(0)
{
}
//常量字符串规定后面默认就有\0
//strlen 计算的是字符串中有效字符的个数,不算 '\0',而常量字符串的结尾默认有一个 '\0',用 new开辟空间的时候需要多开一个用来存储结尾的 \0
string(const char* str="")
:_size(strlen(str))//0
,_capacity(_size)//0
{
_str = new char[_capacity + 1];//1
strcpy(_str, str);
}
对于上述代码中形参上必须加 const 修饰,这样才能用 C 语言中的常量字符串来初始化 string 类对象,上面两种初始化的方式都可以一个是无参的一个是有缺省值的,形参的的缺省值直接给一个空字符串即可如上述,要注意初始化列表是按照声明的顺序来初始化的。_capacity表示的是可以存储有效字符的容量,而字符串结尾默认的 ‘\0’ 并不算作有效字符,因此最初的 _capacity 就是形参 str 的长度。
🌟对于为什么不可以用 ‘\0’ “\0” 和nullptr当缺省值?
答案:
- 首先对于’\0’:str是一个char*类型,而’\0’是一个char类型的 类型不匹配
- 其次对于给缺省值nullptr:strlen是不会去检查空的,它是一直找到 \0为止的,也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。
- 最后对于"\0":它表示该字符串有一个字符 ‘\0’ ,它的结尾还有一个默认的 ‘\0’,因此有两个 ‘\0’
2.3 拷贝构造函数
//传统写法
string(const string& s)//拷贝构造
{
_str = new char[s._size+1];
strcpy(_str,s._str);
_size = s._size;
_capacity = s._capacity;
}
上述我们称为传统写法下面这种我们称为现代写法,对于现代写法不需要亲自去申请空间初始化,而是调用构造函数去完成。最后再将初始化好的 tmp 交换过来
还要注意:一定要通过初始化列表对 *this 进行初始化,不然交换给 tmp 后,里面都是随机值(不同编译器有的会处理有的不会),最终出了作用域 tmp 去销毁的时候就会出问题。
//现代写法:下面代码是两种不同的现代写法
不过要注意如果 string 对象中有 '\0',只会把 '\0' 前面的字符拷贝过去
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)//拷贝构造
,_size(0)
,_capacity(0)//不处理tmp里面是随机值析构就会发合适呢个错误
{
string tmp(s._str);//构造
swap(tmp);
}
___________________________________________________________________________________________
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);
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
2.4 operator==
//传统写法
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;
}
return *this;
}
对比于上述代码写法,下述这种写法通过调用拷贝构造来申请空间,在利用局部对象出了作用就会被销毁的特点,将需要释放的资源通过 swap 交换给这个局部变量,让这个局部变量帮我们销毁。
//现代写法:这里不能直接用 swap 交换两个 string 类对象,会导致栈溢出
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
if (this != &s)
{
//string tmp(s);//调用拷贝构造
string tmp(s._str);//调用构造
swap(tmp);//这里s2换给了tmp本来s2要析构现在tmp出了作用域调用析构也就意味着原始的s2的空间释放了
}
return *this;
}
下述这种写法不用我们去调用构造或者拷贝构造,直接通过形参去调用,传值传参会调用拷贝构造,tmp是它的实参调用拷贝构造构造的一个一摸一样的空间。
//现代写法的优化版本
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
2.5 operator[]
这两个运算符重载函数构成函数重载,对象在调用的时候会找最匹配的,非const对象会调用非const版本,const 对象会调用const版本。
//可读可写版本
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//只可以读版本
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2.6 c_str
返回的是一个const char*的数组指针,只读不写,这个数组包含的字符序列与string对象的值相同,另外还包含一个以空字符(‘\0’)结尾的字符串,加上 const,这样普通的 string 类对象可以调用,const 类型的 string 类对象也可以调用,普通对象来调用就是权限的缩小。
const char* c_str()
{
return _str;
}
2.7 size()
size_t size()const
{
return _size;
}
2.8 capacity()
size_t capacity()const
{
return _capacity;
}
三、迭代器的实现
iterator 是 string 类的内嵌类型,也可以说是在 string 类里面定义的类型,在一个类里面定义类型有两种方法,typedef 和 内部类。
3.1 begin()和end()
//非const调用
typedef char* iterator;//string 类的 iterator 是通过typedef来实现的
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
——————————————————————————————————————————————————————————
//const调用
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
3.2 范围for
支持范围for写法,范围for的底层是迭代器实现的,但是范围for不是万能的,范围for遇上const类型的对象,会报错,因此要提供const迭代器:typedef const char* const_iterator;
string s1("hello world");
for (auto ch : s1)
{
ch++;
cout << ch << " ";
}
cout << endl;
四、string类增删查改
4.1 reserve():增容函数
reserve 函数不会进行缩容,因此在扩容前要先进程判断,只有当形参 n 大于当前容量的时候才扩容。
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//开n+1个是因为n个有效字符,另一个是'\0'
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
4.2 push_back():尾插字符
对于尾插首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数来进行扩容(选择2倍扩容),
扩容后将ch加到str上,然后 _size++ 最后手动添加一个新的 \0 。
void push_back(char ch)
{
if (_size == _capacity)
{
//reserve(_capacity * 2);
//对于这里可以采用三目来判断_capacity是否为0,若不进行判断空串的_capacity是0,进行扩容0*0=0就会发生越界访问
reserve(_capacity==0?4:_capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
4.3 append():追加字符串
void append(const char * str)
{
size_t len = strlen(str);
if (_size +len> _capacity)
{
reserve(_size+len);//这里就不需要担心_capacity为0的情况
}
strcpy(_str + _size, str);//strcpy会把'\0'也拷贝过去
size += len;
}
4.4 operator+=
+=要有返回值返回*this
//追加一个字符复用push_back()
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//追加一个字符串复用append()
string& operator+=(const char* str)
{
append(str);
return *this;
}
4.5 insert
在pos位置插入字符
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
while (end >= pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
}
注意:上述代码挪动数据时的判断条件中,end 和 pos 都是 sizt_t 类型,例如当 pos = 0 的时候 end >= pos,end–,一直减到end=-1但是end是一个无符号整形,所以循环条件一直成立还可以进入循环, 所以下面有两种修改方式:
为了防止整型提升有以下两种写法:
void 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++;
}
——————————————————————————————————————————————————————————————————————————————————
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
int end = _size;
换成有符号也会报错因为操作符的两边两个数据的类型不同时会发生类型提升,
end变成有符号类型也会被提升到无符号类型因为pos是无符号类型,所以可以强转pos->(int)pos
while (end >=(int) pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
}
在pos位置插入字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len;
while (end >= pos+len)
{
_str[end] = _str[end-len];
--end;
}
strncpy(_str + pos, str,len);
_size += len;}
}
4.6 erase
void erase(size_t pos, size_t len=npos)
{
assert(pos < _size);
if (len == npos||pos+len>=_size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos + len;
while (begin <= _size)
{
_str[begin - len] = _str[begin];
begin++;
}
_size -= len;
}
}
4.7 resize
size_t find(char ch,size_t pos=0)//这里给了一个半缺省
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
4.8 find
size_t find(const char* str, size_t pos = 0)//半缺省
{
const char* p = strstr(_str+pos, str);//strstr找到返回所在位置指针否则返回空
if (p)
{
return p - _str;//返回下标
}
else
{
return npos;
}
}
4.9 substr
这里就表明我们一定要手写一个拷贝构造,编译器默认生成的拷贝构造是一个浅拷贝,会发生调用两次析构的问题所以要手写一个深拷贝
string substr(size_t pos, size_t len = npos)
{
string s;
size_t end = pos + len;
if (len == npos || pos + len >= _size)
{
len = _size - pos;
end = _size;
}
s.reserve(len);
for (size_t i = pos; i < pos + len; i++)
{
s += _str[i];
}
return s;//s是一个浅拷贝出了作用域s销毁
}
五、string类运算符重载
5.1 operator< == <= > >= !=
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 !(*this <= s );
}
bool operator>=(const string& s)const
{
return !(*this < s);
}
bool operator!=(const string& s)const
{
return !(*this == s);
}
5.2 operator<<
因为类函数有this指针传参数容易发生错误匹配原因,>>和<<运算符重载要写在类外面
无论是形参还是返回值,只要涉及到 ostream 或 istream 都必须要用引用
//有以下两种写法:
ostream& operator<<(ostream& out,const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
_______________________________________________________________________
范围for(这里使用范围for要调用const迭代器)
ostream& operator<<(ostream& out,const string& s)
{
for (auto ch : s)//s是一个const对象要用const迭代器
out << ch;
return out;
}
5.3 operator>>
空格符 ’ ’ 和换行符 \n不能直接用 istream 对象来读取的,in >> ch 是读不到空格符和换行符。需要借助 get() 成员函数才能读取到空格符和换行符。
istream& operator>>(istream& in, string& s)
{
s.clear();//清掉原始数据不然就变成尾插了
char ch;
//in >> ch;//拿不到空格或者换行例如sacnf拿不到空格所以出现了getchar
ch = in.get();
while (ch!=' '&&ch!='\n')
{
s += ch;
ch = in.get();
}
return in;
}
对于上面这种写法,在输入的字符串很长的情况下会多次调用 reserve 进行扩容,所以可以采用下述优化版本来实现:先开辟一个数组,将输入的字符存储到数组中,然后从数组中拷贝到string对象中,数组出了作用域就会销毁
istream& operator>>(istream& in, string& s)
{
s.clear();//清掉数据不然就变成尾插了
char buff[128];
size_t i = 0;
char ch;
ch = in.get();
while (ch!=' '&&ch!='\n')
{
buff[i++] = ch;
if (i == 128)
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}