写在前面
怎么都没想到啊,已经写到第三章了,那剩下16章还会远吗,本章讲的是字符串、向量和数组。
3.1 命名空间
这一章终于来了,还在为写输入输出函数时需要手动打std::
而烦恼吗,使用了命名空间之后,你就不再需要敲啦!
此时我们会使用一种比较安全的声明:using
,它的使用方法是 using namespace::name
看到这里,就会有聪明的同学提问了,说阿伦你讲的不对,我们老师说教的是using namespace std;
我怎么没看见::
这玩意儿?
你们感觉不对劲就对咯,这玩意儿是更“安全“的版本,那你们老师教的”不那么安全“的版本需要什么时候能用到呢?
我查了一下书
在702页,坏消息是我们现在在74页,慢慢来同学们,我们尽量按照书的顺序走,毕竟大佬写的教材一定有他这么写的道理(bushi)
按照教材教的这种声明命名空间成员的方式呢,我们每个using
声明只能引入命名空间的一个成员(幸好现在需要引入的不多)
比如我们常见的std::cin
和std::cout
引入时就是 using std::cin;using std::cout;
有了这两句声明之后,你在接下来就可以快乐的使用不带std::
的输入输出标识符啦。
有一点需要注意:尽量不要在头文件中使用using声明成员,这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using
声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
使用例子
#include <iostream>
using std::cout;
using std::cin;
int main()
{
int a = 0;
cin >> a;
cout << a << std::endl;
return 0;
}
有聪明的小伙伴就要问了,为什么你还要在endl前面加std::
呢?当然是因为我没在前面声明这个成员啦。
补充一点一直忘记说的是::
是个什么玩意儿,这是一个作用域操作符,作用是使编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字
3.2 标准库类型string
标准库类型string
表示可变长的字符序列,使用string
类型必须首先包含string头文件
。作为标准库的一部分,string
定义在命名空间std
中。
因此我们在使用的时候别忘了加入什么呢?诶对了,using std::string;
这样就能避免重复输入命名空间啦。
3.2.1 string的定义和初始化
没什么好说的,看表看表
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copyinitialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)。
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
这个很好理解嘛
string arr1(10 ,'a');
string arr2 = arr1;
在这个例子中,arr1
使用了直接初始化,arr2
使用了拷贝初始化
3.2.2 string对象上的操作
话不多说看表
有一点和char
不一样的,string
加号标识两个字符串链接,而char
的加号是两个字符的ascii
码相加。
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
int main()
{
char a = 'a';
char b = 'b';
cout<< a + b << endl;
string c = "c";
string d("d");
cout << c + d << endl;
return 0;
}
输出结果是195和cd
读写string对象
在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从
第一个真正的字符开始读起,直到遇见下一处空白为止。
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
int main()
{
string str;
cin >> str;
cout << str << endl;
return 0;
}
那有没有想过,我如果想读取一整行带空格的字符比如
Hello everyone, my name is alun.
怎么处理呢?
使用getline读取一整行
getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
int main()
{
string line;
while(getline(cin,line))
{
cout << line << endl;
}
return 0;
}
值得注意的是
string的empty和size操作
这俩后期都是老朋友了,很多函数判空条件和取大小都是使用empty
和size
empty
返回一个bool
值来显示string
是否为空,而size
返回string
的长度(string
中字符个数)
值得注意的是,size
返回值并不是int而是size_type
,一个无符号数。这就令人费解了,而且在书中是这么描述的:
作者表示也不是很清楚size_type
的细节,但强调了它是一个无符号数,这就意味着,我们在做判断或者取值的时候尽量不要用int或者其他可能为负值的类型来接受这个变量。
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
int main()
{
string line;
while(getline(cin,line))
{
if(!line.empty())
{
cout << line << endl;
cout << line.size() << endl;
}
}
return 0;
}
string对象的比较
这是很有意思的点,我们一般的认为string
对象在做比较的时候是直接比较两个字符串长度?大错特错!
string
比较分两种情况,当比较的两个字符串中一个是另一个的子串时,子串小于目标字符串;如果不是,则比较的是第一个相异字符串的ascii
码值
举个例子
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
int main()
{
string str1 = "Hello";
string str2 = "Hello,World!";
string str3 = "HiYa";
cout << (str1 < str2 && str2 < str3 && str1 < str3) <<endl;
return 0;
}
我们会发现,很反直觉的事字符数最短的str3
反而是最大的!
3.2.3 处理string对象中的字符
首先在cctype
头文件中定义了一组标准库函数
这个头文件是从ctype.h
来的
而如果想对其中的数据进行处理有以下两种处理方式
范围for处理
在前面的章节,我曾“提前“告知了在C11中增强for的存在,其实增强for在处理string数组或者其他数组的时候尤其好用,特别是在遍历数组元素方面
假设我想对字符串中所有小写字符改成大写
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
using std::toupper;
int main()
{
string str = "Hello";
for(auto &c:str)
{
c = toupper(c);
}
cout << str << endl;
return 0;
}
我们可以利用标准库中的toupper
进行处理
那如果我只想处理一部分字符呢
下标访问
我们可以通过下标运算符[]
来实现string
的下表访问,运算符内接受的值是上文中提到的size_type
中的值。
这个时候我又不明白了,不应该先讲讲数组吗,这个字符串本质上就是一个字符数组,因此下标访问的初始值是0(第一个数据),最后一个数据是size_type - 1
只要字符串不是常量,就能为下标运算符返回的字符赋新值。
而且甚至不需要顺序访问,可以随机下标访问
比如我们做一个10进制转16进制的转化器
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::string;
int main()
{
string str = "0123456789ABCDEF";
string::size_type a = 0;
while(cin >> a)
{
cout << str[a];
}
return 0;
}
每次访问的时候就访问对应字符的下标。
3.3 vector
终于进入到STL容器了,有人会说怎么连数组都还没讲就开始讲动态数组了,这可能就是作者的小心思吧(猜不透),不过好消息是,这和前面章节讲过的很多技术是一样的,在后面都会反复被提及(特别是在第九章)所以不用担心。
首先我们来看看定义:标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
想要使用它必须包含头文件<vector>
,而且它是一个函数模板。
函数模板又是啥?按照解释
C++语言既有类模板(classtemplate),也有函数模板,其中vector是一个类模板。只有对C++有了相当深入的理解才能写出模板。
好消息是,我们还有很久才会接触这玩意儿
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。使用方法如vector<int> arr;
但是要注意几个点
- vector能够容纳绝大多数类型的对象作为其元素,但不能包含引用。这是因为引用不是对象。
- vector的元素可以是大多数非引用的内置类型和自定义类型,甚至可以是vector类型。
- 在早期版本的C++标准中,如果vector的元素还是vector(或其他模板类型),则其定义的形式与现在的C++11新标准略有不同。在旧的标准中,必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,例如应该写成vector<vector>而不是vector<vector>。
因此
3.3.1 定义和初始化vector 对象
还是得看表
列表初始化vector 对象
我们可以用一个大花括号来初始化列表对象
比如vector<int> arr = {1,2,3,4};
这个时候在我的ide里面出现了一些问题
这个时候就说明什么呀,我们的代码检查并不是c11以上版本的
因为当我们使用这个代码
#include <iostream>
#include <vector>
using std::cout;
using std::cin;
using std::endl;
using std::vector;
int main()
{
vector<int> arr = {1,2,3,4};
for(auto &num : arr)
{
cout << num << " ";
}
cout << endl;
return 0;
}
时能正确输出
因此我们要对我们的IDE做一些修改
创建指定数量的元素
还可以使用vector<int> arr (10,-1)
来将vector
初始化为有10个-1
的数组
值初始化
如果是int
这类的则初始化为0
,如果是string
这类的初始化为默认值
初始化的是列表的值还是数量
基于上述,我们知道了,如果是花括号{}
,则初始化的为列表元素,如果是圆括号()
则初始化的为数量,如果有两个值则第二个为初始化值。
3.3.2 向vector对象中添加元素
要么你在初始化赋值比如vector<int> arr (10,-1)
或者vector<int> arr{1,2,3,4,5,6,7,8,9,0}
,但如果有100、1000个数,显然这种方法就不靠谱了,可以通过push_back
实现,用法是列表名.push_back(值)
,实现的功能是将数据压入列表末端。
#include <iostream>
#include <vector>
using std::cout;
using std::cin;
using std::endl;
using std::vector;
int main()
{
vector<int> arr;
int a = 0;
while(cin >> a)
{
arr.push_back(a);
}
return 0;
}
有个需要注意的点:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环(具体原因第五章会讲)
3.3.3 其他vector 操作
看表
值得注意的是,其中大多数的操作和string是很相似的,比如size和empty是完全一致的,比较也是差不多一致的,如果表1是表2的子集,则表1小于表2,如果表1不是表2的子集,那他们第一个不同位置哪个值大就哪边大,在此不做赘述。
而且vector同样可以通过下标访问元素,但不可以用下标添加元素。
3.4 迭代器介绍
3.4.1 使用迭代器
和指针不一样的是,获取选代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。
一般的,迭代器都有两个成员:begin
和end
,begin
指向首元素,end
指向尾元素的下一个元素(也就是一个标志但无法提取实际值),空迭代器begin == end
迭代器运算符
如表所示
将迭代器从一个元素移动到另外一个元素
迭代器使用递增++
运算符来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增和整数的递增类似,整数的递增是在整数值上“加1”,迭代器的递增则是将迭代器“向前移动一个位置”。
要注意的点是,因为end只是一个标志,因此无法对其作出移动操作。
又到了科普时间,不过这次科普的内容后面会讲到,我们可以先看一下
迭代器类型
又是一个我们不知道的类型
就像不知道string和vector的size_type成员到底是什么类型一样,一般米说我们也不知道(其实是无须知道)迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型
const_iterator
const_iterator和常量指针差不多,能读取但不能修改它所指的元素值。相反,iterator的对象可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator。
begin和end运算符
begin和end运算符begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator
结合解引用和成员访问操作
结合解引用和成员访问操作解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否力空,令it是该vector对象的选代器,只需检查it所指字符串是否为空就可以了
某些对vect or 对象的操作会使迭代器失效
面试必考题,但是后面讲,我们只要知道已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效就可以了。
迭代器运算
迭代器的递增运算令选代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。类似的,也能用==和!=对任意标准库类型的两个有效迭代器(参见3.4节,第95页)进行比较。
string和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作迭代器运算(iteratorarithmetic)
见表
3.5 内置类型数组
3.5.1 定义和初始化内置数组
内置类型数组的大小在编译时必须已知,因此不能使用变量来定义数组的大小。数组的初始化可以通过直接指定元素的值或者使用花括号进行列表初始化。如果数组在声明时没有初始化,则会进行默认初始化(例如,所有元素被初始化为0)。
初始化数组示例
int arr[10]; // 声明一个包含10个整数的数组,所有元素默认初始化为0
int arr2[5] = {1, 2, 3, 4, 5}; // 声明并初始化一个包含5个整数的数组
int arr3[3] = {2}; // 声明并初始化一个包含3个整数的数组,其余元素默认初始化为0
3.5.2 访问数组元素
数组元素可以通过下标运算符[]
来访问。数组的下标从0开始,因此第一个元素的下标是0,最后一个元素的下标是n-1
,其中n
是数组的大小。
访问数组元素示例
int value = arr[2]; // 访问数组arr的第三个元素
arr[2] = 10; // 将数组arr的第三个元素设置为10
3.5.3 指针和数组
在C++中,数组名可以被看作指向数组首元素的指针。因此,可以使用指针运算来遍历数组。但是,需要注意的是,数组名隐式转换为指针时,并不包含数组的大小信息。
使用指针遍历数组示例
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // p是指向arr首元素的指针
for (int i = 0; i < 5; ++i) {
std::cout << *(p + i) << std::endl; // 通过指针访问数组元素
}
3.5.4 C风格字符串
C风格字符串是以空字符'\0'
结尾的字符数组。在C++中,虽然支持C风格字符串,但推荐使用标准库类型string
,因为它更安全、更方便。
C风格字符串的处理
char str[] = "Hello, World!"; // C风格字符串
std::cout << str << std::endl; // 输出C风格字符串
3.5.5 与旧代码的接口
C++提供了与C语言兼容的特性,允许使用C风格字符串和数组。这使得C++能够与旧的C语言代码进行接口。
使用C风格字符串与数组
char* cstr = "Hello, World!"; // C风格字符串
int arr[] = {1, 2, 3, 4, 5}; // 内置数组
3.6 多维数组
多维数组可以看作是数组的数组。在C++中,多维数组实际上是数组的数组,因此需要两个维度来定义。
多维数组的初始化
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; // 3x4的多维数组
总结
内置类型数组在C++中是基础的数据结构,虽然其功能相对简单,但在处理固定大小的数据集时非常有效。C++提供了更安全、更易用的string
类型来处理字符串,但在与旧代码接口时,C风格字符串仍然有其用武之地。多维数组的使用场景与一维数组类似,但提供了更多的数据组织方式。在使用数组时,需要注意数组的大小和下标访问的正确性,以避免越界等错误。
这次就不留作业了,大家自习(bushi)可以试着使用string实现16进制数的换算,并且实现16进制数的显示和使用vector实现多个M+和M-功能