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


480

C ++从C继承了数组,几乎可以在任何地方使用它们。C ++提供更易于抽象使用和更不容易出错(std::vector<T>因为C ++ 98和std::array<T, n>C ++ 11),因此需要对阵列不会出现相当经常,因为它在C.确实但是,当读取遗留编写代码或与用C编写的库进行交互,您应该对数组的工作原理有把握。

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

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

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

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

(注意:这本来是Stack Overflow的C ++ FAQ的一个条目。如果您想批评以这种形式提供FAQ的想法,则可以在开始所有这些操作的meta上进行发布。)该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)


如果指针总是指向起点,而不是指向目标的中间,那么它们会更好……
Deduplicator14年

您应该使用STL向量,因为它为您提供了更大的灵活性。
Moiz Sajid

2
结合使用std::arrays,std::vectors和gsl::spans-坦率地说,我希望获得有关如何在C ++中使用数组的常见问题解答,其内容是:“到现在为止,您可以开始考虑正确,使用它们了。”
einpoklum

Answers:


302

类型级别的数组

数组类型被表示为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*需要a的任何地方,您都可以提供a 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是一个指针变量(通过旁边的小方框可辨别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作为第一个参数。这对我来说有点太简洁了,这也使模板参数的推导对编译器来说有点困难,因为在这种情况下,第一个参数是一个数组,而第二个参数是一个指针。(再次,阵列到指针的衰减开始。)


其中阵列不衰变到一个指针的情况下是这里示出以供参考。
legends2k 2014年

@fredoverflow在访问或范围部分,可能值得一提的是C数组与基于C ++ 11基于范围的for循环一起使用。
gnzlbg

135

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

多维数组

大多数程序员都熟悉命名多维数组,但是许多程序员并不知道多维数组也可以匿名创建的事实。多维数组通常称为“数组数组”或“ true多维数组”。

命名多维数组

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

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;

至于建议:应该指出的是,那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在编译时是已知的。
RobertS支持Monica Cellio

88

分配

出于特殊原因,不能将数组彼此分配。使用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]) a它确实看起来像是按值传递数组,a在内部foo修改也会修改原始数组。这应该很清楚,因为数组不能被复制,但是加强它可能是值得的。
gnzlbg

C ++ 20具有ranges::copy(a, b)
LF

int sum( int size_, int a[size_]);-从(我认为)C99起
角斗士厨师

73

5.使用数组时的常见陷阱。

5.1陷阱:信任类型不安全的链接。

好的,您已经被告知或发现自己,全局变量(可以在翻译单元外部访问的命名空间范围变量)是Evil™。但是您知道它们到底有多邪恶吗?考虑下面的程序,它由两个文件[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进行编译和链接。

由于类型不匹配,因此运行该程序时会使其崩溃。

Windows 7崩溃对话框

正式的解释:该程序具有未定义行为(UB),因此它不会崩溃也可以挂掉,或者什么都不做,或者可以向美国,俄罗斯,印度,中国和瑞士,让鼻守护者从你的鼻子中飞出来。

实践中的解释:main.cpp将数组中的指针视为指针,放置在与数组相同的地址处。对于32位可执行文件,这意味着int将数组中的第一个 值视为指针。即,在main.cppnumbers变量包含或看似包含(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] ))

由于array在需要时会衰减指向第一个元素的指针,因此表达式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个元素,调用显示...
1个元素。

  1. 编译器将重写int const a[7]为just 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个元素,调用display ...
断言失败:(“ N_ITEMS需要一个实际数组作为参数”,typeid(a)!= typeid(&* a)),文件runtime_detect ion.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 ++ 14陷阱:使用constexpr数组大小函数。

使用C ++ 11和更高版本是很自然的,但是您会发现很危险!替换C ++ 03函数

typedef ptrdiff_t   Size;

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

using Size = ptrdiff_t;

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

其中使用的重要改变是constexpr允许该函数产生编译时间常数

例如,与C ++ 03函数相反,可以使用这样的编译时间常数来声明一个与另一个大小相同的数组:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

但是考虑使用以下constexpr版本的代码:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

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

陷阱:截至2015年7月,上述代码可使用MinGW-64 5.1.0 -pedantic-errors和进行编译,并使用gcc.godbolt.org/的在线编译器进行测试,也可以使用clang 3.0和clang 3.2,但不能使用clang 3.3、3.4进行编译。 1、3.5.0、3.5.1、3.6(rc1)或3.7(实验性)。对于Windows平台来说很重要,它不能与Visual C ++ 2015一起编译。原因是有关在C ++ 11 / C ++ 14中使用引用的声明constexpr表达式中:

C ++ 11 C ++ 14 $ 5.19 / 2 9 短划线

条件表达式 e是一个核心常量表达式除非的评价e,如下所述抽象机(1.9),将评估下面的表达式中的一个的规则:
        ⋮

  • 引用引用类型的变量或数据成员的id表达式,除非引用具有先前的初始化且
    • 用常量表达式初始化它,或者
    • 它是对象的非静态数据成员,其生存期始于e的评估;

总是可以写得更详细

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

…但这在Collection不是原始数组时会失败。

为了处理可能是非数组的集合,需要n_items函数的可重载 性,但是,对于编译时的使用,则需要数组大小的编译时表示。而经典的C ++ 03解决方案(也可以在C ++ 11和C ++ 14中正常工作)是让函数不通过值报告结果,而是通过函数结果类型报告结果。例如这样:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

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

关于为的返回类型的选择static_n_items:此代码不使用,std::integral_constant 因为std::integral_constant结果直接表示为constexpr值,从而重新引入了原始问题。Size_carrier可以让函数直接返回对数组的引用来代替类。但是,并不是每个人都熟悉该语法。

关于命名:constexpr-invalid-due-to-reference问题的这种解决方案的一部分是使编译时间常数的选择明确。

希望oops那里有一个涉及您的引用的constexpr问题将在C ++ 17中解决,但是在此之前,像STATIC_N_ITEMS上面这样的宏才具有可移植性,例如到clang和Visual C ++编译器,保留类型安全。

相关:宏不遵守作用域,因此为了避免名称冲突,最好使用名称前缀,例如MYLIB_STATIC_N_ITEMS


1
+1出色的C编码测试:我已经在VC ++ 10.0和GCC 4.1.2上花费了15分钟的时间来尝试解决此问题Segmentation fault……在阅读您的解释后,我终于找到/理解了!请写您的
§5.2

好。一个尼特-countOf的返回类型应该是size_t而不是ptrdiff_t。可能值得一提的是,在C ++ 11/14中,它应该是constexpr而没有例外。
Ricky65 2014年

@ Ricky65:感谢您提到C ++ 11注意事项。对于Visual C ++,对这些功能的支持已经很晚了。关于size_t,我在现代平台上没有任何优势,但是由于C和C ++的隐式类型转换规则,它存在许多问题。也就是说,ptrdiff_t非常有意地使用它来避免的问题size_t。但是,应该知道,g ++在将数组大小与模板参数匹配方面存在问题,除非是这样size_t(我不认为非特定于编译器的问题size_t很重要,但是YMMV)。
干杯和健康。-Alf 2014年

@Alf。在标准工作草案(N3936)8.3.4中,我读到-数组的界限是……“一个转换后的常量表达式,类型为std :: size_t,其值应大于零”。
Ricky65 2014年

@Ricky:如果您指的是不一致,那么当前的C ++ 11标准中就没有此语句,因此很难猜测上下文,但是存在矛盾(动态分配的数组可以在C中绑定为0。 +11§5.3.4/ 7)可能不会以C ++ 14结尾。草稿就是这样:草稿。如果您要问的是“它”指的是什么,它指的是原始表达式,而不是转换后的表达式。如果您在第三手提到这一点是因为您认为也许这样的句子意味着一个人应该使用它size_t来表示数组的大小,那当然不是。
干杯和健康。-阿尔夫

72

数组创建和初始化

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

自动阵列

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

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

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

  • 如果TPODint如上例所示),则不会进行初始化。
  • 否则,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语言中,匿名数组是通过malloc和朋友创建的。在C ++中,匿名数组是使用以下new T[size]语法创建的,该语法返回一个指向匿名数组第一个元素的指针:

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

如果在运行时将大小计算为8,则以下ASCII文字描述了内存布局:

             +---+---+---+---+---+---+---+---+
(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)代替delete[]释放数组也是未定义的行为


2
static在C ++ 11中删除了对命名空间范围中使用的弃用。
legends2k 2013年

因为new是am运算符,所以它肯定可以通过引用返回分配的数组。只是没有意义...
Deduplicator

@Deduplicator不行,因为历史上new比参考早很多。
fredoverflow

@FredOverflow:因此,有一个原因它无法返回引用,这与书面说明完全不同。
重复数据删除器2014年

2
@Deduplicator我认为不存在对未知范围数组的引用。至少g ++拒绝编译int a[10]; int (&r)[] = a;
fredoverflow 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.