前言
相信大多数的同学都是第一门能接触到语言是 C/C++, 其中的指针也是比较让人头疼的部分了, 因为光是指针都能专门出一本叫《C 和指针》的书籍, 足见指针的强大. 但如果不慎误用指针, 这些指针很大可能就会像恶魔一样把你的程序给直接搞崩溃.
3 个月前, 我编写了一份这些指针都是恶魔吗?.c 的文件, 里面从大多数常用的指针类型, 一路延伸到纯粹只是在窥探编译器所能产生的恐怖造物, 为了增加趣味性, 我还把这些指针都划分了段位, 只有辨识出该段位绝大多数的指针才能升段. 目前看到的同学基本上都分布在青铜到黄金的水平. 现在我要将这些恶魔般的指针公诸于世, 欢迎大家前来接受挑战自虐.
前置声明:
题目会包括数组, 指针, 函数, 以及它们的各种谜之复合体;
本文后面提及的一些指针不考虑什么实用性, 就当做是玩个游戏, 但适当情况下会对这些指针做必要讲解;
如果你对指针开始产生不适, 恐惧感, 建议你提前离开, 以免伤到你对 C 语言的热情;
你想从这些指针里面挑一道作为自己的题目? 随你喜欢.
这些指针都是恶魔吗?
青铜(答对所有题升至该段位, 正确率 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 元素)] 的指针, 符号化描述即 p3 是 int(*)[10]类型.
int *p4[10]; 中的 p4 考虑到优先级, 会先与 [] 先结合, 而不是 *, 说明 p4 是一个含 10 元素的数组, 然后把 p4[10]拿开, 则元素类型为 int*. 答案: p4 是一个 int 指针的数组 (10 元素), 符号化描述即 p4 是 int* [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]); 你以为 arr 是 int** 吗? 那就又错了. 如果退化成 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++ 的引用呢...
欢迎在评论里面留下自己的段位证明(请诚实对待). 坑挖的太大也难免会有一些错漏, 欢迎指正.
现在, 我们都是恶魔了
来源: https://www.cnblogs.com/X-Jun/p/10051247.html