一.指针,内存,地址,指针变量的关系
二.对编址以及指针变量大小的理解
三.指针类型的意义
四.解应用操作符
五.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,回调函数,转移表。