零基础学 C 语言,深入理解指针 (1) 内存、地址与指针变量
[TOC]
零基础学C语言,深入理解指针 (1)
一、 内存和地址
1.1 内存和地址
在介绍之前,我们可以想一个现实生活中的例子,假设有一个学生宿舍,那就会有很多的房间,我们应该如何高效管理这些房间呢?我们可以用房间号,比如101,522 等等。
内存也是一样的,如果把内存想象成一个空间,为了高效管理这些空间,我们把这一个大的空间分成了一个个的部分,我们给这些小的内存空间取名叫内存单元,而内存单元的“房间号”—内存编号,就是地址。而每一个内存单元的大小规定是一个字节。
在C语言中,我们给地址取了一个名字———指针。
所以,内存编号==地址==指针。
1.2 如何理解编址
电脑的中央处理器(CPU),在进行计算的时候,需要的数据是从内存中读取的,计算的结果也是要储存到内存里去的。
CPU在访问某个内存单元的时候,必须知道这个字节在内存的哪个位置,而内存中的内存单元非常多,如何高效准确的找到相应的内存单元?这时,我们就需要给内存进行编址。(就像宿舍很多,给宿舍编号一样)
首先,我们要理解,计算机里有很多的硬件单元,而这些硬件单元是要协同工作的。
从物理上来看,这些硬件单元是孤立的,那么它们怎么互相协同工作呢?答案是用**“线”**链接起来。
CPU和内存之间至少有三组线,分别是控制总线,数据总线,地址总线。
控制总线用来传输命令,比如CPU要访问内存中的一个字节吗,这个命令就是由控制总线传输。
数据总线,顾名思义就是用来传输数据,比如CPU计算了一个1+1=2,现在要把这个2存入内存,就是通过数据总线,当然,数据由内存传输到CPU也是通过数据总线。
而地址总线,就是用来产生地址的,比如说控制总线传输过来一个命令:CPU要访问一个字节,那么这个字节的地址就由地址总线(32个一起)产生,CPU可以快速找到这个地址,从而访问这个字节。
这里需要特别注意,计算机的编址是在硬件层面完成的,并不是由地址总线产生记录的,也就是说,地址不在内存中储存。
这里可以类比钢琴,黑白块上并没有写哪一个是 do re me fa……,但我们还是能知道那个是 do ,哪个是 re。这是一个共识。同样的,计算机编址也是这样的,do re me等等就是地址,而黑白块就是内存单元。
地址总线相当于产生一个虚拟地址,通过这个虚拟地址来找相同的地址。
在32位环境中,地址总线一共有三十二根,每一根地址总线都可以通过高低电平来表示 0 和 1 。那么三十二根地址总线就会有 2 的 32 次方个组合方式,每一种组合都代表一个地址。
同样的,三十二根地址总线就会产生32个 0 或 1,一个 0 或 1 在内存中是一个比特位,三十二个就是三十二个比特位,8个比特位是一个字节,所以一个地址的大小就是4个字节。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
二、指针变量和地址
2.1 取地址(&)操作符
了解了内存和地址的关系后,回到C语言。指针变量顾名思义就是储存指针的变量,而指针就是地址。而取地址操作符(&),就是取出元素的地址。
在C语言中,创建变量就是向内存申请空间,可以看到上图中int a 在内存中的地址(int 类型占四个字节)。那么我们是如何获得a的地址的呢? &a 就可以了。
如下图:
有敏锐的读者发现这里&a ,取出的是a 第一个字节的地址(较小的字节的地址)。因为整数在内存中占四个字节,所以只需要取出第一个字节的地址,就可以访问这个变量。
2.2 指针变量和 *
2.2.1 指针变量
刚刚我们了解了&操作符,可以取出变量的地址。如果一个变量用来储存这个地址,那么这个变量就是指针变量。
1 | #linclude <stdio.h> |
指针变量和其他类型的变量一样,有类型,上面那段代码里面指针变量pa的类型就是int *。以此类推,指针变量储存的是什么类型变量的地址,指针变量的类型就是(…*),
比如存放整形地址的就是 (int *),存放字符地址的就是(char *)。
这里的 * 表示的是这是一个指针变量。也就是指针变量的标志。
2.2.2 解引用操作符 *
解引用操作符也是*。如果对一个指针变量解引用,那么就可以访问这个指针变量储存的地址,从而访问是这个地址的变量,如下图:
也就是说,& 和 * 是一个对相反的操作,一个是取出变量的地址,一个是通过地址访问变量。 如果 * & a 的话,就无事发生。
有的人可能会想,既然 *pa 就是 a,那么为什么还要这么麻烦用指针呢?其实这里是多了一种表达的方法,可以使代码更加的灵活。
2.3 指针变量的大小
上文我们了解到,假设在32位环境下,一共有三十二个地址线,那么产生的地址(指针)也就是32个比特位,也就是 4 个字节。所以指针变量的大小就是 4 个字节。
如果在64位环境下,有64根地址总线,指针变量的大小就是 8 个字节。
下图是 x86 (32位) 环境下指针变量的大小:
所以,指针变量的大小和指针变量的类型是无关的,是和环境有关的。
3 指针变量类型的意义
既然指针变量在相同环境下大小都一样,那么指针变量类型的意义是什么呢?
3.1解引用操作:
我们通过调试来观察
如下图:
调试到这一步时,我们创建了一个 int a , 把 a 的地址放到了 int * 类型的指针变量pa,可以观察到右侧的内存中 a 的地址和数值。
我们继续往下走:
到这一步,我们对 pa 解引用,然后访问了变量 a ,把 a 的值改成了0,可以看到右侧 a 的内存修改了四个字节(一个整形的大小),都改成了零。
我们再来看,和上面对比:
这里我们把整形 a 的地址放到了 char *指针里面,我们继续看:
执行把 a 变成零的时候,可以看到,内存里只有一个字节(一个字节的大小)被改成了零。
总结一下,指针变量的类型的类型的一个作用就是指针变量解引用时能访问的字节数量。比如说,整形的大小是四个字节 ,所以整形指针解引用时就可以访问四个字节的内存。字符型的大小是一个字节,所以字符指针解引用时就只能访问一个字节。
3.2 指针加减整数
我们还是用代码来解释:
看下图的代码:
可以看到整形指针pa1加一,跳过了四个地址,也就是跳过了四个字节(一个整形),而字符型指针pa2加一,跳过了一个字节(一个字符型)。
所以,指针类型的第二个区别就是:一个整形指针加一跳过一个整形,一个字符型指针加一跳过一个字符型。
3.3 void * 指针
指针变量类型里面有一个特别的类型,就是 void * 类型。 void *类型的指针变量可以接受任何类型变量的地址。
如下图:
如果用一个字符型的指针变量接受整形的地址,那么编译器就会报警告,而如果我们用 void * 来接收整形的地址,就不会有错误。
但是,void * 类型的指针不可以解引用,如果对 void * 的指针进行解引用,就会报错,如下:
那么 void * 指针有什么作用呢?在接下来的文章中,小编会进行解释(有没有下一期都不一定 2333)
4 const 修饰指针变量
有过一定的c语言基础的话就会知道,const 可以修饰变量 让变量具有常量的性质—-不可以修改,我们成为常变量。const 修饰指针呢?
const修饰指针的时候有两种情况。我们知道指针变量定义的时候,是 void * p = &a;
第一种:const 可以放在 * 的右边,即: void * const p = &a;
第二种:const 也可以放在 * 的左边,即: const void * p = &a;
那么这两种方式有什么区别呢?
我们先来看第一种:
我们可以看到,当我们想把 b 的地址放到 pa 里面时,编译器报错了。因为这里的 pa 被 const 修饰,有了常量的性质,不可以再修改。
而如果我们对 pa 解引用,然后修改 * pa 的数值时是正常的,如图:
我们再来看第二种:
我们可以看到,当我们 修改 * pa 的值的时候,编译器报错了。因为这里* pa 具有了常量的性质,不可以进行修改。
而我们把 b 的地址放到 pa 里面时,编译器没有报错,是正常的,如图:
总结一下 :
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
const 如果放在*的右边,修饰的是指针的指向,也就是指着变量本身,保证指针的指向不能改变。但是指针变量指向的内容可变。
本篇完。
























