在C语言中只提供了一系列str的库函数,字符串是以'\0'为结尾的一些字符的集合,这些库函数和字符串是分离的,在C++中添加了一个类——string,用来管理字符串以及一系列的函数。
在string类中一般来说有三个成员函数,分别是_str、_size和_capacity,_str是一个指向我们存放字符串内容的指针,_size表示这个字符串的的长度(不包括结尾的'\0'),_capacity当前这个string对象能够存储的字符串的长度,这个长度是能够动态增长的。
一、构造函数
string类常见的实例化对象的方式共有以下几种
//当不传入参数时,会构造出一个空字符串
string()
//传入一个字符串时,会根据这个字符串构造一个函数
string(const char* s)
//传入的参数是一个已经存在的string对象时,要进行深拷贝
string(const string&s)
首先我们知道字符串都是以'\0'为结尾的,所以我们在使用这个默认构造函数的时候构造出来的“空”字符串并不是真的没有东西,里面至少有一个'\0';同样是这个原因,因为字符串是要以'\0'为结尾的,一个string对象能存储的字符的个数是_capacity-1个,为了让string对象能够存下_capacity个字符并且最后以'\0'结尾,我们在申请空间的时候就要多申请一个。
//这里把传字符串和无参的糅合到一起成默认构造函数了
string(const char* str="")
{
if(str == nullptr)
return;
//为了存下'\0'多开了一个位置
_str = new char[strlen(str)+1];
strcpy(_str,str);
}
string(const string& s)
{
//因为是成员函数,所以可以直接取得s对象的_str
//用s对象的_str直接调用默认构造函数,这样就可以直接拷贝出一份我们想要的空间
//再把这个空间交换给要实例化的对象
string tmp(s._str);
//这里的swap并不是类模版里的swap函数
swap(str);
}
string& operator=(const sting& s)
{
//这里要判断是否是用自己来初始化自己
//如果是自己初始化自己,深拷贝之后释放空间以后就会出错
if(this!=&s)
{
//这里不仅tmp构造好了三个成员变量,因为tmp是局部变量,所以出了这个作用域
//tmp就会自动销毁,就会调用析构函数,会把this交换过去的_str所指向的那块空间自动释放掉,省去了自己释放空间的过程
string tmp(s._str);
swap(tmp);
}
return *this;
}
//类模版里的swap是实例化出一个c对象,借助c对象把我们要交换的a,b对象的内容进行交换
//这里的string类中的_str指向了堆上的资源,所以在进行拷贝构造和赋值重载的时候都要进行深拷贝
//这里如果用默认的swap要进行三次深拷贝,效率很低
//我们把重载一个swap的成员函数,手动对这三个成员变量进行交换,因为这三个成员变量都是内置类型
//虽然_str指向一块堆上的资源,但是它本身只是一个指针,所以在交换的时候都不涉及深拷贝
void swap(string& s)
{
swap(_str,s._str);
swap(_size,s._size);
swap(_capacity,s._capacity);
}
二、string类对象的访问及遍历操作
在string中多有一个组成部分叫做迭代器——iterator。有了迭代器,我们就可以用类似于指针的形式访问string对象里的内容string类中的迭代器有两种分别是iterator和const_iterator,iterator的权限是可读可写,而const_iterator的权限是只能读不能写。string类中的两个成员函数begin()和end()分别能取得string对象开始位置的迭代器和string对象存储的最后一个字符的下一个位置的迭代器。
//这里我们就用指针来模拟实现迭代器的功能
typedef char* iterator
typedef const char* const_iterator
iterator begin()
{
return _str;
}
iterator end()
{
//_size表示存储的字符的个数,下标就是位数-1,所以_size就是最后一个字符的下一个位置,也就是'\0'的位置
return _str+_size;
}
//才有引用返回可以实现类似s[1]=‘s’的功能
char& operator[](int i)
{
return _str[i];
}
//比如出现const char& x = s1[0]的情况时x被const修饰,如果只有上面的几种函数就会报错
//因为参数x被const修饰,而返回值是可读可写的,权限被放大了,所以还有一系列const修饰的迭代器的函数
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str+_size;
}
const char& operator[](int i) const
{
return _str[i];
}
还有两个函数rbegin 和 rend也是用来获取迭代器的,不过这个迭代器是从尾部开始获取,rbegin获取的是最后一个字符所在位置的迭代器,rend获取的是第一个字符之前位置的迭代器,对rbegin获取的迭代器进行++操作是是向前遍历的。
因为有了迭代器的存在所以在遍历的时候就多了两种方式,如果想要遍历string对象中的数据,就可以采用下面的迭代器方式和范围for。其中范围for里的参数auto是让编译器自动识别变量的类型,在把s里的数据轮流赋值给ch,其实本质上在编译以后,编译器会把范围for的形式转化为上面的迭代器的形式,所以如果我们在模拟实现的过程中想要使用范围for,就一定要写好begin和end函数。
string::iterator it=s.begin();
while(it!=s.end())
{
cout<<*it;
}
for(auto ch : s)
{
cout<<ch;
}
三、string类对象的修改操作
这个类型的函数主要有push_back,append,operator+=,c_str、find以及resize。
push_bakc就是在字符串的尾部添加一个字符c,但是这里就会涉及到一个扩容的问题,当一个string类里数据存满了以后我们要堆容量进行扩容,string类里也提供了一个功能类似的函数reserve。
reserve函数是用来设置对象的容量,当我们想要设置的容量大于当前对象已有的容量时,会另外开辟一个我们想要的大小的空间,然后再把对象中原有的数据拷贝过去,之后再回首原本的空间。当我们想要设置的空间是小于当前对象的容量的时候,就不会对对象进行操作。
reserve是对容量进行操作,而resize是对对象存储数据的个数进行操作,当传入的n要大于_capacity时,把对象的容量扩容至n同时用c把剩余的空间填满,当n大于_size并且小于_capacity时,则只进行填充操作不扩容,当n小于_size时,则把对象的内容调整至n个,但是不改变对象的容量。
void reserve(size_t n)
{
//扩容的空间比原来大才扩容,比原来小不操作
if(n>_capacity)
{
//多开一个位置存放'\0'
char* tmp=new char[n+1];
strcpy(tmp,_str);
delete[] _str;
_str=tmp;
_capacity=n;
}
}
void resize(size_t n, char c = '\0')
{
if (n > _size)
{
if (n > _capacity)
reserve(n);
memset(_str + _size, c, n - _size);
}
_size = n;
_str[_size] = '\0';
}
利用这个函数就可以很方便的进行扩容
void push_back(const char& c)
{
//_size和_capacity相等的时候表示存满了 多开的那一个位置需要用来存'\0'
if(_size == _capacity)
{
//如果当前容量为0就一次性给4个空间
reseve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++]=c;
//我们只是给'\0'开了一块空间,这里是一个字符一个字符的进行赋值,所以原来最后一个位置的'\0'被这个字符覆盖了,所以在赋值以后要自己添加上一个'\0'
_str[_size]='\0';
}
append函数是在对象的末尾加上一段字符串,这里的逻辑和push_back的逻辑差不多,只是这里扩容就不能进行简单的二倍扩容,有可能存在想要加入的字符串的长度比二倍扩容还有长的情况,同时因为字符串的结尾一定有'\0',所以赋值以后不用自己加上。
void append(const char* str)
{
//这里计算出来的长度并不包括'\0'但是我们在给对象开空间的时候已经给'\0'预留过空间了,所以不需要加上
size_t len=strlen(str);
//判断剩下的空间是否足够存下字符串str,如果不够则进行扩容
if(len > _capacity - _size)
{
//如果二倍扩容以后的空间还不能存下str,则存下str需要多少,我们就扩容多少
reserve(len > 2 * _capacity ? len : 2 *_capacity);
}
strcpy(_str+_size,str);
_size += len;
}
对string对象进行+=操作也有两种形式,可以+=一个字符,也可以+=一个字符串,都是在对象的尾部添加数据,这里和我们上面实现的两个函数功能相同,就可以进行复用了
//这里才有传引用返回的形式就可以支持连续+=的操作
string& operator(const char c)
{
push_back(c);
return *this;
}
string& operator(const char* str)
{
append(str);
return *this;
}
当然我们还可以支持查看string对象中_str里的数据,同时我们对对象进行了封装,我们不希望在类外,使用者能够自行对_str里的数据进行操作,只能通过我们给的接口对对象进行操作,所以这样。
const char* c_str() const
{
return _str;
}
find函数也支持两种查找,find(char c, size_t pos),从pos位置开始查找c第一次出现的位置,如果找到则返回这个位置的下标,没有找到则返回npos(对象中存储数据的结束位置,也就是'\0'这里不属于这个对象),find(const char* s, size_t pos)同样是从pos位置开始查找字符串s第一次出现的位置,找到则返回s的第一个字符的下标,如果没找到也返回npos;这两个函数中的pos如果没有指定,则会从头开始搜索。
size_t find(char c, size_t pos = 0) const
{
for(size_t i=pos;i<_size;++i)
{
if(_str[i] == c)
return i;
}
return npos;
}
size_t find(const char* s,size_t pos = 0)const
{
char* tmp = strstr(_str+pos,s);
if(tmp)
return tmp-_str;
return npos;
}
四、string类的非成员函数
在string类的非成员函数中最重要的就是operator>>和operator<<,对这两个运算符进行重载以后就可以直接堆string类的对象进行插入和提取操作了。
要注意的是在进行operator>>重载的时候有几个要注意的点。一是:如果我们直接使用cin对内容进行提前的话,cin会默认跳过' '和'\n'这样函数就一直提取不到这两个字符,就不能用这个两个字符作为提取的结束标志,所以我们要采用cin.get()的方式,这样就可以提取到' '和'\n';二是:如果对一个已经有内容的对象进行插入操作时,并不会把原先的内容清除,所以我需要有一个函数来清除对象内原本存在的内容,但是不回收空间,这样能提供效率;三是:如果在插入字符串的时候是一个字符一个字符的进行插入,那这样扩容的成本会很高,因为我们每次都只进行二倍的扩容,假如我们对象开始的容量很小,但是要插入的字符串很长,这里就会浪费很多效率在扩容上,如果我们一开始就给对象一个很大的容量,当插入的字符串很短的时候,又会浪费空间;所以这里我们采用自己设计一个缓存区,当这个缓存区满的时候我们才给对象赋值,这样对象一次就能开比较大的空间同时,如果要插入的内容很短的时候,也不会浪费空间。
//这里是要进行流提取不会对对象s进行操作,所以可以用const修饰并且采用传引用传参能减少拷贝
ostream& operator<<(ostream& out,const string& s)
{
for(auto e : s)
{
out<<e;
}
return out;
}
void clear()
{
//只要把_size指向第一个位置,同时第一个位置置成'\0'就完成清除数据的工作了
_str[0]-'\0';
_size=0;
}
istream& operator>>(isteram& in,string& s)
{
s.claer();
char ch;
const int N = 256;
char buff[N]={ 0 };
ch = in.get();
int i=0;
while(ch != '\0')
{
buff[i++] = ch;
if(i == N - 1)
{
buff[i] = '\0';
s+=buff;
i = 0;
}
ch = in.get();
}
if( i != 0)
{
buff[i] = '\0';
s+=buff;
}
return in;
}
五、在指定位置的插入和删除
insert函数可以实现在指定的位置插入数据,同样有插入字符和插入字符串两个版本,而erase则是删除从pos位置开始长度为len的字符串。
void insert(size_t pos, char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//_size存的是'\0'所以实际上空的位置是_size + 1
size_t end = _size + 1;
while(end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
}
void insert(size_t pos, const char* str)
{
int len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
}
size_t end = _size + len;
while (end>pos)
{
_str[end] = _str[end - len];
--end;
}
memcpy(_str + pos, str,len);
_size += len;
}
void erase(size_t pos, size_t len)
{
//当要删除的长度超过_size时,和清空数据一样操作
//直接在要清空的位置置成'\0'再把_size指向这个位置就完成清除操作了
if (pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
}