• C语言中指针的介绍


    指针

    一:指针是什么?

    指针是什么?
    指针理解的2个要点:

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

    1:内存

    程序的运行需要内存,我们为了有效地使用内存,就需要将内存划分为一个个小的内存单元,每一个单元的大小是一个字节。(一个字节比较合理,这个内存单元太小也不好,太大也不好)为了 能够有效地使用每个内存单元,我们给每一个单元都定了一个编号,这个编号就叫做这个内存单元的地址。

    就像在我们的生活中,比如说你在网上购物。你就一定需要告诉商家,你自己的确切位置,比如说xx市xx区xx路xx号(几号楼)几号宿舍。)这个内存的地址也是这个道理,就像楼中的门牌号,通过编号的方式,内存的单元地址也就确定了。我们可以轻松地找到对应的地址,而不需要一个一个去找。
    在这里插入图片描述

    2:地址的生成

    我们的电脑中都有硬件电路,用于生成地址的电线叫地址线。当电路中有电路通过时,会产生正负脉冲,从而表示0与1.此处我们以32位电脑为例,它在生成地址时32根地址线同时产生电信号表示1或0,当每一个地址线组合起来时就有了许许多多的不同的排列组合方式。

    00000000000000000000000000000000——对应0

    00000000000000000000000000000001——对应1

    1111111111111111111111111111111111111——最终可能会变成32个1

    这样的排序方式一共有2^32次方种 内存中一共有这么多byte的空间。
    (1024B=1KB 1024KB=1MB 1024MB=1GB) (1byte=8bit)
    但是这个数字不是很直观,我们先对它除以1024得到4194304个KB,再除1024得到4096个MB,再除以1024得到4GB,也就是说在早期的三十二位电脑内存中一共有4GB的内存空间。

    3:数据的储存

    变量是创建内存中的(在内存分配空间的),每个单元都有地址,所以变量也有地址
    利用&:
    &:取地址操作符,取出谁的地址。
    打印地址,%p是以地址的形式打印

    #include 
    int main()
    {
        int a = 10;//向内申请4个字节,存储10
        &a;//取出a的地址,&为取地址符号
        //这里的a共有4个字节,每个字节都有地址,
        //但我们取出的是第一个字节的地址(较小的地址)
        printf("%p\n", &a);//打印地址,%p是以地址的形式打印
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ​​打印出来后 通过调试F10 内存 监视窗口得到此图
    在这里插入图片描述
    我们实际上取出的只有0x010FF808这个地址(起始位置的地址)

    0a 00 00 00一行显示了四个字节 (设置了四列)

    a的值为10,用二进制表示即为:0000 0000 0000 0000 0000 0000 0000 1010(二进制的数字表达最后一位表示2的0次方,倒数第二位就表示2的1次方,以此类推,十就是2的3次方加2的一次方也就是1010),在这个时候我们以每个四位为一组,就可以得到数据的表示方法:00 00 00 0a(在16进制数中,a表示10,b表示11,c表示12,d表示13,e表示14,f表示15)

    ** 0000 0000 0000 0000 0000 0000 0000 1010
    0 0 0 0 0 0 0 a**

    其实十进制的储存方式是这样的
    0x 00 00 00 0a 倒着存

    4:指针变量

    #include
    int main()
    {
        int a = 10;
        int* p = &a;
     //我们把a这个变量的地址储存在这个变量p中,
     //这个p就叫做指针变量,类型为int*   
     //变量p是创建出来存放地址(指针)的。   
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在内存单元中:
    1:编号就是地址 而地址就是指针
    2:当a创建好后,占用了4个字节,每个字节都有一个地址,&a拿到的是第一个字节的地址

    对int* pa =&a;的理解

    1.pa代表0x010FF808这个起始位置的地址
    2.中间的*表示p是个指针变量,注意指针变量是pa,而不是*pa
    3.int说明pa指向的对象是int类型的(本例子说明pa指向的是a)
    4.pa为指针变量,接受&a的内容,(即应该将地址存到指针变量中去)也就是变量的首地址
    5.int*整体是一个整型指针类型

    在这里插入图片描述

    创建变量的本质:
    向内存申请了空间。有了内存空间之后,每个变量都有了属于自己的地址。

    (1):指针变量的大小

    %zu表示打印sizeof

    #include 
    int main()
    {
            printf("%zu\n", sizeof(char*));//zu表示打印sizeof
            printf("%zu\n", sizeof(short*));
            printf("%zu\n", sizeof(int*));
            printf("%zu\n", sizeof(float*));
            printf("%zu\n", sizeof(double*));
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    你可能认为输出结果是:1 2 4 4 8

    但实际上是:4\8 4\8 4\8 4\8 4\8 (4或8)

    因为:
    指针变量储存的是地址,也就是说指针变量的大小取决于存放一个地址需要多大的空间,32位平台下地址是32个bit位(即4个字节),而64位平台下地址是64个bit位(即8个字节),所以指针变量的大小就是4或8.

    结论:
    32位环境下,地址的序列就由32个0/1组成的二进制序列,要存储进来,需要4个字节。
    64位环境下,地址的序列就由64个0/1组成的二进制序列,要存储进来,需要8个字节。

    (2):如何一口气定义好几个指针变量?

    int main()
    {
        int* p1,p2,p3 = &a;
        //这个定义方式是不正确的,
        //只有p1是指针变量而其他两个是整型变量
        int* p1,*p2,*p3;
        //这个定义方法才是正确的,
        //在第一种方法下,*只给第一个变量使用
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    二:指针与指针类型

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

    int num = 10; 
    p = #
    
    • 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 类型变量的地址。
    那指针类型的意义是什么?

    1:指针+-整数

    #include 
    int main()
    {
        int n = 10;
        char* pc = (char*)&n;
        int* pi = &n;
        printf("&n=%p\n", &n);
        printf("pc=%p\n", pc);
        printf("pc+1=%p\n", pc + 1);
        printf("pi=%p\n", pi);
        printf("pi+1=%p\n", pi + 1);
        return 0;
    }
    //结果:
    //&n = 006FF944
    //pc = 006FF944
    //pc + 1 = 006FF945
    //pi = 006FF944
    //pi + 1 = 006FF948
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    打印1~10

    //用指针打印1~10
    #include
    int main()
    {
    	int arr[10] = { 0 };
    	int* p = arr;
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		//法1:
    		*p = i + 1;//p指向第i+1(下标为i)个元素(数组下标(0~n-1))
    		p++;//p指向下一个元素
    		//法2:
    		*(p + i) = i + 1; 
    		//p+i就是下标为i元素的地址,
    		//通过对p+i的解引用,找到下标为i的元素。
    		printf("%d ", arr[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

    总结:
    1:无论我们打印&n,还是指针变量pc还是pi,它们的结果都是一样的。
    2:(当我们观察pc+1与pi+1的时候发现两者结果并不相同)
    +1表示跳过一个数据类型(char、int、double…)
    指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
    比如: char* 的指针解引用就只能访问1个字节,而 int* 的指针的解引用就能访问4个字节double*的指针的解引用就能访问8个字节
    3:指针类型决定了指针的步长(向前/向后,走n步有多大距离)
    比如:
    char*+1,意思是跳过一个字符,也就是向后走了1个字节
    short*+1,意思是跳过一个短整型,也就是向后走了2个字节
    int*+1,意思是跳过一个整型,也就是向后走了4个字节
    double*+1,意思是跳过一个double,也就是向后走了8个字节

    2.指针的解引用

    #include 
    int main()
    {
        int n = 0x11223344;
        char* pc = (char*)&n;
        int* pi = &n;
        *pc = 0;
        printf("%x\n", n);
        *pi = 0; 
        printf("%x\n", n);
        return 0;
    }
    //结果:
    //11223300
    //0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在内存中,0x11223344这个数字以小端字节序存储(44 33 22 11),先用char* 的指针解引用只能访问一个字节,所以会把第一个字节改为0,也就会打印11223300;而 int* 的指针解引用能访问四个字节,所以会把第四个字节都改为0,也就会打印0

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

    三:野指针

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

    对于野指针,我们形象的可以把它比喻成野狗,接近它会受伤。

    1:野指针成因

    (1) 指针未初始化

    
    #include 
    int main()
    {
    	int* p;//局部变量指针未初始化,默认为随机值,p为野指针
        //p里面放的随机值就当成了地址
    	//对p解引用,我们去找那个地址对应的空间
    	//把20放进去后,发现找不到那个空间
    	//或者找到的那个空间不属于当前的程序.
    	*p = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    (2)指针越界访问

    用个代码来演示一下

    #include 
    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++)
    	{
    		//当指针指向的范围超出数组arr的范围时,p就是野指针
    		*(p++) = i;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    用图来解释的话:

    在这里插入图片描述

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

    int* test()
    {
    	int num = 100;
    	return &num;
    	//num是局部变量,
    	//其生命周期是从test函数进入开始到出test函数
    	//出函数后,num这块空间需要还给我们的操作系统
    	//返回后这块空间已经被释放,
    	//不属于我们了(这个空间已经不在程序的作用域内)
    }
    
    int main()
    {
    	int* p = test();
    	*p = 200;
    	//但是当我们回到主函数里面去的时候,p记住了这块空间的地址
    	//p有能力能够找到这块空间,但地址不属于我们了(非法访问)
    	//这个时候p就为野指针
    	//我们无法确定是否会有其他的操作会改变它的值
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    形象的理解方式就是:
    假设张三2天住进了一个酒店,订了302号房间,2天后他退房,可这个时候他记得当时他来的酒店住的房间是302,他硬要住302号房间,结果却发现不能住进去了。

    这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

    2:如何规避野指针

    1. 指针初始化
    2. 小心指针越界
    3. 指针指向空间释放即使置NULL
    4. 避免返回局部变量的地址(准确来说是避免返回栈空间的地址)
    5. 指针使用之前检查有效性
    #include 
    int main()
    {
    	int* p = NULL;
    	*p = 20;//err,初始化空指针后相当于给野狗栓上了绳子
    	//....
    	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
    • 14

    四:指针的运算

    1:指针±整数

    #include
    #define N_VALUES 5
    int main()
    {
    	float values[N_VALUES];
    	float* vp;
    	//指针+-整数;指针的关系运算
    	for (vp = &values[0]; vp < &values[N_VALUES];)
    		//这里虽然下标为五的元素属于越界访问,但是并没有读取它的内容,不算越界访问
    	{
    		*vp++ = 0;
    		//vp先解引用,然后再++
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    指针加减整数可以让地址向后或向前移动对应的字节数。

    2:指针-指针

    注:指针+指针是无意义的

    #include
    #include 
    //1. 计数器
    int my_strlen(char* str)
    {
    	int count = 0;
    	while (*str)
    	{
    		count++;
    		str++;
    	}
    	return count;
    }
    //2. 递归的版本
    
    //3. 指针-指针
    int my_strlen(char* str)
    {
    	char* start = str;
    	while (*str)
    		str++;
    	return str - start;//6
    }
    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
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3:指针的关系运算

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

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

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

    五:指针和数组

    区别:
    数组和指针不是一个东西。
    数组是能够存放一组数,连续的空间,数组的大小取决于元素个数
    指针是一个变量,是存放地址的,大小为4/8个字节。
    相同点:
    数组名就是地址(指针)
    数组把首元素的地址,交给一个指针变量后,可以通过指针来访问数组。

    #include 
    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    	int* p = arr; //指针存放数组首元素的地址
    	int i = 0;
    	//printf("%p\n", arr); //0077F884		
    	//printf("%p\n", &arr[0]);//0077F884
    	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);
    		//printf("%d ", *(p + i));//1 2 3 4.....0
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    六:二级指针

    指针变量也是变量,是变量就有地址,那指针变量的地址就可以储存在二级指针内

    二级指针解引用需要两次才能找到变量

    #include
    int main()
    {
        int a = 0;
        int* p1 = &a;
        int** p2 = &p;
        //二级指针,存放指针变量的地址
        //可以看作int* *p2,
        //前面的int*表示指向的对象为int*类型,后面的*表示p2为指针变量
        printf("%d",**p2);
        //p2解引用一次得到指针变量p1,再解引用得到a
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    七:指针数组

    指针数组是指针还是数组?
    答案:是数组。是存放指针的数组。
    数组我们已经知道整形数组,字符数组。

    #include
    int main()
    {
    	//指针数组-存放指针的数组
    	int a = 10;
    	int b = 20;
    	int c = 30;
    	int d = 40;
    	int e = 50;
    	int* arr1[5] = { &a,&b,&c,&d,&e };//存放整型指针的数组
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		printf("%d ", *(arr1[i]));
    	}
    	//char* arr2[6];//存放字符指针的数组
    
    	int arr1[] = { 1,2,3,4,5 };
    	int arr2[] = { 2,3,4,5,6 };
    	int arr3[] = { 3,4,5,6,7 };
    	int arr4[] = { 4,5,6,7,8 };
    	int* arr5[4] = { arr1,arr2,arr3,arr4 };//指向首元素的地址
    	int i = 0;
    	for (i = 0; i < 4; i++)
    	{
    		int j = 0;
    		for (j = 0; j < 5; j++)
    		{
    			printf("%d ", arr5[i][j]);//*(*(arr5+i)+j))
    		}
    		printf("\n");
    	}
    	return 0;
    // 1 2 3 4 5
    // 2 3 4 5 6
    // 3 4 5 6 7
    // 4 5 6 7 8	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    进阶版请等待后续文章发布,尽情期待!

  • 相关阅读:
    JavaEE - Spring MVC
    科学高效备考AMC8和AMC10竞赛,吃透2000-2024年1850道真题和解析
    计算机毕业设计(附源码)python影城在线售票及票房数据分析系统
    各种添加路由的方法
    vue2.0 使用可选链操作符
    mybatis
    R语言使用match函数获取向量中特定值的位置(position of a particular value)、which.min函数获取向量中最小值的位置
    【JAVA开发】提高开发效率的工具分享
    关于el-input-number在el-table中只能点击一次的问题完美解决答案
    tomcat之高并发配置Server.xml
  • 原文地址:https://blog.csdn.net/yanghuagai2311/article/details/126122882