目录
了解string类
string的内存管理
VS下string的结构
g++下string的结构
string的模拟实现
string的构造函数
浅拷贝
深拷贝
string的遍历
重载 [] 下标访问
迭代器访问
reserve
resize
增删查改
push_back()
append和+=
insert和erase
find
substr
swap
流插入和流提取
getline
string其他基本功能
⭐了解string类
⭐string的内存管理
✨VS下string的结构
- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
- 大多数情况下字符串的长度都小于16,当string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
- 还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
- 还有一个指针做一些其他事情。
- 故总共占16+4+4+4=28个字节。
✨g++下string的结构
- 空间总大小
- 字符串有效长度
- 引用计数
- 指向堆空间的指针,用来存储字符串。
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
⭐string的模拟实现
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
✨string的构造函数
// 为了和标准库区分,此处使用String
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
String(const char* str = "")//默认包含 \0
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非法
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
📖浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
📖深拷贝
✨string的遍历
📖重载 [] 下标访问
char& operator[](size_t pos)//可读可写
{
assert(pos < _size);
return _str[pos];
}
//重载一个const
const char& operator[](size_t pos) const//只读
{
assert(pos < _size);
return _str[pos];
}
首先访问之前需要判断pos是否再合法访问之内,即小于等于size,然后直接返回字符串数组中对应的元素。由于存在const对象和非const对象,所以需要写两个重载版本。
📖迭代器访问
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
其实底层就是指针,所以直接返回对应的地址就可以了。
✨reserve
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n+1];
strcpy(tmp, _str);
//释放旧空间,指向新空间
delete[] _str;
_str = tmp;
//修改capacity,不用修改size
_capacity = n;
}
}
reserve是提前预留部分空间,它接收的空间大小不能比本来就有的容量小,如果n合法,则需要将原数组从旧空间移向一块更大的新空间,并释放掉旧空间。
✨resize
void resize(size_t n,char ch='\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (int i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
resize是设置字符串的大小,如果n比字符串原来的大小小,则会发生截断;如果比原来的大小大,则会reserve一块n大小的空间。
✨增删查改
📖push_back()
void push_back(char ch)
{
//扩容2倍
if (_size == _capacity)
{
reserve(_capacity==0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
直接尾插就可以,需要先判断空间是否足够,最后更新size的大小。
📖append和+=
void append(const char* str)
{
//扩容
//根据追加的字符串的长度扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
// +=
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(const string s)
{
append(s._str);
return *this;
}
append和+=都是在字符串的尾部追加字符或者字符串,需要先判断容量是否足够,不够则需要扩容, 根据追加的字符串的长度扩容。
📖nsert和erase
对于insert,0位置的插入可能产生问题,end是int类型,pos是size_t类型,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换;也可以使用另一种解决方法,将end的初始值赋值为size+1,每次使用这种方法后移字符串 _str[end] = _str[end - 1];,则end最后不会变成-1。
//在pos之前插入
//插入字符
void insert(size_t pos,char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//int end = _size;
0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换
//while (end >= (int)pos)
//{
// _str[end+1] = _str[end];
// end--;
//}
//第二种解决方法
int end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
//在pos之前插入
//插入字符串
void insert(size_t pos,const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_size +len > _capacity)
{
reserve(_size + len+1);
}
//int end = _size;
0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换
//while (end >= (int)pos)
//{
// _str[end+1] = _str[end];
// end--;
//}
//第二种解决方法
//在pos之前插入
int end = _size + len;
//pos 1 2 end
while (end > pos+len-1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos,str,len);
_size+=len;
}
//释放删除
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
// pos+len 存在溢出风险
//if (len == npos || pos + len >= _size)
if (len == npos ||len >= _size-pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
对于erase,需要根据传递的参数的大小来判断需要删除多少个字符。
📖find
//寻找匹配
size_t find(char ch,size_t pos = 0) const
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t find(const char* sub, size_t pos = 0) const
{
assert(pos <= _size);
const char* p=strstr(_str+pos, sub);
if (p)
{
return p - _str;
}
else
return npos;
}
实现方法比较简单,就是普通的暴力查找。
📖substr
截取子串,需要注意len的大小。
string substr(size_t pos = 0, size_t len = npos)
{
string sub;
if (len == npos|| len >= _size - pos)
{
for (size_t i = pos; i < _size; i++)
{
sub += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
✨swap
众所周知,C++算法库里面存在swap这个函数模板,但是为什么string内部自己也有一个swap呢?
如果用std::swap交换两个string对象,将会发生1次构造和2次赋值,也就是三次深拷贝;
而string内部的swap仅仅只交换成员,代价较小。
//交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
为了符合算法库里面swap的用法,可以再将swap重载成全局函数。
void swap(string& x, string& y)
{
x.swap(y);
}
✨ 流插入和流提取
//重载成全局是为了调整顺序
//流插入
ostream& operator<<(ostream& out, const string& s)
{
//这里不需要写成友元函数,因为不需要直接访问私有成员
for (auto ch:s)
{
cout << ch;
}
return out;
}
//流提取
//C++ 流插入,流提取可以支持自定义类型使用
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
char buff[128];
//in >> ch;//默认把空格当作分隔符、换行,不读取
ch = in.get();//C++中读取一个字符
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
//s += ch;//重复+=,会重复扩容,消耗较大
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
因为在这里不需要直接访问类的私有成员,所以流插入和流提取可以不用重载成string类的友元函数。
对于流提取,如果频繁的尾插,会造成频繁扩容。而且C++的扩容和C语言的扩容不一样,C++使用new不能原地扩容,只能异地扩容,异地扩容就会导致新空间的开辟、数据的拷贝、旧空间释放。为了防止频繁扩容,我们可以创建一个可以存储128字节的数组,作为缓冲,如果数组满了,则将这个字符数组追加到s上,如果没慢,但是遇到空格或者换行了也需要追加。
另外由于C++的标准输入流默认把空格和换行当作分隔符,不读取,所以这里要用in.get()来接收字符。
✨getline
基本上可以直接复用流提取的代码。
//读取空格
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
//in >> ch;//默认把空格当作分隔符、换行,不读取
ch = in.get();//C++中读取一个字符
while ( ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
✨string其他基本功能
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
重载运算符,要写成全局的函数。
bool operator==(const string& a ,const string& b)
{
int ret = strcmp(a.c_str(), b.c_str());
return ret == 0;
}
bool operator<(const string& a, const string& b)
{
int ret = strcmp(a.c_str(), b.c_str());
return ret < 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 !(s1 == s2);
}
____________________
⭐感谢你的阅读,希望本文能够对你有所帮助。如果你喜欢我的内容,记得点赞关注收藏我的博客,我会继续分享更多的内容。⭐