C进阶之指针完全解读

       虽说自己是学安卓的,但是现在感觉不能精通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));
	}
}


没结果,看着不起劲是吧!那么我上结果图。

  

C进阶之指针完全解读_第1张图片


我们首先定义了一个指针p,一个数组。把数组初始值赋值给了指针p。那么问题来了。  

给指针赋值是怎么一回事?指针到底是数据类型还是什么?还是具体内存地址?这个也是大家疑惑的地方。

在这,我画个"简陋"的图来表示表示。  

C进阶之指针完全解读_第2张图片

上面这个图是什么意思呢?我们可以看到指针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;
}


C进阶之指针完全解读_第3张图片


要图么?哈哈!还是继续上图。为了生动点,文字特地加的。好了,是不是清楚点了呢?关于什么指针的指针,int **a;。

大家可以理解为一个是用来声明的一个是用来指向这个指针的。看完上面的内容这个应该不难理解了!后面再说这个。


要点1:用指针访问一维数组


上面对指针访问变量已经详细地述说了。那么接下来我们来看下如何用指针来访问一维数组的。访问一维数组的思路是什么呢?

其实最前面的那个例子已经是对一维数组实现了访问。

C进阶之指针完全解读_第4张图片

此时是不是很明了了。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;
}

C进阶之指针完全解读_第5张图片


通过以上的演示,已经知道了如何来表示二维数组的首元素了。一句话,二维数组的首元素就是大一维数组的首元素。


那么我们可以这样,把一个指针指向二维数组的首元素。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;
}

C进阶之指针完全解读_第6张图片


没错吧!那么现在我们需要循环输出所有的元素,该怎么写呢?我们可以如下这样写:


#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;
}


运行程序,直接输出所有的二维数组元素,很酷很简单有木有。


C进阶之指针完全解读_第7张图片  


但是得注意了,数组元素不能越界。什么意思呢?假设我的元素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;
}

C进阶之指针完全解读_第8张图片


看到了吗?出问题了对不对,直接输出了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;
}


这个应该能看懂吧!看不懂的话仔细看看。效果图是下面这样的:


C进阶之指针完全解读_第9张图片


不知道大家看懂了没有。其实还是很简单吧!思路是这样的。

首先,定义个指针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;
}

大家都会说两个 " * "就是指针的指针,那么到底什么什么含义呢?

C进阶之指针完全解读_第10张图片


我们看到结果,大家会发现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;
}
先上结果图:   

C进阶之指针完全解读_第11张图片


我做这个测试的原因是想给大家看看最后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;
}


在上面代码中,我需要说明的是,在字符数组中,通常会以\0来结束。也就是说字符串的结尾是以\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;
}


数组在初始化后,每个元素指向字符串的首地址。这个我就不截图了。效果图就是依次打印出数组元素。

下面我画个简单的图来表示下指针数组各个元素对应关系。

C进阶之指针完全解读_第12张图片

要点5:指针作函数参数

指针作为函数参数怎么理解呢?在什么时候会用到呢? 通常我们在传入数组的时候就会用到。指针的灵活性让我们的数组元素访问那个方便性呢?不用说嘛,要是不好,大家也没必要用指针了是不是。C语言的函数声明有个特点,就是形参参数名可以省略。


首先,我们前面说过,当我们访问数组的任何元素时候,都可以通过p+i来指向它。那么i代表什么呢?指针来指向数组元素有两种写法,一种是通过p+i的形式来指向,另外一种是通过下标的方式,比如p[0],p[1],p[2]...p[N]。假如啊,我们的元素个数是M个的话,那么访问最后一个元素的方法我们可以这样写,p[M-1],或者是p+M-2,M是总元素个数。这个能理解吧!不能理解的话看这个图,前面用过一次。

C进阶之指针完全解读_第13张图片

指针作为函数参数传递到函数中,假如我们需要传递一个数组到这个函数中,那么我们可以选择用指针作为函数参数,大家通过前面所讲的内容都知道了,只要知道指针的指向,通过移动指针指向,就可以访问不同的数组元素。当我们将指针作为参数传递进函数的时候,传递一个指针的内存指向,再改变指向位置就可以了。下面我们通过一个实际例子来说明指针做为函数参数的用法。

例子:从键盘上输入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;
}

然后我们看看结果图:  



好了,指针作为函数参数就到这里为止,大家可以通过编写传递指针数组或者字符串指针对其进行加深理解。咱们看看下一个要点。

指针函数


本篇文章将会对指针函数进行详细解读,重点对指针函数和函数指针进行分析! 好啦,写辛苦了。指针在函数中的用法会在本篇的后面继续更新,所以本篇还没写完咯。这两天更新完。

谢谢大家的阅读。如果文章中有什么错误的地方,敬请指出!



你可能感兴趣的:(编程,C++,C语言,指针,精通C)