C语言KR圣经笔记 5.3指针和数组 5.4地址运算
5.3 指针和数组
在 C 语言中,指针和数组有着非常强的关联,强到应当把两者同时拿出来讨论。任何可以通过数组下标来做到的操作,也都能用指针来做到。而指针的版本通常会更快,但至少对初学者来说会更难理解。
如下声明
int a[10];
定义了一个大小为 10 的数组 a,即由10个名为 a[0], a[1], ... a[9] 的连续对象所组成的块。
用 a[i] 来表示数组的第 i 个元素。如果 pa 是指向整数的指针,其声明为
int *pa;
则赋值语句
pa = &a[0];
使 pa 指向 a 的第 0 个元素;也就是说, pa 包含了 a[0] 的地址。
现在如果再赋值
x = *pa;
会将 a[0] 的内容拷贝给 x。
如果 pa 指向数组的某个特定元素,则根据定义,pa + 1 会指向下一个元素,pa + i 指向 pa 之后的第 i 个元素, pa - i 指向 pa 之前的第 i 个元素。因此,如果 pa 指向 a[0],则
*(pa+1)
表示 a[1] 的内容,而 pa + i 表示 a[i] 的地址,而 *(pa+i) 是 a[i] 的内容。
其实,不管数组 a 里面的变量是什么类型,占据多大空间,上述说法都是正确的。“将指针加1” 的含义是 pa + 1 指向下一个对象,由此扩展到所有指针运算,可得 pa + i 指向 pa 之后的第 i 个对象。
下标和指针运算之间有非常紧密的关联。根据定义,类型为数组的变量或表达式,其值为数组第 0 个元素的地址。因此,经过如下赋值之后
pa = &a[0];
pa 和 a 有相同的值。由于数组的名称就是其首个元素位置的同义词,赋值 pa = &a[0] 也能够写成
pa = a;
而更令人惊奇(至少在首次看到时)的事实是,a[i] 也能够写成 *(a+i)。在计算 a[i] 时,C会立即将其转换为 *(a+i);两种形式是等价的。将操作符 & 分别应用到两者,就能得到 &a[i] 和 a+i 也是等价的:a+i 是 a 之后第 i 个元素的地址。从另一个角度看,如果 pa 是指针,可以对它加下标来使用;pa[i] 等价于 *(pa+i)。简而言之, 数组+下标的表达式,等价于指针+偏移的表达式。
数组名称和指针有一个区别必须牢记。指针是一个变量,因此 pa=a 和 pa++ 都是合法的。但数组名不是变量;像 a=pa 和 a++ 这样的结构是非法的。
当数组名称被传递给函数时,传递的是数组首元素的位置。在被调函数中,该参数是一个局部变量,因此数组名参数是一个指针,即一个包含地址的变量。我们可以利用这个事实来写另一个版本的 strlen,计算字符串的长度:
/* strlen: 返回字符串s的长度 */
int strlen(char *s)
{int n;for (n = 0; *s != '\0'; s++)n++;return n;
}
由于 s 是一个指针,对其递增是完全合法的;s++ 对调用 strlen 的函数里面的字符串不起任何效果,它仅仅是对该指针在 strlen 中的私有拷贝进行递增。这意味着如下调用:
strlen("hello, world"); /* 字符串常量 */
strlen(array); /* char array[100]; */
strlen(ptr); /* char *ptr; */
都是可行的。
作为函数定义中的形参
char s[];
和
char *s;
是等价的;我们更偏向后者,因为它更显式地表明该参数是指针。当数组名称被传递给函数时,函数可以根据自己的意愿来认定它处理的是数组还是指针,并进行对应的操作。如果能够让代码看起来更恰当、更清晰,甚至可以使用两种表示法。
通过传递指向子数组开头的指针,可以将数组的一部分传递给函数。例如,如果 a 是数组,则
f(&a[2])
和
f(a+2)
都是把从 a[2] 开头的子数组地址传给函数 f 。在函数 f 中,参数声明可以写为
f(int arr[]) { ... }
或是
f(int *arr) { ... }
从 f 的角度而言,参数指向的是大数组的一部分还是数组真实的首地址,都无关紧要。
如果能保证元素的存在,可以将数组向前索引;p[-1] 和 p[-2] 等在语法上都是合法的,它们都指向 紧挨着 p[0] 的之前的元素。当然,引用数组边界之外的元素是非法的。
5.4 地址运算
如果 p 是指向数组中某个元素的指针,则 p++ 将 p 递增以指向下一个元素,而 p+=i 将 p 递增 i 以指向当前元素后的第 i 个元素。这些及其类似的结构,是指针或地址运算的最简单形式。
C 语言地址运算的方式是一致而且有规律的;对指针,数组和地址运算的集成是 C 语言的优势之一。我们写一个简单原始的内存管理器来说明。有两个例程。第一个是 alloc(n),返回一个指针 p, 指向 n 个连续字符的位置,alloc 的调用者可以用它来保存字符。第二个是 afree(p),释放通过alloc 获取到的内存,使这块内存后续能被重用。说它们是“简单原始”的,因为必须以 alloc 相反的顺序来调用 afree 。也就是说,alloc 和 afree 管理的内存是一个栈,或者叫后进先出队列。标准库提供的类似函数叫做 malloc 和 free ,没有这个限制;在8.7节会展示如何来实现它们。
最简单的实现是让 alloc 交出一个我们称之为 allocbuf 的大字符数组中的一小部分。这个数组是 alloc 和 free 私有的。因为它们使用指针而不是下标来处理,其他例程不需要知道数组的名字,因此在包含 alloc 和 free 的源文件中,该数组可以声明为 static,使之对外部不可见。在实际的内存管理器中,数组甚至都不需要有名字;它可能是通过调用 malloc 或请求操作系统,从而获取到的一个指向未命名内存块的指针。
另一个所需的信息是 allocbuf 用了多少。我们使用一个指针 allocp 来指向下一个空闲的元素。当 alloc 被要求 n 个字符时,它检查在 allocbuf 中是否存在足够的空间。如果是,它返回当前的 allocp 值(即空闲块的开头),并将其递增 n,以指向下一个空闲区域。如果没有足够空间,alloc 返回零。afree(p) 仅仅是将 allocp 设为 p,如果 p 在 allocbuf 内部的话。
#define ALLOCSIZE 10000 /* 可用空间 */static char allocbuf[ALLOCSIZE]; /* alloc所用空间 */
static char *allocp = allocbuf; /* 下一个空闲位置 */char *alloc(int n) /* 返回指向n个字符的指针 */
{if (allocbuf + ALLOCSIZE - allocp >= n) { /* 空间足够 */allocp += n;return allocp - n; /* 旧的指针 */} else { /* 空间不足 */return 0;}
}void afree(char *p) /* 释放p指向的空间 */
{if (p >= allocbuf && p < allocbuf + ALLOCSIZE)allocp = p;
}
通常,指针可以像其他变量一样初始化,不过正常情况下,有意义的初始值只有零,或者是包含之前定义过且类型匹配的地址的表达式。如下声明
static char *allocp = allocbuf;
将 allocp 定义为字符串指针,并将其初始化为指向 allocbuf 的开头,即程序启动时的下一个空闲位置。这也可以写成
static char *allocp = &allocbuf[0];
因为数组名称正是其第0个元素的地址。
如下判断
if (allocbuf + ALLOCSIZE - allocp >= n) { /* 足够 */
用来检查是否有足够的空间可满足分配 n 个字符的请求。如果有,则 allocp 的新值最多能到达的位置比 allocbuf 的末尾元素还超过一个【注意这个地址已经不属于allocbuf了,只可用来比较,不能分配】。如果请求能够满足,alloc 返回指向一块字符的起始位置的指针(注意alloc函数的声明)。如果不能,alloc 必须能够返回指示空间不足的信号。C 语言保证 0 永远不会是数据的合法地址,因此返回值零用来指示不正常的事件,此时为空间不足。
指针和整数是不可以相互交换使用的。零是唯一的例外:常量零可以被赋给指针,且指针可以与常量零比较。通常用符号常量 NULL 作为助记符来代替零,以更清晰地表示这是指针的特殊值。NULL 在 <stdio.h> 中定义。此后我们都将使用 NULL。
如下判断
if (allocbuf + ALLOCSIZE - allocp >= n) { /* 足够 */
以及
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
显示了指针运算的一些重要方面。首先,指针在某些环境下可以进行比较。如果 p 和 q 都指向同一数组的元素,则关系操作符如 ==,!=,<,>= 等等,都能正常使用。例如若要
p < q
为真,则 p 指向的元素在 q 指向的元素之前。任何指针都能与零进行相等或不等的比较,这是有意义的。但如果在不指向相同数组的指针之间进行运算或比较,其行为是未定义的。(有一个例外,数组末尾之后的第一个元素可以用于指针运算)
第二,我们已经观察到,指针和整数可以相加或相减。如下结构
p + n
表示 p 当前所指地址之后的第 n 个对象的地址。不管 p 指向何种类型的对象,这个说法总是正确的;n 会根据 p 指向的对象的大小进行放大,对象大小是由 p 的声明所决定的。例如,如果 int 占四个字节,则 int 会乘以四。【即如果p指向整数,则C编译器在计算 p + n 时会把n乘以4,例如 p=12345678 ,n=1,则 p+n = 12345678 + 1*4】
指针减法也是合法的:如果 p 和 q 指向同一数组内的元素,且 p<q,则 q-p+1 是 p 和 q 之间的元素个数(包含两端)。可用利用这个事实再写出另一个版本的 strlen:
/* strlen: 计算字符串s的长度 */
int strlen(char *s)
{char *p = s;while(*p != '\0')p++;return p - s;
}
在声明中,p 被初始化为 s,即字符串的首个字符。在 while 循环中,挨个检查每个字符,直到发现末尾的 '\0'。由于 p 指向的是字符,p++ 每次都将 p 移到下一个字符,而 p - s 表示移动过的字符数,即字符串的长度。(字符串中的字符数量可能太大,int 保存不下。头文件<stddef.h> 定义了一个类型 ptrdiff_t,它足够大,可以用来保存两个指针之间的有符号差值。然而,如果我们更仔细的话,会使用 size_t 来做 strlen 的返回值,以与标准库的版本相匹配。size_t 是 sizeof 操作符返回的有符号整型。)
指针运算是一致的:如果我们要处理比 char 占内存更多的 float,而 p 是指向 float 的指针,则 p++ 会指向下一个 float。这样,仅仅需要把 alloc 和 afree 中的所有 char 替换成 float,我们就能写出 alloc 的 float 版本 。所有的指针操作都会自动地考虑到所指向对象的大小。
合法的指针操作有:将指针赋给相同类型,一个指针与一个整数的加减,指向相同数组的两个指针的减法或比较,以及与零的赋值和比较。其他所有指针运算都是非法的。对两个指针相加是非法的,非法的还有相乘或相除,移位或者掩码,以及将指针与 float 或 double 相加,甚至,在没有强制类型转换的情况下,将一个类型的指针赋给另一个类型的指针。最后一种情况对 void * 是特例,它是可以不用强制类型转换。
相关文章:

C语言KR圣经笔记 5.3指针和数组 5.4地址运算
5.3 指针和数组 在 C 语言中,指针和数组有着非常强的关联,强到应当把两者同时拿出来讨论。任何可以通过数组下标来做到的操作,也都能用指针来做到。而指针的版本通常会更快,但至少对初学者来说会更难理解。 如下声明 int a[10]…...

设计模式:简单工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式、工厂方法模式、抽象工厂模式 1. 为什么需要工厂模式?2. 简单工厂模式2.1. 定义2.2. 代码实现2.3. 优点2.4. 缺点2.5. 适用场景 3. 工厂方法模式3.1. 有了简单工厂模式为什么还需要有工厂方法模式?3.2. 定义3.3. 代码实现3.4. 主要优点3.5.…...
Could not load library libcudnn_cnn_infer.so.8
报错: Could not load library libcudnn_cnn_infer.so.8. Error: /root/miniconda3/lib/python3.10/site-packages/torch/lib/libcudnn_cnn_infer.so.8: undefined symbol: _ZNK10cask_cudnn14BaseKernelInfo18minorCCVCompatibleENS_8SafeEnumINS_47ComputeCapa…...

ELement UI时间控件el-date-picker误差8小时解决办法
一、问题描述: 在项目中引用了elementui中的date-picker组件,选中的时间跟实际相差八小时,且格式不是自己想要的格式 <el-date-pickertype"date"placeholder"选择日期"format"yyyy/M/d"v-model"form…...
Linux日志论转
系统日志、审计日志、诊断日志 日志系统rsyslog 日志管理基础: rsyslog 日志管理 logrotate日志轮转常见的日志文件 #tail -f /var/log/messages #动态查看日志文件的尾部,系统主日志文件#tail -f /var/log/secure #记录认证、安全的日志…...

第7课 利用FFmpeg将摄像头画面与麦克风数据合成后推送到rtmp服务器
上节课我们已经拿到了摄像头数据和麦克风数据,这节课我们来看一下如何将二者合并起来推送到rtmp服务器。推送音视频合成流到rtmp服务器地址的流程如下: 1.创建输出流 //初始化输出流上下文 avformat_alloc_output_context2(&outFormatCtx, NULL, &…...

Microsoft Visual Studio 2022 install Project 下载慢
1. 关闭Internet 协议版本6 2. 如果没有效果,打开Internet 协议版本4,更改DNS 3. 在浏览器中下载后安装,下载地址如下: Microsoft Visual Studio Installer Projects 2022 - Visual Studio Marketplace 4. 安装时注意关闭vs&…...

uniapp---安卓真机调试提示检测不到手机【解决办法】
最近在做APP,由于华为手机更新过系统,再次用来调试APP发现就不行了。下面给出具体的解决方法: 第一步:打开【允许开发人员选项】 找到【设置】点击【关于手机】找到【版本号】点击7次或多次,允许开发人员选项。 第二…...
Nginx(十四) 配置文件详解 - 负载均衡(超详细)
本篇文章主要讲ngx_http_upstream_module模块下各指令的使用方法。 1. upstream 上游服务器组/集群 Syntax: upstream name { ... } Default: — Context: http upstream指令定义了一个上游服务器组/集群,便于反向代理中的proxy_pass使用。服务器可以监听…...

大数据应用安全策略包括什么
大数据应用安全策略是为了保障大数据应用中的数据安全而采取的一系列措施,其重要性不容小觑。以下是大数据应用安全策略所包含的主要内容: 一、数据加密与安全存储 数据加密:对于敏感数据,应采用加密技术进行保护,包括…...

Ubuntu软件和vmware下载
https://cn.ubuntu.com/download/desktop VMware 中国 - 交付面向企业的数字化基础 | CN...

如何修改Anaconda的Jupyter notebook的默认启动路径
1.打开Anaconda控制台 2.输入下面的命令 jupyter notebook --generate-config 这个命令的作用是生成 Jupyter notebook 的配置文件。如果你是第一次运行,会直接生成这个文件。如果曾经运行过这个命令,就会像下图一样问你时候要覆盖原来的文件。这个时候…...

密码学:带密钥的消息摘要算法一数字签名算法
文章目录 前言手写签名和数字签名前置知识点:消息摘要算法数字签名算法数字签名算法的由来数字签名算法在实际运用的过程附加:签名和摘要值的解释 数字签名算法的家谱数字签名算法的消息传递模型经典数字签名算法-RSA实现 数字签名标准算法-DSA实现 圆曲…...
JVM中部分主要垃圾回收器的特点、使用的算法以及适用场景
JVM中部分主要垃圾回收器的特点、使用的算法以及适用场景: Serial GC(串行收集器) 特点:单线程执行,对新生代进行垃圾回收时采用复制算法(Copying),在老年代可能使用标记-压缩或标记…...

vue保姆级教程----深入了解Vuex的工作原理
📢 鸿蒙专栏:想学鸿蒙的,冲 📢 C语言专栏:想学C语言的,冲 📢 VUE专栏:想学VUE的,冲这里 📢 CSS专栏:想学CSS的,冲这里 Ǵ…...
(JAVA)-(网络编程)-InetAddress(ip)
InetAddress类就表示ip地址,他是一个接口,有两个实现类:Inet4Address和Inet6Address,分别表示IPv4和IPv6. 创建对象:此类没有对外提供构造方法,创建ip对象要使用InetAddress类的静态方法getByName()。 st…...
手把手带你死磕ORBSLAM3源代码(二十二)Tracking.cc PrintTimeStats介绍
目录 一.前言 二.代码 2.1完整代码 一.前言 这段代码是一个C++函数,名为Tracking::PrintTimeStats(),它属于Tracking类。这个函数的主要目的是计算和打印出各种跟踪和本地映射任务所花费的平均时间和标准差,并将这些信息同时输出到控制台和一个名为ExecMean.txt的文本文件…...
【操作系统xv6】学习记录3-Wsl2 Ubuntu18.04图形化界面
不知道为啥,wls2和windows用vscode ssh的方式连接识别, 既然如此,那就装一个桌面版的,其实2年多前装过一次,后来pc机跑深度学习的任务,硬盘坏了~ 开干前再试一次 ref:https://zhuanlan.zhihu.…...

CCNP课程实验-03-Route_Path_Control_CFG
目录 实验条件网络拓朴需求 基础配置需求实现1.A---F所有区用Loopback模拟,地址格式为:XX.XX.XX.XX/32,其中X为路由器编号。根据拓扑宣告进对应协议。A1和A2区为特例,A1:55.55.55.0/24,A2:55.55…...

STM32 学习(二)GPIO
目录 一、GPIO 简介 1.1 GPIO 基本结构 1.2 GPIO 位结构 1.3 GPIO 工作模式 二、GPIO 输出 三、GPIO 输入 1.1 传感器模块 1.2 开关 一、GPIO 简介 GPIO(General Purpose Input Output)即通用输入输出口。 1.1 GPIO 基本结构 如下图࿰…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...
Python 高级应用10:在python 大型项目中 FastAPI 和 Django 的相互配合
无论是python,或者java 的大型项目中,都会涉及到 自身平台微服务之间的相互调用,以及和第三发平台的 接口对接,那在python 中是怎么实现的呢? 在 Python Web 开发中,FastAPI 和 Django 是两个重要但定位不…...
2025.6.9总结(利与弊)
凡事都有两面性。在大厂上班也不例外。今天找开发定位问题,从一个接口人不断溯源到另一个 接口人。有时候,不知道是谁的责任填。将工作内容分的很细,每个人负责其中的一小块。我清楚的意识到,自己就是个可以随时替换的螺丝钉&…...
SQL进阶之旅 Day 22:批处理与游标优化
【SQL进阶之旅 Day 22】批处理与游标优化 文章简述(300字左右) 在数据库开发中,面对大量数据的处理任务时,单条SQL语句往往无法满足性能需求。本篇文章聚焦“批处理与游标优化”,深入探讨如何通过批量操作和游标技术提…...