本文共 5083 字,大约阅读时间需要 16 分钟。
从变量的三要素开始谈起
为了把复杂的事情说简单,我们抛开指针先从变量谈起。(好吧,不知道这个笑话是不是够冷)一个变量(Variable),或者顺便兼容下面向对象(OO)的概念,我们统一称为对象(Object),除了保存于其中的内容以外,只有三个要素:
其中,我们习惯于把后两者合并在一起称之为,变量的"类型"。
地址数值(Address Value)
地址的数值是一个无符号整数,其位宽由CPU的地址总线宽度所决定。话虽如此,其实主要还是编译器在权衡了“用户编写代码的便利性”以及“生成机器码的效率”后为我们提供的解决方案:例如,针对8位机,编译器普遍以等效为uint16_t的整数来保存地址信息;针对16位机和32位机,编译器则普遍选择uint32_t的整数来保存地址信息;针对64位机,编译器则可能会提供两种指针类型,分别用来对应uint32_t的4G地址空间和由uint64_t所代表的恐怖地址空间……提问,8086有20根地址线,请问用哪种整形来表示其地址呢?(uint16_t、uint32_t还是uint20_t)——由于uint20_t并不存在,也并不适合CPU进行地址运算,所以统一用uint32_t来表示最为方便。
总而言之,地址的数值是一个无符号整数。知道这个有什么用呢?我们待一会再说。这里我们需要强调一句废话:地址的数值既然是整数,那么它就可以用另外的变量(类型合适的整形变量或者指针变量)进行保存——任何指针变量,其本质,首先是一个无符号整形变量。任何指针常量,其本质首先是一个无符号整数。
请一定要记住(重要的事情说三遍):
变量的三要素中,仅有地址值有可能会占用物理存储空间。
变量的三要素中,仅有地址值有可能会占用物理存储空间。
变量的三要素中,仅有地址值有可能会占用物理存储空间。
大小(Size)和对齐
如果仅从变量的大小来看整个计算机世界,就好像一副彩色图片被二值化了,到处是Memory Block,他们的尺寸通常是1个字节、2个字节、4个字节、8个字节、16个字节或者由他们组合而成的长度各异Block。这些Block通常被编译器在代码生成的时候对其到地址的宽度上,比如地址宽度是32bit的,就对齐到4字节,地址宽度是16bit的,就对齐到2字节……如果你习惯于使用汇编语言来进行开发,你一定能体会我所描述的这种感觉。这些你统统都可以忘记,但有一点绝对要记住(重要的事情说三遍):
变量的三要素中,大小值从不会额外占用物理存储空间。
变量的三要素中,大小值从不会额外占用物理存储空间。
变量的三要素中,大小值从不会额外占用物理存储空间。
注意: 地址的大小信息描述的是这个变量占用几个字节,这里说大小信息并不占用物理存储器空间,并不是说,变量中保存的内容不占用存储器空间。请注意区别。
C语言中,可以用sizeof( )来获取一个变量的大小。前面我们说过,指针首先是一个整形变量,那么容易知道:
uint8_t *pchObj;uint16_t *phwObj;uint32_t *pwObj;
sizeof(pchObj) 、sizeof(phwObj)、sizeof(pwObj)以及sizeof任意其它指针的结果都是一样的,都是当前系统保存地址数值的整形变量的宽度。对32位机来说,这个数值就是4——因为,sizeof( ) 求的是括号内变量的宽度,而指针变量首先是一个整形变量!同一CPU中同一寻址能力的指针,其宽度是一样一样一样的!
一个类型的大小信息除了描述一个变量所占用的存储器尺寸以外,还隐含了该变量的对齐信息。从结论来说,32位处理器架构下:对普通的变量类型来说,编译器“倾向于”将小于等于64Bit的数据类型自动对齐到与其大小相同的整数倍上;比如2字节大小的变量会被对齐到2的整数倍地址上,4字节大小的变量会被对齐到4的整数倍地址上,以此类推。详情参考文章《漫谈C变量——对齐(1)》
对结构体和共用体来说,它会以所有成员中最大的那个对齐作为自己的对齐值。比如,下面的结构体就是对齐到4的整倍数,因为结构体内最大的对齐类型来自于一个指针(pTarget),而指针在32位系统下是4字节,因此整个结构体的对齐就是4:
struct example_t { uint8_t chID; //!< 对其到1字节 uint16_t hwCMDList[4]; //!< 对齐到2字节 void *pTarget; //!< 对齐到4字节};
适用的方法(Method)和运算(Operation)
对面向对象中的对象来说,方法就是该对象类中描述的各种成员函数(Method);对数据结构中的各类抽象数据类型(ADT,Abstract Data Type)来说,就是各类针对该数据类型的操作函数,比如链表的添加(Add)、插入(Insert)、删除(Delete)、和查找(Search)操作;比如队列对象的入队(enqueue)、出队(Dequeue)函数;比如栈对象的入栈(PUSH)、出栈(POP)等等……
对普通数值类的变量来说,就是所适用的各类运算,比如针对 int的四则运算(+、-、*、/、>、<、==、!=…)。你不能对float型的数据进行移位操作,为什么呢?因为不同的类型拥有不同的适用方法和运算。
也许你已经猜到了,类型所适用的方法和运算也不会占用物理存储空间。由于变量的“大小信息”和“适用的方法和运算信息”统称为“类型(Type)信息”,我们可以简化为:
变量的三要素中,类型信息从不会额外占用物理存储空间。
变量的三要素中,类型信息从不会额外占用物理存储空间。
变量的三要素中,类型信息从不会额外占用物理存储空间。
化繁为简的威力
前面说了那么多,实际上可以简化为下面的等式:Variable = Address Value + Type Info
变量 = 地址数值 + 类型信息
其中,地址数值的保存、表达和运算是(有可能)实实在在需要占用物理存储器空间的(RAM和ROM);而类型信息则是编译器专用的——仅仅在编译时刻会用到,用来为编译器语法检测和生成代码提供信息的——话句话说,你只需要知道,类型信息是一个逻辑上的信息,是虚的,在最终生成的程序中并不占用任何存储器空间。你也可以理解为,类型信息最终以程序行为的方式体现在代码中,而并不占用任何额外的数据存储器空间。
void 的字面意思是“空类型”, void *则为“空类型指针”, void *可以指向任何类型的数据。
const修饰的是只读的变量。const 定义的只读变量从汇编的角度来看, 只是给出了对应的内存地址, 是在编译的时候确定其值。
const可以节省空间,避免不必要的内存分配,同时提高效率。
编译器会把一切编译时刻已经确定的东西尽可能只保留其语法意义的同时确保其不占用实际内存空间
根据这条规则,如果你做了一个 结构体,结构体定义实例的时候用了一个 const,那么这个结构体很可能是可以避免实际占用空间的——除非你作死,对这个结构体做了取地址运算。如果这个const的结构体里有函数指针,那么对这个函数指针的访问会变成普通函数调用——而不是借助函数指针调用——因为编译器已经知道你的意图,觉得没必要真的经过一个结构体。同理,如果里面有其它普通整形变量,由于const的存在,编译器可能会直接把他们当成立即数来处理。这条规则本质是一个编译器优化规则,就是说,如果编译时刻,信息都是确定的,那么我只要保留语法意义就行了(因为语法意义只有编译器去遵守,并不需要占用存内存空间去保存,属于“虚”的东西),只有绕不开才分配。
举个例子:你有个函数指针,你声明为const,那么通过这个函数指针访问函数的地方就会变成直接对函数的调用,而这个函数指针实体是不会存在的——这就是用到了,但绕的开。如果你作死,对这个函数指针取地址,并且这个地址还被赋值给别的变量——那就是用到了,且绕不开,那真的就没办法,只能分配一个实体了。这个原则对编译器来说是优化的指导原则,所以,不开优化,开不同等级的优化,不同编译器,都有可能有行为上的差异。
const修饰指针:
const int *p; // p 可变, p 指向的对象不可变 int const *p; // p 可变, p 指向的对象不可变 int *const p; // p 不可变, p 指向的对象可变 const int *const p; //指针 p 和 p 指向的对象都不可变先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。 “近水楼
台先得月”,离谁近就修饰谁。 const int p; //const 修饰p,p 是指针, *p 是指针指向的对象,不可变 int const p; //const 修饰p,p 是指针, *p 是指针指向的对象,不可变 int *const p; //const 修饰 p, p 不可变, p 指向的对象可变 const int const p; //前一个 const 修饰p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变什么是窥孔优化?
volatile 关键字可以关闭窥孔优化。
volatile修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile在多线程共享变量时,可以保证可见性。
例如以下例子: int i=10; int j = i; //(1)语句 int k = i; //(2)语句这时候编译器对代码进行优化,因为在(1)、(2)两条语句中, i 没有被用作左值。这时候编译器认为 i 的值没有发生改变,所以在(1)语句时从内存中取出 i 的值赋给 j 之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给 k 赋值。编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。但要注意: (1)、(2)语句之间 i 没有被用作左值才行。
再看另一个例子:
volatile int i=10; int j = i; //(3)语句 int k = i; //(4)语句volatile 关键字告诉编译器 i 是随时可能发生变化的,每次使用它的时候必须从内存中取出 i的值,因而编译器生成的汇编代码会重新从 i 的地址处读取数据放在 k 中。
这样看来,如果 i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问,也就是一个线程改变了共享变量的值,另一个线程能立即可见。
如何对volatile 修饰的变量进行手工优化
转载地址:http://xdnii.baihongyu.com/