那些C语言的‘坑‘,你拿捏了吗?

本文详细剖析了C语言中容易遇到的问题,包括char变量溢出、数值类型提升与无符号打印的陷阱、else悬空现象、二维数组与指针的关系以及宏定义的理解。通过实例解析,帮助读者深入理解这些知识点,避免编程过程中的常见错误。
摘要由CSDN通过智能技术生成

学完c语言,回过头来看,发现还是有许多点没有get到,就整理了一下写成这篇文章(每一个点都由例题引出,对c语言基础薄弱的小伙伴还是比较友好的.哈哈!)下面一起来看看吧.
在这里插入图片描述

(一).char变量值的溢出问题.

1. 例题

#include<stdio.h>
int main()
{
	char a = 128;
	printf("%d\n",a);
	return 0;
}

看了上面的题,你是不是觉得是打印出128呢?如果你是这样认为的,那就错了(O(∩_∩)O哈哈~),那结果应该打印出什么呢?我们一起来看看吧.
在这里插入图片描述

2. 浅析

  1. 再讲此题之前,我们先来了解下计算机的存储方式,数据在计算机中都是以二进制串的形式存储!字节是内存的基本单位,计算机存储和管理数据以字节为最小单位.我们都知道原码是将数字以二进制形式翻译出,反码是让原码的符号位不变,其他位依次取反,补码是由反码+1得到,对于整形来说,数据存放在内存中时实际上是存放的补码,对于有符号整数,二进制的最高位表示正负,不表示数值,最高位为 0 时表示正数,为 1 时表示负数,正数的原码,反码,补码都相同.
  2. 字符型(char)用于储存字符,如英文字母或标点。严格来说,char 其实也是整数类型,因为 char 类型储存的实际上是整数,而不是字符。而char类型的范围是-128~127,其中-128原码可以这样理解为:1 1000 0000(个人理解!),有9位,最高位符号位,再算它的反码:1 0111 1111,进而,补码为:1 1000 0000,这是 -128 的补码,发现和原码一样,但是在 char 型中,是可以用 1000 0000 表示 -128 的原码的,关键在于char 是 8 位,它把 -128 的最高位符号位 1 丢弃了,截断后-128的补码为 1000 0000,即在c语言中便规定了1000 0000为-128的补码(这点很重要!).
  3. 现在,我们来看上面这道题,我们可以这样理解128=127+1,而127的原码为0111 1111,使其+1,二进制变为1000 0000(这不就是-128的原码吗,同样也为-128的补码),所以当以整数的形式打印出时,应该打印出128(这才是正确答案)为了方便理解,可以看看下面的这张图.

这里是引用

  1. 好了,这道题就先这样哩.但是我们现在再来想一想,如果将上题中的printf语句中的%d改为%u(注意了!!第二个坑出现了)又会发生什么有趣的事呢?这就不得不说第二个问题了.

(二).数值类型的自动提升与无符号打印(unsigned)中的’坑’.

1.例题

#include<stdio.h>
int main()
{
	char a =128;
	printf("%u",a);
	return 0;
}

当你看到这道题时,是不是有点熟悉呢,没错,就是第一道例题(不过动了点手脚,你发现了吗…),那么这道题会打印出什么呢?(嘿嘿!你现在是不是有点懵了呢.),你可能想了一会,还有可能会是-128吗.其实这道题将会打印出一个很大的数字,那究竟是多少呢?我们一起来看上一看吧.
在这里插入图片描述

2.浅析

  1. 上面说了-128的补码为1000 0000,那我们现在来看看128的补码为多少,128的补码为0000 0000 0000 0000 0000 0000 1000 0000(32位),但是a为char类型,只能存8位,要将128存在char类型的a中,便发生截断,即将1000 0000存在了a变量中,但是注意到是用%u打印(表示将右边的变量强制按无符号整数输出),所以首先发生整形提升,而char是认为有符号的,发生提升时便在最前面补充符号位数字,直到32位.所以提升后的值为1111 1111 1111 1111 1111 1111 1000 0000(补码)存在a中,%u无符号整形打印(原码与补码相同),所以最高位的1不再表示符号位,而是表示一个有效位,所以最后打印出的值为4294967168(是不是有点不可思议呢,你可以自己试试哦!).

整形提升分为算数提升逻辑提升,其中算数提升较为常用.算数提升是在二进制的最高位补符号位数字,直到32位;逻辑提升是无论二进制的最高位是什么都只补0,直到32位.

  1. 看了上面的讲解,是不是恍然大悟呢.那现在也应该有能力来做一道相似的题了吧!那现在给出下面的一道例题,一起来做做看吧,欢迎在评论区讨论哈.
#include<stdio.h>
int main()
{
	unsigned int i;
	for(i = 9;i >= 0;i--)
	{
		printf("Hello\n");
	}
	return 0;
}

请问上述代码中一共会打印出多少个Hello呢?

(三).else悬空问题

1.例题

#include<stdio.h>
int main()
{
	int a=5;
	int b=3;
	if(a==0)
		if(b==0)
		printf("NO\n");
	else
		printf("YES\n");
	return 0;
}

2浅析

那么上面这个程序最后会打印什么呢?(其实我故意将第一个if与else对其了!!!),实际上这个程序什么都不会打印出来,是因为C语言有这样的规则,else始终与同一对括号内最近的未匹配的 if 结合。即else与第二个if匹配上了.上面这个代码与下面的代码等同.

#include<stdio.h>
int main()
{
	int a=5;
	int b=3;
	if(a==0)
	{
		if(b==0)
			printf("NO\n");
		else
			printf("YES\n");
	}
	return 0;
}

这样便一目了然了.

(四).二维数组地址,数组内容和指针之间的关系.

1.例题

在学二维数组和指针指向数组内容时,我真的是头都大了,不知道你有没有这种情况呢.(哈哈),下面一起来看看吧!
在这里插入图片描述

#include<stdio.h>
int main()
{	
	int a[4][2]={{2,4},{6,8},{1,3},{5,7}};
	printf("   a=%p ,   a+1=%p\n",a,a+1);
	printf("a[0]=%p ,a[0]+1=%p\n",a[0],a[0]+1);
	printf("  *a=%p ,  *a+1=%p\n",*a,*a+1);
	printf("a[2][1]=%d, *(*(a+2)+1)=%d\n",a[2][1],*(*(a+2)+1));
	printf("sizeof(a[2])=%d\n",sizeof(a[2]));
	printf("sizeof(a[2]+1)=%d\n",sizeof(a[2]+1));
	printf("sizeof(*(a[2]+1))=%d\n",sizeof(*(a[2]+1)));
	return 0;
}

看了上面这道例题,你有什么想法呢,静下心做做看.(如果能把这道题搞懂,二维数组与指针就理解的差不多了),答案在下面.(一定要先做做看哦).
在这里插入图片描述
注:其它系统显示的地址值和地址形式可能不同.

2.浅析

  • 首先,在理解二维数组时可以将其理解为一个一维数组,只不过在这个一维数组中每个元素都是一个一维数组,如int a[4][2]可以理解为是一个一维数组,该数组有4个int类型的元素,每个元素又是一个含有2个int类型的一维数组.
  • printf(" a=%p , a+1=%p\n",a,a+1);
    printf(" a[0]=%p , a[0]+1=%p\n",a[0],a[0]+1);
    printf("*a=%p , *a+1=%p\n",*a,*a+1);
    从这三行代码及其打印的值可知:二维数组a的地址和一维数组a[0]的地址相同,它们的地址都是各自首元素的地址,但它们也有差别.在我们系统中,int为4个字节,a[0]实际上指向一个4字节的对象,因此a[0]+1其值加4,此时a[0]+1a[0][1]的地址,而数组名a是一个内含2个int类型值的数组的地址,所以a指向一个8字节的对象,因此,a+1,它所指向的地址加8个字节.此时a+1a[1][0]的地址.从上面的代码可以看出a[0]和*a效果完全一样,是因为对二维数组名解引用两次,得到储存在数组中的值.
  • printf("a[2][1]=%d, *(*(a+2)+1)=%d\n",a[2][1],*(*(a+2)+1));由打印的值可知,a[2][1]*(*(a+2)+1)等价,a+2是二维数组第三个元素(一个一维数组)的地址,*(a+2)就是二维数组第3个元素(一个一维数组)的首元素(一个int类型的值)地址.*(a+2)+1就是二维数组第3个元素(即一个一维数组)的第2个元素(即一个int类型的值)的地址,故*(*(a+2)+1)就是二维数组第3个一维数组的第2个元素的值(即a[2][1]的值,便是3).为了方便理解,可以画出如下的图.

这里是引用
注:这是以行序为主序存储的(想了解行序和列序存储的区别,可以百度哦!)

  • printf("sizeof(a[2])=%d\n",sizeof(a[2]));a[2]是一维数组的数组名,sizeof(a[2])是将数组名直接放在了sizeof()内,计算的是整个数组的大小即二维数组第3个元素的大小(一个一维数组),故大小是8个字节.
    printf("sizeof(a[2]+1)=%d\n",sizeof(a[2]+1));a[2]+1是二维数组第三个元素(一个一维数组)的第2个元素(一个int类型的值)的地址,相当于a[2][1]的地址.在32位系统中,地址的大小为4个字节,在64位系统中,地址的大小为8个字节.故sizeof算出的大小为8.(64位系统).
    printf("sizeof(*(a[2]+1))=%d\n",sizeof(*(a[2]+1)));现在我们知道了,*(a[2]+1)相当于a[2][1],故这行代码计算的是a[2][1]的大小,是int类型,所以大小为4个字节.

注意:数组名的意义
1.sizeof(数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小.
2.&数组名,这里的数组名也表示整个数组,故取出的是整个数组的大小.
3.除此之外所有的数组名都表示首元素的地址.

(五).对宏定义理解模糊

1.例题

#include<stdio.h>
#define SQUARE(x) x*x
int main()
{
	int a=4;
	printf("%d",SQUARE(a+1));
	return 0;
}

上面这道例题打印出的值是多少呢?我刚开始看时,发现也太简单了吧,那肯定是打印25呀,可当运行出结果时发现打印出的值为9,我当场就懵了.后来去查资料,发现是我没用将宏定义的概念理解到(真是惭愧).那我们现在一起来看看吧.

2.浅析

  1. 首先,我们来了解下宏的定义格式为#define name( parament-list ) stuff,其中parament-list是一个由逗号隔开的符号表,它可能出现在stuff中,如上面的#define SQUARE( x ) x*x.
  2. 当以后的程序中出现SQUARE(x)时,预处理器就会用x*x这个表达式来代替它.假设后面的程序出现SQUARE(4),就会用4*4来代替SQUARE(4).
  3. 现在来看看上面的例题,当程序执行SQUARE(a+1)时,它就会将参数x替换为a+1,所以相当于执行printf("%d",a+1*a+1);即printf("%d",4+1*4+1);答案就为9了.而我们算错是因为自己默认的将a+1计算出4+1=5,然后再代入x*x,即5*5=25.替换文本时,是原样替换(很重要!),我们在计算时,千万不要画蛇添足,将其计算后带入进去了.
  4. 那如果我们想要使打印结果为25,那该怎么办呢?只需要将x*x改为(x)*(x)就ok了,所以,当我们自己在使用宏定义时应该注重括号的使用,以便达到自己期望的结果.

总结:

我相信,当你仔细看完这几道题(并仔细思考的话),这几个’坑’点你一定可以拿捏了.
最后,本文若有错误的地方,麻烦各位大佬在评论区指正.
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值