📝目录
🪧 前言
本章将介绍两种最重要的标准库类型:string 和vector 。string 表示可变长的 字符序列,vector 存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类 型,和其他内置类型一样,数组的实现与硬件密切相关。因此相较于标准库类型string 和vector,数组在灵活性上稍显不足
一、命名空间 using 的声明
目前为止,我们用到的库函数基本上都属于命名空间std,而程序也显式地将这一点 标 示 了 出 来 。 例 如 , std::cin 表 示 从 标 准 输 入 中 读 取 内 容 。 此 处 使 用
作 用 域 操 作 符 ( :: )
的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那 个 名 字。 因 此 , std::cin 的 意 思 就 是 要 使 用 命 名 空 间 std 中 的 名 字 cin。
上面 的 方法 显 得 比 较 烦 琐, 然 而 幸运 的 是, 通 过 更 简 单 的 途 径 也 能 使 用 到 命 名 空间 中 的成员。本节将学习其中一种最安全的方法,也就是使用using 声明(using declaration )。
有 了using声 明 就 无 须 专 门 的 前 缀 (形 如 命 名 空 间 :: ) 也 能 使 用 所 需 的 名 字 了。 using声明具有如下的形式:
using namespace::name;
一旦声明 了上述语句,就可以直接访问命名空间中的名字,例如:
#include<iostream>
using std::cin;
int main()
{
int i = 0;
cin >> i;
//正确的,向i中输入数据,因为在上面进行了声明using std::cin;
cout<<i;
//错误的,因为没有在上面进行声明,也没有显示使用std,
std::cout << i;
//显示的从std中使用了cout
return 0;
}
🌳每个名宇都需要独立的using 声明
按照规定,每个using 声明引入命名空间中的一个成员。例如,可以把要用到的标 准库中的名宇都以using声明的形式表示出来,重写上面的程序如下:
#include<iostream>
using std::cin; //输入
using std::cout;//输出
using std::endl;//结束
int main()
{
int i = 0;
cin >> i >>j;
cout<<i << j << endl;//输出i和j
return 0;
}
在上述程序中,一开始就有対cin、cout 和endl 的using声明,这意味者我 不用再 添加 std:: 形式的前级就能直接使用它们。C++语言的形式比较自由,因此既可以一行只 放一条u sing 声明语句,也可以一行放上多条。不过要注意,用到的每个名字都必须有 自 己 的 声 明 语 句 , 而 且 每 句 话 都 得 以 分 号结 束
🌳一次声明全部
还有一种对于新手比较方便的声明,那就是一次性声明,所有的std里面的函数都可以使用,写法:using namespace std,上面的代码就可以这样写:
#include<iostream>
using namespace std;//一次性都声明了
int main()
{
int i = 0;
cin >> i >>j;
cout<<i << j << endl;//输出i和j
return 0;
}
不过需要注意的是,一般不推荐这样写,这样写虽然方便,但全部声明了却没有使用到,可能会占用内存,运行速度受影响,不过新手还是比较推荐这样写的。
🌳头文件不应包含using 声明
位 于 头 文 件 的 代 码一 般 来说 不 应 该 使 用using声 明 。 这 是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using 声明, 那 么 每 个 使 用 了 该 头 文 件 的 文 件 就 都 会 有 这 个 声 明。 对 于 某 些 程 序 来 说 , 由 于 不 经 意 间 包 含 了一 些 名 字, 反 而 可 能 产 生始料 未 及 的 名 字沖 突。
二、标准库类型string
标准库类型string表示可変的字符序列,使用string类型必首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。
//使用时需要加上这两个
#include<string>
using std::string;
string初始化的方式
string s1 | 默认初始化,s1 是一个空串 |
---|---|
string s2(s1) | s2 是s1的副本 |
string s2 = s1 | 等价于s2(s1),s2 是s1的副本 |
string s3(“hello”) | 直接初始化,s3就为hello |
string s3 = “hello” | 拷贝初始化 |
string s4(n, ‘c’) | 把s4初始化为由连线n个字符c组成的串 |
1️⃣ 定义和初始化 string 对象
如果使用等号(= ) 初始化一 个变量, 实际上执行的是
拷贝初始化
(copy initial ization ),编译器把等号右侧的 初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化
string s5 = "hello world" //拷贝初始化
string s6("hello world"); //直接初始化
string s7(10,'c'); //直接初始化,s7 的内容是cccccccccc
string s5 = "hello world"
我们可以把这种初始化看作是给一个变量赋值,只是这个变量是字符串类型的。
string s6("hello world");
和上面的没啥区别,都可以做到初始化,看个人喜欢,用哪一个都可以。
还可以进行以下操作:
string temp (10, 'c'); //temp的内容是cccccccccc
string s8 = temp; //将temp拷贝给s8
2️⃣ string 对象上的操作
string是一个类,既然是一个类,那么它就有很多关于类的操作,接下来我们就说一下string关于类的操作:
string的操作:
注:is是标准输入框,也就是终端运行程序的时候需要输入内容,就叫is,os是标准输出框,也就是输出到运行时出现的界面或者终端
os<<s | 将s写到输出流os 中,返回os |
---|---|
is >> s | 从is中读取字符串赋给s,字符串以空白分隔,返回is |
getline(is, s) | 从 is中读取一行赋给s,返回is |
s.empty() | s字符串为空返回true,否则返回false |
s.size() | 返回s中字符的个数 |
s[n] | 返回s中第n个字符的引用,位置n从0计起 |
s1 + s2 | 返回s 1 和s2 连接后的结果,两个字符串相加 |
s1 = s2 | 将s2拷贝给s1 |
s1 == s2 | 判断s1和s2是否相等 |
s1 != s2 | 判断s1和s2是否不相等 |
<, <= , >, .= | 利用字符在字典中的顺序进行比较,且对字母的大小写敏感 |
🌸读写string对象
我们曾经介绍过,使用标准库中的iostream米读写int、double 等内置类型的值。 同 样 , 也 可 以 使 用 IO操 作 符 读 写string对 象:
#include<iostream>
#include<string>
using namespace std;
//声明全部,其中就包含uisng std::string
int main()
{
string str; //空字符串
cin >> str;
//对str进行输入,遇到空白停止,比如输入:“hello world”,只会将hello输入进去,遇到空白停止
cout << s << endl; //输出str
return 0;
}
和内置类型的输入输出操作一样,string 对象的此类操作也是返回运算符左侧的运算 对 象 作 为 其 结 果 。 因 此 多 个 输 入 或 者 多 个 输 出 可 以 连 写 在 一起 :
string s1, s2;
cin >> s1 >> s2; //把第一个输入读到s1中,第二个翰入读到s2中
cout << s1 << s2 << endl; //输出两个string对象
假设给上面这段程序输入与之前一样的内容“ Hello World! "
,输出将是 “ HelloWorld! ”
。
🌸使用getline 读取一整行
有 时 我 们 希 望 能 在 最 终 得 到 的 字 符 串 中 保 留 输 入时 的 空 白 符 , 这 时 应 该 用
getline
函 数 代 替 原 来 的 >> 运 算 符 。
getline 函 数 的 参 数 是 一个输 入流 和 一个 string 对 象, 函 数从给定的输入流中读 入内容,直到過到换行符为止( 注意换行符也被读进来了),然后 把所读的内容存入到那个string对象中去(注意不存换行符)。
getline 只要一遇到换 行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的 一 开 始 就 是 换 行 符 , 那 么 所 得 的 结 果 是 个 空string
int main()
{
string str;
while(getline(cin, str))//每次读取一整行存入str里面,直到到达文件末尾
cout << str << endl;
return 0;
}
触 发 getline 函数 返 回 的 那 个 换 行 符 实际 上被 丢 弃 掉 了, 得 到 的 string 对 象中并不包含该换行符。
🌸string 的 empty 和 size 操作
empty
函数根据string对象是否为空返回一个对应的布尔值(true 和 false);
int main()
{
string str;
if(str.empty())
cout << "str是空的" << endl; //因为str是空字符串,所以输出这个
else
cout << "str不为空" << endl;
return 0;
}
size
函数返回string对象的长度。
int main()
{
string str = "hello world";
cout << "str的长度:"<< str.size() << endl;
//输出11,有11个字符
return 0;
}
size() 函数的返回值其实不是int 类型,而是string 标准库里面自定义的类型:size_type
🌸比较string 对象
string 类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较string对象中的字符,并且对大小写敏感,也就是说,在比较时同一 个字母的大写形式和小写形式是不同的。
相 等 性 运 算 符 ( == 和 != ) 分 別 检 验 两 个 string对 象 相 等或 不 相 等,string对 象 相 等意味着它们的长度相同而且所包含的字符也全都相同。 关系运算符<、<=、>、>= 分别检验一个string 对象是否小于、小于等于、大于、大于等于另外一个string对象。上述这些运算符都依照 (大小写敏感的)字典顺序:
- 1 如果两个string对象的长度不同,而且较短string对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短string对象小于较长string 对象。
- 2.如果两个string 对象在某些对应的位置上不一致,则string对象比较的结果 其实是string 对象中第一对相异字符比较的结果,也就是同一位置的字符谁大,那一个字符串就大。
示例:
string str1 = "Hello";
string str2 = "Hello World";
string str3 = "Hiya";
//这三个字符串进行比较
//根据第一条:str1和str2比较,两个字符串长度不同,但前面的字符串是一样的,所以长度长的那个字符串比较大,所以str2比较大
//根据第二条:str2和str3比较,长度不同或者相同,但是 e 和 i 比较,i 比 e 大,所以str3比str2大
🌸两个string 对象相加
两 个string对 象 相 加 得 到 一 个 新 的 string 对 象 ,其 内 容 是 把 左 侧 的 运 算 对 象 与 右 侧的运算对象串接而成。也就是说,对string 对象使用加法运算符(+)的结果是一个 新 的 string对 象 , 它 所 包 含 的 宇 符由 两 部 分 组 成: 前 半 部 分 是 加 号 左 侧 string 对 象 所含的字符、后半部分是加号右侧string 对象所含的字符
string str1 = "hello, ";
string str2 = "world";
string str3 = str1 + str2; //得到:"hello, world"
str1 += str2; //等同于str1 = str1 + str2;
🌸字面值和string对象相加
//想要输出:"hello, world"
string str1 = "hello";
string str2 = "world";
string str3 = str1 + ", " + str2; //string对象和 ", " 进行相加
string str4 = "hello" + "world"; //错误,必须要有一个是string对象
string str5 = "hello" + ", "+ str2; //错误,从左到右算,前两个相加没有string对象
string str6 = "hello" + str2 + " ."; //正确,从左到右,前两个又一个是string对象,后面两个也是
string str7 = str1 + ". " + "world"; //正确,从左到右运算
当把 string 对象和字符字面值及字符串字面值混在一条语向中使用时,必须确保每个加
法 运 算 符 (+ ) 的 两 侧 的 运 算 对 象 至 少 有 一 个 是 string:
3️⃣ 处理 string 对象中的字符
我们经常需要单独处理string对象中的宇符,比如检查一个string对象是否包含 空白,或省把string 对象中的字母改成小写,再或者查看某个特定的字符是否出现等
cctype 头文件中定义了一组标 准 库 函 数 处 理 这 部 分 工 作:
isalnum(c) | 当c是学母或数字时为真 |
---|---|
isalpha(c) | 当c是 字母时 为真 |
iscntrl(c) | 当c是控制字符 时为真 |
isdigit(c) | 当c是数字时为真 |
isgraph(c) | 当c不是空格但 可打印 时为真 |
islower(c) | 当c是小写字母时为真 |
isprint(c) | 当 c 是可打印字符时为真 (即 c 是空格或 c 具 有 可视形式 ) |
ispunct(c) | 当c 是标点符号时为真(即c不是控制字符、数宇、字母、可打印空白中的 一 种) |
isspace(c) | 当c是空白时为真(即c 是空格、横向制表符、织向制表符、回车符、换行 符、进纸符中的一种) |
isupper(c) | 当c是大写 字母时 为真 |
isxdigit(c) | 当 c是 十 六 进 制 数 字时 为 真 |
tolower(c) | 如果c是大写字母,输出对应的小写字母:否则原样输出c |
toupper(c) | 如果c是小写字母,输出对应的大写字母:否则原样输出c |
🌸处理每个字符?使用基于范團的for 语句
\qquad
如果想对string 对象中的每个字符做点儿什么操作,目前最好的办法是使用C++11 新标准提供的一种语句:范围 for 语句
。这种语句遍历给定序列中的每个元 素并对序列中的每个值执行某种操作,其语法形式是:
for(存放遍历的数据 :遍历的对象)
循环体;
//没有括号
//示例:
string str = "hello world";
for(auto c : str) //auto可以自动设置数据类型
cout << c << endl; //这就会循环打印出str里面的各个字符
我们可以用这个for语句来实现对字符串里面的字符进行改变:
string str = "Hello World";
//转换为全大写
for(auto &c : str) //c是一个引用,这样可以改变str每个字符。
c = toupper(c);
cout << str << endl;
//最后输出的结果是:"HELLO WORLD"
🌸只处理一部分字符
要想访问string 对象中的单个字符有两种方式:一种是使用下标,另外一种是使用迭代器,其中关于迭代器的内容将在后面讲解。
下标运算符
[ ]
接收的输入参数是string::size_type
类型的值,这个参数表示要访问的字符的位置:返回值是该位量上字符的引用。
string 对象的下标从0计起。如果string对象 s 至少包含两个字符,则s[0]是第1个字符、s [1 ] 是第 2 个字符、s [s.size()-1]是最后一 个字符
string str = "hello world";
cout << str[1] << endl; //输出e
str[2] = toupper(str[2]); //将 l 变成大写 L
cout << str << endl; //输出 heLlo world
三、标准库类型vector
标准库类型
vector
表示对象的集合,其中所有对象的类型都相同。集合中的每个对 象都有一个与之对应的素引,索引用于访问对象。因为vector“容纳着” 其他对象,所 以 它 也 常 被 称 作容 器
要 想 使 用 vector , 必 须 包 含 适 当的 头 文件 。 在 后 续 的 例 子中 , 都 将 假 定 做 了如 下 :
#inlcude<vector>
using std::vector;
C++语言既有类模板,也有函数模板,其 中 vector 是 一 个 类 模 板 。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说 明。编译器根据模板创建类或函数的过程称为实例化 ,当使用模板时,需 要指出编译器应把类或函数实例化成何种类型。
\qquad 对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类, 需耍提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括 号,在括号内放上信息:
vector<int> num; //num保存int类型的对象
vector<string> str; //保存string类型的对象
vector<vector<int>> //存放的是vector对象
需 要 指 出 的 是 , 在 早 期 版 本 的 C++ 标 准 中 如 果 vector 的 元 素 还 是 vector (或 者 其 他模板类型), 则其定义的形式与现在的C+11新标准略有不同。过去,必须在外层vector 对象的右尖括号和其元素类型之问添加一个空格,如应该写成vector<<vctor< int> > 而非vector<< vector < int>>。
这个vector 可能有些人还是不理解,简单来说,就是一个可以自由扩展的数组。vector容器的使用方法和数组一样。
1️⃣ 定义和初始化 vector 对象
和任何 一种类类型一样,vector 模板控制着定义和初始化向量的方法:
T 是你想要创建的类型
vector< T > v1 | V1 是一个空vector,它潜在的元素是T类型的,执行默认初始化 |
---|---|
vector< T > v2(v1) | v2中包含有v1所有元素的劏本 |
vector< T > v2 = v1 | 等价手v2(v1),v2 中包含有V1所有元素的副本 |
vector< T > v3(n, val) | v3包含了n个重复的元素,每个元素的值都是val |
vector< T > v4(n) | v4包含了又个重复地执行了值初始化的对象 |
vector< T > v5{a,b,c…} | v5包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector< T > b5 = {a,b,c…} | 等 价 于 v 5 {a , b , c . . . } |
\qquad 看起来空vector 好像没什么用,但是很快我们就会知道程序在运行时可以很高效地往 vector 对象中添加元素。事实上,最常见的方式就是先定义一个空vector,然后当运行时获取到元素的值后再逐一添加。
🌲列表初始化vector对象
\qquad C++11新标准还提供了另外一种为vector 对象的元素赋初值的方法,即列表初始化。 此 时 , 用 花 括 号 括 起 来 的 0个 或 多 个 初 始 元 素 值 被 赋 给 vector对 象 :
vector<int> num = {1,3,4,5};//正确
vector<int> num{1,3,4,5};//正确
vector<int> num(1,3,4,5); //错误,必须是花括号
这个vectro对象就包含了四个元素,四个整型,相当于一个存有四个元素的数组。
还可以用vector 对象容纳的元素数量和所有元素的统一初始值来初始化vector 对象:
vector<int> num(10,5);//10个int类型的数,并且每个数都是5
vector<string> str(10, "hi!");//10个string类型的数,并且每个都是"hi!"
vector<int> num1(10);
//这样写并不是初始化为10,而是参考:vector<T> v1(n, val);上面那条,只不过你没有给它一个值,所以默认是0,所以这个vector也就是10个0.
2️⃣ 向 vector 对象中添加元素
vector和数组最大的区别在于它可以改变数组的长度,在后期添加或者删除元素;而数组却不可以改变长度,不能删除元素,而只能改变该元素的值。所以我们使用vector可以随意改变容器的长度和添加内容,添加内容的函数为:push_back
push_back 函数: 向该容器的末尾添加元素。
vector<int> v1;
for(int i = 0; i < 10; i++)
{
v1.push_back(i); //向这个容器里面添加0~9
}
for(int j = 0; j < 10; j++)
{
cout << v1[j] << endl;
}
输出结果为:0 1 2 3 4 5 6 7 8 9
3️⃣ 其他的 vector 操作
除了push_back 之外, vector 还提供 了几种 其他操 作, 大多数都和string 的相 关操作类似:
v.empty() | 判断容器是否为空,为空就返回真,不为空就返回假 |
---|---|
v.size() | 返回v 容器中元素的个数 |
v.push_back(t) | 向v 容器的尾端添加一个值为t的元素 |
v[n] | 返回v 中第n个位置上元素的引用,和数组访问元素一样,可以改变值 |
v1 = v2 | 用 v2 中元素的拷贝替换 v1 中 的元素 |
v1= {a, b, c…} | 用列表中元素的拷只替換v1中的元䄅 |
v1 == v2 | v1和v2 相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 | 判断不相等 |
<, <=, >, >= | 顾名思义,以字典顺序进行比较 |
v.erase() | 删除指定元素,需要用到迭代器 |
v.pop_back() | 删除最后一个元素 |
应用:
vector<int> v1;
for(int i = 0; i < 10; i++)
{
v1.push_back(i); //向这个容器里面添加0~9
}
cout<< v.size() << endl;//输出容器的长度:10
v1[3] = 10;
//将第四个元素改为10,容器里面的内容为:0 1 2 10 4 5 6 7 8 9
vector<int> v2;
v2 = v1;//将v1的内容拷贝到v2中
不能以下标的形式来添加元素:
vector<int> v1;
for(int i = 0; i < 10; i++)
{
v1[i] = i; //这是错误的,只能用push_back来添加元素
}
四、迭代器介绍
我们已经知道可以使用下标运算符来访问string对象的字符或vector 对象的元素,还有另外一种更通用的机制也可以实现同样的目的,这就是
迭代器
(iterator)。
除了vector 之外,标淮库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。严格来说,string 对象不属于容器类型,但是string支持很多与容器类型类似的操作。vector 支持下标 运算符,这点和string一样; string支持迭代器,这也和vector是一样的。
\qquad
类似于指针类型,迭代器也提供了对对象的间接访问。就 迭代器而言,其对象是容器中的元素或者string对象中的宇符。使用迭代器可以访问某 个元素, 迭代器 也 能 从 一 个 元 素 移 动到 另 外 一 个 元 素
。 选 代 器 有 有 效 和 无 效 之 分 , 这 一 点 和 指 针 差 不 多。 有 效 的 选 代 器 或 者 指 向 某 个 元 素 , 或 者 指 向 容 器 中 尾 元 素 的 下一 位 置 : 其他所有情況都属于无效。
1️⃣ 使用迭代器
\qquad
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭 代器的成员。比如,这些类型都拥有名为begin
和end
的成员,其中begin 成员负责返回指向第一个元素(或第 一个字符)的达代器。如有下述语句:
//由编译器决定b 和e 的类型
//b 表示v的第一个元素,e 表示v尾元素的下一位置
auto b = v.begin(),e = v.end();//b 和 e 的类型相同
\qquad
end 成员则负责返回指向容器 “尾元素的下一位置”的选代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后
” 元素。这样的迭代器没什么实际含义,仅是个标记而己,表示我们己经处理完了容器中的所有元素。end 成员返回的迭代器常被称作尾后迭代器
(off-the-end iterator )或者简称为尾 迭 代 器
。 特 殊 情 况 下 如 果 容 器 为 空 , 则 begin 和 end 返 回 的 是 同 一 个 迭代器。
✈️迭代器运算符
*itert | 返回迭代器iter所指元素的引用,可通过这个改变所指向元素的值 |
---|---|
iter->mem | 解引用iter 并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter 指 向容 器 中 的 下一 个 元 素 |
–iter | 令iter 指 向容 器 中 的 上一 个 元 素 |
iter1 == iter2 | 判断两个迭代器是否相等,如果指向的是同一个元素,则相等 |
iter1 != iter2 | 判断两个迭代器是否不相等 |
\qquad 和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必 须合法并确实指示着某个元素。试图解引用一个非法迭代器或 者尾后迭代器都是末被定 义的行为。
示例:
//将字符串中小写字母大写字母
string str("Hello World");
auto it = str.begin(); //获取第一个字符的迭代器
while(it != str.end()) //如果不等于结尾迭代器
{
*it = toupper(*it); //进入循环,将小写字母转换为大写字母
++it; //令迭代器指向下一个元素
}
cout << str << endl;
//最后输出 HELLO WORLD
✈️迭代器类型
\qquad 就像不知道string 和vector 的size_type 成员到底 是什么类型一样,一般来说我们也不知道( 其实是无须知道,迭代器的精确类型。而实际上, 那些拥有迭代器的标准库类型使用iterator和const_iterator 来表示迭代器的类型:
vector<int>::iterator it; // it 能读写 vector<int>的元素
string::iterator it2; //it2 能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //it4只能读字符,不能写字符
const_iterator和常量指针差不多,能读取但不能修改它所指的元素值。相反,iterator 的对象可读可写。如果vector 对象或string对象是一个常量,只能使用const_iterator:如果vector对象或string对象不是常量, 那么既能使用iterator也能使用const_iterator。
✈️begin 和end 运算符
\qquad begin 和end返回的具休类型由对象是否是常量决定,如果对象是常量,begin 和 end返回const_iterator; 如果对象不是常量,返回iterator:
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是 vector<int>::iterator
auto it2 = cv.begin(); //it2的类型是 vector<int>::const_iterator
为 了便 于 专 门 得 到 const_iterator 类 型 的 返 回 值 , C++11 新 标 准 引 入 了 两 个 新 函 数 , 分 别 是 cbegin 和 cend:
auto it3= v.begin(); //it3的类型是vector<int>:const_iterator
✈️结合解引用和成员访问操作
\qquad 解引用迭代器可获得选代器所指的对象,如果该对象的类型怡好是类,就有可能希望 进 一步访问它的成员。例如,对于一个由字符串组成的vector 对象来说,要想检查其元素是否为空,令it是该vector 对象的迭代器,只需检查it 所指字符串是否为空就可以了,其代码如下所示:
(*it).empty()
\qquad
为了 简 化 上 述 表 达 式 , C++ 语 言 定 义 了 箭 头 运 算 符 (->)
。 箭 头 运 算 符 把 解 引 用 和 成 员访问两个操作结合在一起,也就是说,it- >mem和(*it).mem表达的意思相同。
2️⃣ 迭代器运算
\qquad
string和vector 的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次 移动路过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作 迭代器运算
iter + n | 迭代器加上一个整数值仍得一个选代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
---|---|
iter - n | 迭代器减去一个整数值仍得一个选代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
iter += n | 迭 代 器 加 法 的 复 合 赋 值 语 句, 将 iter1加 n 的 结 果 赋 给 iter1 |
iter -= n | 迭 代 器 减 法 的 复 合 赋 值 语 句, 将 iter1减 n 的 结 果 赋 给 iter1 |
iter1- iter2 | 两个迭代器相滅的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
>, >=, <, <= | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前 ,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个位置 |
auto mid= vi.begin()+ vi.size()/ 2; //计算得到最接近vi中间元素的一个迭代器
auto iter= vi.begin() + 2; //从左的第三个迭代器
只要两个选代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距高指的是右侧的选代器向前移动多少位置就能追上左侧的迭代器,其类型是名为
differencet_ype
的带符号整型数。string 和vector 都定义了aifference_type,因为这个距离可正可负,所以aifference_type 是带符号类型的。
五、数组
数组是一种类似于标淮库类型vector 的数据结构,但是在性能和灵活性的权衡上又与vector 有所不同。与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector 不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定, 因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。
1️⃣ 定义和初始化内置数组
数组是一种复合类型。数组的声明形如a[d],其中a是数组的名字,d 是数组的维度,也就是数组的长度。 维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候长度应该是已知的。也就是说,长度必须 是 一 个常 量 表 达 式:
int a = 10; //不是常量表达式,a不是常量
//const 和 constexpr都可以
constexpr int b = 12; //被constexpr定义的一定是一个常量表达式
int arr[10]; //含有10个整数的数组
int *pint[b]; //含有12个整型指针的数组
int arr1[b]; //错误,b不是常量表达式
string strs[get_size()]; //当get_size()是constexp时正确,否则错误
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断 型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
定义数组为什么只能用常量表达式:因为数组的长度是不可以改变的,是固定的,而我们使用变量来定义数组,变量是可以改变的,所以可能会存在改变数组长度的风险,因此只能用常量
✈️数组的初始化
const int a = 3;
int num[a] = {0, 1, 2}; //含有三个元素的数组,元素值分别为:0,1,2
int nums[] = {0, 1, 2}; //没有给长度,但你给了初始值,编译器会默认长度为3
int num1[5] = {0, 1, 2};
//等价于 num1[5] = {0, 1, 2, 0, 0},没有给初始值会默认为0
int num2[3] = {"hi", "name"};
//等价于num2[3] = {"hi", "name", ""},默认为空
int num3[2] = {0, 1, 2}; //错误,初始值的个数大于数组长度
✈️字符数组的特殊性
\qquad 字符数组有一种额外的初始化形式,我们可以用字符串字面值 ,对此类数组初始化。 当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空符,这个空字符也会像字符串的其他 字符一样被拷贝到字符数组中去:
char a1[] = {'C','+', '+'}; //列表初始化,没有空字符'\0'
char a2[] = {'C', '+', '+', '\0'}; //列表初始化,含有显示的空字符'\0'
char a3[] = "C++"; //自动添加表示字符串结束的空字符'\0',等价于:"C++\0"
const char a4[5] = "hello";
//错误,字符数组的长度是5,而后面的字符串看似只有5个,其实还有一个用于表示结束的字符'\0'
//所以加起来就有6个,因此是错误的
✈️不允许拷贝和赋值
不能 将 数 组 的 内 容 拷 贝 给 其 他 数 组 作 为 其 初 始 值 , 也 不 能 用 数 组 为 其 他 数 组赋 值 :
int a[] = {0, 1,2};
int a2[] = a; //错误,不允许使用一个数组初始化另一个数组
a2 = a; //错误,不能把一个数组直接赋给另一个数组
✈️关于数组的引用
int &refs[10] = "hi"; //错误,不存在引用的数组
int (&arrRef)[10] = arr;//arrRef引用一个含有10个整教的教组
至于这个的解释,有点复杂,怕你们看不懂,感兴趣的可以私信我。
2️⃣ 访问数组元素
与 标 准 库 类 型 v e c t o r 和 s t r i n g 一 样 , 数 组 的 元 素 也 能 使 用 范 围 f o r 语 句 或 下标 运算符来访问。数组的索引从0开始,以一个包含10 个元素的数组为例 ,它的索引从0 到9,而非从1到10。
在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的 无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。
数组除了大小固定这一特点外,其他用法与vector 基本类似。
数组访问元素通过下标进行访问
//假如有这样一个数组
int ia[] = {1,2,3,4,5,6,7,8,9,10};
这个数组的下标为:0 1 2 3 4 5 6 7 8 9
下标是从0开始的,数字代表着位置,0是第一个数,1是第二个数,以此类推
访问方式:数组名[下标]; ia[0]为1, ia[1]为2
-----------------------------------------
int arr[10] = {};
for(int i = 0; i < 10; i++)
{
arr[i] = i; //循环10次将0~9存入数组中
}
for(int j = 0; j < 10; j++)
{
cout << arr[j] << " "; //通过for循环将数组里面的数显示出来
}
3️⃣ 指针和数组
在C++语言中,指针和数组有非常紧密的联系。就如即将介绍的,使用数组的时候编 译器一般会把它转换成指针。
\qquad 通常情況下,使用取地址符来获取指向某个对象的指针,取 地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定 位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:
string nums[] = {"one", "two", "three"}; //数组元素是string对象
string *p = &nums[0]; //p 指向 nums 的第一个元素
\qquad 然而,数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换 为一 个指向数组首元素的指针,简单来说,数组名是一个指针:
string *p2 = nnums; //等价于 p2 = &nums[0]
//既然是指针,那么可以用auto来自动获取类型
int ia[] = {0,1,2,3,4,5,6,7,8,9};
//ia是一个含有10数的整型数组
auto ia2(ia); //因为ia是一个整型指针,所以ia2的类型也是一个整型指针
ia2 = 42; //因为ia2是一个整型指针,所以不能用整型赋值
✈️指针也是迭代器
\qquad vector 和string 的迭代器 支持的运算,数组的指针全都支特。例如, 允 许 使 用 递 增 运 算 符 将 指 向 数 组 元 素 的 指 针 向 前 移 动到 下一 个 位 置 上:
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p指向arr的第一个元素
++p; //p 指向arr[1]
cout << *p <<endl; //输出 1
//我们可以设法获取数组尾元素之后那个不存在的地址
int *p1 = &ia[10];
//很显然,数组最后一个元素是ia[9],而ia[10]超出了
//我们可以拿到这个元素的地址,但是我们不能对它进行解引用和递增
✈️标准库函数begin 和end
管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全, C++11新标准引入了两个名为
begin
和end
的函数。这两个两数与容器中的两个同名成员功 能 类 似 , 不 过 数 组 毕竟 不 是 类 类 型 , 因 此 这 两 个 函 数 不 是 成 员 函 数。
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); //指向ia首元素的指针,也就是0的地址
int *last = end(ia); //指向ia尾元素的下一个位置的地址,9后面一个的地址
使 用 begin和 end 可 以 很 容 易 地 写 出 一 个 循 环 并 处 理 数 组 中 的 元 素 。 例 如 ,假 设 arr是一个整型数组,下面的程序负责找到 arr 中的第一个负数:
//pbeg指向arr 的首元素,pend指向arr 尾元素的下一位置
int *pbeg = begin(), *pend = end(arr);
//寻找第一个负值元素,如果已经检查完全部元素则结束循环
while(pbeg != pend && *pbeg >= 0)
++pbeg;
//如果首元素的地址不等于尾元素的下一个地址,则说明没有遍历完
指针的运算
const size_t a = 5;
int arr[a] = {1,2,3,4,5};
int *p = arr;
int *p2 = p + 4; //p2 指向arr的尾元素 arr[4],5
int *p3 = arr + a;
//正确,arr是指向数组首元素的指针,p指向arr尾元素的下一个位置,警告:不要对p3进行解引用
int *p2 = arr + 10;//错误,arr只有5个元素,+10就超出范围了
我们可以通过迭代器,进行相减计算出它们之间的距离,也就是元素个数
auto n = end(arr) - begin(arr);
//n就为arr中元素的个数
4️⃣ C风格字符串
⚠️注意:尽管C++支持C风格字符串,但在C1十程序中最好还是不要使用它们。这是因 为C风格宇符串不仅使用起来不大方便,而且极易引发程序漏洞,是诸多安全 问题的根本原因。
字符串字面值是一种通用结构的实例,这种结构即是C++由C 继承而来的
C 风格字符串
。 C 风 格 字 符 串 不 是 一 种 类 型 , 而 是 为 了 表 达 和 使 用 字 符 串 而 形 成 的一种约定俗成的写法。按此习惯书写的 字符串存放在字符数组中并以空字符结束 。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(‘\0’)。 一 般利用指针来操作这些 字符串。
✈️C标准库String 函数
下表 列举了C 语言标准库提供的一组函数,这些函数可用于操作C风格字符串,它 们定义在 cstring 头文件中,cstring 是C语言头文件 string.h 的C++版本
strlen( p ) | 返回p 的长度,空字符不计算在内 |
---|---|
strcmp( p1, p2) | 比较p1和p2的相等性。如果p1==p2,返回0;如果p1>p2, 返回一个正值:如果p1<p2,返回一个负值 |
strcat( p1, p2) | 将p2 追加到 p1 之后,返回p1 |
strcpy( p1, p2) | 将p2拷贝给p1,返回p1 |
#include <cstring>
#include<iostream>
using namespace std;
int main()
{
char str1[] = "hello";
//计算长度
cout << "str1 的长度为: "<< strlen(str1) << endl;
char str2[] = "world";
if(strcmp(str1, str2)) //比较大小
{
cout << "str1大于str2" << endl;
}
else if(strcmp(str1, str2) == 0)
{
cout << "str1等于str2" << endl;
}
else
{
cout << "str1 小于str2" <<endl;
}
//追加
strcat(str1, str2);
cout << "追加之后的:"<<str1 <<endl;
//拷贝
strcpy(str1,str2);
cout <<"拷贝之后的:" << str1 << endl;
return 0;
}
输出:
5️⃣ 与旧代码的接口
很 多 C++程 序 在 标 准 库 出 现 之 前 就 已 经 写 成 了, 它 们 肯 定 没 用 到 s t r i n g 和 v e c t o r 类型。而且,有一些C++程序实际上是与C 语言或其他语言的接口程序,当然也无法使用 C++标准库。因此,现代的C++程序不得不与那些充满了数组和/或C风格字符串的代码衔 接 , 为 了 使 这 一 工 作 简 单 易 行 , C ++ 专 门 提 供 了一 组 功 能 。
混用string 对象和C 风格字符串
如果程序的某处需要一个C 风格字符串,无法直接用 st ring 对家米代替它。例如,不能用st ring 对象直接初始化指向字符的指针。为了完 成 该 功 能 , string 专 门 提 供 了一 个 名 为c_str
的 成 员函 数:
string s("hello world");
char *str = s; //错误,不能用string对象来初始化char*
const char *str = s.c_str(); //正确
使用数组初始化vector对象
允许使用数组米初始化vector 对象。要 实 现 这 一 目 的 , 只 需 指 明 要 拷 贝 区 域 的 首 元 素地 址 和 尾 后 地 址 就 可 以 了:
int arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(arr), end(arr));
//ivec有6个元素,分別是arr中对应元素的副本
vector<int> sub(arr+1, arr+4);
//只初始化一部分:arr[1],arr[2],arr[3]
六、多维数组
严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点, 对今后理解和使用 多维数组大有益处。
\qquad 当一个数组的元素仍然是数组时,通常使用两个维度来定 义它:一个维度表示数组本 身大小, 另外一 个维 度 表 示 其 元素(也是数组) 大 小 :
int nums[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
int arr[10][20][30] = {0};
//大小为10 的数组,它的 每个元泰都是大小为20 的数组,
//这些数组的元素是含有30 个整数的数组
//将所有元素初始化为0
对于二维数组来说,常把第一个维度称作行,第二个维度称作列。
int nums[3][4] = {
{0,1,2,3}, //花括号括起来的也是一个数组,第一行
{4,5,6,7}, //第二行元素
{8,9,10,11} //第三行元素
};
多维数组的初始化
允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组 一样。下面的初 始化形式中, 多维数组的每一行分别用花括号括了起来:
int nums[3][4] = { //三个元素,每个元素都是大小为4的数
{0,1,2,3}, //第一行的初始值
{4,5,6,7}, //第二行初始值
{8,9,10,11} //第三行初始值
};
其中内层嵌套着的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的 功能和上面这段代码完全 一样:
int nums[3][4] ={0,1,2,3,4,5,6,7,8,9,10,11};
//编译器会根据数组的长度来自己分配,4个一组
int nums[3][4] ={0,1,2,3};
//这样它就只初始化第一行的四个数了,其他的都初始化为0
多维数组的使用
0 | 1 | 2 | 3 |
---|---|---|---|
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
向上面的表格就像是一个二维数组,一行就是一个元素,列数是里面数组的元素个数。
num[0][0] -> 0
num[0][1] -> 1
num[0][2] -> 2
num[0][3] -> 3
num[1][0] -> 4
num[1][1] -> 5
num[1][2] -> 6
num[1][3] -> 7
num[2][0] -> 8
num[2][1] -> 9
num[2][2] -> 10
num[2][3] -> 11
//二维数组赋值
int nums[3][4] ={};
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
nums[i][j] = 10;
}
}
//输出二维数组
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
cout << nums[i][j] << " ";
}
cout << endl;
}
/*结果
10 10 10 10
10 10 10 10
10 10 10 10
*/
基于范围的for循环
for(auto &row : nums)
for(auto &col : row)
cout << col <<endl;
//这样也能显示
✈️指针和多维数组
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
注意:定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组
\qquad 因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4]= ia; //p指向含有4个整数的数组
p = &ia[2]; //p指向ia的尾元素
我们首先明确( *p)意味着p是一个指针。接着观 察右边发现,指针p 所指的是一个维度为4 的数组;再观察左边知道,数组中的元素是整 数 。因 此 , p 就 是 指 向 含 有 4 个 整 数 的 数 组 的 指 针 。
\qquad 随 着C++11新 标 准 的 提 出 , 通 过 使 用 auto 或 者 decltype就能尽可能地避免在数组前面加上一个指针类型了:
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
//输出ia中每个元素的值,每个内层数组各占一行
//p指向含有4 个整数的数组
for(auto p = ia; p != ia + 3; ++p) {
// q指向4个整数数组的首元素,也就是说,q指向一个整数
for(auto q = *p; q != ia + 4; ++q)
cout << *q << ' ';
cout << endl;
}
这章就介绍到这里了,关注我,正在更新中…