我为什么讨厌狼人杀?

昨天早上在知乎上看见有朋友分享这个「你为什么讨厌狼人杀」的问题。点进去之后我非常惊讶——居然在政治、社会话题之外还有这样的让我感觉「解气」的问答。说大家是落井下石也好,不会玩也好,总之无法否认,这个风靡街头巷尾的游戏,是像任何一个明星一样,有不少人对此反感的。

我记得我最开始接触这个游戏的时候还是在初中。那时对杀人游戏和狼人傻傻分不清,印象里反正是那些认识很多朋友的外向同学出去玩的标配。玩得不多,「天黑请闭眼」还是记得住的。一直到刚上大学那会,每次玩这个游戏似乎大家都傻乎乎的,如果是陌生人那就随便票,如果是认识的一起玩,当然就是把大家最爱开玩笑的对象投死了。

其实我真的也不知道狼人游戏是怎么在不长的时间内转变成现在这个样子的,就像我不知道如今微博上一个小有名气的明星转发评论赞数都能上万了一样。狼人和狼人杀有什么区别我不清楚,不过我猜想这名字或多或少都得跟三国杀有关系。说起来,随便一家小的桌游吧藏有的游戏都能摆满一整个柜子,可是大多数人出去玩能想到的恐怕还真只有三国杀和狼人。UNO的话,其实真不太想跟不熟的人玩这个。跟前两者比起来,这个游戏的标准规则存在感实在是太弱,变种又太多,有冲突就挺尴尬的。不过好在它节奏快,而且各自为战,运气成分强,玩起来还是算轻松愉快的。

回到原题。头一次接触这种「高端」的狼人游戏是去年夏天给一个朋友送行的时候。一帮还算熟的人去桌游吧玩狼人,我当时倒是爽快地答应了。第一把如往常,大家一开始就把东道主给票死了。第二把的时候就被叫出去和陌生的老玩家一起玩。头一回见着这样的分析我是很震撼的,还在一边听一边掏出手机查那些接头暗号一样的术语到底是什么意思。第一轮,很自然的,因为我都不认识,所以随便挑了一个指。结果就是这样随便一指,在第二轮搞得高玩们分析了半天,还纳闷为什么大多数人都喜欢装预言家。很有趣,但是也挺无聊的。那天晚上后来又经历了几次随便过然后被票,也有分析了一波带了下节奏的。回去之后跟其他同学聊天的时候把那天晚上的见闻吹嘘了一番,毕竟我的确是震惊不已。后来我再也没有玩过狼人杀,也的确不想再玩。

其实在我看来重点不在于什么黑话多不多。仔细分析一下可以发现狼人杀是一个「推进感」很强的游戏。这个词是我自己采用的。推进感是什么?狼人杀可以说不需要什么硬件设施,就几张牌抽身份而已,要搬上网络不能更简单,基本上属于一个纯语言类的游戏。既然没有硬件设施,游戏的进行全靠每个玩家的发言作为载体。如果每个玩家都不说两句话,这个游戏几乎就无法进行。这样的逆向淘汰也就是早期狼人深受交友爱好者喜爱的原因,因为他们更爱说话,没有太多顾忌,尽管那时还没有这么多套路。如果你是一个喜欢发言并且掌控节奏的人,这个游戏当然能带给你别致的体验。但是对另一部分玩家来说,这就是巨大的压力——为了游戏的正常进行,你不得不发言,强行分析。和日常辩论不一样的是,这样的发言充斥着谎话。我当然知道游戏中的谎言无法代表玩家现实生活中的人品,但是不能否认,有不少人无法这样面不改色地说谎。就我个人的感受而言,这个游戏过于紧迫,不像我在玩游戏,而像是我在被这个游戏和其他玩家玩。

竞技类游戏也是这样吗?大概是的。不过如果你不是职业玩家的话,完全可以娱乐一点。而且嘴炮水平和实际能力完全不挂钩,有时候不会让人那么反感……而狼人杀,现在大有一种「图个乐呵」的玩家被所谓的高手绑架的趋势。更令人不悦的是,乐在其中的人一方面在游戏过程中强调它的竞技性,反感那些不认真玩的玩家或者是坑队友的新手;另一方面又把这个游戏和社交挂钩,默认所有人都应该像自己一样充满节奏感。如果我和人出去玩的时候拒绝一起玩狼人,恐怕是会被扣上「不合群」的帽子的。电脑游戏的话,至少别人拉我去上网的时候我说我不会玩英雄联盟玩玩其他的好像也不是什么严重的问题。

长大了真的会发现,人和人之间的价值观可能会有巨大的差异。于我个人而言,我真的不太喜欢游戏过程中的竞技性。初中一开始我也和同学一起打篮球,后来的Dota什么的我也玩过。为什么总是进行不下去呢?配合少也好,水平次也好,玩游戏真的就图个热闹,这是价值观的问题。所以我选择不参与,不干扰好胜玩家的正常游戏。我认为这是一种尊重。我不玩狼人杀,我喜欢被动一点硬件多一点的游戏,我会对没玩过的桌游感兴趣。我说了许多我不喜欢狼人杀这个游戏的理由,也许在读这篇文的人一条都不认同,但你没有资格反驳我。因为我没有干扰你的游戏,我也不会因为一个人的爱好就随便下结论。我们当然也可以成为很好的朋友,但未必就要用这种令我不悦的方式。

我很怀念高中的时候周五偷偷玩七大奇迹把科技树点到底,也怀念大富翁下得几千上万生死一线间,一起玩UNO被加了十六张大家哄堂笑我也很激动。回头看也许这才是面对面游戏本质的乐趣。

可我不喜欢狼人杀。准确地说,是这个以社交名义绑架你我的狼人杀。

关于大学老师上课的一些看法

好像很长的时间都没有好好写过文章了,也不知道放在这里会不会有人发现。之前约定自己,每天或者每周更新这个博客,可惜这件事到现在并没有做到。说实话,其实就是自己懒,未能规划好时间。说到这里,的确是有点羡慕那些平日上课按部就班的同学了。也许他们的实际「水平」不如另外一些人,不过每天跟着老师的进度走,自己完成作业,也按时交,不怕点名,感觉大概是很棒的。这个学期期末的时候要交数据结构的课程设计,那些作业本可以很早以前就写完然后上交的,然而总是觉得这不对那不对。题目只要求用命令行的界面,但是怎么看怎么觉得有点不太能上台面;但是用GUI做吧,用什么库呢?好像也不好看,带个东西也臃肿得很,要求叫可执行文件,还得麻烦一下用Windows的同学。所以就一直拖着。一直到Deadline前,才意识到这个东西实在不能再拖下去了,于是就赶紧三下五除二赶好。你别说,赶作业那段时间虽然时间紧,但是感觉还挺好的。之前的C语言大作业、Cocos的游戏作业,都是这样的,有别人推着走的感觉会很稳。不像自己学东西,没有什么自制力,说好的自己要写编译器,拖拖延延到现在还是没个头。

不过呢,这次去北京参加大型主机的比赛,整个过程中开了不少的会,也在北京玩了一把,收获确实是不少的。每天或者每几天review一次的感觉相当麻烦,不过真的可以保证某个项目的稳定推进。之前想的是,对自己制定一个完整的规划,自己照着这个规划来,每一步完成一个什么。但是一个可行的规划就必须要求制订者对这个项目和工作效率有基本的认识,多数时候这就是意味着丰富的经验。对于个人学习的项目来说,这种做法多半是不可行的。所以不如「走一步看一步」,但是要求一定得强硬,这比较适合假期一个人的时候学习。又说回这个编译器的想法,该写的是一定要写的。可以不忙着设计语言,就照着C11的标准来就行。实现语言的话,可以用Ruby可以用C++,乃至OCaml。我还计划大三的时候开门课讲编译原理呢,估计那时候就真的得用C++来搞了。说来也是惭愧,这学期选了门编译原理,结果好像就没听几次课(老师懂肯定是懂的,但是上课真的无比蛋疼),下周就要考试了还不知道做了多少准备。

说到老师的授课方式,好像进入了这篇文章想要说的正题。讲真,第一次在这个博客上如此激动的想写那么一点东西下来。其实回想自己大学生活的过程,可以很明显地感觉到对自己的要求在一步一步地变低。说大一的时候不谙世事也好,说是因为想转专业积累绩点也好,那个时候听课的确是认真的。也不知道是不是我的错觉,老师也比现在要走心一点。第一节西方哲学史的课堂上,韩潮老师看着几个迟到的同学说:「你们现在才大一,不要这么早就像高年级的同学一样了啊。」对他的课印象比较模糊,因为我总是想睡着,可就像高中的物理课一样,每次真的要倒下去的时候,脖子又会坚强地抬起来。这样往复,还真的能坚持几十分钟。政治课也是认真听的,下课一没事就去图书馆看看书。那个时候心里没底,但生活却是最规律的。

来软件了之后,直到大二,我似乎明白了一个道理:大部分专业都会把讲课最令人舒服的老师放到大一来开课,可能是让大家至少不要「堕落」得那么快吧。都说物理像高数一样,不过那时候我的高数课上得还是挺开心的,困归困,基本都不玩手机,按时交作业,身边也没有同学可以抄。那时候想的是,大学的学习也不过如此嘛。哎,人就不能过早下定论。

怎么讲呢,之前我也是从来没想过挂科的。就连物理上,也是期末抢救了一番之后,成功保住了一个及格。虽然是同一个老师,可物理下就没有那么幸运了。大概是我太水吧,真的一点不会,平时也不想听。最后期末复习看书的时候,真的有一种想杀人的感觉——我真的,从小到大都没有这样窘迫过,为什么一定要这么为难我?为什么?恶性循环嘛,所以到今天,我的物理挂了三次。第一次挂科的感觉真的是很难受的。在同济,挂科的人每学期一定不会少,找老师求情的也不会少,但是像我一样把自己的光荣事迹放到校内公共平台上,同时质疑物理课开课必要性的,恐怕真的没几家了。说真的,在网上狠狠同别人吵了一架之后,物理对我造成的伤害反而没那么大了。我的概率论好像也挂了。看来到这个时候,就是我的问题了。或者说,早就习惯了人文的复习方式,一下这样「动真格」,是的确不适应。

好像又说远了。各种有大学生的论坛里,吐槽授课教师上课水平低的绝对不是一两例了。可不知道有一些人到底是真糊涂还是装糊涂,强行立了一个逻辑,说「学生水平还不如老师,所以没资格评判老师的上课水平」。这就让我想起了高中时候的某同学。我说,我觉得汪东城长得不帅啊。他讲,「你自己照照镜子看看你有他帅么」。事实上学生当然是有理由指出老师上课的不足之处的,因为学生就是老师授课的对象,恐怕最有资格来做评教这件事。可惜一个老师来上某门专业课未必是出于普及知识拯救学生的信念,而是无奈受迫所致。这就导致许多同学毕业以后,不记得自己上过多少有干货的专业课,反而对某些选修课文采飞扬的老师印象颇深。你当然可以说,真知都是枯燥的,仿佛老祖宗讲的良药苦口一样。可为什么现在的不少药,外面都得加个糖衣吗?复杂、系统的知识当然需要花费大量的时间理解和练习,但是这绝对不是任课教师不认真备课,不认真思考课程改革方案的理由。当然这不只是任课教师的锅,学院的思想僵化,领导独断专行,都是共同原因。不过在下确是认为这些不太受学生欢迎的老师,应该反思反思,自己有没有落实好这个「培养计划的最后一公里」的角色。

知乎上跟教师有关的话题,尤其是大学教师的话题,我时常觉得不堪入目。大清亡了都一百多年了,女权主义都要成热点话题了,为什么众人对教师职业的看法还停留在那个时代?老旧的尊师观念、苏联式办学传统和官僚体制下僵化的大学培养制度共同诞生出了我国目前本科教育这个怪胎。很多人讨论问题的时候都是在想当然。同济有没有严格的老师?有。但是这些老师被学生骂到死了吗?没有,反而受一代代的学生尊重,每年选课的时候都会成为抢手货。因为这些老师的严格,不是目的,而是达成教学目标的一种手段。学生通过老师的严格,最后获得了真正的知识和成绩,所以才会反过来感谢老师。有些老师口口声声「其实你成绩怎么样和我有什么关系」,实质上就是在为自己的不负责任找借口。学生不听课,不代表你老师就可以瞎干了啊。

但这个问题并不好解决。所以在可见的大学生活里面,学习这档子事情,还是只能靠自己。自觉是不一定靠得住的,正如一个人只能溺水而死,不能憋气而死一样,要适当创造一些「自己无力改变」的短时条件,比如上课不带手机和电脑。或许效果会好很多,至少我可以自己看书。

所以还是想给下学期好好定个安排,我记得以前也定过,常变常新嘛。

  • 物理一定要过,否则以后的课程安排会受很大影响
  • 实现一个C语言编译器,后端生成nasm汇编代码,实在没时间用LLVM也可以,要在今年GSoC之前完成
  • 至少把《经济学的思维方式》《纳粹德国:一部新的历史》和《死亡》读完
  • 下学期上课尽量不带手机和电脑,每次上课坐前1/3排
  • 买一对哑铃,或者每周去健身房三次
  • 读《Ruby源码剖析》这本书,尽量向今年GSoC的Ruby项目靠拢,大三可能没那么多机会了
  • 有空的话,深入学习OCaml,看一下《近世代数》
  • 去一个江浙沪之外的地方旅游
  • 每周至少更新一次这个博客

一开始的语言可能有些激动,请原谅我这个人许多时候的自以为是。

祝好,送一声迟到的新年快乐和早来的新春快乐。

Ruby on Rails开发入门手记

上个月的博文里说过,由于要开发同济权益的二手书管理系统,所以用PHP写过这样的代码。第一个版本是纯PHP撸成的,没有用到任何的外部框架,路由也是纯手写.htaccess文件实现的,一共就四个文件,负责分发请求,最后交给处理数据库的文件来输出对应的JSON信息。现在看来,这大概叫强行RESTful吧,哈哈。话说回来呢,当时写的时候,甚至还有点基本的ORM思想,不过觉得太麻烦就没着手完成这个。后来才知道这个模型有个高大上的英文缩写叫做ORM。当然,这个版本的代码,模块性一点都不好。五百多行,基本没法维护。

后来寒假的时候,确实是觉得这个太不行了,需要找个框架重写,顺便学习一个。用哪个框架呢?知乎搜索了一下,就用CodeIgniter吧,相对轻量,又确实有用。这个框架是MVC架构的,但是也没有实现完整的关系对象模型,仅仅是封装了方便的数据库查询器而已。这个版本,自我感觉代码结构好多了,不过好像和前端交互的API还是没有改。

到了四月初的时候,听说软件学院要搞一个Android应用开发比赛,所以呢,后端可能还得改。这次大家坐在一起开了个会把需求相对完整地定了下来。不过,还没等得及下手,一切又变了。应用还没开始做呢,就又变卦了。这次是得改变架构,网页和后台不分离。大概就剩一个月的时间了。怎么搞呢?用Ruby on Rails吧。听说,这玩意很快啊。

要说之前,Ruby这个语言我就没有碰过。虽然它和Python我都不熟,不过其实还是用Python相对多一点。但是不行啊,硬着头皮也得上。所幸Rails的文档写得确实不错,我做这个网站的过程中很大程度参考了Ruby on Rails的中文指南。如果有朋友对Rails这个框架感兴趣,非常推荐这一系列文档。同时,也听说Ruby China在国内的技术社区里面是最有氛围的一个了。

Rails无非也就像是其他MVC的Web框架一样,在目录下通过命令生成一个新项目,然后运行服务器,打开看到一个正确运行的页面。这就标志着我们的开发工作正式开始了。当然由于Rails这个框架比较复杂,生成的代码也比较多,所以新建一个控制器还不能像CodeIgniter这种框架一样直接手动新建个controller文件就好了,需要用到generate命令。敲下命令之后,发现Rails确实爆炸,自动帮我生成了RESTful风格的路由和一堆代码。

由于朝RESTful看齐的特点,直接在Rails的路由文件routes.rb里像这样写:

Rails.application.routes.draw do

  resources :books
  resources :users
  resources :appointments

  root 'welcome#index'
end

寥寥几行代码,Rails就帮我们生成好了书、用户、预约三种资源的所有路由,已经定义了网站根目录对应的控制器方法。

说到生成,Rails还有一个更厉害的东西,叫做「脚手架」。利用它,可以一键生成控制器、视图和模型的代码。虽说这些代码直接拿来用不太现实,不过也可以作为初学者学习的重要参考。

Rails的脚手架用法如下:

rails g scaffold books title:string price:decimal

这样就快速创建了一个叫做books的资源,包含title和price两个属性。脚手架为我们快速创建了路由、控制器、视图、模型,甚至测试文件。脚手架生成的代码虽然完整,但是往往不能完全满足我们的需求,我们也未必喜欢它给我们强加上的一堆网页样式。我们可以基于脚手架的代码来修改,不过更多的时候只是用它当作例子来学习,看看标准的Rails代码是个什么样子。

Rails严格区分了资源的单复数。一个资源在作为Model层的时候是单数的,比如Book;而在View层和Controller层的时候是复数的,比如books_controller.打开Rails为我们创建的这个controller,我们可以发现七种RESTful的操作已经预先定义好了,并且自带html和json两种模式。很迷的是在许多方法里只有一行语句甚至没有语句,那么Rails是怎么渲染View的呢?

实际上,Rails会自动找到与controller里的方法同名的模版文件(Rails默认用的是erb)然后渲染。当然有些方法是不需要HTML模版的,比如POST、PUT这些HTTP请求使用的controller就不需要特别的模版,用得更多的是重定向。当然,我们可以手动选择要渲染的模板。即使用render方法。

Rails的模板是怎么写的呢?因为Ruby语言不靠缩进来标记语法层次,所以Ruby可以轻松地嵌入到HTML模板文件当中,这一点比不得不自造模板语言的Django高到不知哪里去了。这类模板也就是所谓的erb。Rails可以在设置里选用其他的模板,比如haml乃至markdown。

被渲染的erb模板能够正确地加载调用它的控制器里的所有实例变量。什么意思?比如像这样:

# controllers/books_controller.rb
class BooksController
  def index
    @time = Time.now
  end
end

# views/books/index.html.erb
<p>Current time is <= @time %>.</p>

所谓的带@符号的变量是Ruby的一种语法,称为实例变量,表示类的数据成员。如果我们去掉这里的@符号,把time定义成局部变量的话,模板渲染的时候就会报错了。

我们把Rails的视图层和控制器都极简地介绍了一下。尽管没说几句话,但是已经基本可以构建一个可以运行的网站了。这都要归功于Rails的良好封装和Ruby语言的自由度。现在我们来看看之前被忽略掉的Model层。实际上,Rails更加鼓励把更多的逻辑写到模型层里面。(Rails还需要我们为controller写什么代码吗?)对于书这种资源,我们也能在models目录下发现对应的代码文件book.rb,注意到book用的是单数。

class Book < ActiveRecord::Base
end

咦,这个Book里什么代码也没有,就一个空类嘛,有什么用?

别急,如果我们在Controller里面想要查询关于Book的信息,非常简单:

def get
  @book = Book.find_by_id params[:id]
end

有趣,这个find_by_id是哪来的?噢,忘说了,Ruby里面方法调用跟Perl一样不需要括号噢。

哈哈,这就是Ruby语言的威力,它可以把字符串和代码相互转换,并且可以在运行时动态定义新的方法,甚至覆盖旧的方法。Ruby看到我们find_by_id这个方法时,不会急着报错,而是去寻找有没有「当方法找不到时的解决方案」,而Rails在这里为我们提供了一个。它会去数据库的结构里查找id这个字段,然后返回给我们想要的结果。也就是说,如果我们的Book表里新增了一个字段叫ISBN,那么我们可以直接find_by_isbn了,什么也不用改。

这只是Rails的数据库子包ActiveRecord强大功能的冰山一角。另一个值得一提的地方是它的关联机制。比如这样:

class Article < ActiveRecord::Base
  has_many :comments
end

# 注意单复数
class Comment < ActiveRecord::Base
  belongs_to :article
end

# Rails会去寻找Comment表里article_id这个字段
some_article = Article.take
some_article.comments # 我们已经得到了此文章对应的所有评论

这里对article_id这个字段名的假定体现了Rails的一个重要原则:「约定大于配置」。相比于Java繁复的各种XML配置文件,不知道Rails的这种风格有没有让你觉得清爽呢?

拖了六个月,说了这么多,其实也就是Rails最基本的一点点。不过足以让许多人认识到Rails框架和Ruby语言的魔力了。整个Ruby社区很有趣的一点就是,从Rails的插件到各个方面的库,作者都会以这个库的使用方式「看起来像英语」为荣,比如:

every 3.hours do
  sleep 2.minutes
end

得益于独特的语言文化和语言的强大能力,这样的DSL在整个Ruby社区到处都是!

文章很短,不过希望你能从中感受到用Ruby编程的魅力。就像DHH说的,成为百万富翁以后才发现,开兰博基尼还不如写Rails来得快乐。(虽然我没开过)

以下有两个页面可供想学习Ruby on Rails的你参考:

不受待见的程序员

程序员正在遭受来自这个社会的,同对医生一样的误解。为什么大众会误解医生呢?或者说,为什么医生与患者间的关系开始变糟了呢?行医救人的职业古已有之,尽管那时尚无「白衣天使」这种搪塞个人追求的名字。然而工业时代来袭,全世界的居民都接受了经过塑造的现代生活方式、谋生手段,且几百年而不绝,这种改变仍在快速进行之中。物资丰富、日新月异的年代,每个人都有权获得更好的生活。为了区分这个先后缓急,才有了资本、技术、劳动作为收入分配的依据。医术自古即是一门技术,但那时技术还算不得资本,更何况是「士农工商」的年代?

现今的人不能理解社会系统、国家机器运作的复杂性和脆弱性,他们仍习惯于以小农时代的思维模式打量这个世界的运转。也正由于此我们能够时常看到身边人关于社会管理、国际关系等的的种种呓语。正如和你大谈高层秘辛的出租车司机不必深究汽车原理便可参与营运一样,现代社会允许其成员未曾窥过其逻辑即可自由地享受生活。简言之,现时的医生早已不同于古代,就医过程的参与者也不仅是患者与医者双方。用想当然的逻辑思考规整化的医疗体系,自然是会出问题的。

程序员就更可怜了。如果说医者的职业目的大众还算心中有数的话,对程序员,就真算一片空白了。「开发软件」?软件是什么?即使是日常用到软件的人,也不清楚这些司空见惯的程序的复杂程度(「2000块给我做个淘宝」)——从这个角度讲,社会和软件倒有相似之处。须知,大多数人对职业的认识几乎完全是从个人经验出发的——比如律师就是耍嘴皮子,设计就是随便画画。在建筑行业整体遭冷的今天,仍有不少的重庆人相信搞桥梁是最好的职业之一,因为他们一辈子见得最多的就是桥。更何况,在风口可以吹猪上天的今天,程序员引来了诸多行业的羡慕与嫉妒。又有大批对计算机几无敬畏与热爱的人入行以工程师自居。这些拿着半吊子水平的Java就大谈而谈各路本质论的人继续伤害着程序员在大众眼中的观感,误解自此不可避免。

说「这个世界还未能体会程序员带来的美和智慧」有些过了。对于我这个不爱车的人来说,我也没法感受搞汽车的人乐趣在哪。不过我清楚汽车是什么,做什么,大概是个什么样子。我也明白现今驾驶、乘坐的体验是上百年一代代工程师的努力带来的。可……把「汽车」换成「软件」呢?这确是教育的问题。今天又越来越多的人把杨永信的网瘾治疗当作是一个笑话,但不要忘了在2016年还有大量的人想在互联网上再来一次卢德运动。可悲又可笑。

未来当然会有越来越多的非程序员学会编程并用以解决自己领域的问题,也当然会有程序员丢掉饭碗,正如原来的电话接线员。实际上,如果现在的程序员穿越回二三十年前:「不会手写键盘驱动来接受输入,你也配叫会编程?」技术的进步是为了解放人类,使其能考虑更高层次的问题。国内僵化落后的计算机普及教育,却会使更多的外行视编程为洪水猛兽,也无法正式程序员这个职业。

做一个程序员有哪里好羞耻的呢?在「劳心者治人而劳力者治于人」的文化环境中,程序员的火热本身就是对「读书无用论」的重击。我很期待在未来看到工作到六七十岁的老程序员,那是社会理念更进步的表现。想一想,还有多少专业的人有资格以纯粹的兴趣看待专业问题呢,并拥有如此低的创造成本呢?

全世界程序员,联合起来!

cJSON源码分析-实现

好了好了,这篇文章拖了真久啊,活活三个月。不过之前承诺要经常更新博客的,所以先把这个烂尾的坑给填了。

之前提到了cJSON这个C语言写成的JSON解析库的接口,也就是头文件里的内容。这一次我们来分析一下实现。上车吧。

还是按照源代码的逻辑走。我们发现cJSON最开始有一个全局的字符指针ep,以及一个用以返回ep的函数。可以看出,这个ep是用来存储错误信息的。这种实现是C语言的常用手法,即把一些状态用static的方式隐藏在单个文件中,并实现一些函数当作接口。

static const char *ep;
const char *cJSON_GetErrorPtr(void) {return ep;}

下面是一个大小写无关的字符串比较函数。看后面的源码可以发现这个函数用在了JSON对象名的查找当中。实现没有什么特别的难点,所以就不提了。顺便说一句,在C++里要实现这个大小写无关比较,可以用STL里的一个方法叫做lexicographical_compare,搭配lambda表达式可以轻松达到目的。

作者把负责动态内存分配和释放的函数直接「填」成了标准库的malloc和free,像C++的allocator一样。如果担心内存碎片的问题,可以自己再当个二道贩子,实现一个内存池,不过那有点背离本文的主题了。作者自己实现了一个版本的strdup,里面也是用这个用户可以更换的cJSON_malloc进行内存分配的。

新建JSON和释放JSON的工作不难,前者就是内存分配的问题,后者就判断一下JSON的type,如果是基本类型就直接释放,如果是数组或者对象就递归删除(类似二叉树)。

目前好戏来了。第一个函数是解析数的。写过词法分析器的就懂,这个用自动机很容易描述,不过这里实在是没有什么好的能在电脑上绘制自动机图的工具,所以用列表表示这个过程,看看应该能体会。

  1. 开头有负号吗?有就记下来,向前走。
  2. 第一个数字是0吗?是就前进,反正默认的结果都是0.
  3. 这是小数点以前的部分,一位一位地循环解析就好了,到第一个非数字的字符为止。
  4. 有点并且点后面有数字吗?有就把小数部分也解析了加上去。
  5. 有E或者e吗?有就算10的幂次方。

简单吧?我们接着往下走。看到一个…奇怪的函数。

static int pow2gt (int x)
{
    --x;
    x|=x>>1;
    x|=x>>2;
    x|=x>>4;
    x|=x>>8;
    x|=x>>16;
    return x+1;
}

看见这个函数心里大概会想——什么鬼?名字看不懂,内容也看不懂。唔,不过看在它参数和返回值都是简单的int类型,不妨写个小程序测试一下结果。(限于篇幅省略结果)跑完之后我们猜测,这个函数的目的大概是返回一个不小于x的2的整数次幂。(对2的整数次幂还不敏感吗?)那我们来根据代码验证一下。

先略过这个减1的过程,看看位运算。我们假设整数的二进制形式从右向左,最后一个值为1的位是第n位,那么运算的过程是:

  1. 首轮,第n位右移1位,经过按位或运算,第n和n-1位(n右边那一位)确保为1.
  2. 第二轮,类似地,n和n-1都右移2位,所以n-3到n都是1.
  3. 第三轮,同样,此时n-7到n位都可以确保为1.
  4. 第四第五轮后,从1到n位都是1了。右移到16截止是因为这里的整数只有32位。

最后往这n位连续的1上再加个1,就是1后n个0,即2^n了。起来这个过程有点故弄玄虚的意思,因为我们也可以用循环的方式来解决。不过这里作者巧妙利用了整数位数的限制,用五次位运算达成了对任意整数都有效的效果。为什么要减1呢?因为不减1再加回去的话,对一个已经是2^n的数进行运算会得到2^(n+1),不符合我们的预期。实际上,这样的位运算技巧在《高效算法的奥秘》和《深入理解计算机系统》中都有相关的阐述。

这个函数有什么用呢?搜索一下就会发现。它只用在了一个地方,就是下面这个ensure函数。继续追踪可以发现,这个函数包括下面的update以及printbuffer这个结构体,都是用来存储缓冲区的。在缓冲区里面空间成2倍地扩大。不过我们的重点在字符串解析和内部的数据结构。

到这里,我们回过头来整理一下思路。JSON的类型有6种,而操作又都有解析和输出两种。

  • null和bool,由于bool只有两种固定的值,所以对于这两者,输出和解析都是简单的strcmp、strcpy就可以。
  • string,解析本身难度不是太大,去掉两端引号中间的就是字符串内容。但是有两个(或者说就是一个)问题,一是要注意反斜杠’\’开头的转义字符,二是字符串涉及到utf16到utf8的转换。输出的话不是什么太大问题。
  • number的解析前面已经说过了,浮点数输出要考虑一下精度,IEEE754标准和utf8都是坑。
  • array和object都是递归解析。如果要按格式输出,缩进是一个问题。

唔,好尴尬,写到这里突然不知道怎么继续了。在这里贴一下主要的解析函数parse_value的代码。

/* Parser core - when encountering text, process appropriately. */
static const char *parse_value(cJSON *item,const char *value)
{
   if (!value)         return 0;   /* Fail on null. */
   if (!strncmp(value,"null",4))   { item->type=cJSON_NULL;  return value+4; }
   if (!strncmp(value,"false",5))  { item->type=cJSON_False; return value+5; }
   if (!strncmp(value,"true",4))   { item->type=cJSON_True; item-&gt;valueint=1;    return value+4; }
   if (*value=='\"')               { return parse_string(item,value); }
   if (*value=='-' || (*value>='0' && *value<='9')){ return parse_number(item,value); }
   if (*value=='[')                { return parse_array(item,value); }
   if (*value=='{')                { return parse_object(item,value); }
   ep=value;return 0;  /* failure. */
}

跟我前面的分类一样,很清楚了。至于空格,这个函数在每次被调用之前都会先调用一次skip函数,用来跳过空白的:

/* Utility to jump whitespace and cr/lf */
static const char *skip(const char *in)
{
    while (in && *in && (unsigned char)*in<=32)
        in++;
    return in;
}

查看ASCII码表就可以知道32之前的基本都是不可见的控制字符或者空白,这里的条件判断真是简单粗暴。

parse_array的过程已经说得比较清楚了,就是不断地跳过空格、读取逗号、再跳过空格、读取一个新的对象的循环……直到遇到反方括号。前面那个指针ep的用途也明白了,就是指向读取失败的地方。parse_object类似,只是每次循环还要插入一个读名字的过程。

输出部分没有什么特别值得提的地方(其实是懒),要注意的就是输出array和object的时候需要控制一下缩进。总的来说,cJSON的代码逻辑就是这个样子。阅读这样「接地气」的代码,好处在于能够快速学到很多这门语言的最佳实践,但是繁杂的工程细节也会让人厌烦。好在大一些的项目往往在抽象上做得更好,方便我们抽丝剥茧,寻得新知。

夏夜怨记

大概真的有很久没有认真写过一篇日志了。

拖延症想来真是可怕的一件事,从高考、到生日、到搬嘉定,一直想记录下这些时刻,然而最后都因为各种原因忘掉了。

本来说,这次回家,要好好看书,复习物理,认真把之前没做的事情做完。可没想到睡着睡着这暑假就睡过快一半了。从现在开始大概还来得及,但是真的有机会让我意思到「快来不及」吗?

从去年七月底学籍变更开始算起,转到软件学院基本上算整整一年了。说没学到什么东西,那是假的。但是能不能合上当初对于大一的预期呢?恐怕这里不是个问号,甚至都应该是感叹号了。

大一开学第一门课好似一记闷锤,重重地把暑假群里的欢声笑语打得不知所踪。我本是个好为人师的人,也因此,有不少人认识了我。虽说常常觉得会有人因为此对我颇有微词,但是更多的时候笑一笑,只是觉得帮助别人挺好的,足矣。

说Deadline是第一生产力,这话我同意一半。Deadline只能带来产品,带不来作品。除了紧迫感以外,要创造出作品必须的还有一种「渴望」。这样的「渴望」是发自内心的,但对于常人而言,要利用它创造作品,恐怕这期限得是无穷远加上一个epsilon,所以它和期限交织在一起,就是促发我们为作品拼命的动力。

既然半年混一混就过去了,那再来半年似乎就更不是问题了。下半个学期过得更加无趣和无所事事。Cocos已经足够不能带给人成就感了,更何况一个拖了许久需求一删再删的半成品游戏。年级群也在变得越发沉闷,只有俱乐部还能带给人些许慰藉。

其实说到底,这些问题都是自己没有规划没有坚持造成的。规划是简单的,大不了发现有问题再改。难的是坚持。你说每天写一篇日志容不容易?好像也不难。但是甫一打开后台,脑子里各种毛病就来了。觉得浪费时间也好,质量不高也好,一天天就混吃等死推过去了。吹牛的时候我老喜欢提自己初中就学编程的事,然而到今天有太大正面影响吗?还真的没啥。

这并不需什么天才,那个年纪就是能自学那种程度的知识和技能。问题是环境是否容许,例如放学后去办公室搞到午夜才回家,同时学业成绩不能太差。

在知乎上看见Milo谈小孩子学编程的时候说的话,深以为然。如果时间能够给我再一次机会,我真的会做出不一样的选择吗?

在软件这一年,有收获,有遗憾,但这段经历比不上大一一年带来的震撼。回头看自己一直以来的读书经历,好像也能总结出一个道理:

永远不要试图框定自己的界限。否则,你总会发现外面还有所遗漏。

汶川地震八年记

看见今天各大新闻的头条,不知不觉,那场地震已经过去八年了。

尽管身为离四川很近的重庆人,然而每次跟别人聊起地震的时候,我都无比确信我那个时候真的毫无震感。而一切的记忆都是那么清晰而真切,虽说不上就像在昨日发生一般,却也成了生命光影里深深定格的镜头。那个时候刚刚上美术课,被我们调侃过无数次的美术老师让我去隔壁的办公室拿一盒彩色粉笔。还没等我走到办公室门口,就见到人们一窝蜂冲了出去往楼下跑。我当时并不知道怎么回事,也确未感知到地震的来临,所以满脸不解的表情,甚至还以为是马蜂来袭,同学们集体外撤。大概等到人已经冲出一半了,我才听见有人喊到一声「地震了」。此刻的我虽然紧张却更不愿往下跑了——老师不是教育过我们,地震来临要躲在桌子下面吗?不过几乎所有人都跑出去了,外加城乡结合部小学的建筑质量确实也不太能让人信赖,我还是跟着下楼,成了最后出去的人。

可能我对事故的嗅觉从来都是人群里最迟钝的那一类,地震是,外滩踩踏亦是。大家都下撤到教学楼下的操场以后,老师开始点名,看有没有遗漏的人。(尽管我一直觉得,如果地震真的把那教学楼震踏了,我们在球场上根本不能幸免)点名完毕后就组织回家,联系家长。我走出学校,径直走向家附近的一家商店,将近十个中老年人围着一个电视机说着什么。我也凑上去看,才知道刚刚是四川汶川地震了。电视台一次又一次修正地震的震级,不过对当时的我来说,就是知道那很厉害而已了,而对于震源地的死亡,也实在没有概念。

我清楚地记得那天的一切,记得我回去还玩了一把无双大蛇,还看了一集记单词教程。还有工人们坐在路边的聊天。我都记得。不过,对于不幸的朋友而言,可能所有的记忆到那一刻就完全停止了。一个小学生大概真的不太容易明白如此巨大地震的后果。意识到这是一件会打破正常学校生活秩序的大事以后,心里反而有些许奇怪的兴奋感。当然,那段时间的电视新闻永远只有这一个主题。无数的故事,生离死别,当时的我不太能体会这种沉重,但对失去亲人的悲怆,我也有具体的想象。

重庆人都喜欢开玩笑说重庆是一个安全的地方,水火不侵。不过一当有传言曰大余震要来了,市民们也紧张得很。那天晚上社区的家家户户都收拾睡具到球场上过了一夜。我睡不着,于是夜游,那经历甚是有趣。当然夜游的经历不只这一次,不过每次,似乎都跟死亡有那么点点关联。

再然后就是全国哀悼日了。那几天网游什么的都是不能玩的。唔,不过我也没有什么兴趣。小伙伴在楼下唤我出来,做什么我忘了,大概也是吹牛吧。毕竟那是一个放学之后到同学家门口就能吹一个小时的年纪啊。

说了这么多,似乎都是无病呻吟,没什么实在的意义。是。只是借此回忆一下,回忆一下那时那个对初中生活还有充分幻想的我自己。毕竟这个时间节点实在是太特殊。当然,许多无辜的生命,就突然停止在那个时刻,他们的故事再也没有机会被续写。活着的人呢,也许素不相识,因为一场地震而相遇。人生实在是太神奇。地震灾后的修复也许早已完成,但它的回音,似乎远未结束。

谁也不知道未来会发生什么。

cJSON源码分析-接口

最近因为各种忙碌,博客一直没有更新。下半学期的ACM/ICPC报名已经过去了,我没有参加,因为觉得自己对这种比赛没有什么特别的兴趣。其实话说回来,要和人「当面比」的事情,我很多都不喜欢。大概是因为从小到大的不自信导致的。这边Rails的项目快要写完了,收获不少,会找个时候专门用一篇博文记述。喜欢编程,所以就总是停不下来,想找点事情做。大家都说,提升编程能力要做项目。这话不假。但是编程就像写作文,初中生洋洋洒洒写个小说,很可能只会被成年人看作是幼稚文章。编程作为一门手艺,阅读他人的源码也是很重要的。感谢自由软件运动让我们有大量的源码可以用来学习,也感谢GitHub这样的平台能让我们更加方便地获取和发布源代码。所以最近可能会陆陆续续地更新一些程序的分析,也借这个过程提高一下自己细粒度层面的编程水平。希望不会烂尾。(flag已立!)

要分析呢,就从最熟悉的语言开始吧,也就是C咯。然后找一个简单的开源项目。好啦,中央已经决定了,就是cJSON了。同济2014级软件学院的C语言期末作业就是要求写一个cJSON解析器,后来我才发现那个提供的头文件就是cJSON里的……

软件的第一要义就是,它创造出来必须有用。那cJSON,顾名思义,就是一个程序,它能够:

  1. 将文本化的JSON转化为C语言可以操作的数据结构(解析)
  2. 将内存里的数据结构输出为格式化的JSON字符串(序列化)

具体JSON是什么格式,不用我再多说了吧。在JavaScript流行的今天,JSON由于其简单的语法和支持嵌套的特点,得到了广泛应用。许多框架的配置文件什么的,都是用的JSON格式。也有许多人试图用JSON逐渐取代XML语言。

我们先来看源码的文件结构:

  • cJSON.c
  • cJSON.h
  • test.c
  • README.md
  • LICENSE
  • CMakeLists.txt
  • Makefile
  • cJSON_Utils.h
  • cJSON_Utils.c
  • test_utils.c
  • test/

其中末尾带util的都是针对JSON做的一些扩展(RFC6901和RFC6902),我们先略去不谈。其实作为库,核心部分就两个文件,一个cJSON.h声明,一个cJSON.c实现。那么我们就先来看看头文件,cJSON到底提供了哪些接口。

/* cJSON Types: */
#define cJSON_False  (1 << 0)
#define cJSON_True   (1 << 1)
#define cJSON_NULL   (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array  (1 << 5)
#define cJSON_Object (1 << 6)

#define cJSON_IsReference 256
#define cJSON_StringIsConst 512

/* The cJSON structure: */
typedef struct cJSON {
    struct cJSON *next,*prev;   /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *child;        /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */

    int type;                   /* The type of the item, as above. */

    char *valuestring;          /* The item's string, if type==cJSON_String */
    int valueint;               /* The item's number, if type==cJSON_Number */
    double valuedouble;         /* The item's number, if type==cJSON_Number */

    char *string;               /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
} cJSON;

typedef struct cJSON_Hooks {
      void *(*malloc_fn)(size_t sz);
      void (*free_fn)(void *ptr);
} cJSON_Hooks;

整个分成三个部分,一个是标记Type的宏,包括了cJSON结构体里type成员的所有取值。它在这里额外增加了IsReference和StringIsConst两个类型标记。我们注意到作者表示cJSON的类型不是用的正常的自然数的顺序排布,而是利用位移运算构成了等比数列。为什么要这样呢?因为这样的话一个类型就可以和IsReference和StringIsConst叠加了。这是C语言里的常用技巧。

再往下,我们可以看到作者定义了一个叫做cJSON_Hooks的结构,包含了malloc_fn和free_fn两个函数指针作为成员。很容易看出来这两个函数指针的原型也刚好对应malloc和free的函数原型。虽然还没有开始阅读源代码,不过我们自信地猜想,这个结构的作用类似于C++ STL中的allocator,负责标准分配之外的分配方式。话说我一开始写maolang的容器的时候也用过这个方法,但是后来觉得太累赘而放弃了。

再看下面的代码。

/* Supply malloc, realloc and free functions to cJSON */
extern void cJSON_InitHooks(cJSON_Hooks* hooks);

/* Supply a block of JSON, and this returns a cJSON object you can interrogate. Call cJSON_Delete when finished. */
extern cJSON *cJSON_Parse(const char *value);
/* Render a cJSON entity to text for transfer/storage. Free the char* when finished. */
extern char  *cJSON_Print(cJSON *item);
/* ... */

/* Returns the number of items in an array (or object). */
extern int    cJSON_GetArraySize(cJSON *array);
/* ... */
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
extern const char *cJSON_GetErrorPtr(void);

限于篇幅,把更多的函数声明省略了。函数声明前面加上extern关键字是可选的,仅仅是标注它是一个外部链接的函数而已。然后是一堆用以创建、删除、插入、修改JSON结构的函数,更详细的内容在头文件里。

/* Duplicate a cJSON item */
extern cJSON *cJSON_Duplicate(cJSON *item,int recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
need to be released. With recurse!=0, it will duplicate any children connected to the item.
The item->next and ->prev pointers are always zero on return from Duplicate. */

复制的函数有一个额外参数,表示是否选择递归复制(深拷贝)。这里还有一些函数,我们分析实现的时候再说。在头文件的最后还有一个有趣的宏:

/* Macro for iterating over an array */
#define cJSON_ArrayForEach(pos, head) \
    for(pos = (head)->child; pos != NULL; pos = pos->next)

虽然仅仅是简陋的宏替换,不过还真是搞出了现代语言的感觉呢。

看完头文件之后,我们发现,cJSON这个简单的解析器,名堂却不小,提供了不少实用的接口。至于这些接口内部实现的细节,我们下一篇文章再来讨论啦。

计协,南京和其他

这篇日志本来是想周四晚上写的,结果拖着拖着拖到了现在。

虽然学籍上仍然是个大一学生,但是已经是在计算机协会的第四个学期了。计协的活动,前前后后在时间线上串起来,大概就是我将近两年的大学生活。大一的时候,班上女生多,上海女生尤其多,本来班级活动也少,大家待了大半个学期都不认识。计协反而成了寄托。如今回头看那个时候的照片,还会惊叹——原来这些事情都是第一个学期发生的啊!要知道,彼时我还每周认真写高数作业,还没有想好要去软件还是计算机。往后的日子就如眨眼般过去了,连我自己也到了要去嘉定的时候。

大一初到上海,陌生的城市,陌生的人,未知的未来,都在不断地敲打自己。那个时候被「抓进」医院住院,在病房里坐着看着窗外的赤峰路。嘉定是什么样子呢?大四的时候……哦大四还好遥远呢?殊不知,如果我没有留级的话,这四年已将近走完一半了。实话说,在我不知道同济计协的时候我就有加入这类组织的想法了。我记得初中的时候学校就有跟编程有关的社团,可惜那是高中部没法加入。在网上发现一位高我一年级的VB高手,两人还一见如故惺惺相惜。可惜后来断了联系,也不知他现在在哪里。高中的时候呢,觉得老师太蠢,于是把OI放弃了,后来还是有那么点后悔的。可是后悔这种东西没有意义,让我回到那个时候再选一次大概还是一样的决定。

说来好笑,在病房里看贴吧关于百团大战的贴子,把所有跟计算机有关的社团全部默默记了下来。所以第一次百团的时候一下看到了计协就报了名。有趣的是,我拿传单的瞬间还刚好被拍下来了。

面试的时间在十月七日,如今都还历历在目。国庆第一天就去了南京,第一次一个人坐火车还是挺有意思的体验。第一个夜晚到了南京还不觉得,第二天开始就有了一种深深的无力感,用自己的话说就像是在住院。现在想来,大概是因为第一次来到一个谁也不认识的城市,住在酒店里,感到自己对身边的一切毫无掌控力。人类就是这样盲目乐观的动物,看上去,一切都秩序井然。殊不知人类创造的很多规则和理念都是很脆弱的。大众都为大城市的繁华而惊叹,倘若是战争一来,几千万人的大城市一失去秩序就是人类历史上难以想象的灾难。地狱当然是存在的,比如1945年的柏林。

又说远了。面试当然是没问题,见着这些学长还挺激动。那个学期的活动还挺丰富的,从义诊到活动周到计协十周年,我还混过一个小组长当,哈哈哈。后来是下学期的参观七牛,计协一刻钟……说到这里语词混乱也不知道该再讲什么了。只是,真的想感叹一下时间的流逝。一年前我就告诉自己要好好抓紧时间多看点书,结果一年过去了还是明日复明日明日何其多。可惜没有办法,谁叫我是大一呢?谁叫我还要学这么多不得不学的乱七八糟的课呢?哎,已经是毕业生的心了,却还是个新生的籍。

过去了两天,当时的那种心情已经被冲淡了许多。那天晚上看到老照片,真的好想好想感叹。室友问我,「你对这个社团看来很有感情啊」。「当然」。

C语言里的void类型

今天去图书馆坐了坐,看罢《Essential C++》,觉得过分基础,实在没什么意思。碰巧包里还有一本神作《C标准库》,详述了实现ANSI C标准库的所有过程。第一章讲的便是assert.h的实现。这个宏本身没有什么难度,无非是在一个函数的基础上包装一下,说不定GCC或者Clang直接把这个函数做成builtin了。不过不管是书上还是musl库的代码,都有一个让我注意到的地方:

#ifdef NDEBUG
#define    assert(x) (void)0
#else
#define assert(x) ((void)((x) || (__assert_fail(#x, __FILE__, __LINE__, __func__),0)))
#endif

这里的这个__assert_fail就如同我上文所说,是一个用于输出内容的函数,作用是输出错误信息然后调用abort函数退出。

关键在于这里有个奇怪的(void)0表达式。首先我们可以判定表达式的类型是void,对吧?不过这个void类型的表达式有什么意义呢?我们学习C语言的教材对这一点基本都语焉不详。我尝试了一下,把(void)0赋给一个int类型的变量,编译器是这样给我抱怨的:

test.c:3:9: error: initializing 'int' with an expression of incompatible type 'void'
  int i = (void)0;
      ^   ~~~~~~~

反正意思就是无法把这个类型转换成匹配的int啦。

那我们转念一想,尝试用void来定义一个变量呢?得到这样的错误提示:

test.c:4:10: error: variable has incomplete type 'void'
  void j;
       ^

等等!incomplete type?哪里见过这个?对啦!如果在一个结构体内部定义一个以这个结构体为类型的对象,编译器就会抱这个错误,提示类型还没有定义完。所以写链表或者二叉树的时候,里面存储的其实是「指向节点的指针」。

继续带着疑惑,我查询了C11的标准草案(正式版是收费的,不过两者在这些基础问题上相差无几),其中有三处提到了我想知道的「void类型」:

…The void type comprises an empty set of values; it is an incomplete object type that cannot be completed…

…An lvalue is an expression (with an object type other than void) that potentially designates an object…

The (nonexistent) value of a void expression (an expression that has type void) shall not be used in any way, and implicit or explicit conversions (except to void) shall not be applied to such an expression. If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A void expression is evaluated for its side effects.)

而《C程序设计语言》里这样描述:

void对象的(不存在的)值不能够以任何方式使用,也不能被显式或隐式转换为任一非空类型。因为空(void)表达式表示一个不存在的值,这样的表达式只可以用在不需要值的地方,例如作为一个表达式语句(参见A.9.2节)或作为逗号运算符的左操作数(参见A.7.18节)。

可以通过强制类型转换将表达式转换为void类型。例如,在表达式语句中,一个空的强制类型转换将丢掉函数调用的返回值。

这下终于明白了!那么稍微总结一下:

  1. 在C语言中,void可以作为一个合法表达式的类型,亦即它在语法结构里可以作为一个表达式
  2. void类型的表达式不能转换为其他任何类型的表达式,也就没有了赋值的可能
  3. 编译器会特殊看待void类型,将其作为一个「未被定义完整」的类型,也就没有了定义变量的可能
  4. 尽管void未被定义完整,但是如同其他结构体一样,我们是可以正常使用void的,并且直接对一个void解引用,结果是void类型的表达式
  5. void类型和其他类型不相容,但是该有的表达式副作用还是会有

可以说这里面的逻辑是非常自洽且合理的。所以不得不佩服设计C语言和C++的人,这些概念就像物理定律,看上去复杂,但是用这套逻辑推导下去很多看似不同的东西都可以得到统一解释。

现在回头看assert宏的实现代码,不难理解啦。因为__assert_fail函数的返回值类型是void,但是它需要被用在一个逻辑表达式里。于是它巧妙地结合了逗号运算符,配合短路求值的规定实现了assert需要的效果。至于把最后的表达式类型也转换为void,是为了不让它作为值被赋给变量。而在定义了NDEBUG的状态下,用(void)0占位也比什么也不写来得好,编译器会提示类型不相容,而直接替换成空的话,在代码复杂的地方错误类型也许会莫名其妙。