• 梦开始的地方 —— C语言指针进阶



    指针进阶

    0.前言

    从上一篇博客指针入门我们知道了,几点

    1. 指针是一个变量,存放的是地址,地址是某一块内存空间的唯一标识
    2. 指针的大小在不同平台的大小不一样,32位平台的是4个字节,64位平台是8个字节
    3. 指针的类型决定了指针解引用和指针加减能走多远

    1. const修饰指针

    我们知道const修饰一个变量,它就是一个常变量是不会被直接修改的。

    #include 
    
    int main()
    {
    	const int num = 10;
    	num = 10; //这里会报错
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当我们可以通过指针来进行修改,也就是不直接对它进行修改,而是取出它的地址通过地址来对它进行修改。

    #include 
    
    int main()
    {
    	const int num = 10;
    	int* p = #
    	*p = 20; //num被修改成了20
    	printf("%d\n", num);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    那么const的就失去了原本的意义,其实还有一种另外的方式可以达到我们想要的效果。那就是const修饰指针

    代码示例:如果再尝试使用地址的方式修改变量编译器就会报错。

    #include 
    
    int main()
    {
    	int num = 10;
    	const int* p = #
        //int const* p = # 这两个写法没区别
    	*p = 20;
    	printf("%d\n", num);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么问题又来了,我们不能通过地址修改但此时又可以通过直接修改的方式进行修改

    #include 
    
    int main()
    {
    	int num = 10;
    	const int* p = #
    	num = 20;// num被修改成了20
    	printf("%d\n", num);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    依旧是可以解决的,我们在用一个const修饰一下 p就好了,再用const修饰变量,此时无论是直接修改还是通过取地址都无法修改num的值了,那const到底怎么修饰指针呢?

    #include 
    
    int main()
    {
    	const int num = 10;//让num不能通过变量名修改
    	const int* const p = #//不允许通过*修改num的地址,同时指针变量p也不能被改变
    	printf("%d\n", num);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    总结:

    • const写在星号*左边,修饰的是指针指向的内容,使得指针指向的内容,不能通过指针来改变。但是指针变量本身是可以修改的。

      #include 
      
      int main()
      {
      	int num = 10;
      	int n = 100;
      	const int*  p = #
      	*p = 20;//错误写法
      	p = &n; //正确写法
      	num = 200;//正确写法
      
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • const写在星号*右边,修饰的是指针变量本身,使得指针变量本身不能修改。但是指针指向的内容可以通过指针来改变,是可以修改的。

      #include 
      
      int main()
      {
      	int num = 10;
      	int n = 100;
      	int* const  p = #
      	*p = 20;//正确写法
      	p = &n; //错误写法
      	num = 200; // 正确写法
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    2. 字符指针

    在C语言中字符指针用char*来表示

    #include 
    
    int main()
    {
        char c = 'Q';
        char* cp = &c;
        printf("%c\n", *p);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    char*还有另外一种用途,那就是常量字符串

    #include 
    
    int main()
    {
        char* str = "hello";// 常量字符串
        char arr[] = "hello";
        printf("%c\n", *str);
        printf("%s\n", str);
        printf("%s\n", arr);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    str 里存放的是“hello”字符串的首地址,且这是一个常量字符串,也就是它不能被修改

    而arr是一个字符数组它是可以被修改的

    在这里插入图片描述

    再来看一段代码

    #include 
    
    int main()
    {
        char* str1 = "hello";
        char* str2 = "hello";
    
        char arr1[] = "hello";
        char arr2[] = "hello";
        
        if (str1 == str2)
        {
            printf("str1 == str2\n");
        }
        else
        {
            printf("str1 != str2\n");
        }
        if (arr1 == arr2)
        {
            printf("arr1 == arr2\n");
        }
        else
        {
            printf("arr1 != arr2\n");
        }
        
        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

    运行结果

    str1 == str2
    arr1 != arr2
    
    • 1
    • 2

    str1 == str2 是因为,"hello"是一个常量字符串,常量字符串是不能被修改的。所以常量字符在内存中只会存一份,所以 str1和str2存放的都是 “hello”这个字符的首地址

    而arr1 和 arr2 是数组名,数组名存储放的是首元素的地址,这是两个不同的数字当然不相等。

    注意:比较字符串相等要使用 strcmp

    在这里插入图片描述

    2. 指针数组

    整形数组、字符数组都是数组,那么指针数组也是数组,它是一个存放指针的数组。上一篇博客也提到过

    基本语法

    #include 
    
    int main()
    {
        int* arr[10];//整形指针数组
        char* ch[10]; //字符指针数组
        int** arrP[10]; //二级整形指针数组
    
        int a = 10;
        char c = 'a';
        int* p = &a;
    
        arr[0] = &a;
        ch[0] = &c;
        arrP[0] = &p;
        
        printf("%d\n",*arr[0]);
        printf("%c\n",*ch[0]);
        printf("%d\n",**arrP[0]);//第一次解引用拿到一级指针p的地址,再次解引用就拿到了a的地址
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3. 数组指针

    1) 数组指针概念

    数组指针是指针还是指针?

    我们知道整形指针int *指向的是一个整形变量,也知道字符指针char*指向的是一个字符变量,那么数组指针它也是一个指针,它是一个指向数组的指针。

    那么下面两个哪个才是数组指针呢?

    int *arrP[10];
    int (*pArr)[10];
    
    • 1
    • 2
    • int *arrP[10],因为[]的优先级要高于*,所以arrP会和[]先结合变成arrP[10]这就是一个数组,那么int就会和*结合变成int *,那么此时就变成了一个int*类型的数组,也就是整形指针数组
    • (*pArr)括起来,那么它们结合就是一个指针,指向了一个数组[10],所以这里可以描述为,*pArr指向了一个10个元素的数组,每个元素的类型是int,所以int (*pArr)[10]是一个数组指针

    2) 数组名 和 &数组名

    我们给一个数组

    int arr[10]
    
    • 1

    这个数组的arr&arr分别是啥?

    来看一段代码

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

    打印结果:发现arr和&arr的地址是一样的?我们知道arr存的是数组首元素的地址,那 &arr也是吗?并不是

    00000030B82FFCF8
    00000030B82FFCF8
    
    • 1
    • 2

    再来看一段代码

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

    打印结果

    000000FA108FF608
    000000FA108FF608
    ==========
    000000FA108FF60C
    000000FA108FF630
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们发现,arr&arr打印的地址是一样的。但是它们的意义是完全不一样的。

    arr+1只是跳过了4个字节的地址,而&arr+1则是条过了40个字节的地址,相当于跳过了整个数组。

    从这里看出来&arr取出的是整个数组的地址!

    注意:

    • &数组名
    • sizeof(数组名)
    • 除此上面2种情况之外-所有遇到的数组名都是数组首元素的地址

    3) 数组指针的使用

    了解了数组指针,那么数组指针是怎么使用的呢?

    整形指针存的是整形的地址,那么数组指针指向的是一个数组,那它存的就是数组的地址。

    前面我们了解到通过指针±可以遍历数组,指针p存的是数组首元素的地址,那么怎么通过数组指针遍历数组呢?

    #include 
    
    int main()
    {
        int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
        int* p = arr;
        int i = 0;
        for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
        {
            printf("%d ", *(p+i));
        }
        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,10 };
        int(*pArr)[10] = &arr;
        int i = 0;
        for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
        {
            printf("%d ", (*pArr)[i]);
        }
        printf("\n");
        for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
        {
            printf("%d ", *(*pArr+i));
        }
        printf("\n");
        for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
        {
            printf("%d ", pArr[0][i]);
            //等价于
            //printf("%d ", *(*(p) + 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
    • 25
    • 26
    • (*pArr)拿到的是整个数组的地址,而整个数组的地址是有arr来维护的,其实*pArr[0]等价于arr[0],也就可以理解为 *pArr <==> arr
    • 第二种写法:*pArr等价于arr,那么其实它就是首元素的地址,那么*pArr+i其实拿到的是下标为 i 元素的地址,再对其解引用拿到的就是这个元素
    • 第三种写法:pArr[0]其实等价于*(pArr+0),也就是说 pArr[0][i]<==> *(*(p+0) + i))

    上面的那些写法让人决得很变扭,一般不会这么写。

    来看看正常的数组指针的使用,一般是用于二维数组

    我们通过函数来打印数组正常传参,传过去的是二维数组就通过二维数组来接收。其实还可以通过数组指针来接收。

    #include 
    
    void prt1(int arr[3][4], int row, int col)
    {
        int i = 0;
        for (i = 0; i < row; i++)
        {
            int j = 0;
            for (j = 0; j < col; j++)
            {
                printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
    
    }
    void prt2(int (*p)[4], int row, int col)
    {
        int i = 0;
        for (i = 0; i < row; i++)
        {
            int  j = 0;
            for (j = 0; j < col; j++)
            {
                printf("%d ", *(*(p + i) + j));
            }
            printf("\n");
        }
    }
    int main()
    {
        int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
        prt1(arr, 3, 4);
        prt2(arr, 3, 4);
    
        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
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    数组名是首元素地址,而二维数组的数组名则是二维数组第一行的地址。

    那么*(*(p+i)+j)是什么意思呢?

    • p 是第一行的地址
    • p+i 是跳过i行后的那一行的地址
    • *(p+i) 跳过i行之后的那一行,也就相当于这一行的数组名
    • (*(p+i)+j) 跳过i行后的那一行的下标为j的元素地址
    • *(*(p+i)+j) 跳过i行后的那一行的小标为j的元素

    需要注意的是,这里传递过去的是数组数组名,也就是数组首元素的地址,也就是二维数组的第一行。

    再来看一段代码

    #include 
    
    int main()
    {
    	int arr[3][4] = { 0 };
    	int (*p1)[4] = arr;//二维数组首元素,也就是第一行的地址
    
    
    	int (*p2)[3][4] = &arr;//这是整个二维数组的地址
    	printf("%p\n", p1);
    	printf("%p\n", p2);
    	printf("==============\n");
    	printf("%p\n", p1+1);
    	printf("%p\n", p2+1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:我们发现p1+1只是跳过了一行一行4个元素也就是16个字节,而p2+1跳过了整个二维数组,也就是48个字节。说明二维数组其实和一维数组,数组名和&数组名是一样的。

    0000002D8135F4B8
    0000002D8135F4B8
    ==============
    0000002D8135F4C8
    0000002D8135F4E8
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4) 简单汇总

    //这是普通数组
    int arr[10];
    // pArr1和[]结合这是一个数组,int*结合,所以这一个 指针数组
    int *pArr1[10];
    // *和pArr2结合,这是一个指针,指向的是一个存放10个元素的数组,数组的类型是Int。所以这是一个指针数组
    int (*pArr2)[10]; 
    // pArr3和[]先结合这是一个数组,它是存放数组指针的数组
    int (*pArr3[10])[5]; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    解释一下最后一个数组

    pArr3和[]结合他是一个数组,每个数组有10个元素

    把数组名去掉,剩下的就是元素类型 int (*)[5],这是一个指针指向一个元素5个元素的数组,每个数组的元素类型是int

    比如 int (*p)[5] = &arr; 这是一个数组指针,假设把p去掉 int(*)[5],剩下的就是它的元素类型了。

    pArr3[10]是一个存放数组指针的数组,数组有10个元素

    所以这是一个

    int main()
    {
        int arr1[5] = { 0 };
        int arr2[5] = { 0 };
        int arr3[5] = { 0 };
        int (*pArr3[10])[5] = { &arr1,&arr2,&arr3 };
    
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    4. 数组和指针传参

    1) 一维数组传参

    #include 
    //数组传参数组接收没问题
    void test1(int arr[]){}
    //数组传参数组接收写了元素类型也没问题(这本质是一个指针,写不写都一样)
    void test1(int arr[10]){}
    //传数组名,本质上就是首元素的地址,没问题
    void test1(int* arr){}
    //传指针数组,指针数组接收每问题
    void test2(int* arr[20]){}
    //指针数组的数组名本质上也是一个地址,数组的每个元素都是int* 类型,那它的首元素就是一个一级指针,用一个二级指针接收一级指针每有任何问题
    void test2(int** arr){}
    int main()
    {
    	int arr[10] = { 0 };
    	int* arr2[20] = { 0 };
    	test1(arr);
    	test2(arr2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2) 二维数组传参

    #include 
    //传二维数组用二维数组接收每任何问题
    void test(int arr[3][5]){}
    //在C语言中二维数组的列是不能省略的!
    void test(int arr[][]){}
    //省略行每任何问题,省略行没问题,可以不知道有多少行,但必须知道每行有多少列,猜方便运算
    void test(int arr[][5]){}
    //二维数组传的是二维数组首元素也就是第一行的地址,拿一个int* 的指针来接收肯定是不行的。可以存放到数组指针去
    void test(int* arr){}
    //这是明显错误的,这么写表示这是个整形指针数组
    void test(int* arr[5]){}
    //这是正确的写法,这是一个指针指向了一个二维数组第一行的地址,数组每一个行有5个元素,
    void test(int(*arr)[5]){}
    //错误写法,这是一个二级指针,只能用来接收一个一级指针的地址
    void test(int** arr){}
    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

    3) 一级指针传参

    一个int*的指针能接收什么参数?

    #include 
    void test(int *p)
    {
    
    }
    
    int main()
    {
    	int a = 10;
    	int *pa = &a;
    	int arr[10] = {0};
        //下面三中写法都是正确的
    	test(&a);
    	test(pa);
    	test(arr);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    char*又能接受什么参数?

    char*有两种表示形式,一种是字符指针,一种是字符串,存的是字符串的收地址。

    #include 
    void test(char *p)
    {
    
    }
    
    int main()
    {
    	char c = 'a';
    	char str[] = "hello";
    	char arr[] = {'h','e','l','l','o','\0'};
    	test(&c);
    	test(str);
    	test(arr);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4) 二级指针传参

    二级指针存放的是一级指针的地址,所以传递个一级指针的地址,或者二级指针都是可以的

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

  • 相关阅读:
    lua --- 基本语法学习
    MindStudio模型训练场景精度比对全流程和结果分析
    亚马逊电动移动设备法规、标准和要求准则!!!
    浅谈免杀下的持久化
    【三维重建-PatchMatchNet复现笔记】
    【SpringMVC】_设置响应状态码与Header
    Elasticsearch:将关系数据库中的数据提取到 Elasticsearch 集群中
    Prometheus配置Basic Auth进行安全防护,实现登录控制
    echarts label fomatter a b c d含义
    23年计算机408复习规划以及高分技巧--上岸学姐总结
  • 原文地址:https://blog.csdn.net/weixin_53946852/article/details/127657595