C语言的指针入门

来吧,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;  

 

 

  • 9
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值