来吧,C语言最重要的东西就在于指针了。这节好好看,多查多思考,多画图,有助于理解
一、地址的概念
“地址”:每个内存单元(字节)的编号
“值“:内存单元中存入的内容
地址也就是C语言中所谓的指针。
也就是说我们可以这么理解,内存编号 == 地址 == 指针。同时,指针变量也是一种特殊的变量,就是用于存储其他变量的地址的一个变量,它也有自己的地址。这点先了解一下就行。
二、指针变量
指针变量基础
指针变量的概念:一种专门存放其它变量的地址(指针)的变量,即指针变量。
定义形式:数据类型 * 变量名。
例:int *p1;float *p2;char *p3;
字符” * “,表示该变量是一个指针变量。int 类型代表,该指针变量存储的地址是一个整型类型变量的地址。
&----取址运算符,(&a)就是 a 的地址。
*----指针运算符,也叫解引用运算符,*p表示p这个地址指向的变量。
int a=10;
int *p=&a;//指针变量p存储了a的地址(&a)
*p=20;//通过间接引用p所指向的对象,更改该对象的值,即a(*p)=20
这里我们得出以下结论:
第一点:a=*p=*(&a)
第二点:p=&a=&(*p)
这也就是说,取址运算和指针运算是一对互逆运算。上面两个结论非常重要,当变量和指针变量特别多的时候, 一定要搞清楚指针变量存储的是哪个变量的地址,该变量的地址存在了哪些指针变量里。Tips:咬文嚼字。也就是说指针变量同一时间只能存储一个变量的地址,要想存储别的变量的地址,需要把当前存储地址覆盖。但同一时间,一个变量的地址可以存在于多个指针变量。
指针变量大小:
指针变量的大小取决于地址的大小。
#include <stdio.h>
//32位平台下地址是32bit位(4个字节)
//64位平台下地址是64bit位(8个字节)
int main()
{
printf("%zd\n",sizeof(int *));
printf("%zd\n",sizeof(char *));
printf("%zd\n",sizeof(double *));
printf("%zd\n",sizeof(short *));
return 0;
}
32位机器有32根地址总线,每根地址总线出来的信号转换成数字信号后是0或1,如果把32根地址总线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要四个字节存储。
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
Tips:注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
既然如此,我们又为什么要有各种各样的指针类型呢?其实:指针的类型决定了对指针解引用的时候有多大的权限(即一次能操作几个字节)。比如 :char*的指针解引用就只能访问一个字节,而int*的指针解引用就能访问四个字节。
指针±整数
先看一段代码,调试观察地址的变化。
#include <stdio.h>
int main()
{
int n=10;
char *pc=(char*)&n;
int *pi=&n;
//%p--地址占位符
printf("%p\n",&n); //&n = 00AFF974
printf("%p\n",pc); //pc = 00AFF974
printf("%p\n",pc+1); //pc+1 = 00AFF975
printf("%p\n",pi); //pi = 00AFF974
printf("%p\n",pi+1); //pi+1 = 00AFF978
return 0;
}
我们可以看出。char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过了四个字节。这就是指针变量的类型差异到来的变化。指针+1,其实就是跳过1个指针指向的元素。指针可以+1,那自然也可以-1.
结论:指针的类型决定了指针向前或向后走一步有多大(距离)。
三、存取方式
直接访问:通过变量名直接访问变量内容
间接访问:通过中间变量(指针变量)间接访问
我们想输出ch和num的值,有两种方法,第一种就是直接输出num和ch。第二种就是通过p1和p2存储的地址,顺藤摸瓜,找到num和ch。下面两行代码,输出的结果是一样的。
printf("%d %c",num,ch);
printf("%d %c",*p1,*p2);
那有人就会有疑问了,能直接访问,我为什么还要间接访问呢,这不是画蛇添足,多此一举吗。不是的,我们学过函数了,都知道了形参改变不影响实参,那假如我们把实参的地址传过去,实参是不是就可以改变了。来看一段代码。
void swap1(int a,int b)
{
int tmp=a;
a=b;
b=tmp;
return;
}
void swap2(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
retrun;
}
int mian()
{
int a=3,b=5;
swap1(a,b);
printf("%d %d",a,b);//3 5
swap2(&a,&b);
printf("%d %d",a,b);//5 3
return 0;
}
看完之后,心中明悟了吗。写一个交换两个变量的值的函数的时候。在传参的时候,我们传值,那么在新的函数里就是简单的copy一下原来变量的值,内部交换,换完之后局部变量销毁,内存释放。实参没有发生变化。但如果,我们通过传入地址参数,我们循着这个地址就能改变所指向对象的值。来,看个图。
这就是我们说的一个变量的地址可以存储在多个指针变量种。他们任何一个引用,都将导致该变量本身发生改变。本质就是将地址这个特殊的”数字“传进了被调函数,被调函数得到了该变量的“联系方式”(家庭住址) ,我就可以找到它并操作它。
四、特殊指针
泛型指针
void*指针(也称泛型指针),无具体类型的指针,它可以接受任意类型地址。但也有局限性,void*指针不能直接进行指针的±整数和解引用的运算。
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样设计可以实现泛型编程的效果。使得一个函数可以处理多种数据类型的数据。
空指针
空指针也就是指针变量未存储任何变量的地址,即NULL。
野指针
野指针也就是指针的指向是不确定的,未知的,随意的。野指针有以下三条成因:
1.指针变量未初始化。
2.指针变量指向的空间被释放。
3.指针越界访问。
如何规避野指针?
1.指针初始化,已知确定指向的指针进行具体初始化,暂时不知指向的指针初始化为NULL,让其成为空指针。
2.适时将指针置NULL。当指针不再使用时,将指针指向为空。
3.避免将局部变量的地址返回。
4.明确指针界限,提防越界情况。
修饰指针
被const修饰的指针,有着特殊的用法。而且,const处于的位置不同,意义也不同。
int *p=NULL; //无修饰指针
int const *p=NULL; //const左修饰
int * const p=NULL; //const右修饰
左修饰:const在 * 左面,代表,不可通过指针修改指向对象的值,但指针的指向可以改变。
右修饰:const在 * 右面,代表,不可改变指针变量所存储的地址,但可以改变指针指向对象的值。
五、数组与指针
数组名的理解
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int *p=&arr[0];
这里我们使用&arr[0]的方式拿到了数组第一个元素的地址。但其实数组名本来就是地址。而且是数组首元素的地址。下面我们来做个测试:
#include <stdio.h>
int main()
{
int arr[10]={0};
printf("&arr[0] = %p\n",&arr[0]); //&arr[0] = 004FF9CC
printf("arr = %p\n",arr); //arr = 004FF9CC
return 0;
}
我们发现数组名和数组首元素地址打印的结果一模一样。数组名就是数组首元素的地址。
那么sizeof(arr)怎么理解对于上边那段代码,计算sizeof(arr)的值是40,但有个疑问,数组名不是首元素地址吗,地址大小不该是4或者8吗。其实上面结论没有任何问题,但是"大道五十,遁去其一",这里面也有两个例外:
· sizeof(数组名),sizeof中单独放数组名,这里的数组名代表整个数组,计算的是整个数组的大小,单位是字节。
· &数组名,这里的数组名代表整个数组,输出的是整个数组的地址(整个数组的地址和数组的首元素地址是有区别的)
除此以外,任何地方使用数组名,数组名都是首元素的地址。
&arr[0] //0077F820
&arr[0] + 1 //0077F824
arr //0077F820
arr + 1 //0077F824
&arr //0077F820
&arr + 1 //0077F848
这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是首元素的地址,+1就是跳过⼀个元素。 但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。到这⾥⼤家应该搞清楚数组名的意义了吧。数组名是数组首元素的地址,但是有两个例外。
指针访问数组
有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。
&arr[i] == arr+i //我们知道arr在这里不在两个例外的范围内,那么arr就是数组首元素地址,+i就是跨过i个步长,也就是arr[i]的地址。
arr[i] == *(arr+i) //对上式进行解引用操作,就得到了这个等式,非常简单。
Tips:arr[i] == *(arr+i) == *(i+arr) == i[arr] 虽然C语言支持这么写,但不建议这么写。不够直观,干扰理解。
指针数组
指针数组是指针还是数组?
我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是存放指针的数组。
指针数组的每个元素都是⽤来存放地址(指针)的。 指针数组的每个元素是地址,⼜可以指向⼀块区域。
数组指针
之前我们学习了指针数组,指针数组是一种特殊的数组,存储的每一个元素都是地址(指针)。
那么数组指针是指针变量还是数组呢?
答案是:指针变量。一种指向数组的指针变量。
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
int *p1[10];
int (*p2)[10];
问:p1、p2分别是什么?
数组指针变量:int (*p)[10];
指针数组:int *p[10];
解释:p先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是 ⼀个指针,指向⼀个数组,叫 数组指针。
这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
初始化:数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名。
int arr[10] = {0};
&arr;//得到的就是数组的地址
那么初始化就是:int(*p)[10] = &arr;