《C陷阱与缺陷》第一章【词法“陷阱”】

墨蓝 2023-10-05 22:34 182阅读 0赞

前言:

先在这里和关注我的小伙伴们说一声对不起,因为我已经连续三天没更新文章了。是因为学校的线上课程结束了,线下几乎每一天都是满课,写博客的时间少了很多,不过我会在五一假期期间尽量把之前的补回来。

这个分栏是对《C陷阱与缺陷》这本书的介绍以及我对这本书的理解,希望可以帮助到大家,另外有什么不足的地方还请指出,毕竟一千个读者就有一千个哈姆雷特。

❤️ 温馨提示:

不要整天坐在电脑前,还是要多多运动。定时眺望一下远方和散散步对眼睛和身体好,我们绝不以牺牲自己的健康,来换取自己技术的提升。

目录

⚾️一、=不等于==

1️⃣区分

2️⃣陷阱与缺陷

3️⃣代码示例

4️⃣x == 2 与 2 == x

⚽ 二、& 和 | 不等于 && 和 ||

✏️三、词法分析中的“贪心法”

1️⃣陷阱与缺陷

2️⃣老版本的C语言=+代替+=的一系列问题

3️⃣代码示例

4️⃣y = x/(*p)和y = x/*p

✒️四、整型常量

⏰五、字符与字符串

1️⃣陷阱

2️⃣代码示例

⌚六、练习

⛳练习 1-1

⛳练习 1-2

⛳练习 1-3

⛳练习 1-4


dd2dfdc0533344ab903f3f4c88b3c789.png

⚾️一、=不等于==

1️⃣区分

在C语言中 = 被用作赋值运算== 被用作比较运算

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int a = 10;//赋值
  5. int b = 20;
  6. if(a == b)//比较
  7. {
  8. eat();
  9. }
  10. return 0;
  11. }

一般而言,赋值运算相较于比较运算使用的更加频繁,因此字符数较少的符号 = 就被赋予了更常用的含义——赋值操作。此外,在C语言中赋值符号被当做一种操作符对待,因而进行重复的赋值操作(如 a = b = c)可以很容易的书写,并且赋值操作还可以被嵌入到更大的表达式中。(如 a == b = c)

2️⃣陷阱与缺陷

  • 把比较运算误写成了赋值运算的情形

程序员在作比较运算时,很可能无意的写成了赋值运算

例1:

  1. if (x = y)
  2. {
  3. break;
  4. }

当前代码的本意是检查x是不是等于y,实际上的是将y的值赋给了x,然后检查该值是否为0。

例2:

  1. while(c = ' ' || c == '\t' || c == '\n')
  2. {
  3. c = getc (f);
  4. }

当前代码while循环的判断条件部分本意是判断c是否等于‘ ‘ (空格)或者等于 ‘\t’ 或者等于‘\n’。实际上少写了一个“=”, 也就是赋值运算符;而赋值运算符 = 的优先级要低于逻辑运算符 || ,因此实际上是将以下表达式赋给了c

  1. ' ' || c == '\t' || c == '\n'

因为‘ ‘(空格)的ASCII码值不等于0(‘ ‘ ASCII码值是32),那么无论变量c此前为何值,
上述表达式求值的结果都是1,所以循环将一直进行下去,直到整个文件结束。文件结束之后循环是否还会进行下去,要取决于getc库函数的具体实现,即该函数在文件指针到达文件结尾之后是否还允许继续读取字符。如果允许继续读取字见 符,那么循环将一直进行,从而成为一个死循环。

某些C编译器在发现形如el=e2的表达式出现在循环语句的条件判断部分时,会给出警告消息以提醒程序员。当确实需要对变量进行赋值并检查该变量的新值是否为0时,为了避免来自该类编译器的警告,我们不应该简单关闭警告选项,而应该显式地进行比较。也就是说,下例:

  1. if (x = y)
  2. {
  3. Sleep();
  4. Eat();
  5. Code();
  6. }

因该写作:

  1. if ((x = y) != 0)
  2. {
  3. Sleep();
  4. Eat();
  5. Code();
  6. }
  • 把赋值运算误写成了比较运算的情形

例1:

  1. if (money == Offer(job) < 10000)
  2. {
  3. printf("跳槽\n");
  4. }
  5. else
  6. {
  7. printf("好Offer\n");
  8. }

当前代码是把Offer()函数的返回值与变量money进行比较,如果相等表达式的结果就为1,否则就为0,然后再判断是不是小于10000。然而这段代码的本意是将Offer()函数的返回值赋给money,再判断是否小于10000。因为多写了一个“=”,所以也就无法得到希望得到的结果。

应该改成:

  1. if (money = Offer(job) < 10000)
  2. {
  3. printf("跳槽\n");
  4. }
  5. else
  6. {
  7. printf("好Offer\n");
  8. }

3️⃣代码示例

比较运算写法:

  1. #include<stdio.h>
  2. int Sleep(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. int a = 10;
  9. int b = 20;
  10. int code = 0;
  11. if (code == Sleep(a, b) > 0)//将函数的返回值与变量code比较,再判断是否大于0
  12. {
  13. printf("good\n");
  14. }
  15. else
  16. {
  17. printf("hehe\n");
  18. }
  19. return 0;
  20. }

watermark_type_d3F5LXplbmhlaQ_shadow_50_text_Q1NETiBA6ams5qG25LiK55yL566X5rOV_size_14_color_FFFFFF_t_70_g_se_x_16

赋值运算写法:

  1. #include<stdio.h>
  2. int Sleep(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. int a = 10;
  9. int b = 20;
  10. int code = 0;
  11. if (code = Sleep(a, b) > 0)//将函数的返回值赋给变量code,再判断是否大于0
  12. {
  13. printf("good\n");
  14. }
  15. else
  16. {
  17. printf("hehe\n");
  18. }
  19. return 0;
  20. }

watermark_type_d3F5LXplbmhlaQ_shadow_50_text_Q1NETiBA6ams5qG25LiK55yL566X5rOV_size_14_color_FFFFFF_t_70_g_se_x_16 1

当前代码仅仅是一个“=”之差就得到了两个完全不同的结果,值得注意的是语法并没有错误,所以编译器也不会警告,这需要我们在书写的时候仔细核对。确认我们的需求是什么,然后再来选择使用赋值运算写法还是比较运算写法。

4️⃣x == 2 与 2 == x

我们在编写代码的过程中总会遇到给if语句或是循环语句添加判断条件的时,再使用比较运算来进行判断的时候,如果两个操作数一个是变量,一个是常量,我们可以把常量写在操作符的左边,而变量写在操作符的右边(2 == x)

这样写的好处是在漏写了一个“=”的时候代码会报错(2 = x),因为变量是不能赋给常量的。这是一个语法错误,编译器会报错来提醒我们改正。

watermark_type_d3F5LXplbmhlaQ_shadow_50_text_Q1NETiBA6ams5qG25LiK55yL566X5rOV_size_20_color_FFFFFF_t_70_g_se_x_16

watermark_type_d3F5LXplbmhlaQ_shadow_50_text_Q1NETiBA6ams5qG25LiK55yL566X5rOV_size_20_color_FFFFFF_t_70_g_se_x_16 1

⚽ 二、& 和 | 不等于 && 和 ||

赋值运算符和比较运算符容易少写或多写,而按位与&按位或|逻辑与&&逻辑或||也容易搞混淆。不仅是符号相似,而且名称也是很相似,这让我们在编写的时候很容易因粗心写错而给我们带来不必要的麻烦。关于这些运算符的含义将会在后面的内容中做详细介绍。

对于这四个操作符的使用方法在我其他的文章中有介绍,下面是连接卡片【C语言必修秘籍】之操作符讲解《前章》_马桶上看算法的博客-CSDN博客C语言进阶、操作符详解前章、编程语言favicon32.icohttps://blog.csdn.net/m0\_63033419/article/details/123822247【C语言必修秘籍】之操作符详解《最终章》_马桶上看算法的博客-CSDN博客C语言进阶、操作符、编程语言favicon32.icohttps://blog.csdn.net/m0\_63033419/article/details/123911210

✏️三、词法分析中的“贪心法”

在C语言中像/、*和=,只有一个字符的称为单字符符号;像/*和==,以及标识符,包括了多个字符的称为多字符符号。当C编译器读入一个字符’/‘时后面又跟了一个’*‘,编译器就要做出判断, 是将作为两个分别字符对待,还是合起来作为一个字符对待。 C语言对这个问题的解决方案可以归纳 为一个很简单的规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将符号分解成符号的方法是,从左到右一个字符一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理策略有时被称为“贪心法”,或者更口语化一点,叫“大嘴法”。

1️⃣陷阱与缺陷

除了字符串与字符常量,符号的中间不能嵌有空白(空格符、制表符、换行符)。

例如:==是单个符号,而= =则是两个符号

  1. a---b;
  2. a -- - b

当前两条语句的意思是相同的。

但是与下面这条语句不同

  1. a - -- b

例1:

  1. int b = a---2;
  2. int b = a-- - 2;

当前两条语句的含义都是将a-2的值赋给b,然后a再自减1,所以a和b的值分别为9和8。

例2:

  1. int y = x/*p;

当前代码的本意是将x除以p所指向的值,再把所得商赋给y。

然而因为空格的省略,/*会被编译器理解为一段注释的开始(/*被规定为C语言注释的开始),编译器将不断的读入字符,直到*/出现为止(*/被规定为C语言注释的结束符)。就是说,此语句会直接将x赋给y,而直接忽视了后面的p。

正确写法:

  1. int y = x / *p;

2️⃣老版本的C语言=+代替+=的一系列问题

在老版本的C语言中允许使用=-来代替现在-=的含义。

例1:

  1. a=-1;

当前语句的含义是将-1赋给变量a,然而在老版的C语言中会被理解成将a-1的值赋给a。

也就是这样写:

  1. a =- 1;

也可以理解为:

  1. a = a - 1;

如果程序员的原意是要将-1的值赋给a,还是要加上空格隔开才不会出现意料之外的情况。

也就是这样写:

  1. a = -1;

例2:

  1. a=/*b;

因为在书中也没有指出这条语句的含义是什么,而且我也看不懂,所以就不能给予解答了。不过我猜可能是将a除以a与b的乘积得到的值赋给a。如果大家有什么不同的见解,可以一起在评论区讨论。

尽管/*看上去像是一段注释的开始,但是在老版C语言中会把当前代码当做:

  1. a =/ *b;

当前语句的含义是将a除以b所指向的值,再把商赋给a。

值得一提的是这种老版本的编译器还会将复合赋值视为两个符号,因而可以毫无疑问的处理

  1. a >> = 1;

而一个严格的ANSI C 编译器则会报错。

根据以上的例子,可以很容易的看出加或不加空格对程序结果的影响是非常大的。

3️⃣代码示例

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int a = 10;
  5. int b = 20;
  6. int c = a+++b;//含义是将a+b的值赋给c,然后a再自加1
  7. printf("a = %d\n", a);
  8. printf("b = %d\n", b);
  9. printf("c = %d\n", c);
  10. return 0;
  11. }
  12. #include<stdio.h>
  13. int main()
  14. {
  15. int a = 10;
  16. int b = 20;
  17. int c = a++ + b;//含义是将a+b的值赋给c,然后a再自加1
  18. printf("a = %d\n", a);
  19. printf("b = %d\n", b);
  20. printf("c = %d\n", c);
  21. return 0;
  22. }

显而易见加空格的写法可以更加清晰表达出想要表示的含义。

4️⃣y = x/(*p)和y = x/*p

当我们要编写诸如y = x/*p此类的代码时,可能会因为一些原因忘记加空格。可以使用圆括号将*p括起来, 因为这样写即使是忘记加空格了。代码也不会报错,并且也会得到我所希望得到的结果。

54524ffee34d4443a77717a5465c3b86.png

✒️四、整型常量

如果一个整形常量的第一个字符是数字0,那么该常量将被视作八进制数。因此,10与010的含义截然不同。此外,许多C编译器会把8和9也作为八进制数字处理。这样多少有点奇怪的处理方式来自八进制的定义。例如,0195的含义是1*(8*8)+9*(8*1)+5*(8*0),也就是141(十进制)或者0215(八进制)。我们当然不建议这种用法,ANSI C标准也禁止这种用法。

需要注意以下这种情况,有时候在上下文中为了格式对齐的需要,可能无意间将十进制数写成了八进制数 ,例如:

  1. struct{
  2. int part_bunber;
  3. char* description;
  4. }parttab[] = {
  5. 046, "left-handed widget" ,
  6. 047, "right-handed widget" ,
  7. 125, "frammis"
  8. };

⏰五、字符与字符串

C语言中的单引号和双引号含义迥异,在某下情况下如果把两者弄混,编译器并不会报错,从而在运行时产生难易预料的结果。

⚠️

注意:用单引号括起来的叫字符,用双引号括起来的叫字符串。

例1:

  1. printf("hello world\n");

当前语句是输出一个字符串“hello world\n”,需要注意的是中间的空格也算一个字符。

例2:

  1. char hello[] = { 'h', 'e','l','l','o',' ', 'w', 'o', 'r', 'l', 'd', '\n' };

当前语句是将一个一个的字符放到hello数组里面,但是这两个例子都可以将hello world输出。

1️⃣陷阱

因为用单引号括起的一个字符代表一个整数,而用双引号括起的一个字符代表一个指针,如果两者混用,那么编译器的类型检查功能将会检测到错误。

例如:

  1. char* slash = '/';

在编译的时候将会生成一条错误的信息,因为’/‘并不是一个字符指针。然而有些编译器对函数参数并不会进行类型检查,特别是printf函数的参数。也就造成了下面的现象。

如果用

  1. printf('\n');

来代替正确的

  1. printf("\n");

则会在程序运行的时候产生难以预料的错误,而不会给出编译器诊断信息。在后面的内容还会对此问题进行更加详细的讨论。

译注:现在的编译器一般能够检测到在函数调用时混用单引号和双引号的情形。

整型数(一般为16位或32位)的存储空间可以容纳多个字符(一般为8位),因此有的C编译器允许一个字符常量(以及字符串常量)中包括多个字符。也就是说,用’yes’代替 “yes” 不会被该编译器检测到。后者(即”yes”)的含义是 “依次包含y’、’e’、’s’以及空字符\0’的4个连续内存单元的首地址”前者(即’yes’)的含义并没有准确地进行定义,但大多数C编译器理解为,“一个整数值 由’y’、’e’、’s’所代表的整数值按照特定编译器实现中定义的方式组合得到”。因此,这者如果在数值上有什么相似之处,也完全是一种巧合而已。

2️⃣代码示例

a5e0fd3e1bbf4046953e0eb64ce99037.png

如图代码顺利打印出了字符串

29efe3848a6648389fc32bb4e0d5fb09.png

可以看到使用单引号并没有成功打印字符串

#

⌚六、练习

⛳练习 1-1

某些编译器允许嵌套注释。请写一个测试程序,要求无论是对嵌套注释的编译器,还是对不允许嵌套注释的编译器,该程序都能正确通过编译(无错误消息出现),但是这两种情况下程序执行的结果却不相同。提示:在双引号括起的字符串中,注释符 /* 属于字符串的一部分,而在注释中出现的双引号 ‘’ ‘’ 又属于注释的一部分。

⛳练习 1-2

如果由你来实现一个C编译器,你是否会允许嵌套注释?如果你使用的C编译器允许嵌套注释,你会用到编译器的这一特性吗?你对第二个问题的回答是否会影响到你对第一个问题的回答?

⛳练习 1-3

为什么 n—>0的含义是 n— > 0,而不是n- -> 0?

⛳练习 1-4

a+++++b的含义是什么?

对于以上的问题大家可以在评论区讨论

e53c2d9c36df41718aaa20ed478194fe.gif

发表评论

表情:
评论列表 (有 0 条评论,182人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C陷阱缺陷-连接

    检查外部类型 假定有一个拥有两个源文件的的C程序,一个个文件中包含外部变量的声明:extern int n; 另一个文件中包含外部变量n的定义:long n; 且这两个语