《C语言深度解剖》(19):全面剖析理解C语言指针和数组

2024-06-13 16:36

本文主要是介绍《C语言深度解剖》(19):全面剖析理解C语言指针和数组,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》《精通C指针》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨ 


目录

前情提示:

1. 指针是什么 

1.1 指针的内存布局 

1.2 指针解引用 

2. 指针和数组 

2.1 数组的内存布局  

2.2 理解 &a[0] 和 &a 的区别

2.3 指针和数组的恩怨 

2.4 以指针的形式访问和以下标的形式访问 

2.5 a 和&a的区别

3. 指针数组和数组指针

3.1 指针数组和数组指针的内存布局 

3.2 int(*)[10] p2 也许应该这么定义数组指针 

4. 多维数组和多级指针

4.1 二维数组 

4.1.1 基本概念 

4.1.2 基本内存布局 

 4.1.3 空间布局

5. 数组参数和指针参数 

5.1 一维数组传参 

5.2 一级指针传参 

5.3 二维数组参数和二级指针参数 

6. 函数指针 

7. 函数指针数组

8. 函数指针数组指针

9. 强烈推荐两个专栏


前情提示:

在之前的两个专栏中——《C语言深度解剖》,《精通C指针》我已经由浅入深地讲解了指针。无论您是在阅读本文之前还是之后,都可以查阅以下文章以获得更全面的理解:

指针就是这么简单icon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/article/details/137881292?spm=1001.2014.3001.5501

数组指针、指针数组和数组指针数组icon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/article/details/138248557?spm=1001.2014.3001.5501

函数指针、函数指针数组、指向函数指针数组的指针、回调函数icon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/article/details/138322410?spm=1001.2014.3001.5501本文将从宏观角度审视指针的难点易错点,若您已认真学习过上述内容,阅读本文将更为轻松。


1. 指针是什么 

在回答这个问题之前,我想先问几个问题? 

1. 如何看待下面代码中的a变量? 

结论:

同样一个a变量,在不同的应用场景中,a本身的含义是不同的。 

本质区别就是 左值和右值的区别。

变量的空间:左值; 变量的内容:右值;

重新理解变量:

定义一个变量,本质是在内存中根据类型来进行开辟空间。有了空间,就必须具有地址来标识空间,来方便CPU进行寻址。有了空间,就可以把数据保存起来。

2. 什么是指针? 

指针就是地址!那么地址本质是什么呢?地址是数据,那么数据可不可以被保存在变量空间里面呢?当然可以。

3. 有没有指针变量这个概念? 

保存指针(地址)数据的变量就叫做指针变量 

4. 指针 和 指针变量又有何不同?我们口语中的"定义一个指针"究竟是什么意思?我们该如何理解这种说法? 

  • 严格意义上,指针和指针变量是不同的,指针就是地址值,而指针变量是一个变量,要在特定区域开辟空间,要用来保存地址数据,还可以被取地址。(先分开)
  • 但是,我们经常在口语化表达的时候,又经常将这两个概念混合,具体原因无从考证,不过个人认为与最早的C语言资料(书, 文档之类)的翻译有关。然后,书与书之间互相借鉴,形成了这样的说法。
  • 同时,简化说法,也更符合人的表达习惯,估计老外也是这么想的。(在关联)
  • 那么我们以后怎么认为呢?我们分开理解,但是依旧关联使用。自己使用的时候,混合使用可以。和别人讨论,最好明确概念。 

结论:指针就是地址,指针变量是一个变量,变量内部保存指针(地址)数据。 

为什么要有指针?

回答一个问题:为何每间宿舍都要有门牌号呢?
结论:提高查找效率。 

类比到计算机中:

CPU在内存中寻址的基本单位是多大?

在32位机器下,最多能够识别多大的物理内存?

既然CPU寻址按照字节寻址,但是内存又很大,所以,内存可以看做众多字节的集合。

其中,每个内存字节空间,相当于一个学生宿舍,字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位。

每间宿舍都有门牌号就等价于每个字节空间对应的地址,即该空间对应的指针。

那么,为何要存在指针呢?为了CPU寻址的效率。如果没有,该怎么找在字节空间中的数据呢?

究竟该如何理解编址? 

1.1 指针的内存布局 

1.2 指针解引用 

*p完整理解是,取出p中的地址,访问该地址指向的内存单元(空间或者内容)(其实通过指针变量访问,本质是一种间接寻址的方式) 

口诀:对指针解引用,就是指针指向的目标。所以*p,就是a


2. 指针和数组 

2.1 数组的内存布局  

概念:数组是具有相同数据类型的集合。 

先看下面的例子:

int a[5];

所有人都明白这里定义了一个数组,其包含了5个int型的数据。我们可以用a[0]、a[1]等来访问数组里面的每一个元素,那么这些元素的名字就是a[0]、a[1]....吗?看看下图内容:

如上图所示,当我们定义一个数组a时,编译器根据指定的元素个数和元素类型分配确定大小(元素类型大小×元素个数)的一块内存,并把这块内存的名字命名为a。

先看下面一段代码:

我们发现,先定义的变量,地址是比较大的,后续依次减小。

这是为什么呢?

a,b,c都在main函数中定义,也就是在栈上开辟的临时变量。而a先定义意味着,a先开辟空间,那么a就先入栈,所以a的地址最高,其他类似。

运行结果:

  

2.2 理解 &a[0] 和 &a 的区别

看下面的例子: 

口诀:对指针+1,本质是加上其所指向类型的大小。 

2.3 指针和数组的恩怨 

        很多初学者弄不清指针和数组到底有什么样的关系,我现在就告诉你:它们之间没有任何关系,只是它们经常穿着相似的衣服来逗你玩罢了。
        指针就是指针,指针变量在 32 位系统下,永远占 4 字节,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到呢? 

        数组就是数组,其大小与元素的类型和个数有关;定义数组时必须指定其元素的类型和个数;数组可以存任何类型的数据,但不能存函数。
        既然它们之间没有任何关系,那为何很多人经常把数组和指针混淆,甚至很多人认为指针和数组是一样的呢?这就与市面上C语言的书有关了,很少有书把这个问题讲得透彻,讲得明白。

2.4 以指针的形式访问和以下标的形式访问 

可以理解成:

[] 是对 *() 的缩写

结论:指针和数组指向或者表示一块空间的时候,访问方式是可以互通的,具有相似性。但是具有相似性,不代表是一个东西或者具有相关性。

2.5 a 和&a的区别

结论: &a叫做数组的地址,a做右值叫做数组首元素的地址,本质是类型不同,进而进行+-计算步长不同 


3. 指针数组和数组指针

3.1 指针数组和数组指针的内存布局 

        初学者总是分不出指针数组和数组指针的区别,其实这很好理解。
        指针数组首先它是一个数组,数组的元素都是指针,数组占多少字节由数组本身决定。它是“储存指针的数组”的简称
        数组指针:首先它是一个指针,它指向一个数组。在32位系统下永远是占4字节,至于它指向的数组占多少字节并不知道。它是“指向数组的指针”的简称
        下面到底哪个是数组指针,哪个是指针数组呢?

(A)int *p1[10];
(B) int(*p2)[10];

这里需要明白一个符号之间的优先级问题。“ [ ] ”的优先级比“ * ”要高,p1 先与“ [ ] "结合,构成一个数组的定义,数组名为 p1,int * 修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个指向 int 类型数据的指针,即指针数组。至于 p2 就更好理解了,这里“() ” 的优先级比“ [ ] ”高,“ * ”号和 p2 构成一个指针的定义,指针变量名为 p2, int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指针,它指向一个包含10个int类型数据的数组,即数组指针。 

我们看下图进行理解

3.2 int(*)[10] p2 也许应该这么定义数组指针 

很多小伙伴学不会数组指针或者指针数组等比较复杂的指针有时候并不是怪自己!而是C语言在复杂类型的设计上确实不太 “优雅”!

这里有个有意思的话题值得探讨一下:平时我们定义指针都是在数据类型后面加上指针变量名,这个数组指针 p2 的定义怎么不是按照这个语法来定义的呢?也许我们应该这样来定义p2: 

int(*)[10] p2;

int(*)[10] 确实是指针类型,p2是指针变量。其实数组指针的原型确实就是这样子的,只不过C语言的设计风格,把指针变量p2前移了,与“ * ” 号紧挨着在一个小括号里。你私下完全可以这么理解,有助于判断复杂数据类型的学习,只不过编译器语法不通过罢了。 


4. 多维数组和多级指针

4.1 二维数组 

4.1.1 基本概念 

几乎大部分书中所画的二维数组,都是矩阵样子,具体可以参考书中的图,但是,现在我们要在这里澄清,书中的图,最多只能称之为示意图,并非真的内存布局图。
可以想象一些问题:如果按照书中矩阵样子画二维数组的话,那么三维数组,四维数组又该如何画呢?

4.1.2 基本内存布局 

运行结果:
  

结论:二维数组在内存地址空间排布上,也是线性连续且递增的 

 4.1.3 空间布局

以它为例:char a[3][4] = { 0 };


5. 数组参数和指针参数 

5.1 一维数组传参 

C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素类型的指针。 

所以说一维数组当做函数参数的时候,数组元素的个数可以忽略不写,因为降维成了指向其首元素类型的指针了,已经指向了这块数组空间了,可以进行访问了,传参时的个数已经不重要了。

5.2 一级指针传参 

1. 能否把指针变量本身传递给一个函数

因为指针变量,也是变量,在传参上,它也必须符合变量的要求,进行临时拷贝! 

2.无法把指针变量本身传递给一个函数 

这很像孙悟空拔下一根猴毛变成自己的样子去忽悠小妖怪,与其类似,fun函数实际运行时,用到的都是_p2这个变量而非p2本身。如此,我们看下面的例子: 

5.3 二维数组参数和二级指针参数 

前面详细分析了二维数组和二级指针,那它们作为参数时与不作为参数时又有什么区别呢?看例子:

void fun(char a[3][4]); 

我们按照上面的分析,完全可以把 a[3][4] 理解为一个一维数组 a[3],其每个元素都是一个含有4个char类型数据的数组。上面的规则:“C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素类型的指针。”在这里同样适用,也就是说我们可以把这个函数声明改写为:

结论:

任何维度的数组,传参的时候,都要发生降维,降维成指向其首元素类型的指针

那么,二维数组,内部“元素”是一维数组!那么降维成指向一维数组的指针 


6. 函数指针 

顾名思义,函数指针就是函数的指针。它是一个指针,指向一个函数。看例子: 

1. (*(void (*) ()) 0) ()-这是什么

7. 函数指针数组

8. 函数指针数组指针

注意,这里的 pf 和 第 7 章节的 pf 就完全是两码事了。4第 7 章节的 pf 并非指针,而是一个数组名;这里的 pf 确实是实实在在的指针。这个指针指向 :
        一个包含了3个元素的数组;这个数组里面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针,参数为一个指向字符的指针的函数。这比第 7 章节的函数指针数组更拗口。其实你不用管这么多,明白这是一个指针就ok了,其用法与前面讲的数组指针没有差别。

9. 强烈推荐两个专栏

《C语言深度解剖》icon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/category_12344236.html
《精通C指针》icon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/category_12659166.html

这篇关于《C语言深度解剖》(19):全面剖析理解C语言指针和数组的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1057842

相关文章

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法

《JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法》:本文主要介绍JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法,每种方法结合实例代码给大家介绍的非常... 目录引言:为什么"相等"判断如此重要?方法1:使用some()+includes()(适合小数组)方法2

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?