【c语言】指针就该这么学(2)

简介: 本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。

一、指针访问数组

1.理解数组名

       之前我们使用指针访问数组时,使用了如下方法:

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];

这里我们使用&arr[0]来得到数组首元素的地址。不过,数组首元素还有其他表示方法:数组名


在c语言中,除了以下两种情况,数组名都表示首元素地址


1.sizeof(数组名)。sizeof中单独放数组名时,数组名表示的是整个数组,sizeof计算的就是整个数组的大小。


2.&数组名。此时数组名表示的也是整个数组,取地址取出的是整个数组的地址(虽然值与首元素地址相同,但是两者类型有区别)


2.使用数组名访问数组元素

       接下来,我们尝试使用数组名来访问数组元素:

#include <stdio.h>
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(arr + i));//数组名表示首元素地址
    }
    return 0;
}

由于数组名就是首元素地址,我们就可以对其进行加减操作,访问数组中的元素。


我们还可以这么写:

#include <stdio.h>
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = arr;//将首元素地址赋值给指针变量p
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}

运行结果:



以上代码中,我们将数组名传给了指针变量p,程序仍然能打印出数组元素,说明数组名arr等价于&arr[0],也等价于p


       我们都知道,可以使用arr[i]的方法表示数组元素,既然arr等价于p,那么我们是否可以用p[i]来访问呢?

#include <stdio.h>
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = arr;
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", p[i]);
    }
    return 0;
}

运行结果:



可以看到,效果也是一样的。实际上,p[i] 和 *(p+i)是等价的。使用下标引用操作符的时候,编译器也是通过找到首元素地址和偏移量并且执行解引用操作来实现数组元素的访问。


二、一维数组传参

       接着我们来讨论一下一维数组传参的本质。我们都知道,数组名表示首元素地址,那在数组传参的时候,传入数组首元素的地址就可以对数组进行操作了。但是由于函数接收的只是一个地址,无法知道数组的具体大小,可能会造成数组越界的情况,所以再需要传入一个变量来表示数组的元素个数。


       我们来写一个函数,实现打印数组的功能:

#include <stdio.h>
 
int print(int arr[], int sz)//数组形式的形参
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(arr, sz);//传入数组的首元素地址和元素个数
    return 0;
}

既然数组名是数组首元素的地址,那么我们也可以将形参写成指针形式:

#include <stdio.h>
 
int print(int *arr, int sz)//指针形式的形参
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(arr, sz);
    return 0;
}

运行结果:



三、二级指针

       我们之前学习的指针,例如int*,char*,float*等等,它们都是一级指针。而二级指针又是什么呢?首先看一段代码:

#include <stdio.h>
 
int main()
{
    int a = 10;
    int* pa = &a;
    return 0;
}

这里使用指针变量p来接收a的地址。既然p是一个变量,那么它本身也有地址。它的地址如何存放呢?这里就需要使用二级指针

#include <stdio.h>
 
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;//这里的ppa就是一个二级指针变量
    return 0;
}


如果想要通过二级指针ppa来访问变量a,就需要两次解引用操作,首先找到pa,然后找到a。例如:

#include <stdio.h>
 
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    **ppa = 30;//两次解引用操作
    printf("%d\n", a);
    return 0;
}


可以看到,a的值被成功修改成了30。


       当然,二级指针变量也是变量,它也有地址,可以存放在三级指针变量中。不过更高级指针往往在一些复杂程序中出现,我们目前还不会接触到。


四、指针数组

1.指针数组的概念

       我们都知道,数组就是一组相同类型元素的集合。例如整形数组中存放的都是整形变量,字符数组中存放的都是字符型变量。同理,指针数组就是存放指针变量的数组,数组中每一个元素的类型是指针类型


和定义整形数组的方式类似,定义整形指针数组的方式是:


int *arr[10];


这表示一个整形指针数组,数组中存放10个元素。每个元素都是int*类型的指针变量。



2.指针数组的使用

       当我们需要创建多个同类型的指针变量时,我们就可以创建一个指针数组。例如:

#include <stdio.h>
 
int main()
{
    int a = 0;
    int b = 0;
    int c = 0;
    int* parr[3] = { &a,&b,&c };//三个指针变量分别指向a,b,c
    for (int i = 0; i < 3; i++)
    {
        *parr[i] = i + 1;//通过指针修改变量的值
    }
    printf("a=%d b=%d c=%d\n",a, b, c);
    return 0;
}

运行结果:



这里我们定义了一个指针数组,数组中分别存放a,b,c的地址,然后通过指针来修改a,b,c的值。


3.使用指针数组模拟二维数组

       接下来,我们来一个关于指针数组的高级操作:模拟二维数组。我们知道二维数组可以看成是一维数组的数组,那么我们就可以使用指针数组来分别存放每一个一维数组首元素的地址来模拟出一个二维数组。



代码实现:

#include <stdio.h>
 
int main()
{
    //创建三个一维数组
    int arr1[5] = { 1,2,3,4,5 };
    int arr2[5] = { 2,3,4,5,6 };
    int arr3[5] = { 3,4,5,6,7 };
 
    //将三个数组的首元素地址赋给指针数组
    int* parr[3] = { arr1,arr2,arr3 };
 
    //访问模拟的二维数组
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

运行结果:



这里的“parr[i]”表示解引用操作找到了指针数组的某一个元素,也就是整形数组的首元素地址。“arr[i][j]”表示通过整形数组的首元素地址,解引用操作找到了整形元素。需要注意的是:这只是使用指针数组模拟出的二维数组,并不是真正的二维数组,因为三个整形数组之间不一定是连续存放的。

五、数组指针

1.数组指针的概念

       之前我们学习了指针数组,接下来我们探讨一下数组指针。要注意:这两者可是完全不同的概念,前者本质是数组,后者本质是指针。顾名思义,数组指针就是指向数组的指针,它存放的是整个数组的地址。数组指针的定义方式:


int (*p)[10];


从定义方式上来看,它就是在指针数组的基础上使用括号将变量名和 * 号结合起来了,它表示p是一个指针变量。这里的 int 表示它指向的数组的元素类型,后边的 [10] 表示的是它指向的数组有十个元素。


这里有一个知识补充:[](下标引用操作符)的优先级要高于 *(解引用操作符),所以加括号才有意义。


2.数组指针初始化

       既然数组指针存放的是整个数组的地址,那么初始化时将整个数组的地址赋值给数组指针变量就好了。&数组名得到的就是整个数组的地址。

#include <stdio.h>
 
int main()
{
    int arr[5] = { 0 };
    int(*p)[5] = &arr;
    return 0;
}

3.数组指针类型的理解

       首先观察以下程序:

#include <stdio.h>
 
int main()
{
    int arr[10] = { 0 };
    int(*p1)[10] = &arr;
    int* p2 = arr;
    printf("%p\n", p1);
    printf("%p\n", p2);
    return 0;
}

在这里,我们打印了数组的地址和数组首元素的地址。可以先猜猜运行结果是什么。。


运行结果:



结果表明,这两者的地址是一样的。这样就会有人想:数组的地址难道就是首元素的地址吗?其实两者是有区别的。我们观察一下调试窗口:



可以看到,两者的值虽然相同,但是它们的类型是不同的。数组首元素的地址类型是int* ,而数组地址的类型是 int[10]* ,也就是数组指针类型。结合我们之前学过的指针知识,不同的指针类型决定了它访问变量时的权限和步长


六、二维数组传参

       在学习了数组指针之后,我们就可以探讨二维数组传参的本质了。我们首先编写一个程序,以函数的方式实现二维数组的打印:

#include <stdio.h>
 
void print(int arr[3][3], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
 
int main()
{
    int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
    print(arr, 3, 3);
    return 0;
}

运行结果:



这里我们传入了二维数组的数组名,行数和列数,函数的形参也写成了数组形式,那么如何理解二维数组的数组名呢?


       既然数组名是数组首元素地址,那么二维数组也不例外。


二维数组的数组名代表二维数组的首元素的地址,也就是第一行的地址。由于第一行是一个一维数组,所以它就是这个一维数组的地址。


既然传入的是一个一维数组的地址,那么我们也可以用数组指针的形式表示形参:

#include <stdio.h>
 
void print(int(*arr)[3], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
 
int main()
{
    int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
    print(arr, 3, 3);
    return 0;
}

当然,我们在访问二维数组元素的时候,还可以这么写:


*(*(arr+i)+j);


以上语句是什么意思呢?首先,arr是第一行的地址,给它加上i,就相当于跳过了i行。之后使用 * 对这一行的地址进行解引用操作,得到这一行(也就是这一行首元素的地址)。给这个地址再加上j,就相当于跳过了j个元素,最后再对这个元素的地址进行解引用操作,就得到了这个元素的值


       以上的操作可能感觉比较复杂,但是本质上也就是通过地址来访问元素而已。当然,它与arr[i][j]是等价的。这里要特别注意:以上的形式是把指针解引用了两次,你可能会认为它是一个二级指针,实际上它跟二级指针是没有关系的,不要将它与二级指针混淆


总结

       这篇文章我们主要探讨了指针与一维数组、二维数组之间的关系。后续博主还会继续和大家探索指针的更多知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
1月前
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
35 1
|
17天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
43 0
|
16天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
12 2
|
17天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
17天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
23天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
23天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
23天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
23天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
23天前
|
C语言
C语言指针(2)
C语言指针(2)
12 1