【C 陷阱与缺陷】(二)语法陷阱

Souleigh ✨ 等级 525 0 0

0. 理解函数声明

请思考下面语句的含义:

(*(void(*)())0)() 

前面我们说过 C 语言的声明包含两个部分:类型和类似表达式的声明符。

最简单的声明符就是单个变量:

float f, g; 

由于声明符和表达式的相似,我们可以在声明符中任意使用括号:

float ((f)); 

这个声明的含义是:当对 f 求值时,((f))的类型为 float 类型,可以推知 f 也是浮点类型。

同样的,我们可以声明函数:

float ff(); 

这个声明的含义是:表达式 ff()求值结果是 float 类型,也就是返回 float 类型的函数。

类似的:

float *pf; 

这个声明的含义是:*pf是一个 float 类型的数,也就是说 pf 是指向 float 类型的指针。

以上的声明可以结合起来:

float *g(), (*h)(); 

*g()(*h)()是浮点表达式。因为()(和[])的优先级高于**g()也就是*(g()):g 是一个函数,该函数返回一个指向浮点数的指针。同理,可以得到 h 是一个函数指针,h 所指向的函数返回值为浮点类型。

一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到:只需要把声明中的变量名和声明末尾的分号去掉,再用括号整体括起来

比如:

float (*h)();

(float (*)())p; 

假定变量 fp 是一个函数指针,那么如何调用 fp 所指向的函数呢?调用方法如下:

(*fp)(); 

*fp 就是该指针所指向的函数。ANSI C 标准允许将上式简写为:

fp(); 

但是要记住这是一种简写方法。

注意:(*fp)()*fp()的含义完全不同,不要省略 *fp 两侧的分号。

现在我们声明一个返回值为 void 类型的函数指针:

void (*fp)(); 

如果我们现在要调用存储位置为 0 的子例程,我们是否可以这样写:

(*0)(); 

上式并不能生效,因为运算符 * 需要一个函数指针作为操作数。我们需要对 0 进行类型转换:

(* (void (*)())0 )(); 

我们可以使用 typedef来使表述更加清晰:

typedef void (*funcptr)();
(*(funcptr)0)(); 

1. 运算符优先级问题

if(FLAG & flags != 0){
    ...
} 

FLAG 是一个已经定义的常量,FLAG 是一个整数,该数的二进制表示中只有某一位是 1,其余的位都为 0 ,也就是 2 的某次幂。为了判断整数 flags 的某一位是否也是 1,并且将结果与 0 作比较,我们写出了上面 if 的判断表达式。

但是!=的优先级高于&,上面的式子被解释为:

if(FLAG & (flags != 0)){
    ...
} 

这显然不是我们想要的。

high 和 low 是两个 0 ~ 15 的数,r 是一个八位整数,且 r 的低 4 位与 low 一致,高 4 位与 high 一致,很自然想到:

r = high<<4 + low; 

但是,加法的优先级高于移位运算,本例相当于:

r = high<<(4 + low); 

对于这种情况,有两种更正方法:

r = (high<<4) + low; 

或利用移位运算的优先级高于逻辑运算:

r = high<<4 | low; 

下面我们说几个比较常见的运算符的用法:

  • a.b.c的含义是(a.b).c而不是a.(b.c)

  • 函数指针要写成:(*p)(),如果写成了*p(),编译器会解释为:*(p())

  • *p++会解释为:*(p++)而不是(*p)++

  • 记住两点:

    • 任何一个逻辑运算符的优先级低于任何一个关系运算符。
    • 移位运算符的优先级比算数运算符要低,但是高于关系运算符。
  • 赋值运算符结合方式从右到左,因此:

    a = b = 0; 

    等价于:

    b = 0;
    a = b; 
  • 关于涉及赋值运算时优先级的混淆:

    复制一个文件到另一个文件中:

    while(c = getc(in) != EOF)
        putc(c, out); 

    但是上式被解释为:

    while(c = (getc(in) != EOF))
        putc(c, out); 

    关系运算符的结果只有 0 或 1 两种可能。最后得到的文件副本中只包含了一组二进制为 1 的字节流。

2. 注意作为语句结束标志的分号

考虑下面的例子:

if(x[i] > big);
    big = x[i]; 

这与:

if(x[i] > big)
    big = x[i]; 

大不相同。

前面的例子相当于:

if(x[i] > big) {}
    big = x[i]; 

无论 x[i] 是否大于 big,赋值都会被执行。

如果不是多写了分号,而是遗漏了分号,一样会招致麻烦:

if( n < 3)
    return
logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2]; 

遗漏了 return 后的分号,这段程序仍然会顺利通过编译而不会报错,它等价于:

if( n < 3)
    return logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2]; 

还有一种情形,也是有分号与没有分号实际效果相差极为不同。那就是当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型。考虑下例:

struct logrec{
    int date;
    int time;
    int code;
}
main(){

} 

上面代码段的实际效果是声明函数 main 返回值是结构 logrec 类型。

如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。

3. switch 语句

switch(color){
    case 1: printf("red");
            break;
    case 2: printf("blue");
            break;
    case 3: printf("yellow");
            break;
} 

如果稍作改动:

switch(color){
    case 1: printf("red");
    case 2: printf("blue");
    case 3: printf("yellow");
} 

假定 color 的值为 2,那么将会输出:

blueyellow 

因为程序的控制流程在执行了第二个 printf 函数的调用后,会自然地顺序执行下去。第三个 printf 函数也会被调用。

switch 的这种特性,即使它的弱点,也是它的优势所在。

对于两个操作数的加减运算,我们可以将操作数变号来取代减法:

case SUBTRACT:
    opnd2 = -opnd2;
case ADD:
    ... 

在这里,我们是有意省略 break 语句。

4. 函数调用

C 语言要求:在函数调用时,即使函数不带参数,也应该包含参数列表。如果,f 是一个函数:

f(); 

是一个函数调用语句,而:

f; 

却是一个什么也不作的语句,f 表示函数的地址。

5. 悬挂 else 引发的问题

这个相信大家学习 C 的时候老师都会讲,不懂可以去参考相关。

这里说一点,写 if 语句时,不要省略括号是一种可以学习的习惯。

参考资料《C 缺陷与陷阱》


以上就是本次的内容,感谢观看。

如果文章有错误欢迎指正和补充,感谢!

收藏
评论区

相关推荐

C++概述
概述 C 是静态,可编译,通用,大小写敏感,格式自由的编程语言,它支持程序化,面向对象的,和泛型编程方式。 C 被看作是中间层语言,因为它同时包含了低级语言和高级语言的特性。 C 是于 1979 年在新泽西的茉莉山丘的贝尔实验室由 Bjarne Stroustrup 开发的,它是 C 语言的加强版,最开始它被称作 “C with Classes”,但是
C++ 基本语法
C 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。 对象 对象具有状态和行为。例如:一只狗的状态 颜色、名称、品种,行为 摇动、叫唤、吃。对象是类的实例。 类 类可以定义为描述对象行为/状态的模板/蓝图。 方法 从基本上说,一个方法表示一种行为。一个类可以包含多个
【C 陷阱与缺陷 学习笔记】(一)词法陷阱
一 内容 0\. 不同于 当程序员本意是作比较运算时,却可能无意中误写成了赋值运算。 1.本意是检查 x 与 y 是否相等: c if(x y) break; 实际上是将 y 的值赋值给了 x ,然后再检查该值是否为 0 。 2.本意是跳过文件中的空白字符: c while(c '' || c '\t' ||
【C 陷阱与缺陷】(二)语法陷阱
0. 理解函数声明 请思考下面语句的含义: ((void()())0)() 前面我们说过 C 语言的声明包含两个部分:类型和类似表达式的声明符。 最简单的声明符就是单个变量: float f, g; 由于声明符和表达式的相似,我们可以在声明符中任意使用括号: float ((f)); 这个声明的含义是:当对 f 求值时,(
C#入门教程(二)–C#常用快捷键、变量、类型转换-打造C#
C入门教程(一)–.Net平台技术介绍、C语言及开发工具介绍打造C学习教程 上次教程主要介绍了.Net平台以及C语言的相关介绍。以及经典程序案例,helloworld程序。 初来乍到,第一次做教程难免有各种各样的问题。望请大家见谅。有什么不明白的地方也可以直接添加qq群: 538742639。一起交流学习。 本人学习C方向编程开发两年有余。也算是
Swift与Objective-C混合编程之Swift与Objective-C API映射
原创文章,欢迎转载。转载请注明:关东升的博客 Swift与ObjectiveC API映射 在混合编程过程中Swift与ObjectiveC调用是双向的,由于不同语言对于相同API的表述是不同的,他们之间是有某种映射规律的,这种API映射规律主要体现在构造函数和方法两个方面。 1、构造函数映射 在Swift与ObjectiveC语言进行混合
数据库系统概论
一、范式与规范 1.1 一个二元组一定属于BCNF eg: R {A, B, C},{B C, BA } 等价于{B AC} 1.2 求候选码 1. 列出左右出现的元素:L, R, LR,N。(当右边出现组合元素时,拆分开来) 1. 从(L N) 中的元素开始求闭包,能推出所有元素则一定是唯一的候选码。 1. 如果L中的闭包推不出
统计字符串中字符出现的次数(Python版)
字符串转list python s 'aabbccd' list1 list(s) 方法一: python list1 'a', 'a', 'b', 'c', 'c', 'c', 'c' dict_cnt {} for value in list1: dict_cntvalue dict_cnt.get(value,
C语言_二进制文件
(https://blog.csdn.net/Ustinian_116/article/details/114783972)二进制文件 其实所有文件最终都是二进制的 想想我们之前printf()一个整数的时候
c++11 实现单例模式
C11出来后,里面新增加了好多好用的功能 下面的单例就是使用了C11中的标准库中的mutex和unique_prt 进行内存管理的. 此单例模式不用担心内存的释放问题 pragma once include <memory include <mutex template <class T class Singleton { public: ty
我的C语言基础
C语言32个关键字auto 声明自动变量short 声明短整型变量或函数int 声明整型变量或函数long 声明长整型变量或函数float 声明浮点型变量或函数double 声明双精度变量或函数char 声明字符型变量或函数struct 声明结构体变量或函数union 声明共用数据类型enum 声明枚举类型typedef 用以给数据类型取别名co
C语言入门系列之2.数据类型、运算符和表达式
一、数据类型C语言常见数据类型如下: 1.数据类型 基本数据类型基本数据类型最主要的特点是,其值不可以再分解为其他类型。也可以说,基本数据类型是自我说明的。 构造数据类型构造数据类型是根据已定义的一个或多个数据类型用构造的方法来定义的。也就是说,一个构造类型的值可以分解成若干个“成员”或“元素”。每个“成员”都是一个基本数据类型或
C语言基础习题50例(一)1-5
虎为百兽尊,罔敢触其怒。惟有父子情,一步一回顾。 习题1 有 1 、 2 、 3 、 4 个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?实现思路:显然,这个题目需要用到循环,并且是循环嵌套,先列出所有可能的组合,再去掉重复的组合即可。代码如下:cinclude <stdio.hint main(){ int i, j, k,
游戏安全实践的一些思考
移动的游戏能够稳定健康的上线。主要需要依赖以下在四个方面:1.前端展示,或者说客户端正常运行。性能稳定不崩溃,不过热能够稳定运行。2.后端,或者游戏后台服务端的。不但要稳定。还有能在有限的服务器资源下,能承受大量的同时在线用户。而且要让游戏中的每个模块都能够承受承受大量的同时在线用户。3.安全也是重点之中。这既包括客户端,又包括服务端。客户端的安全,包括要防
IO编程实例——使用缓冲流实现文件的拷贝
数据源:"C:\\Users\\你是小朱老师呀\\Desktop\\test.txt"数据的目的地: "C:\\Users\\你是小朱老师呀\\Desktop\\XSC\\test.txt"实现步骤:1.创建源文件与目标文件2.创建节点流3.创建缓冲流4.读取、写入5.释放 package person.xsc.praticeIII;import java.