指针理解A部分

目录

目录

1.指针是什么?

2.取地址符的使用

3.指针的使用

        3.1指针的定义

        3.2指针的解引用

        3.3指针的运算

3.3.1指针 +- 整数

3.3.2指针- 指针

3.3.3指针的关系运算

4.空指针

5.野指针

        5.1野指针成因

5.2如何规避野指针

6.assert断言

7.const修饰指针

        7.1const修饰变量

        7.2const修饰指针

7.2.1const放在最左边

7.2.2const放在 int 和 * 中间

7.2.3const放在 * 和 p 中间


1.指针是什么?

        顾名思义,指针就像一根针一样,作为c语言中不可或缺的一部分,它指向的对象是一个空间。而指针本质其实是一个储存地址的变量,主要和电脑的内存打交道。

        和其它所有的变量一样,在计算机中,指针也有它自己的内存空间,它的所占内存主要是由计算机决定的,计算机有32位机器和64位机器,32位代表着32根地址线,每根线可以输出0和1两种信息,那么32根线就可以组成2^32种情况,同理,64根线就可以组成2^64种情况。在32位条件下,指针就占32个位,也就是4个字节;在64位条件下,一个指针就占8个字节。

       在VS环境下面,X86表示的是32位环境,X64表示的是64位环境,我们也可以通过sizeof来测试这样的结果:

X86(32位环境下)                                               X64(64位环境下)

很明显,指针变量和其他普通变量不同的是:指针变量的类型并不能改变一个指针所占的字节空间大小,只有操作系统会决定一个指针变量的大小

2.取地址符的使用

        我们要学会使用指针,我们就要结合我们之前学过的知识,在指针这部分,类比推理是一个非常好用的一个方法,用好了可以让你子啊指针这部分“驰骋沙场”。

        在这之前,我们要了解一个操作符——取地址操作符(&)。听起来可能有点陌生,其实我们在使用函数scanf时,我们就经常接触到这个操作符。我们要使用scanf输入一个变量时,我们要输入的参数除了变量名,还要在变量名前面加上这个取地址操作符。比如下面这样:

int a = 0 ;//创建变量并初始化
scanf("%d",&a);//读取输入值

        取地址操作符,简单的说,就是为创建的一个变量开辟一个空间。同样,我们也可以通过取地址符来打印一个变量的地址,使用%p作为占位符,比如下面这样:

int a = 10;
printf("%p", &a);

        在这里我们可以试一试输出的结果:

这样,我们就成功输出了一个变量的地址。

3.指针的使用

        3.1指针的定义

        使用指针,我们首先要知道怎么定义一个指针,我们知道,指针是一个指向地址的变量,指针里面储存的应该是一个地址,而获取一个地址又要使用取地址(&)操作符。我们就要这样:

int a = 10;//定义一个整形变量a
int * p = &a;//使用取地址操作符取出变量a的地址,然后把它赋值给一个指针p

        这样我们就定义了一个指针,并在里面存入了一个整形变量a的地址,这个时候我们可以使用类比推理法来理解这个指针。

        这样,通过类比推理,我们可以知道定义一个指针变量是怎么构成的。

        3.2指针的解引用

        每一个变量都会有自己专门的职责,而一个指针变量的职责就是储存一个地址。我们得到一个变量地址,我们就应该获得对这个变量在内存中访问的权限。而在指针里,通过指针里面的地址读取相应的变量的操作叫做“解引用”。

        要使用解引用这个动作,我们就要使用一个操作符——解引用操作符(*),在指针变量p前面加上一个*,我们就可以通过这个指针访问到储存在指针p指向的那片内存中的值。

int a = 10 ;//定一个整形变量a
int  * p = &a ;//定义一个指向变量a的指针p
printf("%d",*p);//通过解引用操作符*加上指针,我们可以得到指针p指向的a里面的值
//我们可以试着打印出来这个值

打印一下就是下面的情况

可以看到,我们就成功通过解引用操作符和指针相结合,打印出了a的值。

        同时我们也可以想到:既然一个指针p里面存放的是一个变量的地址,我们使用解引用操作符就可以通过地址读取这个地址里面存放的值,那么我们也可以推断出

p == &a;
*p == a;
*p == *&a ==a;

这样我们知道,既然*&a==a,所以解引用操作符 * 和取地址操作符 & 是两个可以相互抵消的操作,同样我们也可以使用printf函数打印一下   *&a  的结果:

我们都得到了储存在a里面的值10,所以我们可以成功推断出解引用操作符和取地址操作符&是两个相互抵消的操作符这个结论,而这个结论对于我们理解后面指针更加复杂的知识时,会有很大的帮助。

        3.3指针的运算

       上面我们了解过,与普通变量不一样的是,指针的大小和指针的类型是无关的,那么有人可能会疑惑:既然连空间都一样,我们费心思去把指针分类干什么呢?直接把全部指针定义成一种类型不就好了。

        很显然,c语言的发明者这么设计肯定不是为了好玩,能用更加方便的方法肯定要用啊,指针有它们自己的类型是有原因和作用的。那就是我们接下来要讲的内容——指针的运算。

        指针运算有三种形式,分别是:

                

3.3.1指针 +- 整数

        首先我们还是要明确:一个指针储存的其实是一个别的变量的地址,我们指针的类型就是我们指针指向的那个变量的类型,指针加减一个整数其实就是对一个地址进行加减,我们地址加减1对应地址变化的字节数的大小是多少,就取决于这个指针的类型是什么。

        为了方便理解,我们这里引入数组的知识,我们知道数组是连续存放的一系列数据,它们的相邻元素的地址其实就是数组对应的类型,这里我们举一个栗子:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//定义一个数组,数组里面每一个元素的类型都是int类型

我们可以定义一个指针p,指向第一个元素arr [ 0 ] ,指针储存的是arr [ 0 ] 的地址,指针的类型就是int * :

int* p = &arr[0];

这样我们就获得了一个数组的首元素的地址,我们还要获得后面的元素的地址,就可以使用指针加减整数就可以得到结果:

    printf("&arr[0] = %p\n", p);//获得数组第一个元素的地址并打印
    printf("&arr[1] = %p\n", p+1);//获得数组第二个元素的地址并打印

试试结果:我们在x86(32位环境)下运行可以看到,两个相邻地址相差了4个字节,正好是一个 int  的大小

我们还可以改变一下指针和数组的类型:

在x86(32位环境)下运行,刚好是一个char 的大小

        从上面的经验可以知道,一个指针加减一个整数变化的值取决于这个指针的类型。当然,在这里我们得到的是一个地址,通过前面的学习我们可以知道,我们可以使用一个解引用操作符 * 来获得一个地址里面的存储的值。

        我们还是使用int类型的数组和 int * 的指针来操作,当然,我们可以使用无序数组避免偶然性

我们可以看到,我们的确使用了解引用操作符读取并打印了对应的值。

3.3.2指针- 指针

        前面了解过,在指针这部分,类比推理法是一个可以帮助你理解指针的一个很好的方法,除了使用我们学过的c语言知识,我们还可以使用我们现实的例子类比推理。

        我们要理解指针-指针,比如,我们就可以从日期入手,日期减日期,比如1月11日减1月9日,我们得到的其实是它们中间相差的天数2天,而不再是一个日期。同理,我们可以知道其实一个指针减去另一个指针,其实质就是一个内存中的地址减去另外一个地址,得到的结果是地址的差值而不是地址,也不是一个指针。

        这个其实和strlen函数的实现原理很类似,通过最后字符串最后的一个地址减去第一个地址,我们就得到了这个字符串的长度

        比如我们创建一个字符串数组str,里面写入内容为abcdefg,像下面这样:

我们可以在“调试”里面找到“窗口”再找到“监视”,输入数组名str,等到调试光标走到创建完str数组之后我,我们可以看到str这个字符串数组里面的内容:

我们可以看到,我们的字符串数组后面默认自动加入了一个‘\0’元素,这个其实就是字符串的结束标志。我们想要获取的是我们输入的字符串“abcdefg”的字符串长度,并不需要加上这个‘\0’,所以我们其实可以通过得到这个‘\0’的地址,然后减去‘a’的地址,我们就可以得到我们的字符串的长度,按照这个原理,我们就可以制作出我们自己的strlen函数:

#include <stdio.h>
int mystrlen(char str[])//参数为字符串数组
{
    char* s = &str[0];//定义指针s接收首字符地址
    char* p = s;//定义指针p接受首字符地址,用于通过变换得到终止符'\0'的地址
    while (*p != '\0')
    {
        p++;//循环让指针p加一,直到p得到终止符的地址
    }
    return p - s;//返回终止字符和首字符地址之差
}
int main()
{
    char str[] = "abcdefg";//定义字符串函数
    int ret = mystrlen(str);//定义变量ret接受mystrlen函数返回值
    printf("%d", ret);//打印返回值ret
    return 0;
}

我们可以试一下这个函数的效果:

得到结果为7,完全正确,很不错

3.3.3指针的关系运算

        指针关系可以使用 > 和 < 比较。在这之前,要补充一个很常用的知识:在数组名字单独拿出来时,一般代表的其实就是整个数组第一个元素的地址,也就是arr==&arr[ 0 ] ,后面我们也会对这部分深入了解的,现在只是稍微了解一下。从某种程度上,数组也算是一种指针了。

        在之前,我们知道我们打印一个数组里面的全部元素,一般都是使用for循环,还要再定义一个变量i=0,这样我们才可以打印一个数组里面的全部元素,其实,我们也可以使用一个指针来代替这个操作。比如下面的代码,我们就是使用了这个原理:

#include <stdio.h>

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;//等价于int * p = & arr[0];
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (p < arr + sz)
    {
        printf("%d  ", *p);
        p++;
    }
    return 0;
}

这里可以看一下结果 

可以看到和我们之前使用for循环时一样的,这样做有什么好处呢?可以让我们写代码的时候思维和方式更加灵活。

4.空指针

        空指针,就是一个没有固定指向类型的指针。

        指针可以指向很多种类型,可以是int*,short*,char*,float*,double*...,空指针不属于这里面的任何一个,空指针其实就是一个类型为 void* 的指针

        空指针有什么用呢?

        我们知道,我们如果定义了一个int型的变量,我们可以使用int*类型的指针来指向它,但是我们如果改成别的类型的指针呢?比如我们定义一个char*的指针指向这个变量会怎么样呢?

多说无益,我们在这里使用一个代码来实验一下:

可以看到我们的VS编译器报错了,也许,我是说也许,这个代码在其他的编译器或许编的过去,但是这样的写法绝对不是一个正确的写法,这要是你去实习写出这样的代码转正估计是无望了。

        但是我们的空指针就可以接受这种代码,我们可以使用void*类型的指针接收任何类型的指针,都不会报错,但是与此同时,我们也会失去对指针操作的权限,比如使用转义字符把a里面的内容通过指针打印出来啊,或者是使用*p把它换成其他变量的地址啊,都不可以,编译器都会报错。

        所以,空指针的真正用途是什么呢?其实,在c语言里面,我们使用空指针主要是用在函数的参数部分负责传参,后面我们也会慢慢认识到。

5.野指针

        野指针,顾名思义,就是一个随机的没有确定指向位置的指针,它指向的位置是不可知的,没有明确的限制。

        5.1野指针成因

野指针成因主要有三种:

  1. 指针未初始化
  2. 指针越界访问
  3. 指针指向的空间释放

1)指针未初始化

        一般情况下我们定义了一个指针就要在里面放入一个变量的地址,不然我们这个指针就会变成一个空指针,比如下面这个就是很经典的由于指针未初始化导致的野指针:

#include <stdio.h>
int main()
{
    int  *p;
    return 0;
}

        这种行为是非常不对的,很明显,你很不负责地创建了一个指针,但是你却对它不管不顾,就这样任凭它自生自灭。创建了一个野指针就像你买了一个小狗宝宝,但是你把它扔到大街上,随着日子慢慢过去,这个狗宝宝就变成了一个可能会咬人的野狗,这个是非常危险的一件事情。所以,如果你决定要创建一个指针,就给它一个地址,不要让它流浪,好吗?

2)指针越界访问

        我们在认识数组的时候,我们就知道,数组的空间其实就是紧密排列的一片数据,它们在内存中的地址其实是连续的。这么一说,我们就可以定义一个指针指向第一个元素的地址,然后随着我们做完我们想要做的操作,再把这个指针加一,得到数组下一个元素的地址。

        比如,我们想要打印一个数组里面的全部元素,我们就可以使用这样的操作,为了更加好描述,我们使用一个变量 i 来进行for循环,像下面这样:

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int i = 0;
    int* p = &arr[0];
    for (i = 0; i < 11; i++)
    {
        printf("%d", *p);
        p++;
    }
    return 0;
}

        我们发现,for循环停止的条件是i < 11,这么一说,我们的循环其实就是循环了11次,第一次是打印数组的第一个元素,第二次是第二个元素...第十次打印的就是数组的第10个元素了,在这个动作之后,我们的循环还要进行一次,现在我们的指针指向的是数组第十个元素的地址,我们还要再进行一次循环,我们在第十一次循环时,我们的指针p指向的就不是数组里面的元素地址了,而是数组第十个元素的地址+1得到的地址,那个地方是一个未知的领域...我们来看看输出结果:

        很明显,在打印完全部我们的数组里面的数之后,我们还打印了一个数,这个数是我们没有存入的数据,所以它打印出来的是一个未知的随机数。这个时候,我们就应该意识到,我们在第11次循环时,我们使用的是一个由于指针越界访问产生的野指针,它指向了一个未知的区域,给我们打印了一个未知的随机数。

3)指针指向的空间释放

        在一个函数中,我们一般定义的形参其实是一个临时变量,它会在调用函数并进入函数时产生,然后在出了这个函数时销毁,比如下面的代码:

#include <stdio.h>

int* test()//返回的值是一个地址,所以这里我们使用一个int*用来接收地址
{
    int n = 100;//创建变量n,在内存中开辟了一块空间用来储存这个形参
    return &n;//返回的是n的地址
}//在这里其实我们的n就已经销毁了
int main()
{
    int* p = test();//这里的意思是创建一个int*类型的指针变量p用来接收test函数的返回值
    printf("%d", *p);//这里意思是想要通过p找到n所在的地址,从而使用解引用操作符打印n的值
    return 0;
}

但是我们这个时候我们如果直接运行代码,会出现下面的结果:

        事情并没有像我们所预料的那样出现打印错误,p还是得到了n的地址并将它准确地打印了出来,这是因为我们在函数结束后,虽然n变量被销毁了,但是我们的p仍然获取到了n的地址并这个地址进行了访问,这个时候这个地址上n销毁前储存的值没有被及时销毁掉,所以我们依然准确打印出来了n在函数里面的值。

        那么要怎么验证我们的结论呢?其实很简单,只要在p得到n的地址之后加一个任何处理步骤就可以了,这样在外面计算机处理这个我们额外增加的步骤的时候,同时处于n销毁之前创建的地址上的数据100就会被覆盖,变成一个随机值,比如,我们可以在这句代码后面立刻加上一句printf(“hhhh\n”);这样我们就给计算机足够的时间去覆盖n的地址上之前写入的值。像下面这样:

这样我们就没有办法再得到n原来的值了。这个时候,我们的指针p其实就成为了一种意义上的野指针。

5.2如何规避野指针

        针对上面的几种可能会造成野指针的情况,我们可以做出下面几种措施

1)对指针及时初始化(如果不知道指针指向哪里,就赋给它一个NULL)

2)小心指针越界

3)避免返回局部变量的地址存入指针中

4)及时把不会再使用的指针置为NULL

6.assert断言

        arrest.h头文件里面定义了宏arrest(),⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错并终⽌运⾏。这个宏常常被称为“断⾔”。 

arrest(p!=NULL);

        比如这段代码 就是在程序运行到这里时,判断p是否为NULL。如果确实不等于NULL,程序就会继续运行,否则就会报错并且终止运行。这个就是arrest断言的基本实现形式。

        这里是更完整的介绍:assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的⽂件名和行号。

        同时我们有几种方式可以选择使用或者关闭arrest()机制:

        1)我们可以使用在引用#include <arrest.h> 之前使用宏定义一个DEBUG,这样我们的arrest就不会运行了;

#define DEBUG
#include <arrest.h>

        2)我们还可以将发布版本从Debug改成Release,在Release版本中选择禁用arrest就可以。在Rlease版本里面就是相当于直接优化了代码,在Debug版本下可以帮助我们排查问题,在Release版本下就不会影响用户使用程序的效率

7.const修饰指针

        7.1const修饰变量

        在我们了解const修饰指针变量前,我们可以先了解一下const修饰其他变量的时候的作用。

        我们知道:变量是可以修改的,我们在正常情况下定义一个变量,我们可以很简单的就改变储存在这个变量里面的值,但是我们使用const修饰一个变量时,这个变量就会变成不可修改的变量。这里我们可以做一个对比

我们可以看到,m没有使用const修饰,我们就成功修改了m里面的值,但是我们的n被const修饰;了,就发生了报错:

可以知道,我们如果给一个变量在定义的时候用const修饰,我们就获得了一个“常量”,也就是说,我们使用const可以让一个变量转变成一个不可修改的“定量”。

        但是,在有一些编译器里,我们仍然可以绕过“定量”n,使用指针获得n的地址把n的值更改,比如在devc++就可以这样做:

        但是这样我们的const修饰就没有const原来的意义了,所以我们就可以想到使用const让我们的值无法通过地址更改,简单说就是无法更改这个指针,这就是我们接下来要说的使用const修饰指针。

        7.2const修饰指针

        const修饰一般发生在定义一个变量的时候,我们就可以再定义我们指针变量时使用const修饰指针。但是,在这个时候,问题来了,我们可以在很多地方放const修饰,那么我们放在不同的地方都有什么不同作用呢?

        这个时候我们就可以进行分类讨论不同的情况:

7.2.1const放在最左边

更改p中储存的地址:

运行一下:

成功更改

再换一下直接更改*p:

报错

小结:const放在最左边时,修饰的主体是*p,你无法更改*p但是你仍然可以通过p来更改地址。

7.2.2const放在 int 和 * 中间

更改p中储存的地址:

成功修改

来看看更改一下*p中储存的值:

报错

小结:const放在int和*中间时和放在最左边的时候一样,可以通过p更改地址,但是不能通过*p更改内容

7.2.3const放在 * 和 p 中间

修改地址:

报错

再看一下修改*p里面储存的值

成功修改

小结:和上面的两种正好相反,这里只能修改*p里面储存的值,但是不能修改地址

总结:

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。

const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

通过图示表示一下:

———————————————————————————————————————————

指针的A部分暂时就写到这里,如果对你有作用的话点个赞再走喔~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值