前言

相信大多数的同学都是第一门能接触到语言是C/C++,其中的指针也是比较让人头疼的部分了,因为光是指针都能专门出一本叫《C和指针》的书籍,足见指针的强大。但如果不慎误用指针,这些指针很大可能就会像恶魔一样把你的程序给直接搞崩溃。

3个月前,我编写了一份这些指针都是恶魔吗?.c的文件,里面从大多数常用的指针类型,一路延伸到纯粹只是在窥探编译器所能产生的恐怖造物,为了增加趣味性,我还把这些指针都划分了段位,只有辨识出该段位绝大多数的指针才能升段。目前看到的同学基本上都分布在青铜到黄金的水平。现在我要将这些恶魔般的指针公诸于世,欢迎大家前来接受挑战自虐。

前置声明:

  1. 题目会包括数组、指针、函数,以及它们的各种谜之复合体;
  2. 本文后面提及的一些指针不考虑什么实用性,就当做是玩个游戏,但适当情况下会对这些指针做必要讲解;
  3. 如果你对指针开始产生不适、恐惧感,建议你提前离开,以免伤到你对C语言的热情;
  4. 你想从这些指针里面挑一道作为自己的题目?随你喜欢。

这些指针都是恶魔吗?

下面的所有题目,你可以把自己的思路写在评论中。考验结束后,你也可以在评论里面留下自己的段位证明(请诚实对待)。

青铜(答对所有题升至该段位,正确率100%)

请用文字描述下列指针、数组的具体类型:

int * p0;
int arr0[10];
int ** p1;
int arr1[10][10];
int *** p2;
int arr2[10][10][10];

下面适当留白以供思考,想好后就可以往下翻看答案。








































青铜题解

对于初学C指针的同学基本上应该都能答出来:

int * p0;               // p0是 int指针
int arr0[10];           // arr0是 int数组(10元素)
int ** p1;              // p1是 int二级指针
int arr1[10][10];       // arr1是 int二维数组(10*10元素)
int *** p2;             // p2是 int三级指针
int arr2[10][10][10];   // arr2是 int三维数组(10*10*10元素) 

白银(答对4题升至该段位,正确率80%)

请用文字描述下列指针、数组、函数的具体类型:

int (*p3)[10];
int *p4[10];
int *func0(int);
int func1(int * p);
int func2(int arr[]);   

这些指针还是比较常见、实用的,想好后就可以往下翻看答案。








































白银题解

int (*p3)[10];中的p3*先结合,说明p3是一个指针,然后把(*p3)拿开,剩下的就是p3这个指针所指之物(即int[10])。答案:p3是一个指向[int数组(10元素)]的指针,符号化描述即p3int(*)[10]类型。

int *p4[10];中的p4考虑到优先级,会先与[]先结合,而不是*,说明p4是一个含10元素的数组,然后把p4[10]拿开,则元素类型为int*答案:p4是一个int指针的数组(10元素),符号化描述即p4int* [10]类型。

int *func0(int);中的func0先与括号结合,并且括号内仅是形参类型,说明func0是一个函数,返回值类型为int*答案:f0是函数(形参为int, 返回值为int指针)

int func1(int * p); 答案:func1是 函数(形参为int指针, 返回值为int)

int func2(int arr[]);中,留意int arr[]的写法,仅在函数中才可以这样写,是因为编译器将arr判定为指针类型,即和int * arr的写法是等价的。 答案:func2是 函数(形参为int指针, 返回值为int)

黄金(答对7题升至该段位,正确率70%)

请用文字描述下列函数的具体类型。而对于指针,请描述其可读写的情况(可以代码描述):

int func3(int arr[10]);
int func4(int *arr[10]);
int func5(int(*arr)[10]);
int func6(int arr[10][10]);
int func7(int arr[][10]);
int func8(int **arr);

const int * p5;
int const * p6;
int * const p7;
const int * const p8;       

警告: 到这一步如果你对这些指针已经有所不适的话,建议提前离开,以免你产生了放弃C/C++语言的想法。如果你硬要坚持的话。。。想好后就可以往下翻看答案。








































黄金题解

int func3(int arr[10]); 你以为这里int arr[10]就觉得这个函数的形参是一个int[10]那么简单么?那就错了。事实上这里的arr仍然是int *类型!你要想,如果将一个数组按值传递的话就以为着需要拷贝一份数组给该函数用,10个就算了,那int arr[1000000000]呢,一次copy就可以享受爆栈的快乐了。因此这里编译器会将其视作int *类型,并无视掉后面的10,实际上就是将指针按值传递,这样做可以节省大量内存,但多了一层间接性与越界风险(收益远大于风险)。这里的10实际上也仅仅是要提醒作为开发者的你,传入的数组(or指针)必须保证其地址后面sizeof(int) * 10字节都要能够访问。你可以传入元素个数大于等于10的数组,至于小于10的话...后果自负。答案:func3是 函数(形参为int指针, 返回值为int)

int func4(int *arr[10]); 这道题也好说了,即arr实际上是int **类型,而作为开发者的你,需要保证传入一个元素个数大于等于10的int指针数组。答案:func4是 函数(形参为int二级指针, 返回值为int)

准则1:函数形参中所谓的数组实际上都是指针类型

int func5(int(*arr)[10]); 注意arr本身又不是一个数组,而是指针!一个指向数组的指针! 答案:func5是 函数(形参为指向[int数组(10元素)]的指针, 返回值为int)

int func6(int arr[10][10]); 你以为arrint**吗?那就又错了。如果退化成int**类型的话,那么对于传入的指针做类似arr[3][5]的操作是十分危险的。通常int**用于指向两个维度都是动态分配的二维数组(一个动态的指针数组,每个指针是一个动态数组),即把第一行的元素都当做int*而不是int来看待。把一个二维数组强制变成变成int**,再解除一次引用就会引起野指针的危险操作。因此实际上编译器只会对第一维度的[10]当做*来处理,即等价于int func6(int (*arr)[10]);答案:func6是 函数(形参为指向[int数组(10元素)]的指针, 返回值为int)

准则2:对于函数形参中的多维数组,只会将第一维度作为指针处理

int func7(int arr[][10]); 和上一题等价。答案:func7是 函数(形参为指向[int数组(10元素)]的指针, 返回值为int)

int func8(int **); 这里只接受两个维度都是动态分配的二维数组(即int指针数组)。 答案:func8是 函数(形参为int二级指针, 返回值为int)

const int * p5; 《C++ Primer》称其为顶层const,即指向常量的指针,其所指数据不可修改,但指针本身可以替换,例:

p5 = NULL;  // 正确!
*p5 = 5;    // 错误!

而像const int num = 5这种也是顶层const

int const * p6;p5等价。

int * const p7; 《C++ Primer》称其为底层const,即指针本身为常量,其所指数据可以修改,但指针本身不可以替换,例:

p5 = NULL;  // 错误!
*p5 = 5;    // 正确!

const int * const p8; 包含了顶层与底层const,这样所指和数据与指针本身都不可以修改。

钻石(答对6题升至该段位,正确率75%)

请用文字描述下列指针、函数、函数指针的具体类型:

int (*pfunc1)(int);
int (*pfunc2[10])(int);
int (*(*pfunc3)[10])(int);
int func9(int (*pf)(int, int), int);

const int ** p9;
int * const * p10;
int ** const p11;
int * const * const p12;

实用性正在逐步降低中...








































钻石题解

int (*pfunc1)(int); 答案:pfunc1是 函数(形参为int, 返回值为int)的指针,符号化描述即int(*)(int)

int (*pfunc2[10])(int); f2先与[10]结合,说明f2是一个数组,把f2[10]拿开,则元素类型为int(*)(int)答案:pfunc2是 函数(形参为int, 返回值为int)的指针数组(10元素)

int (*(*pfunc3)[10])(int); 函数没法作为数组的元素,但函数指针可以。经过前面的磨难,应该可以看出来这是一个指向数组的指针,数组的元素是函数指针。 答案:pfunc3是 指向[函数(形参为int, 返回值为int)的指针数组(10元素)]的指针

int func9(int (*pf)(int, int), int); 一个函数里面需要接受一个函数指针作为形参,通常将以这种方式传递的函数叫做回调函数答案:func9是 函数(形参为{函数(形参为{int, int}, 返回值为int)的指针, int}, 返回值为int)

const int ** p9; 具体可以参考下面的示范:

p9 = NULL;  // 正确!
*p9 = NULL; // 正确!
**p9 = 5;   // 错误!

int * const * p10; 具体可以参考下面的示范:

p10 = NULL;     // 正确!
*p10 = NULL;    // 错误!
**p10 = 5;      // 正确!

int ** const p11; 具体可以参考下面的示范:

p11 = NULL;     // 错误!
*p11 = NULL;    // 正确!
**p11 = 5;      // 正确!

int * const * const p12; 具体可以参考下面的示范:

p12 = NULL;     // 错误!
*p12 = NULL;    // 错误!
**p12 = 5;      // 正确!

大师(答对5题升至该段位,正确率62.5%)

如果你有幸能够坚持到这一步,或者已经放弃治疗想看看后续内容,那么接下来你将要面对的可能是各种匪夷所思的、恶魔般指针,这些奇奇怪怪的写法甚至能够通过编译,简直就是恶魔。

现在允许你使用一种伪lambda的描述方式,来对函数或函数指针进行拆解。示例如下:

int (*pfunc1)(int);     // (*pfunc1)(int)->int
int f1(int);            // f1(int)->int

箭头所指的为返回值类型。

那么。。。祝你好运,请用伪lambda描述方式拆解下面函数和函数指针:

int (*pfunc4)(int*());
int (*func10(int[]))[10];
int (*func11(int[], int))(int, int);
int (*(*pfunc5)(int))(int[10], int);

int (*(*pfunc6)(int[10]))[10];
int (*(*pfunc7[10])(int[10]))[10];
int (*pfunc8(int(*(int(*())))));
int (*(*(*pfunc9)[10])(int[], int))(int, int);








































大师题解

int (*pfunc4)(int*()); 基本上都倒在了形参的int*()这种什么鬼写法是吧,不然这怎么能叫恶魔指针呢,哈哈哈... 反正在这篇文章里,就让可读性就统统见鬼去吧!如果你有Visual Studio的话,把这份声明粘贴到VS,然后光标放在上面,你会发现实际上形参的int*()会被解读为int*(*)()答案:(*pfunc4)((*pf)()->int*)->int

int (*func10(int[]))[10]; 这个在《C++ Primer》上似曾相识,如果你之前在里面做过类似的题目话,就会知道这个函数,返回的是一个指向数组的指针。你可以将该函数类似于函数调用的部分func10(int*)拿掉,剩下的就是返回值类型int(*)[10]了。 答案:func10(int*)->int(*)[10]

int (*func11(int[], int))(int, int); 函数返回了一个函数指针。 答案:func11(int*, int)->int(*)(int, int)

int (*(*pfunc5)(int))(int[10], int); 函数指针,所指函数返回了一个函数指针。 答案:(*pfunc5)(int)->((*)(int*, int)->int)

int (*(*pfunc6)(int[10]))[10]; 答案:(*pfunc6)(int*)->int(*)[10]

int (*(*pfunc7[10])(int[10]))[10]; 答案:(*pfunc7[10])(int*)->int(*)[10]

int (*pfunc8(int(*(int(*()))))); 这又是什么鬼玩意???我们先根据现有的经验来进行层层解耦。首先像这种int(*())的外层括号是可以去掉的,只是一个误导,然后就变成了int*()的鬼形式,然后编译器会认为它是int*(*)()。那答案也就呼之欲出了。 答案:(*pfunc8)((*pf1)((*pf2)()->int*)->int*)->int*

int (*(*(*pfunc9)[10])(int[], int))(int, int); 答案:((*pfunc9)[10])(int*, int)->((*pf)(int, int)->int)

结语

如果你能完成上面的所有题目,那么你将获得隐藏称号:人形编译器。

这里的指针几乎就是你这辈子能见到的所有指针了。至于其余变种指针,基本上都围绕这上面提到的方法构成。毕竟我们还没加上C++的引用呢...

坑挖的太大也难免会有一些错漏,欢迎指正。

现在,我们都是恶魔了

12-02 11:56