如何在C++中使用数组?

515
C++继承了C中的数组,在C中,它们几乎被广泛使用。C++提供了更易于使用和更少出错的抽象(自C++98以来的std::vector<T>和自C++11以来的std::array<T, n>),因此,与C中相比,对数组的需求并不那么频繁。但是,当您阅读旧代码或与用C编写的库交互时,您应该牢固掌握数组的工作原理。
本FAQ分为五个部分:
  1. 类型级别上的数组和访问元素
  2. 数组创建和初始化
  3. 赋值和参数传递
  4. 多维数组和指针数组
  5. 使用数组时的常见陷阱
如果您觉得本FAQ缺少重要内容,请编写答案并在此处链接。
在下文中,“array”指的是“C数组”,而不是类模板std :: array 。假定具有C声明符语法的基本知识。请注意,如下所示手动使用 new 和 delete 在面对异常时极其危险,但这是另一个常见问题的主题。

(注意:这是一个Stack Overflow的C++常见问题解答。如果您想批评以这种形式提供常见问题解答的想法,那么启动所有这一切的meta帖子将是批评的地方。对该问题的回答在C++聊天室中进行监控,在那里FAQ的想法首次出现,因此您的回答很可能会被那些提出这个想法的人阅读。)


如果指针总是指向目标的开头而不是中间某个位置,它们会更好。 - Deduplicator
你应该使用STL向量,因为它能够提供更大的灵活性。 - Moiz Sajid
2
有了std::arraystd::vectorgsl::span的共同可用性,我坦率地希望在如何在C++中使用数组的常见问题解答中说:“到现在为止,你可以开始考虑根本不使用它们。” - einpoklum
5个回答

322

类型级别的数组

数组类型表示为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*)的一部分。优点:在类型级别上忘记数组的大小允许指针指向任何大小的数组的第一个元素。缺点:给定指向数组的第一个(或任何其他)元素的指针,没有办法检测该数组的大小或指针相对于数组边界的确切位置。指针非常愚蠢

数组不是指针

每当编译器认为有用时(也就是说,每当一个操作会在数组上失败但在指针上成功时),它都会悄悄地生成一个指向数组的第一个元素的指针。从数组到指针的转换是微不足道的,因为所得到的指针值只是数组的地址。请注意,指针作为数组本身(或内存中的任何其他位置)的一部分存储。数组不是指针。
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是一个指针变量(通过旁边的小框可以看出),但它也可能是函数返回指针的结果(或者任何其他类型为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 个元素,索引从 0n-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作为第一个参数提供。这对我来说有点太简洁了,并且它还使得编译器更难进行模板参数推导,因为在这种情况下,第一个参数是一个数组,但第二个参数是一个指针。(再次,数组到指针的衰减开始起作用。)

2
有些情况下,数组不会衰变为指针,这里有相关的说明供参考。 - legends2k
1
@fredoverflow 在Access或Ranges部分中值得一提的是,C数组可以使用C++11范围-based 循环。 - gnzlbg
1
非常出色的回答。语句“这种转换被称为'数组指针衰减',它是混淆的主要来源”是准确的,其中一部分原因是因为它只在通用语言中被“称为”。在语言草案或标准中,从未使用过这种术语来描述转换为临时指针的上下文。 - WhozCraig

147

程序员经常将多维数组与指针数组混淆。

多维数组

大多数程序员都熟悉命名的多维数组,但许多人不知道匿名多维数组也可以创建。多维数组通常被称为“数组的数组”或“真正的多维数组”。

命名的多维数组

在使用命名的多维数组时,所有维度必须在编译时已知:

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: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

请注意,像上面这样的二维网格仅仅是有帮助的可视化工具。从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**。如果这样的隐式转换存在,结果将是一个指向 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;

1
作为建议:您应该指出,当在编译时已知 HW 时,int connect_four[H][7];int connect_four[6][W];int connect_four[H][W]; 以及 int (*p)[W] = new int[6][W];int (*p)[W] = new int[H][W]; 都是有效的语句。 - RobertS supports Monica Cellio
非常感谢!请告诉我如何从数组(“指针的匿名数组”)中设置/获取元素。 - Borko Djurovic
可能需要明确指出,释放匿名多维数组的正确语法是 delete[] p - philb

93

任务

出于无特定原因,数组不能互相赋值。请使用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的数组,都会从模板实例化一个不同的函数。你还可以编写非常有用的函数模板,它可以抽象出元素类型和大小。

2
也许值得注意的是,在void foo(int a[3])中,虽然看起来像是按值传递数组,但在foo内修改a会修改原始数组。这应该是清楚的,因为数组不能被复制,但有必要加强这一点。 - gnzlbg
C++20有ranges::copy(a, b) - L. F.
int sum(int size_, int a[size_]); -- 从(我认为)C99开始 - Chef Gladiator

77
5. 常见的数组使用陷阱。
5.1 陷阱:信任不安全的类型链接。
好的,你已经被告知,或者自己发现,全局变量(可以在翻译单元之外访问的命名空间范围变量)是邪恶的™。但你知道它们有多邪恶™吗?考虑下面的程序,由两个文件[main.cpp]和[numbers.cpp]组成:
// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

在Windows 7中,这个程序在MinGW g++ 4.4.1和Visual C++ 10.0下都可以编译和链接成功。
由于类型不匹配,当你运行程序时会导致程序崩溃。

The Windows 7 crash dialog

在非正式的解释中:程序存在未定义行为(UB),因此不会崩溃,而是可能会挂起,或者什么都不做,或者发送威胁性的电子邮件给美国、俄罗斯、印度、中国和瑞士的总统,并使鼻子里飞出鼻腔守护进程。
在实践中的解释:在main.cpp中,数组被视为指针,放置在与数组相同的地址上。对于32位可执行文件来说,这意味着数组中的第一个int值被视为指针。也就是说,在main.cpp中,numbers变量包含或看起来包含(int*)1。这导致程序访问地址空间底部的内存,这通常是保留和触发陷阱的。结果:程序崩溃。
编译器完全有权不诊断此错误,因为C++11 §3.5/10关于声明的兼容类型要求的规定如下:
[N3290 §3.5/10] 对于类型一致性规则的违反不需要诊断。
同一段落详细说明了允许的变化。
…声明一个数组对象的声明可以指定不同的数组类型,这些类型可以通过主要数组边界的存在或不存在来区分(8.3.4)。
这种允许的变化不包括在一个翻译单元中将名称声明为数组,在另一个翻译单元中将其声明为指针。
5.2陷阱:过早优化(memset和朋友)。
尚未编写。
5.3陷阱:使用C习惯用法获取元素数量。
有着深厚的C经验,编写以下代码是很自然的...
#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

由于数组在需要时会衰变为指向第一个元素的指针,表达式`sizeof(a)/sizeof(a[0])`也可以写为`sizeof(a)/sizeof(*a)`。它的意思是相同的,无论如何写,这都是用于找到数组元素数量的C语言习惯用法。
主要陷阱:C语言习惯用法不具备类型安全性。例如,代码...
#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

传递指向N_ITEMS的指针,因此很可能会产生错误的结果。在Windows 7中编译为32位可执行文件时,会产生以下结果...

7个元素,调用display...
1个元素。

  1. 编译器将int const a[7]重写为int const a[]
  2. 编译器将int const a[]重写为int const* a
  3. 因此,N_ITEMS被调用为指针。
  4. 对于32位可执行文件,sizeof(array)(指针的大小)为4。
  5. sizeof(*array)等同于sizeof(int),对于32位可执行文件也是4。

为了在运行时检测到这个错误,您可以进行以下操作...

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7个元素,调用显示... 断言失败:(“N_ITEMS需要一个实际的数组作为参数”,文件runtime_detection.cpp,第16行) 此应用程序已请求运行时以异常方式终止。 请联系应用程序的支持团队获取更多信息。 运行时错误检测比没有检测要好,但会浪费一些处理器时间,也许还会浪费更多的程序员时间。最好在编译时进行检测!如果您愿意不支持C++98中的本地类型数组,那么可以这样做:
#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

编译这个定义替换成第一个完整的程序,使用g++,我得到...
M:\count> g++ compile_time_detection.cpp compile_time_detection.cpp: 在函数‘void display(const int*)’中: compile_time_detection.cpp:14: 错误:没有为‘n_items(const int*&)’找到匹配的函数
M:\count> _
它的工作原理:数组以引用的方式传递给n_items,因此它不会衰变为指向第一个元素的指针,函数可以直接返回类型指定的元素数量。
在C++11中,您还可以将其用于本地类型的数组,并且这是一种类型安全的C++习惯用法,用于查找数组的元素数量。
5.4 C++11 - C++20陷阱:使用constexpr数组大小函数。
使用C++11及更高版本,实现数组大小函数的自然方式如下:
// Similar in C++03, but not constexpr.
template< class Type, std::size_t N > 
constexpr std::size_t size( Type (&)[N] ) { return N; }

这将返回数组中元素的数量作为一个编译时常量。这个函数甚至在C++17中被标准化为std::size
例如,size()可以用来声明一个与另一个数组相同大小的数组:
// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    int y[ size(x) ] = {};
}

但是考虑一下使用constexpr版本的代码:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = size( c ); // error prior to C++23
    // ...
}

int main()
{
    int x[42];
    foo( x );
}

陷阱:直到C++23,使用引用 c 在常量表达式中是不允许的,而且所有主要编译器都会拒绝这段代码。根据C++20标准,[expr.const] p5.12

除非根据抽象机器的规则,表达式E的评估会导致以下情况之一,否则表达式E是一个核心常量表达式

  • [...]
  • 引用类型的变量或数据成员的标识表达式,除非引用具有前置初始化并且满足以下条件之一:
    • 它可用于常量表达式,或者
    • 它的生命周期始于E的评估内;

c在常量表达式中既不可用,也没有在constexpr int n = ...中开始其生命周期,因此评估c不是核心常量表达式。这些限制在C++23中已被取消,参见P2280: 在常量表达式中使用未知指针和引用c被视为对未指定对象的引用绑定([expr.const] p8)。

5.4.1 解决方法:与C++20兼容的constexpr大小函数

std::extent< decltype( c ) >::value;不是一个可行的解决方法,因为如果Collection不是一个数组,它将失败。

处理可能不是数组的集合时,需要使用一个可以重载的size函数,但是为了编译时使用,还需要一个数组大小的编译时表示。经典的C++03解决方案,在C++11和C++14中也适用,就是让函数通过其函数结果类型而不是值来报告其结果。例如像这样:
// Example 3 - OK (not ideal, but portable and safe)

#include <array>
#include <cstddef>

// No implementation, these functions are never evaluated.
template< class Type, std::size_t N >
auto static_n_items( Type (&)[N] )
  -> char(&)[N]; // return a reference to an array of N chars

template< class Type, std::size_t N >
auto static_n_items( std::array<Type, N> const& )
  -> char(&)[N];

#define STATIC_N_ITEMS( c ) ( sizeof( static_n_items( c )) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr std::size_t n = STATIC_N_ITEMS( c );
    // ...
}

int main()
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

关于选择static_n_items的返回类型:这段代码没有使用std::integral_constant,因为使用std::integral_constant会直接将结果表示为constexpr值,从而重新引入原始问题。
关于命名:解决constexpr无效的引用问题的一部分解决方案是明确选择编译时常量。
直到C++23,像上面的STATIC_N_ITEMS这样的宏可以实现可移植性,例如对于clang和Visual C++编译器,保留类型安全性。
相关:宏不遵守作用域,因此为了避免名称冲突,使用名称前缀(例如MYLIB_STATIC_N_ITEMS)是一个好主意。

1
+1 很棒的C编程测试:我花了15分钟在VC++ 10.0和GCC 4.1.2上尝试修复“分段错误”...终于在阅读您的解释后找到/理解了!请写下您的§5.2章节 :-) 干杯 - oHo
@Ricky65:感谢提到C++11的考虑事项。对于Visual C++来说,支持这些功能的时间比较晚。关于size_t,我不知道它在现代平台上有什么优势,但由于C和C++的隐式类型转换规则,它存在许多问题。也就是说,ptrdiff_t被有意地使用,以避免size_t的问题。然而,应该注意的是,g++在匹配数组大小与模板参数时存在问题,除非它是size_t(我认为这个特定于编译器的非size_t问题并不重要,但你的情况可能有所不同)。 - Cheers and hth. - Alf
如果你想使用带符号类型,为什么不使用 POSIX.2/C11 中提到的 ssize_t 呢?(在 C++11 标准的注释中提到)。如果你不确定是否存在该类型,可以在引用前使用声明:using ssize_t = typename std::make_signed<size_t>::type; - Deduplicator
@Deduplicator:关于为什么不使用POSIX.2/C11 ssize_t,这要看一下你是在问这个FAQ条目中是否使用ptrdiff_t,还是一般情况下是否使用它。对于简单的SO答案等,我认为保持简单,不引入多余的依赖或复杂性是一个好习惯。对于一般情况,我不确定。似乎Posix的ssize_t如果没有比ptrdiff_t更有优势,就不会存在了。但我不知道具体是什么。 - Cheers and hth. - Alf
@Cheersandhth.-Alf:至少在语义上,它比ptrdiff更适合表示大小。 - Deduplicator
显示剩余7条评论

73

数组的创建和初始化

与任何其他类型的C ++对象一样,数组可以直接存储在命名变量中(这时大小必须是编译时常量; C++不支持VLA(可变长数组)),或者它们可以在堆上匿名存储,并通过指针间接访问(只有在这种情况下,大小才能在运行时计算)。

自动数组

自动数组(存储于“栈”上的数组)每次控制流经过非静态本地数组变量的定义时创建:

void foo()
{
    int automatic_array[8];
}

初始化按升序执行。请注意,初始值取决于元素类型T

  • 如果TPOD(例如上面示例中的int),则不进行初始化。
  • 否则,T的默认构造函数将初始化所有元素。
  • 如果T没有提供可访问的默认构造函数,则程序无法编译。

或者,初始值可以在数组初始化器中明确指定,这是一个由花括号括起来的逗号分隔列表:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

在这种情况下,数组初始化器中的元素数量等于数组的大小,手动指定大小是冗余的。编译器可以自动推导出大小:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

还可以指定大小并提供更短的数组初始化程序:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

在这种情况下,剩余的元素将会被零初始化。请注意,C++允许空数组初始化器(所有元素都会被零初始化),而C89不允许(至少需要一个值)。另外要注意,数组初始化器只能用于初始化数组;它们不能在赋值中后续使用。

静态数组

静态数组(存储在数据段中的数组)是使用static关键字定义的局部数组变量以及命名空间作用域的数组变量(“全局变量”):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

请注意,命名空间作用域的变量隐式为静态变量。在其定义中添加static关键字具有完全不同、已弃用的含义

以下是静态数组与自动数组的差异:

  • 没有数组初始化程序的静态数组在任何可能的初始化之前都会进行零初始化。
  • 静态POD数组仅初始化一次,初始值通常嵌入到可执行文件中,因此运行时没有初始化成本。然而,这并不总是最节省空间的解决方案,也不是标准所要求的。
  • 静态非POD数组在流程控制第一次通过它们的定义时进行初始化。对于局部静态数组,如果从未调用函数,则可能永远不会发生。

(以上内容并非特定于数组。这些规则同样适用于其他类型的静态对象。)

数组数据成员

当创建它们所属的对象时,将创建数组数据成员。不幸的是,C++03没有提供在成员初始化列表中初始化数组的方法,因此必须使用赋值来伪造初始化:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

或者,您可以在构造函数主体中定义一个自动数组并将元素复制过去:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

在C++0x中,由于统一初始化,数组可以在成员初始化列表中进行初始化:
class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

这是唯一适用于没有默认构造函数的元素类型的解决方案。

动态数组

动态数组没有名称,因此唯一访问它们的方法是通过指针。因为它们没有名称,所以我将从现在开始称它们为"匿名数组"。

在C中,可以通过malloc和相关函数创建匿名数组。在C++中,可以使用new T[size]语法创建匿名数组,该语法将返回指向匿名数组第一个元素的指针:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

以下ASCII艺术展示了如果在运行时计算大小为8的内存布局:
             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

显然,由于需要单独存储额外指针,匿名数组比命名数组占用更多的内存。(自由存储区也会有一些额外开销。)

注意这里没有进行任何数组到指针的衰减。虽然评估new int[size]实际上会创建一个整数数组,但是表达式new int[size]的结果已经是单个整数(第一个元素)的指针,而不是整数数组或未知大小的整数数组的指针。那是不可能的,因为静态类型系统要求数组大小为编译时常量。 (因此,在图片中我没有注释匿名数组的静态类型信息。)

关于元素默认值,匿名数组的行为类似于自动数组。通常,匿名POD数组不会被初始化,但有一个特殊语法可以触发值初始化:

int* p = new int[some_computed_size]();

(注意分号前面的括号对。)同样,C++0x简化了规则,并通过统一初始化允许为匿名数组指定初始值:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

如果你使用完一个匿名数组,你需要将其释放回系统:

delete[] p;

您必须仅释放每个匿名数组一次,然后之后不再操作它。完全不释放会导致内存泄漏(或者更普遍地说,根据元素类型,可能会导致资源泄漏),尝试多次释放会导致未定义的行为。使用非数组形式的delete(或free)来释放数组也是未定义的行为


2
在C++11中,命名空间范围内的static用法被取消了弃用。 - legends2k
@Deduplicator 不行,因为从历史上看,“new”比引用要早得多。 - fredoverflow
@Deduplicator 运算符是否允许具有在运行时改变的返回类型? - Buge
好问题。如果new返回引用,那么new int[n]的类型会是什么,其中n是一个不静态已知的值? - fredoverflow
2
@Deduplicator 我认为不存在一个引用未知边界数组的方法。至少 g++ 拒绝编译 int a[10]; int (&r)[] = a; - fredoverflow
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接