我如何在C ++中使用数组?
C ++继承了C语言中的数组,几乎在任何地方都使用它们。 C ++提供了易于使用且易于出错的抽象(从C ++ 98开始,自C ++ 98和std::array<T, n>
之后的std::vector<T>
),所以对数组的需求不会的出现频率与C中的频率相同。但是,当您阅读遗留代码或与用C语言编写的库进行交互时,应该牢牢掌握阵列的工作方式。
本常见问题分为五个部分:
如果您觉得此常见问题解答中缺少重要内容,请撰写答案并将其作为附加链接链接到此处。
在下面的文本中,“数组”意味着“C数组”,而不是类模板std::array
。 假定C声明符语法的基本知识。 请注意,如下所示,手动使用new
和delete
对于例外是非常危险的,但这是另一个常见问题的主题。
(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供常见问题的想法,那么开始所有这些的meta上的贴子将成为这样做的地方。那个问题在C ++聊天室中进行监控,常见问题的想法首先出现在C ++聊天室中,所以你的答案很可能会被那些提出这个想法的人阅读。)
类型级别的数组
数组类型表示为T[n]
,其中T
是元素类型, n
是正数大小,即数组中元素的数量。 数组类型是元素类型和大小的产品类型。 如果其中一种或两种成分不同,您会得到一种独特的类型:
#include <type_traits>
static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8], int[9]>::value, "distinct size");
请注意,大小是类型的一部分,也就是说,不同大小的数组类型是不兼容的类型,它们完全无关。 sizeof(T[n])
等于n * sizeof(T)
。
阵列与指针衰减
T[n]
和T[m]
之间唯一的“连接”是两种类型都可以隐式转换为T*
,并且此转换的结果是指向数组的第一个元素的指针。 也就是说,在任何需要T*
地方,你可以提供一个T[n]
,编译器会默默地提供这个指针:
+---+---+---+---+---+---+---+---+
the_actual_array: | | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^
|
|
|
| pointer_to_the_first_element int*
这种转换被称为“数组到指针的衰减”,它是混淆的主要来源。 在这个过程中数组的大小会丢失,因为它不再是类型的一部分( T*
)。 Pro:在类型级别上忘记数组的大小允许一个指针指向任何大小数组的第一个元素。 Con:给出一个指向数组第一个(或任何其他)元素的指针,没有办法检测到该数组的大小或指针指向数组边界的位置。 指针非常愚蠢。
数组不是指针
只要编译器认为有用,编译器就会默默地生成一个指向数组第一个元素的指针,也就是说,每当一个操作在一个数组上失败,但在一个指针上成功时。 从数组到指针的这种转换是微不足道的,因为生成的指针值只是数组的地址。 请注意,指针不是作为数组本身的一部分(或内存中的其他任何位置)存储的。 数组不是指针。
static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");
一个数组不会衰变为指向其第一个元素的重要上下文是当&
运算符应用于它时。 在这种情况下, &
运算符产生一个指向整个数组的指针,而不仅仅是指向其第一个元素的指针。 虽然在这种情况下值(地址)是相同的,但是指向数组的第一个元素的指针和指向整个数组的指针是完全不同的类型:
static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");
下面的ASCII艺术解释了这种区别:
+-----------------------------------+
| +---+---+---+---+---+---+---+---+ |
+---> | | | | | | | | | | | int[8]
| | +---+---+---+---+---+---+---+---+ |
| +---^-------------------------------+
| |
| |
| |
| | pointer_to_the_first_element int*
|
| pointer_to_the_entire_array int(*)[8]
注意指向第一个元素的指针只指向一个整数(描述为一个小方块),而指向整个数组的指针指向一个由8个整数组成的数组(描述为一个大方块)。
同样的情况出现在课堂上,可能更为明显。 指向对象的指针和指向其第一个数据成员的指针具有相同的值(相同的地址),但它们是完全不同的类型。
如果您不熟悉C语言的语法, int(*)[8]
类型的括号是必不可少的:
int(*)[8]
是一个指向8个整数数组的指针。 int*[8]
是一个8个指针的数组,每个int*
类型的元素。 访问元素
C ++提供了两种语法变体来访问数组中的各个元素。 他们都没有优于对方,你应该熟悉这两者。
指针算术
给定一个指向数组第一个元素的指针p
,表达式p+i
产生一个指向数组的第i个元素的指针。 之后通过取消引用该指针,可以访问各个元素:
std::cout << *(x+3) << ", " << *(x+7) << std::endl;
如果x
表示一个数组,那么数组到指针的衰减将会启动,因为添加一个数组和一个整数是没有意义的(对数组没有加操作),但是添加一个指针和一个整数是有意义的:
+---+---+---+---+---+---+---+---+
x: | | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^ ^ ^
| | |
| | |
| | |
x+0 | x+3 | x+7 | int*
(请注意,隐式生成的指针没有名称,所以我写了x+0
以标识它。)
另一方面,如果x
表示指向数组的第一个(或任何其他)元素的指针,则数组到指针的衰减是不必要的,因为i
要添加的指针已经存在:
+---+---+---+---+---+---+---+---+
| | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^ ^ ^
| | |
| | |
+-|-+ | |
x: | | | x+3 | x+7 | int*
+---+
请注意,在所描述的情况下, x
是一个指针变量(可通过x
旁边的小方块辨别),但它也可以是返回指针(或任何其他T*
类型表达式)的函数的结果。
索引操作符
由于语法*(x+i)
有点笨拙,因此C ++提供了另一种语法x[i]
:
std::cout << x[3] << ", " << x[7] << std::endl;
由于加法是可交换的,所以下面的代码完全一样:
std::cout << 3[x] << ", " << 7[x] << std::endl;
索引操作符的定义导致以下有趣的等价性:
&x[i] == &*(x+i) == x+i
但是, &x[0]
通常不等于x
。 前者是一个指针,后者是一个数组。 只有当上下文触发数组到指针的衰减时, x
和&x[0]
可以互换使用。 例如:
T* p = &array[0]; // rewritten as &*(array+0), decay happens due to the addition
T* q = array; // decay happens due to the assignment
在第一行中,编译器检测到指向指针的指针,该指针平凡成功。 在第二行,它检测从数组到指针的分配。 由于这是没有意义的(但是指向指针赋值的指针是有道理的),像往常一样,数组到指针的衰减开始了。
范围
一个T[n]
类型的数组有n
元素,索引从0
到n-1
; 没有元素n
。 然而,为了支持半开范围(其中开始是包含性的并且末尾是排他性的),C ++允许计算指向(不存在的)第n个元素的指针,但是取消引用该指针是非法的:
+---+---+---+---+---+---+---+---+....
x: | | | | | | | | | . int[8]
+---+---+---+---+---+---+---+---+....
^ ^
| |
| |
| |
x+0 | x+8 | int*
例如,如果你想对一个数组进行排序,以下两种方法同样适用:
std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);
请注意,提供&x[n]
作为第二个参数是非法的,因为这等价于&*(x+n)
,并且子表达式*(x+n)
技术上会在C ++中调用未定义的行为(但不在C99中)。
还要注意,你可以简单地提供x
作为第一个参数。 这对我来说有点太简单了,它也会使编译器对模板参数的推导有点困难,因为在这种情况下,第一个参数是一个数组,但第二个参数是一个指针。 (再一次,数组到指针的衰减开始了。)
程序员经常会将多维数组与指针数组混淆。
多维数组
大多数程序员都熟悉命名的多维数组,但很多人并不知道多维数组也可以匿名创建。 多维数组通常被称为“数组阵列”或“真正的多维数组”。
命名为多维数组
使用命名的多维数组时,必须在编译时知道所有维度:
int H = read_int();
int W = read_int();
int connect_four[6][7]; // okay
int connect_four[H][7]; // ISO C++ forbids variable length array
int connect_four[6][W]; // ISO C++ forbids variable length array
int connect_four[H][W]; // ISO C++ forbids variable length array
这就是命名多维数组在内存中的外观:
+---+---+---+---+---+---+---+
connect_four: | | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
请注意,诸如上述的2D网格仅仅是有用的可视化。 从C ++的角度来看,内存是一个“扁平”的字节序列。 多维数组的元素按行优先顺序存储。 也就是说, connect_four[0][6]
和connect_four[1][0]
是内存中的邻居。 实际上, connect_four[0][7]
和connect_four[1][0]
表示相同的元素! 这意味着您可以采用多维数组并将它们视为大型的一维数组:
int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);
匿名多维数组
使用匿名多维数组,除编译时必须知道除第一个外的所有维度:
int (*p)[7] = new int[6][7]; // okay
int (*p)[7] = new int[H][7]; // okay
int (*p)[W] = new int[6][W]; // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W]; // ISO C++ forbids variable length array
这就是匿名多维数组在内存中的样子:
+---+---+---+---+---+---+---+
+---> | | | | | | | |
| +---+---+---+---+---+---+---+
| | | | | | | | |
| +---+---+---+---+---+---+---+
| | | | | | | | |
| +---+---+---+---+---+---+---+
| | | | | | | | |
| +---+---+---+---+---+---+---+
| | | | | | | | |
| +---+---+---+---+---+---+---+
| | | | | | | | |
| +---+---+---+---+---+---+---+
|
+-|-+
p: | | |
+---+
请注意,数组本身仍被分配为内存中的单个块。
指针阵列
您可以通过引入另一个间接级别来克服固定宽度的限制。
命名的指针数组
这是一个由五个指针组成的命名数组,它们使用不同长度的匿名数组进行初始化:
int* triangle[5];
for (int i = 0; i < 5; ++i)
{
triangle[i] = new int[5 - i];
}
// ...
for (int i = 0; i < 5; ++i)
{
delete[] triangle[i];
}
下面是它在内存中的外观:
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
^
| +---+---+---+---+
| | | | | |
| +---+---+---+---+
| ^
| | +---+---+---+
| | | | | |
| | +---+---+---+
| | ^
| | | +---+---+
| | | | | |
| | | +---+---+
| | | ^
| | | | +---+
| | | | | |
| | | | +---+
| | | | ^
| | | | |
| | | | |
+-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
+---+---+---+---+---+
由于每条线现在都是单独分配的,因此将二维数组视为一维数组不再适用。
匿名的指针数组
这是一个匿名数组(或任何其他数量的)指针,它们使用不同长度的匿名数组进行初始化:
int n = calculate_five(); // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
p[i] = new int[n - i];
}
// ...
for (int i = 0; i < n; ++i)
{
delete[] p[i];
}
delete[] p; // note the extra delete[] !
下面是它在内存中的外观:
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
^
| +---+---+---+---+
| | | | | |
| +---+---+---+---+
| ^
| | +---+---+---+
| | | | | |
| | +---+---+---+
| | ^
| | | +---+---+
| | | | | |
| | | +---+---+
| | | ^
| | | | +---+
| | | | | |
| | | | +---+
| | | | ^
| | | | |
| | | | |
+-|-+-|-+-|-+-|-+-|-+
| | | | | | | | | | |
+---+---+---+---+---+
^
|
|
+-|-+
p: | | |
+---+
转换
数组到指针的衰减自然延伸到数组和指针数组的数组:
int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;
int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;
但是,并没有从T[h][w]
到T**
隐式转换。 如果这样的隐式转换确实存在,其结果将是一个指向的数组的第一个元素h
指针T
(各指向原始2D阵列中的线的第一元件),但该指针数组不存在内存中的任何地方呢。 如果您想要这样的转换,您必须手动创建并填充所需的指针数组:
int connect_four[6][7];
int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
p[i] = connect_four[i];
}
// ...
delete[] p;
请注意,这会生成原始多维数组的视图。 如果您需要一个副本,您必须创建额外的数组并自己复制数据:
int connect_four[6][7];
int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
p[i] = new int[7];
std::copy(connect_four[i], connect_four[i + 1], p[i]);
}
// ...
for (int i = 0; i < 6; ++i)
{
delete[] p[i];
}
delete[] p;
分配
没有特别的原因,数组不能被分配给另一个。 改为使用std::copy
:
#include <algorithm>
// ...
int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);
这比真正的数组赋值可以提供更多的灵活性,因为可以将更大的数组的切片复制到更小的数组中。 std::copy
通常专用于原始类型以提供最佳性能。 std::memcpy
不太可能执行得更好。 如果有疑问,请测量。
虽然不能直接分配数组,但可以分配包含数组成员的结构和类。 这是因为数组成员是由编译器提供的赋值运算符以成员方式复制的。 如果您为自己的结构或类类型手动定义赋值运算符,则必须回退到数组成员的手动复制。
参数传递
数组不能通过值传递。 您可以通过指针或引用来传递它们。
通过指针传递
由于数组本身不能通过值传递,通常指向第一个元素的指针是通过值传递的。 这通常被称为“通过指针”。 由于数组的大小不能通过该指针获取,因此必须传递指示数组大小的第二个参数(传统的C解决方案)或指向数组最后一个元素(C ++迭代器解决方案)之后的第二个指针, :
#include <numeric>
#include <cstddef>
int sum(const int* p, std::size_t n)
{
return std::accumulate(p, p + n, 0);
}
int sum(const int* p, const int* q)
{
return std::accumulate(p, q, 0);
}
作为一种语法选择,您也可以将参数声明为T p[]
,并且它仅在参数列表的上下文中表示与T* p
完全相同的内容:
int sum(const int p[], std::size_t n)
{
return std::accumulate(p, p + n, 0);
}
您可以将编译器视为仅在参数列表的上下文中将T p[]
重写为T *p
。 这个特殊规则部分是对数组和指针的混淆造成部分责任。 在任何其他环境中,将某些东西声明为数组或指针会产生巨大的差异。
不幸的是,你也可以在一个数组参数中提供一个大小,这个参数被编译器默默地忽略。 也就是说,以下三个签名完全等效,如编译器错误所示:
int sum(const int* p, std::size_t n)
// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)
// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n) // the 8 has no meaning here
通过参考传递
数组也可以通过引用传递:
int sum(const int (&a)[8])
{
return std::accumulate(a + 0, a + 8, 0);
}
在这种情况下,数组大小很重要。 由于编写一个只接受8个元素数组的函数几乎没有用处,所以程序员通常会编写像模板这样的函数:
template <std::size_t n>
int sum(const int (&a)[n])
{
return std::accumulate(a + 0, a + n, 0);
}
请注意,您只能使用实际的整数数组调用这样的函数模板,而不能使用指向整数的指针。 数组的大小是自动推断的,并且对于每个大小n
,从模板实例化不同的函数。 你也可以编写非常有用的函数模板,从元素类型和大小中抽象出来。