关于变量的类型及存储剖析和大小端问题

来源:互联网 发布:linux vim装不上 编辑:程序博客网 时间:2024/06/16 09:55

       变量是一个程序最基本的组成单元,程序就是对变量中的数据做一系列的运算以形成某种功能,而存放不同数据的变量的类型可能不一样,本文意在分析关于变量和函数的类型以及在内存中的存储形式。

        在C99标准中数据类型有:基本类型、枚举类型(enum)、空类型(void)、派生类型。我们现在分析的就是其中的基本类型和空类型的变量和函数。

        基本类型可分为整型类型和浮点类型。整型类型包括基本整型(int)、短整型(short int)、长整型(long int)、双长整型(long long int)、字符型(char)、布尔型(bool),每个整型又可分为有符号或者无符号;浮点类型有单精度浮点型(float)、双精度浮点型(double)。

        有这么多的类型,那么怎么来区分这些类型呢?当然,我们在定义变量时可以看到变量的类型,但类型是怎么对变量作用的呢?为何对变量声明某种类型之后变量就具有了这种类型的功能与特征了呢?不同的变量在内存中的储存是怎样的呢?接下来我们来探讨一下。

        首先我用的是32位系统的Vs2015,我们先来探讨一下整型类型。我们用sizeof()来求出整型的大小(也就是在内存中的字节数),我们输入如下代码:

#include<stdio.h>int main(){printf("%d\n", sizeof(int));return 0;}

程序运行结果如下:

可见,整型变量在内存中占用4个字节,其他的数据类型变量也可以用相同的方法得到其占用的内存大小。

        我们知道定义一个变量后,系统就在内存中为这个变量开辟同变量类型大小的空间,那么数据在内存中具体是怎么储存的呢?我们知道,计算机只能识别0和1,不能识别其他数字,所以即使再复杂的数据在计算机中也是用一系列的0和1组成的,也就是我们知道的二进制,也就是说所有数据在计算机中都是以二进制的形式储存的,我们知道数据的二进制有原码、反码和补码三种形式,而在数据进行运算时,由于计算机中只有加法器的原因,原码和反码的很多计算都不能进行,而补码恰好能够弥补原码和反码的不足,因此,数据在内存中以二进制补码的形式储存的,比如我们定义一个整型变量 int  num = 3,则num在内存中的补码形式为

用十六进制来表示就是0x00 00 00 03,我们在vs中输入如下代码:

#include<stdio.h>int main(){int num = 3;return 0;}

调试,调出内存观察num在内存中的十六进制数

显示结果为0x03 00 00 00,我们的推测是0x00 00 00 03,两者相反,这是为什么呢?这就涉及到数据的大小端储存。

大端(存储):数据的高位保存在地址的低地址中,数据的低位保存在地址的高地址中。

小端(存储):数据的高位保存在地址的高地址中,数据的低位保存在地址的低地址中。

如图:

不同的平台是大端存储还是小端存储都不一样,我们可以用一个简单的程序来探索Vs的存储方式,在Vs中输入程序:

#include<stdio.h>int check_sys(){int i = 1;return *((char*)&i);}int main(){int ret = check_sys();if (ret == 0)printf("大端\n");else if (ret == 1)printf("小端\n");elseprintf("未知\n");return 0;}

运行结果:


由程序结果可知,vs2015为小端存储。

        通过上面的分析知道,整型数据在内存中占四个字节以补码的形式存储,那么浮点型数据在内存中的存储是否和整型数据一样呢?我们在vs中输入如下数据:

#include<stdio.h>int main(){int num = 9;float *pFloat = (float *)#printf("num的值为:%d\n", num);printf("*pFloat的值为:%f\n", *pFloat);return 0;}
运行结果为:

num与*pFloat中的数据分明是一样的,为什么输出结果不一样呢?由此可推测,内存中整形和浮点型数据的存储并不同。

        我们知道,整型数据和浮点型数据的一个区别就是整型数据没有小数部分,而浮点型数据包含了小数,因此,如果浮点数在内存中也以补码的方式存储肯定是不行的,在浮点型的二进制中,数据由两个部分组成,小数点前的整数部分和小数点后的小数部分,我们可以由此猜测,浮点数在内存中是否也是这样存储的呢?

        /*根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S*M*2^E

(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。*/

      首先我们用程序分别测出float数据和double数据在内存中的大小:

#include<stdio.h>int main(){printf("%d\n", sizeof(float));printf("%d\n", sizeof(double));return 0;}

运行结果:


得到单精度浮点型数据占用4个字节,即32个比特位,双精度浮点型占用8个字节,即64个比特位。

.       IEEE 754规定:对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。如图

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

        /* IEEE 754对有效数字M和指数E,还有一些特别规定。前面说过,1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。至于指数E,情况就比较复杂。首先,E为一个无符号整数(unsigned int)这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。*/

(以上/**/中的内容引用比特科技整理的资料)

        上面讲了整形和浮点型数据在内存中的存储,我们对定义变量有了一定的理解,现在我们来定义一个变量

int i;

定义了一个整形变量,那么它是什么意思呢?就是对 i 求值得到一个整型值,照此思路那么

int  Fun();

表示对Fun()进行求值,得到一个整型值,也就是说函数的返回值是一个整型值,而

int *p;

求值得到整型,表示p为一个指向某个整型值的指针,我们再来看下面代码

int i = 10;int *p = &i;

第一句中,定义了一个整型变量并赋初值,表示在内存中开辟四个字节的空间,并以整型的储存形式存一个10在里面,第二句定义了一个有能力指向整型的指针变量,并且将整型变量i的地址存入p中,这样,我们就可以直接对i进行访问,也可以间接对 i 进行访问,在用指针p对 i 进行间接访问时,编译器会找到p所指向的地址空间,从所指向的地址处开始数出4个字节的空间,并且读出这四个字节中所存储的数据。我们再来看如下代码

#include<stdio.h>int main(){int arr[10] = { 0 };printf("%p\n", &arr);printf("%p\n", arr + 1);printf("%p\n", &arr + 1);return 0;}


输出结果是什么呢?我们先来分析一下,代码定义了一个整型数组,数组有十个元素,每个元素都是整型值,arr是数组名,在&arr和sizeof(arr)两种情况下,arr代表整个数组,&arr表示取出数组的地址,sizeof(arr)求整个数组的大小,除了这两种情况外,arr表示数组首元素的地址,那么我们现在可以猜测,第一个输出的是数组的地址,第二个为数组的地址加4,第三个为数组的地址加上整个数组的长度,结果如何呢,我们来看一下


果然与我们的猜测一样,我们再来看如下代码

#include<stdio.h>int main(){int arr[10] = { 0 };int *p1 = arr;int *p2 = &arr;printf("%p\n", &arr);printf("%p\n", p1 + 1);printf("%p\n", p2 + 1);return 0;}


这里与上一个代码的不同仅在于这里将数组地址和首元素地址放入一个整型指针中,那么现在的结果还会如上面那样吗?我们来看一看


结果并不如我们所料,后面两个值是一样的,是什么导致这样的结果呢?我们来分析一下,在语句int *p1 = arr; 中将数组首元素的地址存入整型指针变量p1中,当对p1进行解引用的时候将访问四个字节的内存,在语句int *p2 = &arr; 中将数组的地址取出后存入整型指针p2中,同时将数组的地址强制转换为整型变量的地址,当对p2进行解引用的时候也只能访问四个字节,因此p2+1代表跳过一个整型的长度,这就解释了第二个个第三个输出为什么是一样的,我们对代码作一下改变

#include<stdio.h>int main(){int arr[10] = { 0 };int *p1 = arr;int (*p2)[10] = &arr;printf("%p\n", &arr);printf("%p\n", p1 + 1);printf("%p\n", p2 + 1);return 0;}

这里定义了一个数组指针,将数组的地址放入,数组指针p2中,我们来看一下运行结果


结果p2+1跳过了一个数组的长度,这是因为这里p2是一个有能力指向具有十个整型值的数组的数组指针,对p2进行解引用将得到整个数组。

        通过上面的探讨,我们知道了定义了某个类型的变量时,系统就在内存中开辟对应大小的空间来存储对应的数据,在对这个变量进行访问时将读取这个变量所属的空间的数据,当通过指针对这个变量进行间接访问时,系统根据指针变量中的地址,找到所指向的空间,读取对应类型所占用的空间大小的内存中的数据。