从零开始的指针(3)
一.一维数组传参的本质
1.1数组名的理解
在 C 语言中,我们可以通过 &arr[0]
的方式获取数组第一个元素的地址。但实际上,数组名本身就代表着地址,更确切地说,它表示的是数组首元素的地址。
我们通过一个简单的程序来验证这一点:
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n", &arr[0]); // 打印数组首元素的地址printf("arr = %p\n", arr); // 打印数组名表示的地址return 0;
}
程序的输出结果显示,&arr[0]
和 arr
打印出的地址值完全相同。这一结果证实了:在 C 语言中,数组名本质上就是数组首元素的地址。
这种特性使得我们在使用指针操作数组时更加便捷,比如可以直接用 arr
来初始化指向数组首元素的指针,而不必显式地写成 &arr[0]
。
既然数组名代表数组首元素的地址,那该如何理解下面这段代码的输出呢?
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};printf("%d\n", sizeof(arr));return 0;
}
这段代码的输出结果是 40
。如果数组名 arr
仅仅是首元素的地址,那么输出应该是 4(32 位系统)或 8(64 位系统)才对,因为这是指针变量的大小。
其实,"数组名表示数组首元素的地址" 这一说法基本正确,但存在两个特殊情况:
-
当数组名出现在
sizeof
运算符中时(即sizeof(数组名)
),这里的数组名代表整个数组,sizeof
计算的是整个数组所占用的字节总数。在上面的例子中,int 类型占 4 字节,10 个元素总共就是 40 字节。 -
当对数组名使用取地址符时(即
&数组名
),这里的数组名也表示整个数组,取出的是整个数组的地址。虽然这个地址值和数组首元素的地址值相同,但它们的含义不同:前者指向整个数组,后者指向数组的第一个元素。
除了这两种特殊情况外,在其他任何场景中使用数组名时,它都表示数组首元素的地址。
我们来看下面这段代码及其输出结果,进一步理解数组名在不同场景下的含义:
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n", &arr[0]);printf("&arr[0]+1 = %p\n", &arr[0]+1);printf("arr = %p\n", arr);printf("arr+1 = %p\n", arr+1);printf("&arr = %p\n", &arr);printf("&arr+1 = %p\n", &arr+1);return 0;
}
输出结果:
&arr[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848
从结果中可以观察到:
&arr[0]
与&arr[0]+1
相差 4 个字节arr
与arr+1
也相差 4 个字节
这是因为 &arr[0]
和 arr
都表示数组首元素的地址,对它们执行 +1
操作会跳过一个元素(在 32 位系统中,int 类型占 4 字节)。
而 &arr
与 &arr+1
则相差 40 个字节(0077F848 - 0077F820 = 40(十进制)),这是因为 &arr
表示整个数组的地址,对其执行 +1
操作会跳过整个数组(10 个 int 元素,共 4×10 = 40 字节)。
虽然 &arr[0]
、arr
和 &arr
打印出的地址值相同,但它们的本质含义不同:
&arr[0]
和arr
指向数组的第一个元素&arr
指向整个数组
1.2指针访问数组
有了前面知识的基础,结合数组的特性,我们可以很方便地使用指针来访问数组元素。
#include <stdio.h>
int main()
{int arr[10] = {0};// 输入int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);int* p = arr;for(i=0; i<sz; i++){scanf("%d", p+i);// scanf("%d", arr+i); // 也可以这样写}// 输出for(i=0; i<sz; i++){printf("%d ", *(p+i));}return 0;
}
理解了这段代码后,我们再深入思考一下:既然数组名arr
是首元素的地址,可以赋值给指针p
,那么在这个场景下arr
和p
是否等价呢?我们知道可以用arr[i]
访问数组元素,那p[i]
是否也能访问数组元素呢?
#include <stdio.h>
int main()
{int arr[10] = {0};// 输入int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);int* p = arr;for(i=0; i<sz; i++){scanf("%d", p+i);// scanf("%d", arr+i); // 也可以这样写}// 输出for(i=0; i<sz; i++){printf("%d ", p[i]);}return 0;
}
将*(p+i)
换成p[i]
后,程序依然能正常打印数组元素。这说明p[i]
本质上等价于*(p+i)
。
同理,arr[i]
也等价于*(arr+i)
。实际上,编译器在处理数组元素访问时,会将arr[i]
转换为 "首元素地址加上偏移量 i" 得到元素地址,然后通过解引用操作来访问该元素。
这种等价关系体现了数组和指针在访问方式上的内在联系,也让我们能更灵活地选择数组或指针的方式操作数据。
那么 arr 和 p 的区别?
虽然数组名 arr
和指针变量 p
在很多场景下表现相似(比如都能通过 +i
偏移访问元素,都支持 [i]
形式的访问),但它们本质上有显著区别,主要体现在以下几个方面:
1. 本质类型不同
arr
是数组名,代表一块连续的内存空间本身(存储了多个同类型元素),它不是变量,而是一个常量标识符(编译时确定,无法被赋值)。
- 例如:
arr = p;
是错误的,因为数组名不能被修改。p
是指针变量,它是一个独立的变量,本身占用内存(4 字节或 8 字节),专门用于存储地址,可以被多次赋值修改。
- 例如:
p = arr;
p = &arr[5];
都是合法的,指针可以指向不同的地址。2.
sizeof
运算结果不同
sizeof(arr)
计算的是整个数组的总大小(单位:字节)。
- 对于
int arr[10]
,sizeof(arr) = 4×10 = 40
(假设 int 占 4 字节)。sizeof(p)
计算的是指针变量本身的大小(与指向的数据无关)。
- 在 32 位系统中
sizeof(p) = 4
,64 位系统中sizeof(p) = 8
。3. 取地址操作的含义不同
&arr
表示整个数组的地址,类型是 “指向整个数组的指针”(如int (*)[10]
)。
- 对其
+1
会跳过整个数组(偏移 40 字节,如前面的例子)。&p
表示指针变量自身的地址,类型是 “指向指针的指针”(如int**
)。
- 对其
+1
只会跳过一个指针变量的大小(4 或 8 字节)。4. 存储位置不同
- 数组
arr
的元素存储在数据段或栈区(取决于是否为全局数组),数组名只是这块空间的 “标识”,不占用额外内存。- 指针变量
p
本身存储在栈区(局部变量时),需要单独的内存空间来存放它所指向的地址。
总结
arr
和p
的相似性(如arr[i]
与p[i]
等价)是因为编译器在处理数组访问时,会将数组名隐式转换为指向首元素的指针(除了sizeof(arr)
和&arr
两种特殊情况)。但本质上,数组名是 “内存块的标识”,指针是 “存储地址的变量”,二者不可混为一谈。
1.3数组传参本质
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void test(int *arr)//接收数组首元素地址
{int len1 = sizeof(arr) / sizeof(arr[0]);//arr数组首元素的地址,64位环境下指针大小为8字节printf("%d", len1);// 首元素int类型4个字节 8/4=2
}
int main()
{int arr[10] = { 0 };int len = sizeof(arr) / sizeof(arr[0]);//sizeof(数组名)计算的是整个数组的大小=40printf("%d\n", len);// 首元素int类型4个字节 40/4=10test(arr);//arr数组名,没有&,也没有单独放在sizeof里,是数组首元素地址return 0;
}
大家看一下这段代码,大家觉得代码输出的结果是什么?有人会觉得都是数组的大小/数组受元素的大小==40/4=10。那结果就是两个10。是不是呢?
大家可以看到结果是一个10,一个2。那2是咋来的呢?这里就涉及到了一维数组传参的本质。test(arr)这里的数组名没有&,也没有单独放在sizeof里面,所以这里的arr是数组首元素的地址。所以test函数的sizeof(arr)这里的arr并不是数组的地址,是数组首元素的地址。地址就是指针,指针的大小在64位环境下是8个字节。8/4=2。所以一维数组传参的本质传的是数组首元素的地址,写成数组接收是为了方便理解,实际本质上传的是地址,用指针变量接收。
结论:一维数组传参的本质是传数组首元素的地址
1. 为什么不传递整个数组?
如果传递整个数组,意味着需要将数组的所有元素复制一份到函数栈帧中。对于大型数组,这会导致:
- 内存空间的浪费(重复存储相同数据)
- 函数调用效率低下(复制大量数据耗时)
C 语言设计为传递地址而非整个数组,正是为了避免这些问题,提高程序效率。
2. 数组传参时的隐式转换
当数组名作为实参传递给函数时,编译器会自动将其转换为指向数组首元素的指针(即 &arr[0]
)。
例如,下面的函数调用:
int arr[10] = {1,2,3};
func(arr); // 传递数组名
等价于:
func(&arr[0]); // 传递首元素地址
3. 函数形参的本质是指针
接收数组参数的函数,其形参看似是数组形式,实则会被编译器解析为指针。
例如,以下三种函数声明是完全等价的:
// 形式1:看似接收数组
void func(int arr[]);// 形式2:数组长度无意义(编译器会忽略)
void func(int arr[10]);// 形式3:显式声明为指针(本质)
void func(int* arr);
编译器会将前两种形式自动转换为第三种 —— 即形参是一个指向 int
类型的指针,用于接收数组首元素的地址。
4. 函数内部如何操作数组?
由于形参本质是指针(首元素地址),函数内部可以通过指针偏移来访问原数组的所有元素:
void func(int* arr, int sz) // sz 需单独传递数组长度
{for(int i=0; i<sz; i++){printf("%d ", arr[i]); // 等价于 *(arr+i)}
}
这里的 arr[i]
与指针访问数组的逻辑一致,都是通过 “首地址 + 偏移量” 定位元素。
5. 注意:数组长度无法通过形参获取
正因为数组传参本质是传地址,函数内部无法通过 sizeof(arr)
获取原数组的总大小(此时 sizeof(arr)
计算的是指针的大小,即 4 或 8 字节)。
因此,传递数组时通常需要额外传递数组长度(如上面的 sz
)。
总结
一维数组传参的本质是传递首元素地址,这是 C 语言为了效率而设计的特性。函数形参看似是数组,实则是指针,通过指针偏移实现对原数组的访问。理解这一点,就能明白为什么函数内部无法直接获取数组总长度,以及数组传参时的各种 “表面矛盾”(如形参写 [10]
却能接收任意长度数组)。
二.冒泡排序(优化版)
冒泡排序的核心思想可以优化得更精准、简洁:
冒泡排序的核心思想是通过多轮相邻元素比较与交换,使最大(或最小)元素逐步 "浮" 到数组末端。具体可优化为:
-
外层循环控制排序轮次:最多执行
n-1
轮(n
为数组长度)。因为每轮至少能确定 1 个元素的最终位置,当只剩 1 个元素时无需排序。 -
内层循环控制每轮比较范围:第
i
轮(从 0 开始)只需比较前n-1-i
个元素。因为数组后i
个元素已通过前i
轮排好序,无需再参与比较。 -
相邻元素比较交换:若相邻元素顺序不符合要求(如前大后小),则交换两者位置,确保较大元素向数组末端移动。
-
优化点:
- 增加交换标记(如
flag
),若某轮未发生任何交换,说明数组已完全有序,可直接终止后续循环,避免无效比较 - 记录最后一次交换位置,作为下一轮比较的终点,进一步减少无意义的比较次数
- 增加交换标记(如
但是像这样的数组 无需排序,如果按照 原来的思路仍会按部就班的排序,所以我们可以对他 进行优化。用 flag做标记, 判断是否数组 本身就是有序,如果是就直接 break跳出即可。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int count = 0;
void bubble_sort(int arr[],int sz)
{for (int i = 0; i < sz - 1; i++)//控制趟数{int flag = 0;for (int j = 0; j < sz - 1 - i; j++)//控制比较次数{count++;//记录循环次数if (arr[j] > arr[j + 1])//判断是否交换{flag = 1;int tmp = arr[j + 1];arr[j + 1] = arr[j];arr[j] = tmp;//交换}}if (flag == 0)break;//已经排好序直接结束循环}printf("count=%d\n", count);//输出循环次数
}
void printf_arr(int arr[], int sz)
{for (int i = 0; i < sz; i++){printf("%d ", arr[i]);//遍历打印数组}
}
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10};int sz = sizeof(arr) / sizeof(arr[0]);//数组长度bubble_sort(arr,sz);printf_arr(arr, sz);return 0;
}
这里我们用count变量记录循环次数,对比优化前后的循环次数。
优化前
优化后
大家可以看到优化前需要循环45次,优化后循环9次即可。这就是优化后的效果
三.二级指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int a = 10;int* p = &a;//一级指针int** pp = &p;//二级指针int*** ppp = &pp;//三级指针printf("%d", ***ppp);//三级指针解引用三次,才能找到一级指针指向的变量return 0;
}
指针变量也是变量,是变量就有地址,那指针变量的地址存在哪里呢?答案是存在二级指针。二级指针就是接收一级指针地址的指针。以此类推,三级指针就是接收二级指针地址的指针。
*说明这是一个指针,前面的int加上一个星号说明指针指向的是个一级指针,那么这个指针就是二级指针。大家可以发现有多少个星号就说明这是几级指针。
结论:二级指针是存放一级指针地址的指针,有多少个星号说明是几级指针。
四.指针数组与数组指针
4.1指针数组的概念
指针数组是数组,只不过这个数组中存放的元素类型是指针。
我们可以通过类比来理解:
- 整型数组,是专门用来存放整型数据的数组
- 字符数组,是专门用来存放字符数据的数组
以此类推,指针数组就是专门用来存放指针的数组。
它的本质仍然是数组,具备数组的基本特性(如连续的内存空间、固定的长度等),只是其存储的元素类型特殊 —— 不是普通的整型、字符型等数据,而是指针(即内存地址)。
例如 int* arr[5];
定义的就是一个指针数组:
arr
首先是一个数组(因为[]
的优先级高于*
)- 数组中包含 5 个元素
- 每个元素都是
int*
类型的指针(指向整型数据的指针)
4.2指针数组模拟二维数组
#include <stdio.h>
int main()
{int arr1[] = {1,2,3,4,5};int arr2[] = {2,3,4,5,6};int arr3[] = {3,4,5,6,7};// 数组名表示首元素地址(类型为int*),因此可以存入parr数组int* parr[3] = {arr1, arr2, arr3};int i = 0;int j = 0;for(i=0; i<3; i++){for(j=0; j<5; j++){// 原写法printf("%d ", parr[i][j]);// 等价的解引用写法// printf("%d ", *(*(parr + i) + j));}printf("\n");}return 0;
}
代码解析:
-
指针数组的定义:
int* parr[3]
定义了一个包含 3 个元素的指针数组,每个元素都是int*
类型(指向整型的指针)。 -
存储内容:我们将三个一维数组的首地址(
arr1
、arr2
、arr3
)存入parr
中,此时parr
数组就像一个 "容器",收纳了指向不同整型数组的指针。 -
访问方式:
parr[i]
用于访问指针数组的第i
个元素(即指向某个一维数组的指针)parr[i][j]
等价于*(parr[i] + j)
,表示通过指针访问第i
个一维数组的第j
个元素
与二维数组的对比:
从输出结果看,这段代码通过指针数组模拟出了类似二维数组的访问效果(分行打印多个数组),但本质上与二维数组不同:
- 内存布局:二维数组的所有元素在内存中是连续存储的;而这里的
arr1
、arr2
、arr3
是三个独立的一维数组,它们在内存中的位置并不连续。 - 结构本质:指针数组是 "数组存放指针,指针指向数组" 的间接结构;二维数组是直接的二维连续存储空间。
这种通过指针数组组合多个一维数组的方式,虽然能实现类似二维数组的访问形式,但在内存管理和数据连续性上有本质区别。
4.3数组指针的概念
前面我们学习了指针数组,它本质是一种数组,只是数组中存放的元素是地址(指针)。
那么数组指针变量,它是指针变量呢?还是数组?
答案是:指针变量。
我们可以通过已熟悉的概念类比理解:
- 整型指针变量(
int *pint
):存放整型变量的地址,能够指向整型数据 - 浮点型指针变量(
float *pf
):存放浮点型变量的地址,能够指向浮点型数据
以此类推,数组指针变量就是:专门存放数组的地址,能够指向数组的指针变量。
如何区分数组指针变量?
看下面两段代码,哪个才是数组指针变量?
int *p1[10];
int (*p2)[10];
要区分p1
和p2
的本质,关键在于理解运算符的优先级:[]
的优先级高于*
。
-
对于
int *p1[10]
:
由于[]
优先级更高,p1
先与[10]
结合,说明p1
是一个数组;数组的元素类型是int *
(整型指针)。因此,p1
是指针数组。 -
对于
int (*p2)[10]
:
括号()
改变了优先级,p2
先与*
结合,说明p2
是一个指针变量;这个指针指向的是一个 "包含 10 个 int 元素的数组"。因此,p2
是数组指针变量。
简言之,int (*p)[10]
的解读是:
p
是一个指针变量,它指向的是一个大小为 10 的整型数组 —— 这就是数组指针的核心定义。这里的括号必不可少,它确保了p
首先被识别为指针,而非数组。
提问 数组指针变量 int (*p2)[10]
和 int * p=arr有什么区别?
数组指针变量(如 int (*p)[10]
)与 int* p = arr
定义的指针看似都和数组有关,但它们的指向对象、类型、操作逻辑有本质区别,核心差异在于 “指向的是数组整体还是数组元素”。
1. 本质与指向对象不同
-
int* p = arr
p
是整型指针(指向单个整型元素的指针)。- 它指向的是数组
arr
的首元素(即&arr[0]
),本质是 “指向单个int
类型数据”。
-
int (*p)[10]
p
是数组指针(指向数组整体的指针)。- 它指向的是整个数组(如
int arr[10]
),本质是 “指向一个包含 10 个int
元素的数组”。
2. 指针类型与步长不同
指针的 “类型” 决定了它进行 +1
等偏移操作时的 “步长”(跳过的字节数),这是最关键的区别:
-
int* p
(整型指针)- 类型是 “指向
int
的指针”,步长 =sizeof(int)
(通常 4 字节)。 - 例:
p+1
会跳过 1 个int
元素,指向数组的下一个元素(&arr[1]
)。
- 类型是 “指向
-
int (*p)[10]
(数组指针)- 类型是 “指向
int[10]
数组的指针”,步长 =sizeof(int[10])
(10×4=40 字节)。 - 例:
p+1
会跳过整个数组(40 字节),指向内存中该数组后面的位置。
- 类型是 “指向
3. 初始化与赋值的区别
-
int* p = arr
- 数组名
arr
会隐式转换为 “首元素地址”(&arr[0]
),与int*
类型匹配,可直接赋值。
- 数组名
-
int (*p)[10] = &arr
- 必须用整个数组的地址(
&arr
)初始化,不能直接用arr
(arr
是首元素地址,类型不匹配)。 - 若写成
int (*p)[10] = arr
会报错(类型不兼容:int*
无法转换为int (*)[10]
)。
- 必须用整个数组的地址(
4. 访问数组元素的方式不同
假设数组为 int arr[10] = {0,1,2,...,9}
:
-
用
int* p = arr
访问元素- 通过指针偏移访问单个元素:
*(p+i)
等价于arr[i]
(如*(p+3)
访问arr[3]
)。
- 通过指针偏移访问单个元素:
-
用
int (*p)[10] = &arr
访问元素- 需先解引用得到数组本身(
*p
等价于arr
),再访问元素:(*p)[i]
等价于arr[i]
(如(*p)[3]
访问arr[3]
)。 - 若直接写
p[i]
会错误地跳过整个数组(因步长为 40 字节),访问到的是数组外的无效内存。
- 需先解引用得到数组本身(
总结:核心区别表
对比项 | int* p = arr (整型指针) | int (*p)[10] = &arr (数组指针) |
---|---|---|
指向对象 | 数组首元素(单个 int ) | 整个数组(int[10] 类型) |
类型 | int* | int (*)[10] |
+1 步长 | 4 字节(1 个 int ) | 40 字节(整个数组) |
访问元素方式 | *(p+i) 或 p[i] | (*p)[i] 或 *(*p + i) |
简单说:int* p
是 “元素级指针”,用于逐个访问数组元素;int (*p)[10]
是 “数组级指针”,用于指向整个数组(常见于二维数组操作等场景)。
4.4数组指针的初始化
要存储数组的地址,就需要用到数组指针变量,而获取数组地址的方式,正是我们之前学过的 &数组名
(注意:&数组名
取的是整个数组的地址,而非首元素地址)。
举个具体例子:
int arr[10] = {0}; // 定义一个包含10个int元素的数组
&arr; // 这里的&arr获取的是整个数组的地址,而非首元素&arr[0]
由于 &arr
的类型是 “指向 int [10] 数组的指针”(即 int (*)[10]
),普通指针无法存储它,必须用对应的数组指针变量来接收,写法如下:
int (*p)[10] = &arr; // p是数组指针变量,专门存储整个数组的地址
这里的赋值完全匹配:等号左侧 p
的类型是 int (*)[10]
(指向 10 个 int 元素的数组指针),右侧 &arr
的类型也是 int (*)[10]
(整个数组的地址类型),符合类型兼容的规则。
通过调试也能清晰看到,&arr
(数组的地址)与数组指针变量 p
的类型完全一致,进一步验证了两者的匹配性。
我们可以将数组指针的定义 int (*p)[10] = &arr;
拆解,逐部分解析其类型含义:
int (*p) [10]| | || | |—— 表示 p 指向的数组中,包含 10 个元素(数组的长度)| || |—— 表示 p 是一个指针变量(括号确保 p 先与 * 结合,优先识别为指针)||—— 表示 p 指向的数组中,每个元素的类型是 int(数组元素的基础类型)
4.5数组指针类型
数组指针类型该怎么写呢?首先p是指针那我们就让p和*结合,说明p是个指针。再在星号p后面写上方括号[],说明指针指向一个数组。那数组有几个元素呢?方括号里面的数字就代表指向数组的元素数。那数组元素的类型是什么呢?最前面的类型加粗样式就是数组元素的类型。所以数组指针类型我们可以这样写。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int arr[4] = { 1,2,3,4 };int (*p)[4] = &arr;//&arr表示整个数组的地址//*表示是个指针[4],表示指向的数组4个元素,4不可省略//int表示数组元素类型是int类型return 0;
}
注意方括号里的数组表示指向数组的元素个数,所以不可省略,并且指向的数组元素不同,即使类型相同指针变量的类型也是不同的。变量名p去掉后剩下的就是数组指针类型。
- 字符数组指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{char arr[] = "abcd";char (*p)[5] = &arr;//&arr表示整个数组的地址//指针类型:char (*)[5]//注意是五个字符的数组才是这样return 0;
}
- 指针类型为:char (*)[具体数组元素个数]。
- 整型数组指针
#include<stdio.h>
int main()
{int arr[4] = { 1,2,3,4 };int (*p)[4] = &arr;//&arr表示整个数组的地址//指针类型:char (*)[4]//注意是4个整型的数组才是这样return 0;
}
- 指针类型为:int (*)[具体数组元素个数]。
数组指针类型的区别?
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int arr[] = { 1,2,3,4 };printf("%d\n", arr);//数组名表示数组首元素的地址printf("%d\n", arr+1);//指向的是数组元素+1,跳过一个元素,4字节printf("%d\n", &arr[0]);//取出数组首元素的地址printf("%d\n", &arr[0] + 1);//指向的是数组元素+1,跳过一个元素,4字节printf("%d\n", &arr);//&数组名表示整个数组的地址,但也是指向数组首元素地址printf("%d\n", &arr + 1);//指向的是数组+1,跳过一个数组,16字节return 0;
}
以这个代码为例。大家看一下代码结果是啥?为什么会出现这样的结果呢?
我们知道指针类型决定了指针加1向前移动多大距离。&arr的指针类型是int (p)[4*]指向一个四个整形的数组,所以+1跳过整个数组,也就是4个整型,16个字节。
结论:指针类型决定指针加1向前移动多大距离,变量名去掉后就是数组指针的类型。
五.二维数组传参的本质
5.1二维数组的理解
从内存存储的本质来看,二维数组在内存中是连续存储的 “一维结构”,但从逻辑定义和使用场景来看,它是 “按行 / 列组织的二维结构”—— 可以理解为 “由多个一维数组(行数组)拼接而成的连续内存块”,本质上是对连续一维内存的 “二维逻辑封装”。
1. 核心结论:内存中是连续的,逻辑上是二维的
以典型的二维数组 int arr[3][4] = {1,2,3,4, 5,6,7,8, 9,10,11,12};
为例(3 行 4 列):
- 逻辑上:它被看作 “3 个一维数组”(每行是一个长度为 4 的一维数组,即
arr[0]
、arr[1]
、arr[2]
分别是第 1、2、3 行的 “行数组名”); - 内存中:所有 12 个元素会按 “行优先” 顺序(先存完第 1 行,再存第 2 行,最后存第 3 行)连续排列,没有任何空隙,完全等同于一个长度为 12 的一维数组
int arr1[12]
的内存布局。
2. 用内存地址验证 “连续性”
通过打印每个元素的地址,可以直观看到二维数组的连续存储特性(假设int
占 4 字节,地址为十六进制):
元素 | arr[0][0] | arr[0][1] | arr[0][2] | arr[0][3] | arr[1][0] | arr[1][1] | ... | arr[2][3] |
---|---|---|---|---|---|---|---|---|
内存地址 | 0x100 | 0x104 | 0x108 | 0x10C | 0x110 | 0x114 | ... | 0x12C |
可以发现:
- 同一行内,相邻元素地址差 4(
int
的字节数),连续存储; - 跨行时(如
arr[0][3]
到arr[1][0]
),地址从0x10C
直接跳到0x110
(差 4),没有空隙 —— 说明 “行与行之间也连续”,整个二维数组就是一块连续的内存。
3. 关键区别:二维数组 vs 一维数组(逻辑层面)
虽然内存连续,但二维数组的类型和访问方式与一维数组完全不同,这是 “逻辑封装” 的核心体现:
对比维度 | 二维数组 int arr[3][4] | 等效一维数组 int arr1[12] |
---|---|---|
数组名含义 | arr 是 “指向第 0 行的数组指针”(类型 int (*)[4] ),表示 “整个二维数组的首地址” | arr1 是 “指向首元素的整型指针”(类型 int* ),表示 “一维数组首元素地址” |
元素访问方式 | 支持 arr[i][j] (行索引 + 列索引,符合二维逻辑) | 仅支持 arr1[k] (唯一索引,一维逻辑) |
行数组特性 | 存在 “行数组”(如 arr[0] 是第 0 行的数组名,类型 int* ),可单独操作某一行 | 无 “行” 概念,只有单个元素的线性排列 |
总结
二维数组不是 “独立的二维结构”,而是 “用二维逻辑管理的连续一维内存”—— 可以理解为 “多个长度相同的一维数组,在内存中无缝拼接而成”。这种设计既满足了 “矩阵、表格” 等二维数据的逻辑表达需求,又遵循了内存 “线性连续” 的底层存储规则。
5.2二维数组传参本质
二维数组本质其实就是一个特殊的(内存连续的)一维数组。数组每个的元素是一个一维数组。以下面的代码为例。
void test(int arr[3][5], int r, int c)
{for (int i = 0; i < r; i++){for (int j = 0; j < c; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };test(arr, 3, 5);return 0;
}
这是我们常用的二维数组使用方式。现在我们可以根据二维数组的本质进行改写。传的是数组名,数组名代表首元素地址。二维数组首元素是个一维数组,那我们就用数组指针接收。我们在用星号(arr+i)访问第i行数组,星号(*(arr+i)+j)访问第i行第j个元素。所以代码就可以写成这样。
void test(int (*p)[5], int r, int c)
{for (int i = 0; i < r; i++){for (int j = 0; j < c; j++){printf("%d ", *(*(p+i)+j));}printf("\n");}
}
int main()
{int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };test(arr, 3, 5);return 0;
}
后言
今天给大家分享的内容有点多,感谢各位小伙伴的耐心阅读。到这里指针的内容我们已经快讲完了。坚持就是胜利!