目录
例2:求输出结果(char、signed char、unsigned char)
例3:下面程序输出结果是什么?(char a = -128 → %u)
例5:代码结果是什么?(int、unsigned int → %d)
例6:代码执行的结果是什么?(unsigned int → for循环)
例8:求代码执行结果(unsigned char → for循环)
注:
本笔记参考B站up鹏哥C语言的视频
数据类型详细介绍
基本的内置类型:
char | 字符数据类型 |
short | 短整型 |
int | 整型 |
long | 长整型 |
long long | 更长的整型 |
float | 单精度浮点型 |
double | 双精度浮点型 |
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围);
- 如何看待内存空间的视角。
类型的基本归类
整型家族
(因为 char 存储的是字符的ASCII值,所以分类时,通常把char类型归为整型)
char | |
unsigned char | |
signed char | |
short | |
unsigned short [ int ] | |
signed short [ int ] | |
int | |
unsigned int | |
signed int | |
long | |
unsigned long [ int ] | |
signed long [ int ] |
整型分为有符号和无符号,其范围定义在 limits.h 中。
---
浮点型家族
float | |
double |
其范围定义在 float.h 中。
---
构造类型(自定义类型)
数组类型 | 如 {int arr1[10]},类型是 {int [10]};{int arr2[5]},类型是{int [5]} |
结构体类型 | struct |
枚举类型 | enum |
联合类型 | union |
---
指针类型
int *pi | |
char *pc | |
float* pf | |
void* pv |
---
空类型
void 表示空类型(无类型)
通常应用于函数的返回类型[void test()]、函数的参数[void test(void)]、指针类型[void *p]
整型在内存中的存储
||| 复习:变量的创建是要在内存空间中开辟空间的。空间的大小是根据不同的类型决定的。
那么数据在所开辟内存中到底是如何存储的?
如:
int main()
{
int a = -10;
int b = 10;
return 0;
}
通过调试可以看见a在内存中的存储:
b在内存中的存储:
这里的表示形式是十六进制。(4个二进制位 → 1个十六进制)
注意:
(数据在内存中是以二进制的形式存储。)
- 但是整数的二进制有3种表示形式:原码、反码 和 补码
- 对于正整数而言,原码、反码、补码相同
- 对于负整数而言,原码、反码、补码是要进行计算的。
比如在内存中放入 -10:
- -10的原码:10000000 00000000 00000000 00001010
- -10的反码:11111111 11111111 11111111 11110101 - 按照数据的数值直接写出的二进制序列,就是原码,原码的符号位不变,其他位按位取反,得到反码
- -10的补码:11111111 11111111 11111111 11110110 - +1
- -10的补码转换为十六进制之后,是 ff ff ff f6,倒置之后,就是f6 ff ff ff。可得结论:整型在内存中存储的是它的补码。
那为什么存储补码呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
ps:如果负整数的补码符号位不变,其他位按位取反,再+1,可以得到原码
例子:1 - 1
由于CPU只有加法器,使用实际上是模拟减法计算。所以 [1 - 1] 实际上是 [1 + (-1)] 。对于-1:
原码:10000000 00000000 00000000 00000001
反码:11111111 11111111 11111111 11111110
原码:11111111 11111111 11111111 11111111如果原码相加,得:
10000000 00000000 00000000 00000010 即 -2 --- 计算错误
如果反码相加,得:
11111111 11111111 11111111 11111111 ,明显不是正确结果
如果补码相加,得:
00000000 00000000 00000000 00000000 即0 --- 计算正确
大小端介绍(大端字节序和小端字节序)
int main()
{
int a = 0x11223344;
return 0;
}
对a:
0x | 12 | 34 | 56 | 78 |
说明数字是十六进制 | 该数位的权重较高,是数据高位 | 数据的权重较低,是数据低位 |
在内存中如果有不同的存储方式(此处数字代表十六进制):
11 22 33 44(大端字节序) | 内存存储数据保存下来的形式1 |
44 33 22 11(小端字节序) | 内存存储数据保存下来的形式2 |
低地址 高地址 | |
44 11 22 33(不方便、复杂) | |
11 44 33 22(不方便、复杂) |
||| 大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
||| 小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
此时在通过调试窗口观察 a 在内存内的存储:
可以发现当前编译器采取的是小端存储的模式。
为什么会有大小端模式之分:
这是因为在计算机系统中,我们是以字节为单位,每个地址单元都对应着一个字节,一个字节是8bit。但是在C语言中除了8bit的char之外,还有16bit的short类型,32bit的long类型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此出现了大端存储模式和小端存储模式。
例1:设计一个小程序来判断当前机器的字节序
- 选定一个简单数字,如:1 - 00 00 00 01;
- 则小端存储的第一个字节就是 01
- 大端存储的第一个字节就是 00
- char* 的指针就是访问一个字节
#include<stdio.h>
int main()
{
int a = 1;
char* p = (char*)&a;//把int类型强行转换
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
或者使用函数:
#include<stdio.h>
int check_sys()
{
int a = 1;
char* p = (char*)&a;
return *p;//返回1 - 小端 返回0 - 大端
}
int main()
{
//例:设计一个小程序来判断当前机器的字节序
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
例2:求输出结果(char、signed char、unsigned char)
#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;
}
打印结果:a=-1,b=-1,c=255
解析:
-1放入内存中,放入的是补码 --- 11111111 11111111 11111111 11111111
但是char类型大小是一个字节 - 8bit:
- 所以 a 中存入的是 11111111 --- 认为最高位是符号位
- b 有符号,所以放入 b 中的是 11111111 --- 认为最高位是符号位
- c 是无符号的,c 中存入的也是 11111111 --- 认为没有符号位
而在执行打印时,是要求[%d],所以需要整型提升,a 和 b 有符号,要按照符号位提升,所以提升后,成为:11111111 11111111 11111111 11111111;
但是 c 无符号位,高位默认补0,整型提升后,成为:00000000 00000000 00000000 11111111,最高位是0,认为是正数,补码就是原码,打印 11111111 对于的十进制数 - 255。
补充:
- char 到底是[signed char]还是[unsigned char],C语言并没有规定,取决于编译器;
- 但是C语言有规定:int 就是[signed int]、short 就是[signed short]。
例3:下面程序输出结果是什么?(char a = -128 → %u)
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
(%u - 打印一个无符号数)
打印结果:4294967168
解析:
- -128的原码:10000000 00000000 00000000 1000000
- -128的反码:11111111 11111111 11111111 01111111
- -128的补码:11111111 11111111 11111111 10000000
由于char类型,所以 a 中存入的是 10000000。a 是有符号的,所以在打印(整型提升)时,最高位是补1:11111111 11111111 11111111 10000000。
但是,%u 认为最高位不是符号位,此时认为补码就是原码,于是[11111111 11111111 11111111 10000000]未经转换,直接被当中原码打印,直接转换为十进制:4294967168。
ps:
- 如果通过 %d 打印,结果是:-128。
- 129的原码:00000000 00000000 00000000 10000001
- 129如果存入char类型中,截断为:10000001
- 此时129的反码:10000000
- 129的补码:11111111 --- 结果是-127
例4:求打印结果(char a = 128 → %u)
#include<stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
打印结果:4294967168
解析:
128的补码和原码相同:00000000 00000000 00000000 1000000
a 中存放的是:10000000,和上题一样,不再赘述。
---
补充:char类型变量的取值范围
8个bit内存放的二进制数:
正数 | 00000000(0) | (一直+1)→ | 01111111(127) |
负数 | 10000000(无法计算,认为是-128) | (一直+1)→ | 11111111(-1) |
01111111(127) +1 → 10000000(-128)
得出结论:有符号的char的取值范围是:-128 ~ 127(无符号的char类型取值范围是0~255,有符号数和无符号数的区间长度是相同的)
所以有符号的char内放入的127以上的数字就会被解读成其他数字。
例5:代码结果是什么?(int、unsigned int → %d)
#include<stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
打印结果为:-10
解析:
-20的原码:10000000 00000000 00000000 00010100
↓
-20的补码:11111111 11111111 11111111 11101100
10的补码:00000000 00000000 00000000 00001010
-20和10的补码相加:11111111 11111111 11111111 11110110 --- 结果的补码,按照[%d]打印,认为有符号,需要转换为原码:10000000 00000000 00000000 00001010
例6:代码执行的结果是什么?(unsigned int → for循环)
#include<stdio.h>
int main()
{
unsigned int i;
for ( i = 9; i >= 0; i++)
{
printf("%u\n", i);
}
return 0;
}
结果:陷入死循环。
解析:
i 是无符号数,i 在任何情况下都不可能小于0。
||| ps:在打印时,变量的符号有无取决于[%u]、[%d]这些格式。
例7:求最终打印的结果(char a[1000])
#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;
}
打印结果:255
解析:
1000次循环后,arr内存放:
-1 -2 -3 …… -127 -128 127 126 125 …… 3 2 1 0 -1 -2 …… -127 -128 127 ……
[strlen(a)] → 求a的字符串长度。strlen在寻找到'\0'(即0)后结束:-1 -2 -3 …… -127 -128 127 126 125 …… 3 2 1 0
例8:求代码执行结果(unsigned char → for循环)
#include<stdio.h>
unsigned char i = 0;
int main()
{
for ( i = 0; i <= 255; i++)
{
printf("Hello world\n");
}
return 0;
}
结果:死循环
解析:
[unsigned char] - 无符号,这里的char的取值范围是:0~255,i 永远满足循环条件。
故使用无符号数时容易死循环。
浮点型在内存中的存储
常见的浮点数:
||| 3.12149 1E10 浮点数家族包括:float、double、long double 类型。浮点数表示的范围:float.h 中定义。(1E10:1.0 * 10^10)
浮点数存储的例子(引例)
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
printf("\n");
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
代码执行结果:
解析:
(强制类型转换是不会改变变量的值的,发生改变的是变量的地址类型)
[int n = 9]开辟了4个字节(4byte)的空间。n的小端存储模式:
首先:
- [printf("n的值为:%d\n", n)],通过整型的视角存入,通过整型的视角取出,没有问题。
- 上面的[printf("*pFloat的值为:%f\n", *pFloat)],通过浮点型的视角存入,通过浮点型的视角取出,也没问题。
再看:
float类型的指针解引用访问4个字节,恰好可以取出 09 00 00 00
- [int n = 9]在定义变量时,是通过整型的视角定义的。
- 而下面的[printf("*pFloat的值为:%f\n", *pFloat)]则是通过整型的视角存入数值,通过浮点数的视角取出数值(pFloat是浮点型指针,*pFloat认为pFloat指向的空间存放的是浮点数(但是存入n时是存入整数),引用的也是[%f],而打印结果却是:0.000000)
---
- [*pFloat = 9.0],改变变量时,是通过浮点型的视角存入[9.0]。
- 而[printf("num的值为:%d\n", n)]是通过整数的视角取出 n 中的数值。
小结:浮点型和整型在内存中的存储方式(和解读方式)是有区别的。
浮点数在内存中的存储方式
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^S表示符号位,当S = 0,V为正数;当s = 1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位。
浮点数十进制转换为二进制时,要根据小数点的存在,往前的整数部分和往后的小数部分分开判断。
举例1:
- 浮点数5.5,转换为二进制就是101.1,其中小数点后面是0.5,也就是1/2^1。
- 而101.1再转换为科学计数法的形式,就是1.011*2^2(注意:这里是二进制)。
- 再表示符号位:(-1)^0 * 1.011*2^2。
总结:浮点数5.5 - 十进制 → 二进制 → 101.1 → 1.011*2^2 → (-1)^0 * 1.011*2^2 → s=0、M=1.011、E=2。
举例2:
- 十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出s = 0,M = 1.01,E = 2。
- 十进制的-5.0,写成二进制是-101.0,相当于-101×2^2。那么,s = 1,M = 1.01,E = 2。
IEEE 754规定:
1.对于32位的浮点数,最高位的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
---
2.对于64位的浮点数,最高的1位是符号位s,接着的11位是指数E,剩下的52位为有效数字M。
IEEE 754对有效数字M和指数E,还有一些特别规定。
前面提过,1 <= M < 2 ,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时:
默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如:
保存1.01时,只保存01,等到读取时,再把第一位的1加上去,这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M的只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。(如果出现23个bit过多的情况,如 M=1.011,则将空位补齐)
至于指数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,即10001001。
例子
int main() { float f = 5.5f; return 0; }
解析
5.5的二进制数字为:101.1
科学计数法:1.011 * 2^2
s=0 M=1.011 E=2
存入内存:s=0 M=011 E=2+127
对应内存的二进制:
0 10000001 01100000000000000000000
→ 0100 0000 1011 0000 0000 0000 0000 0000
对应内存的十六进制:40 b0 00 00开启调试(浮点数的存储也有大小端问题):
然后,指数E从内存中取出还可以分成三种情况:
- E不全为0或不全为1
||| 这时,浮点数就采用下面的规则表示:
即指数E的的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:0.5(1/2)的二进制形式为0.1,由于规定正数的部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1 + 127 = 126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制的表示形式为:
0 011111110 00000000000000000000000
- E全为0
||| 这时,浮点数的指数E等于1~127(或者1~1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
- E全为1
||| 这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。
解释引例
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
printf("\n");
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
解析:
9的二进制序列:00000000 00000000 00000000 00001001 (赋值)→ n
第一个[printf("*pFloat的值为:%f\n", *pFloat)],此时整型输入,浮点型输出:
- 9的二进制序列被认为是浮点数:0(s) 00000000(E) 00000000000000000001001(M) --- 这是存在内存里的。
- 此时E为全0,不补0:0.00000000000000000001001;
- 再乘以E = -126(1-127):0.00000000000000000001001 * 2^(-126) --- 几乎为0,数字太小,没有打印到1出现的位置。
---
[printf("num的值为:%d\n", n)],此时浮点型输入,整型输出:
- 9.0以浮点数的形式写入:1.001 * 2^3 此时E = 3,3 + 127 = 130
- 所以此时9.0的二进制表达是:0(s) 10000010(E) 00100000000000000000000(M)
- 而打印时认为上面的二进制是整数的二进制,此时符号位是0,原码就是补码。原码即:01000001 00010000 00000000 00000000
- 打印结果: