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语言。
主要陷阱: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]
为just 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个元素,调用display ...
断言失败:(“ 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:\ 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
表达式中:
C ++ 11 C ++ 14 $ 5.19 / 2 9
个短划线
甲条件表达式 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那里有一个涉及您的引用的constexpr
问题将在C ++ 17中解决,但是在此之前,像STATIC_N_ITEMS
上面这样的宏才具有可移植性,例如到clang和Visual C ++编译器,保留类型安全。
相关:宏不遵守作用域,因此为了避免名称冲突,最好使用名称前缀,例如MYLIB_STATIC_N_ITEMS
。