• C语言初阶-指针详解-庖丁解牛篇



    在学习C语言的过程中,你是不是在指针这一章之前学的都还不错,而到了指针这一章就开始有点懵了呢,而指针又非常非常的重要,这一章搞不懂就很影响后面的学习,在进行C语言进阶的时候也就不会得心应手,我在学习的时候就是这样,现在我对于学习C语言时遇到的问题进行总结,希望能够对你有所帮助。

    1. 指针是什么?

    指针理解的2个要点:

    1. 指针是**内存中一个最小单元的编号**,也就是地址
    2. 平时口语中说的指针,通常指的是**指针变量**,是用来存放内存地址的变量

    总结:🔑指针就是地址,口语中所说的指针通常指的是指针变量。

    我们也能这样理解:

    首先我们先了解一下什么是内存,我们在买电脑的时候都会听到说这个内存是8G的,这个内存是16G的。内存顾名思义就是存放数据的,这个没什么好说的,下面我们说一说这么大的内存空间是如何管理的?

    内存空间的管理:
    切割成内存单元 ——1byte(字节)。
    在这里插入图片描述
    上面这个图基本就说明了第一层意思:
    指针是**内存中一个最小单元的编号**,也就是地址

    #include <stdio.h>
    int main()
    {
    int a = 10;  //a就是一个整型变量,占用四个字节
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    在这里插入图片描述
    从上面的例子可以看到,一个整型的数字10占用4个字节,而地址就是首字节的地址。

    指针变量
    我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量。

    #include <stdio.h>
    int main()
    {
    	int a = 10;//在内存中开辟一块空间
    	int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
    	   //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
    	   //中,p就是一个之指针变量。
    		return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    指针本质上就是地址
    口语中所说的指针,其实是指针变量,指针变量就是一个变量,指针变量是**用来存放地址**的一个变量

    小扩展:在计算机中这些地址的编号是不用存起来的,他们是由硬件生成的,直接访问用就好了。

    总结🔑:

    指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
    那这里的问题是:
    一个小的单元到底是多大?(1个字节)
    如何编址?
    经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
    对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电
    平(低电压)就是(1或者0);
    那么32根地址线产生的地址就会是:
    00000000 00000000 00000000 00000000
    00000000 00000000 00000000 00000001

    11111111 11111111 11111111 11111111

    这里就有2的32次方个地址。
    每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
    232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空闲进行编址。
    那64位机器也是同理。

    这里我们就明白:
    在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以
    一个指针变量的大小就应该是4个字节。
    那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地
    址。
    总结:
    指针变量是用来存放地址的,地址是唯一标示一块地址空间的。
    指针的大小在32位平台是4个字节,在64位平台是8个字节。

    2. 指针和指针类型

    这里我们在讨论一下:指针的类型
    我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
    准确的说:有的。
    当有这样的代码:

    int num = 10;
    p = &num;
    
    • 1
    • 2

    要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?
    我们给指针变量相应的类型。

    char  *pc = NULL;
    int  *pi = NULL;
    short *ps = NULL;
    long  *pl = NULL;
    float *pf = NULL;
    double *pd = NULL;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里可以看到,指针的定义方式是: type + * 。
    其实:
    char 类型的指针是为了存放 char 类型变量的地址。
    short
    类型的指针是为了存放 short 类型变量的地址。
    int* 类型的指针是为了存放 int 类型变量的地址。**
    那指针类型的意义是什么?
    下面我们看一下不同的指针变量具体占多少个字节呢?

    #include <stdio.h>
    int main()
    {
    	char* pc = NULL;
    	short* ps = NULL;
    	int* pi = NULL;
    	double* pd = NULL;
    
    	printf("%u\n", sizeof(pc));
    	printf("%u\n", sizeof(ps));
    	printf("%u\n", sizeof(pc));
    	printf("%u\n", sizeof(pd));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    我们可以看到打印出来的都是8个字节,因为我的电脑是64位的,有64根地址线,能够产生64个0或1,所以需要8个字节来存储(一个字节能够存储8个数字,也就是一个字节存储8个二进制序列)。
    如果你的电脑是32位的,那么就应该打印的是4个字节,同理32个二进制序列需要4个字节来存储。

    我们思考一个问题:不同类型的指针变量所占储存都是一样的,那么分类型还有什么用呢?

    //指针类型的意义
    // 0 1 2 3 1 4 5 6 7 8 9 a b c d e f     十六进制的数字
    #include <stdio.h>
    int main()
    {
    	int a = 0x11223344;    //这个十六进制数组存储需要4个字节
    	int* pa = &a;
    	*pa = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    从这个例子我们就能看出,int*这个指针变量能操作4个字节。

    //指针类型的意义
    // 0 1 2 3 1 4 5 6 7 8 9 a b c d e f     十六进制的数字
    #include <stdio.h>
    int main()
    {
    	int a = 0x11223344;    //这个十六进制数组存储需要4个字节
    	char* pa = &a;
    	*pa = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    现在我们吧指针类型改成了char*,可以看到现在只能操作一个字节了。

    总结🔑:
    指针变量的类型决定了指针被解引用操作时能够访问几个字节
    整型指针就只能访问4个字节,
    字符型指针就只能访问1个字节。
    推广到其他类型。

    2.1 指针 + - 整数

    下面我看来看一个代码

    #include <stdio.h>
    int main()
    {
    	int a = 0x11223344;
    	int* pa = &a;
    	char* pc = &a;
    
    	printf("pa = %p\n", pa);
    	printf("pa = %p\n", pa+1);
    
    
    	printf("pc = %p\n", pc);
    	printf("pc = %p\n", pc+1);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    从上面的例子我们可以看到int型指针+1跳过了4个地址,也就是跳过了4个字节,而char型指针+1跳过了1个字节。

    总结🔑:
    指针的类型决定了指针+1或-1操作的时候,跳过几个字节。
    决定了指针的步长。

    2.2 指针的解引用

    #include <stdio.h>
    int main()
    {
    int n = 0x11223344;
    char *pc = (char *)&n;
    int *pi = &n;
    *pc = 0;  //重点在调试的过程中观察内存的变化。
    *pi = 0;  //重点在调试的过程中观察内存的变化。
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    总结🔑:
    指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
    比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

    3. 野指针

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

    3.1 野指针成因

    1. 指针未初始化
    #include <stdio.h>
    int main()
    {
    int *p;//局部变量指针未初始化,默认为随机值
            //一个局部变量不初始化放的就是随机值
      *p = 20;     //非法访问内存了,这里的p就是野指针
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 指针越界访问
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 指针指向的空间释放
    #include <stdio.h>
    
    int* test()
    {
    	int a = 0;
    	return &a;
    }
    
    int mian()
    {
    	int* p = test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们可以看到这个代码,这个函数在掉用结束后,空间就被释放了,但是主函数里的指针还记得那个地址,之后在使用的时候就变成了野指针。

    3.2 如何规避野指针

    1. 指针初始化
    int a = 0;
    int* p = &a;
    
    • 1
    • 2
    1. 小心指针越界
    2. 指针指向空间释放即使置NULL
    if(p != NULL)       //这样就能有效防止野指针的生成
    {
    *p = 100;       
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 避免返回局部变量的地址
    2. 指针使用之前检查有效性

    4. 指针运算

    指针± 整数
    指针-指针
    指针的关系运算

    4.1 指针±整数

    #define N_VALUES 5
    float values[N_VALUES];
    float *vp;
    //指针+-整数;指针的关系运算
    for (vp = &values[0]; vp < &values[N_VALUES];)
    {
      *vp++ = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加入我们要创建一个10个元素的整型数组,并将它全部初始化为1,我们要怎样做呢?
    首先是第一种,数组下标写法:

    #include <stdio.h>
    int main()
    {
    	int arr[10] = { 0 };
    	int i = 0;
    	int sz = sizeof(arr) / sizeof(arr[0]); //计算元素个数
    	for (i=0;i<sz;i++)
    	{
    		arr[i] = 1;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么我们用指针的方式又应该怎么写呢?

    #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++)
    	{
    		*(p + i) = 1;      
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    4.2 指针-指针

    废话不多说,直接上代码

    #include <stdio.h>
    int main()
    {
    	int arr[10] = { 0 };
    	printf("%d\n", &arr[9] - &arr[0]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    这是为什么呢?
    在这里插入图片描述

    总结🔑:
    指针 - 指针得到的是指针与指针之间元素的个数
    注意:
    不是所有的指针都能相减;
    指向同一块空间的2个指针才能相减!不然就没有意义了!

    实战:
    我们要计算字符串的长度,利用指针要怎么算呢?
    那我们只需拿到首元素的地址和末元素的地址就搞定了。

    #include <stdio.h>
    int my_strlen(char* str)
    {
    	char* start = str;
    	while (*str != '\0')
    	{
    		str++;
    	}
    	return (str - start);
    }
    
    int main()
    {
    	int len = my_strlen("abcdef");
    	printf("%d", len);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4.3 指针的关系运算

    下面是一个初始化数组的代码

    for(vp = &values[N_VALUES]; vp > &values[0];)
    {
      *--vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    代码简化, 这将代码修改如下

    for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
    {
      *vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

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

    5. 指针和数组

    数组:一组相同类型元素的集合
    指针变量:是一个变量,存放的是地址

    他们有什么关系呢?
    直接上代码:

    #include <stdio.h>
    int main()
    {
    	int arr[10] = { 0 };
    	//arr是首元素地址
    	//&arr[0]
    	int* p = arr;
    	//通过数组来访问指针
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    #include <stdio.h>
    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    	printf("%p\n", arr);
    	printf("%p\n", &arr[0]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插
入图片描述

    可见数组名和数组首元素的地址是一样的。

    结论:🔑数组名表示的是数组首元素的地址。

    那么这样写代码是可行的:

    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int *p = arr;//p存放的是数组首元素的地址
    
    • 1
    • 2

    既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。
    例如:

    #include <stdio.h>
    int main()
    {
      int arr[] = {1,2,3,4,5,6,7,8,9,0};
      int *p = arr; //指针存放数组首元素的地址
      int sz = sizeof(arr)/sizeof(arr[0]);
      for(i=0; i<sz; i++)
     {
        printf("&arr[%d] = %p  <====> p+%d = %p\n", i, &arr[i], i, p+i);
     }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    所以 p+i 其实计算的是数组 arr 下标为i的地址。
    那我们就可以直接通过指针来访问数组。

    如下:

    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    	int* p = arr; //指针存放数组首元素的地址
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	int i = 0;
    	for (i = 0; i < sz; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    所以这几种写法就是等价

    printf("%d ", arr[ i ]);   
    printf("%d", *(p+i));
    printf("%d", *(arr+i));
    
    • 1
    • 2
    • 3

    6. 二级指针

    指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
    这就要提到二级指针了

    下面代码为一级指针

    #include <stdio.h>
    int main()
    {
       int a = 0;
       int* pa = &a;  //pa是一个指针变量,一级指针变量
       *pa = 20;
       printf("%d\n",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    二级指针:

    #include <stdio.h>
    int main()
    {
    	int a = 0;
    	int* pa = &a;  //pa是一个指针变量,一级指针变量
    	int** ppa=&pa; // ppa是一个二级指针
    	**ppa = 20;
    	//*pa = 20;
    	printf("%d\n",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    那么对于两个星号怎么理解呢?
    在这里插入图片描述

    对于二级指针的
    运算有:

    *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa 。

    int b = 20;
    *ppa = &b;//等价于 pa = &b;
    
    • 1
    • 2

    **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a 。

    **ppa = 30;
    //等价于*pa = 30;
    //等价于a = 30;
    
    • 1
    • 2
    • 3

    总结:🔑

    二级指针变量是用来存放一级指针变量的地址

    7. 指针数组

    指针数组是指针还是数组?
    答案:是数组。是存放指针的数组

    #include <stdio.h>
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int c = 30;
    
    	int arr[10];
    
    	int* pa = &a;
    	int* pb = &b;
    	int* pc = &c;
    
    	//parr就是存放指针的数组
    	//指针数组
    	int* parr[10] = {&a,&b,&c};
    
    	int i = 0;
    	for (i = 0;i < 3;i++)
    	{
    		printf("%d", *(parr[i]));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述
    懂了这个指针数组可以利用指针数组模拟一个二维数组哦,你来试试吧!

    总结不易,对你有用就点个关注吧!
    持续更新哦!

  • 相关阅读:
    第一章 Scala概述
    Linux上部署MySQL5.6 rpm包数据库
    考研政治(一)马克思原理
    python每日一题(模拟用户登录验证)
    39. UE5 RPG角色释放技能时转向目标方向
    RPM方式安装MongoDB,开启远程连接,开启认证,开启oplog
    多人访问同一个springboot项目会不会出现并发问题
    用户登录管理中的Bug修复与技术思考
    【数据结构与算法】泛型的介绍及使用
    java毕业设计计算机组成原理虚拟仿真实验系统mybatis+源码+调试部署+系统+数据库+lw
  • 原文地址:https://blog.csdn.net/qq_43339789/article/details/125565093