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

2024-06-15 15:28

本文主要是介绍《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语言深度解剖》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我已经由浅入深地讲解了指针。无论您是在阅读本文之前还是之后,都可以查阅以下文章以获得更全面的理解:

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

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

函数指针、函数指针数组、指向函数指针数组的指针、回调函数https://blog.csdn.net/weixin_43382136/article/details/138322410?spm=1001.2014.3001.5501icon-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语言深度解剖》https://blog.csdn.net/weixin_43382136/category_12344236.htmlicon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/category_12344236.html
《精通C指针》https://blog.csdn.net/weixin_43382136/category_12659166.htmlicon-default.png?t=N7T8https://blog.csdn.net/weixin_43382136/category_12659166.html

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



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

相关文章

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

韦季李输入法_输入法和鼠标的深度融合

在数字化输入的新纪元,传统键盘输入方式正悄然进化。以往,面对实体键盘,我们常需目光游离于屏幕与键盘之间,以确认指尖下的精准位置。而屏幕键盘虽直观可见,却常因占据屏幕空间,迫使我们在操作与视野间做出妥协,频繁调整布局以兼顾输入与界面浏览。 幸而,韦季李输入法的横空出世,彻底颠覆了这一现状。它不仅对输入界面进行了革命性的重构,更巧妙地将鼠标这一传统外设融入其中,开创了一种前所未有的交互体验。 想象

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念