C语言学习过程化记录-4

对于数据的存储,重点还是在于对各种二进制的分析和运用吧。这里重点介绍整型。本记录要求之前对整型提升有所了解.

目录

数据类型介绍

整型在内存中的存储

为什么用补码计算?

补充知识:大小端

什么是大小端

如何确定当下编译器是大端or小端?

练习 

练习一-大小端程序

练习二-截断和整型提升

补充-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就是数据的高位。

大端存储低地址78563412高地址
小端存储低地址12345678高地址

如何确定当下编译器是大端or小端?

编写一个小程序就可以确定。 拿本人使用的VS2022为例,程序如下:

int main()
{
	int a = 0x12345678;//写一个十六进制数以便在内存窗口分析
	return 0;
}
  1. 在内存窗口输入&a后,找到a地址处的内存。由于a地址最后是“C4”,它下一行地址最后是"C8"(一个地址占4个byte),不难看出地址在由低到高变化,即左边低地址,右边高地址。
  2. 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 也会让补码形成循环。

最后结果就是死循环。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值