C语言指针与数组的深度应用与内存解析

C语言指针与数组的深度应用与内存解析

C语言指针与数组的深度应用与内存解析

在这里插入图片描述

💡 学习目标:掌握指针与数组的等价性原理,熟练运用指针操作数组元素,理解二者在内存中的存储本质,解决实际开发中数组遍历、数据拷贝的高效实现问题。
💡 学习重点:指针与数组名的区别、指针算术运算操作数组、二维数组的指针访问方式、内存视角下的数组与指针关系。

48.1 指针与数组的核心关联:本质与等价性

在C语言中,指针和数组的关系密不可分。很多初学者会混淆数组名和指针的概念,实际上二者既有联系又有本质区别。

48.1.1 数组名的“隐式转换”特性

当数组名出现在表达式中时,它会隐式转换为指向数组首元素的指针。我们可以通过一个简单的例子来验证这个特性:

#include<stdio.h>intmain(){int arr[5]={10,20,30,40,50};// 输出数组首元素地址printf("数组名arr的地址:%p\n", arr);// 输出数组首元素的指针地址printf("&arr[0]的地址:%p\n",&arr[0]);// 用数组名访问首元素printf("arr[0] = %d\n", arr[0]);// 用指针方式访问首元素printf("*arr = %d\n",*arr);return0;}

运行结果

数组名arr的地址:0x7ffeefbff560 &arr[0]的地址:0x7ffeefbff560 arr[0] = 10 *arr = 10 

从结果可以看出,arr&arr[0] 指向的是同一块内存地址。这就是数组名的隐式转换特性。

⚠️ 注意事项

  1. 数组名不是真正的指针变量,它是一个地址常量,不能被修改。比如 arr++ 这种写法是非法的。
  2. 当数组名作为 sizeof 操作符的参数时,不会发生隐式转换,此时 sizeof(arr) 计算的是整个数组的内存大小。

48.1.2 指针算术运算操作数组元素

指针的算术运算(加减整数)是操作数组的核心技巧。指针每加1,偏移的字节数等于其指向数据类型的大小。
我们可以用指针替代数组下标,实现更高效的数组遍历:

#include<stdio.h>intmain(){int arr[5]={10,20,30,40,50};// 定义指针指向数组首元素int*p = arr;int i;for(i =0; i <5; i++){// 指针算术运算:p+i 指向第i个元素printf("arr[%d] = %d,*(p+%d) = %d\n", i, arr[i], i,*(p+i));}return0;}

运行结果

arr[0] = 10,*(p+0) = 10 arr[1] = 20,*(p+1) = 20 arr[2] = 30,*(p+2) = 30 arr[3] = 40,*(p+3) = 40 arr[4] = 50,*(p+4) = 50 

从代码中可以得出结论:arr[i] 等价于 *(arr+i),也等价于 *(p+i)

💡 性能技巧:在循环遍历数组时,使用指针操作的效率略高于下标操作。因为下标操作需要计算 arr+i 的地址,而指针可以直接通过自增实现偏移。

48.2 二维数组的指针访问:多维数组的内存布局

二维数组是C语言开发中处理表格数据的常用结构。理解二维数组的内存布局,才能用指针灵活操作它。

48.2.1 二维数组的内存本质

二维数组在内存中是连续存储的,不存在“行”和“列”的物理分隔。比如 int arr[2][3] = {{1,2,3},{4,5,6}},它的内存布局是:

地址偏移 → 0 4 8 12 16 20 元素值 → 1 2 3 4 5 6 

其中每个 int 类型占4个字节,所以相邻元素的地址偏移量为4。

二维数组的数组名 arr 可以看作是指向一维数组的指针arr 指向第一行的一维数组 arr[0]arr+1 指向第二行的一维数组 arr[1]

48.2.2 用指针访问二维数组元素

我们可以通过三种方式访问二维数组的元素:下标法、数组名指针法、普通指针法。

#include<stdio.h>intmain(){int arr[2][3]={{1,2,3},{4,5,6}};// 方式1:下标法printf("下标法访问:arr[1][2] = %d\n", arr[1][2]);// 方式2:数组名指针法:arr[i][j] 等价于 *(*(arr+i)+j)printf("数组名指针法:*(*(arr+1)+2) = %d\n",*(*(arr+1)+2));// 方式3:普通指针法:将二维数组看作一维数组int*p =&arr[0][0];printf("普通指针法:*(p+5) = %d\n",*(p+5));return0;}

运行结果

下标法访问:arr[1][2] = 6 数组名指针法:*(*(arr+1)+2) = 6 普通指针法:*(p+5) = 6 

⚠️ 注意事项

  1. 二维数组的指针 arr 是指向一维数组的指针,其类型为 int (*)[3],不能直接赋值给 int * 类型的指针。
  2. 只有当普通指针指向二维数组的首元素地址时,才能用 *(p+i) 的方式访问元素。

48.3 实战案例:指针实现数组的高效拷贝与逆序

掌握指针与数组的关联后,我们可以用指针实现更高效的数组操作。下面是两个实际开发中常用的案例。

48.3.1 案例1:指针实现数组拷贝函数

要求:编写一个函数,用指针将源数组的内容拷贝到目标数组,要求不使用下标。

#include<stdio.h>#include<string.h>// 数组拷贝函数:src源数组,dest目标数组,len数组长度voidarr_copy(int*src,int*dest,int len){int*p_src = src;int*p_dest = dest;// 循环拷贝每个元素while(len--){*p_dest++=*p_src++;}}intmain(){int src_arr[5]={1,2,3,4,5};int dest_arr[5]={0};int i;// 调用拷贝函数arr_copy(src_arr, dest_arr,5);// 打印目标数组printf("拷贝后的目标数组:");for(i =0; i <5; i++){printf("%d ", dest_arr[i]);}return0;}

运行结果

拷贝后的目标数组:1 2 3 4 5 

💡 技巧解析:函数中使用 *p_dest++ = *p_src++,先执行赋值操作,再将两个指针自增,实现了简洁高效的拷贝。

48.3.2 案例2:指针实现数组逆序

要求:编写一个函数,用指针将数组元素逆序排列,要求不使用额外数组空间。

#include<stdio.h>// 数组逆序函数:arr待逆序数组,len数组长度voidarr_reverse(int*arr,int len){// 指向数组首元素int*start = arr;// 指向数组尾元素int*end = arr + len -1;int temp;// 首尾交换,直到指针相遇while(start < end){ temp =*start;*start =*end;*end = temp; start++; end--;}}intmain(){int arr[5]={1,2,3,4,5};int i;printf("逆序前的数组:");for(i =0; i <5; i++){printf("%d ", arr[i]);}// 调用逆序函数arr_reverse(arr,5);printf("\n逆序后的数组:");for(i =0; i <5; i++){printf("%d ", arr[i]);}return0;}

运行结果

逆序前的数组:1 2 3 4 5 逆序后的数组:5 4 3 2 1 

💡 技巧解析:通过首尾两个指针向中间移动,交换对应位置的元素,空间复杂度为O(1),是最优的数组逆序实现方式。

48.4 内存视角的总结:指针与数组的核心区别

很多开发者会混淆指针和数组,我们从内存角度总结二者的核心区别:

对比维度数组名指针变量
内存属性地址常量,不能修改指向变量,可以修改指向
sizeof计算计算整个数组的内存大小计算指针本身的大小(32位系统4字节,64位系统8字节)
初始化方式int arr[5] = {1,2,3,4,5}int *p = arr

本章核心结论

  1. 数组名在表达式中会隐式转换为指向首元素的指针,但它不是真正的指针变量。
  2. arr[i] 等价于 *(arr+i),指针算术运算是操作数组的高效方式。
  3. 二维数组在内存中连续存储,可通过指向一维数组的指针或普通指针访问。

Read more

程序员崩溃瞬间:这4道链表题不会,面试直接凉凉?速补!!

程序员崩溃瞬间:这4道链表题不会,面试直接凉凉?速补!!

坚持用清晰易懂的图解+多语言代码,让每道题变得简单! 🌟 🚀呆头个人主页详情 🌱呆头个人Gitee代码仓库 📌 呆头详细专栏系列 座右铭:“不患无位,患所以立。” 👨‍💻 程序员崩溃瞬间:这4道链表题不会,面试直接凉凉?速补!! * 前言 * 目录 * 1.移除链表元素 * 方法思路 * 代码实现 * 代码解释 * 2.反转链表 * 方法思路 * 代码实现 * 代码解释 * 3.查找链表中间结点 * 方法思路 * 代码实现 * 代码解释 * 4.合并两个有序链表 * 方法思路 * 代码实现 * 代码解释 * 为什么不需要循环就能连接所有剩余节点? 前言 🚀 你好,欢迎来到《编程闯关记》! 这里是算法与数据结构的实战基地,也是你从“暴力解法”到“最优解”的进化场。 🔍 专栏初衷: * 用清晰的图解 + 多语言代码(Python/

By Ne0inhk
详解数据结构之跳表

详解数据结构之跳表

目录 跳表的定义 跳表的演化过程 跳表的优化思路 跳表如何保证效率 跳表的时间复杂度 跳表的空间复杂度 跳表的查找 跳表的插入 跳表的删除 跳表的模拟实现 跳表与平衡搜索树及哈希表的对比 跳表的定义 跳表是由William Pugh(音译为威廉·普)发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》,跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。 跳表的演化过程 对于单链表来说,即使数据是已经排好序的,想要查询其中的一个数据,只能从头开始遍历链表,这样效率很低,时间复杂度很高,是 O(n),如下图所示。 那我们有没有什么办法来提高查询的效率呢?我们可以为链表建立一个“索引”,这样查找起来就会更快,如下图所示,我们在原始链表的基础上,每两个结点提取一个结点建立索引,我们把抽取出来的结点叫作索引层或者索引,down

By Ne0inhk
【算法】【动态规划】斐波那契数模型

【算法】【动态规划】斐波那契数模型

目录 * 一、动态规划解题模版 * 二、第N个泰波那契数 * 三、⾯试题 08.01. 三步问题 * 四、746. 使⽤最⼩花费爬楼梯(easy) * 五、91.解码⽅法 一、动态规划解题模版 1. 状态表示:我们一般创建一个一维数组dp,把dp表填满,其中的某一个值就是结果。而状态表示就是指这个dp表中元素的含义; 1.1. 来源:题目要求,经验+题目要求 ,分析问题的过程中的重复子问题 2. 状态转移方程:dp[ i ] = ? 3. 初始化:保证根据状态转移方程填表时不越界 4. 填表顺序:为了填写当前状态的时候,所需要的状态已经计算过了 5. 返回值:题目要求 + 状态表示

By Ne0inhk
Transformer实战(9)——Transformer分词算法详解

Transformer实战(9)——Transformer分词算法详解

Transformer实战(9)——Transformer分词算法详解 * 0. 前言 * 1. 子词分词算法 * 2. 加载预训练分词器 * 3. 常见字词分词算法 * 3.1 字节对编码 * 3.2 WordPiece * 3.3 SentencePiece * 4. 使用 tokenizers 库训练分词器 * 4.1 训练 BPE * 4.2 训练 WordPiece * 4.3 空分词管道 * 小结 * 系列链接 0. 前言 在自然语言处理领域,高效准确的分词算法是构建强大语言模型的基础。随着 Transformer 架构的广泛应用,子词分词算法已成为处理多语言文本和稀有词汇的关键技术。本文将从理论到实践,全面解析现代自然语言处理 (Natural Language Processing,

By Ne0inhk