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

How do I use arrays in C++?

提问人:fredoverflow 提问时间:1/27/2011 最后编辑:FeRDfredoverflow 更新时间:9/15/2023 访问量:140599

问:

C++ 继承了 C 的数组,它们几乎无处不在。C++ 提供了更易于使用且不易出错的抽象(自 C++98 和 std::array<T, n>C++11 以来),因此对数组的需求并不像在 C 中那样频繁出现。但是,当您阅读遗留代码或与用 C 编写的库进行交互时,您应该牢牢掌握数组的工作原理。std::vector<T>

本常见问题解答分为五个部分:

  1. 类型级别的数组和访问元素
  2. 阵列创建和初始化
  3. 赋值和参数传递
  4. 多维数组和指针数组
  5. 使用数组时的常见陷阱

如果您觉得本常见问题解答中缺少一些重要内容,请写下答案并将其作为附加部分链接到此处。

在下面的文本中,“array”表示“C数组”,而不是类模板。假定您具备 C 声明符语法的基本知识。请注意,在遇到异常时,手动使用 and(如下所述)是极其危险的,但这是另一个 FAQ 的主题。std::arraynewdelete


(注意:这是Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供常见问题解答的想法,那么在开始这一切的 meta 上的帖子将是这样做的地方。该问题的答案在 C++ 聊天室中受到监控,FAQ 的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。

C 多维数组 C++-FAQ

评论

0赞 Deduplicator 7/8/2014
如果指针总是指向起点而不是目标中间的某个地方,他们会更好......
0赞 Moiz Sajid 8/30/2015
您应该使用 STL Vector,因为它为您提供了更大的灵活性。
2赞 einpoklum 3/19/2017
随着 s、s 和 s 的组合可用性 - 坦率地说,我希望有一个关于如何在 C++ 中使用数组的常见问题解答,说“到现在为止,你可以开始考虑,好吧,使用它们。std::arraystd::vectorgsl::span

答:

322赞 fredoverflow 1/27/2011 #1

类型级别的数组

数组类型表示为 其中 是元素类型,并且是正大小,即数组中的元素数。数组类型是元素类型和大小的乘积类型。如果其中一种或两种成分不同,则会得到不同的类型:T[n]Tn

#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)

数组到指针衰减

和之间唯一的“联系”是这两种类型都可以隐式转换为 ,并且此转换的结果是指向数组第一个元素的指针。也就是说,只要需要 a,就可以提供 ,编译器将静默地提供该指针: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++ 提供了两种语法变体来访问数组的各个元素。 它们都不优于另一个,您应该熟悉两者。

指针算术

给定指向数组第一个元素的指针,表达式将生成指向数组的第 i 个元素的指针。通过之后取消引用该指针,可以访问各个元素:pp+i

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

如果表示一个数组,那么数组到指针的衰减就会开始,因为添加数组和整数是没有意义的(数组上没有加号运算),但添加指针和整数是有意义的:x

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(请注意,隐式生成的指针没有名称,因此我编写了该指针以识别它。x+0

另一方面,如果表示指向数组的第一个(或任何其他)元素的指针,则不需要数组到指针衰减,因为要添加的指针已经存在:xi

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

请注意,在所描述的情况下,是一个指针变量(可以通过旁边的小框来识别),但它也可能是返回指针(或任何其他类型的表达式)的函数的结果。xxT*

索引运算符

由于语法有点笨拙,C++ 提供了替代语法:*(x+i)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]xx&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

在第一行,编译器检测到从指针到指针的赋值,这很容易成功。在第二行上,它检测从数组到指针的赋值。由于这毫无意义(但指针到指针的分配是有意义的),因此数组到针的衰减会像往常一样启动。

范围

类型的数组具有元素,索引从 到 ;没有元素.然而,为了支持半开放范围(其中开头是包含的,结尾是排他性的),C++ 允许计算指向(不存在的)第 n 个元素的指针,但取消引用该指针是非法的:T[n]n0n-1n

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

例如,如果要对数组进行排序,以下两种方法同样有效:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

请注意,作为第二个参数提供是非法的,因为这等效于 ,并且子表达式在技术上调用 C++ 中的未定义行为(但在 C99 中不调用)。&x[n]&*(x+n)*(x+n)

另请注意,您可以简单地提供作为第一个参数。对于我的口味来说,这有点太简洁了,而且对于编译器来说,这也使模板参数推导有点困难,因为在这种情况下,第一个参数是一个数组,但第二个参数是一个指针。(同样,数组到指针的衰减开始了。x

评论

2赞 legends2k 1/6/2014
此处说明了数组不会衰减为指针的情况,以供参考。
1赞 gnzlbg 6/29/2017
@fredoverflow 在“访问”或“范围”部分中,可能值得一提的是,C 数组使用基于 C++11 范围的 for 循环。
1赞 WhozCraig 2/15/2021
出色的答案。“这种转换被称为”数组到指针衰减“,它是混淆的主要来源”的说法是准确的,这在很大程度上是因为它仅在通用语中“已知”。在语言草案或标准中,在描述转换为临时指针的上下文时,甚至没有使用过一次这种命名法。
93赞 fredoverflow 1/27/2011 #2

分配

由于没有特殊原因,数组不能相互分配。请改用: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::copystd::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赞 gnzlbg 6/29/2017
添加一个注释可能是值得的,即使看起来确实有人按值传递数组,修改内部将修改原始数组。这应该很清楚,因为数组无法复制,但加强这一点可能是值得的。void foo(int a[3])aafoo
0赞 L. F. 9/19/2019
C++ 有ranges::copy(a, b)
0赞 Chef Gladiator 3/18/2020
int sum( int size_, int a[size_]);-- 从(我认为)C99 开始
147赞 fredoverflow 1/27/2011 #3

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

多维数组

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

命名多维数组

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

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**hT

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赞 RobertS supports Monica Cellio 2/17/2020
建议:您应该指出,以及 和 是有效的语句,when 和 在编译时是已知的。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];HW
0赞 Borko Djurovic 12/27/2020
非常感谢!请告诉我如何从数组(“指针的匿名数组”)中设置/获取元素。
0赞 philb 3/3/2021
(编辑队列已满,所以我正在发表评论)我最好明确地提到,要释放一个匿名的多维数组,正确的语法很简单delete[] p
73赞 fredoverflow 2/13/2011 #4

阵列创建和初始化

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

自动阵列

每次控制流通过非静态局部数组变量的定义时,都会创建自动数组(位于“堆栈”上的数组):

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

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

  • 如果是 POD(如上例所示),则不进行初始化。Tint
  • 否则,default-constructor 将初始化所有元素。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 语言中,匿名数组是通过 和 友元创建的。在 C++ 中,匿名数组是使用返回指向匿名数组的第一个元素的指针的语法创建的:mallocnew 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;

您必须只释放每个匿名数组一次,然后再也不碰它。完全不释放它会导致内存泄漏(或者更一般地说,取决于元素类型,资源泄漏),并且尝试多次释放它会导致未定义的行为。使用非数组形式(或)而不是释放数组也是未定义的行为deletefreedelete[]

评论

2赞 legends2k 5/17/2013
在 C++11 中删除了命名空间范围内弃用的用法。static
0赞 Deduplicator 6/28/2014
因为是 am 运算符,所以它当然可以通过引用返回 allcated 数组。这毫无意义......new
0赞 fredoverflow 6/28/2014
@Deduplicator 不,它不可能,因为从历史上看,它比参考文献要古老得多。new
0赞 Deduplicator 6/29/2014
@FredOverflow:所以它不能返回参考文献是有原因的,它只是与书面解释完全不同。
2赞 fredoverflow 8/28/2014
@Deduplicator 我不认为存在对未知边界数组的引用。至少 g++ 拒绝编译int a[10]; int (&r)[] = a;
77赞 Cheers and hth. - Alf 9/16/2011 #5

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),而是 因此,崩溃它可以挂起,或者什么都不做,或者它 可以向美国、俄罗斯、印度的总统发送威胁性电子邮件, 中国和瑞士,让鼻腔恶魔从你的鼻子里飞出来。

实践解释:在数组中被视为指针,放置 与阵列位于同一地址。对于 32 位可执行文件,这意味着数组中的第一个值被视为指针。即,在变量中包含或似乎包含。这会导致 程序访问地址空间最底部的内存,即 传统上是保留的和陷阱造成的。结果:你崩溃了。main.cppintmain.cppnumbers(int*)1

编译器完全有权不诊断此错误, 因为 C++11 §3.5/10 说,关于兼容类型的要求 对于声明,

[N3290 §3.5/10]
违反此类型标识规则不需要诊断。

同一段落详细说明了允许的变体:

...Array 对象的声明可以指定 区别在于是否存在主数组绑定 (8.3.4)。

这种允许的变体不包括将名称声明为一个数组 翻译单元,并作为另一个翻译单元中的指针。

5.2 陷阱:过早优化(&朋友)。memset

还没写

5.3 陷阱:使用 C 习语获取元素数量。

有了深厚的 C 经验,写 ...

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

由于 an 在需要的地方衰减为指向第一个元素的指针,因此 表达式也可以写成 .它的意思是一样的,不管它如何 写成它是用于查找数组的数字元素的 C 习语arraysizeof(a)/sizeof(a[0])sizeof(a)/sizeof(*a)

主要缺陷: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 );
}

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

7 个元素,调用显示...
1 个元素。

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

为了在运行时检测此错误,您可以执行...

#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 需要一个实际的数组作为参数”, typeid( a ) != typeid( &*a ) ), file runtime_detect 离子.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使用引用n之前,不允许使用常量表达式,并且所有主要编译器都拒绝此代码。来自 C++20 标准,[expr.const] p5.12c

表达式是核心常量表达式,除非 的计算结果遵循抽象机器的规则,将计算以下值之一:EE

  • [...]
  • 一个 id 表达式,它引用引用类型的变量或数据成员,除非该引用具有前面的初始化,并且
    • 它可用于常量表达式或
    • 它的生命周期始于 E 的评估;

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

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

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

为了处理可以是非数组的集合,需要函数的可重载性,但对于编译时使用,还需要编译时 数组大小的表示形式。还有经典的 C++03 解决方案,运行良好 同样在 C++11 和 C++14 中,是让函数报告其结果而不是作为值 而是通过其函数结果类型。例如,像这样:size

// 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_itemsstd::integral_constantstd::integral_constantconstexpr

关于命名:此解决方案的一部分是 -invalid-due-to-reference 问题在于明确选择编译时常量。constexpr

在 C++23 之前,像上面这样的宏会产生可移植性, 例如,对 clang 和 Visual C++ 编译器,保留类型安全。STATIC_N_ITEMS

相关:宏不尊重作用域,因此为了避免名称冲突,它可以是 使用名称前缀的好主意,例如 .MYLIB_STATIC_N_ITEMS

评论

1赞 oHo 11/8/2012
+1 很棒的 C 编码测试:我在 VC++ 10.0 和 GCC 4.1.2 上花了 15 分钟试图修复......看完你的解释后,我终于找到/理解了!请写下你的§5.2部分:-)干杯Segmentation fault
0赞 Ricky65 5/28/2014
好。一个尼特 - countOf 的返回类型应该是 size_t 而不是 ptrdiff_t。可能值得一提的是,在 C++11/14 中,它应该是 constexpr 和 noexcept。
0赞 Cheers and hth. - Alf 5/28/2014
@Ricky65:感谢您提及C++11 注意事项。Visual C++ 对这些功能的支持迟迟没有出现。关于,对于现代平台来说,这没有我所知道的优势,但由于 C 和 C++ 的隐式类型转换规则,它存在许多问题。也就是说,非常有意地使用,以避免 .但是,应该注意的是,g++ 在将数组大小与模板参数匹配时存在问题,除非它是(我认为这个特定于编译器的 non- 问题并不重要,但 YMMV)。size_tptrdiff_tsize_tsize_tsize_t
0赞 Ricky65 5/28/2014
@Alf。在标准工作草案 (N3936) 8.3.4 中,我读到 - 数组的边界是......“类型为 std::size_t 的转换常量表达式,其值应大于零”。
0赞 Cheers and hth. - Alf 5/28/2014
@Ricky:如果你指的是不一致,这个语句在当前的 C++11 标准中不存在,所以很难猜测上下文,但矛盾(根据 C++11 §5.3.4/7,动态分配的数组可以是边界 0)可能不会在 C++14 中结束。草稿就是这样:草稿。如果你问的是“它的”指的是什么,它指的是原始表达,而不是转换后的表达。如果第三,你提到这一点是因为你认为也许这样的句子意味着应该用来表示数组的大小,不,当然不是。size_t