Ⅲ. sting类对象的容量操作
函数名称 | 功能说明 |
size (重点) | 返回字符串有效字符长度 |
我们先讲解其中的一部分:
0x00 让我康康字符串有多长的 size() 和 length()
💬 先看代码:
int main(void) { string s1; cin >> s1; cout << "你输入了:" << s1 << endl; return 0; } int main(void) { string s1; cin >> s1; cout << "你输入了:" << s1 << endl; cout << s1.size() << endl; cout << s1.length() << endl; return 0; }
📌 注意事项:
① size() 和 length() 的计算不包含 \0。
解释:它不包含最后作为结尾标识符的 \0,告诉你的是有效的字符长度。
② size() 和 length() 的功能都是返回字符串有效长度,功能上没有区别。
解释: 这是一个 "历史包袱" 问题。
因为 string 比 STL 出现的还要早一些,所以有了STL容器之后就习惯地给出了 size() 。
而 length() 是代替传统的C字符串,所以针对C中的 strlen ,给出相应的函数 length() 。
C++中 string 成员函数 length() 等同于 size() ,功能没有任何区别。
不信可以看 C++标准库中的 string 中 size() 和 length() 的源代码:
size_type __CLR_OR_THIS_CALL length() const { // return length of sequence return (_Mysize); } size_type __CLR_OR_THIS_CALL size() const { // return length of sequence return (_Mysize); }
0x01 返回字符串最大长度的 max_size
返回字符串可以达到的最大长度。
💬 我们来试试:
int main(void) { string s1; cout << s1.max_size() << endl; return 0; }
long long int 最大值
🧀 小芝士:C++string ax_size():返回字符串最大长度 (4294967294) 2^32=4294967296,其中有两位系统做什么了?
-1被占用为不存在的位置string::npos,因此不能用于长度(2^32-1)。字符串最后一字节保留给\0,因此只能再少一字节(2^32-2)。
0x02 返回空间总大小的 capacity()
容量,代表能存多少个有效字符。
💬 代码演示:
int main(void) { string s1("abcdef"); cout << s1.capacity << endl; return 0; }
0x03 清空有效字符的 clear()
📚 clear:清空有效数据
int main(void) { string s1("abcdef"); cout << "清空前: " << s1 << endl; s1.clear(); cout << "清空后: " << s1 << endl; return 0; }
值得注意的是,clear 只是把数据清了,但是容量还在:
int main(void) { string s1("abcdef"); cout << "清空前: " << s1 << endl; cout << s1.capacity() << endl; s1.clear(); cout << "清空后: " << s1 << endl; cout << s1.capacity() << endl; return 0; }
0x04 开完空间就跑路的 reserve() 和开完空间还给你初始化的 resize()
先说 reserve,这个单词的拼写和 reverse 有点像,但是注意分别!
(reserve:保留 reverse:逆置)
📚 reserve 会改变容量
.reserve(size_t res_arg=0) 为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserve不会改变容量大小。 s.reserve(1000); // 请求申请至少能存储1000个数据的容量
我们再来说说 resize。
reserve 开空间,影响容量。而 resize 是开空间,并对这些空间给一个初始值,进行初始化。
(它们的区别类似于 malloc 和 calloc)
举个形象地例子 ——
" reserve 和 resize 都是卖房子的(开空间的),reserve 只是把房子卖给你(开空间),而 resize 是一条龙服务,房子卖给你(开空间)之后还帮你搞装修(初始化),你还可以指定装修风格(初始化内容),如果不指定会按默认的简约风格装修(不指定默认用 \0 初始化)"
💬 看代码:
void TestPushBack() { string s1; s1.reserve(100); string s2; s2.resize(100); }
💬 指定 resize 初始化字符:
string s2; s2.resize(100, 'x'); // 给值,指定初始化字符
💬 如果 resize 的大小比你当前的数据还要小:
string s5("hello world"); s4.resize(5);
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大 小,如果是将元素个数减少,底层空间总大小不变
Ⅳ. string类对象的访问及遍历操作
函数名称 | 功能说明 |
operator[] | 返回pos位置的字符,const string类对象调用 |
范围 for | C++11支持更简洁的范围 for 新遍历方式 |
0x00 访问字符串字符的 operator[] 和 at()
如果访问这个字符串的每一个字符:
int main(void) { string s1("hello world"); for (size_t i = 0; i < s1.size(); i++) { cout << s1[i] << " "; } cout << endl; return 0; }
这里可以和数组一样使用 for 循环遍历,然后通过 s1[i] 来获取每个字符。
我们熟知的数组,可以用 "方括号" 直接解引用,即数组的第 i 个位置解引用。
这里很像,只是在 string 这里它作为函数调用,这就是 operator[] ,并且等价于:
cout << s1[i] << " "; cout << s1.operator[](i) << " "; // 取字符串第i个位置的字符的引用
💬 我们不仅能读,还可以写。比如我们给每个字符+1:
int main(void) { string s1("hello world"); for (size_t i = 0; i < s1.size(); i++) { s1[i] += 1; // 对s1每个位置的字符都+1 } cout << s1 << endl; return 0; }
🤔 思考:operator[] 可以写的原因是什么?
❓ string 把这里设计成引用的目的是什么?operator[] 的底层是这样设计的:
char& operator[] (size_t pos) { return _str[pos]; }
这里的 & 不是为了减少拷贝而使用的,因为就算拷贝一个 char 代价也不大。
这里 operator[] 使用引用返回,是为了能够支持修改返回的变量!
顺便一提,除了 operator[] 还有一个 at() ,at() 也是早期支持的一个接口。
💬 它和 operator[] 用处是一样的,at() 是像函数一样去使用的:
int main(void) { string s1("hello world"); for (size_t i = 0; i < s1.size(); i++) { s1.at(i) += 1; } cout << s1 << endl; return 0; }
at() 和 operator[] 的区别 —— 它们检查越界的方式不一样。
operator[] 是使用断言处理的,断言是一种激进的处理手段。
char& operator[] (size_t pos) { assert(pos < _size); return _str[pos]; }
而 at() 是比较温柔的处理方式,如果 pos >= size 就 throw 一个异常对象出去。
但是一般情况还是 operator[] 用的比较多一些。
0x01 初识迭代器
📚 迭代器是 STL 六大组件之一,是用来访问和修改容器的。
💬 用一下试试:
int main(void) { string s1("hello"); // 迭代器 string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << " "; it++; } cout << endl; return 0; }
注意,这是本教程第一次提到迭代器,后续我会详细讲解。
如果你是第一次接触 "迭代器的概念",不妨可以先把迭代器想象成 "像指针一样的类型"。
(可以试着想象遍历的过程)
❓ 迭代器遍历的意义是什么呢?
对于 string,无论是正着遍历,倒着遍历,下标 + [] 都足够好用,为什么还要迭代器呢?
当然,对于 string,下标和 [] 确实足够好用,我们在学习C语言的时候就先入为主地使用了,
确实可以不用迭代器。但是如果是其他容器(数据结构)呢?
比如 list、map / set 不支持 下标 + [] 遍历,迭代器就排上用场了,这就是迭代器存在的意义。
迭代器是通用的遍历方式。
🔺 总结: 对于 string,你得会用迭代器,但是一般我们还是喜欢用 下标 + [] 遍历。
迭代器有很多,此外还有反向迭代器、const 迭代器……
这些都可以通过看文档去了解和学习。对于迭代器我们下一章还会详细讲解,
(我们今天主要说的还是 string,所以这里要讲究点到为止)
0x02 甜甜的语法糖 —— 范围for
还有一种遍历字符串的方式,范围 for。
这个我们在讲 auto 关键字的时候讲过了,它是一个用起来是很甜的语法糖。
💬 用起来非常爽,不用加加也不用解引用:
int main(void) { string s1("hello"); for (auto e : s1) { cout << e << " "; } cout << endl; return 0; }
如此完美,当然不仅仅是能读,还能写 ——
int main(void) { string s1("hello"); for (auto& e : s1) { // 引用修改 e += 1; } cout << s1 << endl; return 0; }
这里加引用是为了可以修改。不仅如此,遇到数据大的情况下也会有加 & 的时候(减少拷贝)。
其实,你不用 auto 也可以用返回 for。
for (char& e : s1) { e += 1; } cout << s1 << endl;
我们习惯用 auto ,是因为 C++11 改版之后的 auto 可以自动推导类型,
让这个语法糖更特么香了!复习猛戳 👉【C++要笑着学】范围for (位于Ⅲ处 0x00 ~ 0x02)
再说一嘴:这个范围 for 虽然看起来和听上去都很
又是自动迭代又是自动判断结束的,
但其实它底层也就是编译器在编译后把这段代码替换成了迭代器而已。
(下一章讲 string 模拟实现的时候会顺便带大家演示一下范围for的实现原理)