目录
一. 首先先来看看String类的成员结构:
二.接下来的普通函数接口的实现:
三. 其次就是模拟String类对象的扩容机制:
四.增删改查
push_back、append、+=重载函数增添数据:
insert函数——在数组的任意位置添加数据:
删:
查:
改:
五:拷贝构造与赋值重载:
5.1传统写法:
5.2现代写法:
六.流插入/流提取重载函数:
七:迭代器部分实现:
String类代码.h文件:
String作为C++的字符序列类,可以对字符串数据进行一系列的增删查改,下面来看看String类中多个常用成员函数的底层实现 :
class String {
public:
//构造
String(const char* str = "")
{
}
//析构
~String(){
}
private:
char* _arr;
size_t _size;
size_t _capacity;
const static size_t npos = -1;
};
该类的底层是一个连续的存储空间,相当于一个字符数组,该类中共有四个成员变量:
1.其中有一个是字符指针_arr,它指向堆区空间的一块地址,在该地址中存放着对象的字符串内容),因为栈区的内存空间很少,所以需要开辟堆区空间去存放数据 ;
2._size是指在当前数组中已经存储的字符总个数,不包括'\0'字符,我们每次在增删数据时,都需要用到它;
3._capacity是指当前数组的容量有多少,_capacity表示的是数组的上限存储空间,一旦超过了这个容量,就相当于是越界了,'\0'字符不算在_capacity中!
4.最后就是这个npos变量了,它是const静态成员,不可被修改,且被该类的所有成员所共享。
class String{
public:
//拷贝构造
string(const char* str="") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity+1];
strcpy(_str, str);
}
//析构
~string() {
_size = _capacity = 0;
delete[] _str;
_str = nullptr;
}
//求字符串的长度
size_t size() const{
return _size;
}
//求字符串的数组容量大小
size_t capacity() const{
return _capacity;
}
//c_str内容
char* c_str() const{
return _str;
}
//判断该对象数组中是否为空
bool empty() const {
return _size == 0;
}
//获取数组所能存放数据的最大容量
size_t Max_size() {
return npos;
}
//清空对象所有内容
void clear() {
_size = 0;
_arr[0] = '\0';
}
private:
char* _arr;
size_t _size;
size_t _capacity;
const static size_t npos = -1;
};
对于构造函数来说,形参我使用了缺省值“空字符串”去替代,假如创建对象在不初始化的情况下都是无内容的,所以用空字符串更符合;若是初始化就已经赋值的话,可以获取到所赋的字符串的大小,依据大小去开辟空间,进而适配_size、_capacity等成员变量。然后就是在_arr初始化赋值时多new了一个空间,该空间用于存放'\0'终止符,该终止符不算在_size和_capacity成员变量中。
对于析构函数来说,就是释放堆区空间还给操作系统,将剩余成员变量清零即可。
1.在上面这些成员函数中,大多都加上了const,const修饰变量的作用是不允许该变量在后续操作中被修改;而const修饰成员函数的作用也是如此,使得函数在做返回值时避免其成为左值被修改。放在类中的每个非静态成员函数中的第一个形参都是隐藏的this指针,而const修饰的就是这个this指针,意味着this指针就不能再进行修改其成员变量(_size,_arr,_capacity)了。
2.以上这些成员函数几乎都是对成员变量的封装,封装提高了底层成员变量的安全性,不会暴露在外面,被别人随意使用。
注:下面的函数都是放在类中的成员函数!
//扩容机制
void reserve(size_t n){
if (n > _capacity) {
//会重新开辟一块更大的新空间
char* tmp = new char[n + 1]; //扩容的时候多开一个空间,为'\0'开
strcpy(tmp, _str);
//销毁原来的旧空间
delete[] _str;
_str = tmp; //将临时空间再赋值给类成员_str
_capacity = n; //更新容量
}
}
扩容是在原空间容量_capacity不够的情况下进行的,而堆区空间创建空间又是随机性的,所以扩容系统会根据该空间后面是否有空闲空间去扩,若该空间后面有空闲空间,则是原地扩——直接在该空间后面增加所需要的字节空间;另一种就是异地扩——重新选择一块合适大小的地方去开辟空间供其使用。
而我们并不清楚系统是按照异地还是原地去扩容,所以选择一个临时指针(打工人)去帮我们做这些事,等事情做完(扩容完毕)后,我们在从打工人那里获取成果即可。
注:异地扩会导致忘记释放原来的堆区空间,所以要记得销毁~
//插入字符
void push_back(char c) {
//在插入字符时,需要注意对象可能是空字符串,需要手动扩容
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
//插入字符串
void append(const char* str){
size_t len = strlen(str);
if (_size+len > _capacity) {
reserve(_size+len);
}
//方法1:
/*for (int i = 0; i < len; i++) {
_str[_size++] = str[i];
}
_str[_size] = '\0';*/
//方法2:
strcpy(_str +_size, str);
_size += len;
}
//插入字符+=
string& operator+=(char c) {
push_back(c);
return *this;
}
//插入字符串+=
string& operator+=(const char* str) {
append(str);
return *this;
}
对于push_back、append函数来说,它们都属于尾插,尾插的效率对于数组来说是最高的,不需要挪动数据!
其次+=运算符重载函数也是尾插函数,直接复用push_back和append即可。
//在某个位置插入字符
string& insert(size_t pos,char c) {
assert(pos <= _size);
//若该string类对象是空字符串时,需要手动扩容
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
//方法1:
size_t end = _size+1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
//方法2:
/*int end = _size;
while (end >= (int)pos) {
_str[end + 1] = _str[end];
--end;
}
_str[pos] = c;
_size++;*/
return *this;
}
//在某个位置插入字符串
string& insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
//方法1:
size_t end =_size + len;
while (end >=pos+len) {
_str[end] = _str[end -len];
--end;
}
//方法2同上——不展示了
strncpy(_str+pos, str,len);
_size+=len;
return *this;
}
对于insert函数来说,可以在任意位置插入数据,这时需要考虑三种情况:数组末尾插入数据、数组头部插入数据、数组中间插入数据。
对于头插和中间插来说,就需要挪动数据,为插入的位置留下足够的空间,又因为数组挪动数据的时间复杂度为O(N),效率很低,所以insert函数很少被使用。
而以上增添数据的函数,在每次插入前都需要对容量进行检查,查看数组是否满了,是否需要扩容!
//删除字符串
string& erase(size_t pos, size_t len=npos) {
if (len == npos || pos + len == _size) {
_str[pos] = '\0';
_size=pos;
}
else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
删除数据也是需要考虑三种情况:头删、尾删、中间删,由于形参npos缺省值的特殊性,需要做特别情况处理。
//查找字符
size_t find(char c,size_t pos=0) {
assert(pos < _size);
while (pos < _size) {
if (_str[pos] == c) {
return pos;
}
++pos;
}
//若找不到,则返回-1
return npos;
}
//查找字符串
size_t find(const char* str, size_t pos = 0) {
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr) {
return npos;
}
else {
return ptr - _str;
}
}
查找函数就很好写了,查找字符可以利用循环遍历的方式一个一个字符的对比进行,成功了则返回该字符的下标。
而查找字符串,则是用到了strstrC库函数,该函数的作用是扫描指定字符串,成功了则返回指针,不成功则返回空。ptr是查找成功返回的字符串,利用指针-指针=数字的方式可以定位该字符串在整个类对象数组中的下标位置!
//寻找字符串的某个pos位置字符
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
重载了[ ]运算符后,我们可以在主函数中使用循环的方式对该对象的数据进行遍历修改!
string(const string& s) {
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
strcpy(_str, s._str);
}
string& operator=(const string& s) {
if (this != &s) { //加if条件是因为,可能有自己给自己赋值的操作,需要考虑这一情况
char* tmp = new char[s._capacity + 1];
strcpy(_str, s._str);
delete _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
1. 拷贝构造函数和赋值重载函数本质上都是将一个对象的数据赋值/拷贝给另一个对象!
2.不写拷贝构造和赋值重载函数都是由于_arr所指向的是堆区空间,拷贝会发生浅拷贝,会导致两个类对象指向同一块堆区空间,析构时会析构两次导致系统崩溃,所以拷贝构造和赋值重载必须亲自写,为了避免浅拷贝,就必须让被拷贝被赋值的对象拥有一块自己的堆区空间,只拷贝_size和_capacity两个成员变量即可。
3.拷贝构造和赋值重载函数的形参和返回值都尽量使用引用传递,这样可以减少实参和新参的拷贝次数,提升运行效率!
//拷贝构造——现代写法(为了代码的简洁性)
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造—— String s3(s1);
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0){
string tmp(s._str);
this->swap(tmp);
}
//赋值重载—— s3=s1;
string& operator=(const string& s) {
if (this != &s) {
string tmp(s);
this->swap(tmp);
tmp._arr=nullptr; //老板给打工人钱
}
return *this;
}
现代写法要比传统写法更具有代码简洁性,可读性更好。
现代写法的核心就是将左边对象和右边对象(形参)的所有数据进行交换swap(库函数),但在交换前都是通过创建一个临时对象,让该对象与临时对象进行数据交换,这样就不会发生浅拷贝,不会发生同一块空间析构两次的情况。
赋值重载函数代码解析:是用临时对象tmp去拷贝构造形参对象s(形参对象s是类对象s3的别名,使用引用传递就不在该函数中建立空间,直接传的是s3的地址过来,但是this指针没办法去直接拷贝地址的数据,所以通过新创建临时的对象tmp,实体化空间去拷贝s3的数据继而让this指针去拷贝tmp,tmp好比是打工人,替老板做事,事成之后,老板就可以窃取tmp的成果,也给了钱(将tmp的_arr地址置空,释放的时候就不会释放野指针了),相安无事。
class String{
public:
friend ostream& operator<<(ostream& out, string& s);
friend istream& operator>>(istream& in, string& s);
};
//流插入
ostream& operator<<(ostream& out, string& s) {
for (size_t i = 0; i >(istream& in, string& s) {
s.clear();
char buff[128] = { '\0' };
char ch = in.get(); //get函数用来提取每一个字符
size_t i = 0;
while (ch != ' ' && ch != '\n') {
if (i == 127) {
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i > 0) {
buff[i] = '\0';
s += buff;
}
return in;
}
流提取和流插入函数只能放在类外,原因是放在类内的话这俩函数的第一个参数都会是隐藏this指针。若是放在类内,测试时使用:cout<
虽说放在类外,但是由于类内私有了成员变量,类外不能访问,于是友元函数声明解决了这一大问题,类外的函数通过friend关键字在类内进行声明,便可以在类外访问类的的成员!
没看懂的小伙伴可以看这篇文章,里面讲述了关于类的流插入流提取重载运算符放在类外的讲解
typedef char* iterator;
public:
//迭代器
iterator begin() {
return _str; //begin会指向字符串的首个字符位置
}
iterator end() {
return _str+_size; //end会指向最后一个有效字符的下一个位置
}
typedef const char* const_iterator;
//迭代器
const_iterator cbegin() const{
return _arr; //begin会指向字符串的首个字符位置
}
const_iterator cend() const{
return _arr+_size; //end会指向最后一个有效字符的下一个位置
}
迭代器iterator类型名称,是由char*重命名而成,迭代器中的begin、end都是指针,指向类对象数组的开头和结尾。
using namespace std;
#include
#include
#include
namespace Cheng {
class string {
typedef char* iterator;
public:
//迭代器
iterator begin() {
return _str; //begin会指向字符串的首个字符位置
}
iterator end() {
return _str+_size; //end会指向最后一个有效字符的下一个位置
}
typedef const char* const_iterator;
//const迭代器
const_iterator cbegin() const {
return _arr;
}
const_iterator cend() const {
return _arr + _size;
}
//类对象的构造函数,str=""是缺省值,若使用者不给参数,则是默认使用缺省值——无参构造
//若是给参数,则按给参数构造,缺省值失效
string(const char* str="") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity+1];
strcpy(_str, str);
}
//析构
~string() {
_size = _capacity = 0;
delete[] _str;
_str = nullptr;
}
//拷贝构造——传统写法
/*
string(const string& s) {
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
strcpy(_str, s._str);
}
*/
//拷贝构造——现代写法(为了代码的简洁性)
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){
string tmp(s._str);
this->swap(tmp);
}
//赋值——传统写法
/*
string& operator=(const string& s) {
if (this != &s) { //加if条件是因为,可能有自己给自己赋值的操作,需要考虑这一情况
char* tmp = new char[s._capacity + 1];
strcpy(_str, s._str);
delete _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
*/
//赋值——现代写法
//s1=s3;
string& operator=(const string& s) {
if (this != &s) {
string tmp(s);
this->swap(tmp);
}
return *this;
}
//求字符串的长度
const size_t size() const{
return _size;
}
//求字符串的数组容量大小
size_t capacity() {
return _capacity;
}
//寻找字符串的某个pos位置字符
//普通对象:可读可写
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
//c_str内容
char* c_str() {
return _str;
}
//判断该对象数组中是否为空
bool empty() const {
return _size == 0;
}
void shrink_to_fit() {
_capacity = _size;
_arr[_size] = '\0';
}
//获取数组所能存放数据的最大容量
size_t Max_size() {
return npos;
}
//扩容机制
void reserve(size_t n) {
if (n > _capacity) {
//会重新开辟一块更大的新临时空间
char* tmp = new char[n + 1]; //扩容的时候多开一个空间,为\0开
strcpy(tmp, _str);
//销毁原来的旧空间
delete[] _str;
_str = tmp; //将临时空间再赋值给类成员_str
_capacity = n; //更新容量
}
}
void resize(size_t n, char ch = '\0') {
if (n <= _size) {
_arr[n] = '\0';
_size = n;
}
else {
if (n <= _capacity) {
for (size_t i = _size; i < n; ++i) {
_arr[_size++] = ch;
}
}
else{
reserve(_capacity*2);
for (size_t i = _size; i < n; ++i) {
_arr[_size++] = ch;
}
}
_arr[_size] = '\0';
}
}
//插入字符
void push_back(char c) {
//在插入字符时,需要注意对象可能是空字符串,需要手动扩容
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
void append(const char* str){
size_t len = strlen(str);
if (_size+len > _capacity) {
reserve(_size+len);
}
//方法1:
/*for (int i = 0; i < len; i++) {
_str[_size++] = str[i];
}
_str[_size] = '\0';*/
//方法2:
strcpy(_str +_size, str); //使用strcpy会把字符串的斜杆0也拷贝过来,那么最后就不需要再加斜杠0了
//_str指针指向字符串的首元素,_str+_size就会让指针指向字符串的最后一个元素的下一个位置
//那么会在\0位置开始拷贝想要尾插的新字符串
_size += len;
}
string& operator+=(char c) {
push_back(c);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
//在某个位置插入字符
string& insert(size_t pos,char c) {
assert(pos <= _size);
//若该string类对象是空字符串时,需要手动扩容
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
//挪动数据
//情况1:若在头部插入时,即pos=0,那么end只能是>pos的,否则会死循环
//造成死循环原因,size_t不为负,若它为-1,会隐式提升成42亿多
//方法1:
size_t end = _size+1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
//方法2:不建议用
/*int end = _size;
while (end >= (int)pos) {
_str[end + 1] = _str[end];
--end;
}
_str[pos] = c;
_size++;*/
return *this;
}
//在某个位置插入字符串
string& insert(size_t pos, const char* str) {
assert(pos <= _size);
//若该string类对象是空字符串时,需要手动扩容
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;
return *this;
}
//删除字符串
string& erase(size_t pos, size_t len=npos) { //len=npos为缺省参数,若使用的时候mai'n中不给第二个参数
//代表从pos位置会直接删到'\0'结束
if (len == npos || pos + len == _size) {
_str[pos] = '\0';
_size=pos;
}
else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
//查找字符
size_t find(char c,size_t pos=0) { //pos又给缺省参数,因为C++库中的string类find函数,pos可以不给参数,默认为0
assert(pos < _size);
while (pos < _size) {
if (_str[pos] == c) {
return pos;
}
++pos;
}
//若找不到,则返回-1
return npos;
}
//查找字符串
size_t find(const char* str, size_t pos = 0) {
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr) {
return npos;
}
else {
return ptr - _str;
}
}
//清空函数
void clear() {
_size = 0;
_str[0] = '\0';
}
private:
size_t _size;
size_t _capacity;
char* _str;
//
const static size_t npos = -1;
};
//流插入
ostream& operator<<(ostream& out, string& s) {
for (size_t i = 0; i >(istream& in, string& s) {
s.clear();
char buff[128] = { '\0' };
char ch = in.get(); //get函数用来提取每一个字符
size_t i = 0;
while (ch != ' ' && ch != '\n') {
if (i == 127) {
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i > 0) {
buff[i] = '\0';
s += buff;
}
return in;
}