第3章 3.5 数组
文章目录
数组 是一种类似于标准库类型vector的数据结构,但又与vector有所不同:
(1)相似的地方
数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。
(2)不同的地方
数组的大小确定不变,不能随意向数组中增加元素。
3.5.1 定义和初始化内置数组
数组是一种复合类型。数组的声明形如 a[d],其中a是数组的名字,d是数组的维度。
数组的维度即数组中元素的个数,因此必须大于0;另外,维度也属于数组类型的一部分,编译的时候维度应该是已知的,因此其必须为一个常量表达式:
unsigned cnt = 42; // 不说常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // 错误:cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时是正确;否则错误
注意:
(1) 默认情况下,数组的元素被默认初始化。和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值;
(2)定义数组的时候必须明确指定数组的类型,不允许用auto关键字由初始值的列表推断类型;
(3)数组的元素应为对象,因此不存在引用的数组。
【显示初始化数组元素】
对数组的元素进行列表初始化,此时允许忽略数组的维度:
(1)在声明中没有指明维度:编译器会根据初始值的数量计算并推测出来;
(2)在声明中指明了维度:初始值的总数量不应超出指定的大小。如果维度比共的初始值的总数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值。
例如:
const unsigned sz = 3;
int a1[sz] = {0, 1, 2}; // 含有3个元素的数组,元素分别是0,1,2
int a2[] = {0, 1, 2}; // 维度是3的数组,元素分别是0,1,2
int a3[5] = {0, 1, 2}; // 维度是5的数组,等价于 a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hehe", "haha"}; // 等价于a4[] = {"hehe", "haha", ""}
int a5[2] = {0, 1, 2}; // 错误:初始值的总数量超过指定大小
【字符数组的特殊性】
字符数组有一种额外的初始化形式,即可以用字符串字面值对此类数组初始化。当使用这种方式时,字符串字面值结尾处的空字符也会像字符串里的其它字符一样被拷贝到数组里:
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加空字符,数组大小为4
const char a4[6] = "Daniel"; // 错误:没有空间可以放空字符
【不允许拷贝和赋值】
不能将数组的内容拷贝给其它数组作为初始值,也布恩那个用数组为其他数组赋值:
int a[] = {1, 2, 3};
int b[] = a; // 错误
b = a; // 错误
【指针数组和数组指针】
(1)指针数组:是数组,数组里存放的每个元素是指针;
数组指针:是指针,它指向一个数组。
例如:
int *a[10]; // a是指针数组,它是一个含有10个整型指针的数组
int (*b)[10]; // b是数组指针,它指向一个含有10个整数的数组
该例子的解析:
(1)默认情况下,类型修饰符从右往左依次绑定。对于a来说,从右往左理解:首先知道定义的是一个大小为10的数组,它的名字是a,然后知道数组中村发那个的是指向int的指针;
(2)对于b来说,需要从内向外理解:首先是圆括号括起来的部分,*b意味着b是一个指针,接下来看右边,可以知道b是个指向大小为10的数组的指针,最后看左边,知道数组中的元素是int。因此最后就可以判定,b指向一个int数组,数组中包含10个元素。
【数组和引用】
-
数组中的元素是对象,因此 不存在引用的数组(即数组里的元素是引用),因为引用不是对象;
-
数组本身是对象,因此允许定义 数组的引用。
例如:
int &a[10] = {0, 1, 2}; // 错误:不存在引用的数组
int b[10] = {0, 1, 2};
int (&c)[10] = b // 正确:c引用一个含有10个整数的数组
上例中,对于c来说,应该由内向外理解:(&c)表示c是一个引用,接着看右边,知道它引用的对象是一个大小为10的数组,最后看左边,知道数组中元素的类型是int,因此c是对一个包含10个int的数组的引用。
【数组+指针+引用】
由于对修饰符的数量没有特殊限制,所以有时候会出现非常复杂的数组声明,声明中可能会包含指针和引用。例如:
int *ptrs[10];
int *(&a)[10] = ptrs; // a是数组的引用,该数组含有10个指针
上例中的a应该按照由内向外的方法理解:(&a)表示a是一个引用,然后看右边,知道a引用的对象是一个大小为10的数组,最后看左边,知道数组的元素类型是指向int的指针。因此,a是一个含有10个int型指针的数组的引用。
3.5.2 访问数组元素
数组的元素可以使用 范围for语句 或 下标运算符 来访问:
(1)下标运算符
使用数组下标时,通常将其定义为 size_t 类型。**size_t 是一种与机器类型相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。**在 cstddef 头文件中定义了 size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。
数组的索引从0开始。
(2) 范围for语句
与vector和string一样,当需要遍历数组中的所有元素时,最好的办法就是使用范围for语句:
int a[10] = {0, 1, 2};
for(auto i : a)
cout << i << " ";
cout << endl;
3.5.3 指针和数组
数组的元素是对象,因此对数组的元素使用 取地址符 就能得到指向该元素的指针:
string nums[] = {"one", "two", "three"};
string *p1 = &nums[0]; // p1指向nums的第一个元素
数组还有一个特性:在很多用到数组名字的地方,编译器会自动将数组名字替换成一个指向数组首元素的指针:
string nums[] = {"one", "two", "three"};
string *p2 = nums; // 等价于 p2 = &nums[0]
【数组、auto、decltype】
【注意】在大多数表达式中,使用数组类型的对象,其实就是使用一个指向该数组首元素的指针。
(1)auto
当使用一个数组作为一个auto遍历的初始值时,推断得到的类型 是指针而非数组:
int a[] = {0, 1, 2};
auto ia(a); // ia是一个整型指针,指向a的第一个元素
ia = 42; // 错误:ia是一个指针,不能用int值给指针赋值
上例中,尽管a是一个由3个整数构成的数组,但当时用a作为初始值时,编译器实际执行的初始化过程类似于:
auto ia(&a[0]); // 显然,a的类型是int*
(2)decltype
当使用decltype关键字时,则不进行上述转换,decltype(a) 返回的类型是由3个整数构成的数组:
int a[] = {0, 1, 2};
decltype(a) a3 = {2, 4, 6}; // a3是一个含有3个整数的数组
a3 = a; // 错误:不能用整型指针给数组赋值
a3[0] = a[0]; // 正确
【标准库函数begin和end】
C++11新标准引入了两个名为 begin 和 end 的函数,这两个函数定义在 iterator头文件 里。这两个函数与容器中两个同名成员功能类似。但毕竟数组不是类类型,因此这两个函数不是成员函数,正确的使用形式是将数组作为它们的参数,其中begin函数会返回指向数组首元素的指针,而end函数会返回执行数组尾元素下一位置的指针:
int a[] = {1, 2, -3, 4};
int *beg = begin(a);
int *last = end(a);
// 找出数组a中的第一个负数
int *ptr = beg;
while(ptr != last ** *ptr >= 0) {
ptr++;
}
【指针运算】
指向数组元素的指针可以指向的运算包括 解引用、递增、比较、与整数相加、两个指针相减等:
(1)给(从)一个指针加上(减去)某整数值,结果仍是指针;
(2)给指针加上某个整数,得到的 新指针仍需指向同一数组的其它元素,或指向同一数组的尾元素的下一个位置:
int a[] = {1, 2, 3};
int *p1 = a + 3; // 使用警告:不要解引用!
int *p2 = a + 4; // 错误(但编译器不会报错):a只有3个元素,p2是未定义的
(3)两个指针相减的结果是它们之间的距离,参与运算的两个指针 必须指向同一个数组中的元素。两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,和 size_t一样定义在 cstddef 头文件 中。因为插值可能是负值,因此 ptrdiff_t 是带符号类型。另外,两个空指针相减的结果是0;
(4)只要两个指针是指向同一个数组的元素,或指向该数组尾元素的下一位置,就能利用关系运算符进行比较。
【下标和指针】
只要指针指向的是数组中的元素(或数组尾元素的下一位置),都可以指向下标运算:
int a[] = {0, 1, 2, 3, 4};
int *p = &a[3];
int i = p[1]; // p[1]等价于 *(p+1),也就是a[4]表示的那个元素
int k = p[-2]; // p[-2]等价于 *(p-2),也就是a[1]表示的元素
【注意】标准库类型string和vector使用的下标必须是无符号类型,而内置的下标运算可以处理负值,只要结果指针仍然指向原来指针所指向的同一数组或是尾元素的下一位置。