·博主博客链接——点击这里
文章目录
前言
提示:本文较为硬核,学者需认真跟着我的脚步
在讲string之前,需要大家回答一下,我们以前是怎么存一段字符串的?想必绝大部分人还停留在C语言,如下:
int main()
{
const char * tmp = "abcd";
return 0;
}
好的,上面的代码是你的答案,那么,我将对你进行拷打,要是我们要在你上面的tmp指向的字符串中,插入数据、删除数据,求长度等等运算,单靠着上面我们之前学习的代码,能实现,但是会很麻烦,需要我们自己去写对应的函数去实现相应的功能。
但是,只要我们仔细想想,就不难发现难道前辈们没有发现字符串的实现这么复杂吗?难道前辈们不去专门做一个类,这个类专门用来存放字符串,且相关函数都已经实现好(修改,插入…),等着我们直接来使用吗是的,你发现了!!
前辈为我们"造好了轮子",我们使用"现成的轮子"即可。
一、string是什么?
string是编程语言中用于便捷存储、操作字符串的类型,能自动管理内存、还提供丰富的字符串操作方法,让字符串处理更轻松。
二、string的使用
下面,我将根据string文档介绍中对string的重要部分进行讲解
1.string的底层本质

2.string的构造函数
default (1) string();
copy (2) string (const string& str);
substring (3) string (const string& str, size_t pos, size_t len = npos);
from c-string (4) string (const char* s);
from sequence (5) string (const char* s, size_t n);
fill (6) string (size_t n, char c);
range (7) template <class InputIterator> string (InputIterator first, InputIterator last);
可以看到,构造函数重载了7种,我们只需重点学习当中的1、2、4
这里旨在构造空串,也就是字符串只有一个元素,那就是’\0’
#include<iostream>
#include<string>
//使用string必须包含这两个头文件
default (1) string();
//这里旨在构造空串,也就是字符串只有一个元素,那就是'\0'
int main()
{
string s1;
return 0;
}
from c-string (4) string (const char* s);
//这里就运用到我们之前的const char*指针来构造我们的string
int main()
{
const char * s = "abcde";
string s1(s);
//也可以这么写:string s1("abcde");
return 0;
}
//由代码可知,s指向了"abcde",s构造s1,即s1也为"abcde"。
copy (2) string (const string& str);
//大家读文档,可以连蒙带猜。这里就是用已有的一个string去构造
int main()
{
string s1("hello zh");
string s2(s1);
//最终s1与s2对象指向的那块内容都是一样的
return 0;
}
3.size与length
size_t size() const;
//Return length of string
size_t length() const;
//Return length of string
我们可以看到,这两个接口都是返回当前字符串的有效字符个数,例如"abcd",返回的就是4。
这两个接口的功能都是相同的,那为什么同样的接口要实现两个呢?原因解释:那是因为string是祖师爷第一个实现的类,他们想着字符串的大小就是长度,所以就定义了一个叫length的接口。但是另外的数据结构像链表,它是由一个一个的结点拼接而成的,它的大小称为length就有点不太合适,因此,为了耦合性,祖师爷为每个容器都定义了size()接口,用来求这些容器的大小。
总的来说就是length()只适用于string,而size()可以适用于任何的容器。
代码示例如下:
int main()
{
string s1("hello czh");
cout<<s1.size()<<endl;
cout<<s1.length()<,endl;
return 0;
//打印出来的结果均为9
}
4.resize与reserve
re,就蕴含着“再”的意思,那么resize就是对string的大小size进行改变,reserve就是对string的容量进行改变。
❶resize
void resize (size_t n);
void resize (size_t n, char c);
int main()
{
string s("hello world");
s.resize(5);//打印出来结果为:hello
s.resize(15,'c');//打印出来结果为:hello worldcccc,原先size为11,多出来的4个全初始化为c
return 0;
}

❷reserve
void reserve (size_t n = 0);
string,当插入的字符足够多达到最大容量的时候,编译器也会进行扩容的。而且当我们数据的插入足够频繁时,编译器就会不断地为string扩容。
频繁扩容的后果:内存拷贝开销大,造成大量内存碎片,进而导致额外的内存浪费。
所以当我们确认好最终字符串的大小,直接给这个string确定好容量,后面也就不用频繁进行扩容了。
int main()
{
string s;
s.reserve(100);
}
为s先开了100个空间,确保其前期不需要因为容量低而导致频繁扩容。
5.substr与c_str
❶substr
string substr (size_t pos = 0, size_t len = npos) const;
Returns a newly constructed string object with its value initialized to a copy of a substring of this object.
拷贝从原始string的pos位置开始的len个字符

这里将一下第二个缺省参数npos,npos给的值为-1。假设我们不传,就使用这个npos
-1其实是整型的最大值,-1的原码为10000000 00000000 00000000 00000000。补码为
1 10000000 00000000 00000000 00000000,这是一个超出整型范畴的数字。
也就是说,假设我们不传第二个参数,也就是不知道要拷贝多少个字符,那么编译器就会靠别从pos位置开始一直到末尾的字符。
代码示例如下:
int main()
{
string s("hello ahodhaodhao");
cout << s.substr(3)<<endl;
return 0;
}
//打印出来的结果就为llo ahodhaodhao
❷c_str
const char* c_str() const;
Returns a pointer to an array that contains a null-terminated sequence of characters (i.e., a C-string) representing the current value of the string object.
c_str接口返回的是指向这个字符串的指针,并且返回的这个指针类型是const char*
const char * 与一般的指针不同,在我们执行流插入语句时,假设是其他类型的指针,往往都是打印地址,而const char * 不同,打印const char * 都是直接把这个指针所指向的字符串打印出来
代码示例,如下:
int main()
{
string s1("hello");
cout<<s1<<endl;
cout<<s1.c_str()<<endl;//const char *
return 0;
//可以得到,两者打印出来的结构均为:hello
}
6.push_back/append/operator+=
❶push_back
void push_back (char c);
Appends character c to the end of the string, increasing its length by one.
push_back(char c);
即在字符串末尾插入一个c字符。
string s1("abc");
cout<< s1<<endl;
s1.push_back('d');
cout<< s1<<endl;
两次打印的结果分别为: abd abcd。
❷append(选取重点进行讲解)
string& append (const string& str);
string& append (const char* s);
根据append示例,可在原有字符串末尾添加string型与const char * 型
代码示例,如下:
string s(" czh");
const char * tmp = " hottoday";
string s1("hello");
string s2 = s1.append(s);
cout<<s2<<endl;
string s3 = s1.append(tmp);
cout<<s3;
打印出来的结果分别为 hello czh/hello hottoday。
❸operator+=这是我最推荐大家使用的尾插接口
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);
这个接口可以插入单个字符,string类型以及const char * 类型,可谓是功能齐全push_back只能插入单个字符,而append的功能实现得又异常的冗余。
代码示例如下:(为方便,只实现第一种功能)
int main()
{
string s("abc");
string tmp("!!!");
string s1 = s+= tmp;
cout<<s1;
return 0;
//打印出来的结果为:abc!!!
}
三.string的模拟实现
namespace zehong//为避免与库里的string起冲突,我们使用命名空间将我们自己定义的string包起来
{
class String
{
public:
private:
char * _str
size_t _size;
size_t _capacity;
};
}
1.构造的实现
根据文档,我们能为string实现以下三种构造函数
public:
string();//空串
string(const char * str);//内容与str指向的一样
string(const string & s);//拷贝构造
代码实现,如下:
public:
string()
:_str(new char[1]('\0'));//空串表示串中只剩'\0'
,_size(0)
,_capacity(0);
{
}
string(const char * str)
:_size(strlen(str))//我的个数跟你str的个数是一致的
{
_str = new char[_size + 1];//+1是为了留一个位置给'\0'。
_capacity = _size;
strcpy(_str,str);
}
string(const string &s)
:_str(new char[s._size + 1]);
,_size(s._size);
,_capacity(s._capcity)
{
strcpy(_str,str);
}
答疑:为什么第二个构造不采用下面的写法?
string(const char * str)
:_str(new char[str._size + 1])
,_size(strlen(str))
,_capacity(strlen(str))
{
//
}
解答:这是因为strlen函数的原因,strlen函数是计算字符串长度的函数,在遇到’\0’之前就停下来,这本身是一种循环数数的逻辑,上面的写法调用了三次strlen逻辑,三次循环,降低了代码的运行效率与可读性。
2.operator[]
重载operator[]是为了让我们能够实现像下标[]访问数组一样访问我们string。
public:
char & operator[](size_t i)
{
return _str[i];
}
const char & operator(size_t i)const
{
return _str[i];
}
这两个函数有什么区别呢?
❶函数的常量性
char &operator[](size_t i):这是一个非const成员函数。它允许通过对象调用,并且可以用来修改对象内部存储的字符数据(_str)中对应下标的字符。意味着调用者可以通过返回值直接修改原数据。
const char & operator[](size_t i):这是一个const成员函数,函数声明最后的const表示这个函数不会修改对象的成员变量。它适用于const对象调用,比如你有一个const类型的字符串类对象时,只能调用这个const版本的operator[],而且它返回的是从const char类型的引用 ,防止通过返回值意外修改原数据。
❷使用场景
int main()
{
string s = "hello";
s[0] = 'H';//调用非const修改值
cout<<s<<endl;//输出Hello
return 0;
}
int main()
{
const string s = "hello";
char c = s[1];
cout<<c<<endl;//输出e
//s[1] = E;//此时会报错,返回值是const修饰的,无法被修改
return 0;
}
3.插入与扩容
public:
void reserve(size_t n);//扩容
void push_back(char c);
void append(const char * str);
string & operator+=(char c);
string & operator+=(const char * str);
public:
void reserve(size_t n)
{
char * tmp = new char[n + 1];
memcpy(tmp,_str,_size);
delete[] _str;
_str = tmp;
_capacity = n;
}
void push_back(char c)
{
if(_size>=_capacity)//满了就走扩容
{
reserve(size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_size = '\0';//这步大家容易忽略。
}
void append(const char * str)
{
size_t len = strlen(str);
if(_size + len > _capacity)
{
reserve(size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
memcpy(_str + _size,str,len);
_size = _size + len;
}
string & operator+=(char c)
{
push_back(c);
}
string & operator+=(const char * str)
{
append(str);
}
4.insert和erase
public:
insert(size_t pos,char c)
{
if(_size>=_capacity)//满了就走扩容
{
reserve(size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity);
}
int end = _size;
while(end>=(int)pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = c;
++_size;
}
erase(size_t pos,size_t len)
{
assert(pos<_size)
if(len == npos || len >= _size - pos)
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t i = pos + len;
memmove(_str + pos,_str + i,_size + 1 - i);
_size -= len;
}
}
5.substr(getline)
string substr(size_t pos,size_t len)const
{
if(len == npos || len >= _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for(size_t i = 0;i < len;++i)
{
ret._str[i] = _str[pos + i];
}
return ret;
}
6.流提取>>
流插入<<较为简单,这里就先不讲了,来讲讲需要注意的流提取>>。
int main()
{
string s;
cin >> s;
return 0;
}
我们想在s输入字符串该怎么实现呢?先来给大家看一个案例。
istream & operator>>(istream& in,string &s)
{
char ch;
in >> ch;
while(ch != ' '&& ch != '\n')//遇到空格或者换行符就跳出去
{
s+=ch;
in>>ch;
}
return in;
}
在上面的代码中,如果s是想输入hello world中间有个空格的话,编译器是会自动跳过去的,也就是说,你想输入hello world的话,中间的空格会被忽略,导致的结果为输入进去的字符串为helloworld,中间没有空格隔开,这就和我们的预期不符。
想要解决这个问题,就要把’ '(空格符号)实打实取到,那就得需要get函数
代码示例,如下:
istream & operator>>(istream& in,string &s)
{
char ch;
get(ch);
while(ch !='\n')
{
s+=ch;
get(ch);
}
return in;
}
getline
getline可以对一个string进行输入,直到遇到字符delim。
istream& getline (istream& is, string& str, char delim);
//我们可以自定义delim字符
istream& getline (istream& is, string& str);
//如果我们不定义delim字符,那就是默认遇到换行符'\n'就代表输入结束。
代码示例,如下:
int main()
{
string s;
getline(in,s,'!');
cout<<s;
return 0;
}
在上面的代码中,我们自定义了字符串遇到’!'就结束,假设我们输入的是,jaidhdia%¥#!
输出的结果就为jaidhdia%¥#。
7.<
字符串的比较大小都是比各自的第一位字符的ascii码值的大小,谁大谁就大,相等即比较下一个字符,以此类推。
我们这里只讲小于号(<)的重载,其他的符号与这个类似,我就不过多赘述。
bool operator<(const string & s)
{
size_t i1 = 0,i2 = 0;
while(i1 < _size && i2 < s._size)
{
if(_str[i1] < s._str[i2])
{
return true;
}
else if(_str[i1]>s._str[i2])
{
return false;
}
else
{
i1++;
i2++;
}
}
//跳出循环,就说明里面有一个字符串已经匹配到头了,也就是说,短的那个是长的子串
return i1 == _size;//如果i1 == _size,就说明,第一个字符串已经匹配到头了,那就说明第一个字符串比较小,刚好返回真。如果i1!=_size那就说明第一个字符串比较长,还没到头,说明第一个字符串比较大,刚好返回false。
}
★补充:拷贝构造的现代写法

四.总结
本文对STL中的string进行了深度讲解,并且尝试的通过自己来模拟实现库中的string。本篇对于我们学习STL有很大的帮助,可以当作一个模板套在其他的容器上进行使用。
810

被折叠的 条评论
为什么被折叠?



