C从C继承了数组,几乎无处不在 . C提供了易于使用且不易出错的抽象(自C++11以来 std::vector<T>
自C++11以来),因此对数组的需求并不像在C中那样频繁出现 . 但是,当您阅读遗留代码或进行交互时使用C语言编写的库,您应该牢牢掌握数组的工作原理 .
本FAQ分为五个部分:
如果您觉得此常见问题解答中缺少重要内容,请写下答案并将其作为附加部分链接到此处 .
在以下文本中,"array"表示"C array",而不是类模板 std::array
. 假定了C声明符语法的基本知识 . 请注意,如下所示, new
和 delete
的手动使用在面对异常时非常危险,但这是another FAQ的主题 .
(注意:这是Stack Overflow的C FAQ的一个条目 . 如果你想批评在这个表单中提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方 . 这个问题在C聊天室中受到监控,其中FAQ的想法首先出现在那里,所以你的答案很可能被那些提出这个想法的人阅读 . )
5 回答
类型级别的数组
数组类型表示为
T[n]
,其中T
是元素类型,n
是正大小,即数组中元素的数量 . 数组类型是元素类型和大小的产品类型 . 如果这些成分中的一种或两种不同,则会得到一种独特的类型:请注意,大小是类型的一部分,也就是说,不同大小的数组类型是完全无关的不兼容类型 .
sizeof(T[n])
相当于n * sizeof(T)
.数组到指针衰减
T[n]
和T[m]
之间的唯一"connection"是两个类型都可以隐式转换为T*
,并且此转换的结果是指向数组的第一个元素的指针 . 也就是说,只要需要T*
,就可以提供T[n]
,编译器将默默提供该指针:这种转换称为"array-to-pointer decay",它是混淆的主要原因 . 在此过程中,数组的大小将丢失,因为它不再是类型的一部分(
T*
) . Pro:忘记类型级别上的数组大小允许指针指向任何大小的数组的第一个元素 . Con:给定指向数组的第一个(或任何其他)元素的指针,无法检测该数组的大小或指针相对于数组边界的确切位置 . Pointers are extremely stupid .数组不是指针
只要数据被认为有用,编译器就会静默地生成一个指向数组第一个元素的指针,也就是说,只要操作在数组上失败但在指针上成功 . 这种从数组到指针的转换是微不足道的,因为结果指针值只是数组的地址 . 请注意,指针不会存储为数组本身(或内存中的任何其他位置)的一部分 . An array is not a pointer.
数组不会衰减到指向其第一个元素的指针的一个重要上下文是
&
运算符应用于它时 . 在这种情况下,&
运算符产生一个指向整个数组的指针,而不仅仅是指向其第一个元素的指针 . 虽然在这种情况下值(地址)是相同的,但是指向数组的第一个元素的指针和指向整个数组的指针是完全不同的类型:以下ASCII艺术解释了这种区别:
注意指向第一个元素的指针如何仅指向一个整数(描述为一个小框),而指向整个数组的指针指向一个包含8个整数的数组(描述为一个大框) .
同样的情况出现在课堂上,可能更明显 . 指向对象的指针和指向其第一个数据成员的指针具有相同的值(相同的地址),但它们是完全不同的类型 .
如果您不熟悉C声明符语法,则
int(*)[8]
类型中的括号必不可少:int(*)[8]
是指向8个整数数组的指针 .int*[8]
是一个包含8个指针的数组,每个元素的类型为int*
.访问元素
C提供了两种句法访问数组的各个元素的变体 . 它们都不优于另一个,你应该熟悉两者 .
指针算术
给定数组第一个元素的指针
p
,表达式p+i
产生一个指向数组第i个元素的指针 . 通过以后取消引用该指针,可以访问各个元素:如果
x
表示一个数组,那么数组到指针的衰减将会启动,因为添加一个数组和一个整数是没有意义的(数组上没有加法操作),但添加指针和整数是有意义的:(注意,隐式生成的指针没有名称,所以我写了
x+0
以便识别它 . )另一方面,如果
x
表示指向数组的第一个(或任何其他)元素的指针,则不需要数组到指针的衰减,因为将要添加i
的指针已经存在:请注意,在描述的情况下,
x
是一个指针变量(可以通过x
旁边的小方框识别),但它也可能是函数返回指针(或T*
类型的任何其他表达式)的结果 .索引运算符
由于语法
*(x+i)
有点笨拙,C提供了替代语法x[i]
:由于添加是可交换的,因此以下代码完全相同:
索引运算符的定义导致以下有趣的等价:
但是,
&x[0]
通常不等于x
. 前者是指针,后者是数组 . 只有当上下文触发数组到指针衰减时,x
和_3040151才能互换使用 . 例如:在第一行,编译器检测指向指针的指定,这很简单 . 在第二行,它检测从数组到指针的赋值 . 由于这是没有意义的(但指针指向指针是有意义的),数组到指针衰减像往常一样开始 .
范围
T[n]
类型的数组具有n
个元素,索引从0
到n-1
;没有元素n
. 然而,为了支持半开放范围(开头是包含的并且结尾是独占的),C允许计算指向(不存在的)第n个元素的指针,但取消引用该指针是非法的:例如,如果要对数组进行排序,则以下两个方法都可以正常工作:
注意,提供
&x[n]
作为第二个参数是非法的,因为它等同于&*(x+n)
,并且子表达式*(x+n)
在技术上调用C中的undefined behavior(但不是在C99中) .另请注意,您可以简单地提供
x
作为第一个参数 . 这对我来说有点过于简洁,它也使编译器的模板参数推导更难一些,因为在这种情况下,第一个参数是一个数组,但第二个参数是一个指针 . (同样,数组到指针的衰减开始了 . )程序员经常将多维数组与指针数组混淆 .
多维数组
大多数程序员都熟悉命名的多维数组,但许多程序员并不知道多维数组也可以匿名创建 . 多维数组通常称为"arrays of arrays"或“真正的多维数组” .
命名多维数组
使用命名多维数组时,必须在编译时知道所有维:
这就是命名多维数组在内存中的样子:
请注意,上面的2D网格仅仅是有用的可视化 . 从C的角度来看,内存是"flat"字节序列 . 多维数组的元素以行主顺序存储 . 也就是说,
connect_four[0][6]
和connect_four[1][0]
是内存中的邻居 . 实际上,connect_four[0][7]
和connect_four[1][0]
表示相同的元素!这意味着您可以采用多维数组并将它们视为大型一维数组:匿名多维数组
对于匿名多维数组,除了第一个维之外的所有维都必须在编译时知道:
这就是匿名多维数组在内存中的样子:
请注意,数组本身仍然作为单个块分配在内存中 .
指针数组
您通过引入另一层次的间接可以克服固定宽度的限制 .
命名的指针数组
这是一个由五个指针组成的命名数组,它们使用不同长度的匿名数组进行初始化:
以下是它在内存中的样子:
由于现在每条线都是单独分配的,因此将2D数组视为一维数组不再有效 .
匿名指针数组
这是一个包含5个(或任何其他数量)指针的匿名数组,这些指针使用不同长度的匿名数组进行初始化:
以下是它在内存中的样子:
转化
数组到指针的衰减自然会扩展到数组和指针数组:
但是,没有从
T[h][w]
到T**
的隐式转换 . 如果确实存在这样的隐式转换,结果将是一个指向h
指针数组的第一个元素的指针T
(每个指向原始2D数组中一行的第一个元素),但该指针数组不存在在记忆的任何地方 . 如果需要这样的转换,则必须手动创建并填充所需的指针数组:请注意,这会生成原始多维数组的视图 . 如果您需要副本,则必须创建额外的数组并自行复制数据:
作业
无特殊原因,不能将数组分配给彼此 . 请改用
std::copy
:这比真正的数组赋值更灵活,因为可以将较大数组的片段复制到较小的数组中 .
std::copy
通常专门用于基本类型以提供最大性能 .std::memcpy
不太可能表现得更好 . 如果有疑问,请测量 .虽然不能直接分配数组,但可以指定包含数组成员的结构和类 . 这是因为赋值运算符的array members are copied memberwise是由编译器默认提供的 . 如果为自己的结构或类类型手动定义赋值运算符,则必须回退到数组成员的手动复制 .
参数传递
数组不能通过值传递 . 您可以通过指针或引用传递它们 .
通过指针
由于数组本身不能通过值传递,因此通常会通过值传递指向其第一个元素的指针 . 这通常被称为“通过指针传递” . 由于数组的大小不能通过该指针检索,因此必须传递指示数组大小的第二个参数(经典C解决方案)或指向数组最后一个元素之后的第二个指针(C迭代器解决方案) :
作为一种语法替代方法,您还可以将参数声明为
T p[]
,它与T* p
in the context of parameter lists only 完全相同:您可以将编译器视为重写
T p[]
至T *p
in the context of parameter lists only . 这个特殊规则部分地导致了对数组和指针的整体混淆 . 在每个其他上下文中,将某些内容声明为数组或指针都会产生巨大的差异 .不幸的是,您还可以在数组参数中提供一个大小,编译器会忽略该参数 . 也就是说,以下三个签名完全等效,如编译器错误所示:
通过引用传递
数组也可以通过引用传递:
在这种情况下,阵列大小很重要 . 由于编写一个只接受8个元素的数组的函数几乎没用,程序员通常会编写这样的函数作为模板:
请注意,您只能使用实际的整数数组调用此类函数模板,而不能使用指向整数的指针 . 自动推断数组的大小,对于每个大小
n
,从模板实例化不同的函数 . 您还可以编写从元素类型和大小中抽象出来的quite useful函数模板 .数组创建和初始化
与任何其他类型的C对象一样,数组可以直接存储在命名变量中(然后大小必须是编译时常量; C++ does not support VLAs),或者它们可以匿名存储在堆上并通过指针间接访问(只有这样)可以在运行时计算大小) .
自动数组
每次控制流通过时,都会创建自动数组(生活在堆栈中的数组)定义非静态局部数组变量:
初始化按升序执行 . 请注意,初始值取决于元素类型
T
:如果
T
是POD(如上例中的int
),则不会进行初始化 .否则,
T
的默认构造函数初始化所有元素 .如果
T
没有提供可访问的默认构造函数,则程序不会编译 .或者,可以在数组初始值设定项中显式指定初始值,数组初始值设定项是用大括号括起来的逗号分隔列表:
由于在这种情况下,数组初始值设定项中的元素数量等于数组的大小,因此手动指定大小是多余的 . 它可以由编译器自动推导出来:
也可以指定大小并提供更短的数组初始值设定项:
在这种情况下,剩余的元素是zero-initialized . 注意,C允许空数组初始化器(所有元素都是零初始化),而C89不允许(至少需要一个值) . 另请注意,数组初始值设定项只能用于初始化数组;它们以后不能用于作业 .
静态数组
静态数组(数组生成"in the data segment")是使用
static
关键字定义的局部数组变量和命名空间范围("global variables")的数组变量:(请注意,命名空间范围内的变量是隐式静态的 . 将
static
关键字添加到其定义中的是completely different, deprecated meaning . )以下是静态数组与自动数组的行为方式不同:
没有数组初始化程序的静态数组在进一步潜在初始化之前进行零初始化 .
静态POD数组只初始化一次,初始值通常被烘焙到可执行文件中,在这种情况下,运行时没有初始化成本 . 然而,这并不总是最节省空间的解决方案,并且标准不要求它 .
静态非POD数组在控制流第一次通过其定义时初始化 . 在本地静态数组的情况下,如果永远不调用该函数,则可能永远不会发生这种情况 .
(以上都不是特定于数组的 . 这些规则同样适用于其他类型的静态对象 . )
数组数据成员
创建其拥有对象时,将创建阵列数据成员 . 不幸的是,C 03没有提供在member initializer list中初始化数组的方法,因此必须使用赋值伪装初始化:
或者,您可以在构造函数体中定义自动数组,并将元素复制到:
在C 0x中,由于uniform initialization,可以在成员初始化列表中初始化数组:
这是唯一适用于没有默认构造函数的元素类型的解决方案 .
动态数组
动态数组没有名称,因此访问它们的唯一方法是通过指针 . 因为它们没有名字,所以从现在开始我将它们称为“匿名数组” .
在C中,匿名数组是通过
malloc
和朋友创建的 . 在C中,使用new T[size]
语法创建匿名数组,该语法返回指向匿名数组的第一个元素的指针:如果在运行时将大小计算为8,则以下ASCII艺术描述了内存布局:
显然,由于必须单独存储的额外指针,匿名数组比命名数组需要更多内存 . (免费商店还有一些额外的开销 . )
请注意,这里没有数组到指针的衰减 . 虽然评估
new int[size]
实际上确实创建了一个整数数组,但表达式new int[size]
的结果已经是指向单个整数(第一个元素)的指针,而不是整数数组或指向未知大小整数数组的指针 . 这是不可能的,因为静态类型系统要求数组大小为编译时常量 . (因此,我没有在图片中用静态类型信息注释匿名数组 . )关于元素的默认值,匿名数组的行为类似于自动数组 . 通常,匿名POD数组未初始化,但有一个special syntax触发值初始化:
(注意分号前面的尾对括号 . )同样,C 0x简化了规则并允许为统一指定匿名数组的初始值初始化:
如果您使用匿名数组,则必须将其释放回系统:
您必须准确释放每个匿名数组一次,然后再不再触摸它 . 根本不释放它会导致内存泄漏(或更一般地,取决于元素类型,资源泄漏),并且尝试多次释放会导致未定义的行为 . 使用非数组形式
delete
(或free
)而不是delete[]
来释放数组也是undefined behavior .5.使用数组时常见的陷阱 .
5.1陷阱:信任类型 - 不安全链接 .
好的,你已经被告知,或者自己发现,全局变量(可以在翻译单元外访问的命名空间范围变量)是Evil™ . 但是你知道他们是多么真实的Evil™吗?考虑下面的程序,包括两个文件[main.cpp]和[numbers.cpp]:
在Windows 7中,这可以与MinGW g 4.4.1和Visual C 10.0进行编译和链接 .
由于类型不匹配,程序在运行时会崩溃 .
正式解释:该程序具有未定义行为(UB),而不是崩溃它可能只是挂起,或者可能什么都不做,或者它可以向美国,俄罗斯,印度的总统发送威胁电子邮件,中国和瑞士,让鼻子守护进程从你的鼻子里飞出来 .
实践说明:在
main.cpp
中,数组被视为指针,与数组放在同一地址 . 对于32位可执行文件,这意味着数组中的第一个int
值被视为指针 . 即,在main.cpp
中,numbers
变量包含或似乎包含(int*)1
. 这导致程序在地址空间的最底部访问内存,这通常是保留和陷阱引起的 . 结果:你遇到了崩溃 .编译器完全有权不诊断此错误,因为C11§3.5/ 10说明了关于声明的兼容类型的要求,
同一段落详细说明了允许的变化:
这允许的变化不包括在一个翻译单元中将名称声明为数组,而在另一个翻译单元中作为指针 .
5.2陷阱:做过早优化(memset和朋友) .
还没写
5.3陷阱:使用C语言获取元素数量 .
凭借深厚的C经验,写作是很自然的......
由于
array
在需要时衰减指向第一个元素,因此表达式sizeof(a)/sizeof(a[0])
也可以写为sizeof(a)/sizeof(*a)
. 它的含义相同,无论如何编写它都是 C idiom 用于查找数组的数字元素 .主要缺陷:C成语不是类型安全的 . 例如,代码......
将指针传递给
N_ITEMS
,因此很可能产生错误的结果 . 在Windows 7中编译为32位可执行文件,它生成...编译器将
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 .为了在运行时检测到此错误,您可以执行...
运行时错误检测优于无检测,但它浪费了一点处理器时间,也许程序员时间更长 . 在编译时检测更好!如果你很高兴不支持使用C 98的本地类型数组,那么你可以这样做:
编译这个定义代替第一个完整的程序,用g,我得到......
工作原理:数组通过引用传递给
n_items
,因此它不会衰减到指向第一个元素的指针,并且该函数只能返回该类型指定的元素数 .使用C 11,您也可以将它用于本地类型的数组,并且它是类型safe C++ idiom ,用于查找数组的元素数 .
5.4 C 11&C 14陷阱:使用constexpr数组大小函数 .
使用C 11及更高版本它很自然,但是你会看到危险!,以取代C 03功能
同
其中重大变化是使用
constexpr
,它允许此函数产生 compile time constant .例如,与C 03函数相比,这样的编译时常量可用于声明与另一个相同大小的数组:
但请考虑使用
constexpr
版本的此代码:缺陷:截至2015年7月,上面使用
-pedantic-errors
编译MinGW-64 5.1.0,并使用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九分
人们总是可以写得更详细
...但是当
Collection
不是原始数组时,这会失败 .要处理可以是非数组的集合,需要
n_items
函数的可重载性,但是,对于编译时,需要使用数组大小的编译时表示 . 经典的C 03解决方案,在C 11和C 14中也能正常工作,是让函数报告其结果不是作为值而是通过其函数结果类型 . 例如这样:关于
static_n_items
的返回类型的选择:此代码不使用std::integral_constant
,因为std::integral_constant
结果直接表示为constexpr
值,重新引入原始问题 . 可以让函数直接返回对数组的引用,而不是Size_carrier
类 . 但是,并非所有人都熟悉该语法 .关于命名:这个解决
constexpr
-invalid-due-to-reference问题的解决方案的一部分是使编译时选择显式的选择 .希望oops-there-a-reference-in-your-
constexpr
问题将用C 17修复,但在此之前,像上面的STATIC_N_ITEMS
这样的宏会产生可移植性,例如到clang和Visual C编译器,保留类型安全 .相关:宏不遵守范围,因此为了避免名称冲突,使用名称前缀是个好主意,例如:
MYLIB_STATIC_N_ITEMS
.