| 上一篇 | 下一篇 |
|---|---|
| include <> 和 include "" 的区别 |
数组名,以及和指针的区别(一/二维数组)
常见误区:
- 数据名是第一个元素的首地址,是个常量
- 数据名是一个指针(是个常量指针)
- 数据名当函数参数的时候,传递的是指针
- 数组名相当于指针
- 数组名有特殊含义,是特殊指针
- 数组名不可以是左值
&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