1. 存储空间、符号、地址
1.1 存储空间
1.1.1 什么是存储空间
就是程序代码和数据的存放空间,笼统可以分为如下两种情况。
没有运行时:
存储在硬盘(外存)上,所以此时的存储空间为硬盘。
运行时:
代码和数据存放在内存上,供cpu访问。
当然程序在运行时,还需要用到寄存器和cache,寄存器和cache同样也是存储器,它是用于缓存内存中的代码和数据,所有这些存储器(硬盘、内存、寄存器、cache)都是由单个字节堆叠而成,存储空间中的每个字节都有自己的地址,如果没有地址的话,是无法访问它们的,每个字节的地址就好比是每个字节的门牌号。
一般来说,当我们说存储器时,主要指的是内存。
1.1.2 程序运行时,cpu是如何访问内存中的代码和数据的
存储器中的每个字节都有地址,这个地址就是每个字节的门牌号,通过地址就能找到每一个字节并访问它,
访问权限有两种:
· 可读可写
程序的“.data、.bss、堆、栈空间”就是可读可写的。
变量空间要求是可读可写的,所以变量空间就开辟于这些中。
· 只读
.rodata、.text为只读的。
代码和常量只能被读,所以代码和常量就放在这两个中。
疑问:怎么没有“不可读不可写”和“只写”的权限呢?
答:两种权限没有意义
· 不可读不可写:这个存储空间没有任何作用
· 只写:写入数据的目的是为了读出,只写入不读出,这种权限的存储空间也没有任何意义
1.1.3 地址 与 指针
(1)地址是个啥
地址就是一个数,这个数可以唯一标记每个字节的存在,就好比人的身份证一样,也是一个数,可以唯一的标记每个人的存在。
举个例子,如果内存大小为4G,那么内存的地址范围就是
0x00000000 ~ 0xFFFFFFFF:十六进制表示时的范围
0 ~ 4G-1 :十进制表示时的范围
0x00000000 ~ 0xFFFFFFFF这个地址,就是一些用来唯一标记字节空间的数。
(2)指针就是地址的另一个名字
为什么地址也称为“指针”呢?
通过某地址能够唯一的访问某个字节,所以地址唯一的指向了某字节,这就像“指针”一样具有指向作用,因此才被形象的称为了“指针”。
这就好比我知道你们家的地址,然后就能找到你们家,此时就说“你们家的地址”是指向你们家的指针。
所以记住了,指针就是地址,地址就是指针。
(3)指针与指针变量
指针:地址。
指针变量:存放地址(指针)的变量
int a; int *p = &a;
&a:a的指针
a有四个字节,每个字节都有地址,但是只有第一个字节的地址才是a的指针,为什么是这样的,后面在解释。
p:放地址(指针)的指针变量
这里要注意一点,指针变量里面放了指针后,我们就说p指向a,但是我们自己应该清楚,具有指向作用的是p里面的指针(地址),而不是指针变量,指针变量只是放指针的篮子。
指针变量里面的指针发生变化后,这个指向就发生变化了,所以说对于指针变量来说,具有指向作用的是里面放的指针,不是指针变量本身。
不过以后为了称呼的方便,我们往往会将“指针变量”也简称为指针,所有平时称呼“指针”的时候,有可能指的地址,也可能指的是指针变量,就看说话的语境,不过在本章里面,为了表达更清晰,我会按照准确的名字来称呼。
1.1.4 符号与地址
(1)程序运行时,访问存储空间是核心动作
1)CPU执行的代码从哪里来
要访问.text所对应存储空间。
2)CPU按照代码要求去加、减、乘、除、与、或、非运算数据时,数据从哪来
(a)变量数据:需要访问.data、.bss、堆、栈空间。
(b)常量数据:需要访问.text、.rodata的空间。
3)计算后的结果
得到这结果后,这个结果数据不管是拿去给USB输出,还是给LCD显示,还是给扬声器播放,还是控制机械手臂,都要先在.data、.bss、堆、栈中开辟变量空间,暂存这个结果数据,然后再将结果数据输出外设的寄存器、显存、声卡等,就可以控制USB、LCD、扬声器、机械手臂等外设工作了。
其实寄存器、显存、声卡等同样也是存储器,也是通过地址去操作的,只不过这些事情往往都是由驱动程序去做的。
对于应用程序俩说,主要访问的存储器是内存,而不是寄存器、显存、声卡这些玩意,这些是由驱动程序来访问的。
(2)符号 与 地址
前面说过,都是通过地址来访问存储空间的,直接通过地址来访问的话很不人性化的,所以在高级语言里面,地址都被替换为了符号。
比如我使用某个变空间,在高级语言里面,就通过变量名来访问,变量名就是一个符号,通过这个符号就可以操作这个变量,不必直接使用地址。
所以在高级语言的程序里面,程序员基本只见符号,不见地址,正是由于这样的做法,使得高级语言的语法相对汇编来说,非常的人性化,因为直接操作地址的话,你需要了解硬件结构,特别是存储器的结构,否则你就不知道应该操作那个地址,这就很痛苦,但是变成符号后,程序员不用关心这些,只关心符号即可。
编译后,符号虽然会被转化为地址,但是在高级语言的语法里面,符号并不直接等于地址,你不能直接当做地址来使用,在高级语言里面,符号会受到作用域的限制,缺少一定的灵活性。
所以为了能够更加自由的操作存储空间,像c/c++这种高级语言,除了能够使用符号来操作外,它还允许直接使用地址来操作,使用地址来操作时,可以不受符号作用域的限制。
1)作用域受限的例子。
a.c
int fun1(void) { int a = 200; } int fun2(void) { int a; fun1(a); }
fun1和fun2中a的作用域,只在自己函数中有效,所以通过修改fun1的a,对fun2中a不受影响。
a.c
int fun(void) { flag = 100; fun1(); }
b.c
static int flag = 0; static int fun1() { ... }
flag和fun1的作用域只在b.c中,因此a.c中的fun函数无法引用。
疑问:作用域限制是好还是坏?
答:有好有坏。
好处:防止相互干扰,比如防止命名冲突,防止相互篡改数据等
缺点:作用域限制太严,会使得编程过于死板,不够灵活
所以像c/c++为了更加的灵活,就加入直接的地址操作,这样就可以取一个折中,既可以受到作用域的限制,又可以保持灵活性,比如看下面的例子。
2)修改上面的例子
a.c
int fun1(int *a) { *a = 200; } int fun2(void) { int a; fun1(&a); }
直接改fun1和fun2的a,相互不受影响,这个是作用域在发挥作用。
但是由于fun2将a的地址(指针)传给fun1的a,所以在fun1中通过*a就可以读写fun2中a,又可以修改fun2中的a,如此就有了相当的灵活性。
3)再举一个例子
a.c
int fun(int *flag, void (*fun1p)()) { *flag = 100; //引用flag fun1p(); //调用fun1 }
b.c
static int flag = 0; static int fun1() { ... } static int fun2() { fun(&flag, fun1); }
flag和fun1是a.c本地的,在a.c中时无法直接调用的,但是通过fun(&flag, funp)将flag和fun1的地址传递给a.c中的fun后,fun通过地址依然能够引用flag和fun1。
疑问:为什么不将static直接改为extern?
答:这样当然也能解决问题,而且一般情况也都是使用这种方式来解决的,但是当工程代码写的复杂后,必须对于函数全局
全局变量的作用域进行限制,这样可以有更好封闭性,特别当代码需要进行逻辑上的分层时,那么不同层必须需要有相当的封闭性,在封闭之后层与层之间的对接,就需要靠这种方式来实现。
当然对接函数必须是extern的,比如,如果例子中连fun函数都是static的,那就完全隔离了。
疑问:封闭性的好处?
· 防止命名冲突
· 防止胡乱引用
· 防止数据篡改
这就好比为了防止两个区域相互干扰,那就需要建立围栏加以限制,但是不能完全封死,为了让区域间还能沟通,所以还得留下一道门。
在上面的例子中,void (*fun1p)()中fun1p是函数指针变量,有关函数指针变量,本章后面再详介绍。
4)java/c#等语言中有指针吗?
在java和c#等高级语言中,并不允许在代码中直接操作地址,只能使用符号来操作,虽然没了指针后,灵活性降低了,但是也同时规避了指针所带来的风险,因为指针不受作用域限制,直接通过地址操作空间,因此存在潜在的危害,没有了指针,这种风险自然也就没有了,不过代价就是灵活性降低了。