细论数组维度(dimension)的计较
副标题#e#
我们常常需要知道先前界说的数组维度,或是为了对其举办轮回遍历,或是其它。当我们显示初始化数组而没有指定其维度时尤其如此:
int is[]={1,2,3};
有C语言开拓履历的读者大概常常利用如下方法来实现:
int dimension=sizeof(is)/sizeof(is[0])
这在大部门环境下都事情得很好。只是敲的键盘次数有点多。所以,有了如下这个宏的呈现:
#define DIM(a)(sizeof(a)/sizeof(a[0]))
此刻就利便多了。可是依然不完美。思量下列环境:
宏的参数传入一个重载了operator[]操纵符的自界说工具
宏的参数传入一个指针
我们先看第一种环境。当传入一个重载了operator[]操纵的工具时(也许您会说:“等等,我绝对不会这样干的。”但是谁会为您包管呢?),编译器并不会给您报错,甚至吝啬到一条告诫都不会给出。不相信我吗?把如下代码片断拷贝到您的IDE中试试吧。
1.std::vector<int> vi;
2.cout << DIM(vi) << endl;
“岂有此理,我要把我这活该的编译器换掉!”您先别急,据我所知,今朝还没有哪家厂商的编译器会给堕落误或告诫提示,最重要的是,编译器基础没有这个责任。
在办理以上这个问题前,我们先插入一点有关C++数组与指针的常识。
许多环境下,C++中的数组可退化为指针。以下即是一个例子:
1.int is[] = {1, 2, 3};
2.int *pi = is;
我们会见数组时有两种方法:一种称为下标式会见,另一种称为偏移量会见。譬喻,要取得数组is的第二个元素,可别离回收is[1]和*(is + 1),两种方法等价。实际上,指针也有着同样的特点,也就是说pi[1]或*(pi + 1)也是取得第二个元素。更有趣的是,C++中的内建(build-in)下标式会见还可倒过来写,即is[1]与1[is]等价。受惊吧。强调一下,这种特性只有在内建的下标式会见时才正确,换句话说,自界说并重载了operator[]操纵符的范例是不具备这种特性的。通过vi[1]方法可取得vector的第二个元素,而当您写出1[vi]这样的代码时编译器就报错。
好了,回到我们的问题,我们可以借助上面所提到的C++特性来办理。把DIM宏的界说修改为:
1.#define DIM(a) (sizeof(a) / sizeof(0[a]))
第一个问题已被圆满办理。
#p#副标题#e#
继承第二个问题。我们需要通过某种机制让编译器可以或许区分数组与指针,也就是说,当我们传入指针时编译器报错,而传入数组则能正确通过编译。很自然的,我们想到函数挪用,借由函数参数来给以区分。像这样:
1.template<typename T>
2.size_t foo(T *);
3.template<typename T>
4.size_t foo(T ts[]);
很遗憾,这两个函数签名对付编译器来说没有两样,您的编译器会提示您反复界说。别悲观,其实已经很靠近了。稍微修改一下:
1.template<typename T, size_t N>
2.inline size_t DimensionOf(T (&ts)[N])
3.{
4. return N;
5.}
我们界说一个模板函数,吸收一个数组引用,个中T为数组元素范例,N是数组维度,为提高效率,界说成inline形式。编译器会帮我们把N推导出来,很是感激它。
此刻第一、二个问题都办理了:
1.int is[] = {1, 2, 3};
2.int *pi = is;
3.std::vector<int> vi;
4.DimensionOf(is);
5.DimensionOf(vi); // Compile-Error
6.DimensionOf(pi); // Compile-Error
因为是个函数,所以可以置于名字空间内,而inline形式的挪用开销可被忽略不计。很是好,可不完美,因为挪用功效不是编译期常量。我们不能这样利用:
1.template<int N>
2.class cls {};
3.
4.void f()
5.{
6. int is[] = {1, 2, 3};
7. int is2[foo(is)]; // Compile-Error
8. cls<foo(is)> c; // Compile-Error
9.}
操作sizeof操纵是在编译期而非运行期求值的事实,我们可再修改成如下:
1.template <size_t N>
2.struct dimension_help_struct
3.{
4. unsigned char uc[N];
5.};
6.
7.template<typename T, size_t N>
8.inline const dimension_help_struct<N> make_dimension_help_struct(T (&ts)[N])
9.{
10. return dimension_help_struct<N>;
11.}
12.
13.#define DIM(a) (sizeof(make_dimension_help_struct(a)))
首先界说了一个帮助模板布局体dimension_help_struct,我们期望模板参数N即为布局体的巨细,即N == sizeof(dimension_help_struct<N>)恒创立,然后界说了一个模板函数make_dimension_help_struct,让编译器推导出数组ts的维度并生成一个dimension_help_struct工具,最后界说一个DIM宏。
#p#分页标题#e#
为担保N == sizeof(dimension_help_struct<N>)创立,我们得担保编译器对dimension_help_struct工具利用1byte字节对齐。更准确的步伐是对布局体dimension_help_struct加以#pragma pack(1)指令。可是我们有更简朴的步伐:只要确保N == sizeof(dimension_help_struct<N>.uc)恒创当即可。
另外,模板函数make_dimension_help_struct的界说体基础不需要,因为sizeof是编译期求值,用不着函数挪用。不相信的话在第10行前随便敲几其中文,担保您照样能通过编译。
综合以上,最终版本大抵是这样:
1.template <size_t N>
2.struct dimension_help_struct
3.{
4. unsigned char uc[N];
5.};
6.
7.
8.template<typename T, size_t N>
9.const dimension_help_struct<N>& make_dimension_help_struct(T (&ts)[N]);
10.
11.
12.#define DIM(a) (sizeof(make_dimension_help_struct(a).uc))
因为所有步调都在编译期求值,所以无任何机能损耗。独一欠好的一点是DIM宏,不能将其置入名字空间内。
就在我筹备将此文章生存以便第二天提交时溘然对上面的要领又有了改造:
1.template<typename T, size_t N>
2.unsigned char (& dimension_help_fun(T(&ts)[N]))[N];
3.#define DIM(a) (sizeof(dimension_help_fun(a)))
您没看错,这就是全部代码。把前一种要领的帮助布局体都省掉了,只剩下一个帮助模板函数,这个函数吸收一个数组引用ts,返回一个unsigned char型并具有N个元素的数组引用。
(注:以上代码全部在VS2008、GCC4.1.2中测试通过。)
跋文:此文从新至尾所接头的要领即是笔者事情后对数组维度求值所先后回收的要领,笔者愚笨,前后超过多达3年,这也从侧面反应出C++的巨大性。