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 都可以很好地编译和链接。
由于类型不匹配,程序在运行时会崩溃。

正式解释:该程序具有未定义行为(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个元素。
- 编译器重写
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:错误:没有匹配函数调用'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
.