如果要选出C语言中最重要、难度大的概念是什么,那就是指针!难度大,意味着使用方便、实用高效,同时也意味这个知识点复杂、实用的时候容易出错。指针用的好,可以提高代码执行效率、节约系统资源;如果用的不好,程序中就会出现一定的问题。
今天我们就来聊一下指针。从最底层的内存存储空间开始,一直到应用层的各种指针使用技巧,循序渐进,以最简洁的语言进行讲解,希望能对你有一点点帮助就够了。
说明:素材难找,为了偷懒和方便讲解和理解,文中配图的内存空间的地址是随便写的,在实际计算机中是要遵循地址对齐方式的。
变量与指针的本质
内存地址
我们编写一个程序源文件之后,编译得到的二进制可执行文件存放在电脑的硬盘上,此时它是一个静态的文件,一般称之为程序。
当这个程序被启动的时候,操作系统将会做下面几件事情:
·把程序的内容(代码段、数据段)从硬盘复制到内存中;
·创建一个数据结构 PCB(进程控制块),来描述这个程序的各种信息
·在代码段中定位到入口函数的地址,让 CPU从这个地址开始执行。
当程序开始被执行时,就变成一个动态的状态,一般称之为进程。
内存分为:物理内存和虚拟内存。操作系统对物理内存进行管理、包装,我们开发者面对的是操作系统提供的虚拟内存。
这 2个概念不妨碍文章的理解,因此就统一称之为内存。
在我们的程序中,通过一个变量名来定义变量、使用变量。
变量本身是一个确确实实存在的东西,变量名是一个抽象的概念,用来代表这个变量。
那么,我们定义一个变量之后,这个变量放在哪里呢?那就是内存的数据区。
内存是一个很大的存储区域,被操作系统划分为一个一个的小空间,操作系统通过地址来管理内存。
内存中的最小存储单位是字节(8个 bit),一个内存的完整空间就是由这一个一个的字节连续组成的。
32位与 64位系统
我们平时所说的计算机是 32位、64位,指的是计算机的 CPU中寄存器的最大存储长度,如果寄存器中最大存储 32bit的数据,我们就叫作32位系统。
在计算机中,数据一般都是在硬盘、内存和寄存器之间进行来回存取。CPU通过 3种总线把各组成部分联系在一起:地址总线、数据总线和控制总线。地址总线的宽度决定了 CPU的寻址能力,也就是 CPU能达到的最大地址范围。
刚才说了,内存是通过地址来管理的,那么 CPU想从内存中的某个地址空间上存取一个数据,那么 CPU就需要在地址总线上输出这个存储单元的地址。
假如地址总线的宽度是 8位,能表示的最大地址空间就是 256个字节,能找到内存中最大的存储单元是 255这个格子。即使内存条的实际空间是 2G字节,CPU也没法使用后面的内存地址空间。如果地址总线的宽度是 32位,那么能表示的最大地址就是 2的 32次方,也就是 4G字节的空间。
【注意】这里只是描述地址总线的概念,实际的计算机中地址计算方式要复杂的多,比如:虚拟内存中采用分段、分页、偏移量来定位实际的物理内存,在分页中还有大页、小页之分。
变量
我们在 C程序中使用变量来“代替”一个数据,使用函数名来“代替”一个函数,变量名和函数名是程序员使用的助记符。变量和函数最终是要放到内存中才能被 CPU使用的,而内存中所有的信息(代码和数据)都是以二进制的形式来存储的,计算机根据就不会从格式上来区分哪些是代码、哪些是数据。CPU在访问内存实际上是访问地址,而不是访问变量名、函数名。
在程序代码中使用变量名来指代变量,而变量在内存中是根据地址来存放的,这二者之间是通过编译器映射(关联)起来的。
变量有 2个重要属性:变量的类型和变量的值。
示例:代码中定义了一个变量
int a = 20;
类型是 int型,值是 20。这个变量在内存中的存储模型为:
我们在代码中使用变量名a,在程序执行的时候就表示使用 0x11223344地址所对应的那个存储单元中的数据。
因此,可以理解为变量名 a就等价于这个地址 0x11223344。换句话说,如果我们可以提前知道编译器把变量 a安排在地址 0x11223344这个单元格中,我们就可以在程序中直接用这个地址值来操作这个变量。
在上图中,变量 a的值为 20,在内存中占据了 4个格子的空间,也就是 4个字节。为什么是 4个字节呢?在 C标准中并没有规定每种数据类型的变量一定要占用几个字节,这是与具体的机器、编译器有关。
比如:32位的编译器中:
char: 1个字节;
short int: 2个字节;
int: 4个字节;
long: 4个字节。
比如:64位的编译器中:
char: 1个字节;
short int: 2个字节;
int: 4个字节;
long: 8个字节。
为了方便描述,下面都以 32位为例,也就是 int型变量在内存中占据 4个字节。
另外,0x11223344,0x11223345,0x11223346,0x11223347这连续的、从低地址到高地址的 4个字节用来存储变量 a的数值 20。
在图示中,使用十六进制来表示,十进制数值 20转成 16进制就是:0x00000014,所以从开始地址依次存放 0x00、0x00、0x00、0x14这 4个字节(存储顺序涉及到大小端的问题,不影响文本理解)。
根据这个图示,如果在程序中想知道变量 a存储在内存中的什么位置,可以使用取地址操作符&,如下:
printf("&a = 0x%x \n", &a);
这句话将会打印出:&a = 0x11223344。
考虑一下,在 32位系统中:指针变量占用几个字节?
指针变量
指针变量可以分 2个层次来理解:
·指针变量首先是一个变量,所以它拥有变量的所有属性:类型和值。它的类型就是指针,它的值是其他变量的地址。既然是一个变量,那么在内存中就需要为这个变量分配一个存储空间。在这个存储空间中,存放着其他变量的地址。
·指针变量所指向的数据类型,这是在定义指针变量的时候就确定的。例如:int *p;意味着指针指向的是一个 int型的数据。
在 32位系统中,一个指针变量在内存中占据 4个字节的空间。因为 CPU对内存空间寻址时,使用的是 32位地址空间( 4个字节),也就是用 4个字节就能存储一个内存单元的地址。而指针变量中的值存储的就是地址,所以需要 4个字节的空间来存储一个指针变量的值。
示例:
int a = 20;
int *pa;
pa = &a;
printf("value = %d \n", *pa);
在内存中的存储模型如下:
4.在返回到STM32CubeMX后,在Pinout&Configuration里面的pinout view,点击选择PA8 PIN脚,在弹出的小框里面选择RCC_MCO,使得PA8作为MCO输出PIN脚。同样地方式,设置PA1为GPIO Output。
5.本期先分享到这里,想要进群学习单片机编程的同学可以私信我,回复“我要入门”,与我们一起成长,喜欢的可以点个赞关注我们!