学完c语言,回过头来看,发现还是有许多点没有get到,就整理了一下写成这篇文章(每一个点都由例题引出,对c语言基础薄弱的小伙伴还是比较友好的.哈哈!)下面一起来看看吧.
文章目录
(一).char变量值的溢出问题.
1. 例题
#include<stdio.h>
int main()
{
char a = 128;
printf("%d\n",a);
return 0;
}
看了上面的题,你是不是觉得是打印出128呢?如果你是这样认为的,那就错了(O(∩_∩)O哈哈~),那结果应该打印出什么呢?我们一起来看看吧.
2. 浅析
- 再讲此题之前,我们先来了解下计算机的存储方式,数据在计算机中都是以二进制串的形式存储!字节是内存的基本单位,计算机存储和管理数据以字节为最小单位.我们都知道
原码是将数字以二进制形式翻译出,反码是让原码的符号位不变,其他位依次取反,补码是由反码+1得到
,对于整形来说,数据存放在内存中时实际上是存放的补码,对于有符号整数,二进制的最高位表示正负,不表示数值,最高位为0
时表示正数,为1
时表示负数,正数的原码,反码,补码都相同. - 字符型(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的补码(这点很重要!). - 现在,我们来看上面这道题,我们可以这样理解128=127+1,而127的原码为0111 1111,使其+1,二进制变为1000 0000(这不就是-128的原码吗,同样也为-128的补码),所以当以整数的形式打印出时,应该打印出128(
这才是正确答案
)为了方便理解,可以看看下面的这张图.
- 好了,这道题就先这样哩.但是我们现在再来想一想,如果将上题中的printf语句中的
%d
改为%u
(注意了!!第二个坑出现了
)又会发生什么有趣的事呢?这就不得不说第二个问题了.
(二).数值类型的自动提升与无符号打印(unsigned)中的’坑’.
1.例题
#include<stdio.h>
int main()
{
char a =128;
printf("%u",a);
return 0;
}
当你看到这道题时,是不是有点熟悉呢,没错,就是第一道例题(不过动了点手脚,你发现了吗…),那么这道题会打印出什么呢?(嘿嘿!你现在是不是有点懵了呢.),你可能想了一会,还有可能会是-128吗.其实这道题将会打印出一个很大的数字,那究竟是多少呢?我们一起来看上一看吧.
2.浅析
- 上面说了-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位.
- 看了上面的讲解,是不是恍然大悟呢.那现在也应该有能力来做一道相似的题了吧!那现在给出下面的一道例题,一起来做做看吧,欢迎在评论区讨论哈.
#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]+1
是a[0][1]
的地址,而数组名a
是一个内含2个int类型值的数组的地址,所以a指向一个8字节的对象,因此,a+1,它所指向的地址加8个字节.此时a+1
是a[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.浅析
- 首先,我们来了解下宏的定义
格式
为#define name( parament-list ) stuff,其中parament-list是一个由逗号隔开的符号表,它可能出现在stuff中,如上面的#define SQUARE( x ) x*x. - 当以后的程序中出现SQUARE(x)时,预处理器就会用x*x这个表达式来代替它.假设后面的程序出现SQUARE(4),就会用
4*4
来代替SQUARE(4)
. - 现在来看看上面的例题,当程序执行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.替换文本时,是原样替换(很重要!
),我们在计算时,千万不要画蛇添足,将其计算后带入进去了. - 那如果我们想要使打印结果为25,那该怎么办呢?只需要将
x*x
改为(x)*(x)
就ok了,所以,当我们自己在使用宏定义时应该注重括号的使用,以便达到自己期望的结果.
总结:
我相信,当你仔细看完这几道题(并仔细思考的话),这几个’坑’点你一定可以拿捏了.
最后,本文若有错误的地方,麻烦各位大佬在评论区指正.