【C++】【C++ Primer】3-字符串、向量和数组
string和vector是两种最重要的标准库类型。string支持可变长的字符序列,vector表示某种给定类型对象的可变长序列。
迭代器是另一种标准库类型,它是string和vector的配套类型,常用于访问string中的字符或vector中的元素。
内置数组是更基础的类型,string和vector都是对它的某种抽象。数组的实现与硬件密切相关,因此和string与vector相比,数组在灵活性上稍显不足。
1 命名空间的using声明
在之前的博客中,我们用到的库函数基本都属于命名空间std,程序也使用作用域操作符显式地标识了这一点。作用域操作符(::)的含义是:编译器应从操作符左侧所示的作用域中寻找右侧的名字。譬如std::cin表示使用命名空间std中的名字cin。
上述方法比较繁琐,我们可以通过更简单的途径使用命名空间中的成员。
1.1 using声明(using declaration)
using声明具有如下形式:
using namespace::name;
使用using声明后,就无需再使用专门的前缀,可以直接访问命名空间中的名字。
每个using声明引入命名空间中的一个成员。每个名字都必须有自己的声明语句。
头文件中不应使用using声明。头文件中的内容会拷贝到引用它的文件中,如果头文件里包含using声明,就会将using声明添加到这些文件中,可能导致意外的冲突。
1.2 第二种方法候补
2 标准库类型string
标准库类型string定义在命名空间std中,表示可变长的字符序列。使用string类型必须先包含string头文件。
#include <string>
using std::string;
2.1 定义和初始化string对象
2.1.1 默认初始化
string对象会被默认初始化为空,即该string对象中没有任何字符。
2.1.2 拷贝初始化
如果使用等号(=)初始化变量,实际执行的就是拷贝初始化(copy initialization)。编译器会将等号右侧的初始值拷贝到新创建的对象中。
2.1.3 直接初始化
不使用等号初始化变量,执行的是直接初始化(direct initialization)。
2.1.4 初始化string对象的方式
初始值只有一个时,使用直接初始化和拷贝初始化均可。如果提供了一个字符串字面值,该字面值中除了最后的空字符,其余字符都会被拷贝到新创建的string对象中。
如果提供的是一个数字和一个字符,则string对象的内容是给定字符重复给定次数形成的序列。譬如代码示例中的s4这样。这种情况通常只能使用直接初始化的方式。
初始化string对象的方式 | |
---|---|
代码 | 说明 |
string s1; | 默认初始化,s1是空字符串 |
string s2(s1); | s2是s1的副本 |
string s2 = s1; | 等价于s2(s1) |
string s3("value"); | s3是字面值"value"的副本,只是不包含字面值最后的空字符 |
string s3 = "value"; | 等价于s3("value") |
string s4(n, 'c'); | 把s4初始化为由连续n个字符c组成的串 |
2.2 string对象上的操作
C++中的类可以定义成员函数,也可以重定义<<、+等运算符在该类对象上的操作。
初始化string对象的方式 | |
---|---|
代码 | 说明 |
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个字符的引用 |
s1 + s2 | 返回s1和s2连接后的结果 |
s1 = s2 | 用s2的副本替代s1中原来的字符 |
s1 == s2、s1 != s2 | 如果s1和s2中所含的字符完全一样则相等,大小写敏感 |
<, <=, >, >= | 根据字符在字典中的顺序进行比较,大小写敏感 |
2.2.1 使用iostream读写string对象
在执行读取操作时,string对象会自动忽略开头的空白字符,从第一个真正的字符读起,直至遇到下一处空白为止。譬如以下代码,输入" Hello world! “,输出是"Hello”,并不包含任何空白字符。
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s;
cin >> s;
cout << s << endl;
return 0;
}
string对象的输入输出操作也是返回运算符左侧的对象,因此可以将多个输入或多个输出连在一起。譬如以下代码,输入" Hello world! “,输出是"Helloworld!”。
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s1, s2;
cin >> s1 >> s2;
cout << s1 << s2 << endl;
return 0;
}
使用iostream可以读取未知数量的string对象。如果流有效(没有遇到EOF或非法输入),就执行循环体,直至遇到EOF或无效输入。
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s;
while (cin >> s) {
cout << s << endl;
}
return 0;
}
2.2.2 使用getline读取一整行
如果希望读到的字符串中包含输入的空白字符,应使用getline函数来替代>>运算符。
istream& getline (istream& is, string& str);
从以上函数声明可以看出,getline函数的参数是一个输入流和一个string对象。函数从给定的输入流中读入内容,直至遇到换行符为止(换行符也被读入)。然后将读到的内容存到string对象中(丢弃换行符)。如果输入的起始就是换行符,则读到的结果是空string。出发getline的那个换行符实际上被丢弃掉了。
getline也会返回它的流参数,所以可以用getline的结果作为条件:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s;
while (getline(cin, s)) {
cout << s << endl;
}
return 0;
}
2.2.3 string的empty和size操作
empty函数根据string对象是否为空返回布尔类型的结果。以下代码使用empty函数,只输出非空行:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s;
while (getline(cin, s)) {
if (!s.empty()) {
cout << s << endl;
}
}
return 0;
}
size函数返回string对象的长度。以下代码使用size函数,只输出长度超过80个字符的行:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s;
while (getline(cin, s)) {
if (s.size() > 80) {
cout << s << endl;
}
}
return 0;
}
2.2.4 string::size_type类型
size函数的返回值是string::size_type类型,而非int或unsigned。在具体使用时,通过作用域操作符来表明size_type是在类string中定义的。所有用于存放string类的size函数返回值的变量,都应定义为string::size_type类型。
string类即其他大多数标准库类型都定义了几种配套类型,这些类型体现了标准库类型与机器无关的特性。尽管我们不清楚string::size_type的细节,但可以确定它是无符号类型,而且足够存放下任何string对象的大小。
由于size函数返回无符号整数,所以切记不能和带符号数混用。假设n是一个具有负值的int类型变量,则s.size() < n的结果几乎可以肯定是true。因为负值n会自动转换成一个比较大的无符号值。
2.2.5 比较string对象
string对象相等意味着它们的长度相同而且包含的字符也全都相同。
如果两个string对象长度不同,但较短string对象的每个字符都和较长string对象对应位置上的字符相同,则称较短string对象小于较长string对象。
如果两个string对象在某些位置上不同,则string对象比较的结果其实是string对象中第一对相异字符比较的结果。
2.2.6 两个string对象相加
两个string对象相加,会把两个对象的内容串联起来,得到一个新的string对象。
string s1 = "Hello, ", s2 = "world";
string s3 = s1 + s2; // s3的内容是Hello, world
2.2.7 字面值和string对象相加
C++中的字符串字面值并不是标准库类型string的对象,但标准库允许把字符字面值和字符串字面值转换成string对象,所以需要string对象的地方就可以用这两种字面值代替。但将string对象和字符字面值、字符串字面值混在一条语句中使用时,每个加号运算符两侧的运算对象至少有一个是string。
2.3 处理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 |
2.3.1 处理string中的每个字符——使用基于范围的for语句
for (declaration : expression)
statement
expression表示序列,declaration是一个变量。每次迭代,declaration会被初始化为expression部分的下一个元素值。
譬如以下代码,利用基于范围的for语句统计string中符号的个数:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s("Hello world!!!");
decltype(s.size()) punct_cnt = 0;
for (auto c : s) {
if (ispunct(c)) {
++punct_cnt;
}
}
cout << punct_cnt << " punctuation characters in " << s << endl;
return 0;
}
如果想改变string对象中字符的值,必须把循环变量定义成引用类型。
譬如以下代码,将string变为大写:
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
int main()
{
string s("Hello world!!!");
for (auto &c : s) {
c = toupper(c);
}
cout << s << endl;
return 0;
}
2.3.2 只处理string中的部分字符
访问string对象中的单个字符有两种方式:一种是使用下标,另一种是后面要讲的迭代器。
下标运算符接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置。返回值是该位置上字符的引用。
下标的值称作“下标”或“索引”,只要一个表达式是整型值,就可以作为索引。如果某个索引是带符号类型的值,会自动转换为string::size_type类型的无符号类型。
在访问指定字符之前,要先检查这个位置上是否确实有值。
以下代码将s的第一个词改为大写:
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
int main()
{
string s("some thing");
for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) {
s[index] = toupper(s[index]);
}
cout << s << endl;
return 0;
}
3 标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中每个对象都有与之对应的索引,用于访问对象。
因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
要想使用vector,必须包含适当的头文件:
#include <vector>
using std::vector;
C++语言既有类模板(class template),也有函数模板,其中vector是一个类模板。我们后续才会学习自定义模板,现在只是使用模板。模板本身不是类或函数,可以将其视作编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation)。使用模板时,要指出编译器应把类或函数实例化成何种类型。对于类模板来说,我们要提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式是在模板名字后面跟一对尖括号,在括号内放上信息。
以下代码中,编译器根据类模板vector生成了三种不同的类型vector<int>、vector<Sales_item>、vector<vector<string>>:
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象
vector能容纳绝大多数类型的对象作为其元素,但引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数(非引用)内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector。
在早期版本的C++标准中,如果vector的元素还是vector,必须在外层vector对象的右尖括号和元素类型之间添加一个空格:
vector<vector<int> >
3.1 定义和初始化vector对象
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包含了n个重复地执行了值初始化的对象
vector<T> v5{a, b, c, ...}; // v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5 = {a, b, c, ...}; // 等价于v5{a, b, c, ...}
3.1.1 vector对象的默认初始化
vector对象的默认初始化会创建一个指定类型的空vector。vector最常见的使用方式就是先定义一个空vector,运行时获取到元素的值再逐一添加。
vector<string> svec; // 默认初始化,svec不含任何元素
3.1.2 vector对象的拷贝初始化、直接初始化
如果用一个vector去初始化另一个vector,类型必须相同。
vector<int> ivec;
vector<int> ivec2(ivec);
vector<int> ivec3 = ivec;
3.1.3 列表初始化vector对象
C++11标准还提供了列表初始化vector对象的方法,即用花括号括起来的初始元素值赋给vector对象。
vector<string> articles = {"a", "an", "the"};
如果提供的是初始元素值的列表,则只能用花括号进行列表初始化,不能放在圆括号里:
vector<string> v1{"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误
3.1.4 创建指定数量的元素
可以用vector对象容纳的元素数量和所有元素的同一初始值来初始化vector对象:
vector<int> ivec(10, -1); // 10个int类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!"); // 10个string类型的元素,每个都被初始化为"hi!"
3.1.5 值初始化
我们可以只提供vector对象容纳的元素数量,不提供初始值。此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。具体的初值由vector中元素的类型决定。
如果vector对象的元素是内置类型,譬如int,则元素初始值自动设成0。如果元素是某种类类型,譬如string,则元素由类默认初始化。
vector<int> ivec(10); // 10个元素,每个都初始化为0
vector<string> svec(10); // 10个元素,每个都是空string对象
这种初始化方式有两个特殊限制:
- 有些类要求必须明确地提供初始值,此时不能用值初始化
- 必须使用直接初始化,不能使用拷贝初始化
vector<int> vi = 10; // 错误。vector的值初始化必须使用直接初始化
3.1.6 列表初始值or元素数量
我们可以根据传递初始值时用的是花括号还是圆括号来确定初始化的真是含义。
- 圆括号:提供的值用于构造vector对象
- 花括号:会尽量试图列表初始化vector对象,无法执行列表初始化时才考虑其他初始化方式。简而言之,如果初始化时使用了花括号,但提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了
vector<int> v1(10); // v1有10个元素,值均为0
vector<int> v2{10}; // v2有1个元素,值为10
vector<int> v3(10, 1); // v3有10个元素,均初始化为1
vector<int> v4{10, 1}; // v4有2个元素,值分别为10和1
vector<string> v5{"hi"}; // 列表初始化,v5有一个元素,值为hi
vector<string> v6("hi"); // 错误,不能用字符串字面值构建vector对象
vector<string> v7{10}; // v7有10个默认初始化的元素
vector<string> v8{10, "hi"}; // v8有10个值为hi的元素
3.2 向vector对象中添加元素
vector对象直接初始化的方式适用于三种情况:
- 初始值已知且数量较少
- 初始值是另一个vector对象的副本
- 所有元素的初始值都一样
更常见的情况是,创建一个vector对象时并不清楚实际所需的元素个数,元素的值也经常无法确定。即使元素的初值已知,但如果这些值总量较大且不相同,创建vector对象的时候执行初始化操作也过于繁琐。
更好的处理方式是先创建一个空vector,然后在运行时利用vector的成员函数push_back向其中添加元素。push_back负责把一个值当成vector对象的尾元素压到vector对象的尾端。
// 从标准输入中读取单词,将其作为vector对象的元素存储
string word;
vector<string> text;
while (cin >> word) {
text.push_back(word);
}
如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。因为范围for语句体内不应改变其所遍历序列的大小。
3.3 其他vector操作
v.empty() // 如果v中不含有任何元素,返回真。否则返回假
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相等
v1 != v2 //
<, <=, >, >= // 以字典顺序进行比较
3.3.1 vector.empty()
empty检查vector对象是否包含元素,然后返回一个布尔值。
3.3.2 vector.size()
size返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型。注意,要使用size_type类型,首先要指定它是由哪种类型定义的。vector对象的类型必须包含元素的类型。
vector<int>::size_type // 正确
vector::size_type // 错误
3.3.3 vector对象的相等性
只有当元素的值可比较时,vector对象才能被比较。string等定义了自己的相等性运算符和关系运算符,所以可以比较。没有定义相等性运算符和关系运算符的类则不支持比较。
- 如果v1和v2的元素数量相同,且对应位置的元素值相同,则称v1和v2相等。
- 如果两个vector元素的容量不同,但相同位置上的元素值相同,则称元素较少的vector对象小于元素较多的vector对象。
- 如果元素的值有区别,则vector对象的大小关系由第一对相异的元素值的大小关系决定。
3.3.4 计算vector内对象的索引
vector对象的下标从0开始,类型是相应的size_type。使用下标运算符即可获取到指定的元素。
由于只能对确知已存在的元素执行下标操作,所以vector对象和string对象的下标运算符可用于访问已存在的元素,而不能用于添加元素。
4 迭代器介绍
除了使用下标运算符来访问string对象的字符和vector对象的元素,还有另一种更通用的机制可以实现相同目的,这就是迭代器(iterator)。
除了vector之外,标准库还定义了几种容器。所有的标准库容器都支持迭代器,但只有其中少数几种才支持下标运算符。
严格来说,string对象不属于容器类型,但string支持很多与容器类型类似的操作。
迭代器提供了对对象的间接访问,这一点类似于指针类型。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。
迭代器有有效和无效之分。有效迭代器指向某个元素,或指向容器中尾元素的下一位置。其余情况均属无效。
4.1 使用迭代器
4.1.1 获取迭代器
和指针不同,获取迭代器并不使用取地址符。支持迭代器的类会提供返回迭代器的成员函数,譬如begin和end。begin成员负责返回指向第一个元素的迭代器,end成员返回指向容器尾后元素的迭代器。end成员返回的迭代器称作尾后迭代器(off-the-end iteratoro),或简称为尾迭代器(end iterator)。它没有实际含义,只是作为标记,表示我们已经处理完了容器中的所有元素。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
通常来说,我们不清楚,也不在意迭代器准确的类型是什么,而是使用auto关键字来定义变量。
auto b = v.begin(), e = v.end();
4.1.1 迭代器运算符
标准容器迭代器的运算符 | |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素名为mem的成员,等价于(*iter).mem |
++iter | 令iter指向容器中的下一个元素 |
--iter | 令iter指向容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等 |
iter1 != iter2 | 判断两个迭代器是否不等 |
4.1.2 迭代器解引用
和指针类似,可以解引用迭代器来获取它所指示的元素。执行解引用的迭代器必须合法并确实指示某个元素,试图解引用非法迭代器或尾后迭代器都是未定义的行为。
以下代码使用迭代器,将string对象的第一个字母改为大写:
string s("some string");
if (s.begin() != s.end()) { // 确保s非空
auto it = s.begin(); // it指向s的第一个字符
*it = toupper(*it); // 将it指向的字母改为大写
}
4.1.3 判断迭代器相等性
如果两个迭代器指向同一元素或同一容器的尾后迭代器,则称它们相等。否则称这两个迭代器不相等。
4.1.4 将迭代器从一个元素移到另一个元素
迭代器使用递增运算符(++)从一个元素移动到下一个元素。但需注意,end返回的迭代器不实际指示某个元素,所以不能对其进行递增或解引用。
下面的代码利用迭代器,将string对象中的第一个单词改写为大写形式:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
string s = "some string";
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it) {
*it = toupper(*it);
}
cout << s << endl;
return 0;
}
在C语言代码的for循环中,通常使用<进行判断。C++代码的for循环中则更常用!=。这是因为所有标准库容器的迭代器都定义了==和!=,而它们中的大多数都没有定义<运算符。
4.1.5 迭代器类型
通常来说,我们不知道,也无需知道迭代器的精确类型。那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型。
iterator的对象可读可写,const_iterator的对象能读取,但不能修改。如果vector对象或string对象是一个常量,只能使用const_iterator。如果vector对象或string对象不是常量,则iterator和const_iterator均可使用。
vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it2能读写string对象中的元素
vector<int>::const_iterator it3; // it3只能读元素,不能写元素
string::cosnt_iterator it4; // it4只能读字符,不能写字符
4.1.6 begin和end运算符
begin和end返回的具体类型由对象是否是常量决定。如果对象是常量,begin和end返回const_iterator。如果对象不是常量,返回iterator。
但这种默认行为未必满足我们的需求。如果对象只需读操作,那么最好使用const_iterator。因此C++11引入了两个新函数cbegin和cend,不论对象本身是否是常量,这两个函数都返回const_iterator。
4.1.7 解引用与成员访问操作
我们以由字符串组成的vector对象为例,it为其迭代器。需要注意以下代码:
(*it).empty()
*it.empty()
第一行是先对it解引用,解引用的结果再执行点运算符。第二行是先由it执行点运算符,对其结果解引用。
为了简化这种表达式,C++定义了箭头运算符->。以下两行代码作用相同:
it->empty()
(*it).empty()
4.1.8 某些对vector对象的操作会使迭代器失效
vector对象可以动态地增长,这带来了一些副作用:
- 不能在范围for循环中向vector对象添加元素。
- 任何一种可能改变vector对象容量的操作,譬如push_back,都会使该vector对象的迭代器失效。
但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
4.2 迭代器运算
vector与string迭代器支持的运算 | |
---|---|
iter + n | 迭代器加上一个整数值,即将迭代器向前移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置 |
iter - n | 迭代器减去一个整数值,即将迭代器向后移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置 |
iter1 += n | 迭代器加法的复合赋值语句 |
iter1 -= n | 迭代器减法的复合赋值语句 |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离,类型为diffrence_type(带符号) |
>、>=、<、<= | 如果某迭代器指向的容器位置在另一迭代器所指位置之前,则称前者小于后者 |
5 数组
数组是一种类似标准库类型vector的数据结构。相似之处在于,数组也是存放相同类型元素的对象的容器,这些元素本身没有名字,需要通过其所在位置访问。不同之处在于,数组的大小确定不变,不能随意向其中增加元素。
由于数组的大小固定,因此对于某些特殊应用而言,程序运行时性能较好。但也相应地损失了一些灵活性。如果不清楚元素的确切个数,则应使用vector。
5.1 定义和初始化内置数组
5.1.1 定义数组
数组的声明形如a[d],其中a为数组名,d为数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译时维度应该是已知的。因此,数组的维度必须是一个常量表达式。
unsigned cnt = 42; // cnt不是常量表达式
constexpr unsigned sz = 42; // sz是常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // 错误。cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确,否则错误
定义数组时必须指定数组的类型,不允许使用auto关键字由初始值的列表推断类型。
数组的元素应为对象,因此不存在引用的数组。
5.1.2 数组的默认初始化
如果函数内部定义了某种内置类型的数组,默认初始化会令数组含有未定义的值。
5.1.3 数组的显式初始化
可以使用列表初始化来初始化数组,此时允许忽略数组的维度。如果在声明数组时没有指定维度,编译器会根据初始值的数量计算出来。如果指明了维度,则初始值数量不应超出指定的大小。如果提供的初始值比维度小,则使用提供的初始值初始化靠前的元素,剩下的元素默认初始化。
const unsigned sz = 3; //
int ia1[sz] = {0, 1, 2}; // ia1是维度为3的数组,元素值分别为0、1、2
int a2[] = {0, 1, 2}; // a2是维度为3的数组
int a3[5] = {0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // 等价于a4[] = {"hi", "bye", ""}
int a5[2] = {0, 1, 2}; // 错误,初始值过多
5.1.4 字符数组的特殊性
字符数组可以用字符串字面值初始化。需要注意的是,字符串字面值最后有一个隐形的空字符。因此字符数组的维度要额外+1。
char c[4] = "c++"; // 维度为4,实际字符数量为3。多出来的一个是空字符。
5.1.5 数组不允许拷贝与赋值操作
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。某些编译器支持数组的赋值,这称为编译器扩展(compiler extension)。但应避免使用非标特性。
int a[] = {0, 1, 2}; // 含有3个整数的数组
int a2[] = a; // 错误。不允许使用一个数组初始化另一个数组
a2 = a; // 错误。不能把一个数组直接赋值给另一个数组
5.1.6 理解复杂的数组声明
前面讲过,对于复杂的声明,从右向左理解较为便捷。但对于数组来说,由右向左理解不大合理,因为数组的维度是紧跟着被声明的名字的,由内向外阅读则更为准确(按内→右→左的顺序阅读)。
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int &refs[10]; // 错误。不存在引用的数组
int (*Parray)[10] = &arr; // Parray是一个指针,指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; // arry引用一个含有10个整型指针的数组