转自:http://blog.sina.com.cn/s/blog_496be0db0100x9cz.html
阅读Chromium代码时,碰到一个宏arraysizeof,定义如下:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
// That gcc wants both of these prototypes seems mysterious. VC, for
// its part, can't decide which to use (another mystery). Matching of
// template overloads: the final frontier.
#ifndef _MSC_VER
template <typename T, size_t N>
char (&ArraySizeHelper(const T (&array)[N]))[N];
#endif
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
好久没有动手写模板了,看到这个的时候突然想,为什么Google不用下面的方式得到数组元素个数呢?
#define countof( array ) ( sizeof( array )/sizeof( array[0] ) )
翻了翻《Thinking in C++》和MSDN,发现这种做法有个很大的问题,那就是当countof的参数时指针或者类时,这个宏将无法得到正确结果!! 例如:
#define countof( array ) ( sizeof( array )/sizeof( array[0] ) )
class CIntArr
{
private:
int * p;
size_t size;
public:
int & operator [] ( size_t i );
};
int main()
{
......
int a[5];
int* p = &a;
CIntArr ca;
int isize1 = countof(p);//返回指针p的大小,win32下isize1=4
......
int isize2 = countof(ca);//返回函数的大小,而不是返回ca.p的大小
}
多半基于上面类似的原因,Google定义了开始那个奇怪的宏。后来仔细回想了一下,记得从VS2005开始,在stdlib.h里就一个宏 _countof 专门来计算数组的元素个数,_countof 定义如下:
extern "C++"
{
template <typename _CountofType, size_t _SizeOfArray>
char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
#define _countof(_Array) sizeof(*__countof_helper(_Array))
}
使用_countof宏的方法:
#include <stdlib.h>
int main()
{
int arr[5];
......
int arraySize = _countof(arr); //得到数组arr的大小 arraySize=5
}////////////////////////////////////////////////////////////////////////////////////////////////
这是一个比较复杂的宏,VC6不支持这种宏定义。该宏使用了一些不常见的语法语法。_countof宏和arraysize宏很相似,主要区别在于sizeof()的参数类型,前者为了兼容一些老的编译器而使用了数组指针的值(*(char*)[]),而后者使用了数组引用((char&)[])。
下面,以 arraysize为主,理解一下这种类似的宏定义。
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
第1,2行是一个函数模板声明,该函数没有定义实现体ArraySizeHelper是一个函数名,
其参数是T (&)[N](数组引用);其返回值也是一个数组引用 char(&)[N]。(注意:_countof()宏中__countof_helper函数的返回值是“数组指针”char (*)[N]。)
我们先看看一些C++的语法:
1) 数组引用 T (&)[N]
例如:
int arr[10] = {0};
int (&refa1)[10] = arr; //正确,refa是对数组arr的引用,热发大小也是10;
int (&refa2)[6] = arr; //错误,refa的大小和arr不匹配;
int &refa3[10] = arr; //错误,语法不正确
数组信息包括:数组元素的类型以及数组的大小,在声明数组引用时,类型和大小都要匹配,否则编译器将报错:error C2440: 'initializing' : cannot convert from 'int [10]' to 'int (&)[6]'
注意:C++里没有“引用数组”这个语法概念。"int &refa3[10]"这样的语法是通不过编译的。
2) 数组指针 T (*)[N]
int arr[10];
int (*p)[10] = &a;// 正确,p是一个数组指针,指向一个大小固定的数组”
int (*p)[5] = &a;//错误,数组指针大小部队
和引用类似,数组指针声明时类型和大小都要匹配,否则返回编译错误:error C2440: '=' : cannot convert from 'int (*)[5]' to 'int (*)[10]'。
3)指针数组 T* ()[]
C++不支持“引用数组”,但是支持指针数组,而且大家常用,比如:
char * boys[] = {
"Tom",
"John",
"Mike" };
boys就是一个指针数组,该数组包含有3个char类型指针的成员,每个成员指向一个字符串。指针数组里面的成员指针所指向的内存区域大小可以不同。由于指针类型本身只携带所指向内存的开始位置以及所指向内存的数据类型,而不含内存的大小,因此下面的语句都是正确的:
int a[5];
int b[10];
int * p = 0;
p = a; // 正确
p = b; // 正确
4) 数组类型返回值的函数的声明语法:
返回值为数组指针T (*)[N]:T (* Foo( param_list ) )[N];
返回值为数组引用T (&)[N]:T (& Foo( param_list ) )[N];
这个是C++的语法规定,没有办法,大家可以参考《Thinking in C++》或者C++国际标准,语言设计者对(),[],*,&,&&这些符号所放的位置有特殊的要求。
大家有时候容易忘记,sizeof不是一个函数,而是C/C++语言中的关键字(keyword)。编译器在编译期间将对sizeof参数的表达式进行推算得到参数表达式的最后类型信息,然后再对其计算大小,看下面的代码:
int Foo(); // 只定义函数,不提供实现
int main()
{ ......
int a = 1;
sizeof(a++);
cout << a << endl;//输出结果为1 !!!!
cout << sizeof( Foo() );//输出结果为函数地址的大小,win32下是4
......
}
因为sizeof是在编译时候完成对表达式a++结果类型的推算,并返回该类型的大小,因此a++根本没有执行,所以输出结果是1而不是2!!!函数Foo()函数也不会在运行的时候被执行,仅仅是在编译时,编译器需要知道的是Foo()函数返回值的类型是什么,因此不需要函数体。
回顾一下“模板自动推导(template deduction)”这个C++里的概念,模板推导的关键是指编译器完成对模板参数类型信息等的推导发生在编译期间(compile time),而不是在程序运行期(run time)完成的。在此期间不涉及内存布局分配的问题,编译器只关心声明信息(即声明表达式里面所带的类型信息),然后会自动推导模板参数的各种信息(包口参数类型,参数个数和传递的数值等)。例如:
template<int a, int b>
struct _sum_
{
enum { value = a + b};
};
int sum = _sum_<3, 4>::value; // sum=7
这个例子里sum::value的值在编译期间就确定为7了,因此给sum赋值根本没有经过CPU计算。上面这种只包含数值的例子很特殊,下面再看一个常见的包含类型的例子:
template< typename T, size_t N>
void Foo( T (&)[N] );
......
int a[5];
Foo(a);
......
编译期间,编译器经过推导可知类型T=int,数组大小N=5,因为Foo声明的参数使用了数组引用,因此编译器能够通过实参知道数组的类型和数组元素数量。
编译器在进行模板推导时,还会对“实参”和“形参”能够匹配的所有情况进行推导,如果所有的尝试都不能匹配,将报告编译失败错误,比如:
int * p = NULL;
Foo( p); // 错误:类型错误
一个指针不可以赋值给一个数组引用,所以编译会报错error C2784: 'void Fun(T (&)[N])' : could not deduce template argument for 'T (&)[N]'。故对于C++的_countof宏,如果传递一个指针给它,编译将会失败。
最后,注意一下arraysize和__countof宏定义中sizeof()的参数类型:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
template <typename _T, size_t N>
char (*__countof_helper(T (&array)[N]))[N];
#define _countof(array) sizeof(*__countof_helper(array))
注:这里对_countof宏的定义进行了等价的简化。
arraysize里的sizeof()的参数是 “ArraySizeHelper(array)”,因为ArraySizeHelper函数返回值是数组引用,可以返回数组的类型和个数,因此sizeof()返回值是sizeof(char) * N,由于当前所有机器上sizeof(char) = 1,因此返回的大小就是arraysize宏参数中输入的数组的大小N。
_countof里的sizeof()的参数时“*__countof_helper(_array)”,注意前面的*号。因为__countof_helper函数返回值是数组指针,如果直接用sizeof(__countof_helper(_array)),那么那么_countof()返回的值永远是一个指针类型的大小(在32位系统上总是4)。因此需要使用“*__countof_helper(_array)”作为sizeof()的参数,这样将取得该数组指针所指向数组的大小。
注:_countof中sizeof()没有使用引用而指针作为参数,使因为希望兼容老的编译器,而Google则放弃了这些老旧编译器的支持。
下面的例子演示了对指针和数组作为sizeof()参数时候的巨大区别:
int arr[10];//定义大小为10的整型数组;
int *p = arr;// p是普通整型指针,指向数组arr的开始位置
int (*parr)[10] = &arr;// parr是数组指针,指向数组arr
sizeof(p) //返回指针p的大小,win32环境下为4
sizeof(arr) //返回数组arr的大小,win32环境下为 = 5*sizeof(int) = 20
sizeof(*p) //返回指针所指向的第一个元素的大小 = sizeof(int) = 4
sizeof(parr) //返回数组指针的大小,win32环境下为4
sizeof(*parr) //返回parr所指向的数组的大小,win32环境下为 = sizeof(a) = 20
在C/C++中,指针不包含所指向内存空间的大小信息,而数则包含大小信息。但值得注意的是,在C/C++规范中,数组函数的形参时与指针等价,即将实参数组经过函数参数传递后,数组的大小信息将丢失,如:
void Foo( int arr[10] )
{
int n = sizeof(arr); // win32下,返回值n=4,相当于数组类型的大小
}
因此,上面的Foo申明等价于下面的声明,[]中的参数可以忽略:
void Foo( int arr[20] );
void Foo( int arr[] );
void Foo( int *arr );
因此,为了需要把大小信息传递到函数内部,需要额外加一个参数来表示所传递的数组大小信息,该信息由调用者来维护。
void Foo( int a[], int size );
当然,C++里也可以用STL里的vector模板类来代替数组:
void Foo( vector<int>& v); //可以通过vector::size()方法能得到数组大小信息
arraysize中ArraySizeHelper函数和_countof中的__countof_helper函数返回值中,使用了类型char,而不是T,为什么呢?原因和简单,因为sizeof(char)在目前任何机器的操作系统上都等于1,因此这两个函数的返回值都刚好等于输入数组的大小N。
最后要叨一下的是ArraySizeHelperc和__countof_helper函数没有定义函数体的额外难题。由于sizeof是在编译期得到表达式返回类型的大小,因此函数体在运行期间根本不会被调用,除非你在代码中直接调用它们。因此,如果没有特殊需要,就不要定义函数体,这样这些函数在进程运行后不会被加载,省得浪费内存(虽然内存现在是白菜价钱,但能省点就省点吧)。
备注:
在VS2005以及后续的VS中,winnt.h里其实也有个同样功能的宏 RTL_NUMBER_OF_V2
extern "C++" // templates cannot be declared to have 'C' linkage
template <typename T, size_t N>
char (*RtlpNumberOf( UNALIGNED T (&)[N] ))[N];
#define RTL_NUMBER_OF_V2(A) (sizeof(*RtlpNumberOf(A)))
参考:
《Modern C++ Design》(中文名:《C++设计新思维》)
《C++ Templates the complete guide》(中文版本里侯捷翻译的《C++ Templates全览》较好)
《Thinking in C++》
MSDN blog: How Would You Get the Count of an Array in C++? 地址:
http://blogs.msdn.com/b/the1/archive/2004/05/07/128242.aspx
转自:http://blog.chinaunix.net/space.php?uid=25909722&do=blog&id=2901495