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

简介: 有人说,数组有什么可讲的,不就是一堆相同数据类型元素类型的集合吗,确实是这样,在我学习学校C语言的时候,我清晰的记得我们老师很笼统的讲了一下基本知识,并没有深剖,如果你认为数组简单,但它确实有你值得研究的地方。

1、数组的概念

有人说,数组有什么可讲的,不就是一堆相同数据类型元素类型的集合吗,确实是这样,在我学习学校C语言的时候,我清晰的记得我们老师很笼统的讲了一下基本知识,并没有深剖,如果你认为数组简单,但它确实有你值得研究的地方。就好比现在很多大学生学完C语言居然分不清数组名和取地址数组名的区别。

数组简单的使用并遍历打印:

#define N 10
int main()
{
  int arr[N] = { 1,2,3,4,5,6,7,8,9,10 };
  int i = 0;
  for (i = 0; i < N; i++)
  {
    printf("%d ", arr[i]);
  }
  return 0;
}

这里我们要说几个点:

  1. 在C99语法之前,[ ] 中必须是常量值,不能支持变量,在C99标准才支持了变长数组的概念
  2. 数组在定义时如果没有完全初始化,其他默认初始化为 0
  3. 如果定义数组你实在不知道初始化成什么,建议初始化为 0 即可,不然会成随机值
  4. 如果没有指定数组元素个数,则会根据初始化内容来确定元素个数

2、数组的内存布局

在讨论数组内存布局之前,我们首先要了解栈区的空间是如何使用的,通过一串代码我们来看一下:

这里我们发现,先定义的变量,地址是比较大的,后续变量一次减小,这是为什么呢?

首先a,b,c 变量都是在 main 函数中定义的,也就是在栈上开辟的临时变量,而 a 先定义则意味着需要先被开辟空间,那么 a 就先入栈,也就是我们之前所说到,栈是整体向下的,先使用高地址后使用低地址。

紧随着我们来看看数组在内存中的布局,如果按照我们上面的理解,arr[0] 的地址肯定是大于后面元素的地址,那么到底是不是这样呢?

这里我们发现,先定义的 a 的地址确实比后定义的数组所有地址都要大,这个符合我们的预期,但是为什么 arr[0] 的地址却小于后面下标的元素呢?我们先来画图看看他们的内存布局:

所以这里我们能得出:数组是线性且连续增长的,也就是数组下标随着地址的增长而增长,如果我们站在开辟空间的角度,不应该把数组认为一个个独立的元素,应该整体开辟空间!

这里有的小伙伴可能会想,这打印出来的明明不是连续的啊,从arr[0]地址处00F3FE64 到arr[1]地址处00F3FE68这中间明明隔了四个字节啊,这是因为我们是整型数组,而且 '&' 取地址取出的是元素第一个字节的地址,也就是我们上期说的取出的是最小的地址。如果你换成 char 类型数组的话,那么就很显而易见了,感兴趣的下来自己尝试一番。

3、数组名到底是什么?

其实对于这个概念,可能有很多小伙伴还模糊不清,特别是对于刚接触C语言的来说,要弄明白数组到底表示什么,其实很简单,仔细的往后看:

通过这个例子,我们可以明显看出,数组名代表首元素地址,同时 arr+1 也是跳过一个整型,也即 对应着 arr[0] 的地址,所以我们可以简单的得出一个暂定结论:数组名代表首元素地址。

这里我要告诉大家,数组名确实是表示首元素地址,但是有两个例外:

  • &数组名:取出的是整个数组的地址(后面有例子)
  • sizeof(数组名):一定是sizeof() 里面单独放数组名,计算的是整个数组的大小

除了以上两个例外,其他任何地方你见到的数组名都是代表数组首元素地址。

以上的例子只是证明数组名代表首元素地址的例子,我们下面就来看如上所述两个例外的例子:

如上例子确实证明了我们的结论:当 sizeof() 括号内部单独放数组名时,计算的是整个数组的大小,而 &数组名 取出的是整个数组的地址,如何看呢?打印地址打印的是 16进制,我们用 &arr + 1 的结果减去 &arr 的结果,得到的是 28,转换成对应的 10进制 刚好是 40,也就是说,我们 &arr + 1 跳过的是整个数组的大小,同理他也代表整个数组的地址。

这里有的小伙伴可能又有疑问了,指针+1 不是表示跳过对应类型大小的字节吗?那 arr 数组每个元素的类型是 int 啊,怎么不是跳过 4 个字节呢?

这里我们要明白,arr 数组每个元素的类型确实是 int,但是这个数组的类型是 int [10] ,如何理解呢,就好比我们定义一个指针变量,int* p,可以说 p 的类型是 int*,也就是整型指针类型,同理,我们 int arr[10],那么他的类型是不是可以这么理解:这个数组有 10个 int 类型的元素,所以他的类型也就可以理解成 int[10]。也就是 int[10] 的类型大小是 40 字节,但是我们是 &arr,它表示的是整个数组的地址,所以就可以理解成,取地址取出的指针,他指向的是整个数组,那个数组有十个元素,也就是一个指向数组的指针,即 int(*arr)[10] (下期会讲),他的类型就是 int(*)[10] ,那对应的 指针+1 也应该跳过整个数组,所以&arr+1,相较于 &arr 的差值是 40,指针就是地址,我取出数组的地址,也就是一个指针罢了。

4、数组名做左值和右值的区别

对于刚初识C语言的小伙伴,可能对数组还是马马虎虎的,会认为数组名可以当成变量一样去使用,去给他赋值,其实这种认识是错误的,我们看一个例子就知道了:

int main()
{
  int arr[10] = { 0 };
  //赋值成 1~10
  arr = { 1,2,3,4,5,6,7,8,9,10 };//err
  return 0;
}

这样直接拿起数组名赋值显然是错误的,为什么呢?前面我们就讲到,数组名大部分情况表示的都是数组首元素地址,而地址是个常量,是无法被修改的,在C语言的语法层面上就已经不允许这样操作了。

上面是数组名做左值的例子,显然这个例子是错误的,那么当数组名做右值又是什么样的含义呢?

int main()
{
  int arr[10] = { 0 };
  int* p = arr;
  return 0;
}

还是那句话,数组名代表首元素地址,但是有两个例外,但是如上代码的例子显然不是例外当中的,所以他代表的就是 arr[0] 的地址,这样一番操作,就让我们指针变量 p,有权限去访问数组的元素也可以做修改。

总结:数组名做左值是错误的,地址是常量并不能被修改或赋值,数组名做右值,本质等价于 &arr[0]  

5、以指针的形式访问和以数组的形式访问

既然上面我们提到数组名表示首元素地址,而地址就是指针,所以我们是不是也可以通过解引用地址的形式来访问数组的元素呢? 好比 arr 代表首元素的地址,那我们 *(arr) 是不是就可以访问数组的第一个元素呢?是的!如果我们 *(arr + 1),也就是代表着访问数组的第二个元素,那我们设定一个变量 i,i 每次递增1,这样下去就可以达到遍历数组的效果!(为了方便下个例子,我们本例采用字符串数组演示)

显而易见,我们确实可以通过指针的形式去访问数组,同时数组也可以用 [ ] 引用下标的方式来访问,而且数组名代表着首元素地址,地址就是指针,那我们能不能将指针变量和 [ ] 引用下标的方式结合起来呢? 【指针变量里面放的就是地址(指针)

看到这,我相信你对数组和指针又有了更深的理解,这里我们要说一点,上述的测试用例 arr 和 str 其实是有区别的,arr 的整个数组都是在栈上保存,可以被修改,虽然 str 指针变量也是在栈上保存的,但是他存放的是一个常量字符串的首地址,常量字符串放在字符常量区,不可以被修改。

这里有个问题,指针和数组的语法访问方式居然那么相似,那他们之间有什么关系呢?

他们没有任何关系!指针就是指针,指针变量在32位系统下永远占四个字节,他存放的值是某一个内存的地址,而数组就是数组,其大小与元素的类型和个数有关。

从语法层面看,他们的访问方式可以一样,但是从底层来看他们的寻址方案是完全不同的!

总结: 指针和数组指向或表示同一块空间的时候,访问方式具有相似性,你可以用指针解引用的方式,也可以用 [ ] 下标引用方案。但是我们也要明白,他们是指针和数组是两个东西,不能混为一谈。

6、为什么C语言要把指针和数组元素访问互通呢?

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

 上面的代码并没有什么问题,不过,大家可以发现,如果没有将指针和数组元素打通的话,那么在C语言中,如果有大量的函数调用而且有大量的传参,会要求程序员进行各种访问习惯的变化,只要是要求人做的,那么就有提升代码的出错率和调试的难度。

所以干脆,C语言将指针和数组的访问方式打通,让程序员在函数内,也好像可以使用数组那样进行元素访问,本质减少了编程难度。

相关文章
|
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天前
|
存储
如何通过指针数组来实现二维数组?
介绍了二维数组和指针数组的概念及其区别,详细讲解了如何使用指针数组模拟二维数组,包括定义与分配内存、访问和赋值元素、以及正确释放内存的步骤,适用于需要动态处理二维数据的场景。