?个人主页:秋风起,再归来~
?系列专栏:C++从入门到起飞
?克心守己,律己则安
目录
1. 为什么学习string类?
1.1 C语言中的字符串
1.2 两个面试题(先不做讲解)
2. 标准库中的string类
2.1 string类(了解)
2.2 string类常用的构造函数
(1) 空字符串构造函数(默认构造函数)(重要)
(2)拷贝构造函数(重要)
(3) substring(子串,子链) 构造函数
(4) 常量字符串构造函数(重要)
(5) 拷贝字符串前n个构造函数
(6) 填充构造函数
2.3 string类可用的三种遍历方式
(1)下标访问遍历
(2)迭代器遍历(含4种迭代器用法详解)
(3)范围for遍历
2.4 string类的容量操作
1、max_size
2、resize (重点)
3、reserve(重要)
4、clear(重要)
5、empty(重要)
6、shrink_to_fit
(需要用到直接查文档即可(链接都在下面),这里标注了一些重要的常用的接口)
2.5 string类的修改操作
2.6 string类的查找操作
2.7 string类的非成员函数
3. auto关键字
4. vs和g++下string结构的说明
4.1 vs下string的结构
4.2 g++下string的结构
5. 完结散花
1. 为什么学习string类?
1.1 C语言中的字符串
>C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 两个面试题(先不做讲解)
>字符串转整形数字
>字符串相加
>在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、 快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2. 标准库中的string类
2.1 string类(了解)
>string类的文档介绍
>在使用string类时,必须包含#include<string>头文件以及using namespace std;
2.2 string类常用的构造函数
(1) 空字符串构造函数(默认构造函数)(重要)
string();
>构造一个空字符串,长度为零个字符。
//default (1)//string();string s1;cout << "s1;"<< s1<<"****" << endl;
>string类当中重载了流插入和流提取函数,所以我们可以直接用
(2)拷贝构造函数(重要)
string(const string & str);
>构造 str 的副本。
//copy(2)//string(const string & str);string s2 = "hello world!";//==string s2("hello world!") 下面会讲这个构造函数cout << "s2:" << s2 << endl;string s3 = s2;//==string s3(s2)cout << "s3:" << s3 << endl;
(3) substring(子串,子链) 构造函数
string(const string & str, size_t pos, size_t len = npos);
>复制 str 中从字符位置 pos 开始并跨越 len 字符的部分(如果任一 字符串str 太短或 len 为string::npos,则复制到 str 末尾的部分)。
//substring(3)//string(const string & str, size_t pos, size_t len = npos);string s1 = "hello world!";cout << "s1:" << s1 << endl;string s2(s1, 6, 5);cout << "s2:" << s2 << endl;
>如果拷贝的字符串太短(len太长),则从pos位置拷贝到字符串结尾就停止拷贝!
string s1 = "hello world!";cout << "s1:" << s1 << endl;string s2(s1, 6, 100);//向后拷贝100个字符,但字符串s1不够大,只拷贝到结尾就停止cout << "s2:" << s2 << endl;
>如果我们没有给len赋值,则len使用缺省值npos
>npos是string类当中的一个静态成员变量。
string s1 = "hello world!";cout << "s1:" << s1 << endl;string s2(s1, 6);//拷贝到结尾才停止cout << "s2:" << s2 << endl;
(4) 常量字符串构造函数(重要)
string(const char* s);
>复制 s 指向的以 null 结尾的字符序列(C 字符串)。
//from c - string(4)//string(const char* s);const char* s = "hello world!";string s1(s);//==string s1("hello world!")==string s1=scout << "s1:" << s1 << endl;
(5) 拷贝字符串前n个构造函数
string(const char* s, size_t n);
>从 s 指向的字符数组中复制前 n 个字符。
//from buffer(5)//string(const char* s, size_t n) const char* s = "hello world!";string s1(s,5);cout << "s1:" << s1 << endl;
>如果指定的大小超过原字符串的大小,则将原字符串全部拷贝,并在编译器上打印提示,而程序并不会终止!
(6) 填充构造函数
string(size_t n, char c);
>用字符 c 的 n 个连续副本填充字符串。
//fill(6)//string(size_t n, char c); string s1(5, 'x');cout << s1<<endl;
2.3 string类可用的三种遍历方式
(1)下标访问遍历
>在 string类里面重载了一个公共的成员函数operator[],它的返回值是当前pos位置的引用,当没有const修饰时,我们可以对当前字符进行读和写的操作。但当有const修饰时,当前下标的字符是只读的!
>对象调用size和length都返回该对象的字符串长度,但要注意的是,该长度不包含字符串中的’\0'! size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
string s1 = "hello world";//1、下标访问遍历for (int i = 0; i < s1.size() ;i++){cout << s1[i]<<" ";}cout << endl;for (int i = 0; i < s1.size(); i++){s1[i] += 2;//进行写的操作cout << s1[i] << " ";}cout << endl;
(2)迭代器遍历(含4种迭代器用法详解)
>这里只是简单的讲一下迭代器的一小部分用法,在后续的文章会详细讲解迭代器的!
//迭代器遍历string s2 = "hello world!";string::iterator it = s2.begin();
> 我们先来看一下上面的一段代码,我们先突破类域指定迭代器iterator 定义了一个变量it,我们目前可以认为s2调用begin返回了s2的起始位置,也就是第一个字符的位置,然后it接受到了这个位置。我们会发现it是一个很像指针的东西,但是!但是!但是!it并不一定是指针!后面,我们还会对it进行解引用和++的操作,但是这些运算符可能都是重载过的!然而,我们对它表层就可以理解为指针,因为它的用法和指针非常相似(这里可能就是原始指针)!
>注意:end返回的是字符串的最后一个位置‘\0’!
while (it!=s2.end()){cout << *it << " ";it++;}cout << endl;
>当然,我们也可以用迭代器对s2进行写的操作,毕竟它的用法和指针几乎一样!
>当然,如果我们只想对s2只读不写的话,我们也可以用cbegin和cend(c就是const的意思)
>字符串本身是const,不能修改!
//迭代器遍历const string s2 = "hello world!";string::const_iterator it = s2.cbegin();while (it != s2.cend()){//*it += 2;报错:表达式是不可修改的左值cout << *it << " ";it++;}
>字符串本身不是const,也不能修改!
//迭代器遍历string s2 = "hello world!";string::const_iterator it = s2.cbegin();while (it!=s2.cend()){//*it += 2;报错:表达式是不可修改的左值cout << *it << " ";it++;}cout << endl;
> 这里再介绍一种反向迭代器
//反向迭代器遍历string s2 = "hello world!";string::reverse_iterator rit = s2.rbegin();while (rit != s2.rend()){cout << *rit << " ";rit++;}cout << endl;
>注意:rbegin指向的是字符串倒数第一个有效字符(不是'\0'!) ,rend指向的是字符串的第一个字符的前一个位置!rit任然是++,而不是--(这里可以可以肯定的是++运算符一定被重载了)!
>还有最后这一种迭代器,其实就是常量反向迭代器,理解了前面讲的内容,这部分一看就懂了,这里就不赘述了!
(3)范围for遍历
>对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
>范围for可以作用到数组和容器对象上进行遍历
>范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
//范围for遍历string s2 = "hello world!";for (auto s : s2){cout << s << " ";}cout << endl;
>我们先看到上面的一段代码,其中关键字auto(下文有专门针对auto的讲解,这里知道一些用法就没问题了)先定义了一个变量s,它会自动提取到字符串s2中的每一个字符并识别它的类型从而进行匹配,而范围for遍历会自动遍历字符串,并输出每一个字符。这样看起来范围for非常牛逼,然而它的底层走的还是迭代器的原理!
>不过,当我们不用访问字符串,只是单纯的遍历时,范围for用起来还是非常爽的!
>注意:这里还有一点小坑,我们来看下面一段代码~
//范围for遍历string s2 = "hello world!";for (auto s : s2){s += 2;cout << s << " ";}cout << endl;for (auto s : s2){cout << s << " ";}cout << endl;
>通过输出结果,我们可以发现,我们在第一次遍历时,对s2进行了修改的操作,但是在第二次遍历时,s2却没有改变! 所以,我们可以得出结论s只是s2中字符的拷贝,虽然它底层走的是迭代器的原理,但还是略有不同的。不过,我们只要在auto后面加上&符号也可以修改!
2.4 string类的容量操作
>size和length在前文已经讲过了,这里便不再赘述!
1、max_size
2、resize (重点)
>将有效字符的个数该成n个,多出的空间用字符c填充
>resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小(capacity)不变。
string s1="xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;cout<<"修改后--------------------------"<<endl;s1.resize(5);//减小的是有效字符的个数,容量的大小不变cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
string s1="xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;cout<<"修改后--------------------------"<<endl;s1.resize(30,'y');//增加有效字符的个数,并用‘y’填充,且容量的大小增大cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
3、reserve(重要)
>reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
1、预留空间大于有效字符个数,并且大于容量时
//1、预留空间大于有效字符个数,并且大于容量时string s1 = "xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;s1.reserve(30);cout << "预留空间:30!修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
>有效字符不变,容量增大(扩容方式在每个平台下可能会有不同,但结果一定是符合C++标准规定的!)
2、预留空间大于有效字符个数,但小于容量时
//2、预留空间大于有效字符个数,但小于容量时string s1 = "xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;s1.reserve(14);cout << "预留空间:14!修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
>有效字符不变,容量不变!
3、预留空间小于有效字符个数,且小于容量时
3、预留空间小于有效字符个数,且小于容量时string s1 = "xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;s1.reserve(0);cout << "预留空间:0!修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
>有效字符不变,容量不变!
4、clear(重要)
>clear()只是将string中有效字符清空,不改变底层空间大小。
string s1 = "xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;s1.clear();cout << "修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl<<endl;cout << "***************" << endl;
5、empty(重要)
>判断字符串是否为空!
string s1 = "xxxxxxxxxxxxx";cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;cout << s1.empty() << endl;if (!s1.empty()){cout << "s1不为空"<<endl;}s1.clear();cout << "修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;cout << s1.empty() << endl;if (s1.empty()){cout << "s1为空"<<endl;}
6、shrink_to_fit
>根据字符串的大小合理的调整容量的大小!
string s1 = "xxxxxxxxxxxxx";s1.reserve(100);//先预留100个空间大小cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;s1.shrink_to_fit();//再根据字符串的大小调整容量大小cout << "修改后--------------------------" << endl;cout << "总容量大小(capacity):" << s1.capacity() << endl;cout << "有效字符个数 (size):" << s1.size() << endl;cout << "s1:" << s1 << endl;
(需要用到直接查文档即可(链接都在下面),这里标注了一些重要的常用的接口)
2.5 string类的修改操作
1、operator+=(重要!)https://legacy.cplusplus.com/reference/string/string/operator+=/
2、append(在字符串后追加一个字符串)https://legacy.cplusplus.com/reference/string/string/append/
3、push_back(在字符串后尾插字符c)https://legacy.cplusplus.com/reference/string/string/push_back/
4、assign( 为字符串分配一个新值,替换其当前内容)https://legacy.cplusplus.com/reference/string/string/assign/
5、insert(在任意位置插入)https://legacy.cplusplus.com/reference/string/string/insert/
6、erase(删除字符串的部分)https://legacy.cplusplus.com/reference/string/string/erase/
7、replace(替换字符)https://legacy.cplusplus.com/reference/string/string/replace/
8、swap(交换俩个string)https://legacy.cplusplus.com/reference/string/string/swap/注意:string类的交换与库里面的swap有所不同,string类的交换是直接将两个指针进行了交换!
9,、pop_back (尾删一个字符)https://legacy.cplusplus.com/reference/string/string/pop_back/
2.6 string类的查找操作
1、c_str(获取C指针,重要)https://legacy.cplusplus.com/reference/string/string/c_str/
Get C string equivalent (public member function )
//C与C++ 的结合string s1;cin >> s1;FILE* file = fopen(s1.c_str(), "r");//这里的参数必须是C字符串char ch = fgetc(file);while (ch!=EOF){cout << ch;ch = fgetc(file);}fclose(file);file = nullptr;
2、data(与C_str作用一样,但一般不用这个接口)https://legacy.cplusplus.com/reference/string/string/data/
Get string data (public member function )
3、get_allocator(空间配置器,暂且不用了解)https://legacy.cplusplus.com/reference/string/string/get_allocator/
Get allocator (public member function )
4、copy(string拷贝)https://legacy.cplusplus.com/reference/string/string/copy/
Copy sequence of characters from string (public member function )
5、find(从pos位置向前查找子串或字符)https://legacy.cplusplus.com/reference/string/string/find/
Find content in string (public member function )
6、rfind(从pos位置向后查找)https://legacy.cplusplus.com/reference/string/string/rfind/
Find last occurrence of content in string (public member function )
7、find_first_of(从pos位置向前查找所指定的任意字符)https://legacy.cplusplus.com/reference/string/string/find_first_of/
Find character in string (public member function )
8、find_last_of(向后)https://legacy.cplusplus.com/reference/string/string/find_last_of/
Find character in string from the end (public member function )
9、find_first_not_of(向前查找除指定字符外的任意字符)https://legacy.cplusplus.com/reference/string/string/find_first_not_of/
Find absence of character in string (public member function )
10、find_last_not_of(向后)https://legacy.cplusplus.com/reference/string/string/find_last_not_of/
Find non-matching character in string from the end (public member function )
11、substr(返回子串)https://legacy.cplusplus.com/reference/string/string/substr/
Generate substring (public member function )
// string::find_last_of#include <iostream> // std::cout#include <string> // std::string#include <cstddef> // std::size_tvoid SplitFilename(const std::string& str){std::cout << "Splitting: " << str << '\n';//find_last_of从后向前找/或\其中任意字符并返回其下标std::size_t found = str.find_last_of("/\\");//C++中的区间都是左闭右开(取子串)std::cout << " path: " << str.substr(0, found) << '\n';std::cout << " file: " << str.substr(found + 1) << '\n';}int main(){std::string str1("/usr/bin/man");//Linux下的文件目录std::string str2("c:\\windows\\winhelp.exe");//Windows下的文件目录//分离路径与文件名SplitFilename(str1);SplitFilename(str2);return 0;}
12、compare(比较string,基本不用,因为有更好用的比较运算符重载)https://legacy.cplusplus.com/reference/string/string/compare/
Compare strings (public member function )
2.7 string类的非成员函数
operator+(重要)https://legacy.cplusplus.com/reference/string/string/operator+/
Concatenate strings (function )
relational operators(重要)https://legacy.cplusplus.com/reference/string/string/operators/
Relational operators for string (function )
swaphttps://legacy.cplusplus.com/reference/string/string/swap-free/
Exchanges the values of two strings (function )
operator>>(重要)https://legacy.cplusplus.com/reference/string/string/operator%3E%3E/
Extract string from stream (function )
operator<<(重要)https://legacy.cplusplus.com/reference/string/string/operator%3C%3C/
Insert string into stream (function )
getline(重要)https://legacy.cplusplus.com/reference/string/string/getline/
Get line from stream into string (function )
标注:和C中的gets的作用差不多,如果用cin或scanf读取字符串,在遇到空格或换行符时就会停止读取字符串。而getline就是针对string字符串解决这个问题的,并且它的功能还更加强大,可以自定义读取结束的条件!
string s1;getline(cin, s1,'*');//指定读取结束条件!cout << "读取结束!" << endl;
3. auto关键字
在这里补充2个C++11的小语法,方便我们后面的学习。
>在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个 不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型 指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期 推导而得。
>用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项 auto e; int a = 0;auto x = a;//intauto y = &a;//int*auto* z = &a;//int*auto& w = a;//intcout << "a:"<< typeid(a).name() << endl;cout << "x:"<< typeid(x).name() << endl;cout << "y:"<< typeid(y).name() << endl;cout << "z:"<< typeid(z).name() << endl;cout << "w:"<< typeid(w).name() << endl;
>当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main(){auto a = 1, b = 2;//必须为同一类型 // 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型auto a = 1, b = 2,c=3.0;//不同类型就报错return 0;}
>auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
不可以作参数
可以做返回值,但是建议谨慎使用
auto func1(){auto a = 1;return a;}auto func2(){return func1();}auto func3(){return func2;}auto func4(){return func3;}int main(){auto a = func4();return 0;}
看上面的一段代码,如果我们不往前看到第一个函数,我们知道最终a的类型是什么吗!所以我们把auto做返回值时一定要谨慎使用!
>auto不能直接用来声明数组
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型auto array[] = { 4, 5, 6 };
>auto的用武之地
std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange","橙子" }, {"pear","梨"} };// auto的用武之地//std::map<std::string, std::string>::iterator it = dict.begin();auto it = dict.begin();while (it != dict.end()){cout << it->first << ":" << it->second << endl;++it;}
4. vs和g++下string结构的说明
4.1 vs下string的结构
>注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
>vs下string的结构 string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 string中字符串的存储空间:
》当字符串长度小于16时,使用内部固定的字符数组来存放
》当字符串长度大于等于16时,从堆上开辟空间
union _Bxty{ // storage for small buffer or pointer to larger one value_type _Buf[_BUF_SIZE]; pointer _Ptr; char _Alias[_BUF_SIZE]; // to permit aliasing} _Bx;
class String{private://vs下string类里面的成员变量大概是这样char _buff[16];char* str;size_t _size;size_t capacity;};int main(){cout<<sizeof(String)<<endl;return 0;}
>这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建 好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
>其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
>最后:还有一个指针做一些其他事情。 故总共占16+4+4+4=28个字节。
>vs下string扩容
void TestPushBack(){string s;size_t sz = s.capacity();cout << "原始大小:" << sz << endl;cout << "making s grow:" << endl;for (int i = 0; i < 100; i++){s.push_back('c');if (sz != s.capacity()){sz = s.capacity();cout << "capacity change:" << sz << "\n";}}}
>从结果来看,我们可以知道在vs下,当字符串的大小超过16时,vs是两倍扩容的,之后的每一次都是1.5倍扩容的!
4.2 g++下string的结构
>G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个 指针,该指针将来指向一块堆空间,内部包含了如下字段:
1、空间总大小
2、字符串有效长度
3、引用计数
struct _Rep_base{ size_type _M_length; size_type _M_capacity; _Atomic_word _M_refcount;};
4、指向堆空间的指针,用来存储字符串。
>g++下string的扩容
>我们可以看到,在g++下string是两倍扩容的!
5. 完结散花
好了,这期的分享到这里就结束了~
如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~
如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~
我们下期不见不散~~