0


【C语言航路】第六站:指针初阶

一、指针是什么

这部分内容,在我们之前的文章中已经提及过

链接:【C语言航路】第一站:(初识C语言(终幕)

这里在简单的回忆一下

学习指针必须先要理解——内存,内存是电脑上的存储设备,一般都是4G/8G/16G等,程序运行的时候会加载到内存中,也会使用内存空间

我们将内存划分为一个个小格子,每一个格子是一个内存单元,也正好是一个字节的大小,对每一个内存单元进行编号,在生活中我们也将这一个个编号称作地址,而地址在c语言中又叫做指针

我们举一个例子,假设我们定义一个变量 int a=10;那么a是一个int类型的变量,需要占用四个字节的空间,而每个字节都有地址,&a取出的是哪一个的地址呢?其实取出的是第一个字节的地址(较小的地址),也就是说,下图中,&a最终取出来的地址是0x0012ff40 ,而这个地址我们可以存放到一个变量中,int* pa=&a,这颗*代表pa是一个指针,int代表pa所指向的类型是int类型,这个pa也叫做指针变量。

理解指针需要理解两个要点

1.指针是内存中一个最小单元的编号,也就是地址

2.平时口头语中所说的指针,通常指的是变量,是用来存放内存地址的变量

总结:指针就是地址,平时口头语中的指针通常指的是指针变量

我们看这段代码

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    printf("%p\n", &a);
    printf("%p\n", p);
    return 0;
}

运行结果为,打印出来的地址是一样的

我们还可以看这段代码,使用*来解引用指针

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    printf("%p\n", &a);
    printf("%p\n", p);
    *p = 20;
    printf("%d", a);
    return 0;
}

运行结果为

总结:指针变量,就是用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

那这里的问题是: 一个小的单元到底是多大? 其实是1个字节

那么如何编址呢?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电 平(低电压)就是(1或者0)

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

.....

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。 每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB == 2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)

4G的空间进行编址。 同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,我们也可以计算出来

这里我们就知道了:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。

总结: 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。 指针的大小在32位平台是4个字节,在64位平台是8个字节

二、指针和指针类型

1.指针类型的意义

我们在上面了解了

32位机器上,地址是4个字节,指针变量的大小也是4个字节

64位机器上,地址是8个字节,指针变量的大小也是8个字节

那在这里就有人产生困惑了,反正都是4个或者8个字节,那为什么要区分int,char.....这些呢?为什么不直接弄一个通用指针呢?

其实既然计算机没有这个通用指针,那就说明这些类型是有意义的。我们现在就来探讨以下这些指针的类型的意义。

为了了解这个内容,我们先看这个代码

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

我们打开调试,窗口,内存,并将列改为4

继续往下走

我们发现,四个字节全部被改为0

我们在看这一段代码

#include<stdio.h>
int main()
{
    int a = 0x11223344;
    //int* pa = &a;
    char* pc = &a;
    *pc = 0;
    return 0;
}

我们仍然监视内存

继续往下走

我们发现只改变了一个字节。

所以我们得出结论:指针类型是有意义的

指针类型决定指针进行解引用时候,一次访问几个字节(访问权限)

int访问四个字节 char访问一个字节,float*访问四个字节

我们在继续看这个代码

#include<stdio.h>
int main()
{
    int a = 0x11223344;
    int* pa = &a;
    char* pc = &a;
    printf("%p\n", pa);
    printf("%p\n", pa+1);
    printf("%p\n", pc);
    printf("%p\n", pc+1);

    return 0;
}

运行结果为

由此我们发现,pa+1跳过了四个字节,pc+1跳过了一个字节

所以我们得出

指针类型决定指针的步长(指针+1到底跳过几个字节)

字符指针+1,跳过一个字节

整型指针+1,跳过四个字节

我们可以看这个代码

#include<stdio.h>
int main()
{
    int a = 0x11223344;
    char* pc = (char*)&a;
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        *pc = 0;
        pc++;
    }
    return 0;
}

我们先取出a的地址放到一个char类型的指针中,因为a取地址后是一个int类型的指针,所以要强制类型转换。对其进行遍历四次,每次只能改一个字节的空间,四次刚好将a改为0。

我们在这里在总结一下指针类型的意义:

1.

指针类型决定指针进行解引用时候,一次访问几个字节(访问权限)

int访问四个字节 char访问一个字节,float*访问四个字节

2.

指针类型决定指针的步长(指针+1到底跳过几个字节)

字符指针+1,跳过一个字节

整型指针+1,跳过四个字节

2.指针+-整数

我们在上面说过,指针的不同类型,其实提供了不同的视角去观看和访问内存

char*一次访问1个字节,+1跳过一个字节

int *一次访问4个字节,+1跳过四个字节

当然在这里,除了+1,还可以-5,+2等操作

比如int* pa=10;

pa+4 其实就是向后走4*sizeof(int)个字节,也就是+16个字节

pa-5 其实就是向前走5*sizeof(int)个字节,也就是-20个字节

也就是指针类型决定了指针向前或向后走一步有多大(距离)

3.指针解引用

这里也同样在前面说过了

指针的类型决定了对指针解引用时有多大权限(能操作几个字节)

比如char指针解引用一次就能操作一个字节,而int指针解引用一次可以操作4个字节

三、野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

1.野指针的成因

(1)指针未初始化

#include <stdio.h>
int main()
{
    int* p;//局部变量指针未初始化,默认为随机值
    *p = 20;
    return 0;
}

如上代码所示,指针是一个局部变量未初始化,默认未随机值,这样随意进行修改指针的值是非常危险的行为。

(2)指针越界访问

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int* p = arr;
    int i = 0;
    for (i = 0; i <= 11; i++)
    {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
    }
    return 0;
}

如上代码所示,指针的指向范围超出了数组arr的范围,p就是一个野指针,此时如果随意修改指针里的内容是很危险的,假如这个野指针指向的恰好就是本程序已有的其他值,那么就会出现问题。

(3)指针指向的空间释放

我们看这个代码

#include<stdio.h>
int* test()
{
    int a = 10;
    return &a;
}
int main()
{
    int* p = test();
    printf("%d", *p);
    return 0;
}

这个代码也是,a是一个局部变量,返回的时候a已经被销毁了,此时的这个地址就是一个野指针。

当然我们可以运行一下,我们会发现仍然是10,这是因为之前的函数栈帧还没有被破坏掉。我们加上个代码就能破坏掉这个函数栈帧,导致结果不一样

2.如何规避野指针

(1) 指针初始化

#include<stdio.h>
int main()
{
    int a = 0;
    int* pa = &a;//指针的初始化
    int* pc = NULL;//空指针,专门用来初始化指针
    return 0;
}

如上代码所示,如果不知道该初始化成什么,可以初始化成NULL,当然要注意,空指针不可以解引用。解引用空指针会使程序崩溃。因为0地址处是不能让用户使用的

(2) 小心指针越界

(3) 指针指向空间释放,及时置NULL

(4) 避免返回局部变量的地址

(5) 指针使用之前检查有效性

四、指针运算

指针的运算共有三种

1.指针+- 整数

2.指针-指针

3.指针的关系运算

1.指针+-整数

这快的内容在前文中已经涉及过,这里在简单的讲解一个案例

#define N_VALUES 5
#include<stdio.h>
int main()
{
    float values[N_VALUES];
    float* vp;
    //指针+-整数;指针的关系运算
    for (vp = &values[0]; vp < &values[N_VALUES];)
    {
        *vp++ = 0;
    }
    return 0;
}

这段代码唯一需要注意的就是一个表达式*vp++。++的优先级比较高,但是由于他是前置++,所以先解引用vp,然后vp++。最终的效果就是将这个数组置零。

2.指针-指针

指针-指针有个前提

两个指针要指向同一个空间,并且两个指针类型相同

指针-指针的绝对值是两个指针之间的元素个数

我们看这个代码

#include<stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%d", &arr[9] - &arr[0]);
    return 0;
}

运行结果为

我们之前讲解过自己实现一个求字符串长度的解法,一种是计数器,另外一种是递归的思想,今天我们在采用一种指针-指针的方法,使用\0处的指针-起始点

代码如下

#include<stdio.h>
int my_strlen(char* arr)
{
    int* str = arr;
    while (*arr != '\0')
    {
        arr++;
    }
    return arr - str;
}
int main()
{
    char arr[] = "abcdef";
    int len = my_strlen(arr);
    printf("%d", len);
    return 0;
}

当然,我们也可以将*,和++进行合并

#include<stdio.h>
int my_strlen(char* arr)
{
    int* str = arr;
    while (*arr++ != '\0')
        ;
    return arr - str - 1;
}
int main()
{
    char arr[] = "abcdef";
    int len = my_strlen(arr);
    printf("%d", len);
    return 0;
}

3.指针的关系运算

#define N_VALUES 5
#include<stdio.h>
int main()
{
    float values[N_VALUES];
    float* vp;
    for (vp = &values[N_VALUES]; vp > &values[0];)
    {
        *--vp = 0;
    }
}

这段代码与前面一段代码很相似,功能就是将数组置零。这个是先--vp然后在解引用,最终vp停留的位置就是数组元素的起点

我们在看一下这段代码

#define N_VALUES 5
#include<stdio.h>
int main()
{
    float values[N_VALUES];
    float* vp;
    for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
    {
        *vp = 0;
    }
}

这段代码,是将上面的*和--混用的部分代码给拆开写了。这样写确实可以实现我们的功能,但是要注意的是,第二种写法在绝大多数编译器是没有问题的,但是然而我们还是应该避免这样写,因为标准并不保证它可行。在少部分的编译器上还是会出问题的。

因为这个段代码最后会出现数组首元素的前一个地址与这个数组首元素地址进行比较。

而我们的标准是这样规定的:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

五、 指针和数组

1.指针和数组是不同的对象

指针是一种变量,存放地址的,大小4/8字节的

数组是一组相同类型元素的集合,是可以放多个元素的,大小是取决于元素个数和元素类型的

2.数组的数组名是数组首元素的地址,地址是可以放在指针变量中

可以通过指针访问数组

比如下面的代码就可以实现指针访问数组

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

或者也可以这样写

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

    }
    return 0;
}

还有一点需要说明的是

我们在这里出现了

int arr[10];

int* p=arr;

这里说明arr是一个首元素地址

而地址就可以解引用。所以我们知道arr[i]--->*(arr+i)

而加法是满足交换律的,所以进而推出*(i+arr)

进而推出 i[arr]

事实上,这个确实是没有问题的,可以正常使用的,因为 [ ]他只是一个操作符,i和arr是这个操作符的操作数而已。在我们电脑里面arr[i]也会被翻译成*(arr+i)。

六、二级指针

1.什么是二级指针?

我们在之前说过的指针是这样的int* pa=&a;也就是将a的地址放入pa中,pa的类型是int*。

那么pa这个指针其实应该也有一个地址,我们如果取出他的地址,将他放入另外一个变量,这就叫做二级指针。

也就是int*ppa=&pa。其中第二颗代表着他是一个指针,前面的int代表着他指向的类型是一个int类型的数据

如下代码所示

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    return 0;
}

如下图示关系

2.二级指针的解引用

对于二级指针的解引用需要两颗*

有如下等式成立

**ppa==*pa=a

举个例子

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    printf("%d\n", **ppa);
    **ppa = 50;
    printf("%d\n", **ppa);
    return 0;
}

运行结果为

七、指针数组

1.指针数组的概念

在这里,我们首先需要了解指针数组是数组还是指针呢?

其实,是数组,从语言的角度来思考,指针是修饰词,数组才是主语

比如字符数组,他是一个数组,里面存放的都是字符

比如整型数组,他是一个数组,里面存放的都是整型

所以我们便能猜测到,指针数组,他是一个数组,里面存放的都是指针

我们举一个例子

#include<stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;
    int e = 50;
    int* arr[5] = { &a,&b,&c,&d,&e };
    int i = 0;
    for (i - 0; i < 5; i++)
    {
        printf("%d ", *(arr[i]));
    }
    return 0;
}

运行结果为

图解为

2.尝试模拟一个二维数组

我们了解了指针数组的概念以后,我们可以利用其模拟一个二维数组,假设我们要模拟三行四列的二维数组

我们是这样想的,先定义出三个一维数组a,b,c。然后定义一个指针数组,令指针数组的每个元素是这些一维数组的首元素地址。然后我们就可以通过两次遍历就能模拟出来这个二维数组了

代码如下

#include<stdio.h>
int main()
{
    int a[] = { 1,2,3,4 };
    int b[] = { 2,3,4,5 };
    int c[] = { 3,4,5,6 };
    int* arr[] = { a,b,c };
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 4; j++)
        {
            printf("%d ", *(arr[i]+j));
        }
        printf("\n");
    }
    return 0;
}

运行结果为

当然,如果我们将*(arr[i]+j)改为arr[i][j]也是正确的,因为这两种是可以相互转换的


总结

本节课我们主要详细讲解了指针与内存对于他们理解,指针和指针类型,指针类型的意义,野指针的成因,以及如何规避野指针,指针的三种运算,指针与数组的关系,二级指针,以及指针数组

如果对你有帮助的话,不要忘记点赞加收藏哦!!!


本文转载自: https://blog.csdn.net/jhdhdhehej/article/details/128060648
版权归原作者 青色_忘川 所有, 如有侵权,请联系我们删除。

“【C语言航路】第六站:指针初阶”的评论:

还没有评论