1.数据类型详细介绍
前面我们已经学习了基本的内置类型以及它们所占存储空间的大小:
char //字符数据类型 1个字节
short //短整型 2个字节
int //整型 4个字节
long //长整型 4 / 8个字节
long long //更长的整型 8个字节
float //单精度浮点型 4个字节
double //双精度浮点型 8个字节
注意:C语言只规定了sizeof ( long ) >= sizeof ( int )
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
- 如何看待内存空间的视角。
类型的基本归类
整型家族:
char
signed char
unsigned char
short
signed short [ int ]
unsigned short [ int ]
int
signed int
unsigned int
long
signed long [ int ]
unsigned long [ int ]
long long
signed long long [ int ]
unsigned long long [ int ]
注意:字符在内存中本质上存的是ASCLL码值,因此char归到整型家族。
浮点数家族:
float
double
构造类型(自定义类型):
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
指针类型:
int * p;
char * p;
float * p;
void * p;
结构体指针
空类型 :
void 表示空类型( 无类型 );
通常应用于函数的返回类型、函数的参数、指针类型。
2.整型在内存中的存储
一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
int main()
{
int num = 0;//创建一个整型变量,叫num
//num向内存申请4个字节来存放数据
return 0;
}
2.1原码、反码、补码
计算机中的整数(整数的存储类型是整型,1个整型是4个字节,32个比特位。)有三种2进制表示方法,即原码、反码、补码。
三种表示方法均有符号位和数值位两部分。第一位是符号位,1代表负数,0代表正数。剩下的就是数值位。
1.正整数的原码、反码、补码是相同的
原码:根据正整数直接写出二进制序列
2.负整数的原码、反码、补码需要计算
原码:根据正整数直接写出二进制序列
反码:符号位不变,其他位按位取反
补码:反码+1
对于整型来说:数据在内存中是以二进制补码的形式进行存储的。
原因如下:
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值位统一处理。
同时,加法和减法也可以统一处理(CPU只有加法器)。此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
2.2大小端字节序介绍及判断
字节序:以字节为单位,讨论存储顺序的
大端(存储)模式:指把一个数据的低位字节的内容保存在内存的高地址中,而数据的高位字节的内容保存在内存的低地址中;
小端(存储)模式:指把一个数据的高位字节的内容保存在内存的高地址中,而数据的低位字节的内容保存在内存的低地址中。
为什么会有大小端之分?
因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应一个字节,一个字节为8个比特位。但是在C语言中,除了8比特位的char,还有16比特位的int,32比特位的short等。对于这些位数大于8位的处理器,比如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,也就导致了大端存储模式和小端存储模式。
举例说明大端存储模式和小端存储模式:
// 0x11223344 0x表示十六进制数字
//二进制转换为十六进制:每四位二进制序列组成一个十六进制数字
//一个字节八个比特位,八位二进制数字
//即11占一个字节,22占一个字节……
//类比十进制数字的个位十位百位
//可得到十六进制的低位就是44,高位是11
//大端字节序存储:
//11 22 33 44
//小端字节序存储:
//44 33 22 11
例题:写一个程序来判断当前机器的字节序
int main()
{
int a = 0x11223344;
char* p = (char* )&a;//强制类型转换
if (*p == 44)
printf("小端");
else
printf("大端");
return 0;
}
在进行练习之前,我们先来复习一下整型提升
int main()
{
//负数的整型提升
char c1 = -1;
//变量c1的二进制(补码)中只有8个比特位
//11111111
//因为char是有符号的char
//因此整型提升的时候,高位补充符号位,即为1
//提升后的结果就是
//11111111111111111111111111111111
//正数的整型提升
char c2 = 1;
//变量c2的二进制(补码)中只有8个比特位
//00000001
//因为char是有符号的char
//因此整型提升的时候,高位补充符号位,即为0
//提升后的结果就是
//00000000000000000000000000000001
//无符号整型提升,高位补0
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;
}
请问程序输出的结果是什么?
运行结果:
解释如下:
char a = -1;
//原码:10000000000000000000000000000001
//反码:11111111111111111111111111111110
//补码:11111111111111111111111111111111
//截断:11111111(补码)
signed char b = -1;
//原码:10000000000000000000000000000001
//反码:11111111111111111111111111111110
//补码:11111111111111111111111111111111
//截断:11111111(补码)
unsigned char c = -1;
//原码:10000000000000000000000000000001
//反码:11111111111111111111111111111110
//补码:11111111111111111111111111111111
//截断:11111111(补码)
printf("a = %d,b = %d,c = %d", a, b, c);
//%d——以十进制的形式打印有符号的整数
//要发生整型提升
//a : 11111111111111111111111111111111(补码)需要转换为原码打印 -> -1
//b : 11111111111111111111111111111111(补码)需要转换为原码打印 -> -1
//c : 00000000000000000000000011111111(补码)需要转换为原码打印 ->255
练习题二:
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
请问程序输出的结果是什么?
运行结果:
解释如下:
char a = -128;
//10000000000000000000000010000000(原码)
//11111111111111111111111101111111(反码)
//11111111111111111111111110000000(补码)
//截断:10000000
printf("%u\n", a);
//%u以十进制的形式打印无符号的整数
//整型提升:
//11111111111111111111111110000000(补码)
//char 类型最高位是符号位,所以整型提升的时候补的是1
//易错点:无符号位的数字相当于正整数,补码就是它的原码
练习题三:
#include<stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
运行结果:
解释如下:
char a = 128;
//000000000000000000000000010000000(原码、反码、补码均相同)
//截断:10000000
printf("%u\n", a);
//%u以十进制的形式打印无符号的整数
//整型提升:
//11111111111111111111111110000000(补码)
//char 类型最高位是符号位,所以整型提升补的是1
//易错点:无符号位的数字相当于正整数,补码就是它的原码
练习题四:
#include<stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
运行结果:
解释如下:
int i = -20;
//10000000000000000000000000010100
//11111111111111111111111111101011
//11111111111111111111111111101100
unsigned int j = 10;
//00000000000000000000000000001010
printf("%d\n", i + j);
//11111111111111111111111111101100
//00000000000000000000000000001010
//11111111111111111111111111110110(补码)
//11111111111111111111111111110101(反码)
//10000000000000000000000000001010(原码) -> -10
练习题五:
#include<stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
运行结果:陷入死循环
解释如下:
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
//%u——以十进制的形式打印无符号的整数dang
//i = 9—— i = 0
//会打印9、8、7、6、5、4、3、2、1、0
//当i = -1 时
//原码:10000000000000000000000000000001
//反码:11111111111111111111111111111110
//补码:11111111111111111111111111111111
//%u 是无符号的,因此它会将-1的补码打印出来一个特别大的数字,然后递减
//因此会陷入死循环
练习题六:
#include<stdio.h>
#include<string.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
运行结果:
解释如下:
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));//strlen 求字符串长度,统计的是\0之前的字符的个数
//\0 的 ASCLL 是 0
//char 类型可存的数字大小是 -128~127
练习题七:
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i < 255; i++)
{
printf("Hello world\n");
}
return 0;
}
运行结果:陷入死循环
解释如下:
unsigned char i = 0;//全局变量
//char 类型的范围是 0~255
int main()
{
for (i = 0; i <= 255; i++)//永远小于255
{
printf("Hello world\n");
}
return 0;
}
3.浮点型在内存中的存储
常见的浮点数
3.1415926
1E10 1.0 * 10^10
浮点数家族包括:float 、double 、long double 类型
浮点数表示的范围:在头文件中 <float.h> 有定义
为什么叫浮点数呢?
浮点数其实就是数学中的小数
3.1415926 == 31.415926 * 10^(-1)
在科学计数法的作用下,小数点是可以浮动的,因此称为浮点数
3.1一个例子
#define _CRT_SECURE_NO_WARNINGS
#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;
}
大家猜猜输出结果是什么?我猜大多数人猜想的结果如下:
9
9.000000
9
9.000000
让我们来运行一下,来验证我们的猜想吧
这似乎和我们的猜想并不相同,num 和 *pFloat 在内存中明明是同一个数字,为什么浮点数和证书的解读结果会不一样呢?这是为什么呢?接下来,让我们来学习浮点数在内存中的存储规则,来解决我们的疑惑吧。
2.2浮点数在内存中的存储规则
根据国际标准 IEEE (电器和电子工程协会)754 规定,任意一个二进制浮点数 V 可可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^S 表示符号位,当 S = 0 时,V 为正数;当 S = 1 时,V 为负数
- M 表示有效数字,大于等于1,小于2
- 2^E 表示指数位
举例如下:
9.0
//二进制序列是 1001.0
//相当于 1.001 * 2^3
按照 (-1)^S * M * 2^E 的格式
(-1)^0 * 1.001 * 2^3
S = 0
M = 1.001
E = 3
IEEE 754 规定:
对于 32 位的浮点数 ( float ) ,最高的 1 位是符号位 S ,接着的 11 位是指数 E,剩下的 52 位为有效数字 M,图示如下:
对于 64 位的浮点数 ( double ) ,最高的 1 位是符号位 S ,接着的 8 位是指数 E,剩下的 52 位为有效数字 M,图示如下:
IEEE 754 对有效数字 M 和指数 E,还有一些特别规定。
有效数字 M :
在计算机内部保存 M 时,由于M 总是大于等于 1 、小于 2,因此数字的第一位总是 1 ,所以可以被舍去,只保存小数点后面的数字。
举例如下:
9.0
//二进制序列是 1001.0
//相当于 1.001 * 2^3
按照 (-1)^S * M * 2^E 的格式
(-1)^0 * 1.001 * 2^3
S = 0
M = 1.001
E = 3
因此,在计算机内部 M 保存的就是 001
等到读取的时候,再把第一位的 1 加上去。这样做的目的:是节省 1 位有效数字,使精度可以更加准确。
指数 E :
E 是 一个无符号的整数 ( unsigned int ),意味着:当 E 是 8 位时,它的取值范围是 0 ~ 255 ;当 E 是11 位时,它的取值范围是 0~2047。但是科学计数法中,指数是可以出现负数的。因此,IEEE 754 规定,存入内存的时候,E 的真实值必须再加上一个中间数。对于 8 位的 E ,这个中间数是127;对于 11 位的 E,这个中间数是1023。
举例如下:
9.0
//二进制序列是 1001.0
//相当于 1.001 * 2^3
按照 (-1)^S * M * 2^E 的格式
(-1)^0 * 1.001 * 2^3
S = 0
M = 1.001
E = 3
//在 32 位的浮点数 ( float ) 中
在保存 E 时,要给 3 + 127 = 130;
130 的二进制序列是 10000010
即在内存中存储的就是 10000010
//在 64 位的浮点数 ( float ) 中
在保存 E 时,要给 3 + 1023 = 1026;
1026 的二进制序列是 10000000010
即在内存中存储的就是 10000000010
证明如下:
在 32 位的浮点数 ( float ) 中
在 64 位的浮点数 ( float ) 中
然而,指数 E 从内存中取出还可以分成三种情况:
1.E 不全为 0 或不全为 1
浮点数采用指数 E 的计算值减去 127(或1023),得到真实值
2. E 全为 0
浮点数的指数 E 等于 1 - 127(或者 1023)即为真实值。
有效数字 M 不再加上第一位的 1 ,而是还原为 0.xxxxx 的小数,这样做是为了表示 ±0,以及接近于 0 的很小的数字。
3.E 全为 1
如果有效数字 M 全为0,表示 ± 无穷大(正负取决于符号位 S)
浮点数在内存中的存储讲解到此为止,接下来我们在返回看刚开始的那个例子:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;//强制类型转换为float
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;
}
运行结果:
解释如下:
int n = 9;
//00000000000000000000000000001001
float* pFloat = (float*)&n;//强制类型转换为float
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);//*解引用操作符
//*pFloat认为9的二进制序列就是浮点数
//0 00000000 00000000000000000001001
//S E M
//根据(-1)^S * M * 2^E
//符号位 S 为 0
//指数 E 为 -126( E 在内存中全为 0)
//有效数字 M :0.00000000000000000001001
//相乘过后数字极小
//%f只能打印小数点后 6 位
//因此打印结果就是0.000000
*pFloat = 9.0;
//1001.0
//相当于 1.001 * 2^3
//(-1)^0 * 1.001 * 2^3
//S = 0
//M = 1.001
//E = 3
//由前面可知,最后存进内存的二进制序列是
//01000001000100000000000000000000
//即2^20+2^24+2^30=1091567616
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
数据在内存中的存储讲解到此结束,大家有什么疑惑,欢迎在评论区继续和我探讨。