C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现
Posted 跳动的bit
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现相关的知识,希望对你有一定的参考价值。
文章目录
【写在前面】
到这里我们就要学会能自己能看文档了,因为就这个 string 类来说就有一百多个接口函数,那肯定记不住呀,我们平时用的大概就二十个,一般我们都是学习它比较常用的,其它的大概熟悉它们有哪些功能,真要用到时再去查文档。可以看到其实 string 就是一个管理字符数组的顺序表,因为字符数组的使用广泛,C++ 就专门给了一个 string 类,由于编码原因,它写的是一个模板。针对 string,一般情况它有三个成员 —— char* _str、size_t _size、size_t _capacity,我们在下面模拟实现 string 时就会看到,其次在学习深浅拷贝时我们只用 char* _str。
C++ 的文档库严格来说有两个:注意前者并不是 C++ 的官网文档,后者才是。这里我们对于官方文档就作为参考,因为它比较乱,平时就用 cplusplus 也够了。
此外对于类和对象还有一个深浅拷贝的知识我们将会再这里补充。
一、为什么学习string类
💦 C语言中的字符串
C 语言中,字符串是以 ‘\\0’ 结尾的一些字符的集合,为了操作方便,C 标准库中提供了一些 str 系列的库函数,
但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可
能还会越界访问。
💦 两个面试题(暂不讲解)
在 OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。
二、标准库中的string类
💦 string类(了解)
- 字符串是表示字符序列的类。
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string 类是使用 char(即作为它的字符类型,使用它的默认 char_traits 和分配器类型 (关于模板的更多信息,请参阅 basic_string)。
- string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数(根于更多的模板信息请参考 basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如 UTF-8) 的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
- string 是表示字符串的字符串类。
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
- string 在底层实际是:basic_string 模板类的别名,typedef basic_string<char, char_traits, allocator> string;。
- 不能操作多字节或者变长字符的序列。
- 在使用 string 类时,必须包含 string 头文件以及 using namespace std;。
#include<string>
#include<iostream>
using namespace std;
int main()
cout << sizeof(char) << endl;
cout << sizeof(wchar_t) << endl;
char arr1[] = "hello bit";
char arr2[] = "比特";
return 0;
📝说明
对于字符,C++ 里提出了两个类型,现阶段接触的是第一个。
可以看到结果一个是 1 和 2,这个东西与编码有关系。
编码 ❓
计算机中存储只有二进制 0 和 1,如何去表示文字呢 ???
这时就建立出对应的编码表:
-
ASCII > 支持英文
1Byte = 8Bit,0 - 255 ASCII 编码表就是对 256 个值建立一个对应的表示值
-
GBK > 我国制定的,常用于 windows 下
早期 windows 为了打入中国市场,就使用这个编码 -
utf-8 > 通用,兼容 ASCII,常用于 Linux 下
世界各国都开始使用计算机了,而早期计算机中只能表示英文,不能表示其它国家的文字,所以需要建立自己的编码表,但各自搞各自的就非常乱,所以就出现了 utf-8,早期是 UniCode
所以据不完全统计汉字大约有十万个左右,所以我们这里用 2 个字节,大概表示 6 万种状态
💦 string类的常用接口说明(只讲最常用的)
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) —— 重点 | 拷贝构造函数 |
#include<string>
#include<iostream>
using namespace std;
//大概原理
template<class T>
class basic_string
T* _arr;
int _size;
int _capacity;
;
//typedef basic_string<char> string;
int main()
string s1;
string s2("hello");
string s3("比特");
string s4(10, 'a');
string s5(s2);
//对于string它重载了流提取和流插入等,这里就可以直接用
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
//赋值 ———— 深拷贝
s1 = s5;
cout << s1 << endl;
📝说明
basic_string ❗
string s1; || string s2(“hello”); ❗
通过调试我们发现 s1 虽然是无参的,但并不是啥都没有,且会在第一个位置放一个 \\0。
2、string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size —— 重点 | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty —— 重点 | 检测字符串释放为空串,是返回 true,否则返回 false |
clear —— 重点 | 清空有效字符 |
reserve —— 重点 | 为字符串预留空间 |
resize —— 重点 | 将有效字符的个数改成 n 个,多出的空间用字符 c 填充 |
#include<string>
#include<iostream>
using namespace std;
void test_string1()
//1、size | length
string s1("hello world");
cout << s1.size() << endl;
cout << s1.length() << endl;
cout << "----------cut----------" << endl;
//2、max_size
string s2;
cout << s1.max_size() << endl;
cout << s2.max_size() << endl;
cout << "----------cut----------" << endl;
//3、capacity
cout << s1.capacity() << endl;
cout << "----------cut----------" << endl;
//4、resize
string s3("hello world");
cout << s3.size() << endl;
cout << s3 << endl;
//s3.resize(20);//n大于当前的字符串的长度且没有指定c,所以hello world\\0\\0\\0\\0...
//s3.resize(5);//n小于当前的字符串的长度, 它会删除掉从n开始的这些字符
s3.resize(20, 'x');//n大于当前的字符串的长度且指定c,所以hello worldxxxx...
cout << s3.size() << endl;
cout << s3 << endl;
cout << "----------cut----------" << endl;
//5、reserve
string s4("hello world");
s4.reserve(20);
cout << s4 << endl;
cout << s4.size() << endl;
cout << s4.capacity() << endl;
s4.reserve(10);
cout << s4 << endl;
cout << s4.size() << endl;
cout << s4.capacity() << endl;
cout << "----------cut----------" << endl;
//6、clear | empty
string s5("hello world");
cout << s5 << endl;
cout << s5.empty() << endl;;
s5.clear();
cout << s5 << endl;
cout << s5.empty() << endl;
cout << "----------cut----------" << endl;
//7、shrink_to_fit 暂且不演示
void test_string2()
string s;
size_t sz = s.capacity();
cout << "making s grow:\\n" << sz << endl;
for(int i = 0; i < 500; ++i)
s.push_back('c');
if(sz != s.capacity())
sz = s.capacity();
cout << "capacity changed:" << sz << '\\n';
cout << "----------cut----------" << endl;
int main()
test_string1();
test_string2();
return 0;
📝说明
size || length ❗
两者的功能相同,一般我们比较常用的是 size。
对于 string 是在 STL 这个规范前被设计出来的,因此在 Containers 下并没有 string:
早期说要算字符串字符的长度,所以早期提供的接口就叫 length,至于后面要加 size 的原因是后面增加了 map、set 这样的树,所以用 length 去表示它的数据个数就不合适了。
max_size ❗
它也是早期设计比较早的,属于一个没用的接口 —— 从操作系统中获取最大的长度。本意是想告诉你这个字符串最大你能定义多长, 这个接口在设计的时候其实不好实现,它没有办法标准的去定义这个接口,因为它有很多不确定的因素。所以它这个地方是直接给你 232 ,也就是 4G。所以没什么价值,以后再遇到就直接跳过了。
capacity ❗
对于 string 对象而言,如果 capacity 是 15,意味着它有 16 个字节的空间,因为有一个位置是 \\0 的。对于 capacity 它会随着字符串的大小而增容,这里默认是 15。
resize ❗
resize 的前者版本可以让字符串的长度变成 n;后者可以让字符串的 n 个长度变成 c。
如果 n 是小于当前的字符串的长度, 它就会缩减到 n 个字符,删除掉从 n 开始的这些字符。
如果 n 是大于当前的字符串的长度,通过在末尾插入尽可能多的内容来扩展当前内容,倘若指定了 c,则新元素被初始化为 c 的副本,否则它们就是值初始化字符 (空字符 \\0)。
对于 s3.resize(20); s3[19] 是有效位置,因为对于 operator[] 里它会 assert(pos < _size)。
reserve ❗
请求 capacity。
注意这里不是你要多少 capacity 它就给多少 capacity,它在增容时还要对照不同编译器下自己的增容规则,最终容量不一定等于字符串长度,它可能是相等的,也可能是更大的。
如果 n 大于当前字符串的 capacity,那么它会去扩容。
如果 n 小于当前字符串的 capacity,这里跟不同的平台有关系。文档里是这样说的:其他情况下 (小于或等于),有可能它会缩容(开一块新空间,将数据拷贝,释放原有空间),也有可能不对当前空间进行影响,只是变换 capacity 的值。已证,VS 和 Linux 下不会缩容,STL 的标准也是这样规定的,这是实现 STL 的人决定的。
对于 s4.reserve(20); s4[19] 是无效位置,因为对于 operator[] 里它会 assert(pos < _size)。
string 增容是怎么增的 ❓
test_string2():
可以看到 Windows VS 下初始容量是 15 ,除了第一次,其余的大概是以 1.5 倍增容。
g++ ???
可以看到在不同的编译器下增容规则也不同,Linux g++ 下初始容量是 0,其余是以 2 倍增容的。
clear | empty ❗
清理字符串 | 判空字符串
resize 和 reserve 有什么价值 ❓
对于 resize,既要开空间,还要对这些空间初始化,就可以用 resize —— s.resize(20, ‘x’);
对于 reserve,明确知道需要多大空间的情况,可以提前把空间开好,以减少增容所带来的代价 —— s.reserve(500);
3、string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[] —— 重点 | 返回 pos 位置的字符,const string 类对象调用 |
begin + end | begin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 |
范围 for | C++11 支持更简洁的范围 for 的新遍历方式 |
#include<assert.h>
#include<string>
#include<iostream>
using namespace std;
//大概原理
template<class T>
class basic_string
public:
char& operator[](size_t pos)
assert(pos < _size);//检查越界:s2[20] -> err
return _arr[pos];
private:
T* _arr;
int _size;
int _capacity;
;
//typedef basic_string<char> string;
int main()
//逐一遍历字符串
//1、operator[]
string s1("hello world");
for(size_t i = 0; i < s1.size(); ++i)
s1[i] += 1;//写
for(size_t i = 0; i < s1.size(); ++i)
cout << s1[i] << " ";//读
cout << endl;
//2、范围for
string s2("hello world");
for(auto& e : s2)
e += 1;//写
for(auto e : s2)
cout << e << " ";//读
cout << endl;
//3、迭代器
string s3("hello world");
string::iterator it = s3.begin();
while(it != s3.end())
*it += 1;//写
++it;
it = s3.begin();
while(it != s3.end())
cout << *it << " ";//读
++it;
cout << endl;
return 0;
📝说明
operator [] | at ❗
operator[] 和 at 的价值是一样的,但是不一样的地方是对于越界:operator[] 直接使用断言报错,比较粗暴;而 at 则会抛异常。
范围 for ❗
这是 C++11 中提出的新语法,比较抽象,对于它的原理,后面会再模拟实现它。
注意 auto e : s3 只能读,不能写;auto& e : s3 可读可写。
迭代器 ❗
它是 STL 中六大组件中的核心组件,这里先见个面。
begin() 返回第一个有效数据位置的迭代器;
end() 返回最后一个有效数据的下一个位置的迭代器;
目前可以认为它类似于指针,—— 有可能是指针,有可能不是指针,因为不仅仅 string 有迭代器,其它容器里也有迭代器。
while(it != s3.end()) 也可以 while(it < s3.end()),但不建议。这里是数组,换成小于它依然没问题,但是如果是链表,小于就不行了。所以这里统一就用不等于。
迭代器的好处是可以用统一类似的方式去访问修改容器 (也就意味着会了 string 的迭代器就等于会了其它容器的迭代器):
为什么这里要提供三种遍历方式 ❓
其实只提供了两种,因为范围 for 本质是迭代器,也就是说一个容器支持迭代器才会支持范围 for,后面会演示。
operator [] 可以像数组一样去访问,这种方式对于 vector 连续的数据结构是支持的,但是不支持 list。
迭代器这种方式是任何容器都支持的方式,所以迭代器才是容器通用的访问方式。
再简单了解迭代器 ❗
#include<string>
#include<iostream>
using namespace std;
void Print1(const string& s)//不需要写
string::iterator it = s.begin();//err,普通迭代器
//string::const_iterator it = s.begin();//ok,const迭代器
//string::const_iterator it = s.cbegin();//ok,C++11提供的cbegin
while(it != s.end())
cout << *it << " ";
++it;
cout << endl;
void Print2(const string& s)
string::reverse_iterator rit = s.rbegin();//err,普通反向迭代器
//string::const_reverse_iterator rit = s.rbegin();//ok,const反向迭代器
//auto rit = s.rbegin();//ok,auto的体现
while(rit != s.rend())
cout << *rit << " ";
++rit;//注意这里不是--
cout << endl;
void test_string()
string s1("hello");
Print1(s1);
string s2("hello");
Print2(s2);
int main()
test_string();
return 0;
📝说明
这里利用迭代器打印 s1:
查文档:begin 有两个版本
所以报错的原因是:s 是 const 对象,返回的是 const 迭代器,而这里使用普通迭代器来接收。解决方法就是用 const 迭代器接收。
在文档中看到 C++11 中又提供了一个 cbegin ???
为什么 C++11 把 const 迭代器单独拎出来,大概原因应该是 begin 里有普通 / const 迭代器不太明确,容易误导。不知道大家是怎么想的,我是觉得有点画蛇添足了。
rbegin:
rbegin 就是反向迭代器,顾名思义就是反向遍历。
auto 的价值 ❗
到了迭代器这里我们就可以用 auto 来替代比较长的类型,auto 的价值得以体现。
4、string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符 c |
append | 在字符串后追加一个字符串 |
operator+= —— 重点 | 在字符串后追加字符串 str |
c_str —— 重点 | 返回 c 格式字符串 |
find + npos —— 重点 | 从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置 |
rfind | 从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置 |
substr | 在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回 |
#include<string>
#include<iostream>
using namespace std;
void test_string1()
//1、push_back | append | operator +=
string s1("hello");
string s2("world");
s1.push_back('+');
s1.append("world");
cout << s1 << endl;
s1 += '+';
s1 += "bit+";
s1 += s2;
cout << s1 << endl;
cout << "----------cut----------" << endl;
//2、insert | erase
string s3("hello");
s3.insert(0, "bit ");
cout << s3 << endl;
//s3.erase(5);//从5删到nops
s3.erase(5, 2);//从5往后删2个
cout << s3 << endl;
cout << "----------cut----------" << endl;
void test_string2()
//要求取出文件的后缀
string file1("test.txt");
string file2("test.c");
string file3("test.txt.zip");//对test.txt文件打了个包
size_t pos1 = file1.find('.');
if(pos1 != string::npos)//当find的返回值不等于npos就找到了
//1、substr
string sub1 = file1.substr(pos1);//同substr(pos1, file1.size()-pos1);,但没必要,因为这里是取后缀,而substr有缺省参数npos
cout << sub1 << endl;
size_t pos2 = file2.find('.');
if(pos2 != string::npos)
string sub2 = file2.substr(pos2);
cout << sub2 << endl;
//由此可见对于file3,以上代码不适用
size_t pos3 = file3.find('.');
if(pos3 != string::npos)
string sub3 = file3.substr(pos3);
cout << sub3 << endl;
//更适用的来说取文件的后缀应当使用另一个接口rfind
size_t pos4 = file3.rfind('.');
if(pos4 != string::npos)
string sub4 = file3.substr以上是关于C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现的主要内容,如果未能解决你的问题,请参考以下文章
C++初阶:STL —— vectorvector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现
C++初阶:STL —— vectorvector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现
C++初阶:STL —— listlist的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比