理解c语言的指针
指针对于初学者往往是很难以琢磨的东西,因为它并不如变量那么抽象,而是更贴近底层的真实结构。指针操作往往会出现各种各样的岔子,我觉得都是由于对指针理解不深,使用不熟练造成的。以下是我学习c语言一年半来对指针的一些理解,献给和我一样的c语言的初学者。
学校的老师往往会告诉你指针本来就是个很难学的东西,但是它很重要所以比一定要记住:“这个叫指针数组”“这个叫数组指针”“这个叫双重指针”“指针就是数组首地址,这是考试必考内容”,然后考试的时候拿各种数组和指针来为难你,从中挑选出“优秀”的学生。其实指针并不是很难理解的东西,只要抓住一点本质,一切都顺理成章:
指针就是数组下标
这里,不去想什么内存的物理结构,什么栈,就把内存想想成一个巨大的、线性的、连续的存储空间,或者说,就是一个数组,内存就像这样
memory[4G]
这里的4G假设你有个容量为4G的内存。接着,你的所有变量需要存到一个位置,比如,我需要一个int型的变量,于是,我这么想:“在数组里取4个小格子让我存一个整数”,我把我的想法告诉了内存,内存说:“好,258号格子带上它后面的3个就给你吧”,于是,我记住了这个数字258,这是我放整数的地方。在程序中,人们这么写
int *n = (int*)malloc(4);
如果你对这条语句感到陌生不用管它是什么,反正现在n的值就是258了。而实际上,当调试的时候,我们总看到指针的值是一些奇怪的形式,像0xff2dfff的十六进制数,这也总给初学者对指针留下一种神秘感。可以这么想,4G的内存太大了,如果我们用十进制存储势太长了,所以用十六进制压缩一下(实际并不全是因为这个,不过这么理解并没有坏处),而实际上n里面存储的就是一个整数,注意我说的是n而不是*n。可能这里又有一些混乱了,我们再来重复一下刚才所说的话,并坚定的相信
指针就是数组下标
如果你有点不明白我在说什么了,没有关系,请继续往下看;如果你清楚,也不要嫌我罗嗦,因为理解这句话真的很有必要。现在我们继续刚才的操作,内存给了我258号到262号格子让我存储数据。我很高兴的往里面放了一个整数,1
memory[n] = 1
现在明白了么?n就是一个下标,而内存就是一个数组,我用memory[n]来往内存的格子里面存值。而在实际的代码中,并没有这么一个数组memory[]
,对于内存这么一个特殊的数组,需要一种特殊的写法,上面的语句会被写成
*n = 1;
这两句话是同一个意思,就是往内存的n号格子里放一个1.
可能有人注意到了,内存不是给了我4个格子么,而memory[n]
只能代表一个格子呀。是的,所以对于内存数组来说,它下标得不仅能表示位置,还能表示大小。因此,一个内存数组下标(指针)是这么声明的
int *n;
称作int型的指针,这里的int就代表了这个位置n必然会占用一个int的大小,即4个格子。
所以前面的话我也许可以这么扩充:
指针是带大小的数组下标
好了,如果你能看到我唧唧歪歪地说到这里,说明你是真的对指针很有兴趣,并有强烈的搞懂它的欲望。我前面讲了如何来理解指针,现在看看怎么用它。
我们都知道当写了一句int a = 1;
的时候,内存中一定有一个地方来存储这个a
.既然存在内存里就一定有内存地址,即前面说的xxx号格子,或者叫内存数组的下标,或者叫指针。
我问内存“这个a里的1存在哪里呀?”,内存说“它在以8495号开头的四个格子里”,这里我通过变量名能知道变量的位置,然后这个位置用特殊的数据类型存起来。
int *n = &a;
不再赘述,&就是取地址的意思,就是问内存“a在哪里?”。
这个时候n的值是8495,*n的值是1.
还有一件事,声明指针和使用指针的时候使用的 ‘*’ 的含义是一样的。当我使用int *n;
的时候,‘*’ 的意思是“n是一个特殊的整数,我要用它存内存的地址”。而当我使用*n = 1
的时候,’*’的意思是“1是存在n号格子里,而不是把n赋值为1”。
这里*n的过程——从格子号来访问并修改数据,仿佛是我要去拜访住在943大街284房间的xxx,一个指路人给我指路一样。我们形象的比喻这个地址为“指针”。
在我要讲更加复杂的事情之前,我们来回忆一下刚才都说了什么
- 指针就是数组下标
- 指针是一个整数(这是很多同学一直过不去的坎)
- int代表了以指针为起点,后面4个格子都是我的
其它的int*
,*n
, &a
这样的表象并不重要。比如我编程的时候,手上打着*n
,脑子里想的却是memory[n]
。
现在我来问一个问题:
既然指针就是一个整数,我能不能这么做?
int n = &a;
反正n里存的是格子号呀?
-------------------------------------------思考分界线---------------------------------------------
如果你能不假思索的回答不能,并解释为什么,说明我前面写的东西你已经理解了。
之所以要用特殊的数据类型来存储这个整数,是因为它包含了两个信息——位置和大小。用int来存储位置本身不是不可以,只是如果真的这么声明,但我要往n号格子里放东西的时候,我并不知道n后面几个格子是我的。
先让我们来想一些有趣的事。让我觉得有趣的事无非就是就是学校老师是怎么讲指针的……他会告诉你,你这么写
int a = 1;
int *n = &a;
然后a就是1,*n 也是1,你可以愉快的用*n来代表a了,这叫“间接引用”。如果你再写
int *n = 2;
printf(“%d\n”,a);
屏幕上会打印出“2”来。然后台下的同学们都“嗯嗯~”的点着头深表同意。
也许你们学到这里会自然而然地有个疑问,“我为什么要用指针?”。恐怕老师告诉你“因为我要考!”
呵呵,一个玩笑。现在我们来看一个函数
void swap(int a,int b)
{
int c = a;
a = b;
b = c;
}
这个函数很简单,抛去语法规则,也许高中你都能看懂,它的功能是交换a,b的值。但当你真的测试运行这个函数的时候,就会发现它并不能达到我们的目的,运行了swap(a,b);
之后,a,b的值都没有变。
这是为什么?因为函数会将传进来的参数值在内存中复制一份。比如说,你有两个变量,a = 1,b = 2,这个时候a存在1~4号格子,b存在5~8号格子。你调用了swap(a,b)以后,内存会在某处复制一个新的a,b,比如说,用300~303号存新a,304~307号存新b,然后在把新的a和b交换了。回来之后,你看1~4号和5~8号,当然没有任何变化。
这真是一个失败的特性。也许你会这么想。但是也不是没有办法,函数不是有返回值么?交换完之后把a和b返回回来就行了呗、你可能希望有这样的语句
(a,b) = swap(a,b);
然后这个函数能写成
(int,int) swap(int a,int b)
{
int c = a;
a = b;
b = c;
return (a,b);
}
聪明的你会想到更简单的写法
(int,int) swap(int a,int b)
{
return (b,a);
}
那干脆干嘛不(a,b) = (b,a);呢?
不能这么做,因为c语言中只有单返回值。
自然的,就产生了直接在内存上进行操作的想法。既然我的a,b是存在1~4和5~8的,那我干嘛不直接去这些格子里交换呢?
于是我把a和b的位置(和大小)告诉了swap()
,swap()
要能听懂就得这么声明
void swap(int *pa,int *pb);
然后呢?swap()
通过地址找到a,b真正存储的位置,亲手交换了两个值。
void swap(int *pa,int *pb)
{
int c = *pa;
*pa = *pb;
*pb = c;
}
你可以翻回到第一个swap()
函数,看看发生了哪些改变。
这样使用这个函数
swap(&a,&b);
一切都是在memory[]
上进行的,没有人再去复制一边a和b了。
现在你应该明白了指针的一点点作用了。其实如果你阅读过google的c++编程规范,会记得其中会把函数参数分为输入值,输出值和输入兼输出值。这里的a和b就可以成为输入兼输出值。这里讲一点点这篇文档中提到的编程规范。它建议函数参数列表的顺序设置为输入值,输出值,最后是输入兼输出值,这样,如果一个函数有很多参数,调用函数的时候将对应参数的位置写错的机会会小一些。
如果这么理解函数参数列表,那么函数的返回值便显得不是那么有意义,它经常是作为函数调用成功与否的标志而已。
好,现在我的问题又来了。这次的问题同样很简单:
我们在打印的时候通常调用print(),扫描用户输入的时候调用scanf()
为什么printf(“%d”,a);
的a
没有&
,而scanf(“%d”,&a);
要加一个&
呢?
-------------------------------------------思考分界线---------------------------------------------
同样,我希望你能不假思索的答出来。如果你想了很久也想不出来,我建议你把上一个问题之后的东西再回顾一下。不要着急着看答案,养成独立思考的习惯。
如果你觉得很简单,也请你静下心来看完我写的答案,因为也许你疏忽了一些细节。printf()
函数将a的值显示到了屏幕上,并没有对a做任何修改,在这里a是作为“只读”属性。为了保证严格的“只读”属性,有时候我们会在参数的数据类型前加一个const
以保证数据在函数中不会发生变动。而scanf()
是将用户输入值保存在a中,a的值在函数运行过程中需要发生改变,所以需要传入a的地址让函数好在a的位置上直接操作,这里的a作为函数输出值。而其实有时候我们可以通过函数返回值来获得输出,getchar()
就是这么做的。
不知道对于指针作为一个下标同时也是一个指路人这样的等价转换你是否足够熟练。下面我们要更进一步,我们会把指针理解为它的名字所隐喻的那样——就是一个箭头,指向内存的某个地址(其实你也可以认为数组下标就是一个指向数组中某值的箭头)。
接着,我们来看数组。如果内存是个大数组,那么我们在代码中所声明的数组其实是内存数组的一个连续的一块。这个时候,你是否发现了数组与指针有着异曲同工之妙?它们都有一个起点和大小来定义,表示一个或数个格子。
你会发现数组只要给定一个起点和一个长度就能唯一确定。而一个数组在声明的时候既然已经唯一确定,一定给了足够的信息
char a[4];
自然而然地,我们会想到长度就是4个sizeof(char)
,那么起点便是a了。这么看来,数组名作为数组的起点是理所当然的事情,它是一个指针。
我希望这样的解释能给你没们“Ah-ha”之感,而不是上课时听到老师说“这里是考试重点”,然后忙着在书上勾勾划划。
那么它是一个什么样的指针呢?
看这两个声明,a是一个数组,它包含了4个格子,n是一个指针,它指向四个字节。
char a[4];
int *n;
a指针和n指针能等价么?*a是什么含义呢?
这里需要区分的是,*n 表示一个int值,内存去n位置找4个格子,而a是数组的首地址,*a表示一个char
,自然是a[0],只有一个格子;其次,a + 1 指针会向后移动1,而n + 1指针会移动4格(注意a ++这样的语句是不允许的,因为a虽然存着值,但它不是变量)。
归根结底,因为a是一个char
型指针,而n是一个int
型指针。所以这里将a看作指针似乎忽略了一些东西,那就是数组的长度。这也是为什么指针不能完全看作是数组的原因。当我声明了4个char
的数组时,这个指针a并不指向整个数组,只是数组的起点,这么理解会更妥当:
指针是不知道长度的数组
指针与数组很相似却不是完全等同(这个关系对后面的知识非常重要)。指针指向一个地址,作为数组的头,数组只是在这个头的基础上加一个长度而已。这个头不仅可以是char
型,也可以是int
型等等。
所以,数组就像是一辆长长的火车,数组名对应的指针是车头。数组的每一个元素相当与车厢,只是有些火车的车厢大一些,相当于四个格子的容量,有些小一些,相当于一个格子。而从车头无法看出关于火车长度的任何信息。所以,如果你用指针表示的一个数组的时候,往往需要一个int length;
来记录起长度。而数组所占的空间可以直接用sizeof(数组名)
来获得,sizeof(指针名)
只能知道这个下标整数所占空间。
现在你应该能比较好的理解数组与指针的关系了。
现在我们来看看一件有趣的事情,我们知道了a是char
指针,指向火车头,那么*a自然是a[0],*(a + 1)也就是a[1],那2[a]是多少呢?
这不是一道思考题,这是一个c语言中不常用的小知识。让我们抛开程序,回到数学世界。现在我定义两种等价的运算 * 和 [],*(a + n) = a[n],其中 + 表示普通加法。那么有加法交换律,便有*(a + n) = *(n + a) = n[a]了。于是我们看2[a] = *(2 + a) = *(a + 2) = a[2],所以,2[a]就是a[2]啦!有兴趣可以自己试试哦。
好了回到正道。其实指向整个数组的指针是真实存在的,只是声明的时候你要这么写
char (*a)[4];
这个时候,a和前面的n便有了更加相似的性质,比如a + 1会和n + 1一样后移4格。这个就叫指针数组,他是一个神奇的数组,至于为什么这么写会有这样神奇的特性,因为它是一个双重指针。一瞬间出现好多奇怪的名词和性质。现在先别想太多,后面我会讲到。
现在我们再考虑一类有趣的数组,它能丰富你对指针作用的看法。
指针是一个没有记录长度的数组,那么如果一个数组里全存的是指针呢?是不是相当于一个拉直的绳子上悬挂着一串串珍珠链呢?每一个悬挂点都看作是这个数组的元素,而每一个元素都能引申出另一个数组来。是不是有一点像二维数组?
如果你觉得你开始对世界有了新的认识,我们不忙继续往下走,先回到旧认识来,来看看字符串。
字符串是一类特殊的数组,为什么特殊?很多同学可能都简单的把它理解为一个char型的数组,是的,它是char型数组。但是从指针的角度,我觉得它在数组中非常与众不同。
char *a = “Hello,world.”;
我更愿意这样声明一个字符串,而不是
char a[] = “Hello,world.”;
因为前者能体现出字符串的本质。a是字符串的起点,而作为一个数组,我并不关心它的长度,因为字符串用终点来标记长度。终点就是’\0’,就是我们常说的“杠零”。所以字符串很好的特点是,有一个起点就够了。这里插一句,“杠零”准确的说应该这么写“-0”,而’\’应该念“反斜杠”,所以叫“反斜杠零”。
那么,如果我把很多很多这样的起点存在一个数组里,会有什么效果?
char *a[999];
一个珍珠帘。而且每条链还长短不一,参差不齐。
而想想二维数组是什么?是张布帘,一定是一个长方形。
这就是为什么我们在存一些字符串时喜欢这么写:
char *a[]={
“Sunday”,”Monday”,”Tuesday”,”Wednesday”,”Thursday”,”Friday”,”Saturday”
}
否则我们得开一个a[7][10]
的二维数组,这样的数组让人觉得臃肿笨拙。
可是,你有没有想过,”Sunday”为什么能像一个指针一样存进*a[]
作为一个元素呢?
如果你想知道为什么,我强烈建议你立即停下来,打开你的编辑器或者IDE,设计一个实验看看“Sunday”到底是什么东西。
其实我的着重已经提示了你,但是我依然希望你有一种探索精神和对新知识的好奇心。
这里我可以告诉你,“Sunday”
是一个指针,并且在任何使用”Sunday”
的地方都是同一个值,指向内存的同一个位置。你可以试试”Sunday”[1]
看看会是什么,也不妨试试1[“Sunday”]
;D
现在问题来了:
有一些编辑器或IDE会自动生成main()
函数,而且会有这样的
int main(int argc,int *argv[])
请问这两个参数为什么这么写?有什么用?
-------------------------------------------思考分界线---------------------------------------------
如果你没有使用过命令行可能会一头雾水,使用过的同学应该对很多程序的“参数”并不陌生。如果你至今没有使用过命令行来编写、编译、链接、运行一个程序,我建议你至少体验一回这个过程。它能让你明白每次你用vc或vs点了ctrl+F5之后,IDE都做了什么。或者说微软那些优秀的程序员为我们这些菜鸟们做了什么。
比如使用gcc编译c代码,最简单的
$ gcc filename.c
有时候命令会很长
$ gcc -g filename.c -o name -lpthread -lm -lcrypto
其中gcc是一个程序名,程序名后面跟了一长串“参数”,如果程序是用c语言写的,那么这些参数就是通过main()函数传给程序的。其中argc表示输入参数的个数(argument count),argv是每一个参数的值(argument value).这里,参数值就是以字符串的形式传入的,多个字符串存在了一个数组中一次传了进来。
这也许能让你明白“字符串数组”有什么作用,而实际上数组的每个元素不仅可以是char型的指针,也可以是int型等等,比如int *a[5];
凡是元素是一个指针的数组都叫“指针数组”,别把这个想的太复杂。元素是整数的数组你可以叫“int数组”,字符的数组可以叫“char数组”,指针的数组自然就叫“指针数组”了。但是,以这样的方式来声明一个二维的int
数组没有什么必要,反而会降低程序的易读性。毕竟以指针来标志一个数组的时候会丢失长度信息,所以如果你真的需要一个二维int数组,大多时候还是这么做的int a[5][5]
。
所以你知道了指针数组和二维数组有着异曲同工之妙。在我进一步拓展之前,我先要在大脑把这些满天飞指针收一收。
回忆一下我们都讨论了什么:
- 指针是带大小(这个大小不要和数组的长度搞混了,想想火车和火车头的例子)的下标,同时也能理解为一个箭头,指向内存某处
- 指针标记数组的起点,所以可以看作一个不知道长度的数组
- 字符串有天然的终点标记,所以有了起点就可以唯一确定
- 如果数组的元素是指针,这种数组叫“指针数组”,它与二维数组有着异曲同工之妙
好了,现在你感觉自己已经领悟了指针的真谛,可以愉快的用指针码代码了!
我要警告你的是,指针会把你的电脑搞得鸡飞狗跳。初学者(包括我)在使用指针的时候总是会出现各种各样的错误,我有一个笔记记录着我在使用指针时遇到过的疏忽和错误,不幸的是,这条笔记到现在还在不断的扩充着。
我这里举一些简单的错误。
首先,既然指针是一个整数,那么如果你声明的指针,
int *n;
n中的值没有初始化,n指向了哪里不确定的,我们习惯于波浪线的箭头表示一个不知道指向了哪里的指针,或叫“野指针”。如果你声明完之后直接引用,会出现”segmentation fault”。如果是在windows下微软的那一套体系,你会看到弹窗和不知所云的错误提示,并告诉你程序已经被关闭了。你可能会说:“那我想操作1845号格子,能不能给n赋值为1845呢?”这么看似乎是可以的。但是内存的某个格子并不是你说用就用的,在的程序操作某个新的格子之前,你需要向内存申请,以保证没有程序在占用它,并且保证你占用了格子之后别的程序不能再使用它。
int a;
这样的变量会自动向内存申请空间,不需要在程序代码中体现,而指针不一样。
你要手动申请一个空间。
int *n = (int*)malloc(sizeof(int));
向内存申请4个格子,并把这4个格子的信息(位置和大小)保存在n中。
而实际上这就是一个简单的赋值,现在n可能就是1395或者xxx的某个整数了,但是赋的值完全由内存自己把控。
然后,我们来看看什么时候会用到申请空间。
我想链表的概念应该都不陌生——一个结点包含下一个结点的位置信息,或者说指向下一个结点。一个又一个的结点构成了一条链。怎么在节点尾部接一个新的结点呢?要先找到尾部,然后新建一个结点(内存空间),让尾部的指向这个新结点。
我现在有这样一个需求:假设我已经找到尾巴指针tail,想用一个函数来创建新的结点并直接接上去,我是不是可以这么写
tail -> next = create();
如果create()函数能返回一个结点的指针,我就大功告成了。
现在我变一变,我不喜欢用返回值,或者返回值有其它的用途,它要用来表示生成节点是否成功(其实可以返回-1表示失败,但是我们这里不这么做),我要把这个结点传进去,
status = create(tail -> next);
我希望能完成同样的功能,给next后面接一个新的结点,函数要怎么写?
-------------------------------------------思考分界线---------------------------------------------
大脑中的第一反映也许是这样的
int creat(Node *node)
{
node = (Node*)malloc(sizeof(Node));
if(node != NULL)
return FAIL;
return SUCCESS;
}
试试在电脑上验证你的想法,看看有没有新的结点被挂在尾巴后面了。
如果你的程序正确,结果应该是没有。
正确的答案是,如果函数是如我问题中那样调用的,无论函数怎么写也不能达到目的。
原因也很简单,因为node里存着一个整数,你用malloc()函数给node赋了一个值,这个值并没有真正的赋给tail -> next
,而是给了它的内存中的复制。注意你在调用create()函数的时候,把tail -> next传了进去,传进去的是什么?如果你之前进过了初始化,应该是NULL
,也就是0,如果没有,谁也不知道是什么。内存把这个值复制了一份,然后放在了新的地方,接着给你一块空间,用地址修改了这个值。tail -> next是什么还是什么。
如原来一样,如果我需要修改tail -> next
的值,好让它指向一块内存空间,我需要传入一个指针,就是tail -> next
的地址。可是tail -> next
不是原本就是一个指针么?对呀,所以我们要传进去指针的指针。这样的指针叫双重指针。
在学习指针的时候你应该就会想一个问题,指针存着某个格子的编号,但这个编号存在哪里呢?肯定存在内存里呀。既然存在内存就一定有地址,这个地址就是二重指针。这里会不自觉的陷入一个循环反复的圈子里去,地址还有地址,地址还有地址,地址的地址无穷无尽……我们跳出来,从最简单的地方开始想。
我们把tail -> next
传进create()
的目的是什么,是为了让tail -> next
这个指针指向内存某处,占为己有。前面swap()
所讲的,我们传进去一个指针,是为改变指针所指的那个位置存的值,如愿以偿。现在呢?传入一个指针,为了改变这个指针所指的位置,这件事函数做不到。想想,这个位置其实也是一个整数,我们要改变这个整数,就传进去这个整数的地址。
status = create(&(tail -> next));
这个时候,你有需要把指针想成数组下标了。就是一个下标值罢了,传入这个下标值的指针!
然后,我们就在内存中直接修改这个指针吧。
int create(Node **node)
{
*node = (*Node)malloc(sizeof(Node));
if(*node == NULL)
return FAIL;
return SUCCESS;
}
这里,我用两个星号表示传进来的是指针的指针。而*node
才是函数要tail -> next
.
总结为一句话就是
函数参数是地址时可修改地址指向的值,是指针的地址时可修改指针指向的位置
不知道我这样反复变化“指针”“地址”“位置”的说法是让你“Ah-ha”了一声,还是陷入了沉思。其实这些叫法都是等价的,怎么叫都行。我希望你透彻的理解了上面的内容之后再继续往下看,因为好戏才真正开始。下面才是所有让初学者觉得c指针最难以理解的地方。
但是其实也没有那么可怕,无非就是“双重指针”。
你已经知道了“双重指针”就是指针的指针,或者说“指针的地址”,它是内存中的一块空间,保存指针的指向,就是那个下标值。而刚才你也看到了指针和数组的相似性。那么二重指针和数组有什么关系呢?
看看你熟悉的东西,
int d[10][10];
一个二维数组,是不有亲切感呀?它可以被理解为一个网格,或者矩阵,有行和列。那现在我问你,n是什么含义?
你一定会茫然。来个简单的,d[0]是什么含义?或者说(d[0])是什么含义?
显然它是一个指针。因为它是这个矩阵第一行的首地址,也就是说&d[0][0]
。那么依次类推d[1]就是&d[1][0]
,d[2]是&d[2][0]
等等……
好,那么d是什么?
现在你一定会觉得d有几分像int *k[10];
中的k了。
我希望你现在有冲动编写一个程序探索并验证一下你的想法。如果你这么做了,你会发现一个问题,这个问题会引发一系列问题,把我们带到了这篇文章中最难以理解的地方。
-------------------------------------------去发现问题---------------------------------------------
但愿看到这里的时候你已经发现这个不可思议的地方:
d的值与d[0]的值相等
指针的指针和指针应该是不同的值,这里是为什么?
-------------------------------------------思考分界线---------------------------------------------
还记得这个神奇的数组么?
int (*n)[10];
这样的写法似乎难以理解,但是,看看这个你熟悉的样子
int a[10];
然后两个数组做个比较,你会发现这个括号出现的很易于理解。在这两数组中,(*n)的地位与a等同。a是什么?数组的首地址。那n呢?自然就是数组首地址的地址了。所以n是一个双重指针。
我们把这三个数组放在一起,
int a[10];
int d[10][10];
int (*n)[10];
抛开后面的[10]
,他们可以看成这样
int (a)---[10];
int (d[10])---[10];
int (*n)---[10];
这个时候二维降成了一维
int a;
int d[10];
int *n;
它们的关系显而易见。
现在,我把他们放在一起:
int a[10];
int (*n)[10];
int *k[10];
int d[10][10];
重来一遍刚才的思考过程。(*n)[]
是一个10个int
的数组,和a[]
的地位相等。n是个指针,一个不知道长度的数组。那么(*(n + 1))
应该是下一个10个int
的数组。在看d,(d[0])[]
这个数组里面存了10个int
,所以它的地位也等价于a[]
,而(d[0])
本身就是*d
,同时(d[1])[]
、(d[2])[]
等等都一样。所以,自然而然n只是一个不知道长度的d,和前面的指针与数组的关系完美的契合。这两个数组的每一个元素都是一个数组,是真正的数组,在n + 1
和d + 1
的时候可以体现出来。这一点与k不同,因为k是个指针数组,而一个指针虽然相当于一个一维数组,但其实他们仍存在细微的差别:是否知道长度。
指针的值是指针所指空间的第一个字节的地址
这一点你应该早都知道,所以这个指向一大片区域的指针也一定是第一个字节来标记位置的。
因此,d指向了矩阵的第一行这个整体。那么它的值应该是第一个字节的地址,而d[0]指向了第一行的第一个元素,也应该是第一个元素的第一个字节的地址。所以它们俩应该是一样的。
希望这个问题能给你带来更多启示:
为什么有时候main()函数也可以这么写?
int main(int argc,char **argv)
-------------------------------------------思考分界线---------------------------------------------
每次提到字符串,问题都会简化许多。指针是不知道长度的数组,那么char *(*argv)
是一个不知道有多少个(*argv)
的数组,(*argv)
就是字符串。而argc恰好告诉了你有多少个这样的字符串,所以信息都完全了。
现在你看到了int **a;
也能表示二维数组,于是我们发现了四种二维数组的表示方法
int **a;
int (*n)[10];
int *k[10];
int d[10][10];
现在看起来它们的区别很容易理解。d是个已知行和列的数组;k知道有多少行,不知道一行中有多少列;n知道一行中有多少列,却不知道有多少行;而对于a,行列数都不知道。
不知道你现在是否觉得自己领悟了双重指针,如果是,你可以长疏一口气,然后去愉快的编程序了。不过,我还想请问下你是否用过qsort()
?
如果没有,请自学这个函数,我想你会经常用到它。qsort()函数的最后一个参数是一个函数,一个比较的函数,定义了一种偏序关系。你有没想过为什么可以把一个函数像一个参数那样传到另一个函数里呢?
因为你传的是一个函数指针。
比起上面的知识,这里的简单不少。函数名就是函数的指针,它同样指向一块内存空间。这片空间专门给这个函数用。前面我们在swap()
函数中说的“复制”的变量也是存在这里。有了这个指针,程序便知道对那些量进行什么样的操作了。
然后你可能想说,“现在我可以去愉快的编程序了么?”我觉得,如果读到了这里,你应该已经尝试过很多次指针的程序了,如果没有,不管您是否学会了指针,请听我的最后一个建议。
学习计算机的一门技能是实践的过程。在计算机世界,没有对着书死缠就能学会的知识,just do it.
好了,以上是目前我对c语言指针的所有理解,知识大概就这些,想要熟练掌握路还很远。希望能对读者有所帮助,有不足的地方或者建议欢迎与我交流讨论zhehuaxiao at gmail 或发在issue里,荣幸至致.
Zhehua Chang版权所有,转载请标注出处
- 2015/1/9 更新 : 最近在复习数据结构的时候,在清华版严老师书上发现了这样的函数声明
int xxx(int &a);
。书里说这样的写法是借用了C++中更易于理解的语法。在网上查了一下才知道,叫做“引用”。我前面有个关于链表的问题,说要这样调用create(&(node -> next));
,来新建一个节点。有了C++的这种语法,就可以这样声明函数int create(int &node);
,然后使用create(node -> next);
来达到目的了。这样的语法显然是为了帮助程序员免除指针的烦恼,但纯C中是不支持的。更多关于引用的语法自行google。 - 2015/1/10 更新 : 今天还在图书馆借到了那本经典的The C programming language,随手翻了翻指针的地方,发现讲的很简洁明了又浅显易懂。强烈推荐这本书,不管你是初学还是老手,只要没看过这本书都值得一看。里面讲了很多你之前可能忽略或者不清楚的地方,今天随便翻了几页就收获颇多。顿时感觉一门程序语言,尤其是C语言,不管用了多久都不敢称自己精通,用了越久越有这种感觉。
- 2015/1/10 csdn更新 : 服务器一直没有启用,所以个人博客也一直关闭着。但半年多来都没写博客觉得也不好,于是一时兴起就洋洋洒洒写下了这篇博文。先用word写,然后修改为markdown的格式发表在了github上,修改了几天感觉差不多了就粘到这里了。不知道怎么把markdown代码粘到csdn来,就直接把html源码粘进来了csdn似乎不支持markdown的代码格式。