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

c++从C语言中继承了数组,在C语言中数组几乎随处可见。c++提供了更容易使用且更不容易出错的抽象(c++ 98开始使用std::vector<T>c++ 11开始使用std::array<T, n>),因此对数组的需求不像C中那样经常出现。然而,当你阅读遗留代码或与用C编写的库交互时,你应该对数组的工作方式有一个牢固的把握。

本FAQ分为五个部分:

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

如果你觉得这个常见问题中缺少了一些重要的内容,请写下答案并将其链接到这里作为附加部分。

在下面的文本中,"数组"意思是“C数组”,而不是类模板std::array。假定具有C声明器语法的基本知识。注意,下面演示的手动使用newdelete在遇到异常时是极其危险的,但这是另一个常见问题的主题。


(注意:这意味着是Stack Overflow's c++ FAQ。如果你想批评在这种形式下提供FAQ的想法,那么在meta上的帖子开始了这一切将是这样做的地方。这个问题的答案在 c++聊天室中监控,FAQ的想法最初就是在这里开始的,所以你的答案很可能会被想出这个想法的人读到。)

137078 次浏览

类型级别上的数组

数组类型被表示为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)

Array-to-pointer衰变

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++提供了两种语法变体来访问数组的各个元素。

.这两种东西都没有优劣之分,你应该都熟悉

指针的算术

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

赋值

没有特定的原因,数组不能被分配给另一个数组。使用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大小的数组,都会从模板中实例化一个不同的函数。你也可以编写非常有用函数模板,从元素类型和大小中抽象出来。

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

多维数组

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

命名多维数组

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

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指针数组的第一个元素的指针(每个指针指向原始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;

数组创建和初始化

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

自动数组

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

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

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

  • 如果T圆荚体(如上面例子中的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

在这种情况下,剩下的元素是zero-initialized。注意,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[]来释放数组也是未定义的行为

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

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

好吧,你已经被告知,或者你自己已经发现,globals (namespace 可以在翻译单元外访问的范围变量) 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中,此编译和链接与mingwg++ 4.4.1和 Visual c++ 10.0.

由于类型不匹配,程序在运行时崩溃。

 Windows 7崩溃对话框

形式解释:程序具有未定义行为(UB),而不是 因此,它可以挂起来,或者什么都不做,或者它 可以向美国、俄罗斯、印度的总统发送威胁邮件, 中国和瑞士,让鼻子守护神从你鼻子里飞出来。

实际解释:在main.cpp中,数组被视为指针,放置 和数组的地址一样。对于32位可执行文件,这意味着第一个 数组中的int值,被视为指针。例如,在main.cppnumbers变量包含或似乎包含(int*)1。这导致了 访问内存的程序在地址空间的最下面,也就是 传统的保留和陷阱造成。结果:你得到一个崩溃

编译器完全有权利不诊断这个错误, 因为c++ 11§3.5/10规定了兼容类型的要求 对于声明,

< p > [N3290§3.5/10) < br > 违反类型同一性的规则不需要诊断。

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

< p >白马王子;数组对象的声明可以指定数组类型 根据是否存在主数组边界而不同(8.3.4)
这个允许的变化不包括将一个名称声明为一个数组 翻译单元,以及作为另一个翻译单元的指针。< / p >

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习惯用法不是类型安全的。例如,代码 白马王子;< / p >

#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位可执行文件,生成& help;

7个元素,调用display 1元素。< / p >

  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个元素,调用display 断言失败:("N_ITEMS需要一个实际的数组作为参数",typeid(a) != typeid(&*a))),文件runtime_detect . Ion.cpp,第16行

应用程序请求运行时以不寻常的方式终止它 请联系应用程序的支持团队以获得更多信息。< / p >

运行时错误检测比不检测好,但会浪费一点时间 处理器时间,可能还有更多的程序员时间。更好的检测 编译时间!如果你愿意在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++编译第一个完整的程序, 我得到了& help;

M:\count> g++ compile_time_detection.cpp
compile_time_detect .cpp:在函数'void display(const int*)'中:
编译时间检测.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,这允许 这个函数生成编译时间常数.

.

.

.

?

// 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 nine<我> < / i >破折号

A 条件表达式 e是一个核心常数表达式,除非求值 ee,遵循抽象机(1.9)的规则,将计算其中一个 以下表达式:< br > ,,,,,,,⋮< / p >

  • 一个id-expression,引用类型的变量或数据成员 除非引用具有前面的初始化
    • 它是用常量表达式或初始化的
    • 是对象的非静态数据成员,其生命周期开始于 e的取值;
    • 李< / ul > < / >

一个人总是可以写得更详细

// 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类的属性,可以让函数直接返回 引用数组。然而,并不是所有人都熟悉这个语法 关于命名:这个解决方案的一部分__abc0 -invalid-due-to引用 问题是要显式地选择编译时间常数 希望-constexpr中“哎呀,有一个引用涉及”的问题将被修复 但在此之前,像上面的STATIC_N_ITEMS这样的宏会产生可移植性, 例如,clang和Visual c++编译器,保留类型安全 相关:宏不尊重作用域,所以为了避免名称冲突,它可以是一个 使用名称前缀是个好主意,例如MYLIB_STATIC_N_ITEMS.

.

.