504

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

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

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

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

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


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

4

5 回答 5

314

类型级别的数组

数组类型表示为元素类型和正大小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)

数组到指针衰减

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元素,索引从0to 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作为第一个参数。这对我的口味来说有点太简洁了,而且它也使得模板参数推导对编译器来说有点困难,因为在这种情况下,第一个参数是一个数组,而第二个参数是一个指针。(同样,数组到指针的衰减开始了。)

于 2011-01-26T22:14:47.427 回答
142

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

多维数组

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

命名多维数组

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

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]to的隐式转换T**。如果确实存在这样的隐式转换,则结果将是指向指针数组的第一个元素的h指针T(每个指针指向原始二维数组中一行的第一个元素),但该指针数组在任何地方都不存在记忆犹新。如果你想要这样的转换,你必须手动创建并填充所需的指针数组:

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;
于 2011-01-26T22:15:30.997 回答
91

任务

没有特别的原因,数组不能相互分配。改用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);
}

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

于 2011-01-26T22:15:14.367 回答
73

数组创建和初始化

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

自动阵列

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

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

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

  • 如果TPODint如上例),则不会进行初始化。
  • 否则,T初始化所有元素的默认构造函数。
  • 如果T没有提供可访问的默认构造函数,则程序不会编译。

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

    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[]释放数组也是未定义行为

于 2011-02-13T12:52:49.123 回答
73

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;
}

// [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.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] ))

由于 anarray在需要的地方衰减为指向第一个元素的指针,因此表达式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]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 需要一个实际数组作为参数”,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:\计数> _

它是如何工作的:数组通过引用传递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 编译,并使用gcc.godbolt.org/-pedantic-errors上的在线编译器进行测试,也使用 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第九 破折号

条件表达式 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-there-was-a-reference-involved-in-your-constexpr问题将在 C++17 中得到修复,但在此之前,像STATIC_N_ITEMS上面这样的宏会产生可移植性,例如对于 clang 和 Visual C++ 编译器,保留类型安全。

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

于 2011-09-16T01:31:10.727 回答