C++从C继承了数组,在那里它们几乎无处不在。 C++提供了更易于使用且更不容易出错的抽象(std::Vector<;T>
自C++98以来,std::Array<;T,N>
自C++11以来),因此对数组的需求不像C中那样频繁出现。但是,当您阅读遗留代码或与用C编写的库交互时,您应该对数组的工作原理有一个牢固的掌握。
本FAQ分为五个部分:
如果你觉得这个FAQ中缺少了一些重要的东西,写一个答案并把它链接到这里作为额外的部分。
在下面的文本中,“数组”的意思是“C数组”,而不是类模板std::array
。 假定有C语言声明符语法的基本知识。 注意,下面演示的new
和delete
的手动使用在面对异常时是极其危险的,但那是另一个FAQ的主题。
(注意:这是Stack Overflow的C++FAQ的一个条目。如果你想要批评在这个表单中提供FAQ的想法,那么开始这一切的meta上的帖子将是这样做的地方。这个问题的答案在C++聊天室中被监视,FAQ的想法最初是在那里开始的,所以你的答案很可能被提出这个想法的人阅读。)
数组类型表示为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*
)的一部分。 优点:忘记类型级别上数组的大小允许指针指向任意大小数组的第一个元素。 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: | | | | | | | | | | |
+---+---+---+---+---+
由于现在每一行都是单独分配的,因此将2D数组视为1D数组不再起作用。
下面是一个由5个(或任何其他数量)指针组成的匿名数组,这些指针是用不同长度的匿名数组初始化的:
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);
}
作为一种句法选择,您还可以将参数声明为tp[]
,它的含义仅与参数列表上下文中的t*p
完全相同:
int sum(const int p[], std::size_t n)
{
return std::accumulate(p, p + n, 0);
}
您可以认为编译器仅在参数列表的上下文中将tp[]
重写为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
,从模板实例化一个不同的函数。 您还可以编写非常有用的函数模板,从元素类型和大小两方面进行抽象。