前言
C语言中有4种基本数据类型——整形、浮点型、指针和聚合类型(如数组和结构等)。所有其他类型都是从这4种基本类型的某种组合派生而来的。其中整形和浮点型使用的最多。本文将简要讲解整形和浮点型是怎样在内存储存的。
整形在内存的储存
计算机中整形有三种表示方法,即原码,反码和补码,三种形式都由符号位和数值位两边部分组成。最高位是符位,0代表正,1代表负
原码、反码、补码之间的关系
正数的原码反码补码相同,负数的原码反码补码按如下规则计算 原码:直接将十进制数翻译为二进制,最高位放符号位 反码:原码符号位不变,其他位按位取反 补码:反码+1
为什么存在原码、反码、补码
存在原码反码补码的概念,其实是为了引进补码 在计算机系统中,数值一律用补码来表示和存储。原因在于,
- 使用补码,可以将符号位和数值域统一处理;
- 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
如何理解第一点: 比如我要计算1-1=0
int a=1;
int b=-1;
int c=a+b;
1的原码:00000000 00000000 00000000 00000001 -1的原码:10000000 00000000 00000000 00000001 两者相加得到:10000000 00000000 00000000 00000010代表-2,显然错误 但是如果我们使用补码 1的反码:00000000 00000000 00000000 00000001 1的补码:00000000 00000000 00000000 00000001 -1的反码:11111111 11111111 11111111 11111110 -1的补码:11111111 11111111 11111111 11111111 两者补码相加得到:1 00000000 00000000 00000000 00000000 而整形和整形相加的结果将被处理为整形,所以只截取后8位,而这就等于我们想要的结果0 对于其他例子也是同样的原理
如何理解第二点: 从原码反码补码的关系来看我们会用补码-1再符号位不变其他位按位取反得到原码,但实际上,对补码按位取反再+1就能得到原码,这个操作和从原码到补码的操作是相同的 比如-1 -1的原码:10000000 00000000 00000000 00000001 -1的反码:11111111 11111111 11111111 11111110 -1的补码:11111111 11111111 11111111 11111111 补码按位取反:100000000 00000000 00000000 00000000 再+1:10000000 00000000 00000000 00000001 而这就是-1的原码!
大小端介绍
当我们打开vs调试的内存窗口的时候我们会发现类似下面的情况  数据高位的内容竟然放在内存的低位上,看起来数据再内存里就像是倒着存放的,其实这里涉及到编译器大小端模式的概念
什么是大小端
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中; 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
为什么存在大小端
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。 我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
当前机器采用大端模式还是小端模式其实可以通过一个简单的程序判断
百度2015年系统工程师笔试题: 请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

#include<stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
if (1 == check_sys())
printf("小端\n");
else
printf("大端\n");
return 0;
}
关于整形储存的一些例题
例一
#include <stdio.h>
int main()
{
char a= -1;
signed char b=-1;
unsigned char c=-1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}
 解析: 由于是在32位环境下,-1的 原码:10000000 00000000 00000000 00000001 反码:11111111 11111111 11111111 11111110 补码:11111111 11111111 11111111 11111111 而char类型是8bit的,保存到变量里的时候只保存低8位,于是就是11111111 %d要求以有符号数整数形式打印,打印前需要先整形提升,由于a和b都是有符号数,整形提升高位补符号位,都是 11111111 11111111 11111111 11111111 转换成原码打印就是-1 而c是无符号数,整型提升高位补0,得到 00000000 00000000 00000000 11111111 最高位是0,%d认为这是个正数,原反补码相同,直接转为10进制就是打印出来的255
例二
#include<stdio.h>
int main()
{
char a=128;
char b=-128;
printf("%u\n",a);
printf("%u\n",b);
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
 先分析a和b中存的是什么 128的 原码:00000000 0000000 00000000 10000000 反码:同上 补码:同上 所以a存的是10000000 -128的 原码:10000000 00000000 00000000 10000000 反码:11111111 11111111 11111111 01111111 补码:11111111 11111111 11111111 10000000 所以b存的是10000000 %u要求以无符号整数形式打印,打印前需要整形提升,由于a和b都是有符号数,整型提升后均为 11111111 11111111 11111111 10000000 %u把上面这个二进制数看成无符号数直接转成10进制打印,就得到了屏幕上的结果
那为什么a以%d形式打印的结果是-128呢?这同样是由于本应是数值位的1占了符号位,整形提升后,导致它变成了一个负数。a整形提升的结果是 11111111 11111111 11111111 10000000, 这是补码,打印要以原码的形式打印,%d以有符号数的视角看上面这个数,先-1在按位取反得到 100000000 00000000 00000000 10000000 而这就是-128
例三
#include<stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
if ((i + j) > 0)
{
printf("a\n");
}
else
{
printf("b\n");
}
printf("%d\n", i + j);
printf("%u\n", i + j);
return 0;
}
 解析 -20的 原码:10000000 00000000 00000000 00010100 反码:11111111 11111111 11111111 11101011 补码:11111111 11111111 11111111 11101100 10的 原码:00000000 00000000 00000000 00001010 反码:同上 补码:同上 i+j补码相加得到 11111111 11111111 11111111 11110110 而我们注意到i是int型的,j是unsigned型的,执行i+j前会对i先进行算术转换成unsigned int型,结果也被视为unsigned int型,那么 11111111 11111111 11111111 11110110 将被视为一个大于零且相当大的数,故打印了字符a, 而这个数恰好就是4,294,967,286  当我们以%d形式打印i+j时,最高位1被视为符号位,要先转换位原码再打印结果就是-10 我们可以这样理解,数据类型只是决定看待内存中数据的视角,不影响我存入值的操作 例四
#include<stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
死循环 解析 i的数据类型是unsigned int,无论如何表达式i>=0都为真。我们不妨看下i的变化(利用Sleep函数):先从9到0,0-1,i中存的值从 00000000 00000000 00000000 00000000 变成 11111111 11111111 11111111 11111111 上面这个值被解析成无符号数 
例五
#include<stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
 解析 注意’\0’的ASCII码值为0 我们知道char类型能保存的值的范围是-128到127,那么假如对一个char变量一直+1,它的值怎么变化呢?  可见a的值将会从0一直变到127,从127直接跳到-128,再从-128变到0,形成了一个闭环 
例六
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
 死循环,原因如上图,只要把例五种的那个原上的补码按照无符号数解析即可,i从0到255到0形成循环。
浮点型在内存的存储
关于浮点型存储的一个例题
可能不少初学C语言的同学打印浮点数的时候把转换说明写成了%d,导致结果是0.00000,很久也找不出程序哪错了,这里将解决你的疑惑。
#include<stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
 要理解这个程序的结果,我们先要了解浮点数再内存是怎样保存的 根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位。
举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。 十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。     在上面的程序种整形9在内存中为 00000000 00000000 00000000 00001001 %f以浮点数的方式解析这个值,由于阶码E=0,浮点数的指数直接取1-127即-126,并且精度部分不再+1,以表示一个非常接近0的数, V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146) 就得到了打印的0.000000
再看把浮点数9.0以整形形式打印,先看9.0是如何储存的
(-1)^0*1.001*2^3
在内存中为 0 10000010 001 0000 0000 0000 0000 0000 以有符号十进制整数解析就得到 1,091,567,616
|