深入理解指针

本文详细介绍了指针、内存地址、指针变量的关系,编址理解,不同指针类型的含义,应用操作符(*),const修饰指针,传值调用与传址调用的区别,以及如何使用指针访问数组和理解一维、二维数组的传参本质。还涵盖了指针数组、数组指针、函数指针等内容。
摘要由CSDN通过智能技术生成

一.指针,内存,地址,指针变量的关系

二.对编址以及指针变量大小的理解

三.指针类型的意义

四.解应用操作符

五.const修饰指针

六.传值调用与传址调用

七.使用指针访问数组

八.一维,二维数组传参的本质

九.指针数组,数组指针,函数指针变量,函数指针数组及其变量

其实关于指针的内容很多,在这里,我将个人认为较难理解的相关指针内容制成目录,下面将与大家共同分享,探讨。

一.指针,内存,地址,指针变量的关系

举一个例子,在我们生活的小区当中,为了方便查找,我们通常会给房子编号,如第几栋,8楼,807,这样提高了我们查找的效率。在计算机里面也是如此,在我们计算机cpu读取内存时,所需要的数据也是在内存当中读取,为了高效管理与查找数据,我们把内存划分为一个个小的内存单元,

而我们也给每个内存编号,而这个编号也叫地址,c语言中地址被叫做指针。

于是乎我们可以这样理解:内存单元的编号==地址==指针,而指针变量就是存放这个地址的变量。

二.对编址以及指针变量大小的理解

在cpu访问内存的时候,通常会有三根线(地址总线,数据总线,控制总线),控制总线用于传递用户输入,如:R(读),W(写)。而数据总线则为数据传输。例如:键盘读取用户输入,找到对应内存的数据,通过数据总线传给cpu。在这里我们着重介绍理解一根线-->地址总线。我们可以这样理解,在32位机器上,地址线有32根,每根线都有0,1两种形态。则一共有2^32次方种由0,1组成的地址,在64位机器下,则有2^64次方种组合。而指针变量大小则与这两种环境息息相关。

,当在x86环境下,即地址是由32位由0,1组合而成的二进制序列,如果用指针变量来存储这些数列,则需要32个比特位,也就是4个字节。若为x64的编译环境,按照上面的思路则指针变量大小为8个字节。

三.指针类型的意义

在相同的编译环境下,指针类型大小与指针类型是没有关系的,如x86,指针类型大小都为4个字节,x64当中,指针类型大小都为8个字节。那么我们或许会有疑问,那大小既然一样,为什么还会区分出int*,char*,double*等等一系列指针类型呢?

我们不妨看看下面的代码:

#include<stdio.h>
int main()
{
   int n = 0x11223344;
   int *p = &n;
   *p = 0;
   return 0;
}
#include<stdio.h>
int main()
{
   int n = 0x11223344;
   char *p =(char*)&n;
   *p = 0;
   return 0;
}

当我们在vs上面调试代码,不难发现对于指针类型的不同,所访问的权限也有所差异,例如int*一次访问4个字节,char*一次访问1个字节。

四.解应用操作符(*)

其实关于这个操作符相对来说较好理解,解引用其实就是根据地址找到地址所对应的内容,它与&可以说是一来一回-->&(取出地址),*(根据地址找回去)。比如说这样理解:*&pi其实就是pi,&与*相互抵消了。

五.const修饰指针

其实这个相对而言也比较好理解。例如:const int*p = &n。在这句代码当中const修饰的是*p,而*p是根据地址找到对应内容,所以在上面这句代码当中指的是*p指向的内容不可被修改。

若代码变为int *const p = &n;由于这里修饰的是指针变量p,所以强调的是指针变量的内容不可被修改,即地址不可被修改。

当然,在这里如果我们既不希望指针变量被修改,也不希望指针所指向的内容被修改,这句代码则可以改为:int const*const p = &p;这样两者都不可被需改。

六.传值调用与传址调用

在介绍传值调用,我们不妨写一段代码,用以实现两数交换,经过我们的深思熟虑我们或许会这样写:

#include<stdio.h>
void Swap(int a,int b)
{
   int tmp = a;
   a = b;
   b = tmp;
}
int main()
{
   int a = 10;
   int b = 20;
   printf("交换前:a=%d,b=%d",a,b);
   Swap(a,b);
   printf("交换后:a=%d,b=%d",a,b);
   return 0;
}

下面我们用编译器运行一下代码:

,我们发现交换前与交换后两个变量的值并未发生交换。这是因为在Swap函数内部确实已经交换,但是出了Swap函数。创建的函数栈帧随之销毁,空间还给了操作系统(函数栈帧的创建与销毁具体参考我分享的上一篇博客),所以打印的还是原来的a,b的值。

或者我们可以联想我们之前在函数调用学的知识来解释,当传递参数时,函数会创建一个形参来接收实参传来的值,而在Swap函数内部改变形参不会影响实参,即形参是实参的一份临时拷贝,形参的改变不会影响实参

而上面的这些代码属于传值调用,具体的我们在函数调用那一节已经学过了。

下面,我们来试试并体会传址调用,我们不妨将代码这样写:

#define _CRT_SECURE_NO_WARNINGS 1​
#include<stdio.h>
void Swap(int *a, int *b)
{
	int tmp = 0;
	tmp = *b;
    *b = *a;
	*a = tmp;
}
	int main()
	{
		int a = 10;
		int b = 20;
		printf("交换前:a=%d,b=%d", a,b);
		Swap(&a,&b);
		printf("\n");
		printf("交换后:a=%d,b=%d", a,b);
		return 0;
	}

将a,b的地址传过去,通过解引用地址改变a,b的值。下面看看代码运行的结果:

,其实在这里我们可以这样总结:当我们需要计算主函数变量的值,我们可以采用传值调用,若我们想改变主函数变量的值,则可以使用传址调用。

七.使用指针访问数组

有了前面学习积累的知识,现在我们不妨用指针来访问数组。

下面我们看下面这段代码:

#include<stdio.h>
int main()
{
   int arr[10] = {0};
   int i = 0;
   int sz = sizeof(arr) / sizeof(arr[0]);
   int *p = arr;
   for(i = 0;i<sz;i++)
   {
     scanf("%d ",p+i);
   }
   for(i = 0;i<sz;i++)
   {
     printf("%d ",*(p+i));
   }
   return 0;
}

其实这段代码也较好理解,p为指针变量,类型为int,用于存储arr,而数组名又是首元素的地址,在第一个for循环中,scanf输入,p存储数组首元素地址,输入一个整形,存入这个地址里面去,i+1,元素地址向后走一步。

而第二个for循环,完成打印任务,p为首元素地址,刚开始进入循环i=0,*解引用得到第一个元素,随着循环的进行,i不断++,逐渐得到,打印数组的全部内容。

八.一维,二维数组传参的本质

为了方便理解,我们依旧用一段代码来举例:

#include<stdio.h>
void test(int*arr)
{
   int sz2 = sizeof(arr) / sizeof(arr[0]);
   printf("%d",sz2);
}
int main()
{
   int arr[] = {1,2,3,4,5,6,7,8,9,10};
   int sz1 = sizeof(arr) / sizeof(arr[0]);
   printf("%d",sz1);
   test(arr);
   return 0;
}

当我们运行结果时会发现,sz1,sz2的值并不相等,这是为什么:

,我们在test函数里面传递的参数是arr,而数组名是数组首元素的地址,既然是地址我们就能用指针变量来接收,sz1正常打印这没错,但是sz2就没这么简单了,因为test函数内部计算的是地址的大小而不是数组的大小,而此时编译环境为x86,即32位地址总线,大小为4个字节,一个元素大小也为4个字节,所以sz2为1。其实一维数组传递参数本质上就是传递地址。

一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

关于二维数组arr[3][3],其实我们在前面的知识可以知道,二维数组其实可以看成多个一维数组,

a[1]是一个数组名,数组名表示首元素的地址,所以在二维数组当中,a[1]一维数组的地址就是二维数组的首元素地址。当我们访问二维数组每个元素时,可以这样写:

for(int i = 0;i<sz;i++)
{
for(int j = 0;j<sz;j++)
{
printf("%d",*((*arr+i)+j));
}
}

arr是每一行首元素的地址,*arr是拿到二维数组里面一维数组的第一个元素的首地址,+j是依次向后遍历,每一行遍历完成后,执行i++,开始下一行。

其实二维数组可以看成里面每个元素都是数组指针(指向数组的指针),分别指向每一行。

在后续博客当中,我会拿几个指针企业面试题与大家一起分享,探讨相关指针的理解与应用。

九.指针数组,数组指针,函数指针,函数指针数组及其变量

首先,指针数组,是一个数组,里面每一个元素都是指针,如图:

其书写为int*(里面指针的类型)arr[5](元素的个数)。

下面我们用指针数组模拟二维数组:

#include<stdio.h>
int main()
{
   int arr1 = {1,2,3,4,5,6,7,8,9,10};
   int arr2 = {2,3,4,5,6,7,8,9,10,11};
   int arr3 = {3,4,5,6,7,8,9,10,11,12};
   int*p[3] = {arr1,arr2,arr3};//指针数组
   int i = 0;
   int j = 0;
   for(i = 0;i<3;i++)
   {
     for(j = 0;j<3;j++)
     {
         printf("%d",p[i][j]);
     }
     printf("\n");
     return 0;
}

,这里需要注意的是:p[i][j],并不是二维数组的意思,但是却很相似。p[i]的意思是数组指针里面的第一个指针,p[j]则是访问一维数组里面的元素,这是跟二维数组的略微区别,但也十分相似。

九.指针数组,数组指针,函数指针,函数指针数组及其各变量

1.指针数组:

指针数组其实就是数组,只不过里面的每个元素是指针。

指针数组里面的每个元素是地址,又可以指向一块区域。

2.数组指针

这里数组指针则指的是指针,指向的地址是数组的地址。

例如:int则是指向数组元素的类型,int(*p)[5],*p表示指针,指向这个数组的地址,[5]表示这个数组的元素个数。

这里值得注意的是:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合

3.函数指针及其变量

函数指针变量是:存放函数地址的变量,通过该地址可以调用函数(回调函数)。

 

我们发现&test与test打印的地址是一样的,也就是说,函数名就是地址。

,如上这段代码,就是用函数指针变量存储函数的地址,最后打印调用这个函数指针变量,然后访问函数的地址,执行函数内部的逻辑。

函数指针数组:

函数指针数组其实就是一个数组,数组里面每个元素都是函数指针,也就是函数的地址。

例如:int (*arr[10])(),首先这是一个函数指针变量,arr首先跟[]结合,说明这是数组,而数组里面的元素类型是int(*)()的函数指针。

关于深入理解指针就先分享到这,下一篇博客,咱们来了解了解:qsort快排,以及用冒泡排序模拟实现qsort,回调函数,转移表

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值