整数在内存中的存储
整数的二进制表示方式有三种:原码,反码,补码,三种方法均有符号位(0:正,1:负)和数值位,最高位为符号位,其余都为数值位,正数的原码,反码,补码相同。负数的原码直接将数值按正负数形式翻译成二进制,反码为原码的符号位不变,其余按位取反,补码为反码+1。对于整形来说,数据存放在内存中为补码,但是。。。为什么呢?
原因:在计算机系统中,数值一律用补码表示和存储,使用补码可以将数值位与数值域统一处理,同时,也可将加减法同时处理(CPU只有加法器),此外,补码与原码相互转换,其运算过程相同,不需要额外的硬件电路。
大小端字节序判断
一、概念
什么是字节序嘞?名表其意:字节的顺序。当数值超过1个字节时,存储在内存中的顺序就需要确定,例:0x11223344
在大端字节序(将一个数值的低位字节序的内容存到高地址处,高位字节序的内容存放在低地址处)下,它是这样存储的:
低地址 11 22 33 44 高地址
在小端字节序(将一个数值的低位字节序的内容存到低地址处,高位字节序的内容存放在高地址处)下,它是这样存储的:
低地址 44 33 22 11 高地址
大小端的名字还有起源呢,这一切要从一个深奥的问题:剥鸡蛋要从大头还是小头剥说起。。。(格列夫游记的一次历史性会议讨论了这件事)
二、意义
为什么要存在大小端呢?
因为在计算机系统中,以字节为单位,每个地址单元对应着一个字节,一个字节占8个bit位,在C语言中有8bit的char,16bit的short,32bit的int,对于位数>8的处理器(16位、32位)由于寄存器宽度>1byte,必然存在一个如何将多个字节安排的问题,这就推动了大小端存储模式的形成
小端存储:X86,很多ARM,DSP 大端存储:KEIL,C51
大端模式的优点:大端模式中地址的变化顺序(低到高)与数据的阅读顺序(高位到低位)吻合。当沿着地址空间找到某个数据内存的时候,可以根据他的内容的第一个字节来判断正负。
小端模式的优点:强制类型转化,可以直接取出前面的低位两字节。CPU计算是从数据的低位到高位计算,效率高。CPU从地址的低位到高位移动的时候,正好数据也是从地位到高位变化,计算高效。
三、实例
(1)
实战环节:简述大小端字节序的概念,设计一个小程序判断当前机器的字节序(百度笔试题):
#include<stdio.h>
int main()
{
int a = 1;
if ((*(char*)&a) == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
封装到函数中:
int check()
{
int a = 1;
if ((*(char*)&a) == 1)
{
return 1;
}
else
{
return 0;
}
}
#include<stdio.h>
int main()
{
int a = 1;
if (check())
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
简化可写成:
int check()
{
int a = 1;
return (*(char*)&a) ;
}
tips:
万万不可return(char)a;
强转截断低位,截断时都是01。
(2)
#include<stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d b=%d c=%d\n", a, b, c);
return 0;
}
猜猜输出结果是什么?
为什么是这样的输出结果捏?我们先来了解一下char类型:
char类型比较特殊,是signed char还是unsigned char取决于编译器,在VS上,char为signed char。而对于int类型,int就是signed int,unsigned int自己作为一个类型。
char开辟八个bit位存放数据,存储时发生截断
-1
原码
10000000 00000000 00000000 00000001
反码
11111111 11111111 11111111 11111110
补码
11111111 11111111 11111111 11111111
截断存储后:
a:11111111
b:11111111
c:11111111
%d:以十进制形式打印有符号整形-->发生整形提升,整形提升高位补符号位
整形提升后a,b补码相同,都为:
11111111 11111111 11111111 11111111
a,b转成原码后结果为-1
无符号整形在整形提升时高位补0
c补码:
00000000 00000000 00000000 11111111
所以结果为:255
简单计算一下:假设+1,结果就为 100000000(二进制)==2^8==256。
256-1=255==11111111(二进制)
(3)
#include<stdio.h>
int main()
{
char a = -128;
char b = 128;
printf("%u\n", a);
printf("%u\n", b);
return 0;
}
输出?
在这里解释下,%u是打印无符号整数
-128
原:10000000 00000000 00000000 10000000
反:11111111 11111111 11111111 01111111
补:11111111 11111111 11111111 10000000
截断后存于a中:10000000
128
原:00000000 00000000 00000000 10000000
反:00000000 00000000 00000000 10000000
补:00000000 00000000 00000000 10000000
截断后存于b中:10000000
打印时发生整形提升
最高位补符号位,即补1
补完后:11111111 11111111 11111111 10000000
11111111 11111111 11111111 10000000转化成十进制数:就是4294967168
不信你看:
接下来介绍一下char类型的取值范围:
char ----> 8bit
补码们:
0000 0000-->0
0000 0001-->1
...
...
0111 1111-->127
1000 0000-->1111 1111-->10000 0000(溢出了,规定为-128)
1000 0001-->1111 1110-->1111 1111-->-127
1000 0010-->1111 1101-->1111 1110-->-126
...
...
1111 1110-->1000 0001-->1000 0010-->-2
1111 1111-->1000 0000-->1000 0001-->-1
综上,char类型的取值范围为:[-128,127]
(4)
#include<string.h>
#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;
}
strlen遇到\0即止,这是一个轮回:
(5)
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world!!");
}
return 0;
}
结果:死循环(unsigned char取值范围为0~255,判断条件永远符合)
(6)
#include<stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("hello world!");
}
return 0;
}
死循环+1
(7)
#include<stdio.h>
int main()
{
int a[4] = { 1,2,3,4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x %x", ptr1[-1], *ptr2);
return 0;
}
这段程序在x86下执行结果是什么?
解释:
ptr1:
ptr1[-1]-->*(ptr1+(-1))-->*(ptr1-1)
ptr2:
假设数组首元素地址:0x12ff40
0x0012ff40+1-->1244992+1-->1244993-->0x0012ff41
*ptr2:由于小端:0x 02 00 00 00
还是这段代码,x64运行就崩了:读取访问权限冲突:
为什么捏?
x86:指针大小-->4字节
x64:指针大小-->8字节
a是数组名,是数组首元素的地址,地址在64位环境下的大小就是8字节,强转成int必会发生截断
假设a:0x0012ffcd30542320-->0x30542320-->非法访问
若想让他正常,怎么改捏?
改成:
int *ptr2=(int*)((long long)a+1);
就好啦!
浮点数在内存中的存储
一、概念
浮点数类型:
1.float
2.double
3.long double-->C99植入
我们知道,sigend char的取值范围是-128~127,unsigned char的取值范围:0~255,signed short的取值范围:-32768~32767,unsigned short的取值范围:0~65535,这些整形的取值范围定义在一个头文件下:limits.h
浮点数的表示范围也定义在一个头文件下:float.h
下面来说浮点数类型在内存中的存储
二、实例
#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("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
输出结果?
解释概念
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^s表示符号位,当s=0时,V为正数,当s=1时,V为负数
M表示有效数字,(M>=1&&M<2)
2^E表示指数位
V=5.5(十进制)=101.1=
很好理解吧。。。拿十进制记:
V=123.45=
存储
对于32位浮点数,最高位为符号位S,接着的8位存指数E,剩余的23位存有效数字M
对于64位的浮点数,最高的一位存符号位S,接着的11位存指数E,剩下的52位存有效数字M
对于有效数字M和指数E,还有一些特殊规定:
IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保留后面的部分,能节省一位有效数字
对于指数E,情况较为复杂:
首先,E是无符号整数(unsigned int),若E为8位:0~255(中间数:127),若E为11位:0~2047(中间数:1023)
但是E可以是负数:
IEEE754规定:存入内存时,E的真实值必须再加上一个中间数
例:
5.5(十进制)==(二进制)
S=0,M=1.011,E=2
存储:
0 1000 0001 0110 0000 0000 0000 0000 000
1000 0001-->2+127转成二进制
取出
E从内存中取出分三种情况:
1.E不全为0,不全为1
2.E全为0
3.E全为1
(1)
E全为0:
为了表示很小的数字-->
这时规定E=1-127(或1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxxxx的小数,这样表示和接近0的很小的数字
(2)
E全为1:
若有效数字M全为0,则表示无穷大(正负取决于符号位S)
题解
那么这道题该如何解释呢?
9的补码:
0000 0000 0000 0000 0000 0000 0000 1001
在浮点数看来:
0 00000000 00000000000000000001001
情况:E全为0
即
%f:打印小数点后6位,打印出为0.000000
*pfloat=9.0
转成二进制:1001.0-->
存:
0 1000 0010 0010 0000 0000 0000 0000 000
打印时打印原码(正数原反补相同)
二进制转十进制-->结果即为1091567616