1. 数组名的理解
数组名就是数组⾸元素(第⼀个元素)的地址。
sizeof(数组名),sizeof中好的,我们来详细解释一下C语言中数组名的含义。数组名在不同上下文中确实有不同的含义,理解这一点对于掌握指针和数组操作至关重要。
核心概念总结
| 上下文 | 数组名 arr 的含义 |
说明 |
|---|---|---|
| 普通使用 | 首元素地址 (&arr[0]) |
大多数情况下,数组名代表数组第一个元素的地址 |
sizeof(arr) |
整个数组 | 单独放在 sizeof 运算符内时,代表整个数组的大小 |
&arr |
整个数组的地址 | 取地址操作得到的是指向整个数组的指针 |
详细解释与代码验证
1.1 数组名通常表示首元素地址
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\n", (void*)arr); // 首元素地址
printf("&arr[0] = %p\n", (void*)&arr[0]); // 首元素地址
return 0;
}
输出示例:
arr = 0x7ffd4a3b2b10
&arr[0] = 0x7ffd4a3b2b10
重点:
arr和&arr[0]的值相同,证明数组名在普通使用时等价于首元素地址。
1.2 sizeof(数组名) 表示整个数组
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 整个数组大小
printf("sizeof(&arr[0]) = %zu\n", sizeof(&arr[0])); // 指针大小(通常4或8字节)
return 0;
}
输出示例:
sizeof(arr) = 20 // 5个int × 4字节 = 20
sizeof(&arr[0]) = 8 // 64位系统指针大小为8字节
重点:
sizeof(arr)计算的是整个数组占用的内存大小(5 × sizeof(int))。- 若
arr仅是首元素地址,sizeof(arr)应返回指针大小(如8字节),但实际返回20,证明此处arr代表整个数组。
1.3 &数组名 表示整个数组的地址
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("&arr = %p\n", (void*)&arr); // 整个数组的地址
printf("arr = %p\n", (void*)arr); // 首元素地址
printf("&arr + 1 = %p\n", (void*)(&arr + 1)); // 跳过整个数组
printf("arr + 1 = %p\n", (void*)(arr + 1)); // 跳过一个元素
return 0;
}
输出示例:
&arr = 0x7ffd4a3b2b10
arr = 0x7ffd4a3b2b10
&arr + 1 = 0x7ffd4a3b2b24 // 比 &arr 大20字节(5×4)
arr + 1 = 0x7ffd4a3b2b14 // 比 arr 大4字节(1个int)
重点:
&arr和arr的数值相同 ,但类型不同 :&arr的类型是int(*)[5](指向长度为5的整型数组的指针)。arr的类型是int*(指向整型的指针)。
- 指针运算时,
&arr + 1跳过整个数组(20字节),而arr + 1仅跳过一个元素(4字节)。
总结
- 首元素地址 :除
sizeof(arr)和&arr外,数组名arr始终等价于&arr[0]。 - 整个数组 :
sizeof(arr)中单独使用的arr代表整个数组的大小。 - 数组地址 :
&arr获取的是指向整个数组的指针,类型为int(*)[n](如int(*)[5]),而非int*。
2. 使⽤指针访问数组
2.1 数组名与指针的关系
在C语言中,数组名本质上是数组首元素的地址。例如:
c
int arr[5] = {10, 20, 30, 40, 50};
arr等价于&arr[0](首元素地址)。- 因此可以通过指针操作访问数组元素。
2.2 指针访问数组的三种方式
(1) 下标法(传统方式)
c
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 输出:10 20 30 40 50
}
(2) 指针变量法
c
int *ptr = arr; // ptr指向数组首地址
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr); // 输出当前指针指向的值
ptr++; // 指针移动到下一个元素地址
}
(3) 数组名作为指针常量
c
for (int i = 0; i < 5; i++) {
printf("%d ", *(arr + i)); // 等价于 arr[i]
}
2.3 指针运算的核心机制
-
地址偏移计算 :
指针加减整数
n时,实际移动的字节数为:
偏移量=n×sizeof(数据类型) \text{偏移量} = n \times \text{sizeof(数据类型)} 偏移量=n×sizeof(数据类型)例如
ptr + 2在int数组中移动 2×4=82 \times 4 = 82×4=8 字节(假设int占4字节)。 -
解引用操作 :
*ptr表示获取指针当前指向的值。
2.4 内存布局示例
假设数组 arr 起始地址为 0x1000:
地址 | 值
0x1000 | 10 (arr[0])
0x1004 | 20 (arr[1])
0x1008 | 30 (arr[2])
...
ptr = arr→ 指向0x1000ptr + 1→ 指向0x1004(地址增加sizeof(int))
2.5 完整代码示例
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr指向arr[0]
// 方法1:指针遍历
printf("指针遍历: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i)); // 输出arr[i]
}
// 方法2:指针自增
printf("\n指针自增: ");
ptr = arr; // 重置指针
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr);
ptr++; // 移动到下一个元素
}
// 方法3:数组名作为指针
printf("\n数组名指针: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *(arr + i)); // 等价于arr[i]
}
return 0;
}
输出:
指针遍历: 10 20 30 40 50
指针自增: 10 20 30 40 50
数组名指针: 10 20 30 40 50
2.6 注意事项
- 数组名是常量指针 :
arr++非法(不能修改常量指针),但ptr++合法。 - 越界访问危险 :
指针移动超出数组范围会导致未定义行为。 - 类型匹配 :
指针类型需与数组元素类型一致(如int *对应int[])。
总结
- 指针访问数组的核心是 地址计算与解引用。
- 三种访问方式本质相同,但指针变量更灵活。
- 理解
*(arr + i) = arr[i]的等价性是关键。
通过指针操作,可高效遍历数组,尤其在动态内存和函数参数传递中优势显著。
3. ⼀维数组传参的本质
核心观点
- 数组名的本质 :在大多数情况下,数组名
arr代表数组首元素arr[0]的地址。即arr等价于&arr[0]。 - 传参的本质:当将数组名作为参数传递给函数时,实际上传递的是这个首元素的地址。
- 形参的灵活性 :因为函数接收的是一个地址(指针),所以形参部分可以有两种等效的写法:
- 数组形式 :
int arr[] - 指针形式 :
int *arr
- 数组形式 :
重点详解
3.1 数组名即首元素地址
考虑以下数组:
c
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 定义一个整型数组
printf("arr: %p\n", (void*)arr); // 打印数组名本身的值
printf("&arr[0]: %p\n", (void*)&arr[0]); // 打印首元素地址
return 0;
}
运行这段代码,你会发现 arr 和 &arr[0] 打印出来的地址值是完全相同 的。这证明了 arr 在表达式中(除了 sizeof(arr) 和 &arr 等少数情况)代表的就是数组首元素的地址。
3.2 传参传递地址
当我们将数组名 arr 传递给函数 func 时:
c
func(arr); // 传递数组名
实际上传递的是 arr[0] 的地址,相当于传递了 &arr[0]。
3.3 形参的两种等效写法
函数接收到的参数是一个指向 int 的指针。因此,在定义函数时,形参可以有下面两种写法,它们是完全等价的:
-
写法一:数组形式 (语法糖)
cvoid func(int arr[], int size) { // 看起来像数组,实际是指针 for (int i = 0; i < size; i++) { printf("%d ", arr[i]); // 依然可以使用下标访问 } }int arr[]看起来像是接受一个数组,但这只是C语言提供的一种便利的写法(语法糖)。- 编译器看到
int arr[]时,自动将其解释为int *arr。 - 方括号
[]在这里只是表明arr指向的是数组的首元素,它并不意味着函数内会创建一个新的数组副本。方括号内的数字(如果有)也会被编译器忽略。 - 函数内部仍然可以使用下标
arr[i]来访问元素,这是因为指针支持下标操作。
-
写法二:指针形式 (本质)
cvoid func(int *arr, int size) { // 明确写成指针 for (int i = 0; i < size; i++) { printf("%d ", *(arr + i)); // 通过指针运算和解引用访问 // 等价于 printf("%d ", arr[i]); } }int *arr直接表明形参arr是一个指向整型的指针。- 在函数内部,可以通过指针算术
(arr + i)和解引用*(arr + i)来访问数组元素,这等价于使用下标arr[i]。
关键验证:sizeof 操作符
验证形参本质是指针而非数组的最直接方法是使用 sizeof 操作符:
c
void checkSize(int arr[]) {
printf("Sizeof(arr) inside function (array form): %zu bytes\n", sizeof(arr)); // 通常打印 8 (64位系统) 或 4 (32位系统)
}
int main() {
int myArr[10];
printf("Sizeof(myArr) in main: %zu bytes\n", sizeof(myArr)); // 打印 10 * sizeof(int) = 40 (假设 int 为 4 字节)
checkSize(myArr);
return 0;
}
- 在
main函数中,sizeof(myArr)计算的是整个数组myArr在内存中占用的总字节数 (这里是 10×4=4010 \times 4 = 4010×4=40 字节)。 - 在
checkSize函数内部,sizeof(arr)计算的却是一个指针变量的大小(在 64 位系统上通常是 8 字节,在 32 位系统上是 4 字节),而不是数组的大小。 - 这个差异明确无误 地证明了:函数内部接收到的
arr是一个指针(存储地址的变量),而不是整个数组本身。
总结
- 传递地址 :一维数组传参时,传递的是数组首元素的地址 (
&arr[0])。 - 形参等效 :函数形参
int arr[]和int *arr在功能和意义上完全等价 。编译器都将arr视为一个指向int的指针。 - 语法糖 :
int arr[]这种写法是一种语法糖,让代码看起来像是直接传递了数组,更直观易读,但其底层实现依然是指针。 - 操作本质 :在函数内部,对形参
arr的下标操作arr[i]会被编译器转换为基于指针的算术和解引用操作*(arr + i)。 - 影响原数组 :由于传递的是地址,在函数内通过指针或下标修改形参
arr指向的内存,会直接影响调用函数中的原数组元素。 - 需要传递大小 :因为函数内部无法通过形参
arr获取原数组的长度信息(sizeof(arr)得到的是指针大小),所以通常需要额外传递一个参数(如int size)来指明数组的元素个数。
4. 冒泡排序
核心思想:重复遍历待排序序列,依次比较相邻元素,如果顺序错误则交换它们。每轮遍历会将一个最大(或最小)元素"浮"到正确位置,如同气泡上浮。
算法步骤:
- 比较相邻元素:如果第一个比第二个大,则交换它们。
- 对每一对相邻元素做同样工作:从开始第一对到最后一对。此时,最后一个元素应是最大值。
- 对所有元素重复上述步骤(除了最后一个已排序的元素)。
- 持续每次对越来越少的元素重复上述步骤,直到没有元素需要比较。
时间复杂度:
- 最好情况(已有序):O(n)O(n)O(n)
- 最坏/平均情况:O(n2)O(n^2)O(n2)
C语言代码示例:
c
#include <stdio.h>
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void bubble_sort(int arr[], int sz)
{
int i = 0;
//确定趟数
for (i = 0; i < sz; i++)
{
int flag = 1;//标记是否有序:假设已经有序
//一趟内部比较
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (*(arr + j) > *(arr + j + 1))
{
flag = 0;
int tmp = *(arr + j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
}
}
if (flag == 1)
{
break;
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//对数组进行排序 - 升序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
//输出
print_arr(arr, sz);
return 0;
}
5. 二级指针
定义:指向指针的指针。即一个指针变量存储的是另一个指针变量的地址。
用途:
- 动态创建指针数组。
- 在函数中修改一级指针本身的值(例如,改变指针指向的地址)。
- 处理多维动态数组。
声明 :int **pp;
pp是一个二级指针。*pp是一个一级指针(指向int)。**pp是一个int值。
C语言代码示例:
c
#include <stdio.h>
int main() {
int value = 42;
int *p = &value; // p 是一级指针,指向 value
int **pp = &p; // pp 是二级指针,指向 p
printf("value: %d\n", value); // 直接访问
printf("*p: %d\n", *p); // 通过一级指针访问
printf("**pp: %d\n", **pp); // 通过二级指针访问
// 通过二级指针修改 value 的值
**pp = 100;
printf("Modified value: %d\n", value);
return 0;
}
6. 指针数组
定义:一个数组,其每个元素都是指针。
声明 :int *arr[5];
arr是一个包含 5 个元素的数组。- 每个元素
arr[i]的类型是int *(指向int的指针)。
特点:
- 数组本身在栈上分配连续内存(用于存储指针)。
- 每个指针元素可以指向不同大小的内存块(通常在堆上动态分配)。
用途:
- 存储多个字符串(字符串数组)。
- 模拟二维数组。
- 存储不同长度的数据集合。
C语言代码示例:
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 声明一个指针数组,包含 3 个 int 指针
int *ptrArr[3];
// 为每个指针动态分配内存并赋值
for (int i = 0; i < 3; i++) {
ptrArr[i] = (int *)malloc(sizeof(int)); // 分配一个 int 空间
*ptrArr[i] = i * 10; // 通过指针赋值
}
// 访问和打印
for (int i = 0; i < 3; i++) {
printf("ptrArr[%d] points to value: %d\n", i, *ptrArr[i]);
}
// 释放内存
for (int i = 0; i < 3; i++) {
free(ptrArr[i]);
}
return 0;
}
7. 指针数组模拟二维数组
核心思想:利用指针数组(一级指针数组)来模拟二维数组的行为。
- 创建一个指针数组
int **arr(二级指针指向指针数组)。 - 为指针数组的每个元素(一级指针)分配一个一维数组(代表二维数组的一行)。
- 访问元素使用
arr[i][j]的形式,类似于二维数组。
优势:
- 行可以有不同的长度(不规则二维数组)。
- 动态分配内存,大小可运行时确定。
- 内存不一定连续(与真正的二维数组不同)。
C语言代码示例:
c
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
int **simulated2D; // 二级指针
// 步骤1:分配行指针数组 (模拟有多少行)
simulated2D = (int **)malloc(rows * sizeof(int *));
if (simulated2D == NULL) exit(1);
// 步骤2:为每一行分配内存 (模拟每行的列)
for (int i = 0; i < rows; i++) {
simulated2D[i] = (int *)malloc(cols * sizeof(int));
if (simulated2D[i] == NULL) exit(1);
}
// 步骤3:赋值 (使用双重下标)
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
simulated2D[i][j] = i * cols + j; // 或 *(*(simulated2D + i) + j)
}
}
// 步骤4:打印
printf("Simulated 2D Array:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%2d ", simulated2D[i][j]);
}
printf("\n");
}
// 步骤5:释放内存 (先释放每行,再释放行指针数组)
for (int i = 0; i < rows; i++) {
free(simulated2D[i]);
}
free(simulated2D);
return 0;
}
关键点:
simulated2D指向一个指针数组(int *数组)。simulated2D[i]指向第i行的一维数组。simulated2D[i][j]访问第i行第j列的元素。- 内存释放顺序很重要:先释放每一行的内存,再释放存储行指针的数组。