浅淡“人如其‘码’”
——看一道C基础笔试题有感
又是好久没有写博客,年底比较忙,《Performanced C++》系列也在努力酝酿但没有更新。不过近日出笔试题时,看到个题,以及各种不正确答案和烂代码,感触实在太多。
忘了以前在哪看过,说笔试时候写的代码虽然只有短短几行至几十行,但却能完完全全把一个coder的真实水平体现的淋漓尽致,正所谓古有“人如其文”,看他写的文章就知道他有多少斤两。对于我们coder而言,就是“人如其‘码’“,一道看似简单的笔试题,其实完全可以考察出coder至少以下n个方面:
1、代码风格和规范:从落笔第一刻开始体现
2、异常安全的“危机感”:从是否对传入参数的有效性有“敏感”开始,记住一条准则:永远不要信任用户的输入,这也是防SQL注入等手段的最基本原则之一
3、类型安全的“危机感“:很简单的其中一个方面,是否考虑过,诸如指针转换是否安全这样的问题
4、思路清晰程度:很简单, 从写下的代码是否涂了又改,改了又涂的多少程度就可以看出来。但完全没有任何涂改的答案,通常可能知道这个题的答案背下来的,那么,面试时稍加深入一问,就不攻自破
5、算法和数学水平:抽象的讲即“解决问题的能力”,就是最终是否能给出一个正确解,不管代码是否烂、有无考虑其它方面的问题
6、性能优化思想:是否有性能优化、考虑给出最优解的思想,即使最后的答案不是最优的,但在能保证正确性的基础上尝试过努力
7、对所使用的语言的理解程度:这条可能很多人忽视了。对于C语言的笔试,这一条旨在要求笔试者,尽可能从C语言的思考方式——底层,来解决问题,通常就是操作指针,而不是使用库函数甚至第三方“高级”库,假设是Java笔试,是否有OOP的设计思想。当然,明显可以看出,使用C语言笔试更有区分度。
8、平时所写或所读的代码量:通常可以有个估计,起码可以判断笔试者是否有读过某些名著,因为读过就可以把此题至少解的半斤八两
9、移植性如何:很简单,通常对于字符串操作的函数,如果想要声明int是使用的是size_t,证明你考虑过这方面的问题,起码看过一些源代码
……想起来再补充
好了,上面说了很多废话,下面看这篇文章的主要内容:
笔试题,C语言,实现strstr函数。这是一道基础得不能再基础的C语言笔试题,但我google了半天,发现能给出真正正确答案的,却不多。由此可见在真正笔试中,如此一道基础题目,如果严格按照上述几条来判定,足以刷掉80%以上的笔试者。
假设我现在是一个笔试者,拿到此题,假设我不知道strstr是干什么用的(其实题目会告诉你,要求你实现的这个函数的作用。但是,由于strstr是标准C库函数且有点儿常用,而你不知道他干什么用,那么基本可以假定你没有用过这个函数或是用过了也不熟悉,更不会清楚其内部实现,那么也就基本可以假定,你对标准C库不熟悉,也不会清楚其内部实现,那么也就基本可以再假定,你对C语言并不熟悉,从而假定你对以下东西都不怎么熟悉:Unix/Linux、GCC/GDB……从而基本假定这个C语言的职位并不合适你),那么最好的方法就是暂时不去参加笔试,特别是C语言笔试,详细阅读《The C Programming Language K&R》、《C A Reference Manual(5th Ed)》、《UNIX环境高级编程》等世界名著,并自己在Linux下把上述书中提到的标准C库函数都coding一遍,再上战场。
现在假设我知道strstr的作用,很简单,不就在s(src)源字符串中查找目标字符串t(target)吗?找到返回第一次匹配源串中的开始指针,没找到返回NULL。
下面是一个正确的实现:
1 const char* mystrstr(const char* s, const char* t)
2 {
3 const char *p = s; const char *q = t;
4 if(NULL == s || NULL == t)
5 return NULL;
6 while(*s)
7 {
8 p=s;q=t;
9 while(*p == *q && *q != '\0')
10 {
11 p++;
12 q++;
13 }
14 if(*q == '\0')
15 return s;
16 s++;
17 }
18 return NULL;
19 }也许乍看并不觉得这段代码有什么了不起,但如果你和以下的烂代码比较(甚至都不正确),就会发现上面这段代码是多么的elegant(优雅)。
另外要提一句,Linux Kernel/Gcc并非如此实现这个函数,因为这个函数的算法和性能都不是最优的,在上述几个方面的6并不能得到高分。但如果在笔试中能写出这样的答案,通常已经可以得到满分了。Why?因为上述这段代码,首先一点:自己完全从底层(还不够底层?那只能嵌入汇编了)实现这个函数,而没有调用其它任何库函数。我想这是C语言笔试最基本的常识之一。说到这里又要吐槽了,我年轻时候参加一加做金融类软件的公司的笔试,C++的,其中有一道题,把一个Unix时间戳(1970年开始的秒数)转换为当前时间。OK,小case,我写了半天,自己经过计算完成了这个函数。结果面试时,面试官一看就愣了,问我,这个函数要这样实现吗?我一听以为自己写的有问题,没想到面试官说:“你不会用时间函数吗? 不会用时间函数库吗?不管哪个语言都有时间函数吧?!需要自己来实现吗?!你自己写,可能写对吗?!这个时间计算很复杂的,你怎么可能那么快就写出来了?!”于是接下来,愣的就是我了,我当时想问一句“你需要写代码吗?不会买产品外包吗?不管什么需求都可以买到产品或外包吧?!需要自己来开发吗?!……”不过我还是忍住了,毕竟笔试者是弱势群体。有趣的是,这位奇葩的面试官还通过了我的笔试,让我过几天再去复试,我没敢去。我也不知道这样的人怎么能成为该公司的技术管理人员的。我只知道,我回家后,把当天写的时间转换函数又写了一遍,在Linux上跑了几个小时没发现计算有错误。吐槽了那么多,想说的无非就是,既然选择做技术,就要尽可能专业,因为这个行业,是“科学”,实在不是“差不多”、“马马虎虎”、“年底写个报告”、“忽悠忽悠“就OK的行业,如果不能适应这样苛刻的规则,转行是最好的出路。
偏题了。接着说上面的笔试题。上面给出的答案,算法并不是最优的,但其它方面做的很好,而且最容易理解。它是O(n2) 的时间复杂度,熟悉算法的同学,当然知道KMP或其它算法,可以将它优化至O(N)。当然,因为此题考察的并非是KMP算法,而是上述几个方面中除算法优化的其它所有方面。
下面开始看烂代码。
烂代码一:看似OK,其实根本就不正确的代码(出处我就不转了),你能发现哪有问题吗?
1 //烂代码1
2 char* _strstr(char* s1, char* s2)
3 {
4 char * p, *r;
5 p=s1;
6 r=s2;
7 assert(s2 && s1);
8 while(*r!='\0')
9 {
10 while(*p++==*r++);
11 if(*p=='\0')
12 return s2;
13 else
14 {
15 p=s1;
16 r=++s2;
17 }
18 }
19 return NULL;考虑最简单的一个test case:s2为"abcdef",s1为“deg",上述代码竟然输出s2中'd'的地址!,就是认为在s2中可以找到"deg“!
另外,写这个代码的同学,把s2和s1的参数逻辑顺序搞反了,也就是说,标准C库函数strstr的第一个参数,应是源串,第二个参数是目的串(就是要在源串中查找的串)。这位同学刚好到了过来,不仅代码阅读别扭,而且这种与标准C库相背的行为,也是非常不提倡的,同时,代码中的assert判断虽然体现了异常处理思想,但手法十分业余。此答案得分为0,因为连起码的正确性都不能保证。
烂代码二:改进了一下,但还是不对,你能看出哪又有问题吗?
1 char* _strstr(char* s1, char* s2)
2 {
3 char * p, *r;
4 p=s1;
5 r=s2;
6 if(NULL == s1 || NULL == s2)
7 return NULL;
8 while(*p!='\0')
9 {
10 while(*p == *r)
11 {
12 p++;
13 r++;
14 }
15 if(*p=='\0')
16 return s1;
17 else
18 {
19 p=s2;
20 r=++s1;
21 }
22 }
23 return NULL;
24 }再考虑一个最简单的test case:s1,s2均为"abcdef",上述代码竟然可能输出NULL!认为没有匹配到目标字符串。得0分。
烂代码三:可以正确工作的代码,但风格实在太差,又是for,又是临时变量,两次和'\0'的比较竟然风格不一致……这位同学,可以将你的代码写的elegant一点吗?
1 char* my_strstr( char* str1, char* str2 )
2 {
3 if (NULL == str1 || NULL == str2)
4 {
5 throw;
6 }
7
8 char *p = NULL;
9 char *q = NULL;
10 const char v = '\0';
11 for (int i=0; v != str1[i]; ++i)
12 {
13 p = &str1[i];
14 q = str2;
15
16 while (v != *q && *q == *p)
17 {
18 ++p;
19 ++q;
20 }
21
22 if ('\0' == *q)
23 {
24 return &str1[i];
25 }
26 }
27
28 return NULL;另外,既然是C代码,throw从何而来?这就是用C++来写C的例证,通常就是对所使用的语言没有较深入的理解,而是只知皮毛,通常也是各种低效垃圾代码产生的前罩。得分5(满分10)。
现在你知道了,这么基础的一道题,为什么那么多人写不对?并不是说这些笔试者水平就一定差,但通常就是细心和认真的程度不够,即使是在自己的笔试中。
更多烂代码就不再举例了,写这篇文章的目的,就是希望自己和众园友能引以为戒,Try your best to make your code elegant!