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

C ++从C中inheritance了几乎在任何地方使用它们的数组。 C ++提供了易于使用且易于出错的抽象(从C ++ 98开始,自C ++ 98和std::array<T, n>之后的std::vector<T> ),所以对数组的需求不会的出现频率与C中的频率相同。但是,当您阅读遗留代码或与用C编写的库进行交互时,应该牢牢掌握arrays的工作方式。

这个FAQ分为五个部分:

  1. types级别的数组和访问元素
  2. 数组创build和初始化
  3. 分配和parameter passing
  4. multidimensional array和指针数组
  5. 使用数组时常见的缺陷

如果您觉得本FAQ中缺less重要的内容,请写下答案,并将其作为附加链接。

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

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。

在types级别的数组

数组types表示为T[n] ,其中T元素typesn是正数大小 ,即数组中元素的数量。 数组types是元素types和大小的产品types。 如果其中一种或两种成分不同,则会得到不同的types:

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

请注意,大小是types的一部分,也就是说,不同大小的数组types是不相容的types,它们之间完全没有关系。 sizeof(T[n])等于n * sizeof(T)

数组到指针的衰减

T[n]T[m]之间唯一的“连接”是两种types都可以隐式转换T* ,而这种转换的结果是指向数组的第一个元素的指针。 也就是说,任何需要T*地方,你都可以提供一个T[n] ,编译器会默默地提供这个指针:

  +---+---+---+---+---+---+---+---+ the_actual_array: | | | | | | | | | int[8] +---+---+---+---+---+---+---+---+ ^ | | | | pointer_to_the_first_element int* 

这种转换被称为“数组到指针的衰减”,是混淆的一个主要来源。 在这个过程中数组的大小会丢失,因为它不再是types的一部分( T* )。 Pro:在types级别上忘记数组的大小允许一个指针指向任何大小的数组的第一个元素。 Con:给出一个指向数组的第一个(或其他)元素的指针,没有办法检测到该数组有多大,或者指针指向的是数组的边界。 指针是非常愚蠢的 。

数组不是指针

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

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

一个数组不会衰变成指向其第一个元素的重要上下文是当&运算符应用于它时。 在这种情况下, &运算符产生一个指向整个数组的指针,而不仅仅是指向其第一个元素的指针。 尽pipe在这种情况下 (地址)是相同的,但是指向数组的第一个元素的指针和指向整个数组的指针是完全不同的types:

 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个整数组成的数组(描述为一个大框)。

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

如果您不熟悉C语言的语法, int(*)[8]types的圆括号是必不可less的:

  • int(*)[8]是一个指向8个整数数组的指针。
  • int*[8]是8个指针的数组,每个int*types的元素。

访问元素

C ++提供了两种语法变体来访问数组中的各个元素。 他们都不比别人优越,你们应该熟悉这两者。

指针算术

给定一个指向数组第一个元素的指针p ,expression式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是一个指针variables (可通过x旁边的小方块辨别),但它也可以是返回指针(或任何其他T*typesexpression式)的函数的结果。

索引操作符

由于语法*(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]types的数组有n元素,索引从0n-1 ; 没有元素n 。 然而,为了支持半开范围(其中开始是包含的 ,末尾是排他的 ),C ++允许计算指向(不存在的)第n个元素的指针,但是取消引用该指针是非法的:

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

例如,如果你想对一个数组进行sorting,下面两个方法同样适用:

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

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

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

程序员经常混淆multidimensional array和指针数组。

multidimensional array

大多数程序员都熟悉命名的multidimensional array,但是很多程序员并不知道multidimensional array也可以匿名创build。 multidimensional array通常被称为“数组arrays”或“ 真正的multidimensional array”。

命名multidimensional array

当使用命名的multidimensional array时,必须在编译时知道所有维度:

 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 

这就是命名的multidimensional array在内存中的样子:

  +---+---+---+---+---+---+---+ connect_four: | | | | | | | | +---+---+---+---+---+---+---+ | | | | | | | | +---+---+---+---+---+---+---+ | | | | | | | | +---+---+---+---+---+---+---+ | | | | | | | | +---+---+---+---+---+---+---+ | | | | | | | | +---+---+---+---+---+---+---+ | | | | | | | | +---+---+---+---+---+---+---+ 

请注意,如上所述的2D网格仅仅是有用的可视化。 从C ++的angular度来看,内存是一个“扁平”的字节序列。 multidimensional array的元素按行优先顺序存储。 也就是说, connect_four[0][6]connect_four[1][0]是内存中的邻居。 实际上, connect_four[0][7]connect_four[1][0]表示相同的元素! 这意味着您可以采用multidimensional array并将它们视为大型的一维数组:

 int* p = &connect_four[0][0]; int* q = p + 42; some_int_sequence_algorithm(p, q); 

匿名multidimensional array

使用匿名multidimensional array, 除了第一个以外的所有维必须在编译时被知道:

 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 

这就是匿名multidimensional 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: | | | | | | | | | | | +---+---+---+---+---+ 

由于现在每条线都是单独分配的,因此将二维数组视为一维数组不再适用。

匿名的指针数组

这里是一个匿名数组(或任何其他数量的)指针,它们是用不同长度的匿名数组初始化的:

 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指针数组的第一个元素的指针(每个指针指向原始二维数组中的一行的第一个元素),但该指针数组不存在内存中的任何地方呢。 如果您想要这样的转换,您必须手动创build并填充所需的指针数组:

 int connect_four[6][7]; int** p = new int*[6]; for (int i = 0; i < 6; ++i) { p[i] = connect_four[i]; } // ... delete[] p; 

请注意,这会生成原始multidimensional array的视图。 如果你需要一个副本,你必须创build额外的数组并自己复制数据:

 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; 

分配

没有特别的原因,数组不能被分配给另一个。 改用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通常专用于原始types以提供最佳性能。 std::memcpy不太可能执行得更好。 如果有疑问,测量。

虽然不能直接分配数组,但可以分配包含数组成员的结构和类。 这是因为数组成员是由编译器默认提供的赋值运算符成员复制的。 如果您为自己的结构或类types手动定义赋值运算符,则必须回退到数组成员的手动复制。

parameter passing

数组不能通过值传递。 你可以通过指针或引用来传递它们。

通过指针传递

由于数组本身不能通过值来传递,通常指向它们的第一个元素的指针是通过值来传递的。 这通常被称为“通过指针”。 由于数组的大小不能通过该指针进行检索,所以必须传递指示数组大小的第二个参数(传统的C解决scheme)或指向数组最后一个元素(C ++迭代器解决scheme)之后的第二个指针, :

 #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 ,从模板实例化不同的函数。 你也可以编写相当有用的函数模板,从元素types和大小中抽象出来。

数组创build和初始化

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

自动数组

每当控制stream经过非静态局部数组variables的定义时,就会创build自动数组(每个“在堆栈上”的数组):

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

初始化按升序进行。 请注意,初始值取决于元素typesT

  • 如果T是POD (如上例中的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则不(至less需要一个值)。 另请注意,数组初始化器只能用于初始化数组; 他们以后不能用于作业。

静态数组

静态数组(居于“数据段”中的数组)是局部数组variables,它们在命名空间范围(“全局variables”)处使用static关键字和数组variables定义:

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

(请注意,名称空间范围内的variables是隐式静态的。将static关键字添加到其定义中具有完全不同的,不推荐的含义 。

下面是静态数组的行为与自动数组的不同之处:

  • 没有数组初始化器的静态数组在初始化之前是零初始化的。
  • 静态POD数组只是初始化一次 ,初始值通常被烘焙到可执行文件中,在这种情况下,运行时没有初始化成本。 但是,这并不总是最节省空间的解决scheme,并不是标准所要求的。
  • 静态非POD数组是在控制stream首次通过定义初始化的。 在本地静态数组的情况下,如果从不调用该函数,则可能永远不会发生这种情况。

(以上都不是针对数组的,这些规则同样适用于其他types的静态对象。)

数组数据成员

数组数据成员是在创build自己的对象时创build的。 不幸的是,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 } { } }; 

这是与没有默认构造函数的元素types一起工作的唯一解决scheme。

dynamic数组

dynamic数组没有名字,因此访问它们的唯一方法是通过指针。 因为他们没有名字,我从现在开始将他们称为“匿名数组”。

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

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

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

  +---+---+---+---+---+---+---+---+ (anonymous) | | | | | | | | | +---+---+---+---+---+---+---+---+ ^ | | +-|-+ p: | | | int* +---+ 

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

请注意,这里没有数组到指针的衰减。 尽pipe求值new int[size]事实上创build了一个整数数组 ,但是new int[size]的结果已经是指向单个整数(第一个元素)的指针, 而不是整数数组或指向未知大小的整数数组。 这是不可能的,因为静态types系统需要数组大小为编译时常量。 (因此,我没有注释在图片中的静态types信息的匿名数组。

关于元素的默认值,匿名数组的行为类似于自动数组。 通常情况下,匿名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; 

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

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

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

好的,你已经被告知或者已经发现你自己,全局variables(可以在翻译单元之外访问的命名空间范围variables)是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。

由于types不匹配,程序在运行时会崩溃。

Windows 7崩溃对话框

在正式的解释中:该程序具有未定义行为(UB),而不是崩溃,因此可以挂起,或者什么也不做,或者可以发送威胁性的电子邮件给美国,俄罗斯,印度,中国和瑞士,并使鼻恶魔从你的鼻子飞出。

实践中的解释:在main.cpp ,数组被视为一个指针,放在与数组相同的地址处。 对于32位可执行文件,这意味着数组中的第一个int值被视为指针。 即,在main.cppnumbersvariables包含或似乎包含(int*)1 。 这会导致程序在地址空间的最底层访问内存,这通常是保留的并导致陷阱。 结果:你得到一个崩溃。

编译器完全没有诊断这个错误的权利,因为C ++ 11§3.5/ 10说,关于声明兼容types的要求,

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

同一段落详述了允许的变化:

…数组对象的声明可以指定不同的数组types,因为存在或不存在主数组bound(8.3.4)。

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

5.2陷阱:做过早的优化( memset &friends)。

尚未写入

5.3陷阱:用C语言来获取元素的数量。

凭借深厚的C经验,写下…

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

由于array衰减到需要的第一个元素的指针,因此expression式sizeof(a)/sizeof(a[0])也可以写成sizeof(a)/sizeof(*a) 。 它的意思是一样的,不pipe它是如何写的,它是查找数组的数字元素的C语言

主要陷阱:C语言不是types安全的。 例如,代码…

 #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支持本地types的数组,那么你可以这样做:

 #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:在函数'无效显示(const int *)':
compile_time_detection.cpp:14:错误:没有匹配函数调用'n_items(const int *&)'

M:\ count> _

它是如何工作的:数组是通过引用传递给n_items ,所以它不会衰减指向第一个元素,函数可以返回由该types指定的元素的数量。

在C ++ 11中,你也可以使用它来获得本地types的数组,这是types安全的C ++习惯用于查找数组元素的数量。

5.4 C ++ 11&C ++ 14陷阱:使用一个constexpr数组大小函数。

使用C ++ 11和更高版本是很自然的,但是正如你将会看到危险的!replaceC ++ 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 ,并且使用-pedantic-errors的在线编译器进行testing ,也使用了铿锵3.0和3.2版,但是没有使用铿锵3.3,3.4.1,3.5.0,3.5.1,3.6(rc1)或3.7(实验)。 对Windows平台非常重要,它不能用Visual C ++ 2015进行编译。原因是关于在constexprexpression式中使用引用的C ++ 11 / C ++ 14声明:

C ++ 11 C ++ 14 $ 5.19 / 2第九短跑

条件expression式 e是一个核心常量expression式,除非根据抽象机器(1.9)的规则对e进行评估,否则将评估下列expression式之一:

  • 除非引用有前面的初始化,否则引用引用types的variables或数据成员的idexpression式
    • 它是用一个常量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 } 

…但是当Collection不是一个原始数组时,这个失败。

要处理可以是非数组的集合,需要n_items函数的可重载性,但是对于编译时,需要使用数组大小​​的编译时间表示。 而且在C ++ 11和C ++ 14中工作正常的经典C ++ 03解决scheme是让函数报告其结果不是作为一个值,而是通过它的函数结果types 。 比如像这样:

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

About the choice of return type for static_n_items : this code doesn't use std::integral_constant because with std::integral_constant the result is represented directly as a constexpr value, reintroducing the original problem. Instead of a Size_carrier class one can let the function directly return a reference to an array. However, not everybody is familiar with that syntax.

About the naming: part of this solution to the constexpr -invalid-due-to-reference problem is to make the choice of compile time constant explicit.

Hopefully the oops-there-was-a-reference-involved-in-your- constexpr issue will be fixed with C++17, but until then a macro like the STATIC_N_ITEMS above yields portability, eg to the clang and Visual C++ compilers, retaining type safety.

Related: macros do not respect scopes, so to avoid name collisions it can be a good idea to use a name prefix, eg MYLIB_STATIC_N_ITEMS .