前言
前些天发表了一篇博文关于c语言内存地址对齐的一点思考,引起了大家比较热烈的讨论,的确在这篇文章中示例的选择不是很恰当,示例有很多不严谨的地方,博客的评论中得到了很多同学的指点。也有很多同学指出这种做法技巧性太强,不适合在项目开发中使用,的确是这样,没有深厚的功底尤其是对gcc编译器的深度理解,是比较容易出错。
不过写作该篇文章是为了向大家介绍利用内存对齐的特性来存储一些信息,如果大家今后在别人的代码中看到这种用法,不至于一头雾水,至少记得在osc以前某个时刻某人曾经介绍过这种用法,达到这个目的足以。另外需要注意,思考系列博文都是基于GNU gcc,其他编译器可能会出现不一致的情况。
今天我们要探讨的是c语言中的等于运算符(==),很多同学可能会觉得非常奇怪,这个等于运算符有什么好探讨的呢?无非就是比较两个操作数是否相等,如果相等返回1,否则返回0,如1 == 1表达式返回1,1 == 2表达式返回0,如此简单,有什么可以探讨的呢。
事实真的如此吗?让我们先从一个简单的示例开始吧。
示例
我们的探讨依然是从一个简单的示例开始,如下:
int a = 10; long b = 12; (void)(a == b); (void)(&a == &b);
代码很简单,定义两个变量a和b,分别为int类型和long类型,第04行代码将表达式(a == b)的值强制转换为void类型,该行代码没有任何作用,加上void的目的在于该表达式不能用于右值,第05行的代码同第04行,区别在于等于运算符的两个操作数由(int,long)变为(int *, long*)。
接下来我们打开编译器所有告警,用gcc -Wall编译一下源程序,看看会出现什么情况。
我们看到编译的结果有点奇怪,第05行代码报告警信息,信息内容如下:
warning: comparison of distinct pointer types
告警信息的意思是不同指针类型的比较。此时,我们感到非常奇怪,第04行和第05行代码没有本质的区别,都是等于运算符表达式,唯一的区别在于操作数的类型不同,04行比较的是(int,long),05行比较的是(int *, long*),所以如果要告警的话,应该这两行代码都会告警才对,因为04行比较的数据类型也不一致。结果为什么只有05行告警,而04行不告警。
分析
当我们遇到这样的一个问题的时候,我们应该如何去分析去探索呢?在此分享一下鄙人的一点浅薄看法。
当遇到这样的问题的时候,毋庸置疑,源码是最好的去处同时也是信息最全的,最不会骗人的地方。我们知道gcc是一个相对比较庞大的项目,要求我们对编译原理有比较扎实的基础。这个时候我们会体会到科班出身和非科班出身的不同之处,也从侧面给目前还正在大学就读的同学一点警示,大学期间不要为了一点小小小的项目经验,就荒废了自己的计算机专业基础,那是非常非常不值得的。
接下来就简单的谈一下分析思路,从抽象到具体,是我一贯的思路。先整体的了解gcc的架构,然后再定位到我们要解决的具体的问题。其中最难的是对问题源码的定位阶段,这里面有两种情况:
1)如果对整体的架构和流程非常熟悉,则可以一步步的分析流程,跟踪到该问题具体的源码;
2)当我们只了解整体的架构,那么我们不可能一下就定位到问题源码,但是我们可以定位到该问题的一个超集,对于本文示例来说,刚开始的时候,不知道问题源码在哪儿,但至少我可以确定该问题肯定是在AST构造阶段。当我们知道这一点后,接下来利用一些技巧,分析该阶段,这样就缩小了我们的问题规模。
这里面介绍一个小技巧,当出现问题的时候,我们如何快速的定位。技巧很简单,就是在gcc源码下直接grep 'warning: comparison of distinct pointer types' -r *,此时可能会有多个地方出现该字符串,再利用我们之前分析得到的问题发生阶段一步步的排除,缩小范围,最终能够定位到我们的源码。
针对本文,定位的源码信息如下(以下只列出与本文分析相关源码,其他省略):
/*** * Notes: * file location: gcc/c/c-typeck.c * function: tree build_binary_op (location_t location, enum tree_code code, tree orig_op0, tree orig_op1, int convert_p) * */ case EQ_EXPR: case NE_EXPR: ... if ((code0 == INTEGER_TYPE || code0 == REAL_TYPE || code0 == FIXED_POINT_TYPE || code0 == COMPLEX_TYPE) && (code1 == INTEGER_TYPE || code1 == REAL_TYPE || code1 == FIXED_POINT_TYPE || code1 == COMPLEX_TYPE)) short_compare = 1; ... else if (code0 == POINTER_TYPE && code1 == POINTER_TYPE) { tree tt0 = TREE_TYPE (type0); tree tt1 = TREE_TYPE (type1); addr_space_t as0 = TYPE_ADDR_SPACE (tt0); addr_space_t as1 = TYPE_ADDR_SPACE (tt1); addr_space_t as_common = ADDR_SPACE_GENERIC; /* Anything compares with void *. void * compares with anything. Otherwise, the targets must be compatible and both must be object or both incomplete. */ if (comp_target_types (location, type0, type1)) result_type = common_pointer_type (type0, type1); else if (!addr_space_superset (as0, as1, &as_common)) { error_at (location, "comparison of pointers to " "disjoint address spaces"); return error_mark_node; } else if (VOID_TYPE_P (tt0)) { if (pedantic && TREE_CODE (tt1) == FUNCTION_TYPE) pedwarn (location, OPT_Wpedantic, "ISO C forbids " "comparison of %<void *%> with function pointer"); } else if (VOID_TYPE_P (tt1)) { if (pedantic && TREE_CODE (tt0) == FUNCTION_TYPE) pedwarn (location, OPT_Wpedantic, "ISO C forbids " "comparison of %<void *%> with function pointer"); } else /* Avoid warning about the volatile ObjC EH puts on decls. */ if (!objc_ok) pedwarn (location, 0, "comparison of distinct pointer types lacks a cast"); if (result_type == NULL_TREE) { int qual = ENCODE_QUAL_ADDR_SPACE (as_common); result_type = build_pointer_type (build_qualified_type (void_type_node, qual)); } }
从上面的源码,我们可以看到,当两个操作数都为INTEGER_TYPE的时候(long为long int),只做了一行代码处理:
short_compare = 1;
而当两个操作数都为POINTER_TYPE的时候,则做了很多判断:
comp_target_types (location, type0, type1)
addr_space_superset (as0, as1, &as_common)
VOID_TYPE_P (tt0)
VOID_TYPE_P (tt1)
从上面我们可以看到,第一个comp_target_types函数比较了两个操作数指针指向的的数据类型是否一致,当都不满足上述这些条件的时候,编译器就会打印一条告警信息:
pedwarn (location, 0, "comparison of distinct pointer types lacks a cast");
从上面我们可以看到gcc在处理等于运算符EQ_EXPR的时候,不同的操作数类型其处理逻辑是不一样的。上述源码很好的解释了我们示例中提出的问题。
注1:int和long对于我们来说可能认为是两种数据类型,但对于gcc来说,都是INTEGER_TYPE;
注2:后续有时间再详细的和大家一起探索下gcc的架构;
应用
从以上分析,我们知道二元运算符等于运算符,当两个操作数都为指针类型的时候,编译器会先进行指针所指向的数据类型的检查等诸多操作之后再比较操作数的值。
在了解了gcc编译器的这个特性之后,我们自然会思考,这个特性有什么用途呢?接下来我们就看一下关于该特性的一个简单应用。
#define max(x,y) ({ \ typeof(x) _x = (x);›\ typeof(y) _y = (y);›\ (void) (&_x == &_y);› \ _x > _y ? _x : _y; })
这是一个计算两个数最大值的宏,先获得x,y变量值,然后就使用了我们的等于运算符的特性。我们知道宏和函数最大的区别在于宏不能做静态数据类型检查而仅仅是简单的替换,所以一旦出现错误,就很难定位。
第04行的代码的作用就是比较x和y的数据类型是否一致,当类型不一致的时候,就会编译器告警,这样做就避免了计算不同数据类型的最大值。通过这种方式,使得我们在使用宏的同时,可以让我们的代码更加健壮。
从上面的应用我们知道,当我们需要检查两个变量的数据类型是否一致的时候,就可以利用==运算符在处理两个操作数都是指针类型的情况,会对指针指向的数据类型进行比较。
总结
本文先从一个简单的示例引出了gcc在计算等于运算符表达式,当两个操作数都为指针类型的时候的不同处理方式。再从gcc源码的角度分析了,为什么会出现这种情况。接着介绍了该特性的一个简单应用。从以上的分析,我们知道,当我们越来越了解我们的编译器的时候,我们就可以编写更加高效,更加健壮的代码。
这里再吐槽一下,与其花时间去做一些没有太大意义的事情如研究c++强大的功能和语法等,不如花更多的时间去了解我们所使用的系统(这里系统包括编译器,os,处理器等),编写高效的代码,因为我们实在是太不了解我们的系统,有太多的东西值得我们去思考去探索。