C语言指针的小故事
文章来自:https://www.zybuluo.com/FadeTrack/note/160734
C与指针
楔子
初学者常闻到:指针乃C的精华。
那么指针究竟妙在哪里?如何去理解指针这个存在有些迫在眉睫,包括学习中都经常发现很多人以为自己真的懂指针,实际上你对指针理解多少呢?
开篇 从简单的开始
指针,你是如何定义这个名词的存在呢?
指针是一个变量,什么变量,指向一个内存地址的变量。
什么是内存地址?这是个深奥的概念,如果说硬是要扯起来,要涉及的东西比较多。
谭浩强的 《C语言程序设计》中有这样一个例子(我记得是)。
我们把计算机比作一个旅馆,内存地址就是这个旅馆中的房间的门牌号,而指针则是一把唯一的能打开相应房门的钥匙
(这是个有Bug的比喻,请看下面的 戏说多级指针————奇怪的房间问题 解释)。
这个解释的话对于普通的指针解释似乎是行的通的。
int nNum = 0;
int* pNum = &nNum;
那么 nNum
就是一个房间了,而 pNum
就是对应的钥匙,我们要打开房间只需要
简单的通过钥匙就能实现, 而在 C 语言中这种实现是 *pNum
。
那么我们就可以尝试一下了。
int nNum = 0;
int* pNum = &nNum;
if (*pNum == nNum)
{
printf("True");
}
else
{
print("False");
}
房间的大小(指针的区别到底在哪?)
不得不说的变量类型
接下来我们开始考虑一个问题,房间的大小问题,我们知道旅馆的房间有大有小,有圆有方(应该没有圆的),总而言之就是千奇百怪。
我们的变量也是如此,但是事实上并不是这样的哦。
就目前来说,在 x86 环境下,笔者见过的变量类型实质上只有这么几种。
1 字节 ==> 简单的举例: char
2 字节 ==> 简单的举例: short int
4 字节 ==> 简单的举例: int,long (x86 下所有的指针都是 4 字节)
8 字节 ==> 简单的举例: double
有人或许会说,C语言不是还有 bool,额,有必要解释一下。
在 C99 中加入了 一个 _Bool
,这个是目前在 VS2013 上可以直接使用的 C 语言 布尔型关键字。
还有一点就是 byte
并不是关键字。
指针从哪里来
通常我们是这样声明一个指针变量的
在一个基本的变量类型后面加上一个 *
例如:
char* pChar;
int* pInteger;
float* pFloat;
通常我们是这样取出一个变量的地址的
在变量前面加上取地址符 &
int nNum;
int* pNum = &nNum;
联想到的事情---把钥匙变成房间
由于指针本身是一个 4字节的变量,那么我们是不是能用任意一个4字节变量来保存地址呢?
也就是能不能把房间当做钥匙呢?
int nNum;
int npNum = &nNum; // 这里把一个 int 类型当做指针用所以变量命名为 npNum
我们写下如上的代码,测试之。发现编译器报出了一个警告,但是依旧可以编译通过。
警告 1 warning C4047: “初始化”:“int”与“int *”的间接级别不同
根据警告的提示 再修改代码如下:
int nNum;
int npNum = (int)&nNum; // 这里把一个 int 类型当做指针用所以变量命名为 npNum
警告赫然消失了,我们试图输出一下.
printf("0x%X",npNum);
我们再试一下这把新的钥匙能不能开门:
printf("%d",*npNum);
编译器给了我们一个脸色呢:
错误 1 error C2100: 非法的间接寻址
好吧我们得到了一把没办法开门的“钥匙”,接下来我们讨论一下这把“钥匙”为什么不能开门。
你知道么?房间是没办法打开房间的。
由于我们上面已经试验了,房间是打不开房间的,所以我们需要把房间变回钥匙再去开门。
我们来变一下:
void Demo_02()
{
int nNum;
int npNum = (int)&nNum;
printf("%d\n", *(int*)npNum);
}
结果告诉我们,成功的打开了一扇新世界的大门,但是因为我们房间里面还没清理,所以一篇乱七八糟。
void Demo_02()
{
int nNum = 1; // 整理一下房间
int npNum = (int)&nNum;
printf("%d\n", *(int*)npNum);
}
我们可以看到整理干净之后的房间里面住了 1
先生
1
请按任意键继续. . .
糟糕了,256
先生不见了
有一天,旅馆的管理员(我们)不小心把房间变成钥匙的过程搞错了。
void Demo_02()
{
int nNum = 256; // 整理一下房间
int npNum = (int)&nNum;
int nSir256 = *(char*)npNum; // 钥匙错了
printf("%d\n", nSir256);
}
粗心的管理员把 原本是 int
的房间的钥匙给铸成了 char*
了。
打开门之后,我们发现 房间干干净净, 256
先生好像从来没有来过一样。
我们来看一张 int
房间内部的分布图[1]:
| 厨房 | 卫生间| 厕所| 阳台 | 隔壁房间 ....
| 0x00 | 0x01 | 0x00 | 0x00 | 老王.....
原来如此,我们因为钥匙铸错了,所以房间只剩下了一个厨房了。256
先生正在上厕所呢。 :)
我们设想 如果有一天,我们马虎的把 一个 char
房间的钥匙给铸成了一个 int*
,隔壁的老王肯定不会饶了你,给你一个大大的脸色。
新的尝试
我们试着一开始就用上一些奇怪的钥匙。
void Demo_02()
{
int nNum = 256; // 整理一下房间
char* cpNum = (char*)&nNum;
int nSirc = *ncNum;
printf("%d\n", nSirc);
}
果然,结果和我们猜想的一样。那么指针到底是个什么呢?
结论: 指针(钥匙)决定了房间的大小。明天去买一个豪宅(大房间)的钥匙吧。:)
注意: 如果你的房间很小却配了一把豪宅(大房间)钥匙的话,隔壁老王说不定会来找你麻烦。
进阶的修罗场
说实在的,这个东西不怎么好比喻,接下来就是修罗场了。
混乱的运算符优先级
先热热身,指出下面的指针分别代表什么[2]:
1. int* nNum[10];
2. int (*nNum)[10];
3. int (*nNum)(int);
4. int (*nNum[10])(int);
5. int *nNumA, **nNumB;
6. char str[];
7. char* strA, **strB;
:) 你能得到多少分?
:( 如果有很多做不出来,记得好好理解。
- 指针数组 (一个元素个数为10的数组,每一个元素都是一个 int* 的指针)
- 一个指向数组名的指针(实际上数组名本身就是一个指针,这里实际上就是一个 int**)
- 一个函数指针,指向函数名 ( 故意写的 nNum 来混淆视线)
- 一个函数指针数组( 函数指针数组 就是说数组的每一个元素都是 函数指针)
- 一个 int* 指针 和一个 二级指针 (int**)
- 一个 char* 指针
- 一个 char* 指针 和一个 二级指针(char**)
到这里 梳理一下指针运算的几个运算符优先级:
() > [] > *
那么记不住怎么办?没办法,但是如果使用中不记得话 就记得打上括号吧! :) 我一直是这么做的。
int (*Func[10])(int);
int (*(Func[10]))(int);
先说一下这样写的原因, 首先函数指针数组 是一个数组,其次他才是一个 什么类型的数组。
好比说
int nNum[10];
int (nNum[10]);
虽然这样臃肿难看,但是如果记不住优先级的话,请一定不要吝啬你的()
.
混乱的指针长度
void Demo_04()
{
int* pNum;
int nNumArry[10];
int* pNumC = nNumArry;
printf("%d---%d---%d", sizeof(pNum), sizeof(nNumArry), sizeof(pNumC));
}
结果:
4 --- 40 ---- 4
结论:
数组名虽然也是一个地址,甚至可以直接给指针赋值。
但是 数组名不等于指针,这是一个陷阱。
strlen 和 szieof
void Demo_05()
{
// Hello,World 经常被大牛耻笑,虽然这种事情完全没有根据
char szA[20] = "No Hello,World!" ;
char* szB = "No Hello,World!";
printf("A=>sizeof: %d -- strlen: %d\n", sizeof(szA), strlen(szA));
printf("B=>sizeof: %d -- strlen: %d\n", sizeof(szB), strlen(szB));
}
结果:
A=>sizeof: 20 -- strlen: 15
B=>sizeof: 4 -- strlen: 15
二维数组
int nNum[2][3] = {1, 2, 3, 4, 5, 6};
nNum 是什么?
nNum + 1 是什么?
*nNum + 1 是什么?
void Demo_06()
{
int nNum[2][3] = { {1, 2, 3}, {4, 5, 6 }};
printf("%d --- %d --- %d", **nNum, **(nNum + 1), *(*nNum + 1));
}
结果:
1 --- 4 --- 2
结论:
nNum 他的类型并不是一个 int*,而是 int[3],他每加 1 就会后移 3个元素
*nNum 才是一个 int*
&nNum 就是一个 int[2][3] 的类型了,他每加 1 就会后移 6个元素
备注:
如果硬是要说 数组即指针的话,那二维数组请当做二级指针来理解,但是请不要搞混了指针到底是什么类型的。
戏说多级指针————奇怪的房间问题
如何这里再沿用初试的那个比喻就会出现奇怪的问题了。
int** ppNum;
int nNumArry[10];
ppNum = &nNumArry;
于是乎,我们最初的比喻出现了问题了。
如果指针是钥匙,那么二级指针就是放在 钥匙里面的钥匙咯。:)
真的是一个奇怪的说法。
为了消除掉这个 Bug ,我又要开始自圆其说了
想了很久,最后决定这样解释比较好一点。
int**
是一个“假房间”的钥匙。
假房间就是说我明明是拿钥匙要打开房间的,结果打开了之后发现房间变成了一个钥匙。
(不要关心是怎么打开了)
反正你打开了,结果是你没有得到一个房间而是得到一把新的钥匙int *
。
如果再用这把新的钥匙去打开房间 我们就能得到一个真正的房间 int
。
二级指针就是 挂羊头卖狗肉的假房间
。 :)
多级指针的意义[指针的意义]
我觉得我说的这种意义可能有些以篇概全的味道,可能不是很好。
如果说 有一天你要去一个地方旅游,海关告诉你,要去旅游可以,但是有一个有趣的游戏规则。
如果你带了什么过去的话,其实并不会被你真的带过去,有一个叫做系统的家伙会在你要去的地方给你弄个假的东西,和原来一模一样的。
(求求你们放过我吧,不要说什么浅拷贝逻辑不对问题了,不要在意那些细节)。
这样就有了一个问题了,有一天我要回来了,我的房间里面的东西都带不回来了,它会把你以前的房间给你。 TAT..
为了解决这个问题,于是我想到了一个好办法,我不带房间了,我带一把钥匙过去。
这个叫系统的坏家伙就把我的钥匙给复制了一份,但是这并不影响我打开房间。
(不要问房间怎么打开,这是任意门的钥匙,而任意门遍布世界各地)。。(我编不下去了)
于是我回来的时候,我的房间里面的都装回来了
(房间君: 我就没动过好吧,你那是任意门的钥匙(づ ̄3 ̄)づ╭❤~)
虽然钥匙换了几遍,但是它不影响我打开这个门。
接下来是二级指针:
有一天,我突发奇想要把我的钥匙带到另一个地方去做个美容。
(钥匙君:我才不要什么美容)。
于是我又想到了新的方法,
把我的钥匙变成一个假房间,然后我带着这个假房间的钥匙过去
过去之后我打开了这个假房间,于是我得到了之前的那把钥匙。
做完美容之后。我又把这个钥匙变成假房间。
于是系统这个家伙再也没办法对付我的钥匙了。
(钥匙君:我才不要变来变去)
备注:
钥匙 指针
假房间 多级指针
旅游 函数调用
新地方 函数内部
最后说一下引用是什么?
引用就是 我有个叫做 &的兄弟在给系统打工,我把房间给他,他就会帮我干上面的那活。