【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语言将指针和数组的访问方式打通,让程序员在函数内,也好像可以使用数组那样进行元素访问,本质减少了编程难度。

相关文章
|
29天前
|
存储 编译器 C语言
【C语言】【指针1】指针难?看这个就够了!
【C语言】【指针1】指针难?看这个就够了!
|
2天前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
|
8天前
|
存储 安全 C语言
C语言 二级指针应用场景
本文介绍了二级指针在 C 语言中的应用,
|
29天前
|
C语言
C语言------指针
这篇文章是关于C语言中指针的实训,通过示例代码展示了指针的基本概念、定义、赋值、使用和传递,以及指针运算和指针在函数参数中的应用,如交换两个变量的值和找出两个数中的较小值。
C语言------指针
|
19天前
|
搜索推荐 C语言
指针与数组
指针与数组
47 9
|
23天前
|
存储 编译器 C语言
【C语言篇】深入理解指针2
代码 const char* pstr = "hello world."; 特别容易让初学者以为是把字符串 hello world.放 到字符指针 pstr ⾥了,但是本质是把字符串 hello world. 首字符的地址放到了pstr中。
|
23天前
|
存储 程序员 编译器
【C语言篇】深入理解指针1
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
|
26天前
|
算法 Java
双指针在数组遍历中的应用
文章深入探讨了双指针技术在数组遍历中的应用,通过实战例子详细解释了快慢指针和首尾指针的不同用法,并提供了解决LeetCode相关问题的Java代码实现。
|
26天前
|
存储 搜索推荐 C语言
C语言中的指针函数:深入探索与应用
C语言中的指针函数:深入探索与应用
|
29天前
|
存储 编译器 C语言
【C语言】指针练习题目
【C语言】指针练习题目