文章目录
1. 前置语法知识——auto和范围for
1.1 auto关键字
- 在早期C/C++,auto修饰的变量,是具有自动存储器的局部变量,随着语言的演化,这个逐渐变得鸡肋,于是在C++11中,标准组委会便赋予auto一个全新的含义——auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器。
- auto声明的变量必须由编译器在编译时期推导而得。
auto使用事项:
- auto声明指针类型时,auto和auto*没有区别,但声明引用类型,则必须使用auto&。
int main()
{
int n = 100;
auto p1 = &n;//p1与p2没有区别
auto* p2 = &n;
auto& r1 = n;//引用类型必须加 &
cout << *p1 << " " << *p2 << " " << r1 << endl;//输出100 100 100
return 0;
}
- 用auto在同一行声明多个变量时,编译器只会对第一个变量类型进行推导,然后用推导出来的去定义这一行所有变量,故这一行的变量需是同一类型,否则编译报错。
- auto不能做函数参数,但可以做函数返回值,做返回值时也要谨慎使用。
- auto不能直接用来声明数组。
auto的用处之一:
1.2 范围for
- 对于一个有范围的集合来说,由程序员去编写代码说明范围往往多余,稍不注意还会出错,基础此,C++11引入了范围for。
- 使用格式:for循环后的括号由冒号 : 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,实现自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历。
- 范围for的底层很简单,容器遍历实际就是替换为迭代器。
int main()
{
int arr[] = { 1,2,3,4,5 };
//1.普通遍历方式:
cout << "普通遍历方式:";
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
cout << arr[i] << " ";
}
cout << endl;
//2.C++11的范围for:
cout << "C++11的范围for:";
for (auto n : arr)//用auto自动判断类型
{
cout << n << " ";
}
cout << endl;
return 0;
}
2. string类
2.1 为什么要学string类
- 在C中表示字符串常常要用字符数组的形式去表示存储,写法上不够方便,使用时也需要直接去管理底层空间,稍不留神就会越界访问
- 无论是在OJ,还是在常规的工作中,为了方便和可操作性,字符串都是以string的形式出现
- 详细学习string有助于后续学习STL的其他容器,这些容器之间的接口都大差不差
2.2 认识string类
- string类属于STL中的容器部分,但string类却是早于STL出现的,STL的其他容器设计也大多仿照过string,在STL出现后,string便被收入进了STL的容器部分。
- STL的其他容器设计相对更为成熟,是因为它们可以抄string的作业,但string却没有作业可抄,这就导致设计者在设计它的时候不可避免的设计有些冗余。
- 本文会涉及string的一些常用性的接口,一些重点的接口其名字会标粗。
2.3 string类的常用接口说明
注意:在使用string类时,必须包含 #include< string > 以及 using namespace std; 为了方便展示,以下接口的说明只给出函数名。
2.3.1 常见构造
- string() —— 构造空的string类对象,即空字符串
- string(const char*s) —— 用C-string(C格式的字符串) 的形式去构造string对象
- string(const string& s) —— 拷贝构造函数
- string(size_t n,char c)——用n个相同的连续字符去构造string对象
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1; //空构造s1
string s2("hello world!");//用C的字符串去构造s2对象
string s3(s2); //用s2对象去拷贝构造s3对象
cout << " s1对象为:" << s1 << endl;
cout << " s2对象为:" << s2 << endl;
cout << " s3对象为:" << s3 << endl;
return 0;
}
2.3.2 容量操作
以Visual Studio环境为例,需先明确string类对象的底层设计(这里简单介绍底层,下篇文章会模拟实现string类):
class string
{
public:
//成员函数...
private:
char buff[16]; //vs编译器下会先开一个buff数组用来解决短小字符串情景(注意实际可用15个字节,最后一个要放'\0')
char* _arr = nullptr; //底层是字符数组,当字符串长度大于15时才会在堆上申请空间
size_t _size = 0; //当前有效字符个数
size_t _capacity = 15; //当前字符串的容量大小,vs下有buff数组,所以刚开始是15
static const size_t npos;//表示整型的最大值
};
size_t const string::npos = -1;
- size——返回当前对象字符串的有效长度
- length——返回当前对象字符串的有效长度
- capacity——返回当前为字符串分配的存储空间大小,以字节表示
- empty——bool类型,判断当前字符串是否为空串,空返回true,非空返回false
//测试size/length/capacity/empty
void TestString1()
{
string s1;//空字符串
cout << s1.size() << endl; //输出0
cout << s1.length() << endl;//length与size一致,输出0
cout << s1.capacity() << endl;//输出15
cout << s1.empty() << endl; //输出1
cout << endl;
string s2("hello world");//非空字符串
cout << s2.size() << endl;//输出11
cout << s2.length() << endl;//输出11
cout << s2.capacity() << endl;//输出15
cout << s2.empty() << endl;//输出0
}
- clear——清空当前字符串的有效字符,并让有效长度为0
//测试clear
void TestString2()
{
string s1("我是一个苦逼的计算机学生,没有对象,没有甜甜的爱情,只有一行行的代码");
cout << "清空前:";
//输出68 79 0
cout << s1.size() << " " << s1.capacity() << " " << s1.empty() << endl;
s1.clear();
cout << "清空后:";
//输出0 79 1---说明clear并不会改变底层的容量大小
cout << s1.size() << " " << s1.capacity() << " " << s1.empty() << endl;
}
- reserve——为字符串预留空间
//测试reserve
void TestString3()
{
string s2("我是一个苦逼的计算机学生,没有对象,没有甜甜的爱情,只有一行行的代码");
string s3;
//在已经知道字符串大小的时候可以提前开好空间,避免频繁扩容影响效率
s3.reserve(100);
cout << s3.capacity() << endl;//输出111(底层还有个buff数组)
s3 = s2;
cout << s2.size() << " " << s2.capacity() << endl;//输出68 79
cout << s3.size() << " " << s3.capacity() << endl << endl;//输出68 111
//当要预留的空间小于当前容量时,字符串的有效个数,字符串当前的容量都不会改变
s3.reserve(0);
cout << s3.size() << " " << s3.capacity() << endl;//输出68 111
s3.reserve(85);
cout << s3.size() << " " << s3.capacity() << endl;//输出68 111
}
- resize——将原字符串的有效字符的个数(size)改为n,n>size则多出空间会用某个字符填充,n<size则仅改变size
void TestString4()
{
string s1("我是一个苦逼的计算机学生,没有对象,没有甜甜的爱情,只有一行行的代码");
cout << s1.size() << " " << s1.capacity() << endl;//输出68 79
//使有效字符个数减少,底层的容量不变
s1.resize(24);
cout << s1 << endl;
cout << s1.size() << " " << s1.capacity() << endl;//输出24 79
//当不超过当前容量时,增大有效字符个数,并用字符a填充多出的空间
s1.resize(75,'a');
cout << s1 << endl;
cout << s1.size() << " " << s1.capacity() << endl;//输出75 79
//当超过当前容量时,先扩容,再增大有效字符个数,并用字符b填充多出的空间
s1.resize(100, 'b');
cout << s1 << endl;
cout << s1.size() << " " << s1.capacity() << endl;//输出90 118
}
总结:
-
- size() 与length() 方法只是名字不同,其他完全一致,引入size()的原因是为了与其他容器的接口保持一致,一般都用size()
-
- clear() 只是将string中有效字符清空并将size置为0,不改变底层空间大小。
-
- resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到n个,不同的是 n>size时,resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
-
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,一般认为当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小,但有些编译器会进行改变容量大小的操作,如Linux下的g++编译器。
2.3.3 访问及遍历操作
- operator[]——重载了[]运算符,返回pos位置的字符
- begin和end——begin是获取当前容器首字符位置的迭代器,end是获取当前容器最后一个字符的下一个位置的迭代器,返回类型为iterator(迭代器类型)
- rbegin和rend——rbegin是获取 当前容器的最后一个有效字符位置(不是默认\0的位置) 的反向迭代器,rend是获取当前容器首字符的上一个位置的反向迭代器
int main()
{
string s1("abcdefghi");
//正向迭代器变量
string::iterator itb = s1.begin();
string::iterator ite = s1.end();
cout << *itb << endl; //输出a
cout << *(ite - 1) << endl;//防止越界,输出i
//反向迭代器变量
string::reverse_iterator ritb = s1.rbegin();
string::reverse_iterator rite = s1.rend();
cout << *ritb << endl; //输出i
cout << *(rite - 1) << endl;//防止越界,输出a
return 0;
}
- 范围for——string容器可用范围for去遍历
//string容器的遍历方式
void TestString5()
{
string s("abcdefghi");
//普通遍历方式:
cout << "普通遍历方式:";
for (int i = 0; i<s.size();i++)
{ cout << s[i] << " "; }
cout << endl;
//C++11的范围for:
cout << "C++11的范围for:";
for (auto _s : s)
{ cout << _s << " "; }
cout << endl;
//范围for的底层实质是迭代器
//正向迭代器遍历--正向打印
cout << "正向迭代器遍历:";
auto itb = s.begin();
while (itb != s.end())
{
cout << *(itb) << " ";
++itb;
}
cout << endl;
//反向迭代器遍历--反向打印
cout << "反向迭代器遍历:";
auto ritb = s.rbegin();
while (ritb != s.rend())
{
cout << *(ritb) << " ";
++ritb;
}
}
2.3.4 修改操作
- push_back——在字符串后尾插某个字符
- append——在字符串后追加一个字符串
- operator+=——在字符串后追加一个C_str形式的字符串或字符
//测试push_back/append/+=
void TestString6()
{
string s1("hms");
cout << s1 << endl;
//append
s1.append(" is very beautiful");
cout << s1 << endl;
//push_back
s1.push_back('!');
cout << s1 << endl;
string s2("lm");
cout << s2 << endl;
//+=
s2 += " is very handsome!";
cout << s2 << endl;
}
- c_str——返回C格式的字符串
- find——从字符串pos位置开始往后找某个字符或字符串,返回其在字符串中的位置
- rfind——从字符串pos位置开始往前找某个字符,返回该字符在字符串中的位置
- substr——在str中从pos位置开始,截取n个字符,然后将其返回
void TestSting7()
{
//c_str
string s1("shuai_ming");
cout << s1.c_str() << endl;
//find
size_t find = s1.find('_', 0);
cout << s1[find] << endl;
//substr
cout << s1.substr(find) << endl;//默认有个缺省值npos
cout << s1.substr(find, 5) << endl;
}
2.3.5非成员函数
- operator+——单纯+,不改变对象,尽量少用,因为传值返回,导致深拷贝效率低
- operator>>——输入运算符重载
- operator<<——输出运算符重载
- getline——获取一行字符串,直至遇到分隔符,分隔符默认为 \n,也可自己指定分隔符
- relational operators——诸如> 、<、 +等比较大小的运算符
- swap——交换两个字符串
void TestString8()
{
string s1;//空串
cout << "s1的getline()之前:";
cout << s1 << endl ;
cout << "请输入s1的getline()的内容:";
getline(cin, s1);
cout << "s1的getline()之后:";
cout << s1 << endl;
string s2("hello world!");//非空串
cout << "s2的getline()之前:";
cout << s2 << endl;
cout << "请输入s2的getline()的内容:";
getline(cin, s2);
cout << "s2的getline()之后:";
cout << s2 << endl;
}
3.结尾
- string是STL容器的一部分,后面各个容器的学习也大多都有string类似的影子,故学好string类是后续学习STL的基础
- 本篇文章涉及了string类的大部分知识,还有些小知识及string类的模拟实现将放在下篇文章进行讲解
- 由于文章篇幅有限,还有很多东西都没有展示出来,感兴趣的读者可以自行阅读C++库的文档(英文版),去补充更多的知识