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

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


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

常见误区:

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

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

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 ✔️ 是(数组对象) ✔️ &arrint (*)[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

相关推荐
钢琴上的汽车软件1 天前
C 语言中const与指针:三种常见写法辨析
c语言·指针和const
LCMICRO-133108477461 天前
长芯微LPS123完全P2P替代ADP123,高性能、低压差的线性稳压器
单片机·嵌入式硬件·fpga开发·硬件工程·dsp开发·线性稳压器
ZK_H1 天前
嵌入式c语言——关键字其6
c语言·开发语言·计算机网络·面试·职场和发展
澈2071 天前
深入浅出C++滑动窗口算法:原理、实现与实战应用详解
数据结构·c++·算法
A.A呐1 天前
【C++第二十九章】IO流
开发语言·c++
ambition202421 天前
从暴力搜索到理论最优:一道任务调度问题的完整算法演进历程
c语言·数据结构·c++·算法·贪心算法·深度优先
cmpxr_1 天前
【C】原码和补码以及环形坐标取模算法
c语言·开发语言·算法
kebeiovo1 天前
atomic原子操作实现无锁队列
服务器·c++
Yungoal1 天前
常见 时间复杂度计算
c++·算法
6Hzlia1 天前
【Hot 100 刷题计划】 LeetCode 48. 旋转图像 | C++ 矩阵变换题解
c++·leetcode·矩阵