文章目录
前言
本章重点:
- 数据类型详细介绍
- 整形在内存中的存储:原码、反码、补码
- 大小端字节序介绍及判断
- 浮点型在内存中的存储解析
一、数据类型详细介绍
char //字符数据类型 1字节
short //短整型 2字节
int //整形 4字节
long //长整型 32位下是4字节,64位下是8字节
long long //更长的整形 8字节
float //单精度浮点数 4字节
double //双精度浮点数 8字节
//C语言有没有字符串类型?
ps:字符类型char在底层存储的是ASCII码值,所以我们往往把char划分到整形家族里
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
- 如何看待内存空间的视角。
二、类型的基本归类:
2.1整形家族:
char
unsigned char//无符号字符型
signed char//有符号字符型
//char 类型默认是有符号还是无符号是取决于编译器的,大部分编译器默认是有符号的
short
unsigned short [int]//[int]表示这个int可以不写, unsigned short= unsigned short int
signed short [int] //默认情况都是有符号的,signed short=short
int
unsigned int
signed int//默认情况都是有符号的,signed int=int
long
unsigned long [int]
signed long [int]//默认情况都是有符号的,signed long=long
long long
unsigned long long [int]
signed long long [int]//默认情况都是有符号的,signed long long=long long
关于unsigned:
#include<stdio.h>
int main()
{
unsigned char c1 = 255;
printf("%d\n", c1);
char c2 = 255;
printf("%d\n", c2);
return 0;
}
解释如下:
我们知道一个char类型是1字节,也就是8比特位,255转换成2进制是1111 1111
如果是unsigned char,就说明是无符号的char,也就是最高位是有效位,那么这个值就不存在原反补的概念了,类似非负数,原反补一样,内存里存的1111 1111打印出来也是1111 111这个值(我们转换成10进制是变成了255)
如果是char,也就是有符号的char,最高位是符号位,这个就是有原反补码的
补:1111 1111
反:1111 1110 (反码=补码-1)
原:1000 0001(原码=反码符号位不变,其他位全部改变)
我们存储的是补码,打印的是原码,1000 0001(有符号)转换成10进制就是-1
注:无符号char取值范围是0~255
有符号char取值范围是-128~127
2.2浮点数家族:
float
double
构造类型:
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
关于数组类型,这个知识点很简单,
我以int型数组给大家举个例子,推广到其他类型数组也是一样的
int main()
{
int arr[10] = { 0 };//数组类型就是把数组名去掉,剩下的类型int [10]
printf("%d",sizeof(int[10]));//打印40,因为整形int大小为4,一共有10个整形,4*10=40
return 0;
}
2.3指针类型
int *pi;
char *pc;
float* pf;
void* pv;
2.4空类型:
void//一般用于函数返回值类型,也就是不需要返回值的情况
ps:如果你哪天突发奇想想测一下sizeof(void),你会发现,会报错
三、整形在内存中的存储
3.1引子
我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
int main()
{
int a = -1;
return 0;
}
我们对上面代码进行调试,然后监视内存,发现a的地址上存储的是8个f
我们这里一个整形是4字节,然后对应32位比特位(二进制)
f是十六进制的表示,4个二进制才能表示1个十六进制数,
比如1111表示f(他们都对应十进制的15)
那么32个二进制数就可以用8个十六进制表示
ff ff ff ff也就是11111111 11111111 11111111 11111111
也就是我们常说的补码
3.2原码、反码、补码
计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,
符号位都是用0表示“正”,用1表示“负”,
而数值位三种表示方法各不相同
原码
直接将二进制按照正负数的形式翻译成二进制就可以。
反码
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码
反码+1就得到补码。
正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。
我们以%d打印出来的,那个是原码
还是看这段代码:
int main()
{
int a = -1;
return 0;
}
a=-1
-1的原码:10000000 00000000 00000000 00000001
-1的反码:111111111 111111111 111111111 111111110(反码=原码符号位不变,其他按位取反)
-1的补码:111111111 111111111 111111111 111111111(补码=反码+1)
这个补码也就是我们上面说的ff ff ff ff
到这块,我们知道了,计算机里整形存的是补码,但是为啥是补码呢?我直接存原码不行吗?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
下面是原反补互相得到的方式
除了上面这种方式,补码想得到原码,也可以直接按位取反,然后再加一
举个例子:
-1的补码为 11111111 11111111 11111111 11111111
符号位不变,其他位按位取反 1000000 0000000 0000000 0000000
最后+1得 1000000 0000000 0000000 0000001,也就是-1的原码
3.3大小端问题
int main()
{
//8个16进制数字正好占32位比特位
int a = 0x11223344;//0x表示这是一个16进制数字
//11是该数字的高位,44是该数字的低位!
return 0;
}
可以看到,我们这个编译器是把低地址内容放到了低地址处,高地址内容放在高地址处
我们称它为:小端字节序
还有一种放法是:低地址内容放到了高地址处,高地址内容放在低地址处
我们称它为:大端字节序
快速记忆:低位地址放低地址就是小端,否则就是大端。你记住“低-低-小”即可
百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
int main()
{
int a = 1;//00 00 00 01
char* p = (char*)&a;
if (*p == 1)
{
printf("小端");
}
else
{
printf("大端");
}
return 0;
}
3.4整形提升与有/无符号数
下列代码输出什么?
例1:
#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;
}
大部分编译器char都是默认signed char的,笔者这个编译器也是。
所以我们这里只要解释一下为啥char a=-1和unsigned char c=-1即可
-1是一个整形,它的原反补如下:
原码:00000000 00000000 00000000 00000001
反码:011111111 111111111 111111111 111111110
补码:011111111 111111111 111111111 111111111
但是把-1赋值给a时,由于a是char类型,所以存储时会截断变成111111111
同理,c是unsigned char类型,存储时也会截断变成111111111
所以char类型a在打印的时候,会整形提升,会由1字节(8比特位)变成4字节(32位比特位)
整形提升:
如果是无符号数,则高位直接补0;
如果是有符号数,则高位全补符号位。
a是有符号数,高位补符号位
a的补码:111111111 111111111 111111111 11111111
a的反码:111111111 111111111 111111111 11111110
a的原码:10000000 00000000 00000000 0000001
转换成10进制也就是-1
由于c是无符号数,高位补0
c的补码:00000000 00000000 00000000 11111111
然后我们无符号数,是认为和非负数一样的,也就是原反补码相同
c的原码:00000000 00000000 00000000 11111111
转换成10进制也就是255
例2:
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
-128是一个整形,它的原反补如下:
原码:10000000 00000000 00000000 10000000
反码:111111111 111111111 111111111 011111111
补码:111111111 111111111 111111111 10000000
-128赋值给char类型的a,存储时会发生截断变成10000000
我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
补码:111111111 111111111 111111111 10000000
我们打印时,是以%u的形式进行打印,是打印无符号整形(非负数原反补相同)
原码:111111111 111111111 111111111 10000000
转换成10进制是下面这个数字
例3:
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
128是一个整形,它的原反补如下:
原码:00000000 00000000 00000000 10000000
反码:01111111 111111111 111111111 011111111
补码:01111111 111111111 111111111 10000000
-128赋值给char类型的a,存储时会发生截断变成10000000
我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
补码:111111111 111111111 111111111 10000000
我们打印时,是以%u的形式进行打印,是打印无符号整形
原码:111111111 111111111 111111111 10000000
转换成十进制和例2的数字一样
小结:
整形提升的时候,看的是你原先是char a还是unsigned char a
但是打印的时候%d则视最高位为符号位,是否把补码转换成原码你得考虑正负。
%u则视最高位为有效位,补码也就是原码,直接打印补码即可。
例4:
#include <stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
计算机中加减操作都是对补码进行的
-20的原反补如下:
原:10000000 00000000 00000000 00010100
反:111111111 111111111 111111111 11101011
补:111111111 111111111 111111111 11101100
10原反补相同
补:00000000 00000000 00000000 00001010
10+20补码
111111111 111111111 111111111 11110110
ps:这里进行计算时会进行算术转换,整形变成无符号整形
总而言之,你是什么类型的数据存储在内存里不重要,我打印的时候再决定你最后是什么类型
我们打印时,是以%d的形式进行打印,是打印有符号整形
这里就要把补码转换成原码
反码111111111 111111111 111111111 11110101
原码10000000 00000000 00000000 00001010
转换成10进制就是-10
例5:
#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;
}
解释如下图:
上图也可以画成一个圈来帮助记忆,如下图
知道了signed char 的存储,该题基本上就迎刃而解了。
我们把数组a的内容画出来:
知道了数组a里面放的内容,还要知道strlen这个函数的特性,它是检测到\0停止,\0不计算
而我们的0,存放在内存里也就是\0,所以第一次检测到0就结束了。
0前面一共有-1 ~ -128还有127 ~ 1,一共是128+127=255个字符,所以打印255
例6:
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0;i <= 255;i++)
{
printf("hello!\n");
}
return 0;
}
解释如下:
unsigned char也是0000 0000 ~ 1111 1111,只不过这里最高位是有效位(你可以理解为全是非负数)
最高的数也就是1111 1111,转换成10进制是255,然后我们这里1111 1111再+1会变成1 0000 0000
但是unsigned char只有8位,会发生截断,又会变成0000 0000
所以我们这里会变成死循环,永远无法大于255
四、浮点型在内存中的存储
常见的浮点数:
3.1415926
1E10
ps:1E10这种是科学计数法,表示1.0*10^10
浮点数家族包括: float、double、long double 类型。
浮点数存储的实例:
#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;
}
对于第一个值9,因为我们&n得到了n的地址,
然后强制类型转换变成了float类型的指针(和int类型指针一样,大小都是4字节),
把这个指针赋给float*类型 pFloat,虽然指针类型变了,但是指针里面存储的地址还是n的地址,
所以可以通过pFloat找到n,然后以%d形式打印n的值9
而第一个*pFloat的值,我们知道pFloat是存储n的指针,
然后 *pFloat就是对这个指针解引用,得到该地址里面的值,
但是为啥打印的是0.000000呢?
我们n是以整形的形式放进去的,但是我们要拿出来,也就是以%f形式打印,
发现是0.000000,就说明浮点型的存储方式和整形不一样,如果一样,那拿出来应该也是9
对于num的值,我们*pFloat = 9.0,也就是找到n的那块地址,然后把该地址上的值用浮点数9.0赋值,也就是说,我们是用浮点型的形式放进去了。然后我们%d打印该值,发现也不是9.0,
再次验证浮点型的存储方式和整形不一样
第二个*pFloat的值,因为我们是以浮点型的形式放进去,
然后再以浮点型的形式拿出来(以%f的形式进行打印),所以我们这里可以正常打印9.0
num 和 * pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
十进制5.5转换成二进制101.1后,再使用科学计数法,就是1.011*2^2
所以我们计算机中的5.5表示如下
(-1)^0 * 1.011 * 2^2
S=0
M=1.011
E=2
然后类比其他的浮点数,我们也是可以通过SME这三个数很快的还原出来
那么我们内存里也不用存那么多数字了啊,只要存S、M、E即可
ps:对于M和E实际上是存的是和它们相关的值,不是直接存M和E
IEEE 754对有效数字M和指数E,还有一些特别规定。
关于有效数字M
前面说过, 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,即1000 1001。
再比如,2^-1的E是-1,所以保存成32位浮点数时,必须保存成-1+127=126,即0111 1110
解释前面的题目:
#include<stdio.h>
int main()
{
float f = 5.5f;
//5.5转换成二进制是101.1
//(-1)^0 * 1.011 * 2^2
//S = 0
//M = 1.011
//E = 2
//E=2,根据IEEE754,存储在内存中是2+127=129,转换成二进制是1000 0001
//0 10000001 01100000000000000000000
//S E M
//上面的01000000101100000000000000000000转换成十进制就是1,085,276,160
return 0;
}
五、总结
本文介绍了整形和浮点型的数据类型,对于整形又细讲了原码、反码、补码和整形提升相关知识点,整形提升的例题也在文章着重讲解了。对于浮点型则是细讲了IEEE754规定的存储。本文相对而言并不复杂,相信耐心学习的你一定有所收获!