C语言内置的数据类型:
char //字符类型
short //短整型
int //整型
long //长整型
long long //更长的整型
float //单精度浮点数类型
double //双精度浮点数类型
类型的意义:
1.使用类型来创建变量,在内存中开辟一块空间(类型的大小决定了开辟空间的大小)
2.不同的数据有不同的类型,使操作更加灵活
整型:
char
unsigned 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
浮点数类型:
float
double
构造类型:
数组类型
结构体类型 struct
枚举类型 enum
联合体类型 union
指针类型:
char* pc
short* ps
int* pi
long* pl
float* pf
double* pd
空类型:
void
void类型可用于函数的参数,返回的类型,指针的类型。
前面说过,类型可以创建一个变量,并在内存中开辟相应大小的空间,那么数据在内存中是怎么存储的呢?
要先了解几个概念:原码、反码和补码
计算机中的整数有3种表示的方法,分别是原码、反码、补码。
3种表示方法均有符号位和数值位两部分,符号位用‘0’表示正,用'1'表示负。数值位则是具体的数值转化为二进制位。符号位占一个位,数值位占31个位。
(图中一个方块为一个bit位)
原码:
将数值按照正负数的形式转化为二进制位
反码:
将原码的符号位不变,其他位按位取反
补码:
反码加1就是补码
例如
-10的原码为10000000000000000000000000001010
反码为11111111111111111111111111110101
补码为11111111111111111111111111110110
20的原码为00000000000000000000000000010100
反码为01111111111111111111111111101011
补码为01111111111111111111111111101100
正数的原码、反码、补码都相同。
虽然整数有3种的表示方法,但是整数在内存中只存储它的补码。
原因如下:
在计算机系统中,整数的数值一律用补码来表示和存储,我们输入时其实输入的是原码。使用补码可以将符号位和数值位统一处理;
加法和减法也可以统一处理(CPU只有加法器),而且补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
接下来看看-10和20在内存中的存储
可以看到内存中存放的是补码,但是顺序好像有点不太对。
这里又引入一个概念,叫做大小端存储模式。
什么是大端小端:
大端存储模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的高地址中;
小端存储模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的低地址中;
(这里的低位或者高位都至少为1个字节即8个bit位的大小)
例如0xff112233的高位是0xff,而低位是0x33
为什么会有大端小端:
在计算机地址中,是以字节为单位的,每个地址单元都对应着一个字节,而一个字节为8bit位。但是在C语言中除了8bit的char,还有16bit和short和32bit的long(大小根据具体的编译器决定),对于位数大于8位的处理器,比如16位和32位的处理器,由于寄存器宽度大于1个字节,那么必然存在着如何将多个字节安排的问题,所以就产生了大小端的存储模式。
这里举个例子,用short类型创建一个变量a,假设其值为0x1122,&a为0x0001
大端存储模式如图(一个小方块为一个地址单元,即一个字节大小)
小端存储模式如图(一个小方块为一个地址单元,即一个字节大小)
我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。而有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
设计一个小程序来判断当前机器的字节序
思路:创建一个变量a并赋值1存入内存中,其16进制为0x00000001,如果是小端模式,那么a的首地址里存放的是0x01,而如果是大端模式,那么a的首地址里存放的是0x00,对于如何访问a的首地址里的空间,可以用char类型的指针。
代码:
#include
int main()
{
int a = 1;
char* p = (char*) & a;
int ret = *p;
if (ret == 1)
{
printf("小端");
}
else
{
printf("大端");
}
return 0;
}
也可以将对大小端的判断写成一个int类型的函数,是大端,那么返回0,是小端,那么返回1
#include
int check()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端");
}
else
{
printf("大端");
}
return 0;
}
1.输出结果是什么?
#include
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;
}
-1的补码是11111111111111111111111111111111,将这个数放入char类型的a中,要进行截断,因为a只有一个字节即8个位的大小,所以最后a中存放的是11111111
以此类推,可以知道b中存放的是11111111,c中存放的是11111111,有所区别的是,a和b的最高位都是符号位,c则是无符号数。
而打印的时候是以%d的形式打印的,%d打印的是有符号的整数,计算机认为a,b,c都是有符号整数,所以a,b,c都要发生整型提升,对于a来说,a是有符号数,整型提升的时候看符号位是0还是1,是1的话高位补1,是0的话高位补0,所以a整型提升后为11111111111111111111111111111111,b也是有符号数,所以和a的整型提升的结果一样,而c是无无符号数,整型提升的时候高位直接补0就可以了,所以c整型提升的结果是00000000000000000000000011111111,而这些都是补码,是在计算机中发生的运算,最终打印出来我们看到的是原码,所以要转换为原码。
a的原码为10000000000000000000000000000001
b的原码为10000000000000000000000000000001
c的原码为00000000000000000000000011111111
最终输出的结果为a=-1,b=-1,c=255
2.
2.
#include
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
这里也是用一样的方法,先把-128的补码11111111111111111111111110000000写出来,然后截断成10000000放入a中,%u打印的是无符号整数,按照a的类型来发生整型提升为11111111111111111111111110000000,最后打印。
结果为4,294,967,168
3.
#include
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
128的补码是00000000000000000000000010000000,截断后是10000000,整型提升后为11111111111111111111111110000000,%u会将其格式化为无符号数然后打印,所以结果为4,294,967,168
4.
#include
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
整数存入整型中不用截断,因为都是4个字节的大小,而 i + j 的运算可以转化为补码进行运算,最后格式化为有符号整数并打印。
-20的补码为11111111111111111111111111101100
10的补码为00000000000000000000000000001010
相加为1111 1111 1111 1111 1111 1111 1111 0110
按照%d的形式打印,%d会认为其是个有符号数,所以还要把他的原码写出来,是
1000 0000 0000 0000 0000 0000 0000 1010
所以结果是-10
5.
#include
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
对于这个题,可以画一个图来理解
对于unsigned int 类型来说,它的原码、反码、补码都是一样的,图中按照顺时针的顺序,它的取值范围是0——4,294,967,295(这里换算成十进制,图中是二进制)。
而对于int 类型来说,从补码到原码需要换算(图中写的是补码),所以它的取值范围是 -2,147,483,648——2,147,483,647
题中当循环 i 从0开始减1的时候,相当于在内存中 i 的补码由00000000000000000000000000000000变成了11111111111111111111111111111111,所以 i 会始终保持在>=0的范围内。
因此输出结果为死循环并且一直按顺序打印数字。
6.
#include
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
最终输出的是strlen(a),所以要在数组a中找到’\0‘的位置。
同样的可以画一个char类型的取值范围图来理解
-1-i的循环即数字按照图中逆时针来变化(-128减1变成127),当i为255的时候,存入a[255]中的数据刚好为0,根据ASCII码表,0的值对应的就是'\0',所以strlen计算的是0——254的字符个数,为255。
7.
#include
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
unsigned char类型的取值范围图为
所以运行结果为死循环打印hello world\n
#include
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表示符号位,当S为0,V(浮点数)为正数,当S为1,V为负数
M表示有效数字,范围为大于等于1,小于2
2^E中的E表示指数位
举个例子:
十进制的5.0写成二进制是101.0,即(-1)^0*1.01*2^2,则S = 0, M = 1.01, E = 2
十进制的-5.0写成二进制是-101.0,即(-1)^1*1.01*2^2,即S = 1, M = 1.01, E = 2
IEEE75规定,对于32位的浮点数,最高的1位是符号位S,接着的8位是指数位E,剩下的23位是有效数字位M
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数位E,剩下的52位是有效数字位M
IEEE 754对有效数字M和指数E,还有一些特别的规定
对于有效数字M
M的取值范围是1<=M<2,所以M可以写成1.XXXXXX的形式,其中XXXXXX表示小数部分。
IEEE 754规定,在计算机内部保持M时,默认这个数的第一位总是1,所以这个数可以被舍去,只保存后面的XXXXXX部分。
例如在保存1.01的时候,只保存小数点后面的01,等到读取出来的时候,再把第一位的1加上去,这样做的目的,是可以节省1位有效数字,可以提高精度。以32位浮点数为例,用以保存M的位数只有23位,这样舍去第一位后,可以保存24位有效数字。
对于指数E,情况比较复杂
首先,E是一个无符号整数
E如果是8位,那么取值范围是0——255,E如果是11位,取值范围是0——2047。但是科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值,必须要再加上一个中间数,对于8位的E,这个中间数是127,对于11位的E,这个中间数是1023.
例如E为10的时候,要先加上127,将137即1000 1001存入E中
然后,将指数E从内存中取出也可以分为3种情况:
1.E不全为0或不全为1
指数E要减去127(或1023),得到真实值,再在有效数字M前加上第一位的1.
2.E全为0
这时,指数E等于1-127(或1-1023)得到真实值,而有效数字M前不再加上第一位的1,而是还原为0.XXXXXX的小数。这样做是为了表示±0,以及接近于0的很小的数字。
3.E全为1
这时,如果有效数字M全为0,表示±无穷大
关于浮点数的规则,就讲到这里了。
接下来看看开始的那道题目
#include
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;
}
int n = 9 将9存入n中,是将整数按照整型的方式进行存储,然后float* pFloat = (float*)&n是以浮点数的类型对n这块空间进行访问,9存放在内存中的补码是00000000000000000000000000001001,按照浮点数存储规则,可以知道S = 0,E = 00000000, M = 00000000000000000001001.
对此,浮点数V可以写成:
V = (-1)^0*0.00000000000000000001001*2(-126) = 1.001*2^(-146),以%f的形式打印即为0.000000.
对于*pFloat = 9.0,是以浮点数的类型进行存储,所以要对9.0进行一个转换
9.0(十进制) --> 1001.0(二进制) --> (-1)^0*1.001*2^3 --> S = 0, M = 1.001, E = 3
而存入内存的时候M要舍去第一位,E要加上127,所以在内存中的形式为
0 1000 0010 001 0000 0000 0000 0000 0000
而后以%d的形式打印,所以第3个结果为1,091,567,616.
我是煎饼,今后也会不定期更新