如何在 C++ 中使用数组?
Posted
技术标签:
【中文标题】如何在 C++ 中使用数组?【英文标题】:How do I use arrays in C++? 【发布时间】:2022-01-11 09:00:11 【问题描述】:C++ 继承了 C 中的数组,它们几乎无处不在。 C++ 提供了更易于使用且不易出错的抽象(std::vector<T>
自 C++98 和 std::array<T, n>
自 C++11),因此对数组的需求不像在 C 中那样频繁出现。但是,当您阅读遗留代码或与用 C 编写的库进行交互时,您应该牢牢掌握数组的工作原理。
此常见问题解答分为五个部分:
-
arrays on the type level and accessing elements
array creation and initialization
assignment and parameter passing
multidimensional arrays and arrays of pointers
common pitfalls when using arrays
如果您觉得此常见问题解答中缺少重要内容,请写下答案并将其链接到此处作为附加部分。
在下文中,“数组”表示“C 数组”,而不是类模板std::array
。假定具备 C 声明符语法的基本知识。请注意,如下所示手动使用new
和delete
在遇到异常时非常危险,但这是another FAQ 的主题。
(注意:这是Stack Overflow's C++ FAQ 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么the posting on meta that started all this 将是这样做的地方。回答该问题在C++ chatroom 中进行监控,FAQ 想法最初是从那里开始的,因此您的回答很可能会被提出该想法的人阅读。)
【问题讨论】:
如果指针总是指向起点而不是目标中间的某个地方,它们会更好...... 您应该使用 STL Vector,因为它为您提供了更大的灵活性。std::array
s、std::vector
s 和 gsl::span
s 的组合可用性 - 坦率地说,我希望关于如何在 C++ 中使用数组的常见问题解答说“现在,你可以开始考虑到只是,好吧,不使用它们。”
【参考方案1】:
类型级别的数组
数组类型表示为T[n]
,其中T
是元素类型,n
是正数size,即数组中元素的数量.数组类型是元素类型和大小的乘积类型。如果其中一种或两种成分不同,您会得到不同的类型:
#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*
的地方,您都可以提供T[n]
,编译器会默默地提供该指针:
+---+---+---+---+---+---+---+---+
the_actual_array: | | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^
|
|
|
| pointer_to_the_first_element int*
这种转换被称为“数组到指针衰减”,它是造成混淆的主要来源。数组的大小在此过程中丢失,因为它不再是类型的一部分 (T*
)。优点:在类型级别上忘记数组的大小允许指针指向 any 大小的数组的第一个元素。缺点:给定一个指向数组第一个(或任何其他)元素的指针,无法检测该数组的大小或指针相对于数组边界的确切位置。 Pointers are extremely stupid.
数组不是指针
只要数组的第一个元素被认为有用,编译器就会默默地生成一个指针,也就是说,每当一个操作在数组上失败但在指针上成功时。这种从数组到指针的转换很简单,因为生成的指针 value 就是数组的地址。请注意,指针不存储为数组本身(或内存中的任何其他位置)的一部分。 数组不是指针。
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
在第一行,编译器检测到从指针到指针的赋值,这很容易成功。在第二行,它检测从 array 到指针的赋值。由于这是没有意义的(但 pointer 指向指针分配是有意义的),数组到指针的衰减照常开始。
范围
T[n]
类型的数组具有n
元素,索引从0
到n-1
;没有元素n
。然而,为了支持半开范围(开始是 inclusive 而结束是 exclusive),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++ 中调用undefined behavior(但在C99 中不是)。
还请注意,您可以简单地提供x
作为第一个参数。这对我的口味来说有点太简洁了,而且它也使得模板参数推导对编译器来说有点困难,因为在这种情况下,第一个参数是一个数组,而第二个参数是一个指针。 (同样,数组到指针的衰减开始了。)
【讨论】:
数组不衰减成指针的情况illustrated here供参考。 @fredoverflow 在访问或范围部分,值得一提的是,C 数组可与基于 C++11 范围的 for 循环一起使用。 出色的答案。声明,'这种转换被称为“数组到指针衰减”,它是混淆的主要来源”' - 是准确的,在很大程度上是因为它是“已知的”仅在通用语中。在语言草案或标准中,在描述转换为 临时 指针的上下文时,甚至没有使用此命名法一次。【参考方案2】:程序员经常将多维数组与指针数组混淆。
多维数组
大多数程序员都熟悉命名多维数组,但很多人不知道多维数组也可以匿名创建。多维数组通常被称为“数组的数组”或“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: | | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
| | | | | | | |
+---+---+---+---+---+---+---+
请注意,上述二维网格只是有用的可视化。从 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: | | | | | | | | | | |
+---+---+---+---+---+
由于现在每行都是单独分配的,因此不再将二维数组视为一维数组。
匿名指针数组
这是一个由 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
的指针(每个指向原始二维数组中一行的第一个元素),但该指针数组在内存中的任何地方都不存在。如果你想要这样的转换,你必须手动创建并填充所需的指针数组:
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];
是有效的陈述,而 H
和 W
是在编译时已知。
非常感谢!请告诉我如何从数组中设置/获取元素(“指针的匿名数组”)。
(编辑队列已满,所以我改为评论)我可能会明确提到要释放匿名多维数组,正确的语法是 delete[] p
【参考方案3】:
作业
没有特殊原因,数组不能相互分配。请改用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
不太可能表现得更好。如有疑问,请测量。
虽然您不能直接分配数组,但您可以分配包含数组成员的结构和类。这是因为 array members are copied memberwise 由编译器默认提供的赋值运算符提供。如果您为自己的结构或类类型手动定义赋值运算符,则必须回退到手动复制数组成员。
参数传递
数组不能按值传递。您可以通过指针或引用传递它们。
按指针传递
由于数组本身不能按值传递,因此通常将指向其第一个元素的指针改为按值传递。这通常被称为“通过指针”。由于无法通过该指针检索数组的大小,因此您必须传递指示数组大小的第二个参数(经典 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
,都会从模板中实例化一个不同的函数。您还可以编写quite useful 函数模板,从元素类型和大小中抽象出来。
【讨论】:
可能值得添加一个注释,即使在void foo(int a[3])
a
中看起来确实像一个按值传递数组,修改 a
内部的 foo
将修改原始大批。这一点应该很清楚,因为数组不能被复制,但加强这一点可能是值得的。
C++20 有ranges::copy(a, b)
int sum( int size_, int a[size_]);
-- 从(我认为)C99 开始【参考方案4】:
数组创建和初始化
与任何其他类型的 C++ 对象一样,数组可以直接存储在命名变量中(然后大小必须是编译时常量;C++ does not support VLAs),或者它们可以匿名存储在堆上并间接访问通过指针(只有这样才能在运行时计算大小)。
自动数组
每次控制流通过非静态局部数组变量的定义时,都会创建自动数组(位于“堆栈上”的数组):
void foo()
int automatic_array[8];
初始化按升序执行。请注意,初始值取决于元素类型T
:
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
在这种情况下,其余元素为zero-initialized。请注意,C++ 允许空数组初始值设定项(所有元素都初始化为零),而 C89 不允许(至少需要一个值)。另请注意,数组初始化器只能用于初始化数组;以后不能在作业中使用它们。
静态数组
静态数组(位于“数据段”中的数组)是使用 static
关键字定义的局部数组变量和命名空间范围内的数组变量(“全局变量”):
int global_static_array[8];
void foo()
static int local_static_array[8];
(请注意,命名空间范围内的变量是隐式静态的。将static
关键字添加到它们的定义中具有completely different, deprecated meaning。)
静态数组与自动数组的行为方式不同:
没有数组初始化器的静态数组在任何进一步的潜在初始化之前都被初始化为零。 静态 POD 数组只被初始化一次,并且初始值通常被烘焙到可执行文件中,在这种情况下,运行时没有初始化成本。然而,这并不总是最节省空间的解决方案,而且标准也没有要求。 静态非 POD 数组在第一次控制流通过它们的定义时被初始化。在本地静态数组的情况下,如果从未调用该函数,则可能永远不会发生这种情况。(以上都不是特定于数组的。这些规则同样适用于其他类型的静态对象。)
数组数据成员
数组数据成员是在创建其所属对象时创建的。不幸的是,C++03 没有提供初始化member initializer list 中的数组的方法,所以初始化必须用赋值来伪造:
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 中,数组可以在成员初始化列表中被初始化,这要归功于uniform initialization:
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数组是不会初始化的,但是有一个special syntax会触发value-initialization:
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[]
释放数组也是undefined behavior。
【讨论】:
在 C++11 中删除了命名空间范围内static
使用的弃用。
因为new
是am操作符,它当然可以通过引用返回所有的数组。只是没有意义......
@Deduplicator 不,它不能,因为从历史上看,new
比引用要老很多。
@FredOverflow:所以它无法返回引用是有原因的,它与书面解释完全不同。
@Deduplicator 我认为不存在对未知边界数组的引用。至少g++拒绝编译int a[10]; int (&r)[] = a;
【参考方案5】:
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。
由于类型不匹配,运行时程序崩溃。
正式解释:程序具有未定义行为 (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] ))
由于array
在需要的地方衰减为指向第一个元素的指针,所以
表达式sizeof(a)/sizeof(a[0])
也可以写成
sizeof(a)/sizeof(*a)
。它的意思是一样的,不管它是怎样的
写成是C idiom求数组的个数。
主要缺陷: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 个元素。
-
编译器将
int const a[7]
重写为 int const a[]
。
编译器将int const a[]
重写为int const* a
。
因此,N_ITEMS
使用指针调用。
对于 32 位可执行文件,sizeof(array)
(指针大小)为 4。
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: error: no matching function for call to '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
表达式中的引用:
条件表达式
一个 id-expression 引用引用类型的变量或数据成员 除非引用具有先前的初始化并且 它是用常量表达式初始化的或 它是一个对象的非静态数据成员,其生命周期开始于 e的评估;e
是一个核心常量表达式,除非求值 的e
,遵循抽象机(1.9)的规则,将评估其中一个 以下表达式: ⋮
总是可以写得越冗长
// 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 中,是让函数报告其结果而不是作为值
但是通过它的函数结果type。比如这样:
// 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
.
【讨论】:
+1 出色的 C 编码测试:我在 VC++ 10.0 和 GCC 4.1.2 上花了 15 分钟尝试修复Segmentation fault
... 阅读您的解释后,我终于找到/理解了!请写下您的 §5.2 部分 :-) 干杯
好。一点点 - countOf 的返回类型应该是 size_t 而不是 ptrdiff_t。可能值得一提的是,在 C++11/14 中它应该是 constexpr 和 noexcept。
@Ricky65:感谢您提及 C++11 的注意事项。对这些功能的支持在 Visual C++ 中出现的时间较晚。关于size_t
,它没有我所知道的现代平台的优势,但由于 C 和 C++ 的隐式类型转换规则,它存在许多问题。也就是说,ptrdiff_t
是非常有意使用的,以避免size_t
的问题。然而,应该知道 g++ 在将数组大小与模板参数匹配时存在问题,除非它是 size_t
(我认为非size_t
的编译器特定问题并不重要,但 YMMV)。
@Alf.在标准工作草案 (N3936) 8.3.4 中,我读到 - 数组的边界是......“转换后的 std::size_t 类型的常量表达式,其值应大于零”。
@Ricky:如果您指的是不一致,那么当前的 C++11 标准中没有此语句,因此很难猜测上下文,但是矛盾(动态分配的数组 can 为 0,根据 C++11 §5.3.4/7)可能不会在 C++14 中结束。草稿就是:草稿。如果您要询问“其”指的是什么,则它指的是原始表达式,而不是转换后的表达式。如果另一方面你提到这一点是因为你认为这句话可能意味着应该使用size_t
来表示数组的大小,当然不是。以上是关于如何在 C++ 中使用数组?的主要内容,如果未能解决你的问题,请参考以下文章