众所周知,C/C++以及Java、C#等语言都属于强类型语言,在声明一个变量时总要带有有一个具体的类型。不过在基本类型的变化上,C/C++要丰富很多(具体情况下面会进行逐一介绍),而Java和C#中除了基本类型外,能进行迭代组合的就是数组种别了,这几个语言都能够基于数组的维数进行理论上的无穷迭代——比如:int a[][][][][]...[](Java和C#可以将[]放在变量标识符a的前面)。
而C/C++之所以有非常丰富的类型,这要归功于(同时也是备受指责的)指针。实际上最本质的问题还是在于一个非常特殊的操作——取变量地址操作。
在Java中你可能会写以下代码:
Object obj = new Integer();
从上面代码可以获悉,一个指向Object类的对象引用obj指向了一个Integer类的对象。然后就不会有什么戏法了。
我们再看一个C语言的例子:
int a = 0; int *p = &a;
嗯,如果你认为这样就算完了的话,那可就错了。因为还可以基于变量p再作一次更深的引用:
int a = 0; int *p = &a; int **pp = &p;
呵呵,我们可以以此导出int <n个*> <n个p>;
上面,n的理论值可以是无穷大。当然,这肯定是受限于编译器及系统环境的。
不过我们已经可以看出,之所以能够加n个*是因为n级指针变量可以指向n-1级指针变量的地址,当n-1=0时,该变量就不是指针了。
我们再来看另一个例子:
int a[5] = { 1, 2, 3, 4, 5 }; int (*p)[5] = &a;
上面代码中,数组变量a带有最多5个int型的元素,然后指针p指向数组a的地址。
C/C++中对指针定义得非常广泛,即使是数组,也可以取它地址。不过这样的好处是可以利用指向n-1维数组变量地址的指针来访问n维数组元素,比如:
int a[][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; int (*p)[3] = a; p[0][0] += p[2][2];
我们可以看到,利用指针p可以访问二维数组中的任一元素。所以既然能够取数组变量的地址,而且数组的维数本身可以无穷迭代,因此指向数组的指针的类型也可以进行无穷迭代。比如,要指向一个二维数组的指针可以这么写:
int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; int (*p)[2][3] = &a;
上面,指针p指向了二维数组a的地址。所以,对于指向n维数组的int型指针可以这么表示:int (*p)[a1][a2]...[an]。
既然有int(*)[N]类型,那么它当然可以修饰数组本身。如果一个一维数组,其元素类型int(*)[3]类型的话,那么该如何声明这个变量呢?下面可以得到答案:
// Step1: int a[3] = {1, 2, 3}; // Step2: define an array viariable type s[2] = { &a, &a }; // Step3: determine "type" int (*s[2])[3] = { &a, &a };
从上面这个例子我们可以看到,指向数组的指针类型体现为int(*)[3],而将变量标识符放在()内。而上面例子中的变量为:s[2],即带有两个元素的数组。
现在就变得很有意思,上面变量s的类型又是什么呢?它是这么表达的:int(*[2])[3]。如果对s取地址,那么&s的类型是什么呢?
先不说答案,我们可以根据int a[],取a地址的类型情况进行推导——
int a[3]; ==> typeof(&a) == int(*)[3];
int (*s[2])[3]; 我们不妨先将(*s[2])规约为T,这样,int (*s[2])[3] ==> int T[3],那么typeof(&T) == int (*T)[3]
下面我们先展开T,就像玩代数一样——int(*T)[3] ==> int(*(*s)[2])[3],最后将变量标识符拿掉,得:int(*(*)[2])[3]。
这里需要注意的是,在将(*s[2])放出时,变为(*s)[2],而不是(*s[2])。因为(*s[2])表示数组,而不是指针,这里表示指向
取int(*[2])[3]类型数组地址的指针。从上面简单的指向一维数组地址的指针这个例子也能推导出这个结论。int a[3]变为
int (*p)[3],数组标识[3]在()外。
我们可以得到一个完整的代码:
int a[3] = {1, 2, 3}; int (*s[2])[3] = { &a, &a }; int (*(*p)[2])[3] = &s;
呵呵。上面第三句表达式是不是看上去很复杂的样子?但是通过刚才的分析我想再复杂一些的数组指针类型也难不倒你了吧。
下面将介绍更复杂些的关于函数指针的类型。
C/C++中的指针技术可谓发挥到及至,通过函数指针可以实现回调、委托等方案。通过函数指针调用函数实际上相当于计算机指令中的间接寻址调用。也就是call reg(reg为某种寄存器)。在C/C++中,普通函数指针有一种非常有意思的特性,我们先看以下代码:
#include static void Output(void) { puts("Hello, world!"); } int main(void) { void (*p)(void) = &Output; (*p)(); p(); p = Output; (*p)(); p(); return 0; }
上述代码中定义了一个非常简单的函数——Output,不带有返回值也没有参数。然后在main()函数中用了两种初始化函数指针p 的方式以及两种调用方式,这些初始化形式及调用形式都没有问题。对于C/C++中的普通函数而言,由于对函数标识符的引用实际上就是函数入口地址(函数代码的首地址),因此&Output与Output是等价的。而且通过C++的typeid(*p).name()的输出也是得到void(*)(void)类型。因此,这里我们可以发现typeof(Output) == typeof(&Output),它们不仅值(首地址)一样,而且类型也完全一样。这与数组不同,虽然int a[3]; a与&a的值相同,但是它们的类型却不一样——前者为int[3](可直接隐式地转为int*);后者为int(*)[3]。所以后面的利用函数指针间接调用p()与(*p)()都没有问题(因为p与*p类型与值都完全一样)。
那么我们现在再深一步,如果我来个&p操作会怎么样呢?由于p本身是个变量,是变量一定有其地址,因此这里的&p与&Output就不同了,它原本是void(*)(void)类型,而&p加上取地址符后就成了void(**)(void)类型了。因此我们可以看以下代码:
#include static void Output(void) { puts("Hello, world!"); } int main(void) { void (*p)(void) = &Output; void (**pp)(void) = &p; (*pp)(); (**pp)(); return 0; }
上述代码中,pp指向p 的地址。那么对它的调用则至少要加一个*,对其先进行一次解引用,从void(**)(void)类型转为void(*)(void)类型,当然,后面再加一个*也没问题,因为(**pp)与(*pp)类型是等价的。
基于函数指针的迭代类型可谓非常丰富。因为几乎所有类型都可以作为函数的返回类型,除了绝对的数组类型,比如int[3],但是我们可以用int*来代替。下面先举一个简单的例子:
#include static int array[] = { 1, 2, 3, 4, 5, 6, 7 }; static int* GetArray(int index) { if(index >= sizeof(array)/sizeof(array[0])) return NULL; return &array[index]; } int main(void) { int* (*p)(int) = &GetArray; printf("The value is: %d/n", (*p)(5)[0]); return 0; }
上述代码中,函数GetArray返回int指针类型,并且带有一个int类型的参数。在main()函数中,函数指针p 的类型为:int*(*)(int)。由于调用GetArray函数后返回一个数组的某项元素的地址,因此可以直接利用数组下标对元素进行访问。
我们现在可以看到一般简单的函数指针的类型为:<type> (*<id>)(<arg list>),如果是n级函数指针就是:<type>(<n个*><id>)(<arg list>)。下面我们先寻求一份小的刺激,将指向数组的指针作为一个函数的返回类型,然后再看其函数指针的类型。
static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; }
上述代码中,函数GetArray()返回类型为int(*)[10]。在为函数做这样的返回类型时,我们可以将函数标识符连同参数先看作为一个整体T,那么对于类型为int(*)[10],就是int(*T)[10]。然后将T直接展开即可——int (*GetArray(void))[10]。
现在我想用一个指针指向这个GetArray函数。我们也可以用上述方法。首先,函数的返回类型为int(*)[10],因此我们先写int (*T)[10]。然后,我们根据函数指针类型变量的书写格式——<type> (*<id>)(<arg list>),将T进行展开。假设我们现在这个函数指针的变量名为p,那么T就可以写成:(*p)(void)。那么将其代入可得:int (*(*p)(void))[10]。下面贴出完整代码:
#include static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; } int main(void) { int (*(*p)(void))[10] = &GetArray; printf("The value is: %d/n", (*(*p)())[0]); return 0; }
那么,我们将复杂度再做提升。我们先看以下代码:
#include static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; } static int (*(*g_p)(void))[10] = &GetArray; static int (*(*ComplexFunc(int i, char c))(void))[10] { printf("The integer is: %d/n", i); printf("The character is: %c/n", c); return g_p; }
上述代码中在全局定义了一个函数指针g_p指向GetArray函数。然后定义了一个返回g_p变量的函数,它带有两个参数。那么g_p的类型是int(*(*)(void))[10],要定义一个返回该类型的函数看上去比较复杂,但是我们有了前面的经验,这事情就好办了。我们仍然将函数ComplexFunc(int i, char c)作为T,放入类型中,得:int (*(*T)(void))[10],然后直接将T用ComplexFunc替换得到:int (*(*ComplexFunc(int i, char c))(void))[10]。那么下面我们要定义一个指向该函数的指针。同样先利用<type> (*<id>)(<arg list>),那么这里的类型就是int(*(*)(void))[10],再将 (*<id>)(<arg list>)放入即可。假设指针标识符名为p,那么这个形式可以得到:(*p)(int, char)。将它放入返回类型,既得:int (*(*(*p)(int, char))(void))[10]。下面提供完整的代码:
#include static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; } static int (*(*g_p)(void))[10] = &GetArray; static int (*(*ComplexFunc(int i, char c))(void))[10] { printf("The integer is: %d/n", i); printf("The character is: %c/n", c); return g_p; } int main(void) { int (*(*(*p)(int, char))(void))[10] = &ComplexFunc; printf( "The value is: %d/n", (*(*(*p)(10, 'A'))())[0] ); return 0; }
下面我们考虑一下再丰富一点的情况:假设我有下面一段代码:
#include static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; } static int (*(*g_p2[2])(void))[10] = { &GetArray };
从上面的代码中我们可以看到,g_p2是一个带有两个元素的数组,并且元素类型为int(*(*)(void))[10]。我们现在要定义一个函数,其返回值为&g_p2该如何做呢?大家可以动动脑筋看,我贴出整个代码:
#include static int array[10] = { 1, 2, 3, 4, 5, 6, 7 }; static int (*GetArray(void))[10] { return &array; } static int (*(*g_p)(void))[10] = &GetArray; static int (*(*ComplexFunc(int i, char c))(void))[10] { printf("The integer is: %d/n", i); printf("The character is: %c/n", c); return g_p; } static int (*(*g_p2[2])(void))[10] = { &GetArray }; static int (*(*(*ComplexFunc2(const char* s))[2])(void))[10] { printf("The string is: %s/n", s); return &g_p2; } int main(void) { int (*(*(*p)(int, char))(void))[10] = &ComplexFunc; printf( "The value is: %d/n", (*(*(*p)(10, 'A'))())[0] ); int (*(*(*(*p2)(const char*))[2])(void))[10] = &ComplexFunc2; printf( "The value is: %d/n", (*(*(*(*p2)("Hello, world!"))[0])())[0] ); return 0; }
好的。
上面提到的很多复杂类型在C/C++中可以通过利用typedef关键字进行抽象。
要获得具体情况,大家可以在论坛上查找相关文章。
在做项目可不要用太多的*哦,否则让人很难看懂,呵呵。
当然,如果你看了这篇文章相信你对于看这些复杂的东东也不会有像看天书那种感觉了。
这里介绍几种typedef的用法:
#include
typedef int array5[5];
typedef int (*ptr_array5)[5];
typedef void (*PFUNC)(void);
typedef void FUNC(void);
static void Hello(void)
{
puts("Hello, world!");
}
int main(void)
{
array5 a; // ==> int a[5]
ptr_array5 p = &a; // ==> int (*p)[5]
PFUNC fp = &Hello; // ==> void (*fp)(void)
FUNC *pfp = &Hello; // ==> void (*pfp)(void)
(*fp)();
(*pfp)();
return 0;
}
上述代码中,有一点值得注意。FUNC被定义为void(void)类型,也就是十足的函数类型,
因此要用它进行定义一个函数指针变量时,一定要用FUNC*。
以上均是对C/C++通用的迭代类型的介绍。下面将针对C++的特征进行进一步介绍迭代类型。