• 从零开始C语言精讲篇5:指针



    在这里插入图片描述

    一、指针是什么?

    在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
    (points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
    说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
    内存单元

    我们的内存是类型下图这样的空间:
    在这里插入图片描述
    我们在使用的过程中,把内存划分成一个个小的字节,每个字节称为一个内存单元。我们给每个内存单元进行编号,每个编号唯一对应一个内存单元。类似身份证,每人一个身份证,我有这个身份证就可以唯一确定你这个人。这里也是类似的,地址指向了一个确定的内存空间,所以地址也被形象的被称为指针。

    举个例子:

    int main()
    {
    	int a = 10;
    	int* pa = &a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    int* pa=&a,也就是pa里面放的a的地址(指针),pa是(整形)指针变量,它是用来存放指针的


    在这里插入图片描述

    二、指针和指针类型

    int main()
    {
    	int a = 0x11223344;//0x表示16进制
    	//一位16进制对应四位2进制:2*2*2*2=16
    	//我们这里a一共8位16进制,也就是正好把32位2进制占满了(8*4=32)
    	int* pa = &a;
    	*pa = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们按f10进行调试,监视一下,可以看到我们pa里面存放的就是a的地址
    在这里插入图片描述
    监视一下内存,可以看到,我们内存中经过代码pa = 0; 32位是全部变成了0
    ps:下图中我们是16进制一共8位,对应2进制32位
    在这里插入图片描述
    经过
    pa = 0;赋值后
    在这里插入图片描述

    那么问题来了,我们之前说过,32位下指针大小都是4字节,64位下指针大小都是8字节。
    假如我们都在32位下,我能否用char*来接收这个int型的地址(指针)呢?

    我们仅把上面代码中的int * 改成 char * 来测试一下
    在这里插入图片描述

    可以看到放进去是没有问题的,但是如果要对内容修改呢?
    在这里插入图片描述
    经过*pa = 0;赋值后
    在这里插入图片描述
    为什么我这里只改动了一个字节呢?因为我是char * 的指针,我只负责我指向地址开始往后的1个字节。同理,如果我是int * 的指针,我只负责我指向地址开始往后的4个字节。

    指针类型的意义1:指针类型决定了指针解引用操作的时候,一次访问几个字节(访问内存的大小)

    由于这个性质,我们来看看下面这个题目

    int main()
    {
    	int a = 10;
    	int* pa = &a;
    	char* pa2 = &a;
    
    	printf("%p\n", pa);
    	printf("%p\n", pa2);
    
    	printf("%p\n", pa+1);
    	printf("%p\n", pa2+1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    可以看到,我们的pa虽然和pa2都是一个地址,但是当他们加1的时候,就受到了指针类型的管控。char * 指针+1只会跳1个字节,而int * 的指针+1则会跳4个字节。

    指针类型的意义2:指针类型决定了指针±整数时的步长(跳过几个字节)

    三、野指针

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

    3.1野指针成因

    1. 指针未初始化

    int main()
    {
    	int* p;//没有初始化
    	*p = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    如果你没有初始化p,那么指针p里面放的是随机的地址,你*p=20是想把随机地址里面的值改成20。但是我们这个随机地址是不属于我们当前程序的,就会造成非法访问,p就是野指针。

    2. 指针越界访问

    int main()
    {
    	int arr[10] = 0;
    	int i = 0;
    	int*p = arr;
    	for (int i = 0;i <= 10;i++)
    	{//数组元素下标是0-9,你如果访问了10,这个是不属于我们的空间
    		//这就是越界访问
    		*p = i;
    		p++;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    3. 指针指向的空间释放
    这个知识点笔者会在后面的动态内存释放进行详细讲解,这里简单提一下

    int* test()//指针作为函数返回值
    {
    	int a = 10;
    	return &a;
    }
    int main()
    {
    	int* p = test();
    	printf("%d",*p);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们在main函数里调用test函数,test函数返回a的地址。
    需要注意的是,一旦返回了a的地址,test函数结束,a的生命周期也结束了
    也就是说,a所在的空间返还给了操作系统。那么你在main函数中通过p再访问a之前的地址,这就是非法访问了。

    打个比方:你和你好朋友出去旅游,然后你们开了一间房。在使用期内,你们想在房间里干啥都行。但是房间到期后,你再访问,旅店要告你的。

    3.2规避野指针的方法

    1. 指针初始化
    2. 小心指针越界
    3. 指针指向空间释放即使置NULL
    4. 指针使用之前检查有效性

    示例如下:

    #include 
    int main()
    {
        int *p = NULL;//不知该该指向哪里,可以先初始化为NULL
        //....
        int a = 10;
        p = &a;//明确地初始化
        if(p != NULL)
       {
            *p = 20;
       }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    四、指针运算

    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

    上面这段代码啥意思,请看下图的解释
    在这里插入图片描述
    我们在for循环中判断vp指向的地址是否小于&values[N_VALUES]
    ps:地址本质是一个16进制数,可以用来比较

    发现vp指向的地址小于&values[N_VALUES],我们就进行 *vp++,
    需要注意的是,++的优先级是高于 * 的,所以我们本应先进行++再 *,
    但我们这里是后置++也就是说vp在本轮还是指向数组0下标元素,然后 * 解引用赋值为0,
    在这里插入图片描述
    vp因为++了,所以指向了数组1下标元素
    在这里插入图片描述

    然后重复for循环,后面的都一样了,就是把数组元素赋为0,然后vp++直到vp指向3停止
    (vp < &values[4])

    4.2指针-指针

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

    指针-指针,得到的数字的绝对值,是指针之间元素的个数
    在这里插入图片描述

    注:肯定有一些杠精会问:“啊,你这个是两个相同类型的指针啊,如一个char类型指针-int类型呢?”

    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    	char brr[10] = { 0 };
    	printf("%d\n", &arr[9] - &brr[2]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    像上面这种代码,你自己想想有多亏贼?
    首先你两个地址都是随机的,你怎么知道两个随机地址之间隔了多远?
    还有一个问题就是,你两个不同类型指针,到时候划分元素个数是按1字节来化还是4字节来化?

    指针-指针的前提:两个指针指向同一空间(比如指向同一数组)

    具体应用实例:求字符串长度
    法一:使用库函数strlen

    //求字符串长度
    //法一:使用库函数strlen
    #include
    #include
    int main()
    {
    	char arr[] = "abcdef";
    	int len = strlen(arr);
    	printf("%d\n", len);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    法二:使用指针

    int my_strlen(char* p)
    {
    	int count = 0;
    	while (*p != '\0')//字符串末尾都是默认有一个\0
    	{
    		count++;
    		p++;
    	}
    	return count;
    }
    int main()
    {
    	char arr[] = "abcdef";
    	int len = my_strlen(arr);
    	printf("%d\n", len);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    法三:使用指针-指针

    int my_strlen(char* p)
    {
    	char* tmp = p;//标记arr[0]的位置
    	while (*p != '\0')//字符串末尾都是默认有一个\0
    	{
    		p++;
    	}//走完while,p指向arr的末尾元素
    	int count = p - tmp;
    	return count;
    }
    int main()
    {
    	char arr[] = "abcdef";
    	int len = my_strlen(arr);
    	printf("%d\n", len);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    五、指针和数组

    区别:
    数组是一块连续的空间,里面放的是相同类型的元素。
    数组大小和元素类型,元素个数有关系,比如int arr[10],该数组大小=4*10

    指针是一个变量,里面存放地址
    指针变量的大小是4/8byte,这个取决于是32位系统还是64位

    联系:
    数组每个内存单元都有自己的内存地址,而我们指针可以存放这些地址

    重点!!!:数组名是什么?

    #include 
    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

    在这里插入图片描述
    可见数组名和数组首元素的地址是一样的
    这里的一样,不仅仅是数值上,是各种意义上!
    ps:数组名确实是首元素地址,但是有两个例外
    1.sizeof(数组名),这里的数组名表示的是整个数组
    2.&数组名,这里的数组名也是整个数组,&数组名表示整个数组的地址

    #include 
    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    	printf("%p\n", arr);
    	printf("%p\n", &arr[0]);
    	printf("%p\n", &arr);
    	printf("---------我是分界线----------\n");
    	printf("%p\n", arr+1);//地址+4,跳过1个int型
    	printf("%p\n", &arr[0]+1);//地址+4,跳过1个int型
    	printf("%p\n", &arr+1);//地址+40,跳过一个数组int arr[40]
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

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

    
    #include 
    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    	int* p = arr;
    	int i = 0;
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	for (i = 0;i < sz;i++)
    	{
    		*(p + i) = i;
    		printf("%d ", *(p + i));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    六、二级指针

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

    int main()
    {
    	int a = 10;
    	int* pa = &a;//pa里面放的是a的地址
    	int** ppa = &pa;//ppa里面放的是pa的地址
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    示意图如下:
    在这里插入图片描述
    对于二级指针,我们也是可以解引用的
    *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

    当然了,你也可以无限套娃,就会有三级指针、四级指针。。。

    七、指针数组

    指针数组是指针还是数组?
    这个问题你就想,好男孩,好男孩的本质是男孩啊
    指针数组本质还是数组

    举个例子:

    int main()
    {
    	int arr1[5];//整形数组,存放整形的数组就是整形数组
    	char arr2[3];//字符数组,存放字符的数组就是字符数组
    
    	//指针数组,存放指针的数组就是指针数组
    	int* parr[5];//整形指针数组
    	char* pbrr[4];//字符指针数组
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    示意图如下
    在这里插入图片描述
    应用实例:

    int main()
    {
    	int a = 123;
    	int b = 213;
    	int c = 312;
    	int* arr[3] = { &a,&b,&c };
    
    	for (int i = 0;i < 3;i++)
    	{
    		printf("%d\n", *arr[i]);
    		//arr[i]是一个元素地址,*arr[i]对该地址解引用,得到地址指向的元素
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述


    总结

    本文介绍了指针和指针类型(着重掌握)、野指针及其成因、指针运算、指针数组等相关知识。作为C语言的大头,指针这块知识必须要拿下,最后祝读者学业有成,奥利给!

  • 相关阅读:
    nodejs项目实例医生预约平台宠物医院预约挂号网
    浏览器输入url后回车展开过程
    Object.keys的‘诡异’特性,你值得收藏!
    基于springboot地方废物回收机构管理系统springboot11
    React关于渲染列表时的key属性
    Android12及所有版本解决没有system读写权限(只需要magisk面具)
    Seamlessly Manipulate SVG Files in Python
    计算机导论第十一周课后作业
    SQLZOO——1 SELECT names
    Windows端口号被占用的查看方法及解决办法
  • 原文地址:https://blog.csdn.net/m0_57180439/article/details/126087689