目录
构造函数
提前准备工作:
string的成员变量
private:
char* _str;
size_t _size;
size_t _capacity;
需要的头文件
#include<iostream>
#include"string.h" //它是有个头文件的
有参构造
string(const char* str){
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
}
有人可能会走参数列表,单走参数列表的话,在此处是没有特别大的意义,且很容易出错。
这是正确使用参数列表的方法
string(const char* str= "")
:_str(new char[_capacity + 1]),
_size(strlen(str)),
_capacity(_size)
{
strcpy(_str, str);
}
很多会变成出错的原因,就是参数列表没有对应好成员变量的顺序,所以为了避免出现这样的问题就是不用参数列表,直接在函数内定义
析构函数
~string(){
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
c_str
先用来写出打印出自定义类型的,因为IO流不能输出自定义类型
所以要自己写
const char* c_str(){
return _str;
}
无参构造:
string(){
_str = nullptr;
_size = 0;
_capacity = 0;
}
很多朋友可能会写成这个样子,并觉得没有任何问题,那我们就来调用一下
出现了异常,原因就是c_str在返回的时候对空指针进行了解引用的行为
所以报错了
所以要对无参构造怎么搞也要有一个初始值
string()
:_str(new char[1]),
_size(0),
_capacity(0)
{
_str[0] = '\0';
}
无参和有参的结合
那么这么麻烦,其实可以将有参构造和无参构造合二为一的,在缺省参数上赋予一个空字符串
string(const char* str = ""){
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
此处的顺序结构也不能乱,当时我就是把_str放到最上面,导致其实_capacity是随机值的时候
就把空间内容赋给_str,最终报错
//_str = new char[_capacity + 1];
//_size = strlen(str);
//_capacity = _size;
}
拷贝构造我们往下点再说 !
operater[]的实现
char& operator[](size_t i) {
assert(i <= _size);
return _str[i];
}
assert的条件里无需再写 i>=0了,因为参数类型是size_t,所以能很好地避免出现负数的情况
简易版的迭代器
begin
typedef char* iterator;
iterator begin(){
return _str;
}
end
上面已经typedef,这里就不再写了
iterator end(){
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
原因:
因为string是物理线性结构,这个字符的地址的下个地址,就是下一个字符的地址,这是它的底层结构的特性。如果换成物理上不是连续的地址的时候,我们就不能够这样去实现这种迭代器了
比如,链表,树结构
reserve
思想步骤
- 如果要新空间的大小大于原空间的大小的话,就要扩容
- 创建一个临时对象,来接受新增加的空间
- 将原来空间里的数据,拷贝到新空间里面去
- 将旧空间释放掉
- 将新空间的指针变量交给原来的指针变量
- 更新_capacity
void reserve(size_t i) {
if (i > _capacity) {
char* tmp = new char[i];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = i;
}
}
获取_capacity 和 _size
size_t getCapacity() {
return _capacity;
}
size_t getSize() {
return _size;
}
测试
void test1() {
string s1("hello reverse");
cout << "我原来的大小是:" << s1.getSize() << endl;
cout << "我原来的空间是:" << s1.getCapacity() << endl << endl;
s1.reserve(100);
cout << "我现在的大小是:" << s1.getSize() << endl;
cout << "我现在的空间是:" << s1.getCapacity() << endl;
}
push_back
插入数据最核心的点就是扩容, 在push_back之前先完成reserve
开空间时要多开一个,留给\0,因为capacity指的是有效空间,真实的空间是会比他多一个的
思想步骤
-
判断空间是否已满
-
若已满,则进行扩容,用reserve即可
-
将要插入的ch放进对象里的最后一位
-
++size
-
将原本被ch覆盖掉的'\0'补回去
void push_back(char ch) {
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : (_capacity * 2);
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
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是能够将'\0'也拷贝进去的
insert字符
思想步骤:
- 判断空间是否已满
- 给予一个变量,用来记录开始移动的数据
- 给予判断条件,开始移动
- 放入新数据
- ++_size
测验
void test3() {
string s1("hello insert");
cout << s1.c_str() << endl << endl;
s1.insert(0, 'x');
cout << s1.c_str() << endl << endl;
}
我们现在来看看第三步,给予判断条件
size_t end = _size;
while (end >= pos){
_str[end+1] = _str[end];
--end;
}
_str[pos] = ch;
如果是这样的话,能不能成功 ?
肯定不行啦,都这样问了
循环条件的问题
当在0位置插入的时候就挂了
因为end是size_t定义的,当pos为0时,不断--end,当end被减到-1时,就变成了无穷大,所以又变成了>0,陷入了一个死循环
改int也不行
因为当两边的操作符类型不一样的时候,它们会发生类型提升,有符号会向无符号转,所以在比较的时候,会转成无符号进行比较,所以结果还是一样的
所以正确的做法是:避免pos变为-1;
while (end > pos) {
_str[end] = _str[end-1];
--end;
}
变成这样之后,就要考虑一个重点问题了,'\0'怎么办?
这里有两种方法
- end 从_size 变成 _size + 1,让end - 1 所代表的 '\0' 有去处
- 后面给它加一个,_str [_size] = '\0' ;
最终代码为:
void insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
++_size;
}
insert字符串
思想步骤跟insert字符差不多
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) {
_str[end] = _str[end-len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
}
strncpy:避免\0拷贝进去
npos
const static size_t npos = -1 ; 它在私有成员变量里也能够这么定义的,这算是一个特殊处理了吧,既算声明,也算定义
以前我们在学习类的静态成员变量的时候
类的静态成员变量定义和初始化要分离,它不走初始化列表,需要在类外给定义
原因很简单:静态成员变量只能初始化一次,如果你在类里面给缺省值,就相当于我每创建一个对象,就要对这个静态成员变量初始化一次,显然有违背于静态成员变量规则
现在可将它看作是一种特例
没有任何报错
它并不是说加了const就可以!!!
只有整数类型才能接收
erase
清除数据,缩短长度
判断删除数据的长度
再从某个位置清空数据的时候,要么全删,要么删部分长度
全删
当要删的长度刚好到从开始位置到结束位置
当要删的长度是无穷大,即-1
所以判断条件可为
if(len == npos || pos + len >= _size)
部分删
从某个位置开始删除一般长度的字符串,用strcpy即可
最终代码为
void erase(size_t pos, size_t len = npos) {
assert(pos <= _size);
if (len == npos || pos + len >= _size) {
_str[pos] = '\0';
_size = pos;
}
else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
swap
直接调用库里面的swap换一下就可以了
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
find字符
既然是找,给个循环就可以了
size_t find(char ch, size_t pos = npos) {
for (size_t i = 0; i < _size; i++ ) {
if (_str[i] == ch)
return i;
}
return npos;
}
测试
find字符串
用strstr()函数
它会返回第一次出现在str1里面的str2的指针,没找到就返回空
代码
size_t find(const char* str, size_t pos = npos) {
const char* ptr = strstr(_str + pos, str);
if (!ptr) {
return npos;
}
else {
return ptr - _str;
}
}
测验
substr
截取一段字符串,肯定是要返回的
代码
string substr(size_t pos = 0, size_t len = npos) {
assert(pos < _size);
size_t end = pos + len;
if (len == npos || pos + len > _size) {
end = _size;
}
string str;
str.reserve(end - pos);
for (size_t i = pos; i < end; i++) {
str += _str[i];
}
return str;
}
测试
问题提出
可能很多人会认为到这里已经没有任何问题了
那我换种样子呢?
void test6() {
string s("hello substr");
string s2;
s2 = s.substr(6, 6);
cout << s2.c_str() << endl;
}
换动是将string s2 = s.substr(6, 6);
换成 string s2 ; s2 = s.substr(6, 6);
结果就是崩了 !!!
何罪至此?
问题分析
是否还记得这张图,对的,就是我们没有拷贝构造这种东西。
之前没有问题,全是编译器优化的功劳,当在同一段表达式的时候,编译器会直接优化很多,导致我们肉眼看不出来任何区别,
当我们没有写拷贝构造的时候,类里面的默认拷贝构造就只是一个简简单单的浅拷贝(值拷贝),对于我们new出来的空间,只是拷贝一个临时对象,但指向的内容还是一样的
深拷贝的解决方案就是会开一个一模一样的空间,你指你的,我指我的,不会互相打扰的,你被挂掉就挂掉吧,反正临时对象不会挂掉
问题解答
构建拷贝和赋值
string(const string& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
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;
}
测试
此刻便是平安无事
流插入和提取
因为在此处我无需再次使用类里面的私有成员变量,所以在这里无需使用友元函数声明
流插入
循环遍历即可
ostream& operator<<(ostream& out, const string& s) {
for (auto ch : s) {
out << ch;
}
return out;
}
流提取
istream& operator>>(istream& in, string& s) {
char ch;
in >> ch;
while (ch != ' ' && ch != '\n') {
s += ch;
in >> ch;
}
return in;
}
我们先看看这样子行不行
发现即使我们按下回车,也依旧可以继续输入
这就需要我们知道关于cin的一个性质
cin对于空格和换行
在我们使用流插入的时候,或者说平常使用C的scanf的时候,它会自动省略空格和换行
int a, b;
scanf("%d%d", &a, &b);
printf("%d %d", a, b);
int a, b;
cin >> a >> b;
cout << a<<" " << b;
都是下面结果
所以我们就可以用c++里的另一个函数,专门取出字符的函数
get( )
get函数,可以专门提取出字符
istream& operator>>(istream& in, string& s) {
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n') {
s += ch;
ch = in.get();
}
return in;
}
换成这样,我们就能正常的读取到空格和换行
频繁扩容问题
再谈论另外一个问题,我们使用的是s+=ch; 也就是说,我们每次新增一次字符,我们就要扩容一次,因为我们要调用reserve函数,我们可以知道频繁扩容给我们带来的代价是比较大的
性能开销:容器在扩容时需要重新分配内存并复制现有元素到新的内存区域。这个过程是昂贵的,因为它涉及到内存分配和数据复制
内存分配:频繁的内存分配和释放可能导致内存碎片,影响程序的内存使用效率
时间复杂度:如果一个容器在添加元素时需要频繁扩容,那么其时间复杂度可能从理论上的O(1)增加到O(n),因为每次扩容都需要复制所有现有元素
异常安全性:在C++中,如果内存分配失败,会抛出异常。频繁的内存分配增加了抛出异常的可能性,这可能需要额外的异常处理逻辑。
资源竞争:在多线程环境中,频繁的内存分配和释放可能导致资源竞争,从而影响程序的并发性能。
解决办法
所以,为了解决这个问题,我们可以像缓冲区一样,构建一个缓冲
即,先划分一个相对比较合理的空间,让内容先填进去,等到满了,或者触发了某个特定条件,比如空格跟换行,再把缓冲内容倒进容器里。这样就可以避免频繁扩容
所以比之前较为优化的写法为
istream& operator>>(istream& in, string& s) {
s.clear();
char buffer[128];
char ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n') {
buffer[i] = ch;
i++;
if (i == 127) { //不能到128,因为0-127就是128位了
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = in.get();
}
if (i > 0) {
buffer[i] = '\0';
s += buffer;
}
return in;
}
以上就是本次博客的学习内容,如有错误,还望各位大佬指点,谢谢阅读