• “眼花缭乱的指针”



    前言

    本博客主要介绍一些指针和数组,以及一些指针传参和数组传参的方式;
    在初阶指针的时候我们曾接触过一些比较常见和简单的指针,我们也通过理论和实践的证明得知一个指针要么占4个字节(32位),要么占8个字节(64位),不管是什么类型的指针,大小都是这样固定的,当然指针仅仅只有这几种类型吗?当然不是,接下来,我来记录一下另一些比较常见的指针和数组;

    字符指针

    什么是字符指针?通过名字我们就能的知,是一个指向字符的指针;
    比较常见的用法:

    char a='c';
    char *p=&a;
    
    • 1
    • 2

    直接指向一个字符;

    char*str="ABCDEF";
    
    • 1

    根据这个用法我们还能用它来指向一个字符串,其实也不能叫做指向一个字符串,实际上它还是指向的一个字符,只不过这个字符刚好是字符串的首元素而已,我们都知道字符串是连续存放的,我们既然都知道了字符串首元素的地址,那么这个字符不也就跟着找到了吗,所以说字符指针不仅能用来指向一个字符,还能用来指向一个字符串;
    那么它与字符数组有什么其区别呢?

    char *p="ABCDEF";
    char q[]="ABCDEF";
    
    • 1
    • 2

    我们看起来其实区别不大,但是还是有一小点区别;
    此话怎讲呢?
    我们先来说说他们的相同点把:
    1、都有能力找到这个字符串;
    2、p、q都是字符串的首元素地址,都是指向首元素的
    不同:
    就是这两个字符串的存储位置可能会有所不一样;
    我们知道q数组的空间一定是栈区开辟的,那么p字符串是不是也在栈区开辟的?我们都知道,只要不给栈区上的空间加上const修饰我们就能顺利的修改空间的值,对吧,现在我们p、q都没加,q里面的元素肯定能修改,这没毛病,假设p所指向的字符串也是在栈区上开辟的,那我们是不是也能修改?
    我们来试试:
    测试代码:

    int main()
    {
    	char* p = "ABCDEF";
    	*p = ‘H’;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    通过程序运行结果,我们可以得知,我们无法对p所指向的字符串进行修改,这似乎说明p所指向的字符串似乎并不是在栈区上所开辟的;
    实际上也是的确如此,p所指向的字符串所存的趋于位于一个内存中的可读区域,这块区域是操作系统真正管辖的,只能读取,不能写入的区域,与const修饰的那个伪只读区域不一样,在这块区域,我们是一点也没办法去修改!!!;那么自然的我们p里面所存的地址也并不属于栈区!!!;
    既然这个字符串,存在只读区域,那是不是只用存在一份就行了,反正又不能修改,没必要存在多份相同的字符串:是不是呢,我们接下来试一试:

    int main()
    {
    	char* p = "ABCDEF";
    	char* p2 = "ABCDEF";
    	if (p == p2)
    		printf("you can see me!");
    	else
    		printf("you not can see me!");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    通过运行我们可以看见,的确如我们预想的一样;
    既然有了上述知识,我们来看看一道面试题:

    #include 
    int main()
    {
      char str1[] = "hello bit.";
      char str2[] = "hello bit.";
      const char *str3 = "hello bit.";
      const char *str4 = "hello bit.";
      if(str1 ==str2)
    printf("str1 and str2 are same\n");
      else
    printf("str1 and str2 are not same\n");
      
      if(str3 ==str4)
    printf("str3 and str4 are same\n");
      else
    printf("str3 and str4 are not same\n");
      
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    分析:
    通过上面知识可知,str3和str4指向的内容存在于只读区域,且内容一样,那么没必要存在多份,只需存在一份就行了,那么自然也就拥有一块空间,自然str3和str4指向这块空间,自然str3==str4;
    再来看你str1与str2明显是在栈区所开辟的空间,栈区上的数据有可能随时被修改,就没必要存在一份了,谁要改,就去修改自己的那份,各“管”个的;
    自然str1和str2就没必要是指向的同一块空间,自然str1!=str2;
    运行结果:
    在这里插入图片描述
    的确如此;

    指针数组

    在初学的时候,我们学过什么整型数组(int a[])、字符数组( char a[])、浮点数数组(double a[])等等
    我们可以看到这些数组都是由一些相同类型的数据组成起来的;
    那么一些相同类型的指针集合起来,可不可以组成一个数组呢?
    当然可以;
    就比如我们完全可以照葫芦画瓢
    比如:
    int arr1[5];
    照着画:
    intarr2[5];
    arr1是一个数组,数组总共有5个元素,每个元素都是int类型的元素;
    对于这回我们的解释是就是arr2是一个数组,数组总共有5个元素,每个元素都是int
    类型的元素;

    既然是数组我们就得初始化啊;

    int main()
    {
    	int a = 0;
    	int b = 1;
    	int c = 2;
    	int d = 3;
    	int e = 4;
    	int* arr2[5] = {&a,&b,&c,&d,&e};
    	for (int i = 0; i < 5; i++)
    	{
    	    printf("%p " ,arr2[i]);
    		printf("%d\n",*(arr2[i]));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    自然初始化的规则,也满足那些什么不完全初始化和完全初始化;
    同时我们也可以打印出数组中的元素;
    在这里插入图片描述
    我们知道元素是地址,那么对地址解引用,我们不就能访问地址所指向的空间了吗;
    我们常说数组名是数组首元素地址,那么arr2是什么类型的地址?
    首先数组首元素是int类型,那么自然int的地址就是int**;
    我们在大大胆一点;
    既然指针都能有数组,那么数组能不能有有数组呢?
    简单来说就是一群类型相同的一维数组能不能集合起来再组成一个新数组呢???
    在这里插入图片描述
    答案是当然可以!!!既然可以那该怎么写呢?
    还是老样子,照猫画虎
    int arr[5] [5];
    让人是一个数组,数组有5个元素,每个元素都是一个int类型数组(数组元素个数也是数组类型重要的一部分),数组中有5个元素;
    那么按照上述对于数组元素的访问的话,是不是arr2[i]就是数组元素,只不过这个元素还是个数组!!!对于数组我们自然无法显示出来,于是呢编译器给我们显示的是该元素的首元素的地址(数组名):
    在这里插入图片描述

    既然有了首元素地址那我们是不是可以访问元素呢?
    当然可以:
    在这里插入图片描述
    你看看这是不是就是二维数组了!!!!
    依次类推……
    我们多个类型相同的二维数组集合在一起是不是也可以构成数组,那么这个数组也就是三维数组,依次下去四维、五维、六维……
    总之管他几维,本质上都还是一个“一维数组”,至此我们从二维数组开始只有第一个下标可以省略的原因其它下标不能省略,把它当作一维数组来看,第一个下标就是元素个数,这个如果是不完全初始化,就可以根据初始化了多少个元素来确定,因此,我们再初始化和数组传参的时候可以不写这个下标;

    数组指针

    既然都提到了指针数组,那么自然少不了数组指针,这两东西别看名字有几分相似,但是确实两个完全不同的类型;
    从名字可以看出:
    指针数组:本质是个数组!!!
    数组指针:本质是个指针!!!

    那什么是数组指针?
    顾名思义,就是指向一个数组的指针!!!
    在这里插入图片描述
    我们知道arr再两个情况下表示整个数组,其它情况下,表示数组首元素地址;
    1、sizeof(数组名)表示求的是整个数组的大小;
    2、&arr表示取的是整个数组的地址;
    今天跟地址有关,自然就与&arr有关;
    现在我们通过&arr取出了整个数组的地址,应该怎么存呢?
    int (* p)[10]这颗 代表着p是一个指针,注意必须加()不然根据优先级p是会先于[ ]先结合这样一来p就不是一个指针了,而是一个整型指针数组了,因此再写数组指针的时候,要特别这一点;,去掉( p)过后,剩下的就是指针指向的类型了;
    因此这个p所代表的意思就是,p是一个指针,指向一个拥有10个int类型元素的数组;
    因此我们想把arr类型的数组的地址存起:
    在这里插入图片描述
    当然既然存在指针,那么也就存在解引用,不过这个接出来是一个数组,但是如果是单独存在于sizeof()里面和&arr的确是整个数组,但是如果现在没有那么他就是数组首元素的地址!!!因此我们同样可以通过数组指针来访问元素:!!!
    在这里插入图片描述
    当然我们还可以写成二维数组访问的形式:
    在这里插入图片描述
    当然p+1是跳过一个数组,因此我们只能写出p[0] [i]的形式,这个0千万不能变,变了,就是对其它位置的数组进行访问,相当于数组越界了,后果很严重!!!;

    数组参数、指针参数

    在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
    一维数组传参

    #include 
    void test(int arr[])//ok?Y
    {}
    void test(int arr[10])//ok?Y
    {}
    void test(int *arr)//ok?Y
    {}
    void test2(int *arr[20])//ok?Y
    {}
    void test2(int **arr)//ok?Y
    {}
    int main()
    {
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    二维数组传参

    void test(int arr[3][5])//ok?Y
    {}
    void test(int arr[][])//ok?N
    {}
    void test(int arr[][5])//ok?Y
    {}
    //总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
    //因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
    //这样才方便运算。
    void test(int *arr)//ok?N
    {}
    void test(int* arr[5])//ok?N
    {}
    void test(int (*arr)[5])//ok?Y
    {}
    void test(int **arr)//ok?N
    {}
    int main()
    {
    int arr[3][5] = {0};
    test(arr);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    一级指针传参

    #include 
    void print(int *p, int sz)
    {
    int i = 0;
    for(i=0; i<sz; i++)
    {
    printf("%d\n", *(p+i));
    }
    }
    int main()
    {
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]);
    //一级指针p,传给函数
    print(p, sz);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    二级指针传参

    #include 
    void test(int** ptr)
    {
    printf("num = %d\n", **ptr);
    }
    int main()
    {
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    函数指针

    既然数组都有指针,那是不是函数也应该有个地址???
    答案是肯定的?

    在这里插入图片描述
    &Add取出来了怎么存?
    照着上面画:
    int (*p)(int,int );
    同样的这颗 *代表着p是一个指针,指向一个函数,该函数参数是(int,int)返回值也是int类型;
    在这里插入图片描述
    既然存起来了,那么我解引用是不是就能访问这个函数了,的确如此:
    在这里插入图片描述
    其实函数的地址和数组有点像;
    数组名是首元素地址,函数名也是地址;
    其实呢?对于函数来说我们就算不对其地址解引用,也能对其进行调用
    在这里插入图片描述

    函数指针数组

    顾名思义,就是将一群函数指针类型一样的函数指针存起来,就是函数指针数组;
    在这里插入图片描述
    int (*arr[4])(int, int);
    arr[4]先和[ ]结合,表示arr十一个数组,其它的表示数组中的元素类型;
    既然这样,我们就能访问其中的每个元素;
    在这里插入图片描述
    我们通过访问数组元素来实现了函数的调用,这个数组叫做跳转表!!!;

    有趣的代码:

    //代码1
    (*(void (*)())0)();
    //代码2
    void (*signal(int , void(*)(int)))(int);//解读一下代码意思
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    这段代码,只要是将0强制类型转换为一个函数指针类型的指针(无参,返回值为void的函数指针),然后在对该处的0地址当作函数指针进行函数调用;
    在这里插入图片描述
    这段代码表示这是一个名字叫做signal的函数,该函数参数类型为int类型和一个函数指针类型(参数为int,返回值为void的函数指针),该函数的返回值也是一个函数指针(参数为int,返回值为void的函数指针);也就是一个函数声明!!!
    这段代码看起来极度复杂,我们有没有什么办法,使得该代码变得简洁一点呢?
    当然,typedef一下啊,这其中主要是这个返回值和函数指针参数比较复杂,我们就对这种类型重定义一下;
    在这里插入图片描述
    这样代码就变得极其简单明了!!!
    注意不能像这样typedef:
    在这里插入图片描述
    在这里插入图片描述

    包括数组也不行,只能像上述一样!!!;

  • 相关阅读:
    【Python机器学习】零基础掌握RandomForestClassifier集成学习
    KingbaseES应对表年龄增长过快导致事务回卷
    1-前端基本知识-CSS
    【LeetCode】最长有效括号 [H](动态规划)
    LeetCode_动态规划_中等_313.超级丑数
    MMDeploy理解
    springcloud整合seata
    移植RTOS的大体思路
    C++PrimerPlus(第6版)中文版:Chapter17.4文件输入和输出
    C++new和delete运算符介绍
  • 原文地址:https://blog.csdn.net/qq_62106937/article/details/126306149