当前位置: 首页 > news >正文

暑期自学嵌入式——Day06(C语言阶段)

接续上文:

暑期自学嵌入式——Day05补充(C语言阶段)-CSDN博客

主页点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!

主页:

一位搞嵌入式的 genius-CSDN博客一位搞嵌入式的 genius擅长前后端项目开发,微机原理与接口技术,嵌入式自学专栏,等方面的知识,一位搞嵌入式的 genius关注matlab,论文阅读,前端框架,stm32,c++,node.js,c语言,智能家居,vue.js,html,npm,单片机领域. https://blog.csdn.net/m0_73589512?spm=1011.2415.3001.5343

目录

Day06

1. 指针的基本用法1

一、指针的核心地位与优势

二、内存与变量:指针的底层基础

1. 计算机内存的编址规则

2. 变量的本质

三、知识小结:重点与易错点

关键关联:指针与地址的关系

2. 指针的基本用法2

一、指针变量相关知识梳理

1. 指针的初始化

2. 指针的内存模型

(1)指针与变量的关系

(2)内存地址打印与验证

(3)内存布局细节

(4)指针的内存表示(图示逻辑)

3. 重要概念总结

(1)关键区别

(2)操作要点

(3)常见误区

二、知识小结(核心考点与难度)

3. 指针的基本用法3

一、指针基本用法详解

1. 指针的核心概念

(1)指针、目标变量与目标数据

(2)指针的三种关键表示方法

(3)实例验证

2. 指针的赋值运算

(1)赋值的三种基本形式

(2)赋值的禁忌与注意事项

3. 指针的存储大小(占几个字节?)

(1)核心结论

(2)底层原因:地址总线宽度

4. 关键问题解答(面试高频)

(1)“什么是指针?”

(2)“指针为什么占 4 字节(或 8 字节)?”

二、知识小结

三、学习建议

4. 指针运算(上)

一、指针运算的核心概念

1. 指针运算的种类

二、指针的算术运算(核心)

1. 指针加减整数(p + n / p - n)

(1)运算规则

(2)关键意义

2. 同类型指针相减(p - q)

(1)运算规则

(2)应用场景

3. 指针自增 / 自减(p++ / p--)

(1)运算规则

三、指针运算的关键特性

四、知识小结

五、应用示例:用指针遍历数组

5. 指针运算(下)

一、指针的关系运算符

1. 支持的运算符与比较对象

2. 特殊规则与应用

二、指针关系运算的本质与应用场景

1. 本质:地址位置关系

2. 典型应用:链表判空与数组遍历

三、应用案例解析(结合指针运算与优先级)

1. 案例 1:复杂表达式y = (*--p)++的执行顺序

2. 案例 2:指针越界与重置(数组遍历常见错误)

3. 案例 3:*p++与运算符优先级(核心易错点)

四、指针运算的核心注意事项

五、知识小结

6. 指针与数组

一、数组名的本质特性

1. 数组名是地址常量

2. 数组名的合法运算

二、数组元素的四种等价访问方式

1. 核心公式与访问形式

2. 底层实现原理

三、指针与数组名的关键区别

四、应用案例:数组反转(双指针法)

1. 算法思路

2. 关键细节

五、指针运算的常见陷阱

1. 指针移动导致下标基准改变

2. 混淆数组名与指针

六、知识小结

七、实践建议

7. 指针与二维数组(上)

一、二维数组的本质与存储特性

1. 二维数组的逻辑与物理结构

2. 行数组名的特性(以a[0]为例)

二、二维数组的地址表示与指针类型

1. 关键地址表示(以int a[3][2]为例)

2. 元素访问的等价写法

三、编程实现:指针遍历二维数组

1. 方法 1:一级指针遍历(利用存储连续性)

2. 方法 2:行指针遍历(按行访问)

四、常见错误与注意事项

五、知识小结

8. 指针与二维数组(下)

一、二维数组的行地址与列地址

1. 行地址(以数组名a为例)

2. 列地址(以a[0]或*a为例)

3. 关键转换:行地址 → 列地址

二、行指针(数组指针)的定义与使用

1. 声明语法

2. 与二维数组的绑定

3. 访问元素的两种方式

4. 行指针与数组名的区别

三、行指针遍历二维数组(实例)

四、常见错误与注意事项

五、知识小结


Day06

1. 指针的基本用法1

一、指针的核心地位与优势

指针是 C 语言的 “灵魂”,几乎所有 C 程序都会用到,其核心价值体现在四大优势:

  1. 提升程序效率:通过直接操作内存地址,减少数据拷贝,使程序更简洁高效。

  2. 支持复杂数据结构:是实现链表、树、图等链式存储结构的基础(没有指针,链式结构无法实现)。

  3. 动态内存管理:允许程序在运行时灵活申请 / 释放内存(如malloc/free),而不是依赖编译时固定的内存分配。

  4. 实现函数多返回值:C 语言函数默认只能返回一个值,通过指针传递地址,可以间接实现 “返回” 多个结果(如通过指针修改外部变量)。

学习提示:指针的理解需要结合 “内存操作” 场景,从 “地址” 的本质出发,避免死记硬背,通过代码实践逐步掌握。

二、内存与变量:指针的底层基础

要理解指针,必须先搞清楚 “内存如何存储数据” 以及 “变量与内存的关系”。

1. 计算机内存的编址规则
  • 基本单位:内存以字节(Byte) 为最小单位进行连续编号,每个编号就是一个地址(类似楼房的房间号,唯一标识一个内存单元)。

    • 例:地址 0、1、2、……、n,每个地址对应 1 字节内存。

  • 为什么用字节?:字节是 C 语言中最小的数据类型单位(如char类型正好占用 1 字节),与内存单位对齐,便于数据存储。

  • 变量的存储规则

    • 不同类型的变量占用的字节数不同(如short占 2 字节,float占 4 字节)。

    • 变量的 “地址” 指其占用内存的起始地址(最低字节的地址)。

    • 数据在内存中

      从低地址向高地址连续存放

      • 例:short i分配到地址 2000-2001(起始地址 2000);float f分配到 2002-2005(起始地址 2002)。

2. 变量的本质
  • 变量是

    内存空间的 “别名”

    :程序员通过变量名操作内存,而无需直接记住复杂的地址。

    • 声明变量时,编译器会自动分配对应大小的内存(如short i;分配 2 字节)。

    • 给变量赋值(如i=10),本质是向该内存空间写入数据。

  • 变量地址的获取:通过&(取地址符)获取变量的起始地址(如&i返回变量i的起始地址 2000)。

三、知识小结:重点与易错点

知识点核心内容考试重点 / 易混淆点难度
指针基础概念内存以字节为单位编号(地址);变量是内存的抽象,通过变量间接操作内存地址与指针的关系(指针就是存储地址的变量);区分 “字节(Byte)” 与 “位(bit)”(1 字节 = 8 位)⭐⭐⭐
指针的重要性提升效率、支持链式结构、动态内存、函数多返回值指针在链式结构(如链表)和动态内存中的不可替代性(无指针则无法实现)⭐⭐⭐⭐
内存管理原理变量声明时分配连续内存,起始地址为变量地址;不同类型占字节数不同变量地址是 “起始地址”(而非所有占用地址);内存分配的连续性(如两个变量地址通常连续)⭐⭐
指针学习建议从内存地址本质出发,结合代码实践克服 “指针难” 的心理障碍,先掌握 “地址” 概念,再理解 “指针变量存储地址” 的逻辑⭐⭐
指针应用场景动态内存操作、函数参数传递、数据结构实现(链表等)指针与数组的关系(数组名本质是首元素地址);指针运算的边界(避免越界访问)⭐⭐⭐⭐

关键关联:指针与地址的关系

指针变量的本质是 “存储地址的变量”。例如:

int a = 10;    // 变量a占用内存,地址假设为0x1234
int *p = &a;   // 指针p存储a的地址(0x1234),通过*p可访问a的值(10)

这里的p就是指针,它的值是a的地址 —— 这就是指针与地址的核心联系。

掌握这些基础后,后续学习指针运算、指针与数组 / 函数的结合时,会更加清晰。建议结合简单代码(如通过指针修改变量值)实践,直观感受指针操作内存的过程。

2. 指针的基本用法2

一、指针变量相关知识梳理

1. 指针的初始化

指针初始化是指在声明指针变量时,为其赋予一个合法的地址值,确保指针有明确的指向。

  • 语法形式<存储类型> <数据类型>* <指针变量名> = <地址量>

    • 存储类型:如auto(默认)、static等,决定指针变量的存储位置;

    • 数据类型:指针指向的目标变量的数据类型(如intfloat),需与目标变量类型一致;

    • 地址量:通常通过&运算符获取变量的地址(如&a表示变量a的地址)。

  • 示例与等价写法

    int a, *pa = &a;  // 声明时初始化
    // 等价于分步操作:
    int* pa;  // 先声明指针变量
    pa = &a;  // 再赋值(注意:此处pa前不加*,*仅用于声明)
  • 学习建议:初学时优先使用 “声明 + 赋值” 的分步写法,更易理解 “指针变量” 和 “地址赋值” 的逻辑,避免混淆*的作用(声明时表示指针类型,使用时表示解引用)。

2. 指针的内存模型

指针本质是 “存储地址的变量”,其内存模型需明确 “指针自身的存储” 和 “指针指向的目标” 的关系。

(1)指针与变量的关系
类型存储内容核心作用类比说明
普通变量具体数据值(如int a=10直接存储和表示数据房间内的物品(如 “507 室的书”)
指针变量其他变量的地址(如int *p=&a指向目标变量的内存位置快递单上的地址(如 “507 室”)
  • 关键逻辑:指针通过存储目标变量的地址,实现对目标内存的 “间接访问”—— 就像快递员通过地址找到收件人,而非直接携带物品。

(2)内存地址打印与验证

通过代码可直观验证指针与目标变量的地址关系:

#include <stdio.h>
int main() {int a = 10;int *p = &a;printf("a的地址:%p\n", &a);  // 输出a的起始地址(如0x7ffd9a8b9a4c)printf("p存储的地址:%p\n", p);  // 输出与&a相同的地址return 0;
}
  • 输出特征:两者地址完全一致,证明指针p确实存储了a的地址。

(3)内存布局细节
  • 目标变量的内存:如int a占 4 字节(32 位 / 64 位系统通用),地址为连续的 4 个字节(如0x7ffd9a8b9a4c0x7ffd9a8b9a4f),其 “变量地址” 指起始地址(最低地址)。

  • 指针变量的内存:指针本身也是变量,需占用存储空间 ——32 位系统占 4 字节,64 位系统占 8 字节(与指向的数据类型无关)。例如: int *pdouble *q在 64 位系统中,sizeof(p)sizeof(q)均为 8 字节。

(4)指针的内存表示(图示逻辑)
变量a:
地址:0xbf98c768 ~ 0xbf98c76b (4字节)
存储值:10
​
指针p:
自身地址:0xbfb3be5c ~ 0xbfb3be63 (8字节,64位系统)
存储值:0xbf98c768 (即a的起始地址,指向a)
  • 箭头关系:p的存储值 → a的地址 → a的存储值,形成 “指针指向变量” 的逻辑链。

3. 重要概念总结
(1)关键区别
  • 普通变量:int a=10 → 存储 “数据值”(10);

  • 指针变量:int *p=&a → 存储 “地址值”(a的地址);

  • 指针本身的地址:&p → 指针变量自己在内存中的地址(与它存储的地址完全不同)。

(2)操作要点
  • 取地址:用&运算符(如&a获取a的地址);

  • 指针赋值:必须赋值 “地址量”(不能直接赋数据值,如p=10是错误的);

  • 指针大小:与指向的类型无关,只与系统位数有关(32 位 4 字节,64 位 8 字节)。

(3)常见误区
  • 混淆 “指针自身的地址” 和 “指针存储的地址”:&p是指针自己的地址,p是它存储的目标地址;

  • 未初始化指针(野指针):int *p; *p=10; 会访问非法内存,导致程序崩溃;

  • 类型不匹配:float a; int *p=&a; 指针类型(int*)与目标类型(float)不匹配,解引用时会读取错误数据。

二、知识小结(核心考点与难度)

知识点核心内容考试重点 / 易混淆点难度系数
指针初始化声明时用地址赋值(*p=&a),分步写法更易理解*在声明和使用时的区别、赋值必须是地址量⭐⭐⭐
指针内存模型指针存储目标地址,自身也占内存;普通变量存储数据指针自身地址 vs 存储的地址、指针大小与系统位数的关系⭐⭐⭐⭐
指针与变量的关系指针通过地址指向变量,类比 “快递员与地址”指针操作的间接性(不直接存储数据)⭐⭐
指针类型匹配指针类型必须与指向的变量类型一致(int*对应int类型不匹配的后果(运行时错误)⭐⭐⭐⭐
野指针问题未初始化或指向已释放内存的指针,会导致非法访问野指针的成因与避免(初始化时赋NULL⭐⭐⭐⭐⭐

通过以上梳理,可明确指针的核心逻辑:指针是 “地址的容器”,通过存储地址实现对内存的间接操作,其使用的关键是 “明确指向” 和 “类型匹配”。后续学习中,结合代码实践(如打印地址、解引用操作)能更快掌握。

3. 指针的基本用法3

一、指针基本用法详解

1. 指针的核心概念
(1)指针、目标变量与目标数据
  • 本质定义: 指针就是内存地址(内存单元的编号);专门存储地址的变量称为指针变量(简称 “指针”)。

    • 若指针存储变量a的地址,则a称为指针的目标变量

    • a中存储的数据(如10)称为指针的目标数据

  • 生活类比(快递员模型)

    概念类比对象作用
    指针变量(p快递员记住收件人地址
    目标变量(a收件人地址(如 “507 室”)标识数据存储的位置
    目标数据(*p包裹内的物品(如 “书籍”)指针最终要访问的数据
(2)指针的三种关键表示方法
表示形式含义(以int a=10; int *p=&a为例)类比(快递员模型)
p指针变量存储的地址(即a的地址&a快递员手中的地址单(写着 “507 室”)
*p指针指向的目标数据(即a的值10快递员按地址找到的包裹(书籍)
&p指针变量自身的内存地址快递员自己的工号(与地址单无关)

  • 核心关系:

    *

    (解引用)和

    &

    (取地址)是互逆操作:

    *&a == a;  // 先取a的地址,再解引用,结果等于a本身
    &*p == p;  // 先解引用p(得到a),再取地址,结果等于p本身
(3)实例验证
#include <stdio.h>
int main() {int a = 10;int *p = &a;  // p存储a的地址
​printf("p(指针存储的地址):%p\n", p);    // 输出a的地址(如0x7ffd9a8b9a4c)printf("*p(目标数据):%d\n", *p);       // 输出a的值(10)printf("&p(指针自身的地址):%p\n", &p); // 输出p自己的地址(如0x7ffd9a8b9a50)return 0;
}
  • 关键结论:p*p&p是三个完全不同的概念,需严格区分。

2. 指针的赋值运算

指针赋值的核心是 “传递合法地址”,需遵循类型匹配和有效性原则。

(1)赋值的三种基本形式
  1. 变量地址赋给指针(最常用)

    double x = 15.5;
    double *px;  // 声明double类型指针
    px = &x;     // 赋值:px存储x的地址(类型匹配:double* → double)
    • 要求:指针类型必须与目标变量类型完全一致(如int*不能指向double变量)。

  2. 同类型指针间赋值

    float a = 3.14;
    float *p1, *p2;  // 同类型指针(float*)
    p1 = &a;         // p1指向a
    p2 = p1;         // p2也指向a(共享地址,不复制数据)
    • 效果p1p2存储相同地址(均指向a),通过*p1*p2均可访问 / 修改a的值。

  3. 数组地址赋给指针

    int arr[10];
    int *pa;
    pa = arr;  // 等价于pa = &arr[0](数组名即首元素地址)
    • 特性:数组名是 “首元素地址的常量”,可直接赋给同类型指针。

(2)赋值的禁忌与注意事项
  • 禁止直接赋整数int *p; p = 100; 是错误的(100 不是合法地址,可能指向非法内存)。

    • 唯一例外:可赋0(或NULL)表示 “空指针”(不指向任何内存):int *p = NULL;

  • 类型必须匹配float a; int *p = &a; 是错误的(int*float类型不匹配),解引用时会读取错误数据。

  • 赋值后共享地址p2 = p1 不会复制目标数据,只是让p2p1指向同一内存 —— 修改*p2会同时改变*p1和目标变量的值(数据同步)。

3. 指针的存储大小(占几个字节?)
(1)核心结论
  • 指针变量的大小与指向的类型无关,仅由操作系统位数决定:

    • 32 位系统:指针占 4 字节(可表示0~2^32-1的地址,对应 4GB 内存);

    • 64 位系统:指针占 8 字节(可表示0~2^64-1的地址,支持更大内存)。

  • 验证代码

    #include <stdio.h>
    int main() {int *p1;double *p2;char *p3;printf("int*大小:%zu\n", sizeof(p1));   // 32位输出4,64位输出8printf("double*大小:%zu\n", sizeof(p2)); // 同上(与指向类型无关)printf("char*大小:%zu\n", sizeof(p3));   // 同上return 0;
    }
(2)底层原因:地址总线宽度
  • 内存地址的表示依赖 CPU 的 “地址总线”:

    • 32 位 CPU 有 32 根地址线,可表示2^32个地址(需 4 字节存储);

    • 64 位 CPU 有 64 根地址线,需 8 字节存储地址。

  • 类比:门牌号的位数由小区规模决定(小小区用 3 位,大小区用 4 位)。

4. 关键问题解答(面试高频)
(1)“什么是指针?”
  • 标准答案: 指针本质是内存单元的地址(内存以字节为单位的编号)。C 语言中,专门用于存储地址的变量称为 “指针变量”(通常简称 “指针”)。 例如:int *p = &a 中,p是指针变量,存储a的地址(即指针),通过*p可访问a的值。

(2)“指针为什么占 4 字节(或 8 字节)?”
  • 标准答案: 指针的大小由系统位数决定:32 位系统中,地址总线宽度为 32 位(可表示0~2^32-1的地址),需 4 字节存储;64 位系统需 8 字节。 指针大小与指向的类型无关(如int*double*在同系统中大小相同)。

二、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
指针的本质指针 = 内存地址;指针变量是存储地址的变量区分 “指针(地址)” 和 “指针变量(存储地址的变量)”;p(地址)与*p(数据)的区别⭐⭐⭐
指针的赋值需赋值合法地址(如&a、同类型指针);数组名可直接赋给指针禁止将整数赋给指针(除0/NULL);类型不匹配的风险(如int*指向float⭐⭐⭐
多指针共享地址多个指针可指向同一变量(如p2=p1),共享数据访问修改*p2会同步影响*p1和目标变量(数据共享的副作用)⭐⭐
指针的大小由系统位数决定(32 位 4 字节,64 位 8 字节),与指向类型无关sizeof(p)的结果(与int/double等类型大小无关);32 位与 64 位的区别⭐⭐⭐
*&的互逆性*&a = a(先取地址再解引用);&*p = p(先解引用再取地址)运算符的组合使用(如*&*p等价于*p⭐⭐⭐

三、学习建议

  1. 画图理解内存关系: 用方框表示内存单元(标注地址和值),用箭头表示指针指向(如pa的地址),直观区分p*p&p

  2. 通过代码验证结论: 多写测试代码(如打印指针大小、赋值后访问数据),观察结果加深记忆。

  3. 规避野指针: 指针声明后必须初始化(如int *p = NULL;),避免未初始化就解引用(*p=10;)。

掌握这些基础后,后续学习指针运算、指针与数组 / 函数的结合会更轻松。

4. 指针运算(上)

一、指针运算的核心概念

指针运算的本质是地址的计算,但并非简单的数值加减 —— 运算结果会受指针指向的数据类型影响(与目标类型的字节大小绑定)。

1. 指针运算的种类

C 语言中,指针支持的运算类型有限,核心包括三类:

  • 赋值运算:为指针赋予地址(如p = &aq = p),已在前文讲解;

  • 算术运算:加法(p + n)、减法(p - n)、自增(p++)、自减(p--)、同类型指针相减(p - q);

    指针的 “+n” 运算不是简单的数值加 n,而是 “移动 n 个数据单元”:

    • 对于int*指针:1 个数据单元 = 4 字节(sizeof(int)),+2即移动 8 字节;

    • 对于double*指针:1 个数据单元 = 8 字节(sizeof(double)),+2即移动 16 字节;

    • 这也是为什么p+2q+2的地址差值不同(8 字节 vs 16 字节)。

  • 关系运算:比较两个指针的地址大小(如p > qp == q),用于判断地址位置关系。

二、指针的算术运算(核心)

1. 指针加减整数(p + n / p - n
(1)运算规则

指针加减整数n,表示指针指向的地址移动n个 “目标数据单元”,实际移动的字节数 = n × 目标类型的字节大小

  • 示例:

    int *ip;       // int型指针(int占4字节)
    double *dp;    // double型指针(double占8字节)
    ​
    // 假设初始地址:ip = 0x1000,dp = 0x2000
    ip += 2;       // 移动2个int单元 → 实际移动2×4=8字节 → 新地址0x1008
    dp -= 1;       // 移动1个double单元(反向) → 实际移动1×8=8字节 → 新地址0x1FF8
(2)关键意义

指针加减整数的核心作用是在连续内存中定位相邻数据(如数组元素)。例如:

int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[0];  // p指向arr[0](地址假设0x1000)
​
p + 1;  // 指向arr[1](0x1000 + 4 = 0x1004)
p + 3;  // 指向arr[3](0x1000 + 3×4 = 0x100C)
  • 无需手动计算字节偏移,指针会根据类型自动适配,这是指针操作数组的基础。

2. 同类型指针相减(p - q
(1)运算规则

两个同类型指针相减,结果是两者之间相隔的 “数据单元个数”(而非地址的字节差值),公式: 结果 = (p的地址 - q的地址) ÷ 目标类型的字节大小

  • 示例:

    int arr[5] = {1, 2, 3, 4, 5};
    int *p = &arr[0];  // 地址0x1000
    int *q = &arr[3];  // 地址0x100C(0x1000 + 3×4)
    ​
    int diff = q - p;  // 计算:(0x100C - 0x1000) ÷ 4 = 12 ÷ 4 = 3
    printf("%d", diff);  // 输出3(相隔3个int单元)
(2)应用场景

主要用于计算数组中两个元素的位置差(如判断元素间距、遍历数组时统计已访问个数)。

3. 指针自增 / 自减(p++ / p--
(1)运算规则

自增(p++)表示指针向后移动 1 个数据单元,自减(p--)表示向前移动 1 个数据单元,本质是p = p + 1p = p - 1的简写。

  • 前置与后置的区别

    • p++:先使用p的当前值,再移动指针;

    • ++p:先移动指针,再使用新地址的值。

  • 示例

    int arr[5] = {4, 8, 2, 6, 1};
    int *p = &arr[0];  // p指向arr[0](值4)
    ​
    int a = *p++;  // 等价于:a = *p; p = p + 1; → a=4,p指向arr[1]
    int b = *++p;  // 等价于:p = p + 1; b = *p; → p指向arr[2],b=2
    • 输出:a=4b=2(体现前置与后置的执行顺序差异)。

三、指针运算的关键特性

  1. 类型依赖性: 指针移动的字节数由指向的类型决定(与sizeof(类型)绑定):

    • int*(4 字节):p + 1 → 地址 + 4;

    • double*(8 字节):p + 1 → 地址 + 8;

    • char*(1 字节):p + 1 → 地址 + 1(唯一与数值加减结果一致的类型)。

  2. 运算限制

    • 指针不能做乘法、除法、模运算(无实际意义);

    • 不同类型的指针不能相减(编译报错);

    • 指针加指针无意义(编译报错)。

  3. 边界风险: 指针运算可能超出合法内存范围(如数组越界),导致 “野指针” 访问(如arr[5]是数组外的非法内存),需严格控制范围。

四、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
指针加减整数移动n个数据单元,实际字节 = n × sizeof(类型);用于定位数组元素指针移动的字节数与类型相关(如int*+1double*+1);与数值加减的区别⭐⭐⭐
指针相减同类型指针相减得 “数据单元个数”;(p地址 - q地址) ÷ sizeof(类型)结果是 “单元数” 而非字节数;不同类型指针不能相减⭐⭐⭐
指针自增 / 自减p++移动 1 个单元(后置先使用后移动);++p先移动后使用前置与后置的执行顺序(影响*p++*++p的结果);与整数自增的区别⭐⭐⭐
类型对运算的影响指针类型决定移动字节数(sizeof(类型)是核心)char*的特殊性(移动字节 = 数值加减);类型不匹配的运算错误⭐⭐⭐

五、应用示例:用指针遍历数组

#include <stdio.h>
​
int main() {int arr[5] = {10, 20, 30, 40, 50};int *p = arr;  // 指向arr[0](数组名=首元素地址)// 用指针遍历数组(替代下标arr[i])for (int i = 0; i < 5; i++) {printf("arr[%d] = %d(地址:%p)\n", i, *p, p);p++;  // 移动到下一个元素(每次+4字节)}return 0;
}
  • 优势:指针遍历比下标遍历更高效(直接操作地址,无需计算arr + i),是 C 语言处理数组的常用方式。

通过以上内容,可明确指针运算的 “类型依赖性” 和 “地址移动逻辑”,这是指针操作数组、字符串的基础。核心是理解:指针运算不是简单的数值计算,而是 “数据单元” 的移动

5. 指针运算(下)

一、指针的关系运算符

指针的关系运算本质是比较两个指针存储的地址值大小,核心规则如下:

1. 支持的运算符与比较对象
  • 运算符><>=<===!=(与普通变量的关系运算符相同)。

  • 比较对象:指针存储的地址值(内存单元编号),而非指针指向的数据。 例:若p指向地址0x1000q指向0x1004,则p < q为真(地址值0x1000 < 0x1004)。

2. 特殊规则与应用
  • NULL比较

    :指针可与NULL(空指针,本质是(void*)0)比较,用于判断指针是否有效:

    if (p == NULL) {  // 判断p是否为空指针(未指向任何内存)printf("指针无效\n");
    }
  • 禁止的比较:

    • 指针与普通整数(非0)比较无意义(如p > 100错误);

    • 不同类型的指针比较无意义(如int*double*比较);

    • 指向非连续内存的指针比较无意义(如两个无关变量的指针)。

二、指针关系运算的本质与应用场景

1. 本质:地址位置关系

内存地址按 “低→高” 编号,指针关系运算实际是判断地址的前后位置:

  • p指向数组a[0]q指向a[3],则p < q为真(a[0]地址低于a[3])。

  • 应用:遍历数组时判断指针是否越界(如p < &a[5]表示未超出数组范围)。

2. 典型应用:链表判空与数组遍历
  • 链表判空if (head == NULL) 判断链表是否为空;

  • 数组遍历边界控制:

    int a[5], *p = a;
    while (p < &a[5]) {  // 指针未超出数组范围(a[5]是数组末尾的下一个位置)*p = 0;  // 赋值p++;     // 移动指针
    }

三、应用案例解析(结合指针运算与优先级)

1. 案例 1:复杂表达式y = (*--p)++的执行顺序
int a[2] = {5, 10};
int *p = &a[1], y;
y = (*--p)++;  // 核心表达式
  • 执行步骤

    (按优先级):

    1. --p:指针p先向前移动 1 个int单元(从a[1]指向a[0]);

    2. *--p:取移动后指针指向的值(a[0]的原值5);

    3. (*--p)++:对a[0]的值做后置自增(a[0]变为6);

    4. 最终y = 5(自增前的值)。

  • 结果y=5a[0]=6

  • 建议:复杂表达式可读性差,拆分后更清晰:

    --p;       // 指针前移
    y = *p;    // 取值给y
    (*p)++;    // 自增目标值
2. 案例 2:指针越界与重置(数组遍历常见错误)
int a[3] = {1, 2, 3};
int *p = a, i;
​
// 第一次遍历:输出数组元素
for (i = 0; i < 3; i++) {printf("%d ", *p++);  // 输出1 2 3,结束后p指向a[3](数组外)
}
​
// 错误:直接使用越界指针
printf("\n再次访问:%d", *p);  // 输出随机值(越界访问)
​
// 正确:重置指针后访问
p = a;  // 重置指针到数组起始位置
printf("\n重置后:%d", *p);  // 输出1(正确访问a[0])
  • 关键问题:指针运算会改变自身值(p++使p越界),再次使用前必须重置(p = a)。

3. 案例 3:*p++与运算符优先级(核心易错点)
  • 优先级规则: 后缀++(优先级 1) > 解引用*(优先级 4),因此 *p++ 等价于 *(p++)(先移动指针,再取原值)。

  • 示例解析

    int a[2] = {10, 20};
    int *p = a, x;
    x = *p++;  // 等价于:x = *p; p++;
    • 执行后:x=10a[0]的值),p指向a[1](指针已移动)。

  • 对比(\*p)++

    p = a;
    x = (*p)++;  // 等价于:x = *p; *p = *p + 1;
    • 执行后:x=10a[0]变为11(值自增),p仍指向a[0](指针未移动)。

四、指针运算的核心注意事项

  1. 越界风险:指针超出数组 / 合法内存范围(如p > &a[4])会导致 “野指针”,访问时可能崩溃。

  2. 指针位置跟踪:指针运算会改变自身值(如p++),再次使用前需确认是否在合法范围(必要时重置p = a)。

  3. 优先级陷阱:对*p++(*p)++等表达式不确定时,用括号明确顺序(如*(p++))。

  4. 可读性优先:避免一行代码写复杂逻辑(如y = (*--p)++),拆分后更易维护。

五、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
指针关系运算比较指针存储的地址值(p < q表示p地址低于q);可与NULL比较判空指针比较的是地址而非数据;禁止与普通整数(非0)比较⭐⭐⭐
*p++(*p)++*p++:先取值再移动指针(*(p++));(*p)++:先取值再自增数据(指针不动)运算符优先级(后缀++ > *);两者的执行结果差异⭐⭐⭐⭐
指针越界与重置遍历后指针可能越界,需重置(如p = a)才能重新使用越界访问的风险(程序崩溃);指针位置的跟踪(避免 “忘记指针已移动”)⭐⭐⭐
复杂表达式拆分复杂逻辑(如(*--p)++)拆分后更易读,减少错误结合自增、解引用、指针移动的表达式执行顺序⭐⭐⭐

通过以上内容可知,指针关系运算的核心是 “地址比较”,而实际应用中需重点关注指针位置的合法性和表达式的优先级 —— 这是避免指针错误的关键。

6. 指针与数组

一、数组名的本质特性

1. 数组名是地址常量

数组名代表数组首元素的地址(即&a[0]),但它是一个常量,不能被修改:

int a[5] = {1,2,3,4,5};
// 合法:获取数组首地址
int *p = a;      // 等价于 p = &a[0];
​
// 非法:数组名不能自增/自减
a++;             // 错误!数组名是常量,不能被修改
2. 数组名的合法运算

虽然数组名不能被修改,但可以参与地址运算

// 合法:计算数组元素的地址
printf("%p\n", a + 1);    // 输出a[1]的地址(a + 1×sizeof(int))
printf("%p\n", &a[2]);    // 输出a[2]的地址(等价于a + 2)

二、数组元素的四种等价访问方式

1. 核心公式与访问形式

对于数组a和指向数组首元素的指针p,以下四种访问方式完全等价:

int a[5] = {10,20,30,40,50};
int *p = a;  // p指向数组首元素
​
// 四种等价访问方式
a[2]         // 数组下标法
*(a + 2)     // 数组名偏移法
p[2]         // 指针下标法
*(p + 2)     // 指针偏移法
2. 底层实现原理

所有访问方式最终都会被编译器转换为*(基地址 + 偏移量)的形式:

  • a[i] 等价于 *(a + i): 先计算a + i×sizeof(int)的地址,再解引用获取值。

  • p[i] 等价于 *(p + i): 同理,基于指针p的当前地址偏移i个元素。

三、指针与数组名的关键区别

特性指针变量(int *p数组名(int a[5]
是否可修改是(变量)否(常量)
支持自增 / 自减支持(如p++不支持(a++ 报错)
内存分配单独分配内存存储地址值直接代表数组首地址
初始化方式需显式赋值(如p = a隐式初始化为首元素地址

四、应用案例:数组反转(双指针法)

1. 算法思路

使用两个指针分别指向数组首尾,交换元素后向中间移动,直到两指针相遇:

void reverse(int *a, int n) {int *p = a;           // 指向首元素int *q = a + n - 1;   // 指向末元素(a[n-1])while (p < q) {       // 循环条件:p未超过qint temp = *p;*p = *q;*q = temp;p++;              // 指针向中间移动q--;}
}
​
// 调用示例
int main() {int a[5] = {1,2,3,4,5};reverse(a, 5);        // 反转数组// 输出:5 4 3 2 1
}
2. 关键细节
  • 数组长度计算n = sizeof(a)/sizeof(a[0]) 仅适用于数组名,不适用于指针(若a是指针,sizeof(a)返回指针大小,非数组大小)。

  • 循环终止条件: 使用p < q而非p != q,避免偶数长度数组漏交换中间元素对。

五、指针运算的常见陷阱

1. 指针移动导致下标基准改变
int a[5] = {10,20,30,40,50};
int *p = a;
​
printf("%d\n", p[1]);   // 输出20(a[1])
p++;                    // p指向a[1]
printf("%d\n", p[1]);   // 输出30(a[2])
  • 原因p[1] 等价于 *(p + 1)p移动后基准地址改变。

2. 混淆数组名与指针
int a[5] = {1,2,3,4,5};
int *p = a;
​
// 合法:指针可自增
p++;                // p指向a[1]
​
// 非法:数组名不可自增
a++;                // 编译错误!

六、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
数组名的本质数组名是地址常量(指向首元素),不可修改(如a++非法),但可参与运算(如a+1区分数组名作为常量与指针作为变量的差异;a vs &a的含义(&a类型为int (*)[5]⭐⭐⭐
四种访问方式a[i]*(a+i)p[i]*(p+i) 完全等价,底层均为*(基地址+偏移量)理解下标访问的本质是地址运算;指针移动对p[i]的影响⭐⭐⭐⭐
数组反转实现双指针法(首尾指针交换元素),循环条件p < q,需正确计算数组长度奇数 / 偶数长度数组的处理;p < q vs p != q的选择⭐⭐⭐⭐
指针运算陷阱指针移动会改变下标基准(如p++p[1]访问的元素不同)跟踪指针位置;避免在复杂表达式中混用指针移动和下标访问⭐⭐⭐

七、实践建议

  1. 优先使用指针遍历数组: 指针运算比下标访问效率更高(尤其在循环中),且可避免数组越界(通过指针比较控制边界)。

  2. 明确数组长度传递: 函数接收数组时,需额外传递长度参数(如void func(int *a, int n)),避免在函数内使用sizeof(a)(会返回指针大小)。

  3. 慎用复杂指针表达式: 避免在一行代码中混用*++--(如*p++),拆分后可读性更强(如*p; p++;)。

掌握指针与数组的关系是 C 语言的核心技能,通过多写代码验证(如手动实现字符串复制、数组排序)可加深理解。

7. 指针与二维数组(上)

一、二维数组的本质与存储特性

1. 二维数组的逻辑与物理结构
  • 逻辑结构:可视为 “数组的数组”(由多个一维数组组成)。 例如:int a[3][2] = {{1,2}, {3,4}, {5,6}} 可理解为:

    • 包含 3 个 “行数组”:a[0](元素1,2)、a[1](元素3,4)、a[2](元素5,6);

    • 每个行数组是一维数组,行数组名(如a[0])是该一维数组的首地址(常量)。

  • 物理存储: 二维数组在内存中连续存储,按 “行优先” 原则排列(一行存满后接下一行)。 上述a[3][2]的存储顺序为:1 → 2 → 3 → 4 → 5 → 6,相邻元素地址差 4 字节(int类型)。

2. 行数组名的特性(以a[0]为例)
  • 是地址常量:行数组名代表该行首元素的地址(如a[0]等价于&a[0][0]),不能被修改(如a[0]++会报错 “需要左值”)。

  • sizeof 计算sizeof(a[0])返回该行的总字节数(如a[0]含 2 个int,则sizeof(a[0])=8字节)。

二、二维数组的地址表示与指针类型

1. 关键地址表示(以int a[3][2]为例)
表达式含义(地址类型)移动 1 个单位的字节数
a二维数组首地址(行指针,指向行数组a[0]8 字节(一行 2 个int
a + ii行的首地址(行指针)8 字节
a[i]i行首元素地址(列指针,指向a[i][0]4 字节(1 个int
a[i] + ji行第j列元素地址(列指针)4 字节
&a[i][j]i行第j列元素地址(列指针)4 字节

  • 核心转换关系:行指针可通过解引用转为列指针: *(a + i) == a[i]a + i是行指针,解引用后得到列指针)。

2. 元素访问的等价写法

二维数组元素a[i][j]的所有访问方式完全等价:

a[i][j]               // 下标法(最直观)
*(a[i] + j)           // 列指针偏移
*(*(a + i) + j)       // 行指针转列指针后偏移
  • 示例:访问a[1][1](值为 4): a[1][1]*(a[1] + 1)*(*(a + 1) + 1),均返回 4。

三、编程实现:指针遍历二维数组

1. 方法 1:一级指针遍历(利用存储连续性)
#include <stdio.h>
​
int main() {int a[3][2] = {{1,2}, {3,4}, {5,6}};int *p = &a[0][0];  // 指向首元素(列指针)int rows = 3, cols = 2;
​// 遍历所有元素(共3×2=6个)for (int i = 0; i < rows * cols; i++) {printf("%d ", *(p + i));  // 连续访问}// 输出:1 2 3 4 5 6return 0;
}
  • 核心原理:二维数组物理存储连续,可视为 “伪装的一维数组”,通过一级指针连续偏移访问。

2. 方法 2:行指针遍历(按行访问)
// 行指针定义:int (*p)[2] 表示指向“含2个int的数组”的指针
int (*p)[2] = a;  // p指向二维数组首行(a[0])
​
for (int i = 0; i < 3; i++) {for (int j = 0; j < 2; j++) {printf("%d ", *(*(p + i) + j));  // 先定位行,再定位列}
}
  • 行指针特性p + i移动i行(每次 8 字节),解引用后*(p + i)得到该行的列指针。

四、常见错误与注意事项

  1. 行指针与列指针类型混淆int *p = a; 是错误的(a是行指针,p是列指针,类型不匹配),正确写法:int *p = &a[0][0];int (*p)[2] = a;

  2. 误将行数组名视为可修改变量a[0]++ 报错(a[0]是常量),需通过指针变量操作(如int *p = a[0]; p++)。

  3. 计算数组长度的误区sizeof(a) 返回二维数组总字节数(3×2×4=24),sizeof(a[0]) 返回一行字节数(2×4=8),sizeof(a[0][0]) 返回单个元素字节数(4)。 行数 = sizeof(a) / sizeof(a[0]),列数 = sizeof(a[0]) / sizeof(a[0][0])

五、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
二维数组存储特性连续存储,行优先排列;可视为 “一维数组的数组”逻辑行列与物理存储的区别(表面二维,实际连续);行优先存储的顺序(先存满一行再存下一行)⭐⭐⭐
行指针与列指针行指针(a)指向行数组,移动单位为 “行”;列指针(a[0])指向元素,移动单位为 “元素”a + 1(移动一行)与a[0] + 1(移动一个元素)的字节差;行指针转列指针的解引用操作(*a⭐⭐⭐⭐
元素访问等价写法a[i][j]*(a[i]+j)*(*(a+i)+j) 完全等价多层解引用的执行顺序(先定位行,再定位列);指针遍历的两种方法(一级指针 / 行指针)⭐⭐⭐⭐
类型匹配与指针定义行指针需用int (*p)[n]定义,列指针用int *p定义避免行指针直接赋值给列指针(如int *p = a 错误);正确定义行指针的语法(括号不可少)⭐⭐⭐⭐

掌握二维数组与指针的关系,核心是理解 “行优先存储” 和 “行 / 列指针的类型差异”。通过多写遍历代码(如手动实现矩阵转置),可快速掌握指针操作二维数组的技巧。

8. 指针与二维数组(下)

一、二维数组的行地址与列地址

二维数组的地址分为 “行地址” 和 “列地址”,两者的核心区别在于移动单位(以int a[2][2] = {{1,2}, {3,4}}为例,每个int占 4 字节)。

1. 行地址(以数组名a为例)
  • 本质:指向 “一整行元素” 的地址(行指针的常量形式)。

  • 运算特性:a + i移动i行,实际字节数 =i × 每行元素数 × sizeof(元素类型)。

    例:a的初始地址若为0xbfbclfc8,则:

    • a + 1 移动 1 行(2 个int),地址变为 0xbfbclfc8 + 2×4 = 0xbfbclfd0(差值 8 字节)。

  • 应用:用于整行操作(如行交换、行复制)。

2. 列地址(以a[0]*a为例)
  • 本质:指向 “单个元素” 的地址(一级指针形式)。

  • 运算特性:a[0] + j移动j个元素,实际字节数 =j × sizeof(元素类型)。

    例:

    a[0]

    的初始地址与

    a

    相同(0xbfbclfc8),则:

    • a[0] + 1 移动 1 个int,地址变为 0xbfbclfc8 + 4 = 0xbfbclfcc(差值 4 字节)。

  • 应用:用于元素级操作(如遍历单个元素、修改特定值)。

3. 关键转换:行地址 → 列地址

通过解引用运算符*,可将行地址转为列地址: *a == a[0]a是行地址,解引用后得到该行首元素的列地址)。

  • 例:*a + 1 等价于 a[0] + 1(移动 1 个元素,差值 4 字节),与 a + 1(移动 1 行,差值 8 字节)完全不同。

二、行指针(数组指针)的定义与使用

行指针是 “存储行地址的变量”,用于灵活操作二维数组的行。

1. 声明语法
<存储类型> <数据类型> (*<指针名>)[<列数>];
  • <列数>:必须与二维数组的列数一致,决定行指针移动 1 行的元素数量。 例:int (*p)[2]; 表示 p 是行指针,指向 “含 2 个int的数组”(即一行有 2 列)。

2. 与二维数组的绑定
int a[2][2] = {{1,2}, {3,4}};
int (*p)[2] = a;  // p指向a的首行(等价于p = &a[0])
  • 绑定要求p 的列数(2)必须与 a 的列数(2)相同,否则类型不匹配。

3. 访问元素的两种方式

以访问a[1][1](值为 4)为例:

  1. 下标法p[i][j](与数组名用法完全相同) p[1][1] 直接访问第 2 行第 2 列元素。

  2. 指针法*(*(p + i) + j)

    • p + 1:移动到第 2 行(地址0xbfbclfd0);

    • *(p + 1):转为列地址(指向a[1][0],地址0xbfbclfd0);

    • *(p + 1) + 1:移动到a[1][1](地址0xbfbclfd4);

    • *(*(p + 1) + 1):取元素值 4。

4. 行指针与数组名的区别
特性行指针变量(int (*p)[2]二维数组名(int a[2][2]
是否可修改是(变量,如p++合法)否(常量,如a++报错)
地址存储单独占用内存存储行地址本身就是首行地址,不占额外内存
用途灵活遍历、传递二维数组给函数直接访问数组元素

三、行指针遍历二维数组(实例)

#include <stdio.h>
​
int main() {int a[3][2] = {{1,2}, {3,4}, {5,6}};int (*p)[2] = a;  // 行指针指向a的首行
​// 外层循环控制行,内层循环控制列for (int i = 0; i < 3; i++) {for (int j = 0; j < 2; j++) {// 四种等价访问方式printf("%d ", a[i][j]);       // 数组下标printf("%d ", p[i][j]);       // 行指针下标printf("%d ", *(*(a + i) + j));// 数组指针法printf("%d ", *(*(p + i) + j));// 行指针指针法}printf("\n");}return 0;
}

  • 输出结果:每行打印 1 1 1 1 2 2 2 2(四者等价)。

  • 核心逻辑:外层循环通过p + i定位行,内层循环通过*(p + i) + j定位列。

四、常见错误与注意事项

  1. 行指针声明遗漏括号int *p[2] 是指针数组(数组元素是指针),而非行指针;正确行指针声明必须加括号:int (*p)[2]

  2. 列数不匹配int a[2][3]; int (*p)[2] = a; 会报错(p的列数 2 与a的列数 3 不匹配),需保证p的列数与数组一致。

  3. 混淆行指针与一级指针: 行指针p不能直接赋值给一级指针int *q(类型不匹配),需先解引用:q = *p*p是列地址,可赋值给int *)。

五、知识小结

知识点核心内容考试重点 / 易混淆点难度系数
行地址与列地址行地址移动单位是 “行”(a+1移动 1 行),列地址移动单位是 “元素”(a[0]+1移动 1 个元素)a+1a[0]+1的地址差值(行 vs 元素);*a的作用(行地址转列地址)⭐⭐⭐⭐
行指针声明与特性声明:int (*p)[n]n为列数);指向整行,p+1移动 1 行区分int (*p)[n](行指针)与int *p[n](指针数组);列数必须与数组匹配⭐⭐⭐⭐
元素访问等价写法a[i][j]p[i][j]*(*(a+i)+j)*(*(p+i)+j) 完全等价多层解引用的执行顺序(先定位行,再定位列);行指针与数组名的用法一致性⭐⭐⭐
行指针的灵活应用作为函数参数传递二维数组(避免数组名常量限制);实现动态行操作行指针在函数传参中的优势(无需固定行数);整行操作的效率(比逐个元素操作更快)⭐⭐⭐

掌握行指针核心是理解 “以行为单位移动” 的特性,以及它与二维数组的绑定关系。通过对比行地址与列地址的运算差异,可快速区分两者的应用场景。

http://www.dtcms.com/a/294651.html

相关文章:

  • 红松推出国内首个银发AI播客产品,首创“边听边问”交互体验
  • 5.综合案例 案例演示
  • [硬件电路-76]:无论是波长还是时间,还是能量维度来看,频率越高,越走进微观世界,微观世界的影响越大;频率越低,越走进宏观世界,微观世界的影响越小;
  • 销采一体化客户管理系统核心要点速通
  • IDEA202403 超好用设置【持续更新】
  • SAP第二季度利润大增但云业务疲软,股价承压下跌
  • 【笔记】Handy Multi-Agent Tutorial 第三章: CAMEL框架简介及实践(实践部分)
  • HCIP笔记(第一、二章)
  • 电商项目_秒杀_压测
  • 策略模式(Strategy Pattern)+ 模板方法模式(Template Method Pattern)的组合使用
  • 水泥厂码垛环节的协议转换实践:从Modbus TCP到DeviceNet
  • opencv学习(图像读取)
  • CPU,减少晶体管翻转次数的编码
  • haproxy算法
  • LSTM学习笔记
  • unity小:webgl开发注意事项(持续更新)
  • 2025年7月Nature子刊-Adam梯度下降优化算法Adam Gradient Descent-附Matlab免费代码
  • CVE-2025-32463漏洞:sudo权限提升漏洞全解析
  • OpenLayers 快速入门(五)Controls 对象
  • 西安旅游行业从业者:凤凰新闻怎么发稿有哪些注意事项
  • 编程日常开发工具整理
  • 智能工具重塑光伏设计:精准、高效与科学的融合
  • 第二章 W55MH32 DHCP示例
  • 安卓项目--基于百度云的人脸识别考勤系统
  • 基于沁恒微电子CH32V307单片机使用
  • 前端项目下载发票pdf文件要求改文件名笔记
  • LLM指纹底层技术——模型压缩与优化
  • Windows安装git教程(图文版)
  • 批量剪辑矩阵分发系统源码搭建,支持OEM
  • 电机驱动-理论学习-FOC算法理解