# 命名空间的 using 声明
std::cin
的意思就是要使用命名空间 std
中的名字 cin
using
声明具有如下的形式:
using namespace::name; |
一旦声明了上述语句,就可以直接访问命名空间中的名字
例如:
#include <iostream> | |
// using declaration; when we use the name cin, we get the one from the namespace std | |
using std::cin; | |
int main() { | |
int i; | |
cin >> i; // ok: cin is a synonym for std::cin | |
cout << i; // error: no using declaration; we must use the full name | |
std::cout << i; // ok: explicitly use cout from namepsace std | |
return 0; | |
} |
按照规定,每个 using
声明引入命名空间中的一个成员
因此,每个名字都必须有自己独立的声明语句,而且 每句话都得以分号结束
例如:
#include <iostream> | |
// using declarations for names from the standard library | |
using std::cin; | |
using std::cout; | |
using std::endl; | |
int main() { | |
cout << "Enter two numbers:" << endl; | |
int v1, v2; | |
cin >> v1 >> v2; | |
cout << "The sum of " << v1 << " and " << v2 | |
<< " is " << v1 + v2 << endl; | |
return 0; | |
} |
一般来说,头文件不应该使用 using
声明
因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using
声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,可能产生始料未及的名字冲突
# 标准库类型 string
标准库类型 string
表示可变长的字符序列
- 使用
string
类型必须首先包含string
头文件 - 作为标准库的一部分,
string
定义在命名空间std
中
#include <string> | |
using std::string; |
# 定义和初始化 string 对象
string
有两类初始化方式
-
拷贝初始化(copy initialization): 使用等号来初始化一个变量,编译器把等号右侧的初始值拷贝到新创建的对象中去
-
直接初始化(direct initialization): 不使用等号
需注意:
-
string
接受无参数的初始化方式:不论string
对象定义在函数内还是函数外,默认初始化都是得到空串 -
如果提供了一个 字符串字面值 ,该字面值中 除了最后那个空字符外 的所有字符 都被拷贝到新创建的
string
对象中
# string 对象上的操作
string
对象的大多数操作:
其中, is >> s
读取时不包括空格符; getline(is, s)
读取整行,包含空格符
# 读写 string 对象
可以使用 IO 操作符读写 string
对象:
// Note: #include and using declarations must be added to compile this code | |
int main() | |
{ | |
string s; // empty string | |
cin >> s; // read a whitespace-separated string into s | |
cout << s << endl; // write s to the output | |
return 0; | |
} |
在执行读取操作时,
string
对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止
如上所述,如果程序的输入是 “ Hello World! ”(注意开头和结尾处的空格),则输出将是 “Hello”,没有任何空格
和内置类型的输入输出操作一样, string
对象的此类操作也是返回运算符左侧的运算对象作为其结果。因此,多个输入或者多个输出可以连写在一起:
string s1, s2; | |
cin >> s1 >> s2; // read first input into s1, second into s2 | |
cout << s1 << s2 << endl; // write both strings |
假设给上面这段程序输入与之前一样的内容 “Hello World!”,输出将是 “HelloWorld!”
# 读取未知数量的 string 对象
int main() | |
{ | |
string word; | |
while (cin >> word) // read until end-of-file | |
cout << word << endl; // write each word followed by a new line | |
return 0; | |
} |
一旦遇到文件结束标记或非法输入,循环也就结束了
# 使用 getline 读取一整行
getline
能在最终得到的字符串中保留输入时的空白符
getline
函数的参数是一个输入流和一个 string
对象,函数从给定的输入流中读入内容,直到遇到换行符为止(换行符也被读进来了),然后把所读的内容存入到那个 string
对象中去(不存换行符)
getline
只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入一开始就是换行符,那么所得的结果就是一个空 string
和输入运算符一样, getline
也会返回它的流参数。因此,也能使用 getline
的结果作为判断的条件:
int main() { | |
string line; | |
// read input a line at a time until end-of-file (EOF) | |
while (getline(cin, line)) | |
cout << line << endl; | |
return 0; | |
} |
触发 getline
函数返回的那个换行符实际上被丢弃掉了,得到的 string
对象中并不包含该换行符
# string::size_type 类型
size
函数返回 string
对象的长度(即 string
对象中字符的个数),返回的是一个 string::size_type
类型的值
string
类及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type
即是其中的一种。在具体使用的时候,通过作用域操作符来表明名字size_type
是在类string
中定义的
string::size_type
类型的对象,是一个无符号类型的值,而且能足够存放下任何 string
对象的大小
所有用于存放 string
类的 size
函数返回值的变量,都应该是 string::size_type
类型的
在 C++ 11 新标准中,允许编译器通过 auto
或者 decltype
来推断变量的类型:
auto len = line.size(); // len has type string::size_type |
由于 size
函数返回的是一个无符号整型数,如果在表达式中混用了带符号数和无符号数,将可能产生意想不到的结果。例如,假设 n
是一个具有负值的 int
,则表达式 s.size() < n
的判断结果几乎肯定是 true
,因为负值 n
会自动地转换成一个比较大的无符号值
如果一条表达式中已经有了
size()
函数,就不要再使用int
,以避免混用int
和unsigned
可能带来的问题
# 比较 string 对象
string
类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较 string
对象中的字符,并且对大小写敏感。
相等性运算符( ==
和 !=
)、关系运算符( <
、 <=
、 >
、 >=
)都依照(大小写敏感的)字典顺序:
- 如果两个
string
对象的长度不同,而且较短string
对象的每个字符都与较长string
对象对应位置上的字符相同,就说较短string
对象小于较长 string 对象。 - 如果两个
string
对象在某些对应的位置上不一致,则string
对象比较的结果其实是string
对象中第一对相异字符比较的结果。
# 为 string 对象赋值
对于 string
类而言,允许把一个对象的值赋给另外一个对象:
string st1(10, 'c'), st2; // st1 is cccccccccc; st2 is an empty string | |
st1 = st2; // assignment: replace contents of st1 with a copy of st2 | |
// both st1 and st2 are now the empty string |
# 两个 string 对象相加
两个 string
对象相加得到一个新的 string
对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。
string s1 = "hello, ", s2 = "world\n"; | |
string s3 = s1 + s2; // s3 is hello, world\n | |
s1 += s2; // equivalent to s1 = s1 + s2 |
# 字面值和 string 对象相加
标准库允许把字符字面值和字符串字面值转换成 string
对象,所以在需要 string
对象的地方就可以使用这两种字面值来替代。
string s1 = "hello", s2 = "world"; // no punctuation in s1 or s2 | |
string s3 = s1 + ", " + s2 + '\n'; |
当把 string
对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符( +
)的两侧的运算对象至少有一个是 string
:
string s4 = s1 + ", "; // ok: adding a string and a literal | |
string s5 = "hello" + ", "; // error: no string operand | |
string s6 = s1 + ", " + "world"; // ok: each + has a string operand, equivalent to string s6 = (s1 + ", ") + "world"; | |
string s7 = "hello" + ", " + s2; // error: can't add string literals, equivalent to string s7 = ("hello" + ", ") + s2; |
因为某些历史原因,也为了与 C 兼容,C++ 语言中的字符串字面值并不是标准库类型
string
的对象。切记,字符串字面值与string
是不同的类型。
# 处理 string 对象中的字符
我们经常需要单独处理 string
对象中的字符,比如检查一个 string
对象是否包含空白,或者把 string
对象中的字母改成小写,再或者查看某个特定的字符是否出现等
这类处理的一个关键问题是如何获取字符本身。另一个关键问题是要知道能改变某个字符的特性
在 cctype
头文件中定义了一组标准库函数处理这部分工作:
# 使用基于范围的 for 语句处理每个字符
C++ 11 新标准提供的一种语句: 范围 for(range for)
语句。这种语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作,其语法形式是:
for (declaration : expression) | |
statement |
其中, expression
部分是一个对象,用于表示一个序列。 declaration
部分负责定义一个变量,该变量用于访问序列中的基础元素。每次迭代, declaration
部分的变量会被初始化为 expression
部分的下一个元素值
一个 string
对象表示一个字符的序列,因此 string
对象可以作为 范围 for
语句中的 expression
部分
string str("some string"); | |
// print the characters in str one character to a line | |
for (auto c : str) // for every char in str | |
cout << c << endl; // print the current character followed by a newline |
for
循环把变量 c
和 str
联系了起来。此例中,通过使用 auto
关键字让编译器来决定变量 c
的类型,这里 c
的类型是 char
。每次迭代, str
的下一个字符被拷贝给 c
,因此该循环可以读作 “对于字符串 str
中的每个字符 c
,执行某某操作”
举个稍微复杂一点的例子,使用 范围for
语句和 ispunct
函数来统计 string
对象中标点符号的个数:
string s("Hello World!!!"); | |
// punct_cnt has the same type that s.size returns | |
decltype(s.size()) punct_cnt = 0; | |
// count the number of punctuation characters in s | |
for (auto c : s) // for every char in s | |
if (ispunct(c)) // if the character is punctuation | |
++punct_cnt; // increment the punctuation counter | |
cout << punct_cnt | |
<< " punctuation characters in " << s << endl; |
程序的输出结果将是: 3 punctuation characters in Hello World!!!
这里我们使用 decltype
关键字声明计数变量 punct_cnt
,它的类型是 s.size
函数返回值的类型,也就是 string::size_type
# 使用范围 for 语句改变字符串中的字符
如果想要改变 string
对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符
假设我们想要把字符串改写为大写字母的形式,为此,可以使用标准库函数 toupper
,该函数接收一个字符,然后输出其对应的大写形式,程序为:
string s("Hello World!!!"); | |
// convert s to uppercase | |
for (auto &c : s) // for every char in s (note: c is a reference) | |
c = toupper(c); // c is a reference, so the assignment changes the char in s | |
cout << s << endl; |
上述代码的输出结果将是: HELLO WORLD!!!
每次迭代时,变量 c
引用 string
对象 s
的下一个字符,赋值给 c
也就是在改变 s
中对应字符的值
# 只处理一部分字符
要想访问 string
对象中的单个字符有两种方式:
- 使用下标
- 使用迭代器
下标运算符( [ ]
)接收的输入参数是 string::size_type
类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用
string
对象的下标从 0 计起
string
对象的下标必须大于等于 0 而小于 s.size()
。因为 string
对象的下标从 0 计起,最后一个字符的下标应该是 s.size() - 1
下标的值称作 “下标” 或 “索引”
对于任何一个表达式,只要它的值是一个整型值,它就能作为索引
如果某个索引是带符号类型的值,将自动转换成由 string::size_type
表达的无符号类型
if (!s.empty()) // make sure there's a character to print | |
cout << s[0] << endl; // print the first character in s |
只要字符串未被 const
限定符限制为常量,就能利用下标运算符为字符串中指定位置的字符赋新值
例如:
string s("some string"); | |
if (!s.empty()) // make sure there's a character in s[0] | |
s[0] = toupper(s[0]); // assign a new value to the first character in s |
程序的输出结果将是: Some string
再例如:
// process characters in s until we run out of characters or we hit a whitespace | |
for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) | |
s[index] = toupper(s[index]); // capitalize the current character |
程序的输出结果将是: SOME string
在上述程序中, for
循环使用变量 index
作为 s
的下标, index
的类型是由 decltype
关键字决定的
上例使用了逻辑与运算符( &&
):如果参与运算的两个运算对象都为真,则逻辑与结果为真;否则结果为假
对逻辑与运算符(
&&
)而言,C++ 语言规定:只有当左侧运算对象为真时,才会检查右侧运算对象的情况
再次强调,注意检查下标的合法性。一种简便易行的方法是,总是设下标的类型为 string::size_type
,因为此类型是无符号数,可以确保下标不会小于 0 ,此时,代码只需保证下标小于 size()
的值就可以了
# 使用下标执行任意访问
直接获取对应位置的字符,而不是从前往后依次访问
编写一个程序把 0 到 15 之间的十进制数转换成对应的十六进制形式:
const string hexdigits = "0123456789ABCDEF"; // possible hex digits (Note that it is a const string.) | |
cout << "Enter a series of numbers between 0 and 15" | |
<< " separated by spaces. Hit ENTER when finished: " | |
<< endl; | |
string result; // will hold the resulting hexify'd string | |
string::size_type n; // hold numbers from the input | |
while (cin >> n) | |
if (n < hexdigits.size()) // ignore invalid input | |
result += hexdigits[n]; // fetch the indicated hex digit | |
cout << "Your hex number is: " << result << endl; |
假设输入的内容如下: 12 0 5 15 8 15
程序的输出结果将是: Your hex number is: C05F8F
# 成员函数
参考:https://www.boyuai.com/learn/courses/148/lessons/2481/steps/0?from=qz
# 标准库类型 vector
标准库类型 vector
表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为 vector
“容纳着” 其他对象,所以它也常被称作 容器(container)
要想使用 vector
,必须包含适当的头文件:
#include <vector> | |
using std::vector; |
C++ 语言既有类模板(class template),也有函数模板,其中 vector
是一个类模板
由 vector
生成的类型必须包含 vector
中元素的类型,例如 vector<int>
, vector<vector<int>>
组成 vector
的元素也可以是 vector
vector<int> ivec; // ivec holds objects of type int | |
vector<Sales_item> Sales_vec; // holds Sales_items | |
vector<vector<string>> file; // vector whose elements are vectors |
引用不是对象,所以不存在包含引用的 vector
# 定义和初始化 vector 对象
可以默认初始化 vector
对象,从而创建一个指定类型的空 vector
如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
vector<string> v1{"a", "an", "the"}; // list initialization | |
vector<string> v2("a", "an", "the"); // error | |
vector<int> v3(10, 1); // v3 has ten elements with value 1 | |
vector<int> v4{10, 1}; // v4 has two elements with values 10 and 1 |
# 向 vector 对象中添加元素
vector
对象直接初始化的方式适用于三种情况:
- 初始值已知且数量较少
- 初始值是另一个
vector
对象的副本 - 所有元素的初始值都一样
一般情况下,可以先创建一个空 vector
,然后在运行时再利用 vector
的成员函数 push_back
向其中添加元素
push_back
函数:把一个值当成 vector
对象的尾元素,“压到(push)” vector
对象的 “尾端(back)”
vector<int> v2; // empty vector | |
for (int i = 0; i != 100; ++i) | |
v2.push_back(i); // append sequential integers to v2 | |
// at end of loop v2 has 100 elements, values 0 . . . 99 | |
// read words from the standard input and store them as elements in a vector | |
string word; | |
vector<string> text; // empty vector | |
while (cin >> word) { | |
text.push_back(word); // append word to text | |
} |
需要注意:如果循环体内部包含有向 vector
对象添加元素的语句,则不能使用 范围 for
循环
范围 for
语句体内不应改变其所遍历序列的大小
# 其他 vector 操作
除了 push_back
之外, vector
还提供了几种其他操作,大多数都和 string
的相关操作类似:
例如:
vector<int> v{1,2,3,4,5,6,7,8,9}; | |
for (auto &i : v) // for each element in v (note: i is a reference) | |
i *= i; // square the element value | |
for (auto i : v) // for each element in v | |
cout << i << " "; // print the element | |
cout << endl; |
vector
的 size
函数返回 vector
对象中元素的个数,返回值的类型是由 vector
定义的 size_type
类型
要使用 size_type
,需首先指定它是由哪种类型定义的,对于 vector
而言, vector
对象的类型总是包含着元素的类型:
vector<int>::size_type // ok | |
vector::size_type // error |
# 计算 vector 内对象的索引
对于 vector
,可以使用下标运算符获取指定的元素
vector
对象的下标也是从 0 开始计起,下标的类型是相应的 size_type
类型
此外,也能通过计算得到 vector
内对象的索引,然后直接获取索引位置上的元素:例如,假设有一组成绩的集合,其中成绩的取值是从 0 到 100。以 10 分为一个分数段,要求统计各个分数段各有多少个成绩,其代码实现如下:
// count the number of grades by clusters of ten: 0--9, 10--19, . .. 90--99, 100 | |
vector<unsigned> scores(11, 0); // 11 buckets, all initially 0 | |
unsigned grade; | |
while (cin >> grade) { // read the grades | |
if (grade <= 100) // handle only valid grades | |
++scores[grade / 10]; // increment the counter for the current cluster | |
cout << scores << endl; | |
} |
如果输入的成绩如下: 42 65 95 100 39 67 95 76 88 76 83 92 76 93
,则输出的结果应该是: 0 0 0 1 1 0 2 3 2 4 1
# 不能用下标形式添加元素
不能通过 vector
对象的下标形式来添加元素
vector<int> ivec; // empty vector | |
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) | |
ivec[ix] = ix; // disaster: ivec has no elements |
如前所述,正确的方法是使用 push_back
:
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) | |
ivec.push_back(ix); // ok: adds a new element with value ix |
注意,这里不能使用范围 for
语句,只能使用传统的 for
循环,因为 vector
序列长度发生变化了
vector
对象(以及 string
对象)的下标运算符可用于访问已存在的元素,但不能用于添加元素
试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致 PC 及其他设备上应用程序出现安全问题的一个重要原因
如果需要对 vector
对象或 string
对象使用 for 语句,则尽量使用范围 for,以免溢出
# 迭代器
除了使用下标运算符来访问 string
对象的字符或 vector
对象的元素以外,也可以使用 迭代器(iterator)
访问
除了 vector
之外,标准库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符
严格来说, string
对象不属于容器类型,但是 string
支持很多与容器类型类似的操作,例如,下标运算符、迭代器
string
可以理解成vector<char>
类似于指针类型,迭代器提供了对对象的间接访问:使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素
# 使用迭代器
有迭代器的类型同时拥有名为 begin
和 end
的成员
begin
:返回指向第一个元素(或第一个字符)的迭代器end
:返回指向尾元素的下一位置的迭代器(一个本不存在的元素),end
返回的迭代器常被称作尾后迭代器或者简称为尾迭代器
// the compiler determines the type of b and e | |
// b denotes the first element and e denotes one past the last element in v | |
auto b = v.begin(), e = v.end(); // b and e have the same type |
如果容器为空,则 begin
和 end
返回的是同一个迭代器,都是尾后迭代器
只要我们知道其支持 begin
和 end
,就可以使用 auto
关键字来定义返回值的类型
注意:试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为
# 迭代器运算符
注意, *iter
返回的是迭代器 iter
所指元素的引用。因此,可以通过 *iter
来修改 iter
所指元素的值
string s("some string"); | |
if (s.begin() != s.end()) { // make sure s is not empty | |
auto it = s.begin(); // it denotes the first character in s | |
*it = toupper(*it); // make that character uppercase | |
} |
输出结果将是: Some string
迭代器使用递增( ++
)运算符来从一个元素移动到下一个元素
因为 end
返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作
例如,把 string
对象中第一个单词改写为大写形式:
// process characters in s until we run out of characters or we hit a whitespace | |
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it) | |
*it = toupper(*it); // capitalize the current character |
输出结果将是: SOME string
使用 ==
和 !=
来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同,或者都是同一个容器的尾后迭代器,则它们相等;否则,这两个迭代器不相等
关键概念:泛型编程
只有
string
和vector
等一些标准库类型有下标运算符,并不是所有标准库容器都能使用下标运算符访问,但是,所有标准库容器都可以使用迭代器大多数的标准库容器都没有定义
<
运算符,但是,所有标准库容器的迭代器都定义了==
和!=
因此,只要我们养成使用迭代器和
!=
的习惯,就不用太在意用的到底是哪种容器类型
# 迭代器类型
拥有迭代器的标准库类型使用 iterator
和 const_iterator
来表示迭代器的类型:
vector<int>::iterator it; // it can read and write vector<int> elements | |
string::iterator it2; // it2 can read and write characters in a string | |
vector<int>::const_iterator it3; // it3 can read but not write elements | |
string::const_iterator it4; // it4 can read but not write characters |
const_iterator
类似于指向常量的指针,只能读取,不能修改它所指元素的值
iterator
的对象可读可写
如果 vector
对象或 string
对象是一个常量,只能使用 const_iterator
如果 vector
对象或 string
对象不是常量,那么,既能使用 iterator
也能使用 const_iterator
# begin 和 end 运算符
begin
和 end
返回的具体类型由其所指对象决定:
- 如果对象是常量,
begin
和end
返回const_iterator
- 如果对象不是常量,返回
iterator
vector<int> v; | |
const vector<int> cv; | |
auto it1 = v.begin(); // it1 has type vector<int>::iterator | |
auto it2 = cv.begin(); // it2 has type vector<int>::const_iterator |
为了专门得到 const_iterator
类型的返回值,C++ 11 新标准引入了两个新函数,分别是 cbegin
和 cend
:
auto it3 = v.cbegin(); // it3 has type vector<int>::const_iterator |
函数 cbegin
和 cend
分别返回指向容器第一个元素的迭代器、指向最后元素下一位置的迭代器,返回值都是 const_iterator
类型
# 结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能进一步访问它的成员
例如,对于一个由字符串组成的 vector
对象来说,要想检查其元素是否为空,令 it
是该 vector
对象的迭代器,只需检查 it
所指字符串是否为空就可以了,其代码如下所示:
vector<string> v; | |
auto it = v.begin(); | |
(*it).empty(); // dereferences it and calls the member empty on the resulting object |
注意, (*it).empty()
中的圆括号必不可少,该表达式的含义是先对 it
解引用,然后再执行点运算符。如果不加圆括号,点运算符将由 it
来执行,而非 *it
:
*it.empty(); // error: attempts to fetch the member named empty from it, but it is an iterator and has no member named empty |
为了简化形如 (*it).empty()
的表达式,C++ 定义了箭头运算符( ->
)
箭头运算符把解引用和成员访问两个操作结合在一起,即, it->mem
和 (*it).mem
表达的意思相同
it->empty; // it->mem is a synonym for (* it).mem. |
例如,下列程序将输出三行 hello the world!!
// print each line in text up to the first blank line | |
vector<string> text(3, "hello the world!!"); | |
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it) | |
cout << *it << endl; |
# 某些对 vector 对象的操作会使迭代器失效
虽然 vector
对象可以动态地增长,但是也会有一些副作用:
- 不能在
范围 for
循环中向vector
对象添加元素 - 任何一种可能改变
vector
对象容量(capacity,而不是 size)的操作,比如push_back
,都会使该vector
对象的迭代器失效
谨记,如果循环体内使用了迭代器,就不要向相应的容器中添加元素
# 迭代器运算
所有标准库容器的迭代器都支持递增运算(令迭代器每次移动一个元素)
可以用 ==
和 !=
对任意标准库类型的两个有效迭代器进行比较
这些运算被称作迭代器运算(iterator arithmetic)
string
和 vector
的迭代器支持更多的运算:
- 迭代器的每次移动可以跨过多个元素
- 迭代器支持关系运算
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离
两个迭代器的距离,指的是左侧迭代器向右移动多少位置才能追上右侧迭代器,其类型是名为 difference_type
的带符号整型数,因为这个距离可正可负,所以 difference_type
是带符号类型的
string
和 vector
都定义了 difference_type
使用迭代器运算的一个经典算法是二分查找,例如,在升序数组 text
中寻找 sought
:
// text must be sorted | |
// beg and end will denote the range we're searching | |
auto beg = text.begin(), end = text.end(); | |
auto mid = text.begin() + (end - beg) / 2; // original midpoint | |
// while there are still elements to look at and we haven't yet found sought | |
while (mid != end && *mid != sought) { | |
if (sought < *mid) // is the element we want in the first half? | |
end = mid; // if so, adjust the range to ignore the second half | |
else // the element we want is in the second half | |
beg = mid + 1; // start looking with the element just after mid | |
mid = beg + (end - beg) / 2; // new midpoint | |
} |
程序定义了三个迭代器: beg
指向搜索范围内的第一个元素、 end
指向尾元素的下一位置、 mid
指向中间的那个元素
# 数组
数组是一种类似于标准库类型 vector
的数据结构(数组不是类类型)
- 与
vector
相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问 - 与
vector
不同的地方是,数组的大小确定不变,不能随意向数组中增加元素
数组的维度在定义时已经确定,如果我们想更改数组的长度,只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组中去
我们也无法像
vector
那样使用size
函数直接获取数组的维度:如果是字符数组,可以调用strlen
函数得到字符串的长度;如果是其他数组,只能使用sizeof(array) / sizeof(array[0])
的方式计算数组的维度
数组在内存空间的地址是连续的
数组的元素是不能删的,只能覆盖
严格来讲,
vector
是容器,不是数组
# 定义和初始化内置数组
数组是一种复合类型,数组的声明形如 int a[d]
,其中 a
是数组的名字, d
是数组的维度, int
是数组所存放对象的类型
维度,即数组中元素的个数,因此 必须大于 0 ,必须是一个常量表达式
unsigned cnt = 42; // not a constant expression | |
constexpr unsigned sz = 42; // constant expression, due to constexpr | |
int arr[10]; // array of ten ints | |
int *parr[sz]; // array of 42 pointers to int | |
string bad[cnt]; // error: cnt is not a constant expression | |
string strs[get_size()]; // ok if get_size is constexpr, error otherwise |
默认情况下,数组的元素被默认初始化
和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值
定义数组时必须指定数组的类型,不允许使用 auto
关键字推断类型
数组的元素应为对象,因此,不存在引用数组
# 显式初始化数组元素
可以对数组的元素进行列表初始化,此时,允许忽略数组的维度
- 如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来
- 如果指明了维度,那么初始值的总数量不应该超出指定的维度
- 如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值
const unsigned sz = 3; // constant expression | |
int ia1[sz] = {0,1,2}; // array of three ints with values 0, 1, 2 | |
int a2[] = {0, 1, 2}; // an array of dimension 3 | |
int a3[5] = {0, 1, 2}; // equivalent to a3[] = {0, 1, 2, 0, 0} | |
string a4[3] = {"hi", "bye"}; // same as a4[] = {"hi", "bye", ""} | |
int a5[2] = {0,1,2}; // error: too many initializers |
# 字符数组的特殊性
可以用字符串字面值对字符数组数组初始化,但一定要注意,字符串字面值 的结尾处还有一个 空字符 ,这个空字符也会被拷贝到字符数组中去
单引号内的是字符字面值,双引号内的是字符串字面值
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符(字符字面值) | |
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符(字符字面值) | |
char a3[] = "C++"; // 含有空字符(字符串字面值) | |
const char a4[6] = "Daniel"; // 错误:没有空间存放空字符(字符串字面值) |
# 不允许拷贝和赋值
无法直接通过某一个数组的数组名来初始化其他数组或给其他数组赋值
int a[] = {0, 1, 2}; // array of three ints | |
int a2[] = a; // error: cannot initialize one array with another | |
a2 = a; // error: cannot assign one array to another |
若需实现数组的拷贝和赋值,需要逐个元素依次拷贝赋值
# 理解复杂的数组声明
可以定义一个存放指针的数组
因为数组本身就是对象,允许定义数组的指针及数组的引用
不存在引用的数组(即,数组的元素不能是引用),但存在数组的引用(可以定义一个引用来绑定数组)
int *ptrs[10]; // ptrs 是含有 10 个元素(整型指针)的数组(注:[] 优先级高于 *) | |
int &refs[10]; // 错误:引用不是对象,不存在引用的数组(注:[] 优先级高于 & amp;,首先判断 refs 是一个维度为 10 的数组,数组的元素的类型是 int &,即,引用。由于引用不是对象,故语法错误) | |
int (*Parray)[10] = &arr; // Parray 指向一个含有 10 个整数的数组(注:() 优先级高于 [],首先分析 (*Parray) 可知 Parray 是一个指针,然后考虑右边的 [10],可知 Parray 指向一个维度为 10 的数组,最后观察左边,知道数组中的元素为 int) | |
int (&arrRef)[10] = arr; // arrRef 引用一个含有 10 个整数的数组(注:() 优先级高于 [],arrRef 是一个引用,所绑定的对象是一个维度为 10 的数组,数组中的元素为 int) |
默认情况下,类型修饰符从右向左依次绑定
对于 ptrs
来说,从右向左理解其含义比较简单:
- 首先,定义的是一个大小为 10 的数组,数组的名字是
ptrs
- 然后可以知道,数组中存放的是指向
int
的指针
但对于 Parray
而言,由于其定义中包含 ()
括号,需要按照由内向外的阅读顺序来理解 Parray
的含义:
- 首先是圆括号括起来的部分,
*Parray
意味着Parray
是个指针 - 接下来观察右边可以知道,
Parray
指向一个维度为 10 的数组 - 最后观察左边,数组中的元素是
int
- 因此,
Parray
是一个指针,指向一个int
数组,其中,数组包含 10 个元素
同理,对于 int (&arrRef)[10]
,我们按照由内向外的顺序阅读: (&arrRef)
表示 arrRef
是一个引用,它引用的对象是一个维度为 10 的数组,数组中元素的类型是 int
更复杂的:
int *(&arry)[10] = ptrs; //array 是数组的引用,该数组包含 10 个指向 int 的指针 |
首先, ()
优先级高于 []
,知道 arry
是一个引用;其次, []
优先级高于 *
,结合右边可知, arry
引用的对象是一个维度为 10 的数组;最后观察左边知道,数组元素的类型是指向 int
的指针
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读
# 访问数组元素
数组的元素也能使用范围 for
或下标运算符来访问
注意:数组的索引从 0 开始
在使用数组下标的时候,通常将其定义为 size_t
类型: size_t
是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小
size_t
类型定义在 cstddef
头文件中,这个文件是 C 标准库头文件 stddef.h
的 C++ 版本
数组除了大小固定这一特点外,其他用法与 vector
基本类似
// count the number of grades by clusters of ten: 0--9, 10--19, ... 90--99, 100 | |
unsigned scores[11] = {}; // 11 buckets, all value initialized to 0 | |
unsigned grade; | |
while (cin >> grade) { | |
if (grade <= 100) | |
++scores[grade / 10]; // increment the counter for the current cluster | |
} |
当需要遍历数组的所有元素时,最好的办法也是使用范围 for
for (auto i : scores) // for each counter in scores | |
cout << i << " "; // print the value of that counter | |
cout << endl; |
数组的下标应该大于等于 0 并且小于数组的大小
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误
# 指针和数组
使用数组名字时,编译器一般都会自动将其替换为 一个指向数组首元素的指针
string nums[] = {"one", "two", "three"}; // array of strings | |
string *p = &nums[0]; // p points to the first element in nums | |
string *p2 = nums; // equivalent to p2 = &nums[0] | |
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints | |
auto ia2(ia); // ia2 is an int* that points to the first element in ia | |
ia2 = 42; // error: ia2 is a pointer, and we can't assign an int to a pointer |
尽管 ia
是由 10 个整数构成的数组,但是,当使用 ia
作为初始值时,编译器实际执行的初始化过程类似于下面的形式:
auto ia2(&ia[0]); // now it's clear that ia2 has type int* |
当使用 decltype
关键字时,上述转换不会发生,即, decltype(ia)
返回的类型是 由 10 个整数构成的数组
// ia3 is an array of ten ints | |
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ints, so is ia3 | |
int *p = ia; | |
ia3 = p; // error: can't assign an int* to an array | |
ia3[4] = i; // ok: assigns the value of i to an element in ia3 |
# 指向数组元素的指针也是迭代器
vector
和 string
的迭代器支持的运算,数组的指针全都支持
int arr[] = {0,1,2,3,4,5,6,7,8,9}; | |
int *p = arr; // p points to the first element in arr | |
++p; // p points to arr[1] |
因此,使用数组的指针也能遍历数组中的元素。前提是要先获取到指向数组第一个元素的指针,以及 指向数组尾元素下一位置的指针
-
通过数组名字或者数组中首元素的地址,能得到指向首元素的指针
-
可以设法获取数组尾元素之后的那个并不存在的元素的地址,从而得到指向数组尾元素的下一位置的指针
int *e = &arr[10]; // pointer just past the last element in arr
这里使用下标运算符索引了一个不存在的元素(
arr
有 10 个元素,尾元素所在位置的索引是 9),这个不存在的元素就是用来提供其地址以初始化e
可改写此前的循环,令其输出 arr
的全部元素:
for (int *b = arr; b != e; ++b) | |
cout << *b << endl; // print the elements in arr |
尾后指针不指向具体的元素,因此,不能对尾后指针执行解引用或递增的操作
尽管能计算得到尾后指针,但这种用法极易出错
# 标准库函数 begin 和 end
C++ 11 新标准引入了两个名为 begin
和 end
的函数
begin
和 end
两个函数定义在 iterator
头文件中
#include<iterator> |
这两个函数与容器中的两个同名成员功能类似,但是,由于数组不是类类型,这两个函数并不是数组的成员函数
因此,调用 begin
和 end
函数时,需要将数组作为它们的参数,例如:
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints | |
int *beg = begin(ia); // pointer to the first element in ia | |
int *last = end(ia); // pointer one past the last element in ia |
//pbeg 指向 arr 第一个元素,end 指向尾后 | |
int *pbeg = begin(arr), *pend = end(arr); | |
// 寻找第一个负数 | |
while (pbeg != pend && *pbeg >= 0) | |
++pbeg; |
# 指针运算
指向数组元素的指针可以执行 迭代器 中的所有迭代器运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针上和用在迭代器上意义完全一致
注意:给指针加上一个整数,需保证新指针仍指向同一数组的某元素,或者指向同一数组的尾元素的下一位置
constexpr size_t sz = 5; // #include<cstddef> | |
int arr[sz] = {1,2,3,4,5}; | |
int *ip = arr; // equivalent to int *ip = &arr[0] | |
int *ip2 = ip + 4; // ip2 points to arr[4], the last element in arr | |
// ok: arr is converted to a pointer to its first element; p points one past the end of arr | |
int *p = arr + sz; // use caution -- do not dereference! | |
int *p2 = arr + 10; // error: arr has only 5 elements; p2 has undefined value |
和迭代器一样,两个指针相减的结果是它们之间的距离,其中,这两个指针必须指向同一个数组当中的元素
两个指针相减的结果,其类型是一种名为 ptrdiff_t
的标准库类型。和 size_t
一样, ptrdiff_t
也是一种定义在 cstddef
头文件中的机器相关的类型。两个指针相减的差值可能为负值,所以, ptrdiff_t
是一种带符号类型
auto n = end(arr) - begin(arr); // n is 5, the number of elements in arr |
只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较
int *b = arr, *e = arr + sz; | |
while (b < e) { | |
// use *b | |
++b; | |
} |
如果两个指针分别指向不相关的对象,则不能比较它们
int i = 0, sz = 42; | |
int *p = &i, *e = &sz; | |
// undefined: p and e are unrelated; comparison is meaningless! | |
while (p < e) |
# 解引用和指针运算的交互
指针加上一个整数所得结果依然是一个指针,如果该指针指向了一个元素,则允许解引用该指针
int ia[] = {0,2,4,6,8}; // array with 5 elements of type int | |
int last = *(ia + 4); // ok: initializes last to 8, the value of ia[4] |
表达式 *(ia + 4)
计算 ia
前进 4 个元素后的新地址,解引用该结果指针的效果等价于表达式 ia[4]
相比之下,下列代码少了一个括号,故而是先解引用 ia
,再给解引用的结果加 4
last = *ia + 4; // ok: last = 4, equivalent to ia[0] + 4 |
# 下标和指针
对数组执行下标运算,其实是对指向数组元素的指针执行下标运算
int ia[] = {0,2,4,6,8}; // array with 5 elements of type int | |
int i = ia[2]; // ia is converted to a pointer to the first element in ia | |
// ia[2] fetches the element to which (ia + 2) points | |
int *p = ia; // p points to the first element in ia | |
i = *(p + 2); // equivalent to i = ia[2] |
只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),就可以执行下标运算:
int *p = &ia[2]; // p points to the element indexed by 2 | |
int j = p[1]; // p[1] is equivalent to *(p + 1), | |
// p[1] is the same element as ia[3] | |
int k = p[-2]; // p[-2] is the same element as ia[0] |
注意,这里的 p
是指向 ia[2]
的指针,对指针 p
执行下标运算时,相当于给指针加上(减去)某整数值,即,对指针进行移动。例如上例的 p[-2]
等价于 *(p - 2)
,也就是 ia[0]
虽然标准库类型 string
和 vector
也能执行下标运算,但是,标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。即,内置的下标运算符可以处理负值,当然,结果地址必须指向原指针所指数组中的元素(或是同一数组尾元素的下一位置)
# C 风格字符串
字符串字面值是一种通用结构的实例,这种结构是 C++ 由 C 继承而来的 C 风格字符串(C-stylecharacter string)
尽管 C++ 支持 C 风格字符串,但在 C++ 程序中最好还是不要使用它们
这是因为 C 风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因
C 风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法
# C 标准库 string 函数
C 风格字符串的处理函数定义在 cstring
头文件中(C 语言头文件 string.h
的 C++ 版本)
strlen(p)
("len" 是指 "length")strcmp(p1, p2)
("cmp" 是指 "compare")strcat(p1, p2)
("cat" 是指 "concatenate")strcpy(p1, p2)
("cpy" 是指 "copy")
所列函数不负责验证其字符串参数
传入此类函数的指针必须指向以空字符作为结束的数组
char ca[] = {'C', '+', '+'}; // not null terminated | |
cout << strlen(ca) << endl; // disaster: ca isn't null terminated |
此例中, ca
虽然是一个字符数组,但它不是以空字符作为结束的,因此,上述程序将产生未定义的结果( strlen
函数可能会沿着 ca
在内存中的位置不断向前寻找,直到遇到空字符才停下来)
# 比较字符串
比较两个 C 风格字符串的方法 与 比较标准库 string
对象的方法 大相径庭
-
比较标准库
string
对象的时候,用的是普通的关系运算符和相等性运算符string s1 = "A string example";
string s2 = "A different string";
if (s1 < s2) // false: s2 is less than s1
-
如果把这些运算符用在两个 C 风格字符串上,比较的将是指针,而非字符串本身
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if (ca1 < ca2) // undefined: compares two unrelated addresses
正如之前介绍过的,使用数组时,真正用的是指向数组首元素的指针
因此,上面的 if (ca1 < ca2)
实际是在比较两个 const char *
的值,由于这两个指针指向的并非同一对象,所以将得到未定义的结果
要想比较两个 C 风格字符串,需要调用 strcmp
函数
- 如果两个字符串相等,
strcmp
返回 0 - 如果前面的字符串较大,返回正值
- 如果后面的字符串较大,返回负值
if (strcmp(ca1, ca2) < 0) // same effect as string comparison s1 < s2 |
# 目标字符串的大小由调用者指定
连接或拷贝 C 风格字符串也与标准库 string
对象的同类操作差别很大
-
要想把刚刚定义的那两个
string
对象s1
和s2
连接起来,可以直接写成下面的形式:// initialize largeStr as a concatenation of s1, a space, and s2
string largeStr = s1 + " " + s2;
-
如果针对
ca1
和ca2
这两个数组进行同样的操作,就会产生错误。这是因为,表达式ca1 + ca2
试图将两个指针相加,这样的操作没什么意义,也是非法的
正确的方法是,使用 strcat
函数和 strcpy
函数
不过要想使用这两个函数,还必须提供一个用于存放结果字符串的数组,该数组必须足够大以便容纳下结果字符串及末尾的空字符
下面的代码虽然很常见,但是充满了安全风险,极易引发严重错误:
// disastrous if we miscalculated the size of largeStr | |
strcpy(largeStr, ca1); // copies ca1 into largeStr | |
strcat(largeStr, " "); // adds a space at the end of largeStr | |
strcat(largeStr, ca2); // concatenates ca2 onto largeStr |
一个潜在的问题是,在估算 largeStr
所需的空间时,不容易估准,而且, largeStr
所存的内容一旦改变,就必须重新检查其空间是否足够
对大多数应用来说,使用标准库类型
string
要比使用 C 风格字符串更安全、更高效
# 与旧代码的接口
很多 C++ 程序在标准库出现之前就已经写成了,它们肯定没用到 string
和 vector
类型
而且,有一些 C++ 程序实际上是与 C 语言或其他语言的接口程序,当然也无法使用 C++ 标准库
因此,现代的 C++ 程序不得不与那些充满了数组和 / 或 C 风格字符串的代码衔接
# 混用 string 对象和 C 风格字符串
可以使用字符串字面值来初始化 string
对象
string s("Hello World"); // s holds Hello World |
更一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代
- 允许使用以空字符结束的字符数组来初始化
string
对象、为string
对象赋值 - 在
string
对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是) - 在
string
对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧的运算对象
上述性质反过来就不成立了:如果程序的某处需要一个 C 风格字符串,无法直接用 string
对象来代替它。例如,不能用 string
对象直接初始化指向字符的指针
为了完成该功能, string
专门提供了一个名为 c_str
的成员函数
char *str = s; // error: can't initialize a char* from a string | |
const char *str = s.c_str(); // ok |
c_str
函数的返回值是一个 C 风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个 string
对象的一样
c_str
函数的结果指针的类型是 const char *
,从而确保我们不会改变字符数组的内容
注意,我们无法保证 c_str
函数返回的数组一直有效,如果后续操作改变了 s
的值,可能会让之前返回的数组失去效用
如果程序想一直都能使用
c_str()
函数返回的数组,最好将该数组重新拷贝一份
# 使用数组初始化 vector 对象
此前介绍过,不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用 vector
对象来初始化数组
但是,允许使用数组来初始化 vector
对象,只需 指明要拷贝区域的首元素地址和尾后地址 即可
int int_arr[] = {0, 1, 2, 3, 4, 5}; | |
// ivec has six elements; each is a copy of the corresponding element in int_arr | |
vector<int> ivec(begin(int_arr), end(int_arr)); | |
// copies three elements: int_arr[1], int_arr[2], int_arr[3] | |
vector<int> subVec(int_arr + 1, int_arr + 4); |
# 尽量使用标准库类型而非数组
使用指针和数组很容易出错
- 一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与烦琐细节有关的错误
- 其他问题则源于语法错误,特别是声明指针时的语法错误
现代的 C++ 程序应当尽量使用 vector
和迭代器,避免使用内置数组和指针。应该尽量使用 string
,避免使用 C 风格的基于数组的字符串
# 多维数组
严格来说,C++ 语言中没有多维数组,通常所说的多维数组其实是 数组的数组
当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小
int ia[3][4]; // 大小为 3 的数组,每个元素是含有 4 个整数的数组 | |
// array of size 10; each element is a 20-element array whose elements are arrays of 30 ints | |
int arr[10][20][30] = {0}; // initialize all elements to 0 |
对于二维数组来说,常把第一个维度称作行,第二个维度称作列
# 多维数组的初始化
允许使用花括号括起来的一组值初始化多维数组
int ia[3][4] = { // three elements; each element is an array of size 4 | |
{0, 1, 2, 3}, // initializers for the row indexed by 0 | |
{4, 5, 6, 7}, // initializers for the row indexed by 1 | |
{8, 9, 10, 11} // initializers for the row indexed by 2 | |
}; |
其中,内层嵌套着的花括号并不是必需的
例如下面的初始化语句,形式上更为简洁,完成的功能和上述代码完全一样:
// equivalent initialization without the optional nested braces for each row | |
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; |
如果仅仅想初始化每一行的第一个元素,通过如下的语句即可:
// explicitly initialize only element 0 in each row | |
int ia[3][4] = { {0}, {4}, {8} }; |
其他 未列出的元素执行默认值初始化
在这种情况下,如果再省略掉内层的花括号,结果就大不一样了:
// explicitly initialize row 0; the remaining elements are value initialized | |
int ix[3][4] = {0, 3, 6, 9}; |
此时初始化的是第一行的 4 个元素,其他元素被初始化为 0
# 多维数组的下标引用
可以使用下标运算符来访问多维数组的元素,数组的每个维度对应一个下标运算符
如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素
如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组
// assigns the first element of arr to the last element in the last row of ia | |
ia[2][3] = arr[0][0][0]; | |
int (&row)[4] = ia[1]; // binds row to the second four-element array in ia |
程序中经常会用到两层嵌套的 for
循环来处理多维数组的元素,例如:
constexpr size_t rowCnt = 3, colCnt = 4; | |
int ia[rowCnt][colCnt]; // 12 uninitialized elements | |
// for each row | |
for (size_t i = 0; i != rowCnt; ++i) { | |
// for each column within the row | |
for (size_t j = 0; j != colCnt; ++j) { | |
// assign the element's positional index as its value | |
ia[i][j] = i * colCnt + j; | |
} | |
} |
在上例中,外层的 for
循环遍历 ia
的所有元素,注意,这里的元素是一维数组;内层的 for
循环则遍历那些一维数组的整数元素
# 使用范围 for 语句处理多维数组
size_t cnt = 0; | |
for (auto &row : ia) // for every element in the outer array | |
for (auto &col : row) { // for every element in the inner array | |
col = cnt; // give this element the next value | |
++cnt; // increment cnt | |
} |
要改变数组元素的值,所以得把控制变量 row
和 col
声明成引用类型。第一个 for
循环遍历 ia
的所有元素(大小为 4 的数组),因此 row
的类型就是整数数组的引用;第二个 for
循环遍历那些 4 元素数组中的某一个,因此, col
的类型是整数的引用
在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因促使我们这么做
举一个例子,考虑如下的循环:
for (const auto &row : ia) // for every element in the outer array | |
for (auto col : row) // for every element in the inner array | |
cout << col << endl; |
这个循环中并没有任何写操作,但我们依然将外层循环的控制变量声明成了引用类型,这是 为了避免数组被自动转成指针
假设不用引用类型,则循环如下述形式:
for (auto row : ia) | |
for (auto col : row) |
程序将无法通过编译
这是因为:第一个循环遍历 ia
的元素实际是大小为 4 的数组,由于 row
不是引用类型,编译器初始化 row
时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针,这样得到的 row
的类型就是 int*
,显然内层的循环就不合法了
使用范围 for 语句处理多维数组时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型
# 指针和多维数组
使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针
多维数组实际上是数组的数组,因此,由多维数组名转换得来的指针,实际上是指向第一个内层数组的指针
int ia[3][4]; // array of size 3; each element is an array of ints of size 4 | |
int (*p)[4] = ia; //p points to an array of four ints(p 指向 ia 的第一行) | |
p = &ia[2]; //p now points to the last element in ia(p 指向 ia 的第三行) |
以 int (*p)[4] = ia
为例:
- 对于
=
左侧,首先可以根据(*p)
知道p
是一个指针,由int [4]
可以知道p
指向的是一个包含 4 个整数的数组 - 对于等号右侧,
ia
返回的是&ia[0]
,即ia[0]
的地址 - 因此,
p
指向的是ia[0]
,即ia
的第一行
注意,在上述声明中,圆括号必不可少
int *ip[4]; //array of pointers to int([] 优先级高于 * ,故而 ip 是一个数组,其元素为整形指针) | |
int (*ip)[4]; // pointer to an array of four ints |
随着 C++ 11 新标准的提出,可以使用 auto
或者 decltype
,以尽可能避免在数组前面加上一个指针类型
// print the value of each element in ia, with each inner array on its own line | |
// p points to an array of four ints | |
for (auto p = ia; p != ia + 3; ++p) { | |
// q points to the first element of an array of four ints; that is, q points to an int | |
for (auto q = *p; q != *p + 4; ++q) | |
cout << *q << ' '; | |
cout << endl; | |
} |
在上例中,外层的 for
循环首先声明一个指针 p
,令其指向 ia
的第一个内层数组,然后依次迭代直到 ia
的全部 3 行都处理完为止,其中,递增运算 ++p
负责将指针 p
移动到 ia
的下一行
内层的 for
循环负责输出内层数组所包含的值。它首先令指针 q
指向 p
当前所在行的第一个元素( p
指向某一个内层数组,则 *p
是一个数组,在使用 *p
时,会被自动地转换成指向首元素的指针)
使用 标准库函数 begin
和 end
也能实现同样的功能,而且看起来更简洁一些
// p points to the first array in ia | |
for (auto p = begin(ia); p != end(ia); ++p) { | |
// q points to the first element in an inner array | |
for (auto q = begin(*p); q != end(*p); ++q) | |
cout << *q << ' '; // prints the int value to which q points | |
cout << endl; | |
} |
# 类型别名简化多维数组的指针
使用类型别名(处理类型 - 类型别名)能让 读、写和理解一个指向多维数组的指针 变得简单一些
例如:
using int_array = int[4]; // new style type alias declaration; equivalent typedef declaration: typedef int int_array[4]; | |
// print the value of each element in ia, with each inner array on its own line | |
for (int_array *p = ia; p != ia + 3; ++p) { | |
for (int *q = *p; q != *p + 4; ++q) | |
cout << *q << ' '; | |
cout << endl; | |
} |
程序将类型 “4 个整数组成的数组” 命名为 int_array
,用类型名 int_array
定义外层循环的控制变量(由 *p
知道 p
是一个指针,指向一个包含 4 个整数的数组, q
是一个指向整数的指针)
# 术语表
begin
:是 string
和 vector
的成员,返回指向第一个元素的迭代器。也是一个标准库函数,输入一个数组,返回指向该数组首元素的指针
缓冲区溢出(buffer overflow)
:一种严重的程序故障,主要的原因是试图通过一个越界的索引访问容器内容,容器类型包括 string
、 vector
和 数组
等
C风格字符串(C-style string)
:以空字符结束的字符数组。字符串字面值是 C 风格字符串,C 风格字符串容易出错
类模板(class template)
:用于创建具体类类型的模板。要想使用类模板,必须提供关于类型的辅助信息。例如,要定义一个 vector
对象需要指定元素的类型,例如: vector<int>
包含 int
类型的元素
编译器扩展(compiler extension)
:某个特定的编译器为 C++ 语言额外增加的特性。基于编译器扩展编写的程序不易移植到其他编译器上
容器(container)
:是一种类型,其对象容纳了一组给定类型的对象。 vector
是一种容器类型
拷贝初始化(copy initialization)
:使用赋值号( =
)的初始化形式。新创建的对象是初始值的一个副本
difference_type
:由 string
和 vector
定义的一种带符号整数类型,表示两个迭代器之间的距离
直接初始化(direct initialization)
:不使用赋值号( =
)的初始化形式
empty
:是 string
和 vector
的成员,返回一个布尔值。当对象的大小为 0 时返回真,否则返回假
end
:是 string
和 vector
的成员,返回一个尾后迭代器。也是一个标准库函数,输入一个数组,返回指向该数组尾元素的下一位置的指针
getline
:在 string
头文件中定义的一个函数,以一个 istream
对象和一个 string
对象为输入参数。该函数首先读取输入流的内容直到遇到换行符停止,然后将读入的数据存入 string
对象,最后返回 istream
对象。其中换行符读入但是不保留
索引(index)
:是下标运算符使用的值。表示要在 string
对象、 vector
对象或者数组中访问的一个位置
实例化(instantiation)
:编译器生成一个指定的模板类或函数的过程
迭代器(iterator)
:是一种类型,用于访问容器中的元素或者在元素之间移动
迭代器运算(iterator arithmetic)
:是 string
或 vector
的迭代器的运算:迭代器与整数相加或相减得到一个新的迭代器,与原来的迭代器相比,新迭代器向前或向后移动了若干个位置。两个迭代器相减得到它们之间的距离,此时它们必须指向同一个容器的元素或该容器尾元素的下一位置
以空字符结束的字符串(null-terminatedstring)
:是一个字符串,它的最后一个字符后面还跟着一个空字符( '\0'
)
尾后迭代器(off-the-end iterator)
: end
函数返回的迭代器,指向一个并不存在的元素,该元素位于容器尾元素的下一位置
指针运算(pointer arithmetic)
:是指针类型支持的算术运算。指向数组的指针所支持的运算种类与迭代器运算一样
prtdiff_t
:是 cstddef
头文件中定义的一种与机器实现有关的带符号整数类型,它的空间足够大,能够表示数组中任意两个指针之间的距离
push_back
:是 vector
的成员,向 vector
对象的末尾添加元素
范围for语句(range for)
:一种控制语句,可以在值的一个特定集合内迭代
size
:是 string
和 vector
的成员,分别返回字符的数量或元素的数量。返回值的类型是 size_type
size_t
:是 cstddef
头文件中定义的一种与机器实现有关的无符号整数类型,它的空间足够大,能够表示任意数组的大小
size_type
:是 string
和 vector
定义的类型的名字,能存放下任意 string
对象或 vector
对象的大小。在标准库中, size_type
被定义为无符号类型
string
:是一种标准库类型,表示字符的序列
using声明(using declaration)
:令命名空间中的某个名字可被程序直接使用。 using 命名空间::名字
;上述语句的作用是令程序可以直接使用名字,而无须写它的前缀部分 命名空间::
值初始化(value initialization)
:是一种初始化过程。内置类型初始化为 0,类类型由类的默认构造函数初始化。只有当类包含默认构造函数时,该类的对象才会被值初始化。对于容器的初始化来说,如果只说明了容器的大小而没有指定初始值的话,就会执行值初始化。此时编译器会生成一个值,而容器的元素被初始化为该值
vector
:是一种标准库类型,容纳某指定类型的一组元素
++运算符(++ operator)
:是迭代器和指针定义的递增运算符。执行 “加 1” 操作使得迭代器指向下一个元素
[ ]运算符([ ] operator)
:下标运算符。 obj[j]
得到容器对象 obj
中位置 j
的那个元素。索引从 0 开始,第一个元素的索引是 0,尾元素的索引是 obj.size() - 1
。下标运算符的返回值是一个对象。如果 p
是指针、 n
是整数,则 p[n]
与 *(p + n)
等价
->运算符(->operator)
:箭头运算符,该运算符综合了解引用操作和点操作。 a->b
等价于 (*a).b
<<运算符(<<operator)
:标准库类型 string
定义的输出运算符,负责输出 string
对象中的字符
>>运算符(>>operator)
:标准库类型 string
定义的输入运算符,负责读入一组字符,遇到空白停止,读入的内容赋给运算符右侧的运算对象,该运算对象应该是一个 string
对象
!运算符(! operator)
:逻辑非运算符,将它的运算对象的布尔值取反。如果运算对象是假,则结果为真,如果运算对象是真,则结果为假
&&运算符(&&operator)
:逻辑与运算符,如果两个运算对象都是真,结果为真。只有当左侧运算对象为真时才会检查右侧运算对象
||运算符(|| operator)
:逻辑或运算符,任何一个运算对象是真,结果就为真。只有当左侧运算对象为假时才会检查右侧运算对象
参考:C++ Primer 中文版(第 5 版)