| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 嵌入式开发之C语言基础(五.指针详解) -> 正文阅读 |
|
[C++知识库]嵌入式开发之C语言基础(五.指针详解) |
? ? ? ? 这一章我们进入C语言的核心内容—指针,很早就听别人说没学指针就相当于没入门C语言,那么指针到底有什么值得我们为之探索的呢,我们接下来就来详细研究研究。
? ? ? ? 对于地址,我相信大家都不陌生了,那么存放变量的地址需要一种特殊类型的变量,这种特殊的数据类型就是指针,具有指针类型的变量,称为指针变量。一般我们可以认为指针就是地址,地址就是指针。定义形式:类型关键字 *指针变量名; ? ? ? ? 先来段代码来看看: 这段代码中我们先定义了一个整型变量a,再定义了一个整型指针变量p,接着把a的地址存到了p中,所以我们观察输出的结果,发现a = *p,&a = p,这就很好的解释了指针的定义。这里要注意的是:int *p;p = &a;可以写成int *p = &a,这里的*是标识符,表示p是一个指针,而我们使用指针时是用*p,这里的*是取值运算符,表示把地址中的数据取出来。不要被int *p = &a给误导了,误以为*p = &a; ? ? ? ? 这里我们还要关注的一点就是指针类型,我们直接在代码上体现一下: 可以看到我们定义一个整型指针变量和一个字符型指针变量,都来存整型变量a,从结果来看p和c存的地址都是a的地址,故*p和*c都可以取出a的值,那这不就和我们之前的定义有误吗,我们再仔细看看,编译器其实给我们报了警告,意思就是类型冲突,再往下看,我们让其分别做自加运算,可以看到p自加后地址增加4个字节,而c只增加1个字节,这是不是就取决于指针的类型了呢,所以我们还是要规范定义。 ? ? ? ? 我们再来个小题目来巩固一下前面的知识:给三个数进行排序: 大家应该能看懂这段代码,我不做过多解释了,为什么函数传参要用指针,是因为传普通的变量只是把它的值传进去了,在自定义函数中修改它的值回到主函数时值还是原来的,因为自定义函数与主函数中的变量的地址是不一样的,所以在主函数中不会改变,具体相关的内容可以看我之前的文章—嵌入式开发之C语言基础(四)?。
? ? ? ? 聊到数组指针,我们必须先了解指针和数组之间的关系。C语言中的数组名有特殊的含义,它代表存放数组元素的连续存储空间的首地址,意思就是数组名就是地址,我们可以定义一个指针变量指向数组名。我们也在代码上体现一下: 如上图所示,我们定义一个整型数组和整型指针变量,指针指向数组名即数组首地址,下面几个for循环通过不同的方法遍历数组各元素。第一种:就是通过普通的数组遍历,arr[ ],这个大家应该都明白;第二种:通过指针,p是地址,那么*p就可以取出该地址中的数据,故*(p+i)就可以遍历每一个元素,这里的i虽说是0、1、2这几个数字,但在指针偏移时代表的是i个4字节,因为这是一个整型数组,一个整型占4个字节;第三种:这种方法其实和第二种方法很像,唯一不同的就是遍历完之后必须要让p重新指向arr,因为好几个p++之后p的地址不再是首元素地址了,后续如果再需要用到p时,结果就会有问题;第四种:p指向arr即p=arr,所以我们可以和使用arr[ ]一样使用p[ ],故这样也能遍历数组。 ? ? ? ? ?大家再思考一下,既然p=arr,那么我们可不可以通过*(arr++)或者*(arr+i)的方式来遍历数组呢?我们来写段代码来看看: 可以看到*arr++这里报错了,但是*(arr+i)可以成功遍历(上图未显现),这里*arr++报错的原因是因为数组名是指向数组中第一个元素的指针常量,注意是指针常量,指针常量是不可以进行++、--之类的操作,因为arr++后会把arr++后的值赋值给arr,这样arr就不是指向数组的首地址了。 ? ? ? ? 接下来我们还得关注一下数组的大小,上午我们提到p=arr,那么sizeof(p)会不会等于sizeof(arr)呢?我们直接上代码: 很明显可以看到两者的大小是不一样的,arr的大小为12应该不足为奇,因为它是整型数组,里面有三个整型数,故size=3*4=12;那么指针p的大小为什么是8呢?我们再来写写代码:? ?大家应该可以发现,只要是指针,不管其是什么类型,所占内存大小就是8个字节。
? ? ? ? ?数组在函数中的传参大家应该都了解,我在之前的文章也大家聊过了,那如果是指针呢,那我们应该如何在函数中传参呢? 我们之前在主函数调用函数时,传参传的时数组名,用指针时就直接传p;在声明函数时,之前的形参是定义数组的形式即int arr[ ],用指针时同样也是定义指针的形式即int *p。传参其实也不是很难,在这段代码中,我还是想提一下指针使用的形式,上文我们也聊到过了,这里我希望大家关注一下scanf( )中的指针使用,就是要记住scanf( )中是需要地址的,指针parr就是地址,所以直接写parr,不需要*parr。
? ? ? ? ?要想对二维数组使用指针我们必须先了解二维数组的行地址和列地址。比如一个二维数组arr[3][4],我们把arr看成是由arr[0]、arr[1]和arr[2]三个元素组成的一维数组,arr是其数组名,故代表这arr[0]的地址,即arr=&arr[0],要注意的是这里arr[0]、arr[1]和arr[2]虽说是arr的元素,但事实上仍然是个地址。接着我们可以将arr[0]、arr[1]和arr[2]三个元素分别看成是由4个整型元素组成的一维数组,例如arr[0]由arr[0][0]、arr[0][1]、arr[0][2]和arr[0][3]4个整型元素组成,arr[0]是其数组名,故arr[0]代表arr[0][0]的地址即arr[0]=&arr[0][0]。文字看起来不是很好理解,我们在代码上体现: 可以看到arr、arr[0]和arr[0][0]的地址都是一样的,因为它们都是指向首元素的地址,不知道大家有没有和开始的我一样,对&arr[0]和&arr[0][0]这里有疑惑,前面不是说arr[0]是个数组名吗,arr[0][0]是arr[0]的首元素,所以arr[0]=&arr[0][0]是可以理解的,那为什么&arr[0]=&arr[0][0]呢,还是那句话,它们都指向首元素的地址,这里的首元素指的是arr[3][4]中arr[0][0],故地址都是一样的。? 大家可以发现我下面还写了一个*arr,它的值和前面的值一样,这应该很好理解,arr其实就是一个指针,*arr就是把arr地址中的数据取出来,arr存的是arr[0]的地址,*arr就是arr[0][0]的地址,故值是一样的。 ? ? ? ? 大概了解了二维数组的行、列地址之后,我们来对其进行地址偏移来加深理解: 首先,arr、arr[0]和*arr值都是一样的,因为arr和指向的是arr[0]的地址,arr[0]和*arr指向的是arr[0][0]?的地址,本质上都是arr[0][0]的地址;接着,我们对其分别进行偏移:arr+1表示地址由arr[0]偏移的arr[1],所以加了16个字节,arr[0]+1表示地址由arr[0][0]偏移到arr[0][1],所以加了4个字节,*arr+1也表示地址由arr[0][0]偏移到arr[0][1],所以加了4个字节。 至于为什么arr+1=*(arr+1),上文有讲到过,就不做过多赘述。 ? ? ? ? 为了进一步加深理解,我们把对应的数组元素值打印出来: 当打印整形元素时用到**的说明其是个行地址,只用到*的说明其是列地址,比如第一段:arr和arr+1,打印整形数时分别是*(*arr)和*(*arr+1)说明arr和arr+1是个行地址。? ? ? ? ? 有了前面的基础,我们来尝试一下遍历二维数组: 以上给出了三种方法来遍历数组,输出结果也验证了行地址和列地址偏移相关内容。 ? ? ? ? 现在我们来引入指针,仿照一维数组,我们也定义一个整型指针变量int *p=arr,在程序上看看会发生什么: 我们可以看到如果我们用定义一维数组指针的方法定义二维数组就失去二维数组的本质了,用起来就和一维数组一样,虽然打印的结果是对的,但是地址偏移并不是我们所想的会偏移行地址,而是直接偏移列地址。那么正确的二位数组指针应该怎么定义呢?我们再看一段代码: 我们用int (*p)[4]来定义二维数组指针,p指向的是行地址。这样不管是对其进行取值还是偏移,都满足二维数组的本质定义。那我们再来想一想,可不可以用int (*p)[ ]来定义呢?答案肯定是不行的,回到我们最开始讲二维数组的时候,列维度的元素个数是不能省略的,故不可以这样定义。
? ? ? ? ?如果是调用函数传的是普通二维数组我相信大家应该都没问题,我们来段代码简单回顾一下: 代码大家都能看明白,那么现在就用指针来尝试一下: 相信大家如果掌握了上文所说二维数组指针正确的定义方法,这段代码应该也不在话下。? ? ? ? ? ?在以上的基础上我们来到题目来做一做:使用二维数组指针的方式取出对应行和列的值: 这段代码中我们定义了一个指针p指向二维数组,本质上是指向一维数组的地址,所以我们可以看到在Get_Data()这个函数中p可以和line(行)直接相加,而和col(列)相加需要先*p。?这段代码大家应该也能看懂,我要提一嘴的是Tip_Scanf()这个函数,不知道大家有没有想过,可不可以就传变量的值即不传地址,然后再在这个函数中的scanf( )中&line,&col呢?答案是否定的,在我前面的文章讲自定义函数中讲过,传到函数里值尽管是一样的,但是是地址不同,所以两者并不能建立联系。
? ? ? ? ?相比之前我们所说的指针,函数指针大家可能接触的比较少,但是函数又是我们做嵌入式开发不可缺少的一部分,所以对于函数指针我们也必须了解并掌握。我们直接上代码: 可以看到,定义一个函数指针变量必须要有其返回值类型和形参类型;函数名和数组名一样表示的是地址,?所以我们用指针指向函数名(注意:仅仅只是函数名,不需要带括号);接着就是函数的调用,把指针当成函数名一样使用,这里要注意一点,上面的代码中我们可以用p1()调用函数,也可以用(*p1)()调用函数,是不是有点不好理解呢?这里我也不知道怎么解释,C语言中规定好像就是可以这样使用,不过对于我而言更喜欢p1()这样调用,因为p1就是指向函数名的,大家可以根据自己的喜好来选择。 ? ? ? ? ?那么到底要在什么情况下使用函数指针呢?我们也来段代码看看: 通过观察,如果几个函数的形参是相同的,那么我们就可以定义一个函数指针来指向这些函数。这段代码中我们用到了函数的嵌套,函数在函数的传参中和数组一样,传的是函数名,声明函数时函数的形参就是定义函数的形式。
? ? ? ? ?看到指针数组,大家是不是会想起数组指针呢,这两者又有什么区别呢?我们加一个的字,即指针的数组、数组的指针。我们前面提到的时数组指针,也就是数组的指针,重点是指针,指针指向数组;而我们现在要讲的是指针数组,重点是数组,意思就是数组的每个元素都是指针类型的。我们要在代码上体现一下: 大家仔细观察一下,我们定义指针数组时也有的讲究,*号是挨着int的,回顾之前数组指针的定义:int *p = arr;*号是挨着指针变量名的,前者表示的意义是arr[4]数组中每个元素都是int*型,再回忆一下定义一个普通一维数组:int arr[4];大家应该能体会到其中的奥妙之处。 ? ? ? ? 至于printf()中为什么要用*arr[i]或*(*(arr+i))应该不用过多解释吧,arr[i]是地址,*arr[i]便是取出地址中的数据,后者同样如此。? ? ? ? ? ?大家应该还记得我们讲函数指针时做过的一道题,求Max、Min和Sum的程序,我们运行一次完成一个运算,那我们如果像一次性把三个运算全执行一遍呢,这时我们就用到指针数组: 这里我们定义的是一个函数指针数组,不知道大家能否看明白。首先,我们定义一个函数指针,因为这三个函数的形参都是一样的,即int (*p)(int,int);接着我们需要使这个函数指针指向这个三个函数,故引用指针数组存放这三个函数名即地址,int (*p[3])(int,int) = {Get_max......}。这里要注意一下,前面不是说*号要挨着int吗,这路是因为[ ]的优先级大于*,所以p先和[ ]结合了,和上文int* arr[4]是一样的意思。解决了这个问题,大家看懂代码应该没什么问题了吧。
? ? ? ? ?指针函数顾名思义就是指针的函数,也就是返回值为指针的函数,其定义形式大家猜一下应该也能猜出来,我们直接上代码: 稍微解释一下这段代码:scores[3][4]数组的行维度表示学生的个数,列维度表示该学生对应的四科分数,程序要实现的功能就是我们在外面输入某个学生的位置,输出结果得到该学生四科分数。 ? ? ? ? 这里对于二维数组的函数传参我们就不在讨论了,上文也详细讲了。重点来关注指针函数相关内容。前面也说到,既然是指针的函数,所以其函数原型的返回值就是int*,其他的和普通函数一样。通过Get_score()这个函数返回行地址?,得到了行地址那我们不就可以遍历对于行各个元素了吗,程序看懂应该没什么问题。这里要关注的是Get_score()这个函数中p=(int *)(stu+pos)这行代码,stu+pos应该没什么问题,大家可能会对前面强制转换(int *)有点疑惑,(stu+pos)不就是指针吗,为什么还要强转呢?这是因为(stu+pos)是指向行地址的,其地址加1偏移16个字节,而我们定义的int *p是整型指针,地址加1偏移4个字节,这里就和我们最开始举的一个例子相同,即用int*和char*同时指向一个变量,地址是相同的,但是让其进行加1偏移的地址是不一样的?。
? ? ? ? ?二级指针的作用就是指向一级指针,比如我们有一个变量a,一级指针p1=&a,那么二级指针就是p2=&p1,那在代码上具体怎么操作呢,我们看看代码: 我们定义了“二级指针”int *p2=&p1,从结果上来看并没用问题,*p2 = p1即“二级指针”p2确实指向p1的地址,那么我们是否可以通过**p2来获得data的值呢?我们把最后那个printf()的注释去掉再编译一下:? 可以看到已经报错了,报错的原因是p2其实还是一个一级指针,自然不能**p2,其实第一次编译的时候就报了警告,说的是类型冲突,因为p2指向&p1就表示p2是一个二级指针,但是我们是用的一级指针来定义p2,这也是为什么上文的二级指针我用了双引号。那正确的方法是什么呢: 正确定义二级指针的方式是int **p3,可以看到警告错误啥的都没有了,data的值我们也能成功取出来。那这个二级指针要怎么用呢,我们再来用讲解指针函数用到的例子-获取学生成绩:? 这段代码和我们之前的代码相比我们稍做了修改,之前代码中Get_score()函数可以返回一个整型指针变量,现在代码我们不用返回值,而且现在的Get_score()函数的形参中多了一个整型指针变量。 ? ? ? ? 根据输出结果,我们发现在Get_score()中打印能得到我们正确的结果,而在main()中打印的是乱码,说明ppos的地址并未传过去,但是我们不是定义int *ppos吗,传ppos不就是地址吗,那为什么在main()中仍然得不到我们想要的值呢?因为我们要做修改的就是ppos本身,所以要想修改它必须传ppos的地址,即&ppos,故需要用int **ppos来承接。 经过修改,现在这个程序就能实现在main()中打印我们想要的数据。? 这里总结了一下常见的指针的定义:
?大家要是能把这里理解好指针也就掌握了不少,今后再多练练,指针应该也不在话下。 ? ? ? ? 文章到此就结束了,内容比较多,希望大家耐着性子认真读完,相信大家一定会有收获!如果内容有讲得不对的地方欢迎各位大佬批评指正!!指针内容不完全在这,例如还有字符串指针等,我们将在下一篇文章提到。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 9:56:12- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |