数据类型:
要了解数据是如何存储的,我们就得先知道C语言中的数据类型
基本数据类型
基本数据类型,也就是C语言内置类型:
char -> 字符型
short -> 短整型
int -> 整型
long -> 长整型
long long -> 更长的整型
float -> 单精度浮点型
double -> 双精度浮点型
数据类型的大小
C语言中可用sizeof操作符来计算数据类型在内存中所占空间的大小,单位是字节
#include <stdio.h>
int main()
{
printf("%zd\n", sizeof(char)); //char大小
printf("%zd\n", sizeof(short));//short的大小
printf("%zd\n", sizeof(int));//int的大小
printf("%zd\n", sizeof(long));//long的大小
printf("%zd\n", sizeof(long long));//long long 的大小
printf("%zd\n", sizeof(float));//float的大小
printf("%zd\n", sizeof(double));//double的大小
return 0;
}
数据类型的意义
1、类型决定了要开辟空间的大小
2、类型的大小决定了空间的使用范围
3、类型决定了如何看待内存空间的视角
类型的基本归类
整型家族:
char
unsignde char
signed char
short
unsigned short
signed short
int
unsigned int
signed int
long
unsigned long
signed long
long long
unsigned long long
signed long long
注:char类型也是整型家族的一员
char类型在内存中存储的时候,存的是其对应的ASICC码值
unsigned 表示 无符号数
signed 表示 有符号数
浮点数家族:
float 单精度浮点型
double 双精度浮点型
构造类型:
构造类型也叫自定义类型
是我们自己所创建的类型
数组类型
结构体类型 struct
联合类型 union
枚举类型 enum
指针类型:
指针类型分类:
整型指针:int*
字符指针:char*
浮点型指针:float*
空类型指针:void*
........
注:空类型的指针可以接收任意类型的指针数据
空类型
void 空类型(无类型)
用途:
函数的返回类型
函数的参数
指针类型
......
整型在内存中的存储
整型家族在内存中是以二进制的补码进行存储
那么要了解它的存储,
我们首先要了解清楚,原码、反码、补码的概念
原码、反码、补码
为什么存的是补码?
因为使用补码,可以将符号位和数值域统一处理
同时,加法和减法也可以统一处理(CPU只有加法器)
此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
原码 转 补码
原码 —> 反码 —> 补码 (原码转补码)
原码:将整数直接转化成二进制,这个二进制数就是它的原码
反码:将原码的符号位不变,其它位按位取反得到反码
补码:给反码 +1 得到补码
注:转化二进制数的最高位是符号位,其中 0 表示正数,1 表示负数
eg:
#include <stdio.h>
int main()
{
int a = 10;
//定义一个整型变量,在内存中开辟4字节的空间
//变量名为a,这个空间里面存储10
//那么数字10是如何存储的呢?
// 整型家族在存储的时候存的是二进制的补码
// 先将数字化成二进制 得到原码
// 00000000 00000000 00000000 00001010 --》原码
// 再将原码按符号位不变,其它位按位取反得到反码(最高位是符号位,0表示正数,1表示负数)
// 01111111 11111111 11111111 11110101 --》反码
//再让反码+1得到补码
// 01111111 11111111 11111111 11110110 --》补码
//而在内存中存的就是二进制的补码
printf("%d\n",a);
return 0;
}
补码转原码
方法1: 倒着推回去
补码 —> 反码 —> 原码
先让补码 -1 得到反码,
再让反码符号位不变其它位按位取反得到原码
方法2: 重新按照原码转补码的步骤走一遍
先让补码符号位不变其它位按位取反 得到一个二进制序列
再让这个二进制序列 +1 得到原码
eg:
#include <stdio.h>
int main()
{
int a = 10;
// 定义一个整型变量a
// a的原码:00000000 00000000 00000000 00001010
// a的反码:01111111 11111111 11111111 11110101
// a的补码:01111111 11111111 11111111 11110110
printf("%d\n", a);
//打印a
//因为在内存中存的是补码,而我们取出来的时候是要用原码,
//所以 要将补码 转回 成原码
// 方法1:倒着推回去
// a的补码:01111111 11111111 11111111 11110110
// 让补码 -1 得到反码
// a的反码:01111111 11111111 11111111 11110101
// 让反码符号位不变,其它位按位取反得到原码
// a的原码:00000000 00000000 00000000 00001010
// 最后将原码以 %d(十进制数)的形式打印出来
//方法2:重新按照原码转补码走一遍
// a的补码:01111111 11111111 11111111 11110110
// 让补码符号位不变,其它位按位取反 得到一个二进制序列
//二进制序列:00000000 00000000 00000000 00001001
//再让这个二进制序列 +1 得到原码
// a的原码:00000000 00000000 00000000 00001010
return 0;
}
大小端字节序存储
每个机器的存储模式不同,
二进制的补码在存储时 有的机器是从前往后存储,而有的机器是从后王前存储
由于存储顺序不同,就产生了大小端的概念
当前机器是从后往前存储的
大端存储:数据的低权值位保存在内存的高地址处,数据的高权值位保存在内存的低地址处
小端存储:数据的低权值位保存在内存的低地址处,数据的高权值位保存在内存的高地址处
(小端口诀:小小小(低权值位,低地址,小端))
例题:判断当前机器是大端字节序存储,还是小端字节序存储
#include <stdio.h>
int main()
{
int a = 1;
//原码:00000000 00000000 00000000 00000001
//反码:01111111 11111111 11111111 11111110
//补码:01111111 11111111 11111111 11111111
//显示的时候是用十六进制显示
//原码:00000000 00000000 00000000 00000001
// 十六进制显示:00 00 00 01
// 小端存储:10 00 00 00
// 大端存储:00 00 00 01
//将a的第一个字节的地址给b
// *b取到第一个字节的内容,若为1 是小端,若为0 是大端
char* b = &a;
if (*b == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
例题:
//第一题
#include <stdio.h>
int main()
{
char a = -1;
//原码:10000001
//反码:11111110
//补码:11111111
signed char b = -1;
//有符号的char取出来的时候还是将补码转为原码
// 取出来的原码:10000001
unsigned char c = -1;
//无符号char 取出来的时候,认为补码就是原码
//取出来的原码:11111111
printf("a=%d b=%d c=%d\n", a, b, c);// -1 -1 255
return 0;
}
//第二题
#include <stdio.h>
int main()
{
char a = -128;
// 原码:10000000
// 反码:01111111
// 补码:10000000
printf("%u\n", a);//非常大的一个数字
//在取的时候是用无符号整数
//认为补码就是原码
// 10000000 00000000 00000000 00000000
return 0;
}
//第三题
#include <stdio.h>
int main()
{
char a = 128;
// 正数的原反补相同
// 补码: 10000000
printf("%u\n", a);//2^32
//在取得时候 进行整型提升
//10000000 00000000 00000000 00000000
return 0;
}
//第四题
#include <stdio.h>
int main()
{
int i = -20;
//原码:10000000 00000000 00000000 00010100
//反码:11111111 11111111 11111111 11101011
//补码:11111111 11111111 11111111 11101100
unsigned int j = 10;
//原反补相同
//补码:00000000 00000000 00000000 00001010
//补码:11111111 11111111 11111111 11101100
// 11111111 11111111 11111111 11110110
//让他们的补码相加,在取的时候是%d 有符号的,所以结果是-10
// 10000000 00000000 00000000 00001001
// 10000000 00000000 00000000 00001010 -10
printf("%d\n", i + j);
return 0;
}
//第五题
#include <stdio.h>
int main()
{
unsigned int i = 0;
for (i = 9; i >= 0; i--)
{
printf("%u ", i);
}
// 9 8 7 6 5 4 3 2 1 0
//当i--变成-1的时候-1的二进制序列为:10000001
//但因为i是无符号整数,所以在取的时候,会将符号位当做是数位计算进去
//在打印的时候会发生整型提升,所以会发生死循环!
// 10000001 0000000 00000000 00000000
return 0;
}
//第六题
#include <stdio.h>
int main()
{
char a[1000];
int i = 0;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
// -1 ..... -127 128 ...... 0
// 127+128=255
//strlen函数遇到’'\0'才停止 而‘\0'的ASICC值是 0 所以遇到0就结束
printf("%d\n", strlen(a));
return 0;
}
//第七题
#include <stdio.h>
unsigned char i = 0;
int main()
{
// 00000000
// 00000001
// .......
// 11111111
// ......
// 10000000
//死循环
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
浮点型在内存中存储
浮点家族:float、double、long double 类型
浮点数存储的一个例子:
#include <stdio.h>
int main()
{
int n = 9;
float* p = (float*)&n;
printf("%d\n", n);//9
printf("%f\n", *p);//0.000000
*p = 9.0;
printf("n的值为:%d\n", n);//1091567616
printf("*p的值为:%f\n", *p);//9.000000
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
按照IEEE规定则可以看成:(-1)^0 * 1.01 * 2^2
按照公式:S=0,M=1.01,E=2
存储时如何放入内存的呢?
IEEE 754 规定:
1、32位的浮点数的存储:
最高的1位是符号位S,接着的8位是指数E,剩下的32位是有效数字M
2、 64位浮点数的存储
最高的1位是符号位S,接着的11位是指数E,剩下的52位是有效数字M
IEEE 754,对有效数字M,和指数E 有些特别的规定:
1、对于有效数字M:
因为 1≤M<2 所以M总是 1.xxxx... 的数,
因此1 可以被舍弃,我们在存储的时候只存储M的xxxx...部分,
等到读取的时候在xxx...前面把1加上去
好处:舍弃1这样可以存储24位有效数字
2、对于指数E:
首先E为一个无符号整数:
因为E是科学技术法的指数,所以会出现负数的情况,但是E是一个无符号整数,
所以IEEE 754 规定:E在存入内存时,其真实值必须加上一个中间数
对于8位的E 中间数是 127,对于11位的E 中间数是 1023
eg: 2^10 中 指数E为10,将它保存成32位浮点数,E=10+127=137
存入内存中:10001001
然而指数E从内存中取出又分为三种情况:
取E的三种情况:
1、E不全为0或不全为1:
此时取的时候 将E减去中间值(127或1023)得到真实值,
再将有效数字M前面加上第一位的1
eg:浮点数0.5 (1/2) 二进制为:0.1
转化为:(-1)*0 * 1.0 * 2^-1
在存储时 给8位E加上中间数127,舍弃M第一位的1,不够位的补0
S=0,E=-1+127=126,M=1.0
则存储起来的二进制为:0 01111110 00000000000000000000000
2、E为全0:
此时取的时候,指数E=1-127或者E=1-1023,即为真实值
有效数字M不再加上第一位的1,而是还原为 0.xxxx... 的小数
3、E为全1:
如果有效数字M全为0 ,表示无穷大 (±无穷大)(正负号取决于S)
注:由于浮点数跟整数的存取方式不同,用不同的方式取的时候结果也大不相同
此时我们再回过头来解释刚刚的例题
#include <stdio.h>
int main()
{
//由于浮点数跟整数的存取方式不同,用不同的方式取的时候结果也大不相同
int n = 9;//n是整数,存在原反补问题
//原码:00000000 00000000 00000000 00001001
//正数的原码、反码、补码相同
//补码:00000000 00000000 00000000 00001001
// 内存中存的就是整数的补码
float* p = (float*)&n;
//将整型的内存空间强制转化成浮点型
printf("n = %d\n", n);//9
// 用%d读取的时候,打印的是有符号十进制整数
// 由于n是正数,原码、反码、补码都相同
// 所以用%d打印 结果就是9
printf("%*p = %f\n", *p);//0.000000
//用%f读取的时候,打印的是浮点数,
// 而这片空间里面存储的是:00000000 00000000 00000000 00001001
// 用浮点数的存储规则取出来
// 0 00000000 00000000000000000001001
// S E M
// 在取的时候,S为0表示正数,E全为0的情况 有效数字M不加第一位的1
//而在打印的时候%f只能精确到有效数字后六位
// 所以结果为:0.000000
*p = 9.0;
//在p指针指向的空间n里面放入浮点数字9.0
//9.0的二进制数:1001.0 -- 1.001*2^3
//由浮点数的存储规则可知:
//(-1)^0 * 1.001 * 2^3
//S=0,E=3,M=1.001
//在存储的时候让E加上中间值,让M舍弃第一位的1
//S=0,E=3+127=130,M=001
//存入到内存的二进制数,不够位数的补0
//0 10000010 00100000000000000000000
printf("n = %d\n", n);//1091567616
//用%d读取的时候是以整数的形式读取内存中存入的二进制数
//0 10000010 00100000000000000000000
//将内存中的二进制数读取为整数结果是:1091567616
printf("n = %f\n", *p);//9.000000
/*用%f读取的时候读取的是浮点数
而内存中本身就存入的是浮点数
打印的结果就是它本身:9.000000*/
return 0;
}