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

fre*_*low 469 c++ arrays pointers c++-faq multidimensional-array

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

本FAQ分为五个部分:

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

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

在下文中,"数组"表示"C数组",而不是类模板std::array.假定了C声明符语法的基本知识.请注意,面对异常,手动使用newdelete如下所示是非常危险的,但这是另一个常见问题解答的主题.

(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)

fre*_*low 297

类型级别的数组

数组类型被表示为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");
Run Code Online (Sandbox Code Playgroud)

请注意,大小是类型的一部分,也就是说,不同大小的数组类型是完全无关的不兼容类型.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*
Run Code Online (Sandbox Code Playgroud)

这种转换被称为"阵列到指针衰减",它是混淆的主要来源.在此过程中,数组的大小将丢失,因为它不再是type(T*)的一部分.Pro:忘记类型级别上的数组大小允许指针指向任何大小的数组的第一个元素.Con:给定指向数组的第一个(或任何其他)元素的指针,无法检测该数组的大小或指针相对于数组边界的确切位置.指针极其愚蠢.

数组不是指针

只要数据被认为有用,编译器就会静默地生成一个指向数组第一个元素的指针,也就是说,只要操作在数组上失败但在指针上成功.这种从数组到指针的转换是微不足道的,因为结果指针只是数组的地址.请注意,指针不会存储为数组本身(或内存中的任何其他位置)的一部分.数组不是指针.

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");
Run Code Online (Sandbox Code Playgroud)

在其中一个阵列并一个重要方面当衰变到一个指向它的第一个元素是&操作者施加于它.在这种情况下,&运算符产生一个指向整个数组的指针,而不仅仅是指向其第一个元素的指针.虽然在这种情况下(地址)是相同的,但是指向数组的第一个元素的指针和指向整个数组的指针是完全不同的类型:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");
Run Code Online (Sandbox Code Playgroud)

以下ASCII艺术解释了这种区别:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]
Run Code Online (Sandbox Code Playgroud)

注意指向第一个元素的指针如何仅指向一个整数(描述为一个小框),而指向整个数组的指针指向一个包含8个整数的数组(描述为一个大框).

同样的情况出现在课堂上,可能更明显.指向对象的指针和指向其第一个数据成员的指针具有相同的(相同的地址),但它们是完全不同的类型.

如果您不熟悉C声明符语法,则类型int(*)[8]中的括号必不可少:

  • int(*)[8] 是一个指向8个整数数组的指针.
  • int*[8]是一个由8个指针组成的数组,每个元素都是类型int*.

访问元素

C++提供了两种语法变体来访问数组的各个元素.它们都不优于另一个,你应该熟悉两者.

指针算术

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

std::cout << *(x+3) << ", " << *(x+7) << std::endl;
Run Code Online (Sandbox Code Playgroud)

如果x表示一个数组,那么数组到指针的衰减将会启动,因为添加一个数组和一个整数是没有意义的(数组上没有加法操作),但添加指针和整数是有意义的:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*
Run Code Online (Sandbox Code Playgroud)

(注意,隐式生成的指针没有名称,所以我写x+0了以便识别它.)

如果,在另一方面,x表示一个指针到第一(或任何其他)的数组的元素,那么阵列到指针衰变是没有必要的,因为该指针在其i将要加入已经存在:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+
Run Code Online (Sandbox Code Playgroud)

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

索引运算符

由于语法*(x+i)有点笨拙,C++提供了替代语法x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;
Run Code Online (Sandbox Code Playgroud)

由于添加是可交换的,因此以下代码完全相同:

std::cout << 3[x] << ", " << 7[x] << std::endl;
Run Code Online (Sandbox Code Playgroud)

索引运算符的定义导致以下有趣的等价:

&x[i]  ==  &*(x+i)  ==  x+i
Run Code Online (Sandbox Code Playgroud)

但是,&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
Run Code Online (Sandbox Code Playgroud)

在第一行,编译器检测指向指针的指定,这很简单.在第二行,它检测从数组到指针的赋值.由于这是没有意义的(但指针指向指针是有意义的),数组到指针衰减像往常一样开始.

范围

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

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*
Run Code Online (Sandbox Code Playgroud)

例如,如果要对数组进行排序,则以下两个方法都可以正常工作:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);
Run Code Online (Sandbox Code Playgroud)

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

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

  • 数组不衰减为指针的情况[此处说明](http://stackoverflow.com/a/2036125/183120)供参考。 (2认同)

fre*_*low 133

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

多维数组

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

命名多维数组

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

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
Run Code Online (Sandbox Code Playgroud)

这就是命名多维数组在内存中的样子:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

请注意,上面的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);
Run Code Online (Sandbox Code Playgroud)

匿名多维数组

对于匿名多维数组,除了第一个之外的所有维必须在编译时知道:

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
Run Code Online (Sandbox Code Playgroud)

这就是匿名多维数组在内存中的样子:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+
Run Code Online (Sandbox Code Playgroud)

请注意,数组本身仍然作为单个块分配在内存中.

指针数组

您可以通过引入另一个间接级别来克服固定宽度的限制.

命名的指针数组

这是一个由五个指针组成的命名数组,它们使用不同长度的匿名数组进行初始化:

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];
}
Run Code Online (Sandbox Code Playgroud)

以下是它在内存中的样子:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

由于现在每条线都是单独分配的,因此将2D数组视为一维数组不再有效.

匿名指针数组

这是一个包含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[] !
Run Code Online (Sandbox Code Playgroud)

以下是它在内存中的样子:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+
Run Code Online (Sandbox Code Playgroud)

转换

数组到指针的衰减自然会扩展到数组和指针数组:

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;
Run Code Online (Sandbox Code Playgroud)

但是,从没有隐式转换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;
Run Code Online (Sandbox Code Playgroud)

请注意,这会生成原始多维数组的视图.如果您需要副本,则必须创建额外的数组并自行复制数据:

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;
Run Code Online (Sandbox Code Playgroud)


fre*_*low 87

分配

无特殊原因,不能将数组分配给彼此.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);
Run Code Online (Sandbox Code Playgroud)

这比真正的数组赋值更灵活,因为可以将较大数组的片段复制到较小的数组中. 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);
}
Run Code Online (Sandbox Code Playgroud)

作为一种语法替代方法,您还可以将参数声明为T p[],并且它T* p 仅与参数列表的上下文完全相同:

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}
Run Code Online (Sandbox Code Playgroud)

你能想到的编译器为改写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
Run Code Online (Sandbox Code Playgroud)

通过引用传递

数组也可以通过引用传递:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,阵列大小很重要.由于编写一个只接受8个元素的数组的函数几乎没用,程序员通常会将这些函数写成模板:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}
Run Code Online (Sandbox Code Playgroud)

请注意,您只能使用实际的整数数组调用此类函数模板,而不能使用指向整数的指针.自动推断数组的大小,并且对于每个大小n,从模板实例化不同的函数.您还可以编写非常有用的函数模板,从元素类型和大小中抽象出来.

  • 可能值得添加一个注释,即使在`void foo(int a [3])``a`看起来像是通过值传递数组,修改`foo`里面的'a`会修改原文阵列.这应该是清楚的,因为数组不能被复制,但是强化它可能是值得的. (2认同)

fre*_*low 71

数组创建和初始化

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

自动数组

每当控制流通过非静态局部数组变量的定义时,就会创建自动数组(生成在堆栈中的数组):

void foo()
{
    int automatic_array[8];
}
Run Code Online (Sandbox Code Playgroud)

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

  • 如果TPOD(int如上例所示),则不进行初始化.
  • 否则,default-constructor T初始化所有元素.
  • 如果T没有提供可访问的默认构造函数,则程序不会编译.

或者,可以在数组初始值设定项中显式指定初始值,数组初始值设定项是用大括号括起来的逗号分隔列表:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};
Run Code Online (Sandbox Code Playgroud)

由于在这种情况下,数组初始值设定项中的元素数量等于数组的大小,因此手动指定大小是多余的.它可以由编译器自动推导出来:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced
Run Code Online (Sandbox Code Playgroud)

也可以指定大小并提供更短的数组初始值设定项:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced
Run Code Online (Sandbox Code Playgroud)

在这种情况下,剩余的元素是零初始化的.请注意,C++允许空数组初始化器(所有元素都是零初始化),而C89不允许(至少需要一个值).另请注意,数组初始值设定项只能用于初始化数组; 它们以后不能用于作业.

静态数组

静态数组(生成在"数据段"中的数组)是使用static命名空间范围内的关键字和数组变量定义的局部数组变量("全局变量"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}
Run Code Online (Sandbox Code Playgroud)

(请注意,命名空间范围内的变量是隐式静态的.将static关键字添加到其定义中具有完全不同的,已弃用的含义.)

以下是静态数组与自动数组的行为方式不同:

  • 没有数组初始值设定项的静态数组在进一步潜在初始化之前进行零初始化.
  • 静态POD阵列只被初始化一次,初始值通常被烘焙到可执行文件中,在这种情况下,运行时没有初始化成本.然而,这并不总是最节省空间的解决方案,并且标准不要求它.
  • 静态非POD阵列在控制流第一次通过其定义初始化.在本地静态数组的情况下,如果永远不调用该函数,则可能永远不会发生这种情况.

(以上都不是特定于数组的.这些规则同样适用于其他类型的静态对象.)

数组数据成员

创建其拥有对象时,将创建阵列数据成员.不幸的是,C++ 03没有提供在成员初始化列表中初始化数组的方法,因此必须使用赋值伪装初始化:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

在C++ 0x中,由于统一初始化,可以在成员初始化列表中初始化数组:

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};
Run Code Online (Sandbox Code Playgroud)

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

动态数组

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

在C中,通过malloc和朋友创建匿名数组.在C++中,使用new T[size]语法创建匿名数组,该语法返回指向匿名数组的第一个元素的指针:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];
Run Code Online (Sandbox Code Playgroud)

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

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+
Run Code Online (Sandbox Code Playgroud)

显然,由于必须单独存储的额外指针,匿名数组比命名数组需要更多内存.(免费商店还有一些额外的开销.)

请注意,这里没有数组到指针的衰减.尽管评估new int[size]事实上确实创建一个阵列的整数,该表达式的结果new int[size]已经一个指向一个整数(第一元件),整数数组或指针未知大小的整数数组.这是不可能的,因为静态类型系统要求数组大小为编译时常量.(因此,我没有在图片中用静态类型信息注释匿名数组.)

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

int* p = new int[some_computed_size]();
Run Code Online (Sandbox Code Playgroud)

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

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };
Run Code Online (Sandbox Code Playgroud)

如果您使用匿名数组,则必须将其释放回系统:

delete[] p;
Run Code Online (Sandbox Code Playgroud)

您必须准确释放每个匿名数组一次,然后再不再触摸它.根本不释放它会导致内存泄漏(或更一般地,取决于元素类型,资源泄漏),并且尝试多次释放会导致未定义的行为.使用非数组形式delete(或free)而不是delete[]释放数组也是未定义的行为.

  • 在C++ 11中删除了名称空间范围中"静态"用法的弃用. (2认同)
  • @Deduplicator我不认为存在对未知边界数组的引用.至少g ++拒绝编译`int a [10]; int(&r)[] = a;` (2认同)

Che*_*Alf 70

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

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

好的,你已经被告知,或者自己发现,全局变量(可以在翻译单元外访问的命名空间范围变量)是Evil™.但是你知道他们是多么真实的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;
}
Run Code Online (Sandbox Code Playgroud)

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Run Code Online (Sandbox Code Playgroud)

在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] ))
Run Code Online (Sandbox Code Playgroud)

由于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 );
}
Run Code Online (Sandbox Code Playgroud)

将指针传递给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 )                            \
    )
Run Code Online (Sandbox Code Playgroud)

7个元素,调用显示...
断言失败:("N_ITEMS需要一个实际数组作为参数",typeid(a)!= typeid(&*a)),文件runtime_detect ion.cpp,第16行

此应用程序已请求Runtime以不寻常的方式终止它.
有关更多信息,请联系应用程序的支持团队.

运行时错误检测优于无检测,但它浪费了一点处理器时间,也许程序员时间更长.在编译时检测更好!如果你很高兴不支持使用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 )
Run Code Online (Sandbox Code Playgroud)

编译这个定义代替第一个完整的程序,用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; }
Run Code Online (Sandbox Code Playgroud)

using Size = ptrdiff_t;

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

其中重要的变化是使用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.
}
Run Code Online (Sandbox Code Playgroud)

但请考虑使用以下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 );
}
Run Code Online (Sandbox Code Playgroud)

缺陷:截至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进行编译.原因是关于在constexpr表达式中使用引用的C++ 11/C++ 14语句:

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

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

  • 一个id-expression,引用引用类型的变量或数据成员,除非引用具有先前的初始化和任何一个
    • 它是用常量表达式初始化的
    • 它是一个对象的非静态数据成员,其生命周期始于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
}
Run Code Online (Sandbox Code Playgroud)

...但是当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 );
}
Run Code Online (Sandbox Code Playgroud)

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

关于命名:这个constexpr-invalid-due-to-reference问题解决方案的一部分是使编译时选择显式是明确的.

希望oops-there-a-reference-in-your- constexprissue将在C++ 17中修复,但在此之前,像STATIC_N_ITEMS上面这样的宏会产生可移植性,例如clang和Visual C++编译器,保留类型安全.

相关:宏不遵守范围,因此为了避免名称冲突,使用名称前缀是个好主意,例如MYLIB_STATIC_N_ITEMS.