目录
构造函数
提前准备工作:
有参构造
析构函数
c_str
无参构造:
无参和有参的结合
operater[]的实现
简易版的迭代器
begin
end
原因:
reserve
思想步骤
获取_capacity 和 _size
测试
push_back
思想步骤
append
insert字符
思想步骤:
测验
循环条件的问题
insert字符串
npos
erase
判断删除数据的长度
全删
部分删
最终代码为
swap
find字符
测试
find字符串
代码
测验
编辑
substr
代码
测试
问题提出
问题分析
问题解答
构建拷贝和赋值
测试
流插入和提取
流插入
流提取
cin对于空格和换行
get( )
频繁扩容问题
解决办法
构造函数
提前准备工作:
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;}
以上就是本次博客的学习内容,如有错误,还望各位大佬指点,谢谢阅读