深⼊理解指针(3)

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)

重点

  • &arrarr数值相同 ,但类型不同
    • &arr 的类型是 int(*)[5](指向长度为5的整型数组的指针)。
    • arr 的类型是 int*(指向整型的指针)。
  • 指针运算时,&arr + 1 跳过整个数组(20字节),而 arr + 1 仅跳过一个元素(4字节)。

总结

  1. 首元素地址 :除 sizeof(arr)&arr 外,数组名 arr 始终等价于 &arr[0]
  2. 整个数组sizeof(arr) 中单独使用的 arr 代表整个数组的大小。
  3. 数组地址&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 + 2int 数组中移动 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 → 指向 0x1000
  • ptr + 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 注意事项

  1. 数组名是常量指针
    arr++ 非法(不能修改常量指针),但 ptr++ 合法。
  2. 越界访问危险
    指针移动超出数组范围会导致未定义行为。
  3. 类型匹配
    指针类型需与数组元素类型一致(如 int * 对应 int[])。

总结

  • 指针访问数组的核心是 地址计算与解引用
  • 三种访问方式本质相同,但指针变量更灵活。
  • 理解 *(arr + i) = arr[i] 的等价性是关键。

通过指针操作,可高效遍历数组,尤其在动态内存和函数参数传递中优势显著。


3. ⼀维数组传参的本质

核心观点

  1. 数组名的本质 :在大多数情况下,数组名 arr 代表数组首元素 arr[0] 的地址。即 arr 等价于 &arr[0]
  2. 传参的本质:当将数组名作为参数传递给函数时,实际上传递的是这个首元素的地址。
  3. 形参的灵活性 :因为函数接收的是一个地址(指针),所以形参部分可以有两种等效的写法:
    • 数组形式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 的指针。因此,在定义函数时,形参可以有下面两种写法,它们是完全等价的:

  • 写法一:数组形式 (语法糖)

    c 复制代码
    void 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] 来访问元素,这是因为指针支持下标操作。
  • 写法二:指针形式 (本质)

    c 复制代码
    void 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 是一个指针(存储地址的变量),而不是整个数组本身。

总结

  1. 传递地址 :一维数组传参时,传递的是数组首元素的地址 (&arr[0])。
  2. 形参等效 :函数形参 int arr[]int *arr 在功能和意义上完全等价 。编译器都将 arr 视为一个指向 int 的指针。
  3. 语法糖int arr[] 这种写法是一种语法糖,让代码看起来像是直接传递了数组,更直观易读,但其底层实现依然是指针。
  4. 操作本质 :在函数内部,对形参 arr 的下标操作 arr[i] 会被编译器转换为基于指针的算术和解引用操作 *(arr + i)
  5. 影响原数组 :由于传递的是地址,在函数内通过指针或下标修改形参 arr 指向的内存,会直接影响调用函数中的原数组元素。
  6. 需要传递大小 :因为函数内部无法通过形参 arr 获取原数组的长度信息(sizeof(arr) 得到的是指针大小),所以通常需要额外传递一个参数(如 int size)来指明数组的元素个数。

4. 冒泡排序

核心思想:重复遍历待排序序列,依次比较相邻元素,如果顺序错误则交换它们。每轮遍历会将一个最大(或最小)元素"浮"到正确位置,如同气泡上浮。

算法步骤

  1. 比较相邻元素:如果第一个比第二个大,则交换它们。
  2. 对每一对相邻元素做同样工作:从开始第一对到最后一对。此时,最后一个元素应是最大值。
  3. 对所有元素重复上述步骤(除了最后一个已排序的元素)。
  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. 指针数组模拟二维数组

核心思想:利用指针数组(一级指针数组)来模拟二维数组的行为。

  1. 创建一个指针数组 int **arr(二级指针指向指针数组)。
  2. 为指针数组的每个元素(一级指针)分配一个一维数组(代表二维数组的一行)。
  3. 访问元素使用 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 列的元素。
  • 内存释放顺序很重要:先释放每一行的内存,再释放存储行指针的数组。
相关推荐
披着假发的程序唐1 小时前
STM32 H743 MPU的配置使用方法
linux·c语言·c++·驱动开发·stm32·单片机·mcu
栈溢出了1 小时前
GAT(Graph Attention Network)学习笔记
人工智能·深度学习·算法·机器学习
Tutankaaa1 小时前
学校知识竞赛怎么组织?从班级到年级的进阶方案
经验分享·学习·算法·职场和发展
qcx231 小时前
混合检索+重排序:当前 RAG 精度提升最成熟的工程路径
算法·ai·llm·agent·rag·agentic
洛水水1 小时前
【力扣100题】42.杨辉三角
算法·leetcode·职场和发展
地平线开发者1 小时前
地平线 征程 6 工具链进阶教程 征程 6E/M 工具链 QAT 精度调优
算法·自动驾驶
Mr.H01271 小时前
C语言MQTT学习系列(3篇):第一篇:从零开始学MQTT(C语言版):入门必看,跑通最简Demo
c语言·网络·学习
小O的算法实验室3 小时前
2025年IEEE TETCI,异构无人机取送货问题中的转运优化,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
chao18984410 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab