【C语言】指针和数组的深入理解(第三期)

简介: 这里我们先阐述一个问题,为什么很多人会混用指针变量和指针呢?其实说法不是完全错误,为什么呢?请看如下代码:

1、指针变量在口语中为什么会跟指针混用?

这里我们先阐述一个问题,为什么很多人会混用指针变量和指针呢?其实说法不是完全错误,为什么呢?请看如下代码:

int main() 
{
  int a = 10; //当 a 做左值,a 是变量
  int b = a; //当 a 做右值,a本质就是10
  int* p1 = &a; //当 p1 做左值,p1是一个变量
  int* p2 = p1; //当 p1 做右值 p1 等价于 &a,也就是a的地址,即指针
  return 0;
}

看以上代码相信大家就能明白了,当指针做右值的时候,他本质上就是一个地址,地址就是指针,所以在这种情况下,指针变量可以理解成指针,但不建议这么理解,之所以在前两期我没有强调这个问题,是怕初学者会弄混淆了,有了前两期的学习,在来看这个问题就很轻松了。

但是我们心里面还是要把指针和指针变量区分开来,知道什么时候是混用什么时候不是混用的,为了书面表达以及名词阐述,后面的内容在口语上可能会混用,但是你们自己要有一个谱,还是建议要区分清楚这两个概念。

2、指针数组和数组指针

2.1 什么是指针数组

很多小伙伴在接触指针数组和数组指针的时候是非常头疼的,总会混淆两个的概念, 那么本期会以一个很清晰讲解带大家深入理解:

我们再来回顾一遍数组的定义:具有相同类型元素的集合,我们称为数组

指针是类型吗?不是,指针只是地址,那 int* 是类型吗?double* 是类型吗?是的!分别是整型指针类型,双精度浮点型指针类型,假设我有十个整型指针,也就对应其中十个变量的地址,我们可以把它们放到一个数组里吗?可以的!这就是指针数组:存放指针的数组,每个指针的类型都是一样。

它的语法格式是这样的:int* p[10]  意思是这个数组有十个元素,每个元素类型是 int*,那么指针数组是数组还是指针?数组!存放指针的数组!

2.2 什么是数组指针?

上期我们也学习过 &arr,说它要放在一个数组指针里,我们今天就来探讨下什么是数组指针:

前面我们也了解过,int* p 它是一个可以指向整型数据的指针变量,float* p 它是一个可以指向单精度浮点数数据的指针变量。那么可以理解 *p 表示他是个指针变量,int 就是它对应指向的数据类型,那我如果要指向一个数组呢?

数组的类型是什么?int arr[10] 其实在C语言中这样定义数组在阅读上是有点变扭的,像Java中int[10] arr, 是定义数组,上期也讲过,数组拿 int arr[10] 来说,他的类型就是 int[10],一个数组十个元素,每个元素是 int 类型。所以我们要拿指针去指向int[10] 这种类型该如何写呢?

int (*p)[10] 这样就是我们的数组指针,因为 [ ] 的优先级比 * 高,所以我们先 p 跟 * 结合,表示它是一个指针变量,指向的类型是 int[10],刚好跟我们 int* p 的说法吻合。

数组指针是指针!是一个存放数组地址的指针变量。(有时候为了说法方便,会混用指针和指针变量,大家要自己注意区分)

2.3 指针数组和数组指针的布局

为了方便大家的理解,请看以下图片,但是这并不是真实的内存布局。就好似指针变量并没有一根线去指向一个地址,他只是一个变量存放着地址。

所以看到这里,你应该更能理解上一期取地址数组名与数组名的区别了吧!

2.4 数组指针的使用

简单的使用:

void printArr1(int arr[3][5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
            printf("\n");
    }
}
void printAarr2(int(*arr)[5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    printArr1(arr, 3, 5);
    printAarr2(arr, 3, 5);
    return 0;
}

在我们打印数组中,可以用数组指针来接收,数组名是 arr,表示首元素地址,但是二维数组的首元素的第一行,所以我们这里传递的首元素地址本质是第一行一维数组的地址,所以我们可以用数组指针来接受,因为数组指针是一个指向数组的指针。

3、多维数组和多维指针

3.1 二维数组

二维数组,其实也是个麻烦的地方,很多书中都把二维数组画成一个几行几列的图形,这样图确实可以帮助我们理解,但是我们也要清楚一点,这些是示意图,并非真正的内存布局。下面我们来通过代码来看看二维数组在内存中如何布局的:

通过打印每个元素的地址,我们也能发现,二维数组在内存中的布局是连续的。如果我们要画比较真实的内存布局的话,应该这么画:

数组的定义是:具有相同类型元素的集合,数组当中可以保存任何类型,这里的二维数组图是不是相当于数组保存了数组,一个数组有三个元素,每个元素保存了三个数组类型 int[3],其实我们完全可以理解成,这些二维数组可以当成一维数组来看,只不过它内部的元素也是数组而已,这里就能说明,既然一维数组是线性递增的,而一维数组每个元素又保存的数组,所以整体二维数组也是线性递增的,所以:二维数组可以被看作内部元素是一维数组的数组。

那既然是连续的,我们第一次打印数组每个元素的内存时,是不是可以换种方法打印?

int main() 
{
  int arr[3][3] = { 0 };
  int* p = (int*)arr;
  for (int i = 0; i < 3 * 3; i++)
  {
    printf("%p\n", p + i);
  }
  return 0;
}

但是我们通常也不会这么去做,下面有一道关于二维数组的题,如果你能看懂,并且能自己分析,那就证明对数组的理解没有问题了,一定要注意数组名是什么的问题!

int main()
{
  int arr[3][3] = { 0 };
  printf("%d\n", sizeof(arr)); //36->计算的是整个数组的大小
  printf("%d\n", sizeof(arr[0][0])); //4->算的是第一个元素的大小
  printf("%d\n", sizeof(arr[0])); //12
  //arr[0]相当于第一行一维数组的数组名,单独放在sizeof内部
  //所以算出的是第一行一位数组的大小
  printf("%d\n", sizeof(arr[0] + 1)); //4
  //arr[0]相当于第一行一维数组的数组名,数组名表示首元素地址,+1则跳过一个数组元素类型大小
  //数组名没有单独放在sizeof内部,也没有取地址,所以求的是第一行第二个元素的大小
  printf("%d\n", sizeof(*(arr[0] + 1))); //4
  //arr[0] + 1相当于第二行数组名,对第二行数组名解引用,没有&,也没有单独放在sizeof内部
  //所以求的是第二行第一个元素的大小->数组名代表首元素地址
  printf("%d\n", sizeof(arr + 1)); //4或8
  //arr表示数组首元素地址,+1表示跳过一个元素,所以求得是第二个元素地址的大小
  //地址的大小在32位平台下为4字节,64位平台为8字节
  printf("%d\n", sizeof(*(arr + 1))); //12
  //数组名代表首元素地址,arr表示数组首元素,也就是第一行的一维数组
  //arr+1就是第二行的一维数组的地址,对一维数组的地址解引用,访问的是整个数组,所以是12
  printf("%d\n", sizeof(&arr[0] + 1)); //4或8
  //arr[0]表示数组首元素,也就是第一行的一维数组,取地址取出的是一维数组的地址
  //对数组+1本质跳过一个数组,也就表示第二行的地址,但它并没有单独出现在sizeof内部
  //表示的是第二行首元素的地址,地址大小是4或8字节
  printf("%d\n", sizeof(*(&arr[0] + 1))); //12
  //&arr[0] + 1取出的是第二行一维数组的地址,对整个一维数组的地址解引用,访问的是整个数组
  printf("%d\n", sizeof(*arr)); //12
  //数组名代表首元素地址,解引用访问第一个元素,二维数组的第一个元素是第一行的整个一维数组
  //所以*arr也就是第一行的数组名即arr[0],单独放在了sizeof内部,计算的是整个一维数组的大小
  printf("%d\n", sizeof(arr[2])); //12
  //arr[2]表示二维数组的第二个元素也就是第二行的数组名,同上,计算的是整个一维数组的大小
  return 0;
}

3.2 二级指针

在学习二级指针之前,我们要了解,指针变量是一个变量,既然是变量就有地址,如果我们要保存一个一级指针变量的地址该如何做呢?用二级指针变量来存放一级指针变量的地址!

简单使用:

二级指针, 当 pp 第一次解引用,访问的是 p 指针变量,如果此时进行赋值,则是修改了 p 里面存放的地址,当 pp 第二次解引用,本质是先访问 p 指针变量,然后对 p 指针变量解引用,最后也就是访问 p 保存地址对应的内容。

至于超过二维的数组,和超过二级的指针,一般用的很少,感兴趣的可以按照上面分析的方法下来自己研究研究。

3.3 一道面试题

int main()
{
  int arr[5][5];
  int(*p)[4];
  p = arr;
  printf("%p,%d\n", &p[4][2] - &arr[4][2], &p[4][2] - &arr[4][2]);
  return 0;
}

我们结合图解,再来分析这道题的解法:

这道题,首先要找出 &arr[4][2] 所对应的地址,这个很简单,可是 &p[4][2] 对应的地址如何找呢?首先我们知道 指针+1 跳过的是对应类型的大小,而 p 的类型是 int[4] 的类型,所以他 +1 可以跳过四个整型,而 p[4][2] 又可以写成 *(*(p+4)),这样一来 p + 4 先是跳过了 16 个整型,因为是二维数组,解引用也就是相当于找到了一维数组,站在 p 的角度 +4 之后解引用可以访问后续的一维数组,在进行 +2 解引用,则跳过了两个整型,就来到了图中的位置。

首先我们来看图,&arr[4][2] 的地址肯定是要高于 &p[4][2] 的,我们也知道,指针相减,得到的是之间的元素个数,所以 &arr[4][2] - &p[4][2] 得到的肯定是 -4,那如果以 %p 打印的话是打印无符号进制,我们将 -4 的补码当成无符号数打印出来就是:0xFFFFFFFC

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