深入浅出C语言指针:从数组到函数指针的进阶之路(中)

指针是C语言的灵魂,也是初学者最头疼的知识点。它像一把锋利的刀,用得好能大幅提升代码效率,用不好则会让程序漏洞百出。今天这篇文章,我们从数组与指针的基础关系讲起,一步步揭开指针进阶类型的神秘面纱,最后用实战案例巩固所学——保证通俗易懂,还会标注所有重点和坑点。

一、数组与指针:绕不开的基础关系

1.1 数组名的本质:不是简单的地址

很多人以为"数组名就是首元素地址",这句话对但不完整。数组名的本质有两个例外,这是初学者最容易掉的坑!

重点结论:
  • 一般情况:数组名表示数组首元素的地址。
    例:int arr[10] = {1,2,...,10}; 中,arr&arr[0] 地址相同。
  • 两个例外(必须牢记):
    • sizeof(数组名):数组名代表整个数组,计算整个数组的字节大小。
      例:sizeof(arr) 结果为 40(10个int,每个4字节),而非指针大小(4/8)。
    • &数组名:数组名代表整个数组,取出的是整个数组的地址(与首元素地址值相同,但偏移量不同)。
      例:&arr + 1 偏移40字节(跳过整个数组),而 arr + 1 偏移4字节(跳过一个元素)。
实战验证:
#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);      // 首元素地址(等价于上一行)printf("&arr    = %p\n", &arr);     // 整个数组的地址(值相同,意义不同)// 关键差异:+1操作printf("&arr[0]+1 = %p\n", &arr[0]+1);  // 跳过1个元素(+4字节)printf("arr+1     = %p\n", arr+1);      // 跳过1个元素(+4字节)printf("&arr+1    = %p\n", &arr+1);     // 跳过整个数组(+40字节)return 0;
}
输出结果解析:

前三个地址值相同,但&arr+1会跳过整个数组(10个int,共40字节),而前两者只跳过1个元素(4字节)。这证明&arr指向的是整个数组,而非单个元素。

1.2 用指针访问数组:灵活但要谨慎

有了对数组名的理解,我们可以用指针灵活访问数组元素。核心逻辑是:数组元素的访问本质是"首地址+偏移量"

等价关系(重点):
  • arr[i] 等价于 *(arr + i)
  • 指针p指向首元素时,p[i] 等价于 *(p + i)
示例代码:
#include <stdio.h>
int main() {int arr[5] = {10,20,30,40,50};int* p = arr;  // p指向首元素// 两种访问方式等价printf("arr[2] = %d\n", arr[2]);      // 30printf("*(p+2) = %d\n", *(p + 2));    // 30printf("p[2] = %d\n", p[2]);          // 30(指针也支持下标)return 0;
}

1.3 一维数组传参:别被"数组形式"骗了

当数组作为参数传递给函数时,形参看似是数组,本质是指针。这也是为什么在函数内部用sizeof求不出数组长度的原因。

  • 数组传参实际传递的是首元素地址,而非整个数组。
  • 函数形参两种写法(等价):
    void test(int arr[]); // 数组形式(本质是指针)
    void test(int* arr);  // 指针形式(更直观)
    
易错点演示:
#include <stdio.h>
// 形参写成数组形式,本质还是指针
void test(int arr[]) {printf("函数内sizeof(arr) = %d\n", sizeof(arr));  // 4或8(指针大小)
}int main() {int arr[10] = {0};printf("主函数内sizeof(arr) = %d\n", sizeof(arr));  // 40(整个数组大小)test(arr);return 0;
}

1.4.冒泡排序(指针应用)

  • 核心:相邻元素比较交换,通过指针访问数组元素。
  • 优化版(提前终止有序数组):
    void bubble_sort(int* arr, int sz) {for(int i=0; i<sz-1; i++) {int flag = 1; // 假设本趟有序for(int j=0; j<sz-i-1; j++) {if(arr[j] > arr[j+1]) {int tmp = arr[j];arr[j] = arr[j+1];arr[j+1] = tmp;flag = 0; // 发生交换,无序}}if(flag) break; // 无交换,直接退出}
    }
    

二、指针的进阶类型:从二级指针到数组指针

2.1 二级指针:指针的指针

指针变量也是变量,它的地址需要用"二级指针"存储。可以理解为:一级指针指向数据,二级指针指向一级指针

示例图解:
int a = 10;       // 数据
int* pa = &a;     // 一级指针(指向a)
int** ppa = &pa;  // 二级指针(指向pa)
操作逻辑:
  • *ppa 等价于 pa(通过二级指针获取一级指针)
  • **ppa 等价于 *pa 等价于 a(通过二级指针获取数据)

2.2 指针数组:存放指针的数组

指针数组是数组,其元素类型是指针。比如int* arr[5]表示:一个有5个元素的数组,每个元素是int*类型的指针。

用途:存储多个同类型地址
#include <stdio.h>
int main() {int arr1[] = {1,2,3};int arr2[] = {4,5,6};int arr3[] = {7,8,9};// 指针数组存储三个一维数组的首地址int* parr[3] = {arr1, arr2, arr3};// 访问arr2的第2个元素(5)printf("%d\n", parr[1][1]);  // 等价于*(parr[1] + 1)return 0;
}

2.3 数组指针:指向数组的指针

数组指针是指针,它指向一个完整的数组。比如int (*p)[5]表示:一个指针,指向"有5个int元素的数组"。

易混淆对比(重点):
定义本质解读
int* p[5]指针数组先与[]结合,是数组,元素为int*
int (*p)[5]数组指针先与*结合,是指针,指向int[5]数组
数组指针的用法:
#include <stdio.h>
int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};int (*p)[5] = arr;  // arr是首行地址(指向第一行数组)// 访问第二行第三列元素(8)printf("%d\n", *(*(p + 1) + 2));  // 等价于p[1][2]return 0;
}

三、字符串与字符指针:藏着坑的常量

3.1 字符指针的两种用法

字符指针(char*)既可以指向单个字符,也可以指向字符串的首字符。后者更常见,但要注意常量字符串的特性。

示例:
#include <stdio.h>
int main() {// 指向单个字符char ch = 'a';char* pc = &ch;// 指向字符串首字符(重点)const char* pstr = "hello";  // "hello"是常量字符串,不可修改printf("%s\n", pstr);  // 打印整个字符串(从首字符开始直到'\0')return 0;
}

3.2 常量字符串的存储:节省空间的小技巧

C/C++会把相同的常量字符串存储在同一块内存中,这是容易踩坑的点。

示例(面试常考):
#include <stdio.h>
int main() {char str1[] = "hello";  // 数组:开辟新空间,存储"hello"char str2[] = "hello";  // 数组:再开辟新空间,存储"hello"const char* str3 = "hello";  // 指针:指向常量区的"hello"const char* str4 = "hello";  // 指针:指向同一块常量区空间printf("str1 == str2 ? %d\n", str1 == str2);  // 0(地址不同)printf("str3 == str4 ? %d\n", str3 == str4);  // 1(地址相同)return 0;
}
结论:
  • 用常量字符串初始化数组时,每次都会开辟新空间
  • 用常量字符串初始化字符指针时,多个指针可能指向同一块空间(节省内存)

四、二维数组传参:首行地址是关键

二维数组可以理解为"数组的数组"(每个元素是一维数组)。因此,二维数组的数组名表示首行的地址(即第一个一维数组的地址),类型是数组指针。

二维数组传参的正确方式:
#include <stdio.h>
// 形参可写成二维数组形式,或数组指针形式
void print_arr(int (*p)[5], int row, int col) {for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {printf("%d ", p[i][j]);  // 等价于*(*(p+i)+j)}printf("\n");}
}int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};print_arr(arr, 3, 5);  // 传递首行地址return 0;
}

五、函数指针:让指针指向代码

函数也有地址(函数名就是地址),用函数指针可以存储函数地址,实现更灵活的调用(比如回调函数)。

5.1 函数指针的定义与使用

定义格式:返回类型 (*指针名)(参数类型列表)
#include <stdio.h>
int add(int a, int b) {return a + b;
}int main() {// 定义函数指针,指向add函数int (*pf)(int, int) = add;  // 等价于&add// 两种调用方式printf("add(2,3) = %d\n", add(2,3));       // 直接调用printf("pf(2,3) = %d\n", pf(2,3));         // 用指针调用printf("(*pf)(2,3) = %d\n", (*pf)(2,3));   // 等价写法return 0;
}

5.2 函数指针数组与转移表:简化多分支逻辑

函数指针数组是存储函数指针的数组,适合实现"菜单-功能"类逻辑(如计算器),替代冗长的switch-case。

实战案例:用函数指针数组实现计算器
#include <stdio.h>
// 四则运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }int main() {int x, y, input;// 函数指针数组(转移表):下标1-4对应加减乘除int (*operate[5])(int, int) = {0, add, sub, mul, div};do {printf("1:加 2:减 3:乘 4:除 0:退出\n");printf("请选择:");scanf("%d", &input);if (input >= 1 && input <= 4) {printf("输入操作数:");scanf("%d %d", &x, &y);// 用数组下标调用对应函数printf("结果: %d\n", operate[input](x, y));} else if (input != 0) {printf("输入错误!\n");}} while (input != 0);return 0;
}
优势:
  • 新增功能只需添加函数并更新数组,无需修改分支逻辑
  • 代码更简洁,可读性更高

六、typedef:给复杂类型起"小名"

typedef可以为复杂类型(如指针、数组指针、函数指针)重命名,简化代码。但要注意与#define的区别。

6.1 用法示例:

#include <stdio.h>
// 重命名基本类型
typedef unsigned int uint;// 重命名指针类型
typedef int* int_ptr;// 重命名数组指针
typedef int (*arr_ptr)[5];  // 指向int[5]数组的指针// 重命名函数指针
typedef int (*calc_func)(int, int);  // 指向"int(int,int)"函数的指针int main() {uint a = 10;              // 等价于unsigned intint_ptr p1, p2;           // p1和p2都是int*(指针)arr_ptr parr;             // 等价于int (*parr)[5]calc_func pf = add;       // 等价于int (*pf)(int,int)return 0;
}

6.2 与#define的区别(易错点):

#define是简单替换,而typedef是真正的类型重命名。

#include <stdio.h>
typedef int* int_ptr;       // 类型重命名
#define INT_PTR int*        // 宏替换int main() {int_ptr p1, p2;  // p1和p2都是int*(正确)INT_PTR p3, p4;  // 替换后为int* p3, p4; → p4是int(错误)return 0;
}

七、总结与易错点回顾

  1. 数组名的两个例外sizeof(数组名)&数组名表示整个数组
  2. 数组传参本质:形参是指针,需额外传递长度
  3. 指针数组vs数组指针:前者是数组(存指针),后者是指针(指向数组)
  4. 常量字符串存储:相同常量字符串可能共享内存,数组初始化则不共享
  5. 函数指针数组:适合实现多功能菜单,替代switch-case
  6. typedef与#define:typedef是类型重命名,#define是文本替换

指针虽然复杂,但只要抓住"地址"和"类型"两个核心(地址决定指向哪里,类型决定+1跳过多少字节),就能逐步掌握。多写代码验证,少死记硬背,才是学好指针的关键!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917093.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917093.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java web Cookie处理

java web 设置cookie更改启动端口// Directory tree (5 levels) ├── src\ │ ├── a.txt │ └── com\ │ └── zhang\ │ └── ServletContext\ │ ├── cookie\ │ └── servletContext.java └── web\├─…

机器学习—线性回归

一线性回归线性回归是利用数理统计中回归分析&#xff0c;来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法。相关关系&#xff1a;包含因果关系和平行关系因果关系&#xff1a;回归分析【原因引起结果&#xff0c;需要明确自变量和因变量】平行关系&#xff1…

Spring Boot Admin 监控模块笔记-实现全链路追踪

一、概述Spring Boot Admin&#xff08;SBA&#xff09;是一个用于监控和管理 Spring Boot 应用程序的工具。它提供了一个 Web 界面&#xff0c;可以集中管理多个 Spring Boot 应用程序的健康状态、指标、日志、配置等信息。通过 SBA&#xff0c;你可以轻松地监控和管理你的微服…

容器化与Docker核心原理

目录 专栏介绍 作者与平台 您将学到什么&#xff1f; 学习特色 容器化与Docker核心原理 引言&#xff1a;为什么容器化成为云计算时代的基石&#xff1f; 容器化技术全景与Docker核心原理&#xff1a;从概念到实践 文章摘要 1. 引言&#xff1a;为什么容器化成为云计算…

掌握Python三大语句:顺序、条件与循环

PS不好意思各位&#xff0c;由于最近笔者在参加全国大学生电子设计大赛&#xff0c;所以最近会出现停更的情况&#xff0c;望大家谅解&#xff0c;比赛结束后我会加大力度&#xff0c;火速讲Python的知识给大家写完&#x1f396;️&#x1f396;️&#x1f396;️&#x1f396;…

JAVA结合AI

Java 与人工智能&#xff08;AI&#xff09;的结合正经历从技术探索到深度融合的关键阶段。以下从技术生态、应用场景、工具创新、行业实践及未来趋势五个维度展开分析&#xff0c;结合最新技术动态与企业级案例&#xff0c;揭示 Java 在 AI 时代的独特价值与发展路径。一、技术…

本土DevOps平台Gitee如何重塑中国研发团队的工作流

本土DevOps平台Gitee如何重塑中国研发团队的工作流 在数字化转型浪潮席卷各行各业的当下&#xff0c;软件开发效率已成为企业竞争力的核心指标。Gitee DevOps作为专为中国开发团队打造的本土化研发管理平台&#xff0c;正在改变国内技术团队的工作方式。该平台通过从代码管理到…

5G MBS(组播广播服务)深度解析:从标准架构到商用实践

一、MBS技术背景与核心价值 1.1 业务需求驱动 随着超高清视频(4K/8K)、多视角直播、XR元宇宙应用爆发式增长,传统单播传输面临带宽浪费(相同内容重复发送)与拥塞风险(万人并发场景)的双重挑战。5G MBS通过点对多点(PTM)传输实现内容一次发送、多终端接收,频谱效率提…

如何将照片从 realme 手机传输到电脑?

对于 realme 用户来说&#xff0c;将照片传输到电脑可以有多种用途&#xff0c;从释放设备空间到在单独的存储设备上创建备份。这个过程不仅有助于高效管理设备内存&#xff0c;还可以让您利用电脑上强大的照片编辑软件进行高级增强和创意项目。了解如何将照片从 realme 手机传…

Centos 7部署.NET 8网站项目

简介 本文详细介绍了在CentOS 7系统上部署.NET 8网站项目的完整流程&#xff0c;主要内容包括&#xff1a;系统版本更新与检查、PostgreSQL数据库的安装配置&#xff08;含防火墙设置、数据库初始化及远程访问配置&#xff09;、Nginx Web服务的安装与防火墙配置。文章通过分步…

Windows 11下IDEA中使用git突然变得卡慢及解决办法

1. 表象 使用idea的git进行update、commit、push等操作时&#xff0c;极度卡慢。需等待几十秒到几分钟。修改文件后&#xff0c;git刷新也不及时。update命令有时候无法点击。 2.解决方法 停止PC Manager ServiceCtrl shift esc : 打开任务管理器找到服务&#xff1a; 服务中…

MyBatis 的两级缓存机制

现实分布式项目中会不会开启mybatis的二级缓存&#xff1f; 在分布式项目中&#xff0c;是否开启MyBatis的二级缓存需结合具体场景和技术方案综合评估。 以下是关键考量因素&#xff1a; 一、默认二级缓存的局限性 隔离性问题&#xff1a;MyBatis默认的二级缓存基于HashMap实…

分布式原子序列(Distributed Atomic Sequence)

这段内容是关于 Apache Ignite 中的 分布式原子序列&#xff08;Distributed Atomic Sequence&#xff09;&#xff0c;也就是一个分布式 ID 生成器。我们来一步步深入理解它的原理、用途和使用方式。&#x1f539; 一、核心概念&#xff1a;什么是分布式 ID 生成器&#xff1f…

VSCode——插件分享:Markdown PDF

该插件可以将markdown编写内容转成PDF。 ✅ 支持渲染图表、代码高亮、表格等 Markdown 内容 安装 Visual Studio Code安装插件&#xff1a;Markdown PDF 打开扩展商店&#xff0c;搜索 Markdown PDF 并安装 打开你的 .md 文件右键 → 点击 Markdown PDF: Export (pdf)自动生成 …

rust-模块树中引用项的路径

模块树中引用项的路径 为了告诉 Rust 在模块树中如何找到某个项&#xff0c;我们使用路径&#xff0c;就像在文件系统中导航时使用路径一样。要调用一个函数&#xff0c;我们需要知道它的路径。 路径有两种形式&#xff1a; 绝对路径是从 crate 根开始的完整路径&#xff1b…

mac n切换node版本报错Bad CPU type in executable

该node版本仅支持intel芯片&#xff0c;不支持Apple 芯片&#xff08;M1/M2/M3/M4&#xff09;&#xff0c;所以需要下载Rosetta 2 &#xff0c;让node可以在搭载 Apple 芯片的 Mac 上运行。 env: node: Bad CPU type in executable /opt/homebrew/bin/n: line 753: /usr/local…

经典算法之美:冒泡排序的优雅实现

经典算法之美&#xff1a;冒泡排序的优雅实现基本概念工作原理介绍具体实现代码实现总结基本概念 冒泡排序是一种简单的排序算法&#xff0c;通过重复比较相邻的元素并交换它们的位置来实现排序。它的名称来源于较小的元素像气泡一样逐渐“浮”到数组的顶端。 工作原理 介绍…

click和touch事件触发顺序 糊里糊涂解决的奇怪bug

问题详情 在嵌入式硬件设备里&#xff0c;测试 “点击input密码框&#xff0c;弹出第三方自带键盘&#xff0c;点击密码框旁的小眼睛&#xff0c;切换输入内容加密状态&#xff0c;键盘收起/弹出状态不变” 的功能逻辑&#xff1b;实际情况却是 “点击键盘或input框之外的任何地…

【0基础PS】Photoshop (PS) 理论知识

目录前言一、Photoshop 核心概念与定位​二、图像基础理论​三、图层理论&#xff1a;PS 的核心工作机制​四、选区与蒙版​五、调色核心理论​六、常用文件格式​学习建议​总结前言 在数字图像编辑领域&#xff0c;Photoshop&#xff08;简称 PS&#xff09;无疑是行业标杆级…

数据库 设计 pdm comment列表显示和生成建表sql

按如下步骤 生成见建表语句 comment非空使用comment 生成字段注释&#xff0c; 空的时候使用name 生成字段注释 sql脚本模板编辑 参考 PowerDesigner生成mysql字段comment 注释-腾讯云开发者社区-腾讯云 版本不同这边的设置不同 这个勾打上