本文主要介绍库里面string类的模拟实现
文章目录
- 前言
- 一、string的构造函数
- ①无参的构造函数
- ②带参的构造函数
- ③修改构造函数
- 二、析构函数
- 三、拷贝构造
- 四、赋值重载
- 五、返回size 、capacity和empty
- 六、[]的运算符重载
- 七、迭代器
- ① 正向迭代器。
- ② 正向const迭代器
- 八、string比较大小(运算符重载)
- 九、扩容
- ① reserve扩容
- ② resize扩容
- 十、尾插数据
- ①push_back
- ②append
- 十一、+=的运算符重载(替换尾插数据的函数)
- 十二、在固定为插入,删除(insert和erase)
- ①插入一个字符
- ② 插入一个字符串
- ③ 删除
- 十三、交换swap
- 十四、find
- ① 找字符
- ②找子串
- 十五、流插入,流提取
- ① 流插入
- ②流提取
- 十六、全部源码
- string.h
- test.c
前言
首先创建两个文件
string.h
:用来定义和声明类里面的成员函数和成员变量,以及测试函数。使用命名空间包裹
test.cpp
:用来运行代码
一、string的构造函数
由于我们要模拟实现string,为了不和库里面的string冲突,我们需要使用命名空间将其封装起来。
其实底层其实和我们的顺序表很类似,也是三个成员变量(字符指针)
//定义一个命名空间,防止和库里面的sring类冲突
namespace mwq
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
};
我们先把准备工作做好,在test.cpp里面写一个主函数,然后包含这个string.h头文件。
注意:我们还要包含#include <iostream> 以及using namespace std,因为我们后面需要在头文件的命名空间里面写测试函数,需要用到cout!!
#include <iostream>
using namespace std;
#include "string.h"
int main()
{
mwq::test1();
return 0;
}
①无参的构造函数
现在我们开始写一个无参的构造函数
//无参构造函数
string()
:_str(nullptr)
, _size(0)
,_capacity(0)
{}
其实对于我们的无参构造函数看着没什么问题,其实有点问题。那我举一个例子大家看看什么问题。
我们前面了解c_str(返回C形式的字符串),我们如果要实现这个要求我们返回这个字符串的地址。
//返回C形式的字符串
const char* c_str()const
{
return _str;
}
好了现在我们写一个test1测试一下(直接在命名空间里面写就行了)
在test.cpp里
#include <iostream>
using namespace std;
#include "string.h"
int main()
{
mwq::test1(); //test1在string.h文件里面的mwq命名空间下
return 0;
}
在string.h里
//定义一个命名空间,防止和库里面的sring类冲突
namespace mwq
{
class string
{
public:
//无参构造函数
string()
:_str(nullptr)
, _size(0)
,_capacity(0)
{}
//返回C形式的字符串
const char* c_str()
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
void test1()
{
string s1;
cout << s1.c_str() << endl;
};
注意:这里使用了
cout
,所以test.cpp
里面必须有#include<iostream>以及using namespace std
。
因为头文件包含了#include<iostream>以及using namespace std
,当我们预处理时候会将头文件展开,在链接时,cout
就会去他的定义,如果我们没有定义就会报错,
所以在test.cpp
里面必须写#include<iostream>以及using namespace std
(cout是在这个里面定义的)
我们如果想打印我们的字符串,就会发现直接报错,那么出现了什么问题呢?
大家先思考一下
我们s1进行无参的构造函数,并且在初始化列表中初始化了空指针。我们先通过s1找到了c_str的成员函数,这时候是没有崩溃的,并且将空指针返回也是没有问题的。问题就出在我们通过这个空指针打印,因为cout在打印字符串时候会进行解引用,因为要找\0,这时就涉及到了空指针的解引用。就会出现问题。
②带参的构造函数
现在我们开始写一个带参的构造函数
string(const char* str)
:_str(str)
,_size(strlen(str))
,_capacity(strlen(str))
{}
其实他也是有问题的
我们设置成带参数,str
的类型是const char*
,但是我们成员变量_str是char*
类型,我们在初始化列表中传参数的时候就涉及到权限的放大。
那有没有解决办法呢?
我们可以想到一种解决办法就是将我们的成员参数也设置为const。
这样虽然可以运行,但是,这样的操作无疑是局限的,那么我们后续的操作就都不可以改变我们的_str。(因为你变成了const的类型)
例如:我们再实现是个[]运算符重载去访问这个字符串
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
此时去访问s2的数据并试图修改就会报错,这里是因为我们的_str是const,但是返回是char,
那我们再修改一下返回值,将返回值修改成const char&
还是不可以的,其实这个报错很容易看懂,因为我们的返回的是const char,这个是不能修改的。
所以:我们的这个带参的构造函数是有问题的。
综上所述:我们的有无参数的构造函数的问题如下:
- 无参构造函数:不能初始化为nullptr,因为我们打印字符串需要解引用
- 带参构造函数:不能用str直接初始化,因为类型不匹配,我们的_str要设置为char*类型,因为后续还要修改。
③修改构造函数
我们先改我们带参数的构造函数,我们的直接将我们的常量字符串给他,肯定不可以,那么我们就自己开一块空间,将字符串的内容拷贝过来。这样我们自己开辟的空间就可以自己随意修改,而不是像我们上面将成员变量修改为const。
我们开辟空间,就没有必要在初始化列表开空间,就直接在函数中开就可以。具体的细节我们开代码注释。
string(const char* str)
:_size(strlen(str))
{
//为什们将capacity的初始化也放到函数中?
//在初始化列表,初始化的顺序跟定义的顺序有关,
//如果我们先定义的capacity,我们size还没定义就是随机值。
_capacity = _size;
//因为capacity不包括字符的结束标准 \0
//所以在开辟空间的时候,我们多开一位。
_str = new char [_capacity + 1];
//将我们字符串的内容拷贝过来。
strcpy(_str, str);
}
修改无参数的构造函数,我们防止空指针的解引用,我们可以也给他开一个字节的空间,放\0,也就是字符串的结束标志,表示空字符串。
string()
//我们初始化为什们要用new[]呢?
//为了跟我们带参构造函数保持一致,
//在析构的时候用一样的类型。delete[] _str;
:_str(new char[1])
, _size(0)
,_capacity(0)
{
_str[0] = '\0'; //字符串结束标志,不写的话,直接打印s1.c_str()会崩溃,因为找不到\0
}
我们在类和对象中,我们知道,无参的构造函数可以用缺省参数代替,所以我们将带参数的构造函数用缺省参数,就可以将这两个合二为一。
//下面两种形式都可以,我们""里面就有\0。
//string(const char* str = "\0")
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size; //防止一开始_capacity = 0,对后面的代码有影响
_str = new char [_capacity + 1];
strcpy(_str, str);
}
二、析构函数
这个很简单
~string()
{
//注意的点就是,delete一定要和new保持一致
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
三、拷贝构造
编译器会自动生成我们的拷贝构造,但是我们在类和对象的章节学过,它完成的是浅拷贝。
对于我们的字符串,我们_str
是一个指针,指向一块数组空间(new出来的),当我们完成浅拷贝的时候,我们拷贝出来的指针只是将_str
的四个字节拷贝了过去,所以两块空间用指针指向同一块空间。
当我们进行析构函数的时候,我们同一块空间析构了两次,这显然是由问题的,所以会直接报错,我们不能直接用默认拷贝构造,我们要自己写。
深拷贝:
//拷贝构造
string(const string& s)
: _size(s._size)
, _capacity( s._capacity)
{
//防止new失败,我们先创建一个临时变量开辟空间
//拷贝完后,在给_str
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
_str = tmp;
}
四、赋值重载
这个需要考虑的情况比较多,我们看图
这个也是默认成员函数,编译器会自动生成,跟拷贝构造一样进行浅拷贝,和我们上面出现的情况一样,所以我们要进行深拷贝,自己写一个赋值重载。
我们可以看到,上面有三个问题
- 首先就得自己写一个深拷贝,和上面拷贝构造问题一样
- 当新空间>原空间时,需要扩容,还需要小心扩容失败了
- 当新空间=原空间,直接扩容
- 当新空间<原空间,直接拷贝过去会造成资源浪费
基于上面的这么多复杂的原因,我们编译器直接统一处理,直接new一个空间temp,将需要拷贝的数据拷给temp空间,然后释放原空间,再让原空间指向temp即可解决。
注意:这里为了满足连续赋值,我们使用引用返回
//赋值重载
string& operator=(string& s)
{
//防止自己和自己赋值
if (this != &s)
{
// 防止new失败,我们先创建一个临时变量开辟空间
//拷贝完后,在给_str
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[]_str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
五、返回size 、capacity和empty
就是将我们的size直接返回,需要注意的一点就是我们返回size,一般都是不可修改的,不可修改一般就设置成
const
这几个我们都不希望他修改,统一用const修饰,这样普通对象可以调用,const对象也可以调用。
//获取字符串元素个数
size_t size()const
{
return _size;
}
//返回空间容量大小
size_t capacity()const
{
return _capacity;
}
//判空
bool empty()const
{
return _size == 0;
}
六、[]的运算符重载
这个也是比较容易,就是返回字符串的位置,我们可以通过引用返回改变我们字符串的值。
但是当我们想要打印字符串的时候,我们并不想修改字符串,我们就可以用const修饰,让其构成运算符重载。让我们的程序更高效
//[]运算符重载
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//不可修改的[]
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
七、迭代器
我们暂时将其考虑为指针,就可以。
像用指针一样,用迭代器。我们先写一个正向迭代器。
① 正向迭代器。
//迭代器,//我们先知道怎么用就可以
typedef char* itterator;
itterator begin()
{
return _str;
}
itterator end()
{
return _str + _size;
}
用迭代器遍历字符串。
//测试迭代器
void test_string4()
{
string s1("hello world");
string::itterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//范围for的底层就是迭代器
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
② 正向const迭代器
//正向const迭代器
typedef const char* const_itterator;
const_itterator begin() const
{
return _str;
}
const_itterator end() const
{
return _str + _size;
}
八、string比较大小(运算符重载)
注意:这里我们不需要修改字符串,所以使用const修饰
我们比较的不是两个对象的大小,而是两个字符串。使用strcmp即可
int strcmp ( const char * str1, const char * str2 );
str1<str2结果小于0
str1=str2结果等于0
str1>str2结果大于0
//s1>s2
bool operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
//s1==s2
bool operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
//s1>=s2
bool operator>=(const string& s) const
{
return strcmp(_str, s._str) >= 0;
}
//s1<s2
bool operator<(const string& s) const
{
return !(*this >= s);
}
//s1!=s2
bool operator!=(const string& s) const
{
return !(*this == s);
}
//s1<=s2
bool operator<=(const string& s) const
{
return !(*this > s);
}
九、扩容
扩容我们知道有两种方式:一种是reserve,另外一种是resize
注意:reserve和resize都不支持缩容
① reserve扩容
首先介绍一下reserve
void reserve (size_t n = 0);
函数功能:将空间增加为n个,不会改变有效元素的个数(_size)
- 当参数n小于原空间时,啥也不干,空间大小不变
- 当参数n大于原空间时,将空间大小增加为n+1(为\0留一个空间)
void reserve(size_t n)
{
//reserve 不支持缩容
//所以我们自己加个条件
if (n > _capacity)
{
//给\0开一个空间
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
② resize扩容
void resize (size_t n);
void resize (size_t n, char c);
函数功能:都是将字符串的有效字符改变到n个。不同的是当字符个数增多时:resize(n)用\0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。
注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
详细解释:
- 当n<size时,
void resize (size_t n);
将有效字符个数缩减为n个,全部初始化为\0 - 当size <= n <= capacity时,将有效字符个数增加为n个
void resize (size_t n);
将n-size个字符初始化为\0
void resize (size_t n, char c);
,将将n-size个字符初始化为字符c
其实这种情况就是在有效字符结尾后面添加\0或者字符c - n>capacity,将容量扩展至n+1个空间。将n-size个字符初始化为\0或者字符c
void resize (size_t n);
将n-size个字符初始化为\0
void resize (size_t n, char c);
,将将n-size个字符初始化为字符c
我们发现2和3其实差不多,3就先扩容在初始化后面的字符
综上所述:其实我们会发现,
void resize (size_t n);
,其实有点多余,我们直接使用缺省参数知识将
void resize (size_t n, char c = '\0');
即可。
//不写第二个参数默认改为\0,使用缺省参数就可以将其合二为一
//1:n <= size
//2:size < n <= capacity
//3:n>capacity
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
memset(_str, c, n);
_size = n;
_str[_size] = '\0';
}
else
{
//if (n < _capacity)
//{
// memset(_str + _size, c, n - _size); //size后面的n个字符
// _size = n;
// _str[_size] = '\0';
//}
//else //扩容
//{
// reserve(n);
// memset(_str + _size, c, n - _size); //size后面的n个字符
// _size = n;
// _str[_size] = '\0';
//}
//第3种情况和第2种差不多
if (n > _capacity)
{
reserve(n);//扩容
}
memset(_str + _size, c, n - _size); //size后面的n个字符
_size = n;
_str[_size] = '\0';//将即为填上\0,防止出错
}
}
注意:我们以后改变字符串的时候,
- 都要考结尾有没有\0,防止出错
- 记得改变字符串的size
十、尾插数据
增加数据有两种方式,
一种是push_back
,尾插一个字符
一种是append
,尾插一个字符串
①push_back
//尾插字符
string& push_back(char ch)
{
if (_size + 1 > _capacity)
{
//if (_capacity == 0)
//{
//_capacity = 3;
//}
reserve(_capacity * 2); //二倍扩容,
}
_str[_size] = ch;
++_size; //不要忘记改变size了
_str[_size] = '\0'; //结尾加上\0
return *this;
}
注意:我们这里会发现,当_size + 1 > _capacity时,我们才扩容至capacity * 2,但是我们构造函数如果一开始为0的话,这里显然就会出问题了,直接把空间干为0了。
解决办法:
- 我们在构造函数里面处理,一开始就给capacity一个空间,防止为0
- 我们可以在这个再判断一下,如果capacity为0,那我们就给他一个空间也可以
②append
我们这里处理扩容和push_back不一样,我们插多少,扩多少,因为如果原数组太小,知识扩二倍之后还可能放不下
//增加一个字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//如果原数组太小,二倍之后还可能放不下
reverse(_size + len);
}
strcpy(_str + _size, str);
_size += len;//不要忘记改变size了
//最后加上\0
_str[_size] = '\0';
十一、+=的运算符重载(替换尾插数据的函数)
这个超级容易,其实就是直接复用我们的push_back和append。
利用运算符重载直接写两个即可
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
十二、在固定为插入,删除(insert和erase)
这时我们应该用
insert
在我们的插入也分为插入一个字符,和插入一个字符串。
其实思路大致相同,都是先判断是否需要扩容,然后挪数据,插数据
注意:这里挪数据时,很容易出错
总结:以后挪数据都将结尾定位最后一个位置的下一个位置,然后挪动。
①插入一个字符
//再pos
string& insert(size_t pos, char ch)
{
//1:判断是否需要扩容
assert(pos <= _size && pos >= 0);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
//2:挪数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
//3:插数据
_str[pos] = ch;
++_size;//不要忘记改变size了
return *this;
}
② 插入一个字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size && pos >= 0);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);//提前开空间
}
//挪动数据(包括\0)
//循环size-pos+1次
size_t end1 = _size;
size_t end2 = _size + len;
for (size_t i = 0; i <= _size - pos; ++i)
{
_str[end2--] = _str[end1--];
}
//插入数据
strncpy(_str + pos, str, len);
_size += len;//不要忘记改变size了
return *this;
}
我们这里解释一下挪数据的思路:我们只要考虑挪多少次就行了
插入数据直接使用
strncpy
将新的字符串从pos位置插入即可
char * strncpy ( char * destination, const char * source, size_t num );
③ 删除
string& erase (size_t pos = 0, size_t len = npos);
函数功能:就是删除从pos位置开始数,长度为len的字符(包括pos)
- 当len=npos时(npos是整形的最大值)或者要删除的len大于pos后面字符串的长度,直接将pos以及pos后面的所有字符删除,并且给pos位置赋值为\0
- 当len小于pos后面的字符串长度时候1,直接挪数据覆盖即可
注意:别忘了改变size
//将pos位置的字符,横跨len个长度的串删除(包括pos)
string& erase(size_t pos, size_t len = npos)
{
//要删除的个数比pos之后的字符串长
if (len == npos || len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
十三、交换swap
将我们两个指针进行交换。
所以比我们库里的交换函数快,少了一次拷贝构造,和两次赋值重载。
//交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
十四、find
就是在指定位置找。但是查找有两种方式
一种是查找字符
第二种是查找子串
函数原型:
size_t find(char c, size_t pos = 0)const
;从pos位置(包括pos位置)开始找字符
size_t find(const char* str, size_t pos = 0)const
;从pos位置(包括pos位置)开始找字符串
函数功能:找字符或者字符串,找到返回pos位置,找不到npos
① 找字符
size_t find(char ch, int pos = 0)
{
assert(pos <= _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
②找子串
这里我们使用strstr
函数功能:函数就是在一个字符串中找另外一个字符串是否存在,找到了就返回匹配的字符串的首元素地址,找不到就返回空指针
函数原型:
const char * strstr ( const char * str1, const char * str2 );
返回const型指针,对其解引用不能修改
char * strstr ( char * str1, const char * str2 );
//查找一个子串
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
char * p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
else
{
//指针相减就是中间的个数
return p - _str;
}
}
十五、流插入,流提取
① 流插入
我们的流插入不一定就是友元函数,我们也可以用迭代器,和[]来找到他的数据
//流插入
ostream& operator<<(ostream& out, const string s)
{
for (int i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
②流提取
我们看下面这个流提取,并分析他为什么是错的
istream& operator>>(istream& in, string s)
{
s.clear();
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
cin
的缓冲区,输入多个数据,两个数据分割就是空格或者换行,编译器认为我们是要多个字符,会将所以我们的空格换行我们就拿不到,它一直在读数据。所以cin和scanf识别不了' '和'\n'
所以我们可以提取数据需要不区分空格的操作符。
正好C++种get
就不会区分,每一个字符我们都可以拿到。
int get();
istream& get (char& c);
istream& operator>>(istream& in, string& s)
{
char ch = in.get(); //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据;
或者
//char ch;
//in.get(ch);
while (ch != ' ' && ch != '\n')
{
s += ch;
in.get(ch);
}
return in;
}
这个代码对于对象里面没有内容是没问题的,但是当对象里面一开始有数据时候就不可以了。
比如这种:
void test10()
{
string s1("0123456789");
s1 += '\0';
s1 += "xxxxxxxx";
cout << s1 << endl;
cout << s1.c_str() << endl;
cin >> s1;
cout << s1 << endl;
}
解释:我们调试会发现是扩容的原因,具体是strcpy导致的!!
我们发现中间出现了一堆乱码,因为在输入123后,cin开始拿数据,首先拿到1,尾插数据,没问题。当我们再拿2尾插的时候,此时我们的size+1 > capacity.我们就需要扩容。
加上1后。此时里面的数据是这个样子的0123456789\0xxxxxxxx1\0
此时我们的capacity=20,size=20,size+1>capacity,需要二倍扩容
但是,我们这里的扩容使用的是strcpy,他遇到第一个\0就会停下来。导致拷贝过去数据其实是
0123456789\0,后面的都没有拷贝过去,但是我们的size没有变,仍然是20,只是中间的数字全部是随机值了。我们添加数字2和数字3仍然是在末尾处添加,最后打印发现中间都是乱码
解决:我们可以使用s.clear(),将字符串清空,这样就不会出现这种问题了。
void clear();
//会有频繁扩容的问题
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get(); //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据;
或者
//char ch;
//in.get(ch);
while (ch != ' ' && ch != '\n')
{
s += ch;
in.get(ch);
}
return in;
}
但是这个代码其实还可以再优化一下,因为当我们输入的字符串太长时,会发生频繁扩容的问题。这个我们其实也可以解决的
//使用一个数组,将数据填到数组里面,填满了再加一次,然后将i置为0,再重投开始填数组
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get(); //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据;
char buffer[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buffer[i++] = ch;
if (i == 127)
{
buffer[127] = '\0';
s += buffer;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buffer[i] = '\0';//防止加了上一次后面遗留的数据
s += buffer;
i = 0;
}
return in;
}
十六、全部源码
string.h
#pragma once
#include <assert.h>
//定义一个命名空间,防止和库里面的sring类冲突
namespace mwq
{
class string
{
public:
typedef char* iterator; //要放在公共区域
typedef const char* const_iterator;
//迭代器begin和end
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//const迭代器
const iterator begin() const
{
return _str;
}
const iterator end() const
{
return _str + _size;
}
//string()
// :_str(new char[1])
// , _size(0)
// , _capacity(0)
//{
// _str[0] = '\0'; //字符串结束标志,不写的话,直接打印s1.c_str()会崩溃,因为找不到\0
//}
//string(const char* str)
// : _size(strlen(str))
//{
// _capacity = _size;
// _str = new char[_capacity + 1];
// strcpy(_str, str);
//}
//string(const char* str = nullptr) 不可以
//string(const char* str = '\0')
//string(const char* str = "\0")
//构造函数
string(const char* str = "")
: _size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造函数(深拷贝)
string(const string& s)
:_size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
//赋值运算符重载,所有情况统一处理,全部新开一个空间,然后将字符串拷贝到新空间,再销毁_str,再将新空间赋值给_str
string& operator=(const string& s)
{
if (this != &s)
{
char* temp = new char[s._capacity + 1]; //定义一个新空间,防止扩容失败,多开一个放\0
strcpy(temp, s._str);
delete[] _str;
_str = temp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
//[]运算符重载
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//不可修改的[]
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
//获取字符串元素个数
size_t size()const
{
return _size;
}
//返回空间容量大小
size_t capacity()const
{
return _capacity;
}
//判空
bool empty()const
{
return _size == 0;
}
//clear
void clear()
{
_str[0] = '\0';
_size = 0;
}
//返回C形式的字符串
const char* c_str() const
{
return _str;
}
//swap(s1, s2);
//s1.swap(s2);
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//s1>s2
bool operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
//s1==s2
bool operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
//s1>=s2
bool operator>=(const string& s) const
{
return strcmp(_str, s._str) >= 0;
}
//s1<s2
bool operator<(const string& s) const
{
return !(*this >= s);
}
//s1!=s2
bool operator!=(const string& s) const
{
return !(*this == s);
}
//s1<=s2
bool operator<=(const string& s) const
{
return !(*this > s);
}
//尾插字符
string& push_back(char ch)
//void push_back(char ch)
{
if (_size + 1 > _capacity)
{
if (_capacity == 0)
{
_capacity = 3;
}
reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
return *this;
//insert(_size, ch);
}
//尾插字符串
string& append(const char* str)
//void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
return *this;
//insert(_size, str);
}
//+=字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//+=字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size && pos >= 0);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size && pos >= 0);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);//提前开空间
}
//挪动数据(包括\0)
//循环size-pos+1次
size_t end1 = _size;
size_t end2 = _size + len;
for (size_t i = 0; i <= _size - pos; ++i)
{
_str[end2--] = _str[end1--];
}
//插入数据
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
//将pos位置的字符,横跨len个长度的串删除(包括pos)
string& erase(size_t pos, size_t len = npos)
{
//要删除的个数比pos之后的字符串长
if (len == npos || len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
//reserve和resize
//reserve不支持缩容
void reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1];
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
}
//不写第二个参数默认改为\0,使用缺省参数就可以将其合二为一
//1:n <= size
//2:size < n <= capacity
//3:n>capacity
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
memset(_str, c, n);
_size = n;
_str[_size] = '\0';
}
else
{
//if (n < _capacity)
//{
// memset(_str + _size, c, n - _size); //size后面的n个字符
// _size = n;
// _str[_size] = '\0';
//}
//else //扩容
//{
// reserve(n);
// memset(_str + _size, c, n - _size); //size后面的n个字符
// _size = n;
// _str[_size] = '\0';
//}
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, c, n - _size); //size后面的n个字符
_size = n;
_str[_size] = '\0';
}
}
size_t find(char c, size_t pos = 0)const
{
assert(pos >= 0 && pos <= _size);
for (size_t i = 0; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)const
{
assert(pos >= 0 && pos <= _size);
char* p = strstr(_str, str);
if (p != nullptr)
{
size_t pos = p - _str;
return pos;
}
return npos;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos = -1;
};
void test1()
{
string s1;
string s2("hello yaya");
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
s2[0]++;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
}
void test2()
{
string s1;
string s2("hello yaya");
string s3(s2);
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
string s4;
s4 = s3;
cout << s4.c_str() << endl;
}
void test3()
{
string s1("hello yaya");
//1:使用[]
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
//2:迭代器
string::iterator it1 = s1.begin(); //iterator是类型,所以需要typedef一下
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
//不能修改的版本
/*string::const_iterator it2 = s1.begin();
while (it2 != s1.end())
{
(*it2)++;
cout << *it2 << " ";
++it2;
}*/
//3:范围for,不用实现,因为底层就是调用迭代器
for (auto ch : s1)
{
cout << ch << " ";
}
}
void test4()
{
string s1;
string s2("hello yaya");
cout << (s1 > s2) << endl; //0
cout << (s1 == s2) << endl;//0
cout << (s1 >= s2) << endl;//0
cout << (s1 < s2) << endl;//1
cout << (s1 != s2) << endl;//1
cout << (s1 <= s2) << endl;//1
}
void test5()
{
string s1("hello yaya");
s1.push_back('a');
s1.push_back('b');
s1.append("haha");
s1 += 'm';
s1 += "xxxxxx";
}
void test6()
{
string s1("hello yaya");
/*s1.insert(0, 'x');
cout << s1.c_str() << endl;
s1.insert(2, 'x');
cout << s1.c_str() << endl;
s1.insert(10, 'x');
cout << s1.c_str() << endl;*/
s1.insert(0, "123"); //第0个位置
cout << s1.c_str() << endl;
s1.insert(2, "qqq"); //第2个位置
cout << s1.c_str() << endl;
s1.insert(10, "mmm"); //第3个位置
cout << s1.c_str() << endl;
s1.erase(12, 20);
cout << s1.c_str() << endl;
s1.clear();
cout << s1.c_str() << endl;
s1.insert(0, "123"); //第0个位置
cout << s1.c_str() << endl;
}
void test7()
{
string s1("hello yaya");
s1.resize(5, 'x');
cout << s1.c_str() << endl;
s1.resize(20, 'a');
cout << s1.c_str() << endl;
}
void test8()
{
string s1("hello yaya");
cout << s1.find('o', 0) << endl;
cout << s1.find("yaya", 0) << endl;
}
//直接在函数外面定义全局函数,使用迭代器和[]也能找到数据,不一定非要用友元
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i] << "";
}
return out;
/*for (auto ch : s)
{
out << ch << "";
}
return out; */
}
//不行,因为字符换字符之间是空格或者换行作为截至的,但是这里面从缓存区读取空格或者换行时,将其看成是一个字符了,导致无休止的输入
//istream& operator>>(istream& in,string s)
//{
// char ch;
// in >> ch; //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据
// while (ch != ' ' && ch != '\n')
// {
// s += ch;
// in >> ch;
// }
// return in;
//}
//会有频繁扩容的问题
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get(); //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据;
或者
//char ch;
//in.get(ch);
while (ch != ' ' && ch != '\n')
{
s += ch;
in.get(ch);
}
return in;
}
使用一个数组,将数据填到数组里面,填满了再加一次,然后将i置为0,再重投开始填数组
//istream& operator>>(istream& in, string& s)
//{
// s.clear();
// char ch = in.get(); //当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin对象直接从输入缓冲区中取数据;
// char buffer[128];
// size_t i = 0;
// while (ch != ' ' && ch != '\n')
// {
// buffer[i++] = ch;
// if (i == 127)
// {
// buffer[127] = '\0';
// s += buffer;
// i = 0;
// }
// ch = in.get();
// }
// if (i != 0)
// {
// buffer[i] = '\0';//防止加了上一次后面遗留的数据
// s += buffer;
// i = 0;
// }
// return in;
//}
void test9()
{
string s1("hello yaya");
cout << s1 << endl;;
}
void test10()
{
string s1("0123456789");
s1 += '\0';
s1 += "xxxxxxxx";
cout << s1 << endl;
cout << s1.c_str() << endl;
cin >> s1;
cout << s1 << endl;
}
};
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "string.h"
int main()
{
mwq::test10();
return 0;
}