深入理解C语言系列之指针透析(原来你一直没有搞懂C语言指针是因为没有理解其中的规律)

写在前面

指针是C/C++语言的特色之一,它允许程序员直接操纵内存。这一块内容也是比较抽象的,很多人尤其是编程小白,对C语言指针的理解都是模糊的,其实就是没有搞懂C语言指针的实质和规律。

本篇文章则是对六大类C语言指针的透析,把这六类C语言指针搞懂了基本上C语言指针就算过关了。很多有经验的编程大佬可能之前也没有这样对C语言指针进行这样规律的总结,这也算是从另外一个角度去思考C语言指针的方式。

一、思考指针的基础

1、指针的实质

指针就是指向一个地址的变量,一个指针只可以指向一个地址。很多地方可能会说成存放地址的变量,这就是理解方式的不同了,个人觉得“指针指向一个地址”好记一些。

2、指针的层次

我曾经对C语言指针也是尤为模糊的,觉得自己挺懂的,但其实还是没有理解到实质,因此通过花了一整天静下来慢慢思考过后,发现原来自己是没有将层次分清楚。
下图为我自己总结的C语言指针层次图:
01
这个只是层次图,只是 为了明白指针其实分为了定义和调用两种使用指针的方法。

3、指针的分类

这里就把指针的六大类分出来了,回想第二点,就可以知道每一类指针都定义和调用两种使用方法。
02
上面这张图中,后面则为各类指针的定义方式。

4、两个符号(&和*)

然后必须要区分并熟练理解清楚的是关于指针的两个符号的使用,即符号“&”和符号“*”;
(1)符号“&”:取地址运算符,该运算符就是用来取某个变量的地址,如果定义一个int a=2;那么&a就是取a的地址即2的地址。
(2)符号 “*”:间接访问运算符,这个符号用法就稍微复杂一点,主要可以分为以下三点,
03
符号“*”的用法中,第一个是比较好理解的,就是用于定义时用,没有别的特殊含义,就是说明在定义一个指针而不是一个普通的变量;
第二个是最容易混淆的,其原因可能就是没有与第三个区分开来,记住了第三个用法int (*p)[10];是表示行指针的固定用法,那么其他的情况就都是用来访问指针所指向的内容的,这样就可以将符号 “*” 的三种用法比较清晰得区分开了。

二、单指针(int *p)

1、指向单个变量的地址
前面说过了,指针就是指向一个地址的变量,因此可以定义普通变量a,然后将指针指向这个变量的地址:

	int a = 520;
	int *p;
	p = &a;  //也可以写在一起:int *p=&a; 
	printf("a的地址:%d, p的地址:%d\n", &a,p);
	printf("p的值:%d\n", *p);

那么这个指针指向的地址就是变量a的地址了,因此可以打印,打印出两个地址就可以清楚看出来。其次,这个指针便可以访问到变量a中的值了。
04
此时指针p指向的地址是a的地址,此时我们将p加1,则表示将p指向的地址加1,在后面加上以下代码:

	p++;
	printf("p的地址加一:%d", p);

05
但是结果并不是将地址号直接加1了,而是加4了。为何?原来我们定义的是int型变量,而一个int型变量占用的是4个地址空间大小,因此地址加1则是地址号上加4。

2、指向一维数组的首地址(数值)
这一点需要强调的是,指针指向的是某个数组的首地址,并不是指向某个数组!
这里的&a就是表示获取数组a 的首地址,这里p指向的是一维数组的首地址,因此我们可以通过增加地址号来改变指针所指向的地址,从而访问到数组中的其他元素。

	int a[4] = {
    
    1,3,5,7};
	int *p = &a; 
	printf("%d\n", a[1]);
	printf("%d\n", *p);
	p++;
	printf("%d\n", *p);

06

3、指向字符串的首地址(字符)
这一点与第2点是一个道理的,就是指针指向了一个字符串的首地址,这里也可以看出,在C语言中,字符串也是一种特殊的一维数组,这个数组中的每个元素则是每个字符。

	char a[4] = {
    
    'L','O','V','E'};
	char *p = &a; 
	printf("%c\n", a[1]);
	printf("%c\n", *p);
	printf("%d\n",p);
	for( ; *p; p++){
    
    
		printf("%c", *p);
	}

在这里,我们用指针p指向字符数组a的首地址(即字母L),因此我们输出*p则是输出字母L;输出p,则是输出字符串a的首地址(即字母L的地址)。
同第2点一样,可以通过改变地址好来改变指针p指向的字母,因此可以遍历整个字符串。
07

三、指针数组(int *p[10])

1、基本概念
前面我们知道了指针可以指向某个地址,我们可以通过改变对指针的运算来指向其他的地址,然后我们是用间接访问运算符(*)来获取指针指向的值。

接下来分析的是第二类指针,即指针数组,顾名思义,它表示的就是 数组中的元素为指针变量,每个元素都可以指向一个地址(存储空间)。
例如,int *p[10];定义一个int类型的指针数组,则p[0] 则表示第一个指针所指向的地址空间,相当于二维数组中的a[0][0], 这一点非常重要,没有理解清楚就很容易与行指针混淆。

2、指向字符串
因为指针数组中的每个元素都为一个指针,都可以指向不同的地址(存储空间),因此它可以指向不同大小的存储空间,例如指向不同长度的字符串。

	char *p1[3];
	p1[0] = "Monday";
	p1[1] = "Tuesday";
	p1[2] = "Wednesday";
	printf("p1的首地址:%d\n",p1);  //也就是p1[0],即p1的值,是p1的首地址 
	printf("p1[0]中M的地址:%d\n",*p1);  //也就是p1[0]的值,是字符串Monday的首地址 
	printf("p1[0]中M的地址:%d\n",p1[0]);
	printf("p1[0]中M的值:%c\n",**p1);  //也就是*p1[0]的值 
	printf("p1[0]中M的值:%c\n",*p1[0]);
	puts(p1[0]);  //puts可以用来输出字符串 

08
从这里也可以看出来,说字符数组指向一个字符串是不严谨的,实际上是字符数组中的元素(一个个指针)指向一个个字符串的首地址了,因此一个指针数组相当于是一个二维数组。

3、指向二维数组
理解了指向字符串,再来理解指向二维数组就容易得多了,因为字符串就相当于是一个一维数组,上面说指针数组指向字符串,实际上是指针数组中的元素(指针)指向了字符串的首地址;

那么这里的指针数组指向二维数组,则是指针数组中的元素(指针)指向了一维数组的首地址。

	int a1[][3] = {
    
    {
    
    1,3,5}, {
    
    7,9,11}};
	printf("a1的首地址:%d,a1[0]的地址:%d\n",a1,a1[0]);
	int *p1[3];
	p1[0] = a1[0];
	p1[1] = &a1[0][0];
	printf("p1的首地址:%d,*p1的首地址:%d\n",p1,*p1);  //就是p1[0]这个指针的地址;则是p1[0]这个指针指向的地址即a1[0][0]的地址 
	printf("p1[0]的地址:%d,p1[1]的地址:%d\n",p1[0],p1[1]);  //两个都是a1[0][0]的地址 
	printf("p1[0]的值:%d,p1[1]的值:%d\n",*p1[0],*p1[1]);  //两个都是a1[0][0]的值 
	printf("p1[0]+2的地址:%d,a1[0][2]的地址:%d\n",p1[0]+2,&a1[0][2]);  //改变指针指向后的地址 
	printf("p1[0]+2的值:%d,a1[0][2]的值:%d",*(p1[0]+2),a1[0][2]);  //改变指针指向后的值

这里面的各种地址变换特别多,这也就是指针的难点所在,但其实也仅仅只是二维数组的变换而已,仔细思考不走神,还是很快就能懂的。

四、行指针(int (*p)[10])

1、引入行指针
行指针是与指针数组最接近的一类指针,也是指针的一大难点,同样也是指向二维数组的首地址。

行指针中前面的(*p)是它的特色,与行指针类似,后面中括号中的数表示的是二维数组的第二维(列)的大小,与a[][10]对应。这也说明为什么定义二维数组时可以省略第一维的大下,而必须指定第二维大小的原因。

按照文章第一大点的四个技术来思考,能够理解这两行,那么行指针就理解得差不多了。

printf("a2[0][0]的地址:%d,%d,%d,%d\n",(*p2),&(*p2)[0],&(*(p2[0]+0)),&a2[0][0]); 
printf("a2[0][0]的值:%d,%d,%d,%d\n",*(*p2),(*p2)[0],(*(p2[0]+0)),a2[0][0]); 

2、完整代码

	int a2[][3] = {
    
    {
    
    2,4,6}, {
    
    8,10,12}};
	printf("a2的首地址:%d,a2[0]的地址:%d\n",a2,a2[0]);
	int (*p2)[3] = &a2;  //在指针数组中这样则会报错,而在行指针中则是正确的
	printf("a2[0][0]的地址:%d,%d,%d,%d\n",(*p2),&(*p2)[0],&(*(p2[0]+0)),&a2[0][0]); 
	printf("a2[0][0]的值:%d,%d,%d,%d\n",*(*p2),(*p2)[0],(*(p2[0]+0)),a2[0][0]); 
	//表示a2中的12最常用的两个方法:
	printf("%d,%d",*(p2[1]+2),a2[1][2]);

09

五、指针的指针(int **p)

在计算机中,任何数据都是要占用内容的,因此指针也不例外,指针本身也需要占用内存空间,即某个指针也有其地址。如果用一个指针来指向这个指针的地址,则使用的这个指针为二级指针,也就是指针的指针,指向指针的地址。

	int a[][3] = {
    
    {
    
    1,2,3}, {
    
    4,5,6}};
	int *p1, **p2;
	p1 = &a[0][2];
	p2 = &p1;  //*p2 = &a[0][2];是不合法的 
	printf("p1的地址:%d,p2的地址:%d\n",p1,p2);
	printf("p1的值:%d,p2的值:%d\n",*p1,*p2);
	printf("用p2引用p1指向的值:%d\n",**p2);

如上代码所示,p1指针是一级指针,指向的是二维数组a的首地址(1的地址);

而p2是二级指针,指向的是p1的地址

再往下溯源,就可以通过p2这个二级指针引用到p1地址指向的值。
10

六、指针函数(int *fun( ))

函数除了可以返回整型、浮点型、字符型数据之外,还可以返回一个指针,也就是返回一个地址。
这样的函数,就是一个指针函数。从字面上也可以看出,它其实就是一个函数,因此也叫做返回指针的函数。

	int x=2, y=3;
	int f1(int a,int b){
    
      //普通函数 
		if(a>b){
    
    
			return a;
		}
		else
			return b;
	} 
	printf("大数为:%d\n",f1(x,y));  //返回指针的函数 
	int *f2(int *p1, int *p2){
    
    
		if(*p1 > *p2){
    
    
			return p1;
		}
		else
			return p2;
	}
	printf("大数第地址为:%d\n",f2(&x,&y));
	printf("大数为:%d\n",*f2(&x,&y));

如上代码,这样定义的函数(返回指针的函数)返回的就是一个地址,我们可以引用这个地址,也可以使用*号来引用这个地址指向的值。
11

七、函数指针(int (*p)( ))

我们都知道指针是用来存放地址的,函数编译后,其指令也会占用内存空间。因此,我们可以定义指针来存放函数的地址。

C语言规定,函数的名字就是函数的首地址,也就是一个地址值,即函数的入口地址。

而函数指针本质就是一个指针,这种指针可以直接被一个函数地址所赋值,赋值过后,这个函数指针就代表了这个函数,可以通过这个函数指针进行调用函数。

	int x=2, y=3;
	int max(int a, int b){
    
    
		return (a>b?a:b);
	}
	int min(int a, int b){
    
    
		return (a<b?a:b);
	}
	int (*p)(int, int);
	p = max;
	printf("最大值为:%d\n",p(x,y));
	p = min;
	printf("最小值为:%d",p(x,y));

在以上代码中,我们定义的函数指针p代替了函数max和函数min,然后通过调用函数指针传入参数,就将函数调用了,在作用效果上与直接调用函数完全一样。

好了,以上就是C语言中所有的指针用法,都是按照规律来总结的。其实也有点不严谨,主要是为了让大家理解指针的概念,能够通过规律记住指针的各种用法,希望我们的C语言都能越学越好,越学越开心!

猜你喜欢

转载自blog.csdn.net/Viewinfinitely/article/details/109412066