细论数组的维度(dimension)的计算

carylin 2008-09-23 01:24:59
(这是前两天我写的一篇文章,与各位分享。)

我们经常需要知道先前定义的数组维度,或是为了对其进行循环遍历,或是其它。当我们显示初始化数组而没有指定其维度时尤其如此。

1. int is[] = {1, 2, 3};

有C语言开发经验的读者可能经常使用如下方式来实现:

1. int dimension = sizeof(is) / sizeof(is[0])

这在大部分情况下都工作得很好。只是敲的键盘次数有点多。所以,有了如下这个宏的出现:

1. #define DIM(a) (sizeof(a) / sizeof(a[0]))

现在就方便多了。但是依然不完美。考虑下列情况:

1. 宏的参数传入一个重载了operator[]操作符的自定义对象
2. 宏的参数传入一个指针

我们先看第一种情况。当传入一个重载了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]))

第一个问题已被圆满解决。

继续第二个问题。我们需要通过某种机制让编译器能够区分数组与指针,也就是说,当我们传入指针时编译器报错,而传入数组则能正确通过编译。很自然的,我们想到函数调用,借由函数参数来给予区分。像这样:

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宏。
为保证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中测试通过。)
...全文
703 32 打赏 收藏 转发到动态 举报
写回复
用AI写文章
32 条回复
切换为时间正序
请发表友善的回复…
发表回复
danny1221 2008-09-24
  • 打赏
  • 举报
回复
jf
jia_xiaoxin 2008-09-24
  • 打赏
  • 举报
回复
很不错的帖子,学习了.
帅得不敢出门 2008-09-24
  • 打赏
  • 举报
回复
up 阅毕
skineffect 2008-09-24
  • 打赏
  • 举报
回复
到了后面好晦涩的语法呀。。。
matrixdwy 2008-09-24
  • 打赏
  • 举报
回复
很妙很妙
Tuzki 2008-09-24
  • 打赏
  • 举报
回复
int ia[10][10];
pengzhixi 2008-09-24
  • 打赏
  • 举报
回复
mark下
WuBill 2008-09-24
  • 打赏
  • 举报
回复
很好很强大,不接分,只为标记学习
yueyucanyang 2008-09-24
  • 打赏
  • 举报
回复
学习一下
大写的池 2008-09-24
  • 打赏
  • 举报
回复
飘过
星羽 2008-09-24
  • 打赏
  • 举报
回复
支持原创
开心爸爸84 2008-09-23
  • 打赏
  • 举报
回复
学习
fallening 2008-09-23
  • 打赏
  • 举报
回复
int N;
cin>>N;
int arr[N];

:)
roselake 2008-09-23
  • 打赏
  • 举报
回复
学习,JFo(∩_∩)o...
fallening 2008-09-23
  • 打赏
  • 举报
回复
[Quote=引用 17 楼 Jinhao 的回复:]
这似乎你那不叫纬度计算.应该叫元素个数计算.

纬度计算应该这样

C/C++ codetemplate<typename T>structdim
{enum{value=0};
};

template<typename T,intN>structdim<T[N]>{enum{value=dim<T>::value+1};
};

template<typename T,intN>unsignedchar(&helper(T(&)[N]))[dim<T>::value+1];intmain()
{chara[10][1][1][2];charb[1]
std::cout<<sizeof(helper(a))<<std::endl;
std::cout<<sizeof(helper(b))<<std::en…
[/Quote]

运行时确定的怎么办?
Jinhao 2008-09-23
  • 打赏
  • 举报
回复
这似乎你那不叫纬度计算.应该叫元素个数计算.

纬度计算应该这样


template<typename T>
struct dim
{
enum{value = 0};
};

template<typename T, int N>
struct dim<T[N]>
{
enum{value = dim<T>::value + 1};
};

template<typename T, int N>
unsigned char (& helper(T(&)[N]))[dim<T>::value + 1];

int main()
{
char a[10][1][1][2];
char b[1]
std::cout<<sizeof(helper(a))<<std::endl;
std::cout<<sizeof(helper(b))<<std::endl;
}


输出
4
1
richbirdandy 2008-09-23
  • 打赏
  • 举报
回复
up
fallening 2008-09-23
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 carylin 的回复:]
在C++中,0长数组是不合法的,但是g++对其进行了扩展,允许其存在。要是说上面的方法有缺陷,那是更促使你写出符合标准的C++代码,^_^(辩解)。要是你确实想使用0长数组,大可对dimension_help_fun进行一下特化:
template <typename T>
unsigned char (& dimension_help_fun(T(&ts)[0]))[0];
现在保管没错。

[/Quote]
这样做完全可以实现0长数组
忽然发现&还是可以做手脚的,待我看看能不能重载&搞下破坏
fallening 2008-09-23
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 carylin 的回复:]
在C++中,0长数组是不合法的,但是g++对其进行了扩展,允许其存在。要是说上面的方法有缺陷,那是更促使你写出符合标准的C++代码,^_^(辩解)。要是你确实想使用0长数组,大可对dimension_help_fun进行一下特化:
template <typename T>
unsigned char (& dimension_help_fun(T(&ts)[0]))[0];
现在保管没错。

[/Quote]
icc也认0长数组的,上边的程序用icc编译的结果是
$ icpc -c size.cc -Wall
size.cc(5): remark #1419: external declaration in primary source file
unsigned char (& dimension_help_fun(T(&ts)[N]))[N];
^

size.cc(12): error: no instance of function template "dimension_help_fun" matches the argument list
argument types are: (int [0])
std::cout << DIM(arr) << std::endl;
^

compilation aborted for size.cc (code 2)
carylin 2008-09-23
  • 打赏
  • 举报
回复
在C++中,0长数组是不合法的,但是g++对其进行了扩展,允许其存在。要是说上面的方法有缺陷,那是更促使你写出符合标准的C++代码,^_^(辩解)。要是你确实想使用0长数组,大可对dimension_help_fun进行一下特化:
template <typename T>
unsigned char (& dimension_help_fun(T(&ts)[0]))[0];
现在保管没错。
加载更多回复(12)

64,654

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧