目录
前言
string类的模拟实现
成员函数的实现
构造函数
拷贝构造函数
赋值运算符重载
析构函数
元素访问的实现
operator[ ]
Iterator - 迭代器
容量大小的实现
size
capacity
reserve
编辑resize
内容修改的实现
push_back
append
operator+=(char ch)
operator+=(const char* s)
insert
erase
内容查找的实现
find - 字符
find - 字符串
substr
非成员函数的重载
relational operators
operator<< 流插入
operator>> 流提取
getline
string 类的模拟实现整体代码
string.h
string.cpp
test.cpp
前言
本模块呢,我将会带大家一起从 0~1去模拟实现一个STL库中的 string类,当然模拟实现的都是一些常用的接口,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类中的6大默认成员函数等
string类的模拟实现
成员函数的实现
首先想,对于一个自定义函数,在调用的时候第一步是干嘛呢?答案肯定是我们之前讲到的,默认成员函数的实现。因为我们是模拟实现的string,也就是说库里面已经有了,所以我们要加上namespace,防止跟库文件发生冲突。
构造函数
我们先回想一下,string函数都要干嘛呢?首先肯定是要有数据,然后也要保存数据的空间,最后也要有个大小用于计算数据的多少。所以我们的成员变量就是 char* _str; size_t _size; size_t _capacity;
能不能这样初始化呢?获取到char*的str之后,初始化_str;答案是不能的,1.因为你的成员变量_str是char*类型的,构造函数的形参是const类型的,会涉及到权限的放大 2.万一你传的是个常量字符串,也就是不能修改的,所以我们要去开空间初始化。 我们之前也说过要尽量用初始化列表去初始化。
我们同学可能就是这样想的,先去读字符串的大小,然后再进行计算容量,最后计算完再开空间
可是我们运行之后就报错了呀,这是为什么呢?
我们会发现这个地址为啥是错的呢?
这有个之前的问题,初始化列表初始化的顺序是按照成员变量的顺序,而不是初始化列表写的顺序。
也就是说,它会先去走 _str(new char[strlen(str)+1]),而此时capacity还没初始化,就是个随机值,要是很大的话,这个new就会崩掉的了。这里有两种改法。1.让顺序保持一致
接着开了空间之后,我们就要把值拷进来了。
此时我们就发现可以了。
注意:此写法有个大坑,就是成员变量的顺序要声明正确!
2.就是把内置类型_size,_capacity 放到函数里面去初始化
拷贝构造函数
我们有提到过若是一个类在没有显示定义拷贝构造对于内置类型不做处理,而对于自定义类型会去调用 类中默认提供的拷贝构造函数 此时就会造成浅拷贝的问题
- 浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
- 深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法:
写法一:传统写法
传统写法的思想简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的
//拷贝构造
string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
- 通过调试再去观察的话,我们可以发现,此时 对象
s1
和 对象s2
中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响
写法二:现代写法
现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。
//拷贝构造
string(const string& s):_str(nullptr), _size(0), _capacity(0)
{string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象
}
赋值运算符重载
对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个
- 但是呢默认生成的这个也会造成一个 浅拷贝 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1和s3就会指向一块同一块空间,此时便造成了下面这些问题
- 在修改其中任何一者时另一者都会发生变化;
- 在析构的时候就也会造成二次析构的;
- 原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题
- 那么此时我们应该自己去开出一块新的空间,将
s3
里的内容先拷贝到这块空间中来,然后释放掉s1
所指向这块空间中的内容,然后再让s1
指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间 - 最后的话不要忘记去修改一下
s1
的【_size】和【_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;}
写法二:现代写法
很多同学非常地震惊,为何这样子就可以做到【深拷贝】呢?
string& operator=(const string& s)
{if (this != &s) //防止自己给自己赋值{string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象}return *this; //返回左值(支持连续赋值)
}
有关这个swap()函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模板 的时候有提到过库中的这个 swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据
可以看到在我们自己实现的这个swap(string& s) 函数中就去调用了std标准库中的函数然后交换一个string对象的所有成员变量
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
接下去来解释一下这里的原理,我们在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题
透过上面这个图解读者应该对新的这种拷贝构造有了一定的理解:反正你这个tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果
还有种写法
string& operator=(string tmp) {swap(tmp); // 用交换的方法避免了多余的内存操作return *this; // 返回当前对象,支持链式赋值
}
下面我们以一个实例讲解一下
#include <iostream>
#include <string>class MyString {
private:std::string str; // 内部使用标准 string 存储字符串public:// 构造函数MyString(const std::string& s) : str(s) {}// 赋值运算符重载MyString& operator=(MyString tmp) {swap(tmp); // 使用 swap 交换 tmp 和当前对象的内容return *this; // 返回当前对象,支持链式赋值}// 打印函数void print() const {std::cout << str << std::endl;}
};int main() {MyString s1("Hello");MyString s2("World");// 使用重载的赋值运算符s1 = s2; // 赋值:s1 变为 "World"// 打印 s1 和 s2 的内容std::cout << "s1: ";s1.print(); // 应该输出 "World"std::cout << "s2: ";s2.print(); // 应该输出 "World"return 0;
}
此时可能就会有同学问了,在operator=中 return了*this 但是就一个s1=s2 我看没有人接收啊 是不是s1=s2可以转换成别的形式?解答如下:
当operator这个函数结束的时候,会调用析构函数,此时的this就是temp的this,就会把temp的空间给带走了
析构函数
我们要析构,就是要把类中的数据给清除了。那什么才是我们需要清楚的呢?也就是这三个char* _str; size_t _size; size_t _capacity;
这里的顺序不需要进行刻意,因为大小和容量并不是存在delete释放的是_str所指向的内存块
它们是存在栈内存中的
目前我们看数据都是再调试窗口查看的,为了更方便些,我们先定义个c_str,直接返回const char*类型的_str;
const char* c_str() const
{return _str;
}
这里建议加个const,表示 该成员函数不会修改类的成员变量 为什么要加const呢?const对象调const可以达到一个平传的效果。修饰的是this指针指的对象。权限不能放大但是可以缩小
此时我们就可以打印数据了
但是我们有时候也需要个无参的构造,然后再进行增加修改的操作。
默认构造函数包括三种,1.我们不写,编译器自动生成的。2.全缺省的 3.无参的。这里我们写个无参的,那该怎么写呢?我们能不能这样写?
答案是不能的,如果我们这样给_str空的话,那c_str就悬空了,此时返回个空指针那不是就崩了吗?那该怎么做呢?
我们观察库里的string,即便是空的也会有个'\0'
所以我们要对_str先开个空间,然后再给个'\0',注意这里不是"\0",不是字符串\0,而是字符\0
此时,我们说能不能合并下呢?用全缺省的,那这两种写法你们觉得可以吗?
答案是都不能,第一个是类型不匹配。一个是字符,一个是字符串指针
第二个不能给空,如果给空了之后,_size(strlen(str)) 就会崩了
最优写法是这样的
常量字符串默认是有"\0"的。
元素访问的实现
operator[ ]
operator有两种形式,1.可读可写 2.只读
首先最常用的就是这【下标 + [ ]】的形式去进行一个访问,那很简单,我们通过当前所传入的下标值去访问对应的数据即可
可读可写的实现
这里我们先用传值返回,看看会有什么问题呢
这里为什么会报错呢?
问题出在代码的 operator[] 函数部分。你正在定义一个 operator[] 来访问 _str 数组的元素,但是函数的返回值是 char 类型,这意味着返回的是一个值,而不是引用。如果你试图修改 s1[i]++ 中的值,这会导致错误,因为 operator[] 返回的是不可修改的副本。
所以这里要换成引用
只读的实现
读写和读的operator构成函数重载,我们可以加入const对权限进行限定
// 可读不可写
const char& operator[](size_t pos) const// 最后也加上const 因为防止该函数对const进行修改
{ assert(pos < _size);return _str[pos];
}
const加在前面是为了防止返回的数据被修改。const加在后面是为了防止传过来的数据被修改
Iterator - 迭代器
那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了【下标 + []】的形式,我们还可以使用迭代器的形式去做一个遍历,迭代器指向的是位置,而不是数据
- 而对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的
- string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。
typedef char* iterator;
typedef const char* const_iterator;
注:不是所有的迭代器都是指针。
我们知道,对于begin来说,也就是_str所指向的的位置,那对于end呢?end的意思是返回最后一个字符的下一个位置。那既然str_是第一个位置,size又是这个数据的大小,那最后一个位置的下一位不就是_str+size吗?
iterator begin()
{return _str;// 返回指向字符数组起始位置的指针
}iterator end()
{return _str + _size;
}
- 实现了普通版本的迭代器之后,我们再来看看常量迭代器。很简单,只需要修改一下返回值,然后在后面加上一个【const成员】,此时就可以构成函数重载了
const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}
范围for的底层实现就是用iterator迭代器实现的
容量大小的实现
size
首先是 size(),这里的话我们直接返回_size
即可,因为不会去修改成员变量,所以我们可以加上一个【const成员】(因为它是不可被修改滴)
size函数用于获取字符串当前的有效长度(不包括’\0’)
size_t size() const
{return _size;
}
注意:这里的 const 是用来修饰 this指针的
capacity
capacity函数用于获取字符串当前的容量
size_t capacity() const
{return _capacity;
}
reserve
reserve规则:
- 当扩容n大于对象当前的capacity时,将capacity扩大到n或大于n。
- 当扩容n小于对象当前的capacity时,什么也不做。
很明显,只有当这个 新容量大于旧容量的时候,才会去选择去开空间,这里的扩容逻辑和我们在实现旧版本的拷贝构造函数时类似的:也是先开出一块新的空间(这里主要使用这个newCapacity 去开),然后再将原本的数据拷贝过来,让_str指向新空间然后释放旧空间的数据。最后的话不要忘了去更新一下容量大小
void reserve(size_t newCapacity)// 扩容(修改_capacity)
{// 当新容量大于旧容量的时候,就开空间if (newCapacity > _capacity){// 1.以给定的容量开出一块新空间char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'// 2.将原本的数据先拷贝过来strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')// 3.释放旧空间的数据delete[] _str;// 4.让_str指向新空间_str = tmp;// 5.更新容量大小_capacity = newCapacity;}
}
不能先赋值再delete _str,这样会把新地址的str给释放掉。
注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。
resize
首先我们来分析一下,对于【resize】而言主要对对象中的数据去做一个变化,用于调整字符串的大小。那么该怎么调呢
如果调的大小很大,该考虑什么呢?如果调的很小,又该考虑什么呢?接下来我们就要进行分类讨论了
- 如果这个 newSize < _size 的话,那我们要选择去删除数据
- 如果这个 newSize > _size,但是呢 newSize < _capacity 的话,此时要做的就是新增数据但是呢不去做扩容
- 如果这个 newSize > _size 并且 newSize > _capacity,我们便要选择去进行扩容了
当 _size = 10 , _capacity = 15 时
- 在分析完了之后,我们立即来实现一下相关的代码。可以看到,一上来我就直接去判断了
newSize
是否大于_size
,然后在内部又做了一层判断,只有当newSize > _capacity
时,才去执行【reserve】的扩容逻辑 - 如果
newSize
并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数memset- 我们从
_str + _size
的位置开始填充; - 填充的个数是
newSize - _size
个; - 填充的内容是
c
- 我们从
- 若是
newSize <= _size
的话,我们所要做的就是去截取数据,到newSize
为止直接设置一个 \0,然后更新一下当前对象的_size
大小
// 改变大小
void resize(size_t newSize, char c = '\0')
{// 1.当新的_size比旧的_size来得小的话,则进行删除数据if (newSize > _size){// 只有当新的size比容量还来的大,才去做一个扩容if (newSize > _capacity){reserve(newSize);}// 如果newSize <= _capacity,填充新数据即可memset(_str + _size, c, newSize - _size);}// 如果 newSize <= _size,不考虑扩容和新增数据_size = newSize;_str[newSize] = '\0';
}
有几点是易错的,reserve的时候要扩容到新的newSize,而不是扩容newSize-_size。
内容修改的实现
push_back
首先第一块的话简单一点,我们去追加一个字符,那首先要考虑到的也是一个扩容逻辑,因为我们是一个字符一个字符去进行插入的,那么当这个_size == _capacity的时候,就要去执行一个扩容的逻辑了,这边的话是运用到了这个三目运算符,若是容量的大小为0的话,默认开个大小为4的空间就可以了;其他的情况都是以2倍的形式去进行扩充
最后在扩完容之后我们就在末尾去增加数据了,因为此时_size
指向的就是 \0 的位置,所以就把字符放在这个位置上就可以了,顺带地记得去后移一下这个_size
,再放上一个 \0
// 追加一个字符
void push_back(char ch)
{// 如果数据量大于容量的话,则需要进行扩容if (_size == _capacity){ reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++size;_str[_size] = '\0';
}
append
接下去的话是【append】,要追加的是一个字符串,所以我们要先去算出它的长度,接下去判断一下在加上这个长度后是否要去做一个扩容,最后的话还是通过我们熟悉的【memcpy】通过字节的形式一一拷贝到_str + _size的位置(注意拷贝len + 1个,带上最后 \0),最后再把大小_size给增加一下即可
// 追加一个字符串
void append(const char* s)
{int len = strlen(s); // 获取到待插入字符串的长度// 若是加上len长度后超出容量大小了,那么就需要扩容if (_size + len > _capacity){reserve(_size + len);}// 将字符串拷贝到末尾的_size位置memcpy(_str + _size, s, len + 1);// 大小增加_size += len;
}
注意这里的_size + len > _capacity; 用原来的大小加上新追加的字符串的长度,然后再去和容量去比较
为啥要用c_str打印呢?
因为 c_str()
返回的是 const char*
,即一个 C 风格的字符串指针,表示你的 MyString
对象内部存储的字符数组
operator+=(char ch)
首先的话是去【+=】一个字符,这里我们直接复用前面的push_back()
接口即可,最后因为【+=】改变的是自身,所以我们return *this
,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝
string& operator+=(char ch)
{push_back(ch);return *this;
}
operator+=(const char* s)
而对于【+=】一个字符串,我们则是去复用前面的append()
即可
string& operator+=(const char* s)
{append(s);return *this;
}
这里为什么不用char&而是用string&呢
char& 代表一个单个字符的引用,而 operator+= 是一个修改 MyString 对象的操作,而不是修改单个字符
insert
接下去我们就要来实现一下【insert】这个接口了-- 从 pos 位置 开始插入n 个字符
不过在这之前呢我们先要去声明并初始化一个静态的成员变量npos
,它是最大的无符号整数值。但是对于 静态的成员变量 来说我们需要 在类内声明并且在类外进行初始化
// 类内声明
const static size_t npos;
// 类外初始化
const static size_t npos = -1;
首先第一个的话就是要在pos
位置插入n个字符
void insert(size_t pos, size_t n, char ch)
因为这里会传入一个pos
位置,所以第一步我们就是要去考虑这个pos
位置是否合法
assert(pos <= _size);
接下去第二步的话就是去考虑过扩容的问题了,如果_size + n
之后的大小大于_capacity
的话那就要调用【reserve】接口去实现一个扩容的逻辑了
// 考虑扩容
if (_size + n > _capacity)
{reserve(_size + n);
}
第三步呢并不是直接去插入数据,而是要先给需要插入的n个字符腾出位置。从_size
位置开始,让字符以n个单位地从后往前挪即可,若是从前往后挪的话就会造成覆盖的问题
再次强调,这里是挪动end + n 不是 end + 1个位置
// 挪动数据
size_t end = _size;
while (end >= pos)
{_str[end + n] = _str[end];--end;
}
不过呢,我们在这里还要考虑一种极端的情况,如果这个pos == 0的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是呢当这个end超出pos的范围时,也就减到了-1,但是呢这个end的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数
我们可以来看看当这个end
在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos
的值了,此时就会造成一个死循环,导致程序崩溃
// 字符插入测试
void test6()
{xas_string::string s1("abcdefghijk");s1.insert(0, 3, '#');cout << s1.c_str() << endl;}int main()
{test6();return 0;
}
所以我们应该将无符号该有 有符号类型 size_t ----> int
// 挪动数据int end = _size;while (end >=(int)pos){_str[end + n] = _str[end];--end;}
当这个挪动的逻辑结束后,我们就可以从pos这个位置去插入n个字符了。最后再去更新一下这个_size
的大小即可
// 插入n个字符
for (size_t i = 0; i < n; i++)
{_str[pos + i] = ch;
}
_size += n;
erase
删除从pos位置开始的len个有效长度字符
- erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:
1.pos位置及其之后的有效字符都需要被删除
- 这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。
2.pos位置及其之后的有效字符只需删除一部分。
这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。
// 删除 -- 从 pos 位置 删除 长度为 len 的字符串 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;}}
不建议使用 因为挪动数据代价挺大的
内容查找的实现
find - 字符
这个很简单,就是去遍历一下当前对象中的_str
,若是在遍历的过程中发现了字符ch
的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos
这个最大的无符号数
find - 字符串
我直接使用的是C语言中的库函数 strstr函数详解,这个的话我们在 字符串函数与内存函数解读 的时候也有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针。那我们如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可
size_t find(const char* s, size_t pos) const
{assert(pos < _size);char* tmp = strstr(_str, s);if (tmp){// 指针相减即为距离return tmp - _str;}return npos;
}
substr
上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?
首先要考虑的问题是长度的问题,如果我们从pos位置取的子串长度大于剩余子串的长度,那么最多能取得范围也是从pos位置到size_t的位置。所以当取得长度过高的时候,我们就要更新下子串长度的有效范围
- 可以看到,我以这个
n
作为可取的子串长度,一开始得让其等于传入进来的len
长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len
- 但是如果呢这个长度超出了有效范围后,我们便要去更新这个
n = _size - pos
size_t n = len;
if (len == npos || pos + len > _size)
{// 就算要取再大的长度,也只能取到pos - _size的位置n = _size - pos;
}
- 那接下去的话我们就可以去取这个子串了,使用循环的方式从
pos
位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】
string substr(size_t pos, size_t len = npos)
{size_t n = len;if (len = npos || len + pos > _size){n = _size - pos;}string temp;temp.reserve(n);for (int i = pos; i < n + pos; i++){temp += _str[i]; //这里用到了操作符重载 //等于是追加的意思}return temp;
}
为什么是string类型
substr
需要返回一个新的字符串对象,因为原字符串中的一部分需要作为独立的字符串进行存储和操作。
-
char
类型 是用来存储单个字符的基本数据类型,而std::string
是用来存储多个字符的容器。 -
如果
substr
返回char
,就意味着它只能返回一个单一字符,但实际上我们要提取的是 多个字符(即一个子字符串)。这就不适合用char
类型来返回。
为啥不能temp = _str[i]; temp++;
- 这行代码是递增
temp
,也就是将temp
的值加一。如果temp
是一个字符类型的变量(char
),那么temp++
实际上将其字符值增加一个。 - 但,在这种情况下,
temp++
会使temp
的值变成下一个字符,而不是将字符追加到一个字符串中。
这里i的取值范围为什么是n+pos,为什么不是n?
首先我们要知道i是从pos位置开始的,就好比substr(5,2);指的是从第五个位置,取两个字符串。那你难道就写成i = 5;i < 2吗?这肯定是错误的,应该是pos+n才是最后的位置
非成员函数的重载
最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多
relational operators
小于
//< 运算符重载
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)
{return *this < s || *this == s;
}
大于
bool operator>(const string& s)
{return !(*this <= s);
}
大于等于
bool operator>=(const string& s)
{return !(*this < s);
}
不等于
bool operator!=(const string& s)
{return !(*this == s);
}
operator<< 流插入
// 流插入
ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
放在类内就会提示运算符的参数太多了,因为会有个隐藏的this
我们知道为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到友元这个东西,不过呢我们不建议使用这个,会破坏类的封装性
还有一点要提醒的是对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个 防拷贝 的效果,即在后面加上一个= delete
好,那到这里的话,我们是时候来讲讲这个cout << s.c_str()
和 cout << s
的区别了
- c的字符数组, 以\0为终止算长度
- string不看\0, 以size为终止算长度
operator>> 流提取
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。
//重载>>std::istream& operator>> (std::istream& in, string& s){//读取前要先清理掉原来存在的字符s.clear();//用get获取字符char ch = in.get();//先用一个数组存起来,再一起加char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n'){//原始方法,一个字符一个字符加太麻烦,先用一个数组存起来,再一起加//s += ch;buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;//重置i}ch = in.get();}//循环结束后可能还要一些字母没有存进去if (i != 0){buff[i] = '\0';s += buff;}return in;}
getline
getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。
//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in;
}
string 类的模拟实现整体代码
string.h
#pragma once
#include <iostream>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cout;
using std::cin;
using std::endl;// 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域
namespace xas_string
{class string{public:typedef char* iterator; // 迭代器某种意义上就是 指针typedef const char* const_iterator; // 默认成员函数string(const char* str = ""); // 有参构造函数string(const string& s); // 拷贝构造string& operator=(const string& s); // 赋值运算符重载~string(); // 析构函数//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//容量和大小相关函数size_t size() const; // 返回目前 字符串的有效字符个数size_t capacity() const; // 返回目前 字符串的容量void clear(); // 清空字符串bool empty() const; // 判断字符串是否为空void reserve(size_t newCapacity = 0); // 扩容(修改_capacity)void resize(size_t newSize, char c = '\0'); // 改变大小// 修改字符串相关函数void push_back(char ch); // 追加一个字符void swap(string& s); // 交换 --- 交换两个字符串void append(const char* s); // 追加一个字符串string& operator+=(char ch); // 追加一个字符string& operator+=(const char* s); // 追加一个字符串void insert(size_t pos, const char* str); // 插入n个字符void erase(size_t pos, size_t len = npos); // 删除 -- 从 pos 位置 删除 长度为 len 的字符串 // 访问字符串相关函数char& operator[](size_t pos); // 可读可写const char& operator[](size_t pos) const; // 可读不可写const char* c_str()const; // 用 C语言的方式返回size_t find(char ch, size_t pos = 0); // 寻找字符size_t find(const char* str, size_t pos = 0); // 寻找 字符串string substr(size_t pos = 0, size_t len = npos); // 截取字符串 从某个位置 取 len 个字符// 关系运算符重载bool operator<(const string& s)const; // < 运算符重载 bool operator==(const string& s)const; // ==运算符重载bool operator<=(const string& s); // <=运算符重载bool operator>(const string& s); // >运算符重载bool operator>=(const string& s); // >=运算符重载bool operator!=(const string& s); // !=运算符重载private:char* _str; // 指向字符数组的指针size_t _size; // 字符数组的有效数据的长度size_t _capacity; // 字符串数组的容量const static size_t npos = -1; //静态成员变量(整型最大值)};// 流插入ostream& operator<<(ostream& out, const string& s);//>>运算符的重载istream& operator>>(istream& in, string& s);//读取一行含有空格的字符串istream& getline(istream& in, string& s);void print_str(const string& s); // const对象的输出
}
string.cpp
#include "string.h"// 有参构造函数
xas_string::string::string(const char* str) // "" --- 为空的字符串
{_str = new char[strlen(str) + 1]; // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1_size = strlen(str);_capacity = strlen(str); // capacity 不包括 '\0'strcpy(_str, str);
}//拷贝构造
xas_string::string::string(const string& s):_str(nullptr), _size(0), _capacity(0)
{string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象
}// 赋值运算符重载
// 传统写法
//xas_string::string& xas_string::string::operator=(const string& s)
//{
// if (this != &s)
// {
// char* tmp = new char[s._capacity + 1];
// //memcpy(tmp, s._str, s._size + 1);
// strcpy(tmp, s._str);
// delete[] _str;
//
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}//现代写法2
xas_string::string& xas_string::string::operator=(const string& s)
{if (this != &s) //防止自己给自己赋值{string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象}return *this; //返回左值(支持连续赋值)
}// 析构函数
xas_string::string::~string()
{delete[] _str;_str = nullptr;_size = 0;_capacity = 0;
}char& xas_string::string::operator[](size_t pos) // 可读可写
{assert(pos <= _size);return _str[pos];
}const char& xas_string::string::operator[](size_t pos) const // 可读不可写
{assert(pos < _size);return _str[pos];
}xas_string::string::iterator xas_string::string::begin()
{return _str;
}xas_string::string::iterator xas_string::string::end()
{return _str + _size;
}xas_string::string::const_iterator xas_string::string::begin() const
{return _str;
}xas_string::string::const_iterator xas_string::string::end() const
{return _str + _size;
}// 针对------const 对象的访问
// 打印这个字符串 --- 不能修改
void xas_string::print_str(const string& s)
{for (size_t i = 0; i < s.size(); i++){std::cout << s[i] << " ";}std::cout << std::endl;string::const_iterator it = s.begin();while (it != s.end()){// 内容不能修改std::cout << *it << " ";// 指针可以修改it++;}std::cout << std::endl;
}void xas_string::string::push_back(char ch) // 追加一个字符
{// 如果数据量大于容量的话,则需要进行扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}// 返回目前 字符串的有效字符个数
size_t xas_string::string::size() const // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _size;
}size_t xas_string::string::capacity() const // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _capacity;
}bool xas_string::string::operator<=(const string& s) // <=运算符重载
{return *this < s || *this == s;
}// 清空字符串
void xas_string::string::clear()
{_str[0] = '\0';_size = 0;
}
// 判断字符串是否为空
bool xas_string::string::empty() const
{return 0 == _size;
}
void xas_string::string::reserve(size_t newCapacity)// 扩容(修改_capacity)
{// 当新容量大于旧容量的时候,就开空间if (newCapacity > _capacity){// 1.以给定的容量开出一块新空间char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'// 2.将原本的数据先拷贝过来strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')// 3.释放旧空间的数据delete[] _str;// 4.让_str指向新空间_str = tmp;// 5.更新容量大小_capacity = newCapacity;}
}void xas_string::string::resize(size_t newSize, char c) // 改变大小
{// 1.当新的_size比旧的_size来得小的话,则进行删除数据if (newSize > _size){// 只有当新的size比容量还来的大,才去做一个扩容if (newSize > _capacity){reserve(newSize);}// 如果newSize <= _capacity,填充新数据即可memset(_str + _size, c, newSize - _size);}// 如果 newSize <= _size,不考虑扩容和新增数据_size = newSize;_str[newSize] = '\0';
}void xas_string::string::append(const char* s) // 追加一个字符串
{int len = strlen(s); // 获取到待插入字符串的长度// 若是加上len长度后超出容量大小了,那么就需要扩容if (_size + len > _capacity){reserve(_size + len);}// 将字符串拷贝到末尾的_size位置memcpy(_str + _size, s, len + 1);// 大小增加_size += len;
}xas_string::string& xas_string::string::operator+=(char ch) // 追加一个字符
{ push_back(ch);return *this;
}xas_string::string& xas_string::string::operator+=(const char* s) // 追加一个字符串
{append(s);return *this;
}void xas_string::string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}int end = _size;// 向后挪动while (end >= (int)pos){_str[end + len] = _str[end];end--;}// 这里不能用 strcpy 会把 '\0' 拷贝过来strncpy(_str + pos, str, len);_size += len;}void xas_string::string::erase(size_t pos, size_t len) // 删除 -- 从 pos 位置 删除 长度为 len 的字符串
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}
}size_t xas_string::string::find(char ch, size_t pos) // 寻找字符
{for (size_t i = 0; i < _size; i++){if (_str[i] == ch){return i;}}return npos;
}size_t xas_string::string::find(const char* str, size_t pos) // 寻找字符串
{const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}
}
xas_string::string xas_string::string::substr(size_t pos, size_t len ) // 截取字符串 从某个位置 取 len 个字符
{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;
}bool xas_string::string::operator<(const string& s)const // < 运算符重载
{return strcmp(_str, s._str) < 0;
}bool xas_string::string::operator==(const string& s)const //==运算符重载
{return strcmp(_str, s._str) == 0;
}
bool xas_string::string::operator>(const string& s) // >运算符重载
{return !(*this <= s);
}
bool xas_string::string::operator>=(const string& s)
{return !(*this < s);
}
bool xas_string::string::operator!=(const string& s)
{return !(*this == s);
}// 用 C语言的方式返回
const char* xas_string::string::c_str() const // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _str;
}// 流插入
ostream& xas_string::operator<<(ostream& out, const xas_string::string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
//>>运算符的重载
istream& xas_string::operator>>(istream& in, xas_string::string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in; //支持连续输入
}
//读取一行含有空格的字符串
istream& xas_string::getline(istream& in, xas_string::string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in;
}// 交换
void xas_string::string::swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
test.cpp
#include "string.h"// 测试初始化,与循环打印 迭代器
void test1()
{// 初始化测试xas_string::string s1("hello string");cout << s1.c_str() << endl;cout << endl;// 拷贝构造测试xas_string::string s2(s1);cout << s2.c_str() << endl;cout << endl;//赋值运算符重载xas_string::string s3("hello world!");s1 = s3;cout << s1.c_str() << endl;// 引用返回 是可以进行修改的for (size_t i = 0; i < s3.size(); i++){s3[i]++;}cout << s3.c_str() << endl;cout << endl;// 迭代器xas_string::string::iterator it = s1.begin();while (it != s1.end()){std::cout << *it << " ";it++;}std::cout << std::endl;// 范围 forfor (auto ch : s1){std::cout << ch << " ";}cout << endl;cout << endl;// 常量对象print_str(s1);}// 测试容量相关的函数
void test2()
{xas_string::string s1("hello string");cout << s1.size() << endl;cout << s1.capacity() << endl;cout << s1.empty() << endl;s1.clear();cout << s1.empty() << endl;cout << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;s1.reserve(100);cout << s1.size() << endl;cout << s1.capacity() << endl;
}// 测试 resize()函数
void test3()
{xas_string::string s1("abcdefghijk");s1.reserve(15);cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;cout << endl;s1.resize(18,'x');cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;
}// 测试修改函数
void test4()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;cout << endl;cout << "追加字符:xxxxx" << endl;cout << endl;s1.append("xxxxx");cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;
}void test5()
{xas_string::string s1("abcdefghijk");s1 += 'x';cout << s1.c_str() << endl;s1 += "yyyy";cout << s1.c_str() << endl;
}// 字符插入测试
void test6()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << endl;s1.insert(0,"###");cout << s1.c_str() << endl;}// 字符串删除测试
void test7()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << endl;s1.erase(3, 3);cout << s1.c_str() << endl;}// 访问字符串相关函数测试
void test8()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;size_t pos1 = s1.find('c', 0);cout << pos1 << endl;size_t pos2 = s1.find("def", 0);cout << pos2 << endl;cout << endl;xas_string::string s2 = s1.substr(5, 5);cout << s2.c_str() << endl;
}// 非成员函数重载
void test9()
{xas_string::string s1("hello string");cout << s1.c_str() << endl;xas_string::string s2("hello world");cout << s2.c_str() << endl;cout << (s1 < s2) << endl;cout << (s1 > s2) << endl;cout << (s1 == s2) << endl;cout << (s1 != s2) << endl;
}// 输入输出流测试
void test10()
{xas_string::string s1("hello");s1 += '\0';s1 += "*******";cout << s1.c_str() << endl;cout << s1 << endl;xas_string::string s2;getline(cin,s2);cout << s2;
}int main()
{test10();return 0;
}