我们要模仿STL库里面的string,基本实现常用的一些操作。
目录
成员函数的接口
默认成员函数
构造函数
拷贝构造函数
析构函数
operator=赋值运算符重载
容量大小相关的函数
size( )函数
capacity()函数
empty()函数
reserve()函数
resize()函数
clear()函数
访问下标相关函数
operator[ ]函数
查找相关函数
find()字符
find()字符串
迭代器相关的函数
插入字符的相关函数
insert()一个字符
insert()字符串
尾插push_back( )
追加字符串append( )
operator+=加等于一个字符
operator+=加等于一个字符串
erase删除函数
关系运算符重载函数
operator< 小于
operator== 等于
operator<= 小于等于
operator> 大于
operator>= 大于等于
operator!= 不等于
流插入 流提取 函数重载
流提取
流插入
其他函数
Swap( )交换函数
c形式的字符串
#pragma once
#include
#include
#include
#include
using namespace std;
namespace sjj//自定义一个命名空间
{
class string
{
public:
typedef char* iterator;//string的迭代器就是原生指针
typedef const char* const_iterator;
//构造函数
//string("hello");
string(const char* str = "");
//拷贝构造函数
//s2(s1)
string(const string& s);
//赋值重载
//s2=s1
string& operator=(string s);//传值传参也是拷贝构造
//析构
~string();
//容量大小相关的函数//
size_t size()const;
size_t capacity()const;
bool empty()const;
void clear();
void reserve(size_t n);//对容量进行改变,让容量到n
void resize(size_t n, char ch = '\0');//改变size的大小
//访问下标相关的函数//
char& operator[](size_t pos);
const char& operator[](size_t pos)const;
//查找相关的函数//
//正向查找一个字符
size_t find(char c, size_t pos = 0);
//正向查找一个字符串
size_t find(char* s, size_t pos = 0);
///迭代器相关的函数/
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;
///插入字符串相关的函数/
//尾插
void push_back(char c);
//追加字符串
void append(const char* str);
//加等于
string& operator+=(char c);
const string& operator+=(char* str);
//插入一个字符
string& insert(size_t pos, char c);
//插入字符串
string& insert(size_t pos, const char* s);
//删除相关的函数
//删除pos位置开始的n个字符
string& erase(size_t pos, size_t len = npos);
//其他函数//
//自己写的交换函数
void Swap(string& s);
//C形式的字符串
const char* c_str()const;
private:
char* _str;//字符串数组
size_t _size;//有效字符的个数 不含\0
size_t _capacity;//能存储有效字符的空间,不包含\0
static const size_t npos;//静态成员变量,在类外部初始化
};
const size_t string::npos = -1; //初始化
///关系运算符重载//
bool operator<(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
// operator<< operator>>
// cout< operator<<(out,s1);
///流插入 流提取 重载///
ostream& operator<<(ostream& out, const string& s);
//ostream&是系统的类型,它能够实现输出内置类型的数据
istream& operator>>(istream& in, string& s);
}
给个空字符作为缺省值,不传参时,构造的就是空字符串,传入参数,就用传入的参数。
string(const char* str = "")//给缺省值""空字符串
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];// +\0
strcpy(_str, str);//char *strcpy( char *strDestination, const char *strSource );
}
首先你一定要了解一下深浅拷贝相关的知识,请戳这!
第一种写法:传统写法
//传统写法 //要用自己写的
string(const string& s)
:_size(0)
, _capacity(0)
{
_str = new char[s._capacity + 1];//_str申请一块和s._str一样大的空间
strcpy(_str, s._str);//将两个指针指向交换
_size = s._size;
_capacity = s._capacity;
}
我们是老老实实的进行深拷贝,先new一块和原来一样大的空间,然后再深拷贝数据,这样两块空间才不会相互影响
第二种:现代写法(推荐写)
//现代写法————投机取巧
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);//复用构造函数,构造一个tmp对象出来
this->Swap(tmp);//相当于Swap(this,tmp)
}
总结:
现代写法投机取巧,复用构造函数,让他开一个临时对象出来,出了作用域,局部临时对象自动销毁,我们不用多管它,我们最后只需要把它们的指针指向交换即可,代码很简洁 。
清理动态的一些资源
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
如果我们不写,它也会使用编译器默认生成的,但是也是浅拷贝,不符合我们的使用要求
我们知道要深拷贝,那么首先要考虑两个空间的容量大小问题:
①s3的数据个数大于s1中数据个数,我们要考虑s1的扩容问题
②s1的空间远大于s3的数据个数,我们需要考虑缩容问题
这样我们不如将问题变得简单粗暴一点,直接将s1的空间给释放掉,重新开一块和s3一样大空间,最后再将数据拷贝过去
第一种:传统写法
string& operator=(const string& s)
{
/*if (this != &s)//防止自己给自己赋值s3=s3
{
delete[] _str;//释放掉s1 默认有个隐藏的this指针,实际上是这样: this->_str
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}*/
//优化版本
//new可能会失败,但是却先释放s1了,我们可以先new
if (this != &s)//防止自己给自己赋值s3=s3
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
}
return *this;//返回s1
}
第二种:现代写法(很巧妙)
//更NB的写法
//s1=s3 这里传值传参也是一种拷贝,s充当的就是tmp
string& operator=(string s)
{
this->Swap(s);
return *this; //返回左值,支持连续赋值
}
总结:这里用的现代写法很巧妙,使用传值拷贝,相当于一次拷贝构造函数的调用。
我们直接返回_size即可,不包括\0
const size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
可以利用strcmp函数,比较两个字符串指针。
//判断是否为空
bool empty()
{
return strcmp(_str, "") == 0;
}
调整容量大小到n,我们先new一个n+1的新空间,然后把原来的数据拷贝到新空间上面,再释放掉原来的空间,再交换两个指针的指向,再将_capacity置为n即可。
我们来看看动图演示:
void reserve(size_t n)//对容量进行改变,让容量到n
{
if (n > _capacity)
{
char* tmpstr = new char[n + 1];
strcpy(_str, tmpstr);
delete[] _str;
_str = tmpstr;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')//改变size的大小
{
if (n <= _size)//缩小的情况
{
_size = n;
_str[n] = '\0';
}
else//要插入数据的情况
{
if (n > _capacity)
{
reserve(n);
}
//void *memset( void *dest, int c, size_t count );
memset(_str + _size, ch, n - _size);
_size = n;
_str[n] = '\0';
}
}
resize函数会出现两种情况:
第一种:给定的size的值n小于_size,这种情况数据会被截断,只保留前面的n个
第二种:给定的size的值n大于capacity容量了,要考虑扩容问题
将_size位置置为斜杠0,就相当于清空了字符串。
void clear()
{
_size = 0;
_str[_size] = '\0';
}
operator[ ]下标访问元素,是我们最为熟悉的一种方式,而且非常的简洁方便,实现它是为了让我们可以像访问数组一样去访问字符串,我们可以实现普通版本和const对象版本。
char& operator[](size_t pos)
{
assert(pos < _size);//防止越界
return _str[pos];//返回pos位置的引用,相当于就是把那个字符给返回了
}
//const版本
const char& operator[](size_t pos)const
{
assert(pos < _size);//防止越界
return _str[pos];//返回pos位置的引用,相当于就是把那个字符给返回了
}
npos是string类的一个静态成员变量,其值为整型最大值。
我们通过遍历的方式,从头往后寻找匹配的字符串,找到返回下标,没有找到返回npos。
//正向查找一个字符
size_t find(char c, size_t pos = 0)
{
assert(pos < _size);
for (int i = 0; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;//没有找到目标字符,返回npos
}
利用的是strstr字符串查找函数,找到了返回字符串的第一个字符的指针,没有找到返回NULL。
找到了字符串,我们可以通过指针差值确定目标字符串的位置。
//正向查找一个字符串
size_t find(char* s, size_t pos = 0)
{
assert(pos < _size);
//const char * strstr ( const char * str1, const char * str2 );
const char* ptr = strstr(_str + pos, s);//调用strstr字符串查找函数
if (s)
{
return ptr - _str;//返回找到第一个字符的下标
}
else
{
return npos;
}
}
string类里面的迭代器就相当于原生指针,只不过这里是typedef的,这里我们把它当做指针来看就行了。
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
我们先来看看动图演示的插入原理:
insert函数要在任意位置插入字符,首先检查下标合法性,再判断是否需要扩容,扩容可以使用reserve函数,再将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串
//插入一个字符
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)//判断是否需要增容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//从后往前挪动数据,以免被覆盖
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
原理和插入一个字符类似
//插入字符串
string& insert(size_t pos, const char* s)
{
assert(pos <= _size);//检查有效位置
size_t len = strlen(s);//记录要插入的字符串长度
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, s, len);//拷贝数据到pos位置
_size += len;
return *this;
}
push_back就是在一个字符串尾部拼接上字符,我们首先要考虑空间是否足够,若不够需要扩容,可以利用reserve函数,扩大2倍,(不一定要扩2倍),再插入数据,最后末尾填上\0即可。
void push_back(char c)
{
if (_size == _capacity)//扩容
{
//这里要注意,刚开始初始化给定的_capacity=0时
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//再插入
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
也可以直接复用insert函数,尾插的意思就是在pos位置为_size的位置插入
//尾插字符
void push_back(char c)
{
insert(_size, c); //在字符串末尾插入字符ch
}
append其实和push_back类似,只不过append是拼接上一个字符串,这里问题就是出在字符串,我们是扩2倍?扩1.5倍?都不是很确定,这里要看的是传入的字符串个数,我们的空间至少要开到_size+len,空间刚刚好满足所有的字符。开好空间后,利用strcpy把字符串里面的数据拷贝过去即可。
//追加字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
也是可以复用insert函数,在原来字符串的末尾插入即可。
//尾插字符串
void append(const char* str)
{
insert(_size, str); //在字符串末尾插入字符串str
}
加等于也是我们用的比较多的一个函数,因为它方便简洁,一看就懂。我们可以复用push_back,注意返回新的对象。
//加等于一个字符
string& operator+=(char c)
{
//复用push_back
push_back(c);
return *this;
}
复用append
//加等于一个字符串
const string& operator+=(char* str)
{
//复用append
append(str);
return *this;
}
我们要删除pos位置开始的len个字符
我们需要考虑两个问题:
第一个:pos位置之后的所有位置都要删除,将\0置于pos位置,_size-=pos即可
第二个:pos位置之后的字符只删除一部分,剩余部分要填充前面的空位。
//删除pos位置开始的n个字符
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len > _size)//删除pos位置之后的所有字符
{
_size = pos;
_str[_size] = '\0';
}
else//删除pos位置之后的部分字符
{
strcpy(_str + pos, _str + pos + len);//用后面的剩余字符填充空位
_size -= len;
}
return *this;//返回新的对象
}
strcmp () 函数比较的不是字符串的长度, 而是比较字符串中对应位置上的字符的大小 (即比较的是ASCII码值,而且 还要注意区分大小写,传入c_str是一个非空的指针
bool operator<(const string& s1, const string& s2)
{
// size_t i1 = 0;
// size_t i2 = 0;
// while (i1 < s1.size() && i2 < s2.size())
// {
// if (s1[i1]s2[i2])
// {
// return false;
// }
// else
// {
// ++i1;
// ++i2;
// }
// }
// //while的条件是 且
// //一个一个的字符按照ASCII码值去比较
// //现在是有一个字符串已经走完了
// // "abcd" "abcd" false
// // "abcd" "abcde" true
// // "abcde" "abcd" false
// return i2 < s2.size() ? true : false;
//c_str是一个内容为字符串指向字符数组的临时指针
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
实现了上面的小于和等于,下面的运算符都可以复用已经实现了的。
复用小于等于
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
复用小于等于的反面
bool operator>(const string& s1, const string& s2)
{
return !(s1 < s2&& s1 == s2);
}
复用小于
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
复用等于
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
要注意,ostream,istream类是c++标准输出流中的基类,它能够输出内置类型,我们既可以把它写成全局的函数,也可以重载成为友元,但是友元破坏了程序的封装性,不推荐。
ostream& operator<<(ostream& out, const string& s)
//ostream&是系统的类型,它能够实现输出内置类型的数据
{
for (auto ch : s)
{
out << ch;
}
return out;
}
我们要注意先清理s对象,以免初始化对象时给定了初值。
istream& operator>>(istream& in, string& s)
{
s.clear();//防止对象初始化已经给了值
char ch = in.get();
while (ch != ' ' && ch != '\n')//遇到空格或者换行就停止
{
s += ch;
ch = in.get();//再获取下一个
}
return in;
}
因为多处要用到swap函数,且实现都是差不多的,不妨将其封装成为一个成员函数。
//模拟string类里面的交换函数
void Swap(string& s)
{
std::swap(_str, s._str);//用C++库里面自带的交换函数
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
我们可以查询,C++全局库里面有个swap函数,string类里面也有个swap函数,这两个函数都能实现交换
std库里面的swap函数:
string类里面的swap函数:
int main()
{
std::string s1("hello");
std::string s2("C++");
s1.swap(s2);//调用string类里面的swap函数
swap(s1,s2);//调用全局的swap函数
return 0;
}
这两个谁的效率更高呢?
答案:string类里面专门的交换函数效率更高,因为仅仅是对成员变量进行交换;而std库里面的是用模板实现的,会创建临时对象,并且进行三次深拷贝,代价更高!
返回一个非空的指针
//C语言形式的字符串
const char* c_str()const
{
return _str;
}
谢谢观看!