记一个愚蠢的bug

我们操作系统的大作业是模拟一个文件系统,我的想法是搞一个二进制文件出来,然后针对这个二进制文件模拟操作系统的管理方式。但是在 login 的时候,我就遇到了麻烦。这个bug我查到凌晨三点才发现,最后真是哭笑不得,觉得很有必要写一写。bug虽然很蠢,但是对我的教训却是很深啊。

我想要实现的是,将用户id和名字密码等信息永久地保存在一个二进制文件中。用的文件操作函数是 fwrite函数 和 fread函数,将一个用户结构体直接写到文件里面去,用到的时候再读出来。我的 user 结构体是这样写的:

然后就发现了这样的错误:登陆之前必须初始化文件,然后添加用户,删除用户,切换用户登陆都没有问题。但是一旦退出再次进行调试,保准会运行异常挂掉。不能做到永久存储。

再扯一下我试过的调试方法吧。因为对二进制读写用的不是很多,所以就以为是文件读写有问题,于是查了一堆fwrite和fread的用法和参考,搞了一堆demo来测试——都没问题。然后以为是流程问题,搞了堆输出来测试,也没问题。后来才用了跟踪调试,老老实实一步步跟进去,才发现了问题。

答案就是:string类型存放的其实是一个指针,并不是实际的字符串。而fwrite是将整个结构体作为二进制写进去的,也就是说存了个指针。一旦退出程序,下次启动,这个string存放的地址就不再是用户名和密码了,变得毫无意义。正确地方式是,使用char型数组来存放。如下:

发现这个的时候,凌晨三点,周围很静。我一下子想起来一周之前在机房问Wii要用什么语言写,他说C,我嘲笑说pure  C? low。现在终于吃到了苦头。对着屏幕傻笑了好一会儿。

教训总结:

  1. 以为STL会带来很多便捷,就想当然地使用了C++来写,太愚蠢了。最近看的一些系统的书,都提到要使用C,甚至是汇编。即使是pure C,从0实现的时候,也不能使用printf这种函数,甚至还要针对gcc等编译器进行优化,相当底层。我都有点觉得,汇编才是最好的编程语言了!
  2. 一定要区分C和C++的区别。这点仁者见仁吧,我以前把秋波老师的话奉为信条,他就说过:我管你是C还是C++,能写出程序来就行了,老朱也说过,除了io的时候用C,别的混用无所谓的,AC就行。现在自己觉得,区分开来很重要!C++面向对象,拥有丰富的STL。C效率高,偏底层。该用什么用什么,用C++,就要用C++的风格,比如用迭代器替换i++之类的。
  3. Always ask WHY.学知识一定要学的明白,懂基本原理,使用这方面,倒是可以一点一点来熟练。基本不扎实,是我花了这么长时间调试的根本原因。string是个对象啊!那它存的就是个指针啊!二进制写进去这么个对象,怎么能类似”深拷贝“地把对象也写进去呢?从逻辑上想想,这真的是一个很白痴的错误。现在想想,是很简单,但是在学习的时候想到”这里保存的是指针“这一点会引发什么样的后果,却很难。需要认真思考。

很多东西,说说简单,不自己吃点亏,是记不住的。

 

一年炉石传说的游戏体验

炉石传说这款游戏,最先是从玩星际争霸2的朋友那里知道的,那时候刚发布,大家都去申请了公测账号,当时也跟着去试了一把。当时的兴趣都在星际2上,所以玩了几把就忘掉了。直到去年去苏州玩,旅馆里无聊,又开始玩了这个游戏,重新发现了这个游戏的乐趣,就整整玩了一个暑假。到现在,玩了大约有十个月吧,昨天终于把这个账号卖掉了,算是给这段游戏经历画上了句号。

Hearthstone_Logo_CN

这是非常值得回忆的一段游戏时光。有段时间,每天起床都特别开心,因为能开新的游戏卡包了(这个游戏的日常任务能得40或60金币,100金币买一包卡)!一个卡包里面有5张卡至少有一张稀有,有几率开出史诗,传说(非常珍贵)。我现在几乎能记起来每一张传说卡的瞬间!一张传说能带来几天的好心情。正因为这个游戏的这种集卡机制,使得我对账号的感情比较深,毕竟都是自己一张一张开出来的。

Card Box_Cinematic

这应该算是一款相当成功的卡牌游戏了。游戏界面制作精美:UI全部设计在一个盒子里,这种风格贯穿始终;游戏采用“酒馆”的世界背景,每次官方消息,游戏指引等都已酒店老板自称,非常有意思。比如说,玩家数超过三千万,官方新闻说“我们需要更多的椅子了!”;对战界面也安排的极为精致,代入感很强。卡牌的获得方式算是一大创新吧,除了副本奖励的卡牌能够稳定获得,其余的卡牌都需要开扩展包来得到。而通过扩展包开到的卡牌,是完全随机的。

hotelboss

炉石传说最大的特点,就是卡牌的平衡性了。成也是它,败也是它。可以说,这是所有暴雪游戏的特点了。单说这款游戏中,平衡性就体现在,每一张卡牌都有独到的用途,虽然有些逗比卡,但是每一张都是不能被完全替代的。Amaz就说过,每一张都是好卡。我相信像米尔豪斯这种卡,在将来都会有不可替代的用途。在天梯就体现在,没有一套卡是百分之百无敌的,一套卡的兴起,必然会引起另一套克制的卡组流行。肯定会有人用T7猎,机械法,动物园,这种横行天梯的卡组来反驳我。只能说,概率而已。这种卡组对起手要求低,“天糊”几率大,就像在星际2里面,低分段神族4bg胜率高,一样的。并不影响游戏的平衡性。

Every Card is good card.

——Amaz

其中说的败,就是这个游戏太平衡了。如果你的卡比对手多很多,那么你的组合肯定就多得多,胜率一定高一些。加上游戏只有1v1的对战模式,就决定了它注定是一个孤独的游戏。和好友对战的模式单一,趣味性比较低。而且好友对战不是天梯,你的朋友卡组质量如果不是和你一个等级的,那么好友对战基本没法打。在对战中遇到的网友,顶多以后有观战任务能去看一下别人打,基本上也很少有别的交流,因为好友对战没有奖励,认识之后交流也不多。别的游戏例如星际2,魔兽世界中,认识朋友之后交流比较多。

Screenshot_8_full

另外提一点,关于交流。游戏关闭与对手对话功能,只有添加好友之后才能对话。我想,禁止骂人所带来的游戏体验肯定远大于弱化社交。但是游戏依然提供了内置的“你好”,“抱歉”等语音,依然可以用这些嘲讽。我在美服也打过,自己的感觉就是:美服称赞,问好用的比较多,国服基本都是“抱歉”,或者在失误的时候“打得不错”,来嘲讽。总体的玩家素质,大陆就是比较低,这是事实。

说了这么多,我AFK的主要原因是缺钱。次要原因是玩腻了,开卡包带来的惊喜少了,一局一局的对战也渐渐没有了感觉,已经厌倦了这种游戏模式。不过,这是一个超级棒的游戏。

 

《以撒的结合:重生》网页版图鉴

这个月以撒登陆 PSN 啦!最近又拾起了这个游戏,却发现好多道具都忘记了。《以撒的结合:重生》这款游戏中,有四百多种道具,而且游戏里面,道具没有说明,只有图片。所以在拾起来道具之前,很难知道道具的作用。

b6159350522b811dc19cef604e4d8b1c

于是,图鉴就成为这个游戏重要的辅助工具。以前我们用的图鉴,都是从一个以撒的 wiki 上面截图下来的,要找一个道具的话,需要翻多页。我想,如果做成一个网页版的话,就方便多了,点击一个道具,直接出来信息。所有的道具呈现在一页里面,查找方便。网上没有这样的图鉴,我就想自己做一个。

我的想法是:

  1. 写个程序将道具的信息格式化成 xml 格式;
  2. 前端主要将所有的道具图片呈现出来;
  3. 用 jquery 设置监听,点击图片之后,显示出道具信息。

在实际写的时候,才遇到了各种各样的麻烦,这篇文章中,将这些问题简单总结一下,大家如果有兴趣,可以自己深入了解一下。

C++操作中文

整合道具信息这块,我是用 C++写的。处理 string 很需要经验,我花了一个好长时间学习对中文的操作。中文是用两个字节表示一个字符的,单独获取一个字节没有任何意义。或许,可以针对中文字符写一个库?

监听 jquery 添加的元素

使用 jquery 添加的元素,是无法使用$()来选择,进行监听函数绑定的。这个项目中,我用了   .delegate( selector, eventType, handler(eventObject) )  这个函数来代替。这个函数可以匹配的元素包括那些目前已经匹配到的元素,也包括那些今后可能匹配到的元素。但是,它已经被 .on() 代替了。(on函数字太多了,我没看= =)

背景图片的适应

css 中可以对背景图片的大小适用只有以下这种:

  • auto:背景图像的真实大小。
  • cover:将背景图像等比缩放到完全覆盖容器,背景图像有可能超出容器。
  • contain:将背景图像等比缩放到宽度或高度与容器的宽度或高度相等,背景图像始终被包含在容器内。

看起来,好像 css 中只适合对那种合适平铺的图片作为背景啊。我的处理方式是:直接加一个图片的 div,这个图片的 z-index 设置为-1,即在所有的元素的下面,然后这个图片的宽和高都可以拉伸了!(但是joyme说我这样做 background,显得一点都不专业 = =,但也想不出别的办法呀)

现在想起来的问题,就这么多。吐个槽吧,想想这么简单的东西,真正做的时候,却遇到了这么多麻烦!尤其是图片拉伸,来来回回尝试各种方法,花了四个多小时!前端也真是不简单啊。更糟糕的是,自己花了那么长时间,给室友看,用的360浏览器,一打开布局全完蛋。前端工程师都是勇士!

最近熬了两天夜了,今天一定好好睡一下。第一天,主要实现了基本功能,可以查询了。

psb

 

昨天花了一天时间————加!特!技!duang!变成了这样子:

psb-2

 

今天又补齐了道具,把 Demo 挂在 Github 上了。大家有好的建议或者报错可以给我留言~或者在 Github 上提交 issue 或者 pr。

Demo

以撒的结合:重生 中文 在线图鉴:http://laixintao.github.io/isaac/

项目地址:https://github.com/laixintao/ISAAC-Atlas-online

 

 

分清 C++的指针、引用和数组

C++ 里面的指针常常使初学者感到困惑,尤其是和引用,常量,数组等组合起来的时候,一个变量是数组,还是指针,指针是常量,还是所指对象是常量就显得更模棱两可了,例如下面这些代码:

如果你也有类似的困惑的话,希望在读完本文之后,会有一个清晰的概念。

热身

一个内存位置,保存一个值,内存位置也可以作为值,这就叫做指针。在事情变得复杂之前,我们可以确定两件事:一个变量保存的要么是地址,要么是值。现在,我们从最简单的概念开始:

  1. 指针保存的是地址,解引用得到对象的值
  2. 引用类型是真实对象的“绑定”,可以当做这个对象的另一个名字来使用,对引用类型的操作,就是对真实对象的操作(引用必须初始化!)。取地址得到的是对象的地址
  3. 解引用 * ,取地址 & 具有双重含义:在表达式中作用如1 2 粗体所示;在声明中,它们用来组成复合类型。注意:int *p = i;

举个栗子:

那么问题来了:改变的是指针指向的对象呢?还是对象的值呢?

法则一:改变的永远是等号左侧的对象。

比如,第2行,等号的左侧是定义,r 是 int 类型的引用,引用“绑定”了 i;第4行,等号左侧是指针,那么改变的就是 指针的值,也就是所指的对象;第7行,等号左边是指针解引用,那么修改的就是指针指向的对象,即 i;最后一行,等号左侧是引用类型取地址,是一个地址,额,对一个地址赋值,很显然这一行是错误的……

注意:虽然上文说了某些符号在定义式中有另一种含义,但这还是常常使人混淆——*p不应该是变量的值吗? 我有个小技巧,将在定义式中的*理解成为类型的一部分,将它看成和 int 的组合就好了,像这样:int * p = i;

复合类型

上面的概念是不是很简单?接下来要将指针和引用相互组合,产生更复杂的类型了。

指向指针的指针

首先,指针先指向了一个对象。这个对象保存的是个地址,对这个地址解引用,可以得到令一个对象的值。

在内存中,是这个样子的:

pointer

指向指针的引用

由于引用不是真实的对象,所以不存在“指向引用的指针”,但却可以有“指向指针的引用。”这时,上文的规则同样适用,即作为指针的别名使用。

现在就有点复杂了,r 到底是指针还是引用呢?法则二:从右向左读。例如第三行的 r ,左边是 &,所以它是个引用类型,再向右,* 表示引用的这个类型是一个指针,再向右 int 表示,这个指针的类型是int,综合,r就表示“一个指向 int 型的指针的引用。

const限定符和constexpr

当 const 涉及指针的时候,可以分为两种情况:

  1. 顶层const:指针本身是个常量,不能改变所指的对象。
  2. 底层const:指针指向的对象是一个常量,不能改变或者通过指针改变这个对象的值。

不要慌,这里法则二依然适用:从右向左。对于 p1,右侧是 const ,表示 p1 是一个常量,向右,p1 是常量指针,再右,常量指针 p1 指向了 int 型。

小试牛刀:解释一下p2和p3吧!

constexpr

这是 C++11 的新规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。

“变量的值”,就是左值,说白了就是基本类型是不是一个常量。比如 a 基本类型是 int ,constexpr 就指定 int 型为常量。 p 是一个指针,constexpr 指定指针 p 是一个常量。由此,我们可以得到法则三:constexpr 声明中如果有指针,那么constexpr 只对指针有效,而与对象无关。

类型别名

类型别名顾名思义,就是将复杂的变量名起一个好听好记的名字。可以参考1-3行,这是类型别名的常用的例子(其实最常用的时候,是给长长的结构体取“外号”)。

看完了前3行之后,你可能觉得“就是个别名,那我理解的时候,替换成原来的名字就好了!”所以你就会像 p_c_str 那样,去解释 c_p_str :按照我们的从“右向左法则”,它首先是一个指针,然后指向 char 型,这个 char 型是 const 的,所以这是一个“指向char 型常量的指针”。

这是完全错误的!是我们的“从右向左”法则出错了吗?不是的,要相信,从右向左在任何时候都是对的,如果它错了,那么一定是你错了——你错就错在,将 typedef 指定的别名拆开理解了。正确的理解很简单,对于 typedef,你要从心底认为这是同一个类型的别名,pstring 是“指向 char 型的指针”,永远都是。对于 c_p_str,从右向左的时候,第一步是“它是一个指向 char 型的指针”,而不是“它是一个指针”。第二步理所应当的是“这个指针的类型是 const 的”,综合,c_p_str 就是“指向 char 类型的常量指针”。

法则四:typedef 定义的类型别名不要替换成原来的名字去理解!

小试牛刀,解释一下 ps 吧!答案是:“ps 是一个指针,它指向了一个对象,这个对象是一个指向 char 的常量指针。”

decltype 类型指示符

这也是 C++11 的新标准,编译器分析表达式并返回其类型,要注意的是,不会计算表达式的值。这点非常重要,因为上文说过,引用从来都是作为所指对象的同义词出现,但只有在deltype 的时候是个例外。原因如下:

第三行因为编译器阶段并不会对 *p 求值,所以直接返回一个引用类型。这个地方,引用就和所绑定的对象有区别了。(事实上,第三行是错误的,因为定义引用的时候没有初始化。)

法则五:在(且仅在)deltype 处,引用不是对象的同义词!

数组

引用可以绑定数组,指针可以指向一个数组,数组可以存放指针,但是唯独不存在引用的数组!参考下面的例子:

很显然,这里不能再用从右向左了,不然的话,”[ ]”永远也读不到了。对于负责的数组声明,要由(内)向外阅读——法则六。

例如 p_array ,首先,它是一个指针(内指的是括号内),然后类型修饰符从右向左再依次绑定:指向的是一个数组,这个数组是int型。 再复杂的数组,也可以以此类推。

Summary

  1. 改变的永远是等号左侧的对象。
  2. 从右向左读(类型修饰符依次绑定)。
  3. constexpr 声明中如果有指针,那么constexpr 只对指针有效,而与对象无关。
  4. typedef 定义的类型别名不要替换成原来的名字去理解!
  5. 在(且仅在)deltype 处,引用不是对象的同义词!
  6. 对于复杂的数组声明,要先从内向外读,再按照从右向左。

不是 So easy 呢?你会读了吗?

 

笑话三则

昨天在隔壁宿舍讨论算法的时候,一个室友大喊了一声:“卧槽,老子就是更新了个 QQ 而已,这 QQ 浏览器和 QQ 音乐是哪里来的!”

说起来,我昨天从一个网站上下载了一些扑克牌的素材图片,过了一会儿……打字的时候,输入法已经默默地变成了百度输入法……

不过还是另一位朋友更惨,他说:“有次回宿舍,手机顺手插在电脑的 USB 上就上厕所去了,回来的时候,手机已经装好了360……。”

形成鲜明对比的是,最近收到一封广告邮件,于是发回去问他们是怎么知道我地址的。竟然真的收到了礼貌的回复:

emailreplay