字符串容量头文件:<string>
string本质上就是字符顺序表;
class string
{
private:
char* str;
size_t _size;
size_t capacity;
};
1. string类对象的常见构造
(constructor)
函数名称
|
功能说明
|
---|---|
string()
(重点)
|
构造空的
string
类对象,即空字符串
|
string(const char* s)
(重点)
|
用
C-string
来构造
string
类对象
|
string(size_t n, char c)
|
string
类对象中包含
n
个字符
c
|
string(const string&s)
(重点)
|
拷贝构造函数
|
无参:空的string
string s1;
带参:用一个常量字符串初始化string
string s1("hello world");
拷贝构造是深拷贝
string s3(s2);
2. string类对象的容量操作
C++为string提供了许多接口
函数名称
|
功能说明
|
---|---|
size
(重点)
|
返回字符串有效字符长度
|
length
|
返回字符串有效字符长度
|
capacity
|
返回空间总大小
|
empty
(重点)
|
检测字符串释放为空串,是返回
true
,否则返回
false
|
clear
(重点)
|
清空有效字符
|
reserve
(重点)
|
为字符串预留空间(提前开辟)
|
resize
(重点)
|
将有效字符的个数该成
n
个,多出的空间用字符
c
填充
|
size:
返回字符串的长度(以字节为单位);
这是符合字符串内容的实际字节数,不一定等于其存储容量;(省去了"\0")
int main()
{
string s("hello world");
cout << s.size() << endl;//11,不算"\0"
return 0;
}
length:
和size基本一样,但是size能包含的内容更大;
max_size:
最大的size大小(不同容器,不同平台不同);
capacity:
(容量:开的空间)
返回已分配存储的大小
返回当前为字符串分配的存储空间的大小,以字节为单位表示;
此容量不一定等于字符串长度。它可以相等或更大,当向字符串添加新字符时,额外的空间允许对象优化其操作;(显示的是有效字符个数,不算"\0")
接下来,我们看看在VS下是如何扩容的:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "first_capacity:" << sz << endl;
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
在VS中设计:当数据个数小于16时,会先存在内部的一个_buff里边 (VS防止小块内存在堆上)
微软工程师实现:
class string
{
private:
char _buff[16];//n<16,这也是为什么第一次按2倍扩的原因
char* str;//n>=16时,_buff就废弃了,全部开在了str指向的堆上
size_t _size;
size_t capacity;
};
但是在不同平台有着不同的实现方式:
g++下string的结构(标准的2倍扩容,没有_buff)
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小
字符串有效长度
引用计数
指向堆空间的指针,用来存储字符串
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
频繁的扩容是代价的,拷贝数据-释放空间,会降低效率,那我们如何避免扩容,减少扩容呢?
解决:reserve,resize
reserve:
请求将字符串容量调整为计划的大小更改,则该函数会导致容器将其容量增加到 n 个字符(或更大);
在所有其他情况下,它被视为一个非约束性请求,以缩小字符串容量:容器实现可以自由地进行优化,并使大于 n;(节省空间,但一般不会缩容(根据不同平台:VS不缩容(其实_buff就可以看出来了),g++缩容),因为缩容是要付出代价的,因为待会需要空间的话还要继续扩)
参考:典型的缩容:shrink_to_fit :capacity减到size
此函数对字符串容量没有影响,并且不能更改其内容;
reserve开的大小不包含\0,实际上最少要开n+1;
作用:提前开空间,避免扩容,提高效率
void TestPushBack()
{
string s;
s.reserve(100);
size_t sz = s.capacity();
}
resize:
(扩容插入字符)
void resize (size_t n);void resize (size_t n, char c);
将字符串的大小调整为 n 个字符的长度;
如果 n 小于当前字符串长度,则当前值将缩短为其前 n 个字符,并删除第 n个字符之外的字符;
如果 n 大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展当前内容,以达到 n 的大小。如果指定了 c,则新元素将初始化为 c 的副本,否则,它们是值初始化的字符(null 字符);
size_t _sz = s.size();
s.resize(_sz + 2, '+');
clear:
void clear();
清除字符串
擦除字符串的内容,该字符串将变为空字符串(长度为 0 个字符)。
clear一般是只清理内容,不清除掉容量的(怕白干了),但个别少数会连容量(空间)一起清除的
s.clear();
empty:
(判空)
bool empty() const;
测试字符串是否为空
返回字符串是否为空(即其长度是否为 0);
此函数不会以任何方式修改字符串的值;
cout << s.empty() << endl;
注意:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size();
2. clear()只是将string中有效字符清空,不改变底层空间大小;
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, charc)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变;
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小;
3. string类对象的访问及遍历操作
函数名称
|
功能说明
|
---|---|
operator[ ]
(重点)
|
返回
pos
位置的字符,
const string
类对象调用
|
begin
获取一个字符的迭代器
+
end
获取最后一个字符下一个位
置的迭代器
| |
begin
获取一个字符的迭代器
+
end
获取最后一个字符下一个位
置的迭代器
| |
范围
for
|
C++11
支持更简洁的范围
for
的新遍历方式
|
string::iterator(正向迭代器)(string::const_iterator)
begin:
返回一个迭代器,该迭代器指向字符串的第一个字符;
如果字符串对象是 const 限定的,则该函数返回const_iterator。否则,它将返回一个迭代器;
成员类型 iterator 和 const_iterator 是随机访问迭代器类型(分别指向字符和 const 字符);
string::iterator it = s2.begin();
end:
返回一个迭代器,该迭代器指向字符串的末尾字符("\0")
string::iterator it = s2.end();
string::reverse_iterator(反向迭代器) (string::const_reverse_iterator)
rbegin:
返回反向迭代器以反向开始
返回一个反向迭代器,指向字符串的最后一个字符(即其反向开头)(有效字符);
反向迭代器向后迭代:增加迭代器会使它们朝向字符串的开头;(++倒着走)
rbegin 指向 member end 将指向的字符之前的字符;
string::iterator it = s2.rbegin();
rend;
将反向迭代器返回到反向端
返回一个反向迭代器,指向字符串的第一个字符(被视为其反面端)之前的理论元素;(也就是第一个字符的前一个位置);
string::rbegin 和 string::rend 之间的范围包字符串的所有字符(顺序相反)(左(rbegin)闭右(rend)开);
operator[ ]
获取字符串的字符
返回字符串中位置 pos 处的字符的引用
1.pos是size_t类型;
2.pos在字符串中是从0开始,即:第一个字符位置用0表示;
3.如果字符串对象是 const 限定的,则该函数返回 const char&,否则,它将返回 char&;
因此,我们可以去访问字符串中的字符:(适用于数组)
s2[0] = 'x';
//下标+[]
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i] << " ";
}
string是支持迭代器的方式进行遍历,迭代器是STL的六大组件之一,迭代器是用来遍历访问容器的;(所有的容器都可以用迭代器访问)
//迭代器
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
it++;
}
//范围for:所有容器都支持
//从s2中取字符给ch
//本质是迭代器
for (auto ch : s2)
{
ch += 2;
cout << ch << " ";
}
反向迭代器:(反向访问)
4.string提供的库---Function
stoi:
int stoi (const string& str, size_t* idx = 0, int base = 10); int stoi (const wstring& str, size_t* idx = 0, int base = 10);
将字符串转换为整数
解析 str,将其内容解释为指定基数的整数,该整数作为 int 值返回;
如果 idx 不是 null 指针,则该函数还会将 idx 的值设置为 str 中第一个字符在数字之后的位置;
str:
表示整数的 String 对象
idx:
指向 size_t 类型的对象的指针,该对象的值由函数设置为 str 中下一个字符在数值之后的位置;
此参数也可以是 null 指针,在这种情况下,不会使用它;
base:
确定有效字符及其解释的数字基数(基数);
如果此值为 0,则使用的基数由序列中的格式确定;
请注意,默认情况下,此参数为 10,而不是 0
类似的:
long long x=stoll(num1);
to_string:
string to_string (int val); string to_string (long val); string to_string (long long val); string to_string (unsigned val); string to_string (unsigned long val); string to_string (unsigned long long val); string to_string (float val); string to_string (double val); string to_string (long double val);
将数值转换为字符串
返回一个表示为 val 的字符串
类似:
5. string类对象的修改操作
函数名称
|
功能说明
|
---|---|
在字符串后尾插字符
c
| |
在字符串后追加一个字符串
| |
在字符串后追加字符串
str
| |
返回
C
格式字符串(兼容C)
| |
从字符串
pos
位置开始往后找字符
c
,返回该字符在字符串中的
位置
| |
从字符串
pos
位置开始往前找字符
c
,返回该字符在字符串中的
位置
| |
在
str
中从
pos
位置开始,截取
n
个字符,然后将其返回(比copy好点,还要开buffer)
|
push_back:
将字符追加到字符串
将字符 c 追加到字符串的末尾,使其长度增加 1
append:
追加到字符串
通过在字符串的当前值末尾追加其他字符来扩展字符串
operator+=:
就是push_back和append的结合,很香的,可以丢掉前俩个(底层是按扩容的逻辑走的)
以后尾插用+=就对了
void test_string5()
{
string s("hello world");
s.push_back(' ');
cout << s << endl;
s.append("!!!");
cout << s << endl;
s += "1314";
cout << s << endl;
s += '!';
cout << s << endl;
}
补充:
insert
说到尾插,还有头插,实际string没有头插,但是可以用insert接口实现头插:
s.insert(0, "hello everyone");
但头部,中间插入应该谨慎使用,有效率的流失
还有:
//插入一个
s.insert(0, "h");
s.insert(0, 1, 'h');//区别(小细节)
s.insert(0, 'h');//报错
由于历史的原因,整个string设计比较冗余,但是我们要尊重历史
语言都是向前兼容的,但python比较大胆~~~👍
erase
erase就比较好(指定位置删除字符)
擦除字符串的一部分,减小其长度:(头删就不用提供其他的接口)
void test_string6()
{
string s("hello aaworld");
s.erase(6, 2);//想要全删,默认给缺省值就行
cout << s << endl;
}
其迭代器(接口)也支持头,尾删除(pop_back也有尾删功能):
//头删
s.erase(s.begin());
//还可以
s.erase(0, 1);
//尾删
s.erase(--s.end());
//还可以
s.erase(s.size() - 1, 1);
//都是间接实现
replace:
如果想把字符串的某一个位置替换成另外一个字符/字符串,可以用replace(本质:插入删除)(效率也不高)
string s("hello world");
s.replace(5, 1, "%%");//换一个就行,换两个就把1换成2
find and npos:
查找字符:(返回类型:size_t)(找到返回下标,没找到返回npos(npos是静态成员变量,要制定类域))
(将所有空格转换成'!'):
void test_string7()
{
string s("hello world hello everyone nice to meet you ");
size_t pos = s.find(' ');
while (pos != string::npos)
{
s.replace(pos, 1, "!");
pos = s.find(' ', pos + 1);//防止对空格的从头开始查找
}
cout << s << endl;
}
但是面对大量数据是效率极低的,不可行的
void test_string7()
{
string s("hello world ");
size_t pos = s.find(' ');
while (pos != string::npos)
{
s.replace(pos, 1, "!");
cout << s << endl;
pos = s.find(' ', pos + 1);//防止对空格的从头开始查找
}
cout << s << endl;
}
那该如何解决呢,我们可以拿空间换时间,又简单又高效:
void test_string7()
{
string s("hello world ");
string tmp;//牺牲空间
tmp.reserve(s.size());//减少/避免 扩容
for (auto ch : s)
{
if (ch == ' ')
{
tmp += "!";
}
else
{
tmp += ch;
}
}
swap(s, tmp);//swap更高效
cout << s << endl;
}
5. string类非成员函数
函数名称
|
功能说明
|
---|---|
尽量少用,因为传值返回,导致深拷贝效率低c
| |
operator>>
(重点)
|
输入运算符重载
|
operator<<
(重点)
|
输出运算符重载
|
getline
(重点)
|
获取一行字符串
|
relational operators
(重点)
|
大小比较
|
operator+
string+字符串:
string s1("hello ");
string s2 = s1 + "world";
字符串+string: (这样就不能重载为成员函数了,左操作数被string牢牢占用了)(只能重载为全局的,跟流插入一样)
string s1("hello ");
string s2 = "world" + s1;
getline:
在做一些题目的时候,我们时时需要输入空格,但是cin会将空格/换行默认为输入分割的标志,因此,getline很好的为我们解决了这个问题
getline:遇到空格不终止,还是从缓冲区拿数据,默认是换行才是分割的标志
string str;
getline(cin,str);
也可以遇到自定的符号作为结束的标志:(现在不默认了)
string str;
getline(cin,str,'*');