如何理解stdlib.h里的_countof()宏

在stdlib.h里有一个宏_countof,如下:

extern "C++"
{
template
char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];


#define _countof(_Array) sizeof(*__countof_helper(_Array))
}

 

这个宏的作用就是得到一个数组元素的大小。

 

如下使用:

#include

int main()

{

      int a[5];

      ......

      int arraySize = _countof(a); //得到数组a的大小 

}

 

这个宏使用了一些大家平时都比较少用到的语法。所以理解起来,一下子不是很明白。

所以这里简单说说,如何理解它吧。

为了说明方便,改写如下:
template T, size_t N>
char (* __countof_helper ( T (&_Array)[N] )  )[N];

 

这是一个函数模板声明,没有定义实现体。
__countof_helper是一个函数(红色部分);
其参数是一个“数组引用”T (&)[N](蓝色部分);
而返回值是一个“数组指针”(指向一个数组的指针) char (*)[N](绿色部分);


首先,需要理解3个C++的语法:


1) 数组引用

T (&)[N] (注意:有个括号)。比如:
int a[5] = {0};
int (&ra)[5] = a; 这里ra就是一个对数组a的引用,其这个数组的大小也是5;

 

数组的信息包括:数组元素的类型,和数组的大小;声明“数组引用”时,类型和大小都要匹配,否则是错误的。比如:
int a[5] = {0};
int (&ra)[4] = a; //编译错误:error C2440: 'initializing' : cannot convert from 'int [5]' to 'int (&)[4]'

 

注意:C++里没有“引用数组”这个语法概念。"int &ra[5]"这样的语法是通不过编译的。

 

2) 数组指针

T (*)[N] (注意:有个括号)。比如:
int a[5];
int (*p)[5] = &a;// 是一个指针,该指针指向“一个有5个元素的数组”;简称为“数组指针”

 

同样,声明时类型和大小都要匹配,如下是错误的。
int a[4];
int (*p)[5] = &a;//error C2440: '=' : cannot convert from 'int (*)[4]' to 'int (*)[5]'

 

上面说了,C++不支持“引用数组”;但是“指针数组”是绝对支持的,平常都会经常到的。比如如下:
char * names[] = {
                       "Mike",
                       "John",
                       "Tom" };
names就是一个指针数组,该数组有3个元素,每个元素都是一个char类型指针,指向一个字符串。
从上面的例子可以看出,指针数组里面的指针所指向的那块内存区域里的数据大小可以是不同的。
因为指针本身所带的信息只知道被指的内存是什么数据类型,不知道大小信息。比如:
int a[4]; int b[5];
int * p = 0;
p = a; // ok
p = b; // ok

 

3) 函数返回“数组指针”的声明语法

T (* Fun( param_list ) )[N];
Fun是一个函数,其返回值类型是 T (*)[N];

为什么不可以如下这么声明?
T (*)[N] Fun( param_list ); 这么声明多好理解啊。但是这么是不行的。
C++语法比较复杂,语言设计者对(),[],*,&,&&这些符号所放的位置有特殊的要求。
<>的第3章有一节“Aside: complicated declarations & definitions”,举了一些复杂声明语法的例子。

 

第二,需要知道的是:sizeof是在编译期完成的。
sizeof是C/C++语言中的keyword,不是函数。对其参数里的表达式是不会在执行期去执行的,而只是在编译期去推算整个表达式的最后的类型信息。
比如: int a = 1; sizeof(a++); cout << a;   // a还是1. 不会是2;
因为是在编译期完成,编译器只需要类型信息,不需要函数的实现体。所以就可以不用提供定义体。
下面这个例子可能更能说明这个问题。
int Fun(); // only declare it without definition
sizeof( Fun() );  // 这里Fun()函数不会在运行的时候被执行,仅仅是在编译时,编译器需要知道的是:Fun()函数返回值的类型是什么。


第三,需要理解的是“模板自动推导(template deduction)”
模板推导是由编译器在编译期(compile time)完成的,而不是在执行期(run time)完成的。

这个是理解的关键之一。不涉及内存布局分配的问题。编译期间,编译器只关心声明信息(也就是声明式里面所带的类型信息)。模板推导会自动推导模板参数的各种信息(类型,传递过来的数值等)。以下是一个例子:
template
struct sum_
{
  enum { value = x + y };
};
int sum = sum_<3, 4>::value; // sum的值在编译期间就已经确定下来了,不是在执行期间由CPU运算得到。
这个例子里的模板,仅仅只有数值的传递,没有包含类型。这个可能大家很少见到的。但是模板是支持的。

 

例子二:
template< typename T, size_t N>
void Fun( T (&)[N] );

这个模板里既有类型T,又有数值N;模板推导时,是根据Fun的实参来推导得到的。比如:
int a[5];
Fun(a); // 编译器经过推导就知道,T=int,N=5。注意这里的N能得到5,是因为Fun的参数声明决定的。这里Fun的参数是:数组引

用。对于“数组引用”,上面说过,编译器能够从实参那里知道数组的类型,又能知道数组的大小。

 

同时,编译器在进行模板推导时,会对“实参”和“形参”能够匹配的所有情况进行推导,经过所有的尝试都不能匹配,就会报告编译失败信息。比如,对于这个例子,我们将一个指针作为实参传递给它:
int * p = NULL;
Fun( p ); // error C2784: 'void Fun(T (&)[N])' : could not deduce template argument for 'T (&)[N]' from 'int *'
因为一个指针不可能赋值给一个数组引用。所以编译就会出错。


所以对于stdlib.h里的这个_countof(),如果传递一个指针给它,编译就会失败。

 

同时,需要指出的是,VC6对模板支持的很不好,一些复杂的模板语法就编译不了。根据我的测试,就这里所说的_countof(),在VC6里就编译不了。VC2005/2008就已经支持的很好的。所以这个_countof()是在VC2005中才提供的。在VC6所带的stdlib.h里没有这个_countof()宏的。

 

【题外话:模板是泛型编程的基础。利用模板自动推导的能力,可以构建出很有技巧性的代码实现。比如:Boost库。更甚者:Loki库(中文名:C++设计新思维)这本书附带的库。关于模板的语法和自动推导,可以看

complete guide>,大陆翻译的中译本编译的不好。侯捷翻译的《C++ Templates全览》比较好,网上有电子的pdf可以找到,但是只

能在电脑上看,不能打印,也不能copy里面的文字。】

 

第四:注意事项
template
char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
#define _countof(_Array) sizeof(*__countof_helper(_Array))

 

a) #define里的sizeof()的参数是 “*__countof_helper(_Array)”(注意:有个*),而不是“__countof_helper(_Array)”,
如果是“__countof_helper(_Array)”,那么_countof()返回的值永远是一个指针的大小(在32位系统上总是4)。
因为__countof_helper返回的是一个“数组指针”,加上*,就是取得该指针所指向的数组。


sizeof(一个指针)和sizeof(一个数组),是有很大的区别的。看下面的例子:


int a[5];
int *p = a;
int (*pa)[5] = &a;


// p和pa都是指针,但是指针的类型不同:p的类型是int*;pa的类型是int(*)[5]。


sizeof(p)       // = 4;
sizeof(a)       // = 20 = 5*sizeof(int)
sizeof(*p)     // = sizeof(int) = 4;
sizeof(pa)    // = 4;
sizeof(*pa)  // = sizeof(a) = 20;

 

C/C++对于数组和指针是明确区分其类型的。数组的信息包括(类型+大小),而指针则没有大小信息。
在这里附带要说一下,数组经过函数参数传递后,数组的大小信息就丢失了。比如:
void Fun( int a[5] )
{
   int n = sizeof(a);  // n为4,相对于就是sizeof(int*).因为编译器只传递过来了数组的地址,大小信息没有了。
}


所以,上面Fun的参数声明里面的数组的具体数字是被忽略掉的。其等价声明式可以是:
void Fun( int a[10] );
void Fun( int a[] );
void Fun( int *a );


为了需要把大小信息传递到函数里面去,需要额外加一个参数来表示大小信息,这个大小信息需要调用者来提供,并且有调用者来保证大小信息的正确性。
void Fun( int a[], int size );
或者使用stl里的vector模板类来代替:void Fun( vector& v); 因为vector是一个类,它有size()方法能得到大小信息。

 

b)__countof_helper函数的类型是char,而不是_CountofType。因为sizeof( * (char(*)[N]) )【注:这么写只是为了示范说明,

在代码里不能这么写,通不过编译的】 刚好等于N,因为sizeof(char)在任何操作系统上都是=1;

 

c) 这个模板函数只有声明(declaration),没有定义(definition)。没有定义体的原因是:sizeof是在编译期完成的,不需要具体

的定义体出现(上面第二点里说了)。同时,模板推导也是在编译期完成的。所以_countof()整个宏在编译期就完成了。不需要函

数的具体实现体。
如果定义了实现体,_countof依然能工作,但是那些实现体都是不会被用到的,而这些实现体会在程序运行起来之后,加载到内存

里。这样就浪费内存了。
因为__countof_helper()没有定义体,所以使用的时候只能使用_countof宏本身,可不要像下面那样调用__countof_helper()。
int a[5];
__countof_helper(a); // 能编译通过,但是链接时,链接器会告诉你找不到实现体。


d) google chrome浏览器源代码里也有一个类似的宏如下:
template
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))

跟_countof的区别是:
1) ArraySizeHelper函数返回的是 char (&)[N],而不是char (*)[N];
2) #define里没有*号。因为sizeof( char (&)[N] ) = N;

 

为什么stdlib.h里的_countof宏,不用&,而要用*?
在winnt.h里其实也有个同样功能的宏RTL_NUMBER_OF_V2【注:VC6里没有,VC2005才提供的】,其中有

一句注释是这样的:
“We could return a reference instead of a pointer but older compilers do not accept that.”:)
That's it.

 

winnt.h 里的注释说明的很好,值得看看。摘抄如下:
//
// RtlpNumberOf is a function that takes a reference to an array of N Ts.
//
// typedef T array_of_T[N];
// typedef array_of_T &reference_to_array_of_T;
//
// RtlpNumberOf returns a pointer to an array of N chars.
// We could return a reference instead of a pointer but older compilers do not accept that.
//
// typedef char array_of_char[N];
// typedef array_of_char *pointer_to_array_of_char;
//
// sizeof(array_of_char) == N
// sizeof(*pointer_to_array_of_char) == N
//
// pointer_to_array_of_char RtlpNumberOf(reference_to_array_of_T);
//
// We never even call RtlpNumberOf, we just take the size of dereferencing its return type.
// We do not even implement RtlpNumberOf, we just decare it.
//
// Attempts to pass pointers instead of arrays to this macro result in compile time errors.
// That is the point.
//
extern "C++" // templates cannot be declared to have 'C' linkage
template
char (*RtlpNumberOf( UNALIGNED T (&)[N] ))[N];

#define RTL_NUMBER_OF_V2(A) (sizeof(*RtlpNumberOf(A)))

 

P.S. 下面这个链接值得看。其中的解释相当好。
http://blogs.msdn.com/the1/archive/2004/05/07/128242.aspx

 

附图:一些语法

你可能感兴趣的:(如何理解stdlib.h里的_countof()宏)