对于数据的存储,重点还是在于对各种二进制的分析和运用吧。这里重点介绍整型。本记录要求之前对整型提升有所了解.
目录
补充-char指的是signed char还是unsigned char?
数据类型介绍
整型在内存中的存储
创建一个变量需要在内存中开辟空间,变量的数据在内存中都以二进制存储。以下来研究整型数据如何在内存空间中实现存储的。
首先引入一个程序,调试时打开内存窗口(调试-窗口-内存),以此看见整型数据如何在内存中存储。
在窗口中可以看见a,b的存储:
a的地址:00 00 00 0a
b的地址:ff ff ff ec
这上面的是16进制情况下的内存,我们要研究存储方式,还要将十六进制转换为二进制。要注意的是这里采用的是大端存储(本部分末尾补充介绍,如有不懂可先转去查看),所以我们分析十六进制要倒着看。( 大端存储举例解释如下图 )
在此我们以b的十六进制地址为例,进行进制转换练习。
f —— 十进制的15 —— 二进制的 1111
e —— 十进制的14 ——二进制的 1110
c —— 十进制的12 ——二进制的 1100
将它们按位置放入b地址,得到二进制的地址表达式:11111111 11111111 11111111 11101100
同理可得a的二进制地址:00000000 00000000 00000000 00001010
而我们可以知道a( =10 )的原码、反码、补码均为00000000 00000000 00000000 00001010;
对于b( = -20 )来说:
原码 | 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0100 |
反码 | 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110 1011 |
补码 | 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110 1100 |
根据红蓝标出的二进制数进行比较,可以发现内存里存放的都是补码。再者, 计算机进行运算时, 也是用补码进行的.
为什么用补码计算?
这一部分算是领略计算机之美了,补码很神奇。就拿1-1的计算为例。
1的补码: 00000000 00000000 00000000 00000001
-1的补码:111111111 111111111 111111111 111111111
当使用二者的补码进行相加后,可得到结果:00000000 00000000 00000000 00000000,将这一补码转换成原码可以发现就是0,即1-1=0。计算机只执行加法计算,它会将1-1转化为1+(-1),而用补码计算可以同时处理操作数的正负和它的值。
(源自比特网课课件)
补充知识:大小端
什么是大小端
大端字节序和小端字节序是程序存储内存的两种方式。大端倒着存进去倒着取出来,小端正着存进去,正着取出来。存放和排序的单位都是字节。
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
比如一个int型变量a=0x1234 5678 ,且假设低地址在左,过渡到高地址在右。
那么78就是数据的低位,12就是数据的高位。
大端存储 | 低地址 | 78 | 56 | 34 | 12 | 高地址 |
小端存储 | 低地址 | 12 | 34 | 56 | 78 | 高地址 |
如何确定当下编译器是大端or小端?
编写一个小程序就可以确定。 拿本人使用的VS2022为例,程序如下:
int main()
{
int a = 0x12345678;//写一个十六进制数以便在内存窗口分析
return 0;
}
- 在内存窗口输入&a后,找到a地址处的内存。由于a地址最后是“C4”,它下一行地址最后是"C8"(一个地址占4个byte),不难看出地址在由低到高变化,即左边低地址,右边高地址。
- a里的78为低位,存放在左边——低地址处。可以知道当前编译器采用的是小端模式。
练习
跟着网课,在此把课上的几道题放在这分析一下。
练习一-大小端程序
前半部分简述概念,可详见前文对大小端的讲解。重点在如何设计程序。
本题实质上是要判断当前机器是大端还是小端,这就关系到每个字节在内存中是以什么顺序存放的。为了能分析到每个字节,考虑选择"char*"的来创建一个指针变量,因为char*每次只拿出一个字节。比如一个int型变量存储会用到4个字节,我们使用char*可以把它们分别拿出。
而为了得出大端或小端的结论,我们还需要定义一个参数,对参数里的相邻字节进行研究。不妨定义int a=1,1的二进制补码除了最后一位是1,其余都是0,指针解引用后只需判断char* 是0还是1,就可以确定内存存放方式了。
由此写出代码如下:
int main()
{
int a = 1;//a的补码:00000000 00000000 00000000 00000001
char* p = (char*)&a;
//为了让*p可以接受地址,需要把&a由int*强制转化为char*。
//(转化前后取得的a的地址其实是一样的)
//如果机器字节序为大端字节序,*p得到的字节是00
//如果机器字节序为小端字节序,*p得到的字节是01
if (*p == 0)
//如果*p是0,说明低地址存放了0,而0在上述补码里处于高位,高位放在低地址,是大端
printf("大端\n");
else
//如果*p是1,说明低地址存放了1,而1在上述补码里处于低位,低位放在低地址,是小端
printf("小端\n");
return 0;
}
而当我们进一步改进程序,专门创建函数来检测大小端,可以得到改版如下:
int check_sys()
{
int a = 1;
char* p = (char*)&a;
return *p;//返回1-小端,返回2-大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
最后得到结果为:
练习二-截断和整型提升
本题求a, b, c的输出分别为多少
分析该题, 首先将一个整型数据-1 放入char型变量里面,然后又将char型变量整形输出. 关键在于一个32位的整型放进去只读取了低位的8位二进制, 发生截断; 而后又要将8位补齐为32位进行整型输出.
int main()
{
//-1的原码:100000000 00000000 00000000 00000001
//-1的反码:111111111 11111111 11111111 11111110
//-1的补码:111111111 11111111 11111111 11111111
char a = -1;
//a获取到的-1的数据只有后8位:11111111
//a虽然前面没有标注signed,但也是有符号型,将最高位1识别为负号
signed char b = -1;
//b获取到的-1的数据只有后8位:11111111
//b前标注signed,是有符号型,将最高位1识别为负号
unsigned char c = -1;
//a获取到的-1的数据只有后8位:11111111
//c前标注unsigned,无符号型,将最高位1识别为二进制数字
printf("a=%d, b=%d, c=%d\n", a, b, c);
//要将char型数据用来int型打印,需要整型提升——有符号数用首位补齐,无符号用0补齐
//对于a, b来说,
//整型提升后存储的补码为11111111 11111111 11111111 11111111
//因此a, b最后打印-1
//对于c来说,
//整型提升后存储的补码为00000000 00000000 00000000 11111111
//因为补码最高位为0,表示正数,正数的原码、补码、反码相同
//因此c最后打印255
return 0;
}
最终得到结果如下:
个人认为本题重点在于对无符号型的理解, 以及对截断和整型提升的应用.
补充-char指的是signed char还是unsigned char?
c语言标准并没有规定, 这取决于编译器. 而在vs2022里面, char是指signed char .
练习三
和上一道题同类型, 这里研究的是无符号整型输出.
最开始自己分析该题时, 在将char整型提升的思路上出了问题: 先将char 变为unsigned char , 而后提升为整型( 用0补齐 ). 这个思路在先后顺序上搞错了, %u是按照无符号整型输出, 应该先有整形, 再化为无符号. 正确的应该是是先将char整型提升( 用符号位补齐 ), 而后转换成unsigned int.
int main()
{
char a = -128;
printf("%u\n", a);
//%u——无符号整型输出
//-128的原码:10000000 00000000 00000000 10000000
//-128的反码:11111111 11111111 11111111 01111111
//-128的补码:11111111 11111111 11111111 10000000
//截断后,a收到的:10000000
//打印时,思路上是先将a提升为整型,再变成无符号整型输出
//而a是signed char,所以char整型提升时应该用符号位补齐
//整型提升后:11111111 11111111 11111111 10000000
//无符号整型打印时,最高位看作二进制数,而正数补、原码相同
//最终输出为4,294,967,168
return 0;
}
最后得到结果为:
举一反三
将-128 改为128 传给a, 最后输出结果又是多少?
char a = 128;
//128补码:00000000 00000000 00000000 10000000
//a截断得到的:10000000
//打印时,先整型提升:11111111 11111111 11111111 10000000
//以无符号整型打印后,结果依然为4,294,967,168
printf("%u\n", a);
return 0;
练习四
同类型题. 当有符号数和无符号数一起运算后, 最终输出是什么呢?
该问题关键在于知道两数相加时, 是一同化为signed int进行运算, 还是一同化为unsigned int 进行运算. 需要知道的是, 计算机在处理两个不同类型的数相加时, 哪个能表示更大的数就转为哪个类型, 这也就是算术转换.
因此本题中, int的最高位是符号位( int的表示范围为-2147483648~+2147483647, 即 -2^31 ~ 2^31-1 ), 而unsigned int将最高位作为二进制位( unsigned int的表示范围为0~4294967295, 即 0 ~ 2^32-1 ), 后者显然可以表示更大的数.
因此本题的思路是, 先将i和j都化为无符号整型, 二者补码相加, 最后再以整型的形式输出.
int main()
{
int i = -20;
//i的原码:10000000 00000000 00000000 00010100
// 反码:11111111 11111111 11111111 11101011
// 补码:11111111 11111111 11111111 11101100
unsigned int j = 10;
//j的补码:00000000 00000000 00000000 00001010
printf("%d\n", i + j);
//先用补码将两数相加,得:11111111 11111111 11111111 11110110
//再用整型输出,而整型负数的原码=(补码-1)取反
//所以通过分析i+j原码:10000000 00000000 00000000 00001010,可知最后打印为-10
return 0;
}
练习五
本题进一步考察对于无符号数的理解.
乍一看这个题, 好像就只是打印出来 9876543210 罢了. 但是仔细分析, 由于i 是无符号整型, 而无符号整型的表示范围是 0~2^32-1, 最小只可能是0.
因此i 恒大于0, for循环无法跳出, 结果是死循环.
就本题再思考一下, 当i 取0之后再进行 i-- , i 的下一个值会是多少?
打开程序监视窗口,可以看见
i 之后取得的数字是无符号整型的最大数, 它的补码全为1. 这是不是可以说明无符号整型在 0 - 1后进行了向高位取1?
练习六
带着上一题的疑问,我们继续研究下一题。 本题求a字符串的长度。
初步分析可以看出,a的数值是从-1,-2,-3......不断下降的。而char是有表达范围的,对此我们可以对char表示范围具体是多大展开研究。
有了这个理解,再回来分析题目。那么可以看出a数组里面元素存储的值应该也是不断轮回的。就像是将上图圆圈上的值逆时针取出,即a里的值:
-1, -2, -3, ...... -126, -127, -128, 127, 126, ...... , 2, 1, 0, -1, -2 ...
最后输出的是strlen(a),已知strlen测量字符串长度时,遇到\0就会停止测量。又因为\0的ASCII码值就是0。所以strlen测的长度就是当a[i] 等于0 前的总长。易知a数组里第一个0前的字符串长度为128+127=255.
所以最后输出结果如下:
通过对char的取值范围进行分析,我们可以举一反三思考无符号数的补码不断-1,是否最终也会形成取值上的循环。
其实,当无符号00000000 00000000 00000000 00000000 再减1,取位后就又会得到11111111 11111111 11111111 11111111,也就是4294967295。实现从unsigned int的最小值变为最大值。这也是一个“循环”。
练习七
最后一道题。判断程序会打印多少次。
肯定是无数次,无符号char型,取值范围在0 ~ 2^8-1。根据上道题得出的结论,举一反三,对i 不断加1 也会让补码形成循环。
最后结果就是死循环。