虽说自己是学安卓的,但是现在感觉不能精通C语言有种羞愧,不管大家从事的职业能不能用上C,但是我也希望大家能学好,静不精通是另外一码事对吧!我也希望自己有朝一日能精通C。让我想起来一句话,树高万丈不忘根。所以拾起C这门艺术性语言。本篇主要详细讲述了指针的用法,关于结构体的话,下篇详细写。本文实例代码均为visual c++这一款IDE编写。
虽然可以用eclipse CDT插件和visual studio等开发工具。但是eclipse基本被我用来写php和java了,visual studio用来写c#。个人用惯了visual c++这款老式软件来编写c代码,毕竟打开速度快哈!
当然啦,这是题外话。window10环境下完美兼容visual c++6.0。
在大一时候老师并没有教我们什么是指针,什么是链表,什么是结构体。只是把前面的浅显的C语言基础讲完了。
现在好好总结下,正所谓温故而知新嘛,默默地思考着C语言指针的独特和难以理解。现在就来记录下这一早上的思考!
什么是指针呢?我就不追根溯源地谈论谁发明了指针。感觉发明指针的人太聪明。 跟着本文来,你会对指针的理解会迅速理解。
害怕似懂非懂混混沌沌,我把整个思绪整理了下,把所有疑问整理了下。写下此文,也希望能帮助C语言初级及进阶学习者。
用口语化的方式来让大家弄懂指针的用法是我写这篇博客的目的。在本文的最后,我找了大量的指针编程题给大家作为练习并附上答案进行讲解。
本篇文章为大家整理的纲要如下:
疑问1:
指针变量和指针变量地址究竟是什么?
疑问2:
对指针变量进行*运算到底什么鬼 ?
疑问3:
指针变量p和*p区别是什么?
疑问4:
p+1和p++到底什么区别在哪里?
带着这些疑问?我思考了一上午。脑子当时是混沌的。但是思考过后,清晰了。
首先,我写个从键盘上录入并输出的例子。我们先来小试牛刀。这个程序非常简单,咱们先不管它干嘛,当然学过指针的一看就知道。
我写这个小程序是为了让大家产生疑问。没疑问的当然更好。在后面的分类要点中,我会按照我拟写的纲要进行详细讲解。别急。
#include<stdio.h> void main(void){ int *p,i;//定义指针p int arr[5];//定义数组 p=arr; //令指针指向数组初始位置 for(i=0;i<5;i++){ scanf("%d",p+i); } for(i=0;i<5;i++){ printf("输出第%d个的值是%d\n",i+1,*(p+i)); } }
没结果,看着不起劲是吧!那么我上结果图。
我们首先定义了一个指针p,一个数组。把数组初始值赋值给了指针p。那么问题来了。
给指针赋值是怎么一回事?指针到底是数据类型还是什么?还是具体内存地址?这个也是大家疑惑的地方。
在这,我画个"简陋"的图来表示表示。
上面这个图是什么意思呢?我们可以看到指针p指向了内存中的变量m地址,这样我们就可以通过指针来访问该地址。
但是看了还有疑问是吗?打个比方,学过java的都知道游标cursor! 那么可以理解为一个游标。但是咱们的指针呢可不止那么简单。
咱们的指针是可以自由移动的。咱们别把它理解一个一个地址去移动,咱们直接理解为跳跃式的,比如说我要访问a这个变量,我把指针跳过去指向它的内存地址。
指向b变量呢?我把指针指向b在内存地址。
但是我们记住了,指针是变量,它有自己的值。重要的话说三遍,指针是变量,是变量,是变量!同志们,注意了啊,指针是变量,不要把指针理解为内存地址了啊!
而是它的值应该为地址。这句话不矛盾。变量和变量的值是两码事。变量有什么特点啊!可以自由赋值对不对,没错,那么赋值赋的是什么呢?地址!
它只是能指向某个内存达到间接访问而已。
怎么理解赋值这个含义呢?赋值咱们应该不难理解。就像给数值类型的int a=10;这样赋值,再或者int a;a=10;这样都是一样的。
在int a=10;中,包括了声明变量a和给a赋值10两个步骤。
说完赋值了,此时,我要说的并不是赋值,而是指向。咱们要是拿赋值来说指针,不好说也难理解。说指向,咱们不管它的赋值到底赋值给了什么内存地址。
咱们只需要知道这个指针变量指向了这个内存地址。我们可以通过这个指向来找到这个地址。说成赋值,请问大家怎么找?这里容易产生误区,我看书也是这样理解的。
结果理解得一塌糊涂。现在我理解为指向。谁会管一个内存地址的值是多少,假如内存地址是0x1703720。
请问,你会记住这个地址吗?答案:不会。那不就是嘛?记住赋值干嘛呢?我只需要有个指针是指向这个地址的就可以了。我需要拿到指针来查找到它,对不对?
这一段总结下:把地址赋值给指针是通俗来说就是指针指向内存地址。如果理解了,肯定觉得这句话是废话了。我也希望这句成为废话咯!
怎么得到指针指向的值呢?在这里我们要注意下一个问题,回头看,声明指针是如何声明的。这里不说什么数组,函数之类的。就拿最实在的int指针。
看下面的这句小小的指针声明,小的不能再小了是不是。不过嘛。别小觑了。
int *p,*q;重点:
这是不是声明了一个指针变量了。对不对?那么单纯的p(不带*)是什么呢?就是指向内存地址的指针变量。
那么进行*运算后(也就是*p)是什么呢 ?也就是得到变量值。这里有个重要的需要注意下。理解错误的话,也是挺苦恼的一件事。
我们不要把它理解为其他的,只把它理解为一种运算符。什么运算符呢?把内存地址转换为变量值。
也就是说,我通过查找内存地址查找到了该变量,但是我不知道变量的值。我这里说的变量,不是说指针变量,而是说的我打
比方用的int变量。怕大家理解错误了,我就说的详细点。查找到了,我指向了这个地址。但是我还留着这个记住这个内存干嘛。
下次我要利用这个指针,我再重新指向不就ok了。比如两个变量值交换,我定义两个指针。分别指向他们,然后把两个指针交换。
再进行*运算,是不是就得到了这个变量的值。这样说也是方便大家理解。
现在,我们看完上面的解释。我们再来写一个例子:
例子:定义2个指针p,q,并随便输入两个数,改变两个指针的指向,使得指针指向最大的数。
看到这个例子,我们需要通过指针来指向这两个变量的内存地址。然后通过判断两个变量的值来交换指针的指向。
我是这样写的。
#include<stdio.h> int main(void){ int a,b; int *p,*q,*tmp; p=&a; q=&b; printf("正在等待客官输入...\n"); printf("温馨提示:输入后请按回车键...\n"); scanf("%d",&a); printf("您所输入的值为%d\n",a); scanf("%d",&b); printf("您所输入的值为%d\n",b); printf("交换前:p=%d,q=%d\n\n",*p,*q); if(*p>*q){ printf("客官请稍后,正在交换中...\n"); tmp=p; p=q; q=tmp; } printf("交换已完成...\n正在打印交换信息...\n"); printf("交换后:p=%d,q=%d\n",*p,*q); return 0; }
要图么?哈哈!还是继续上图。为了生动点,文字特地加的。好了,是不是清楚点了呢?关于什么指针的指针,int **a;。
大家可以理解为一个是用来声明的一个是用来指向这个指针的。看完上面的内容这个应该不难理解了!后面再说这个。
要点1:用指针访问一维数组
上面对指针访问变量已经详细地述说了。那么接下来我们来看下如何用指针来访问一维数组的。访问一维数组的思路是什么呢?
其实最前面的那个例子已经是对一维数组实现了访问。
此时是不是很明了了。p++的意思就是指针移动到下一个变量位置并指向它,p指向变化了。那么p+1呢?p很明显指向没变,只是利用了p+N来访问后面的位置。
下次再使用p的时候就不需要对p的指向进行重置。这就是为什么我需要在for循环之后重置p指针指向的原因了。
要点2:用指针来访问二维数组
上面说完了一维数组,那么现在我们来研究下二维数组。二维数组是怎么样的呢?我们可以画个来表示下二维数组。
二维数组的形式是怎么样的呢?比如int型的二维数组int a[ i ][ j ]={{10,50},{40,60}};这个二维数组中有4个元素。
看到这张图,画的不好,别见怪。我们把每一行称作行数组元素,然后其中的具体列元素就可以表达成a[ i ][ j ]形式。
图上是不是表示得很清楚了哦!二维数组大家应该都知道。我们需要理解的是行数组元素。可以把二维数组先理解为大一维数组。理解了,咱们就可以开始下面的探索了。
#include<stdio.h> int main(void){ int a[2][2]={{10,40},{60,80}}; printf("%d",a[0]); return 0; }
这是个最简单的二维数组。其中有四个元素,按照行数组可以分两行,每行2个元素。我输出了第一个元素的值。怎么输出的呢?
可以看到我直接输出了a[0]对不对。为什么不是&a[0];大家都知道"&"是取址符。可能会问对a[0]求址不就是&a[0]吗?为什么直接a[0]可以输出。
a[0]不是一维数组的第一个元素吗?对的,刚才不是说了吗?咱们的二维数组不就是由大一维数组而得到的吗?
所以a[0]代表的是第一行。并且是第一行的一个元素的地址。也就是a[0][0]的地址; 那么是不是等同于&a[0][0]。是的,没错,a[0]就是&a[0][0]。两者等价。
但是a[0]是行数组的第一个元素,但是它是地址,记住了,是地址。所以再加个"&"显式地表示出来有没有错呢?没有错,大家可能会问,
给地址再加上一个求址符号"&"为什么没有错呢?大家不信的话,我就演示一下,看看到底有没有错。把上面的代码修改一下:
#include<stdio.h> int main(void){ int a[2][2]={{10,40},{60,80}}; printf("&a[0][0]:内存地址是0x%d\n",&a[0][0]); printf("&a[0] :内存地址是0x%d\n",&a[0]); printf("a[0] :内存地址是0x%d\n",a[0]); return 0; }
对不对。事实确实如此。 既然如此,我们就再测试一下。对地址进行" * "运算来求得数组元素的值。
#include<stdio.h> int main(void){ int a[2][2]={{10,40},{60,80}}; printf("%d",*a[0]);//打印出第一个数组元素的值 return 0; }
通过以上的演示,已经知道了如何来表示二维数组的首元素了。一句话,二维数组的首元素就是大一维数组的首元素。
那么我们可以这样,把一个指针指向二维数组的首元素。int *p=a[0];前边说过a[0]是二维数组的首元素地址,也可以表示为&a[0][0]。
那么此时我们要访问后面的元素,不是很简单了吗?假如我要访问a[0][1],就可以使用p+1指向下一个元素了,是不是很简单呢?
再进行" * "运算,就可以得到他的元素值了哦。我们举个例子:
例子:二维数组int a[ 5 ][ 2]={{10,50},{40,60},{45,78},{96,59},{31,41}};,循环打印出数组中的元素。
同志们啊,我们该怎么做?怎么做?怎么做?。。。难吗?不难其实,用数组来整,很简单的嘞。先来分析下。
1.这是个二维数组,可以理解为5行2列。也就是说行数组元素为5行。行内元素为2个。那不就是10个嘛!
2.i=5,j=2。我们可以得到其中任意一个元素值。是多少呢?现在p指针指向第一个元素a[0][0]。那么第三行第二个元素为多少。正确的是a[2][1]吗?
大家可千万别想都不想,就说是a[3][2]嘛!这是错误的啊。请记得正确的是a[2][1]。为什么呢?我们的下标都是从0开始。那么我们用指针如何指向它呢?正确的指向应该是p+2*2+1。那么元素的值应该就是*(p+2*2+1)。我们可以输出查看下。
#include<stdio.h> int main(void){ int a[5][2]={{10,50},{40,60},{45,78},{96,59},{31,41}}; int *p=a[0]; printf("%d\n",*(p+2*2+1)); return 0; }
没错吧!那么现在我们需要循环输出所有的元素,该怎么写呢?我们可以如下这样写:
#include<stdio.h> int main(void){ int a[5][2]={{10,50},{40,60},{45,78},{96,59},{31,41}}; int *p=a[0]; int i,j; for(i=0;i<5;i++){ for(j=0;j<2;j++){ printf("a[%d][%d]的值是%d\n",i+1,j+1,*(p+i*2+j)); } } return 0; }
运行程序,直接输出所有的二维数组元素,很酷很简单有木有。
但是得注意了,数组元素不能越界。什么意思呢?假设我的元素20个,我的指针访问到了元素以外,也就是越界了。此时会发生什么情况。我们实践一下:
增加2行代码:
#include<stdio.h> int main(void){ int a[5][2]={{10,50},{40,60},{45,78},{96,59},{31,41}}; int *p=a[0]; int i,j; for(i=0;i<5;i++){ for(j=0;j<2;j++){ printf("a[%d][%d]的值是%d\n",i+1,j+1,*(p+i*2+j)); } } printf("越界后的结果:%d\n",p+i*2+j+1); printf("越界后的结果:%d\n",*(p+i*2+j+1)); return 0; }
看到了吗?出问题了对不对,直接输出了2个地址。我对其进行了" * "运算和变量输出。都显示为2个不同地址,那么就说明了,指针不能越界。
现在,我们来做个练习吧!
练习:已知声明一个二维数组为a[3][3]={{15,78,74},{65,82,31},{55,96,34}};打印出二维数组中的最大值!
我出的这个练习不算难吧!我们需要是用指针来实现。
下面是我写的实例代码:
#include<stdio.h> int main(void){ int a[3][3]={{15,78,74},{65,82,31},{55,96,34}}; int *p; int max=*a[0]; int *tmp; for(p=a[0];p<a[0]+9;p++){ if(*p>max){ max=*p;//交换值 } } printf("最大值MAX=%d\n",max); return 0; }
这个应该能看懂吧!看不懂的话仔细看看。效果图是下面这样的:
不知道大家看懂了没有。其实还是很简单吧!思路是这样的。
首先,定义个指针p用来指向起始位置。然后对所有元素遍历,将元素对比之后再将元素的值打印出来。
不过注意了,在此过程中,指针p的指向是不断向前移动的,max变量的地址并没有变化,只是指针p的地址和指向都发生了变化。
当指针p指向的元素值比max大的时候,便将指针p的值赋值给max,这个时候是赋值操作了,大家应该能理解,而不是指向操作。
OK了,以上就是通过一维数组和二维数组来对指针有个细致的理解。但是前面所说的还有个地方要注意下。我们用a[0]来表示行数组的第一行的第一个元素,
那么a表示是什么呢?a代表的是二维数组名,那么第二行就是a+1,第三行就是a+2....。那么我们来做个实验,检查下a+1,a+2,a+3分别代表的元素值是多少。
#include<stdio.h> int main(void){ int a[3][3]={{15,78,74},{65,82,31},{55,96,34}}; //验证a代表什么 printf("%d\n",a); printf("%d\n",a+1); printf("%d\n",a+2); //验证对a进行*运算代表什么 printf("%d\n",*a); printf("%d\n",*(a+1)); printf("%d\n",*(a+2)); //验证指针为以行移动 printf("%d\n",*(*a)); printf("%d\n",*(*(a+1))); printf("%d\n",*(*(a+2))); return 0; }
我们看到结果,大家会发现a和*a的值竟然是一样。我们不是说a是行首地址吗?那么*a不就是对地址进行" * "运算得到值才对嘛!
但是现在为什么不一样呢?是不是矛盾了呢?
我们思考一下,请问下,a行首地址得值之后又是什么?那么咱们即便是进行" * "求值运算,得到的是行地址。所以我们可以得出一个结论:*a=a[0]=&a[0][0];
是不是疑惑了?为什么会这样,其实我也在想,为什么会这样。但是呢?事实证明了一件事啊,同志们,看清楚了!
我们写测试代码:
#include<stdio.h> int main(void){ int a[3][3]={{15,78,74},{65,82,31},{55,96,34}}; printf("%d\n",*(*(a+1))); printf("%d\n",*(a[1])); printf("%d\n",*(&a[1][0])); printf("%d\n",*(*(a+1))); printf("%d\n",&a[1][0]); printf("%d\n",*(a+1)); printf("%d\n",a+1); printf("%d\n",*(a+1)); return 0; }先上结果图:
我做这个测试的原因是想给大家看看最后2行和中间隔开的2行。为什么a和*a的地址会是一样。我们应该具备猜测和推理的能力,为什么会一样。
我们通过中间的代码可以看到,*a和&a[0][0]的地址是一致的,如果我们之间进行*a运算,我们得不到元素值,同理,什么*(a+1),*(a+2)...等等。
都是得不到数组元素值,只能得到该行的地址。这就是为什么需要进行2次" * "运算了。弄清楚这点了,我就放心了。希望大家都能懂,是我写博客的乐趣。
要点3:指针处理字符串
前面已经说清楚了一维数组和二维数组,那么接下来我们来看看指针处理字符串吧!
相信大家在看完我前面写的东西之后再看接下来的会轻松很多,如果你仔细读的话,我相信在指针这块,你不会遇到什么问题。跟着我思路一起来,轻松学指针。
现在我给大家再来个例子,单纯讲的一些死概念东西没意思,关键在理解。如果你理解了,那么自然而然就会用了。
例子:从键盘输入任何字符,要求输出数字的个数。
这个大家有思路吗?有思路也好没思路也罢,跟着我思路一起走吧!
我的思路是这样的,通过指针指向字符串的首地址,输入字符串,遍历每个字符,判断字符的值是不是在1到9之间,是的话计数加1。
很简单的例子是不是,嗯啦,确实简单!下面我们来实现这个程序。
#include<stdio.h> int main(void){ char a[100]; char *p; p=a;指向数组元素的首地址 int count; gets(p); for(count=0;*p!='\0';p++){ if(*p>='0'&&*p<='9'){ count++; } } printf("%d\n",count); return 0; }
这个程序就说完了。大家对指针的感觉是不是稍微好点了。
要点4:指针数组
是不是感觉越看越有意思了,我想文章一定不能过于死板,要是我把代码全部贴出来,给大家看,浪费大家时间而且没什么用处,看了半天什么都不会
那么一篇文章也就没有什么多大的价值了。好了,不说闲话了,这个最简单,简单的我就简单说,不搞的复杂了。
重点:指针数组的声明。
声明类似下面这样
int *a[10]; char *arr[10];
既然是指针数组,那么里面存储的肯定是指针变量对不对。跟数组元素的长度无关。只要指针指向了字符串的首地址,我们就能完整得到这个字符串。
并不在乎字符串的长度。
下面我写一个示例,大家请看:#include<stdio.h> int main(void){ char *arr[10]={"one","two","three","four","five","six","seven","eight","nine","ten"}; int i; for(i=0;i<10;i++){ puts(arr[i]); } return 0; }
数组在初始化后,每个元素指向字符串的首地址。这个我就不截图了。效果图就是依次打印出数组元素。
下面我画个简单的图来表示下指针数组各个元素对应关系。
要点5:指针作函数参数
指针作为函数参数怎么理解呢?在什么时候会用到呢? 通常我们在传入数组的时候就会用到。指针的灵活性让我们的数组元素访问那个方便性呢?不用说嘛,要是不好,大家也没必要用指针了是不是。C语言的函数声明有个特点,就是形参参数名可以省略。
首先,我们前面说过,当我们访问数组的任何元素时候,都可以通过p+i来指向它。那么i代表什么呢?指针来指向数组元素有两种写法,一种是通过p+i的形式来指向,另外一种是通过下标的方式,比如p[0],p[1],p[2]...p[N]。假如啊,我们的元素个数是M个的话,那么访问最后一个元素的方法我们可以这样写,p[M-1],或者是p+M-2,M是总元素个数。这个能理解吧!不能理解的话看这个图,前面用过一次。
指针作为函数参数传递到函数中,假如我们需要传递一个数组到这个函数中,那么我们可以选择用指针作为函数参数,大家通过前面所讲的内容都知道了,只要知道指针的指向,通过移动指针指向,就可以访问不同的数组元素。当我们将指针作为参数传递进函数的时候,传递一个指针的内存指向,再改变指向位置就可以了。下面我们通过一个实际例子来说明指针做为函数参数的用法。
例子:从键盘上输入10个数字,对其进行大小比较,输出其中的最大值max。
我们先分析一下:在这里我们需要用函数的形式来实现这个程序。我们首先把数组首元素的地址传进来,再通过指针指向的改变不断与首地址元素进行大小比较。如果比首元素值大那么就把元素值赋值给这个元素值。因为这里并不需要排序,所以不需要进行元素交换,直接赋值就可以了。
接下来编写我们的代码:
#include<stdio.h> #define N 10 int main(void){ int a[N],i; int get_max(int *,int); for(i=0;i<N;i++){ scanf("%d",&a[i]); } int max=get_max(&a[0],N);//这里可以传入a代替&a[0] printf("最大值为%d\n",max); } int get_max(int *p,int n){ int max=*p,i; for(i=0;i<N;i++){ if(*(p+i)>max){ max=*(p+i); } } return max; }
然后我们看看结果图:
好了,指针作为函数参数就到这里为止,大家可以通过编写传递指针数组或者字符串指针对其进行加深理解。咱们看看下一个要点。
指针函数
本篇文章将会对指针函数进行详细解读,重点对指针函数和函数指针进行分析! 好啦,写辛苦了。指针在函数中的用法会在本篇的后面继续更新,所以本篇还没写完咯。这两天更新完。
谢谢大家的阅读。如果文章中有什么错误的地方,敬请指出!