文章目录


前言

学习指针真的很让人头疼,最近几天无时不刻在想怎么使用它,现在又学了数组指针和指针数组,老实说,明明只是几个字调换了个位置就能有这么大差别,于是我想分享一下关于我对它们俩的看法


一、数组和指针的类型区别

       指针数组和数组指针,顾名思义,谁在后面,谁就是重点,因为写出来过于繁杂,所以我想先从类型的区别说起,类型嘛,就是区别这个变量和那个变量代表什么,就比如int a和int* a,他们的类型就是int 和 int*;那类比来看,int* a[ ]和int(*a)[ n],他们的类型分别就是int*[ ]和int(*)[ n](注意,这里的n不能省略,这也与指针类型的作用有关)因为从操作符的优先级来看,' * '的优先级是低于' [ ] '的,所以用 ' () '将' * '单独提出,也是为了告诉我们,这个类型重点是指针。(倒是可以想成去掉变量名就是类型,不过这并不符合概念)

        接着看int*a[ ]和int(*a)[n],从意义上我们可以分辨出,int*a[ ]中,a为指针数组的数组名,

[ ]为数组包含的元素个数,而int*代表数组中包含元素的类型为指针;int(*a)[n]中,a为它的数组指针变量名,而其中的“ * ”只有一个作用,就是说明修饰的变量是指针变量[n]为该指针指向数组包含的元素个数,而int为指向数组中的元素的类型。

二、具体区分二者

1.定义和初始化

代码如下:

int main()
{
    //指针数组
    int a = 10;
    int b = 20;
    int c = 30;
    int* p[]={&a, &b, &c};
    //类比整型数组
    int p_arr = {a, b, c};

    //数组指针
    int arr[] = {1,2,3};
    int(*p)[3] = &arr;
    //类比整型指针
    int* p_int = arr;
    return 0;
}

        可以看到,因为指针数组就是一个数组里面放了几个指针,数组指针就是一个指针指向了整个数组,所以此处的&arr并不是arr数组的首地址的地址什么奇怪的值,而是取出了整个数组的地址。但要注意,虽然如此,p的初始指向还是和p_int相同,指向数组首地址。

(p和p_int地址的具体区别留在之后说)

2.数组指针的指向

       上文的例子说到,数组指针和整型数组指向一个数组时初始指向相同,那么怎么区分它们呢?

这里用实例说明更方便,代码如下:(运行结果是VS2022中拷贝过来,x86环境)

#include <stdio.h>
int main()
{
    int arr[]={1, 2, 3};
    int(*p) [3] = &arr;
    int* p_int = arr;
    //比较数值
    printf("%d\n", *(*p));
    printf("%d\n", *p_int);
    //比较地址
    printf("%p\n", &arr);//或arr
    printf("%p\n", &arr + 1);//或p + 1
    printf("%p\n", arr + 1);//或p_int + 1
    return 0;
}

        我们先从比较数值来直观证明两者的指向问题。首先,由于数组指针中存放的是整个数组的地址,若我们要得到指针的内容就必须先找到数组首元素地址(第一次解引用),再找到首元素地址对应的元素(第二次解引用),接下来比较结果,可以看到都为‘1’,这就是因为初始指向相同,解引用出对应元素自然就是相同的。

那么我们再通过比较地址来说明指针类型的具体作用,也是为什么上文说int(*a)[n]中的n不能省略的原因。从类型中我们知道,该数组指针指向一个int[3]类型的数组,而类型的作用之一就是决定指针加减时一次的跨度,比如此时,数组指针加一就会跨过整个数组,以下用简单的草图演示以下:

应该是比较直观能看出来p+1后的位置变化,而p+1的位置显然已经超出数组了,如果此时再解引用就造成数组越界访问,使用野指针,所以我们比较地址。能看到,数组首元素地址(第一个地址)与p+1后的结果(第二个地址)相差12byte,这说明其跨越了整个数组(3*4byte=12byte)的长度。而最后一个地址是p_int+1后的地址,根据上图能看出指向第二个元素的地址,所以与首元素地址相差4byte(虽然数据的存储用的是二进制,但为了方便显示会使用16进制打印出来)

注:由于p指向一个int[3]类型的数组,p_int指向int类型的数组首元素,所以+1后跨度被指向的类型决定为跨越int[3]或int的长度,这就是为什么int(*p)[3]的元素个数不能省略的原因,否则编译器不知道往后访问的具体长度。

3.指针数组的包含

        数组指针可以通过指针位置的移动来实现上述效果,那么指针数组呢?代码如下:

#include <stdio.h>
int main()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int* p[] = {&a, &b, &c};
    int p_int[] = {a, b, c};
    //比较数值
    printf("%d\n", *(*p));
    printf("%d\n", *p_int);
    //比较地址
    printf("%p\n", *p);
    printf("%p\n", p);
    printf("%p\n", p_int);
    return 0;
}

我们依旧类比整型数组,奇怪的是,明显类型不同,但是比较方式却和数组指针的几乎一样,我的理解是数组名代表首元素地址,而指针的初始指向也是首元素地址,所以有很多相似之处,但它们有个根本区别就是,数组名是一个常量若想直接让它自增自减是不行的,但指针是变量,操作会灵活很多,修改指向的数组、指针算数等。回到这个例子,(*p)的结果应该是数组的首元素,

*(*p)的结果应该是将首元素指针解引用得到a的值,而(*p_int)自然就是首元素a的值。我们检验一下:

可以看出,结果确实无误。

        我们继续比较地址,其实这里的比较地址并没有意义,为什么呢?因为我们知道,定义一个变量需要向内存申请一个空间,而这里的p和p_int两个变量实际上是两个单独开辟的空间,没有任何关系,之所以上文中的指针可以比较是因为它们指向同一个地址,若指针指向也不一样,比较也毫无意义。同理,p中的&a也与p的地址无关,因为在创建变量a时,就已经向内存申请了一个空间了,也就是说它们开辟空间时有明显的先后顺序,在栈区中的位置不一样,完全无关。

4.使用场景

        说了那么多,还是不知道创建出这两个复杂类型的作用,那么我们来看看具体的使用场景:

1.指针数组

#include <stdio.h>
#include <stdlib.h>
int main()
{
    //1.存储字符串
    char result[] = "hello";
    char* p_result[] = {"hello","world"};
    for (int i = 0; i < 2; i++)
    {
        printf("%s\n", p_result[i]);//或*(p_result+i)
    }
    //2.管理申请的动态空间
    char* arr[] = (char*)malloc(20*sizeof(char));
    arr[0]='\0';
    //3.存放数组
    int arr1[] = {1, 2, 3, 4};
    int arr2[] = {2, 3, 4, 5};
    int arr3[] = {3, 4, 5, 6};
    int* p_arr[3] = {arr1, arr2, arr3};
    int i = 0;
    int j = 0;
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 4; j++)
        {
            printf("%d ", p_arr[i][j]);//或(*(*p_arr+i)+j)
        }
        printf("\n");
    }
    return 0;
}

这里列举了三种常见的使用场景,我们一一解释。首先存储字符串,为什么一定要用指针数组呢,因为字符类型只能存储一个字符,字符数组只能存储一个字符串,若要存储多个字符串需要用字符指针数组(自然不是唯一方法),这样我们想访问多个字符串就会方便很多。

        再是管理malloc申请的动态空间,首先我们的目的还是为了管理多个字符串,但不知道具体长度无法初始化,此时最好的方法就是使用malloc申请一个动态空间,而从c++官网我们可以知道malloc函数的具体参数和返回类型:

不仅需要包含头文件stdlib.h,还需要一个指针来接收,所以此时我们可以使用(字符)指针数组来接收一个存放多个字符串的空间(不是唯一方法)。

        最后是存放数组,这其实和二维数组类似,但有根本区别,这里我们还是用草图演示:

依图,我认为两者的根本区别就是,二维数组中的元素是连续存放的,打印二维数组时只需要给出首元素地址就能顺藤摸瓜找到所有元素,但指针数组中只有每个一维数组是连续存放,其中每个一维数组需要解引用之后才能得到元素,显然,元素的内存并不是连续存放的,所以在打印指针数组时需要解引用两次(与二维数组的第二次解引用概念不同),访问不同的内存,效率与二维数组不同。

2.数组指针

        代码如下:

#include<stdio.h>
void print(int (*p)str[5], int len)//int *p[]
{
    int i=0;
    for(i = 0; i < len; i++)
    {
        int j = 0;
        for(j = 0; j < 5; j++)
        {
            printf("%d ", *(*(p+i)+j));
        }
    }
}
int main()
{
    int arr[3][5] = {{1, 2, 3, 4, 5},{2, 3, 4, 5, 6},{3, 4, 5, 6, 7}};
    print(arr, 3);
    
    return 0;
}

        这是数组指针常用的场景:作为形参,接收二维数组的首地址,这也是由于数组指针的特性,即指向整个数组,能够通过两次解引用找到数组每个元素。如果此处使用指针数组,虽然从形式上似乎很合理,但实际上指针数组里存放的是许多个指针,若我们把二维数组传给它会导致类型不匹配,这是根本上的错误。

        若此处想用指针数组作为形参就是另外一个场景,此处用代码说明:

#include <stdio.h>
void print(int* p[], int len)//int** p
{
    int i = 0;
    for(i = 0;i < len; i++)
    {
        printf("%d ", *(*(p+i)));
    }
}
int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    int* arr[3]={&a, &b, &c};
    print(arr, 3);
    return 0;
}

此处若想打印指针数组,形参在此处为了类型匹配只能写指针数组或二级指针,因为二级指针是指向数组首元素的,这可以类比整型数组传参,可以使用数组名也可以使用一级指针,这也是因为一级指针指向了数组首元素,只不过前者多了一次解引用。

        

        

        


总结

        这就是一些我对数组指针和指针数组的一些浅薄的看法,其实最主要的只需要区分哪个是数组哪个是指针,数组名是常量,指针是变量,这是最明显的区别。

Logo

一座年轻的奋斗人之城,一个温馨的开发者之家。在这里,代码改变人生,开发创造未来!

更多推荐