单片机/C/C++八股:(二十二)数组名,以及和指针的区别(一/二维数组)

上一篇 下一篇
include <> 和 include "" 的区别


数组名,以及和指针的区别(一/二维数组)

常见误区:

  1. 数据名是第一个元素的首地址,是个常量
  2. 数据名是一个指针(是个常量指针)
  3. 数据名当函数参数的时候,传递的是指针
  4. 数组名相当于指针
  5. 数组名有特殊含义,是特殊指针
  6. 数组名不可以是左值
  7. &arr 和 arr 地址相同,所以类型一样

这些描述和解释都不准确,为什么?

1)数组名详解

一个一维数组的类型是:类型[长度] ,一个二维数组的类型是:类型[行数][列数], 不是简单的字符型、整型、浮点型......

而数组名是该数组对象的标识符(代表一块连续内存的标识符,可以理解成常量地址),其本身并不属于任何类型:

  • 在一些表达式中使用数组名时,它的类型通常被视为该数组的类型。
  • 但在大多数情况下,数组名会退化为指向首元素的指针,类型变为指针(例如 int*)。

1.1)一维数组

① 声明与类型:

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
  • arr 是一个包含 5 个 int 的数组。数组的类型是 int[5]。arr 作为标识符,在不退化时代表整个数组。

② 关键行为:

表达式 类型 含义说明
arr int*(退化后) 指向数组第一个元素 arr[0] 的指针,值=第一个元素的地址
&arr int (*)[5] 指向整个数组的指针,值=数组首地址(注意:不是 int**)
sizeof(arr) size_t 返回 5 * sizeof(int),因为未退化
arr + 1 int* 指向 arr[1],步长为 sizeof(int)
&arr + 1 int (*)[5] 地址增加 5 * sizeof(int),跳过整个数组

⚠️这里尤其要区分 arr 和 &arr ,虽然他们的值都是数组的起始地址(第一个元素的地址就是数组起始地址),但是两者的类型和含义完全不同,所以会导致指针算数行为完全不同(参考上述表格的后两行)。

示例:

复制代码
printf("%p\n", (void*)arr);      	// 如 0x1000
printf("%p\n", (void*)(arr + 1)); 	// 0x1004 (+4 字节)
printf("%p\n", (void*)&arr);     	// 0x1000 (与 arr 相同)
printf("%p\n", (void*)(&arr + 1)); 	// 0x1014 (+20 字节)

③ 函数传参(退化发生):

c 复制代码
void func(int a[]) { /* 等价于 int* a */ }
// 或
void func(int *a);
  • 此时 a 是指针,sizeof(a) 返回指针大小(如 8),无法得知原数组长度。
  • 必须额外传递长度:func(arr, 5);

1.2)二维数组

① 声明与类型:

c 复制代码
int mat[3][4]; // 3 行,每行 4 个 int
  • 数组的类型是 int[3][4]。它是一个一维数组的数组:外层数组有 3 个元素,每个元素是 int[4] 类型。

  • 二维数组的内存分布为以 arr[2][5] 为例:

② 数组名的含义:

表达式 类型 含义
mat int (*)[4](退化后) 指向第 0 行(即 mat[0],类型为 int[4])的指针
&mat int (*)[3][4] 指向整个二维数组的指针
sizeof(mat) 3*4*sizeof(int) 整个二维数组大小(未退化)
sizeof(mat[0]) 4*sizeof(int) 第一行的大小(mat[0] 是 int[4])
mat + 1 int (*)[4] 指向第 1 行(地址 + 16 字节)
&mat + 1 int (*)[3][4] 地址 + 整个数组大小(48 字节)

示例:

复制代码
printf("%p\n", (void*)mat);        // 如 0x2000
printf("%p\n", (void*)(mat + 1));  // 0x2010 (+16 字节 = 4*int)
printf("%p\n", (void*)&mat);       // 0x2000
printf("%p\n", (void*)(&mat + 1)); // 0x2030 (+48 字节)

③ 元素访问:

  • mat[i][j] 等价于 *(*(mat + i) + j)
  • mat[i] 是第 i 行,类型为 int[4],在表达式中退化为 int*

④ 函数传参(二维数组):

必须指定列数(因为编译器需知道每行宽度以计算偏移):

c 复制代码
void func(int m[][4], int rows);      // √ 推荐
void func(int (*m)[4], int rows);     // √ 等价写法(m 是指向 int[4] 的指针)
void func(int **m);                   // × 错误!不能接收二维数组(除非是动态分配的指针数组)

💡 原因:mat 退化为 int (*)[4],不是 int**。int** 表示"指针的指针",而二维数组是"连续内存块"。

1.3)数组名什么时候能取地址?

表达式 是否左值? 能否取地址?
arr ✔️ 是(数组对象) ✔️ &arr → int (*)[3][4]
arr[0] ✔️ 是(子数组对象) ✔️ &arr[0] → int (*)[4]
arr[0][0] ✔️ 是(int 对象) ✔️ &arr[0][0] → int*
arr + 1 ❌ 否(右值) ❌ 非法
arr[0] + 1 ❌ 否(右值) ❌ 非法
&arr[1] ✔️ 是(左值) ✔️ 类型 int (*)[4]

关键: 只有代表内存中实际对象的表达式(左值)才能取地址 。指针算术的结果(如 p + n)是右值,不能取地址。

正确等价关系(二维数组) :

想表达的含义 正确写法 值(地址)
第 0 行首地址 arr 或 &arr[0] &arr[0][0]
第 1 行首地址 arr + 1 或 &arr[1] &arr[1][0]
第 0 行第 1 列地址 arr[0] + 1 或 &arr[0][1] &arr[0][1]
整个数组地址 &arr 同 arr,但类型不同

2)数组名与指针的区别

类型:

  • 数组名:是一个常量地址,代表整个数组的首地址
  • 指针:是一个变量,存储某个地址值

可修改性:

  • 数组名:不能被赋值或修改(如 arr = p; 非法)
  • 指针:可以被重新赋值(如 p = arr; p++; 合法)

地址与值的含义

  • 对数组名取地址(&arr):
    • 类型是 int (*)[5](指向整个数组的指针)
    • &arr + 1 会跳过整个数组(5 个 int)
  • 对指针取地址(&p):
    • 类型是 int **
    • p + 1 跳过一个元素(1 个 int)

3)什么时候数组名会/不会退化为(常量)指针

在这些情况下,才能勉强说数组名是个常量指针

3.1)不会退化

数组名不退化的三大场景(C 标准规定),无论一维还是多维,以下情况都不会触发数组到指针的退化:

表达式(场景) 是否退化 结果
sizeof(数组名) → \rightarrow → 作为 sizeof 的操作数 ❌ 整个数组所占字节数
&数组名 → \rightarrow → 作为一元 &(取地址)的操作数 ❌ 指向数组的指针
char s[] = "hello" → \rightarrow → 用于初始化字符数组的字符串字面量 ❌

其中第一种场景:在编译期间,数组名的类型会被编译器当作 类型[长度] ,即代表整个数组,而 sizeof 函数是编译时运算符,sizeof(数组名) 等价于 sizeof(类型[长度])=长度*sizeof(类型) 。

3.2)会退化

大多数场景下,数组名会退化为指向首元素的指针 ,类型变为指针(例如 int*):

表达式(场景) 是否退化 结果
void func(int arr[]){} → \rightarrow → 作为函数参数传递时 ✔️ 此时 arr 是指针,sizeof(arr) 返回指针大小(如 8)
a ✔️ 指针
a + 1 ✔️ 指针

4)案例

c 复制代码
#include "stdio.h"

int main()
{
    int a[2][5]={1,2,3,4,5,6,7,8,9,10}
    int *ptr1 =(int *)(&a + 1);
    int *ptr2 =(int *)(*(a + 1));
    printf("%d,%d",*(ptr1 - 1),*(ptr2 - 1));
    return 0;
}

运行结果为:

复制代码
10,5

相关推荐
小成202303202653 小时前
C++~07补充
开发语言·c++
水饺编程3 小时前
第2章:Linux 基础,切换用户
linux·c语言·汇编
hold?fish:palm3 小时前
40 二叉树的直径
c++·leetcode
Lxx_Zw4 小时前
机器周期与共地:时钟基准和电压参考基准
stm32·单片机·嵌入式硬件
解决小子4 小时前
【虚幻引擎】【C++】【GAS框架】构建角色基类、属性集、数值钳制
c++·笔记·游戏引擎·虚幻·gas·能力系统
知识分享小能手4 小时前
C学习教程,从入门到精通,C语言学生信息管理系统 — 知识点详解(18)
c语言·学习·1024程序员节
爱读书的小胖4 小时前
LeetCode 55 跳跃游戏 C 语言解题分析
c语言·数据结构·算法·leetcode·1024程序员节
pipixia200335 小时前
C++ 面试高频:namespace 的 6 个知识点,从 using 到匿名命名空间
c语言·学习·算法·嵌入式·命名空间·c++面试·匿名命名空间
lunar_coder5 小时前
【C++】如何解决智能指针的析构函数,如何实现shared_ptr的引用计数以及解决shared_ptr的循环引用
c++·1024程序员节
我叫洋洋5 小时前
用 Qoder(AI 编程助手)把 Cadence 16.6 从零配到能画板
人工智能·单片机·嵌入式硬件·电路·1024程序员节