深入理解指针2


通过上一期我们已经对指针有了一个基础的了解,那么这期让我们学习一下一维数组传参的本质,二级数组和数组指针。在此之前我们先要了解一下对数组名的理解。

1.数组名的理解

1.1数组名的理解

当我们用指针访问数组时,会有这样的代码

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

这里我们使用 &arr[0] 的方式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址。
让我们来测试一下arr和&arr[0]:

// An highlighted block
#include <stdio.h>
int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("&arr[0] = %p\n", &arr[0]);
 printf("arr = %p\n", arr);
 return 0;
}

输出结果:
在这里插入图片描述
我们可以发现数组名和数组首元素打印出来的地址一模一样,由此我们可以推出:
数组名就是数组首元素的地址

1.2arr的理解

这里我们要注意一下关于arr的理解:
1.sizeof(数组名): sizeof中单独存放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2.&数组名: 这里的数组名名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

除此之外,任何地方使用数组名都表示数组首元素地址
那么这时候或许会有人好奇arr和&arr有什么区别呢?

1.3arr和&arr的区别

arr 是一个数组时,arr&arr 在大多数情况下指向相同的地址,但它们的类型和语义不同。

arr

  • arr 代表数组的首元素地址,即 &arr[0]
  • 类型是指向数组元素类型的指针。例如,如果 arrint 数组,arr 的类型是 int*
  • 在表达式中,arr 会退化为指向首元素的指针(如函数传参时)。

&arr

  • &arr 是整个数组的地址,与 arr 的地址值相同,但类型不同。
  • 类型是指向整个数组的指针。例如,如果 arrint[5]&arr 的类型是 int(*)[5]
  • &arr + 1 会跳过整个数组的大小,而 arr + 1 只会跳过一个元素的大小。

示例代码

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("arr: %p\n", (void*)arr);      // 首元素地址
    printf("&arr: %p\n", (void*)&arr);    // 整个数组地址
    printf("arr + 1: %p\n", (void*)(arr + 1));    // 跳过 4 字节(int 大小)
    printf("&arr + 1: %p\n", (void*)(&arr + 1));  // 跳过 20 字节(5 * int 大小)
    return 0;
}

输出结果
假设 arr 的地址是 0x7ffd12345670,输出可能如下:

arr: 0x7ffd12345670
&arr: 0x7ffd12345670
arr + 1: 0x7ffd12345674
&arr + 1: 0x7ffd12345684

关键区别

  • 类型不同:arrint*&arrint(*)[5]
  • 指针运算不同:arr + 1 跳过一个元素,&arr + 1 跳过整个数组。
    arr 是一个数组时,arr&arr 在大多数情况下指向相同的地址,但它们的类型和语义不同。

2.使用指针访问数组

假设有一个整型数组 arr,可以通过指针遍历其元素:

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr指向数组首元素
for (int i = 0; i < 5; i++) {
    printf("%d ", *(ptr + i)); // 输出arr[i]
}
 

注意事项
指针越界会导致未定义行为。
确保指针类型与数组元素类型匹配。

3.一维数组传参的本质

首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?

// An highlighted block
#include <stdio.h>
void test(int arr[])
{
 int sz2 = sizeof(arr)/sizeof(arr[0]);
 printf("sz2 = %d\n", sz2);
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int sz1 = sizeof(arr)/sizeof(arr[0]);
 printf("sz1 = %d\n", sz1);
 test(arr);
 return 0;
}

输出结果:
在这里插入图片描述
我们发现在函数内部是没有正确获得数组的元素个数。我们已经知道数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

// An highlighted block
void test(int arr[])//参数写成数组形式,本质上还是指针 
{
 printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式 
{
 printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩ 
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 test(arr);
 return 0;
}

在这里插入图片描述
在64位操作系统上一个内存地址用八个字节表示。
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

4.二级指针

4.1一级指针与二级指针的区别

一级指针直接存储普通变量的地址:

int a = 10;
int *p = &a; // p是一级指针,存储a的地址

二级指针存储一级指针的地址:

int a = 10;
int *p = &a;
int **pp = &p; // pp是二级指针,存储p的地址

4.2二级指针的声明与初始化

声明二级指针需要两个星号:

int **pp; // 声明一个二级指针

初始化二级指针需要先有一个一级指针:

int a = 10;
int *p = &a;
int **pp = &p; // pp指向p

通过二级指针可以间接访问原始变量的值:

int a = 10;
int *p = &a;
int **pp = &p;

printf("%d\n", **pp); // 输出10

**pp先通过*pp找到p,然后对p进行解引用操作:*p。那找到的就是a了。
理解
pp->二级指针的名字
*pp->对二级指针解引用代表一级指针p
**pp->先找到一级指针再对其解引用找到a

4.3二级指针在数组中的应用

二级指针可以用于指向指针数组:

int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
int *parr[] = {arr1, arr2}; // 指针数组
int **pparr = parr; // 二级指针指向指针数组

printf("%d\n", pparr[0][1]); // 输出2
printf("%d\n", pparr[1][2]); // 输出6

注意事项

  • 二级指针不能直接指向静态分配的多维数组,因为数组名是常量指针。
  • 避免将数组名强制转换为二级指针,可能导致未定义行为。
  • 二级指针主要用于处理指针数组或动态分配的内存,但在静态分配中也有其用途。

以下是一个完整的示例,展示二级指针在静态分配中的使用:

#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;
    int **pp = &p;

    printf("a = %d\n", a);
    printf("*p = %d\n", *p);
    printf("**pp = %d\n", **pp);

    int arr1[] = {1, 2, 3};
    int arr2[] = {4, 5, 6};
    int *parr[] = {arr1, arr2};
    int **pparr = parr;

    printf("parr[0][1] = %d\n", pparr[0][1]);
    printf("parr[1][2] = %d\n", pparr[1][2]);

    return 0;
}

5.指针数组

5.1指针数组的概念

指针数组是指针还是数组呢?
我们可以类比一下,整形数组是存放整形的数组,字符数组是存放字符的数组,那么指针数组就是存放指针的数组了。
而指针数组又会整形指针数组,字符指针数组等等,形式为int* arr[n],char* arr[n]

我们这里先以整形指针数组示范讲解
指针数组每个元素都是用来存放指针(地址)的,里面的每个元素都是地址,又可以指向一块区域。

5.2指针数组的使用

// 指针数组
#include <stdio.h>
int main()
{
 int arr1[] = {1,2,3,4,5};
 int arr2[] = {2,3,4,5,6};
 int arr3[] = {3,4,5,6,7};
 //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 
 int* parr[3] = {arr1, arr2, arr3};
 int i = 0;
 int j = 0;
 for(i=0; i<3; i++)
 {
 for(j=0; j<5; j++)
 {
 printf("%d ", parr[i][j]);
 }
 printf("\n");
 }

示图理解

指针数组parr里的每一个元素都指向一个数组,就可以根据这个元素来访问对应的数组元素。parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。我们也可以用这样的方发来用指针模拟二维数组
但这只是模拟二维数组,并不是真的二维数组,因为每一行的地址都不是连续的。

这期关于指针的讲解到此结束,我们下期再见👋

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值