﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-SmartPtr-文章分类-3.1 收藏-技术</title><link>http://www.cppblog.com/SmartPtr/category/4848.html</link><description>一手键盘，一手鼠标；品读技术，把弄文章</description><language>zh-cn</language><lastBuildDate>Mon, 19 May 2008 19:00:15 GMT</lastBuildDate><pubDate>Mon, 19 May 2008 19:00:15 GMT</pubDate><ttl>60</ttl><item><title>[转]想成为嵌入式程序员应知道的0x10个基本问题</title><link>http://www.cppblog.com/SmartPtr/articles/30730.html</link><dc:creator>SmartPtr</dc:creator><author>SmartPtr</author><pubDate>Fri, 24 Aug 2007 00:50:00 GMT</pubDate><guid>http://www.cppblog.com/SmartPtr/articles/30730.html</guid><wfw:comment>http://www.cppblog.com/SmartPtr/comments/30730.html</wfw:comment><comments>http://www.cppblog.com/SmartPtr/articles/30730.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/SmartPtr/comments/commentRss/30730.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/SmartPtr/services/trackbacks/30730.html</trackback:ping><description><![CDATA[<em>作者:Jones Nigel&nbsp; 来源:internet</em><br><br>C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年，我既参加也组织了许多这种测试，在这过程中我意识到这些测试能为面试者和被面试者提供许多有用信息，此外，撇开面试的压力不谈，这种测试也是相当有趣的。<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 从被面试者的角度来讲，你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗？这是个愚蠢的问题吗？如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗？这标志着出题者也许花时间在微机上而不是在嵌入式系统上。如果上述任何问题的答案是"是"的话，那么我知道我得认真考虑我是否应该去做这份工作。<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 从面试者的角度来讲，一个测试也许能从多方面揭示应试者的素质：最基本的，你能了解应试者C语言的水平。不管怎么样，看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择，还是只是瞎蒙呢？当应试者在某个问题上卡住时是找借口呢，还是表现出对问题的真正的好奇心，把这看成学习的机会呢？我发现这些信息与他们的测试成绩一样有用。<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 有了这些想法，我决定出一些真正针对嵌入式系统的考题，希望这些令人头痛的考题能给正在找工作的人一点帮助。这些问题都是我这些年实际碰到的。其中有些题很难，但它们应该都能给你一点启迪。<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 这个测试适于不同水平的应试者，大多数初级水平的应试者的成绩会很差，经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好，每个问题没有分配分数，如果选择这些考题为你所用，请自行按你的意思分配分数。<br><br><span style="FONT-WEIGHT: bold">预处理器</span>（Preprocessor）<br><br>1 . 用预处理指令#define 声明一个常数，用以表明1年中有多少秒（忽略闰年问题）<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="COLOR: rgb(0,128,128)"> #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL</span><br>我在这想看到几件事情：<br>1) #define 语法的基本知识（例如：不能以分号结束，括号的使用，等等）<br>2)懂得预处理器将为你计算常数表达式的值，因此，直接写出你是如何计算一年中有多少秒而不是计算出实际的值，是更清晰而没有代价的。<br>3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。<br>4) 如果你在你的表达式中用到UL（表示无符号长整型），那么你有了一个好的起点。记住，第一印象很重要。<br><br>2 . 写一个"标准"宏MIN ，这个宏输入两个参数并返回较小的一个。<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; <span style="COLOR: rgb(0,128,128)">#define MIN(A,B) （（A） &lt;= (B) ? (A) : (B)) </span><br>这个测试是为下面的目的而设的：<br>1) 标识#define在宏中应用的基本知识。这是很重要的。因为在&nbsp;<span style="COLOR: rgb(102,51,102)"> 嵌入(inline)操作符</span> 变为标准C的一部分之前，宏是方便产生嵌入代码的唯一方法，对于嵌入式系统来说，为了能达到要求的性能，嵌入代码经常是必须的方法。<br>2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码，了解这个用法是很重要的。<br>3) 懂得在宏中小心地把参数用括号括起来<br>4) 我也用这个问题开始讨论宏的副作用，例如：当你写下面的代码时会发生什么事？<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; least = MIN(*p++, b);<br><br>3. 预处理器标识#error的目的是什么？<br>如果你不知道答案，请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子，那么应试者最好希望自己不要知道答案。<br><br><br><span style="FONT-WEIGHT: bold">死循环</span>（Infinite loops）<br><br>4. 嵌入式系统中经常要用到无限循环，你怎么样用C编写死循环呢？<br>这个问题用几个解决方案。我首选的方案是：<br><br>while(1)<br>{<br><br>}<br><br>一些程序员更喜欢如下方案：<br><br>for(;;)<br>{<br><br>}<br><br>这个实现方式让我为难，因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案，我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是："我被教着这样做，但从没有想到过为什么。"这会给我留下一个坏印象。<br><br>第三个方案是用 goto<br>Loop:<br>...<br>goto Loop;<br>应试者如给出上面的方案，这说明或者他是一个汇编语言程序员（这也许是好事）或者他是一个想进入新领域的BASIC/FORTRAN程序员。<br><br><br><span style="FONT-WEIGHT: bold">数据声明</span>（Data declarations） <br><br>5. 用变量a给出下面的定义<br>a) 一个整型数（An integer） <br>b)一个指向整型数的指针（ A pointer to an integer） <br>c)一个指向指针的的指针，它指向的指针是指向一个整型数（ A pointer to a pointer to an intege）r <br>d)一个有10个整型数的数组（ An array of 10 integers） <br>e) 一个有10个指针的数组，该指针是指向一个整型数的。（An array of 10 pointers to integers） <br>f) 一个指向有10个整型数数组的指针（ A pointer to an array of 10 integers） <br>g) 一个指向函数的指针，该函数有一个整型参数并返回一个整型数（A pointer to a function that takes an integer as an argument and returns an integer） <br>h) 一个有10个指针的数组，该指针指向一个函数，该函数有一个整型参数并返回一个整型数（ An array of ten pointers to functions that take an integer argument and return an integer ）<br><br>答案是： <br>a) int a; // An integer <br>b) int *a; // A pointer to an integer <br>c) int **a; // A pointer to a pointer to an integer <br>d) int a[10]; // An array of 10 integers <br>e) int *a[10]; // An array of 10 pointers to integers <br>f) int (*a)[10]; // A pointer to an array of 10 integers <br>g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer <br>h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer <br><br>人们经常声称这里有几个问题是那种要翻一下书才能回答的问题，我同意这种说法。当我写这篇文章时，为了确定语法的正确性，我的确查了一下书。但是当我被面试的时候，我期望被问到这个问题（或者相近的问题）。因为在被面试的这段时间里，我确定我知道这个问题的答案。应试者如果不知道所有的答案（或至少大部分答案），那么也就没有为这次面试做准备，如果该面试者没有为这次面试做准备，那么他又能为什么出准备呢？<br><br><span style="FONT-WEIGHT: bold">Static</span> <br><br>6. 关键字static的作用是什么？<br>这个简单的问题很少有人能回答完全。在C语言中，关键字static有三个明显的作用：<br>1)在函数体，一个被声明为静态的变量在这一函数被调用过程中维持其值不变。<br>2) 在模块内（但在函数体外），一个被声明为静态的变量可以被模块内所用函数访问，但不能被模块外其它函数访问。它是一个本地的全局变量。<br>3) 在模块内，一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是，这个函数被限制在声明它的模块的本地范围内使用。<br><br>大多数应试者能正确回答第一部分，一部分能正确回答第二部分，同是很少的人能懂得第三部分。这是一个应试者的严重的缺点，因为他显然不懂得本地化数据和代码范围的好处和重要性。<br><br><br><span style="FONT-WEIGHT: bold">Const</span> <br><br>7．关键字const有什么含意？<br>我只要一听到被面试者说："const意味着常数"，我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法，因此ESP(译者：Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章，只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案，但我接受它作为一个正确的答案。（如果你想知道更详细的答案，仔细读一下Saks的文章吧。）<br>如果应试者能正确回答这个问题，我将问他一个附加的问题：<br>下面的声明都是什么意思？<br><br>const int a;<br>int const a;<br>const int *a;<br>int * const a;<br>int const * a const;<br><br>/******/<br>前两个的作用是一样，a是一个常整型数。第三个意味着a是一个指向常整型数的指针（也就是，整型数是不可修改的，但指针可以）。第四个意思a是一个指向整型数的常指针（也就是说，指针指向的整型数是可以修改的，但指针是不可修改的）。最后一个意味着a是一个指向常整型数的常指针（也就是说，指针指向的整型数是不可修改的，同时指针也是不可修改的）。如果应试者能正确回答这些问题，那么他就给我留下了一个好印象。顺带提一句，也许你可能会问，即使不用关键字 const，也还是能很容易写出功能正确的程序，那么我为什么还要如此看重关键字const呢？我也如下的几下理由：<br>1) 关键字const的作用是为给读你代码的人传达非常有用的信息，实际上，声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾，你就会很快学会感谢这点多余的信息。（当然，懂得用const的程序员很少会留下的垃圾让别人来清理的。）<br>2) 通过给优化器一些附加的信息，使用关键字const也许能产生更紧凑的代码。<br>3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数，防止其被无意的代码修改。简而言之，这样可以减少bug的出现。<br><br><br><span style="FONT-WEIGHT: bold">Volatile</span> <br><br>8. 关键字volatile有什么含意?并给出三个不同的例子。<br>一个定义为volatile的变量是说这变量可能会被意想不到地改变，这样，编译器就不会去假设这个变量的值了。精确地说就是，优化器在用到这个变量时必须每次都小心地重新读取这个变量的值，而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子：<br>1) 并行设备的硬件寄存器（如：状态寄存器）<br>2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)<br>3) 多线程应用中被几个任务共享的变量<br><br>回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道，所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。<br>假设被面试者正确地回答了这是问题（嗯，怀疑是否会是这样），我将稍微深究一下，看一下这家伙是不是直正懂得volatile完全的重要性。<br>1)一个参数既可以是const还可以是volatile吗？解释为什么。<br>2); 一个指针可以是volatile 吗？解释为什么。<br>3); 下面的函数有什么错误：<br><br>int square(volatile int *ptr)<br>{<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; return *ptr * *ptr;<br>}<br><br>下面是答案：<br>1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。<br>2); 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。<br>3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方，但是，由于*ptr指向一个volatile型参数，编译器将产生类似下面的代码：<br><br>int square(volatile int *ptr) <br>{<br>&nbsp;&nbsp;&nbsp; int a,b;<br>&nbsp;&nbsp;&nbsp; a = *ptr;<br>&nbsp;&nbsp;&nbsp; b = *ptr;<br>&nbsp;&nbsp;&nbsp; return a * b;<br>}<br><br>由于*ptr的值可能被意想不到地该变，因此a和b可能是不同的。结果，这段代码可能返不是你所期望的平方值！正确的代码如下：<br><br>long square(volatile int *ptr) <br>{<br>&nbsp;&nbsp;&nbsp; int a;<br>&nbsp;&nbsp;&nbsp; a = *ptr;<br>&nbsp;&nbsp;&nbsp; return a * a;<br>}<br><br><span style="FONT-WEIGHT: bold">位操作</span>（Bit manipulation） <br><br>9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a，写两段代码，第一个设置a的bit 3，第二个清除a 的bit 3。在以上两个操作中，要保持其它位不变。<br>对这个问题有三种基本的反应<br>1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。<br>2) 用bit fields。Bit fields是被扔到C语言死角的东西，它保证你的代码在不同编译器之间是不可移植的，同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序，它用到了bit fields因此完全对我无用，因为我的编译器用其它的方式来实现bit fields的。从道德讲：永远不要让一个非嵌入式的家伙粘实际硬件的边。<br>3) 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法，是应该被用到的方法。最佳的解决方案如下：<br><br>#define BIT3 (0x1 &lt;&lt; 3)<br>static int a;<br><br>void set_bit3(void) <br>{<br>&nbsp;&nbsp;&nbsp; a |= BIT3;<br>}<br>void clear_bit3(void) <br>{<br>&nbsp;&nbsp;&nbsp; a &amp;= ~BIT3;<br>}<br><br>一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数，这也是可以接受的。我希望看到几个要点：说明常数、|=和&amp;=~操作。<br><br><br><span style="FONT-WEIGHT: bold">访问固定的内存位置</span>（Accessing fixed memory locations） <br><br>10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中，要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。<br>这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换（typecast）为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下：<br>&nbsp;&nbsp;&nbsp; int *ptr;<br>&nbsp;&nbsp;&nbsp; ptr = (int *)0x67a9;<br>&nbsp;&nbsp;&nbsp; *ptr = 0xaa55;<br><br>&nbsp;A more obscure approach is: <br>一个较晦涩的方法是：<br><br>&nbsp;&nbsp;&nbsp; *(int * const)(0x67a9) = 0xaa55;<br><br>即使你的品味更接近第二种方案，但我建议你在面试时使用第一种方案。<br><br><span style="FONT-WEIGHT: bold">中断</span>（Interrupts） <br><br>11. 中断是嵌入式系统中重要的组成部分，这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是，产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR)，请评论一下这段代码的。<br><br>__interrupt double compute_area (double radius) <br>{<br>&nbsp;&nbsp;&nbsp; double area = PI * radius * radius;<br>&nbsp;&nbsp;&nbsp; printf("\nArea = %f", area);<br>&nbsp;&nbsp;&nbsp; return area;<br>}<br><br>这个函数有太多的错误了，以至让人不知从何说起了：<br>1)ISR 不能返回一个值。如果你不懂这个，那么你不会被雇用的。<br>2) ISR 不能传递参数。如果你没有看到这一点，你被雇用的机会等同第一项。<br>3) 在许多的处理器/编译器中，浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈，有些处理器/编译器就是不允许在ISR中做浮点运算。此外，ISR应该是短而有效率的，在ISR中做浮点运算是不明智的。<br>4) 与第三点一脉相承，printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点，我不会太为难你的。不用说，如果你能得到后两点，那么你的被雇用前景越来越光明了。<br><br><br><span style="FONT-WEIGHT: bold">代码例子</span>（Code examples）<br><br>12 . 下面的代码输出是什么，为什么？<br><br>void foo(void)<br>{<br>&nbsp;&nbsp;&nbsp; unsigned int a = 6;<br>&nbsp;&nbsp;&nbsp; int b = -20;<br>&nbsp;&nbsp;&nbsp; (a+b &gt; 6) ? puts("&gt; 6") : puts("&lt;= 6");<br>}<br>这个问题测试你是否懂得C语言中的整数自动转换原则，我发现有些开发者懂得极少这些东西。不管如何，这无符号整型问题的答案是输出是 "&gt;6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数，所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题，你也就到了得不到这份工作的边缘。<br><br>13. 评价下面的代码片断：<br><br>unsigned int zero = 0;<br>unsigned int compzero = 0xFFFF; <br>/*1's complement of zero */<br><br>对于一个int型不是16位的处理器为说，上面的代码是不正确的。应编写如下：<br><br>unsigned int compzero = ~0;<br><br>这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里，好的嵌入式程序员非常准确地明白硬件的细节和它的局限，然而PC机程序往往把硬件作为一个无法避免的烦恼。<br>到了这个阶段，应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好，那么这个测试就在这里结束了。但如果显然应试者做得不错，那么我就扔出下面的追加问题，这些问题是比较难的，我想仅仅非常优秀的应试者能做得不错。提出这些问题，我希望更多看到应试者应付问题的方法，而不是答案。不管如何，你就当是这个娱乐吧...<br><br><br><span style="FONT-WEIGHT: bold">动态内存分配</span>（Dynamic memory allocation） <br><br>14. 尽管不像非嵌入式计算机那么常见，嵌入式系统还是有从堆（heap）中动态分配内存的过程的。那么嵌入式系统中，动态分配内存可能发生的问题是什么？<br>这里，我期望应试者能提到内存碎片，碎片收集的问题，变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了（主要是 P.J. Plauger, 他的解释远远超过我这里能提到的任何解释），所有回过头看一下这些杂志吧！让应试者进入一种虚假的安全感觉后，我拿出这么一个小节目：<br>下面的代码片段的输出是什么，为什么？<br><br>char *ptr;<br>if ((ptr = (char *)malloc(0)) == NULL) <br>&nbsp;&nbsp;&nbsp; puts("Got a null pointer");<br>else<br>&nbsp;&nbsp;&nbsp; puts("Got a valid pointer");<br><br>这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc，得到了一个合法的指针之后，我才想到这个问题。这就是上面的代码，该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题，看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要，但解决问题的方法和你做决定的基本原理更重要些。<br><br><span style="FONT-WEIGHT: bold">Typedef </span><br>&nbsp;<br>15 Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如，思考一下下面的例子：<br><br>#define dPS struct s *<br>typedef struct s * tPS;<br><br>以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢？（如果有的话）为什么？<br>这是一个非常微妙的问题，任何人答对这个问题（正当的原因）是应当被恭喜的。答案是：typedef更好。思考下面的例子：<br><br>dPS p1,p2;<br>tPS p3,p4;<br><br>第一个扩展为<br><br>struct s * p1, p2;<br>.<br>上面的代码定义p1为一个指向结构的指，p2为一个实际的结构，这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。<br><br><br><br><span style="FONT-WEIGHT: bold">晦涩的语法</span><br><br>16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗，如果是它做些什么？<br><br>int a = 5, b = 7, c;<br>c = a+++b;<br><br>这个问题将做为这个测验的一个愉快的结尾。不管你相不相信，上面的例子是完全合乎语法的。问题是编译器如何处理它？水平不高的编译作者实际上会争论这个问题，根据最处理原则，编译器应当能处理尽可能所有合法的用法。因此，上面的代码被处理成：<br><br>c = a++ + b;<br><br>因此, 这段代码持行后a = 6, b = 7, c = 12。<br>如果你知道答案，或猜出正确答案，做得好。如果你不知道答案，我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格，代码的可读性，代码的可修改性的好的话题。<br><br><br><font class=f14 id=zoom>好了，伙计们，你现在已经做完所有的测试了。这就是我出的C语言测试题，我怀着愉快的心情写完它，希望你以同样的心情读完它。如果是认为这是一个好的测试，那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年，我就不做现在的工作，也需要找一个。<br><br><span style="FONT-WEIGHT: bold">作者介绍:</span><br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; Nigel Jones 是一个顾问，现在住在Maryland，当他不在水下时，你能在多个范围的嵌入项目中找到他。 他很高兴能收到读者的来信，他的email地址是: <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#78;&#65;&#74;&#111;&#110;&#101;&#115;&#64;&#99;&#111;&#109;&#112;&#117;&#115;&#101;&#114;&#118;&#101;&#46;&#99;&#111;&#109;"><strong><font color=#cc3333>NAJones@compuserve.com</font></strong></a><br><br></font><font class=f14 id=zoom><span style="FONT-WEIGHT: bold">参考文献</span><br>1) Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.<br>2) Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.<br></font>
<img src ="http://www.cppblog.com/SmartPtr/aggbug/30730.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/SmartPtr/" target="_blank">SmartPtr</a> 2007-08-24 08:50 <a href="http://www.cppblog.com/SmartPtr/articles/30730.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>[转]轻松面试找到理想员工－非官方的面试技术指南</title><link>http://www.cppblog.com/SmartPtr/articles/30729.html</link><dc:creator>SmartPtr</dc:creator><author>SmartPtr</author><pubDate>Fri, 24 Aug 2007 00:47:00 GMT</pubDate><guid>http://www.cppblog.com/SmartPtr/articles/30729.html</guid><wfw:comment>http://www.cppblog.com/SmartPtr/comments/30729.html</wfw:comment><comments>http://www.cppblog.com/SmartPtr/articles/30729.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/SmartPtr/comments/commentRss/30729.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/SmartPtr/services/trackbacks/30729.html</trackback:ping><description><![CDATA[<p><em>作者:周思博 (Joel Spolsky)&nbsp; 来源:chinese.joelonsoftware.com<br></em></p>
<p>雇佣合适的人对于Fog Creek软件公司来说是非常关键的。在我们这个领域，有三类人可以挑选。在一个极端, 是哪些混进来的, 甚至缺乏最基本的工作技巧. 只要问这类人两三个简单的问题，再读一下他们的简历，就可以轻易地剔除他们。另一个极端的类型是 <a href="http://joel.spolsky.com/" target=_blank><u><font color=#0000ff>才华横溢的超级明星</font></u></a> 这些人仅仅为了好玩就用汇编语言为Palm Pilot（一种手掌电脑）写了一个Lisp（一种人工智能编程语言）编译器。在这两种极端类型中间的是一大群不能确定水平的候选者，也许他们中的某些人能干些什么？这里的关键是明白超级明星和那一大堆属于中间类型的人的区别，因为Fog Creek软件公司只雇佣超级明星。下面我要介绍一些找出超级明星的技巧。 </p>
<p>Fog Creek公司最重要的雇佣标准是:&nbsp;<br><br><strong>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;有头脑, 并且完成工作</p>
</strong>
<p>就是这些了。符合这样标准的人就是我们公司需要的员工了。 记住这条标准。 每天上床前背诵这条标准。我们公司的目标之一就是雇佣拥有这样的<em>潜质</em>的人，而不是雇佣懂某些技术的人。任何人所拥有的某些具体技术都会在几年内过时，所以，雇佣有能力学习新技术的人，要比雇佣那些只在这一分钟知道SQL编程是怎么回事的人对公司更划算一点。</p>
<p><strong>有头脑</strong>确实是一个很难定义的品质。但是让我们看一些在面试时能提问的一些问题，通过这些提问，我们可以找出拥有这种品质的人。<strong>完成工作</strong>非常关键。看起来<strong>有头脑</strong>但是不能<strong>完成工作</strong>的人经常拥有博士学位，在大公司工作过，但是在公司中没有人听他们的建议，因为他们是完全脱离实际的。比起准时交活儿，他们宁愿对于一些学院派的东西沉思。这些人由以下特性而可以识别出来。他们总是爱指出两个根本不同的概念间的相似性。例如，他们会说&#8220;Spreadsheets是一种特殊的编程语言&#8221;,然后花一个礼拜写一篇动人的，智慧的白皮书。这篇白皮书论述了，作为一个编程语言，spreadsheet关于计算语言特性的方方面面。聪明，但是没用。</p>
<p>现在，我们来谈谈<strong>完成工作</strong>但是没有<strong>头脑</strong>的人。他们爱做蠢事。从来也没有考虑过将来得靠他们自己或者别的什么人来亡羊补牢。通过制造新的工作，他们成为了公司的<em>负债</em>而不是资产。因为他们不仅没有为公司贡献价值，还浪费了好员工的时间。这些人通常到处粘贴大堆的代码，而不愿意写子程序。他们是完成了工作，但是不是以最聪明的方式完成工作。</p>
<p>面试时最重要的法则是: </p>
<blockquote><strong></strong>
<p><strong>做决定</strong></p>
</blockquote>
<p>在面试结束时，对于被面试者，你不得不做一个直截了当的决定。这个决定只有两个结果：<strong>雇佣</strong>或者<strong>不雇佣</strong>. 回到你的电脑前，立刻用电子邮件通知招聘负责人你的决定。电子邮件的主题应该是<strong>雇佣</strong>或者<strong>不雇佣</strong>。接着你需要在正文中写两段来支持你的决定.</p>
<p>没有其他的答案。<em>永远不要</em>说，&#8220;雇佣你，但是不能在我的团队中&#8221;。这是非常粗鲁的，因为你在暗示应试者没有聪明到能有和你一起工作的资格，但是以他的头脑适合进入那些天生输家队伍。如果你发觉自己被诱惑，想说出那句&#8220;雇佣你，但是不能在我的队伍中&#8221;，那么就简单的把这句话变成&#8220;不雇佣&#8221;再说出口。这样就没事了。甚至如果某个人在特定领域很能干，但是在别的队伍中将会表现不好，也是<strong>不雇佣</strong>。事物变化的如此之快，我们需要的是在任何地方都能成功的人。如果某些情况下你发现了一个白痴专家（拥有某些特殊能力的白痴），这个专家对于SQL非常，非常，非常的精通，但是除此之外什么也学不会，<strong>不雇佣</strong>。在Fog Creek公司他们没有将来。</p>
<em></em>
<p><em>永远不要</em>说，&#8220;也许，我吃不准&#8221;。如果你吃不准，意味着<strong>不雇佣</strong>。看，比你想象的容易的多。吃不准？就说不！同样，如果你<em>不能作出决定</em>，那意味着<strong>不雇佣</strong>。不要说，&#8221;嗯，雇佣，我想是这样的。但是关于...，我想知道 ...&#8221;。这种情况就是<strong>不雇佣</strong>。</p>
<p>最重要的是记住这点，放弃一个可能的好人要比招进一个坏人强（译者按：中国有位哲人说过，宁可错杀一千，不可放过一个，呵呵）。一个不合格的求职者如果进入了公司，将要消耗公司大量的金钱和精力。其他优秀员工的还要浪费时间来修复这个人的错误。如果现在你还在犹豫，<strong>不雇佣</strong>。</p>
<p>如果你是Fog Creek公司的面试官，当你拒绝了大量的应聘者时，不要为Fog Creek公司将因此雇不到任何人了而忧虑。这不是你的问题。这是招聘负责人的问题。这是人力资源部的问题。这是Joel（译者注: Fog Creek公司的老板，本文作者）的问题。但<em>不是</em>你的问题。不停地问自己，哪种情况更糟糕？一种情况是我们变成了一个庞大的，糟糕的软件公司，充斥着许多脑袋空空如可可果壳的家伙，另一种情况是我们是一个小而高品质的公司。当然，找到优秀的应聘者（并聘用他们）是很重要的。找到有头脑而且完成工作的人是公司中的每个员工的日常工作之一。但是当你作为Joel Creek公司的一员真的开始面试一个应聘者时，要装作现在正有很多优秀的人想打破头挤进Fog Creek公司。总之，无论找到一个不错的应聘者是多么的难，永远不要降低你的标准。</p>
<p>但是你如何作出雇佣或者不雇佣这样艰难的决定？你只要在面试过程中不停地问自己：<em>这个人<strong>有头脑</strong>吗？</em>这个人能<em><strong>完成工作</strong>吗？</em>要作出正确的回答，在面试时你必须问对问题。</p>
<p>开个玩笑，下面我要问个有史以来最差的面试问题: &#8220;Oracle 8i中的数据类型varchar和varchar2有什么区别&#8221;这是一个可怕的问题。掌握这种琐碎的技术细节和Fog Creek公司想雇佣你之间没有任何联系。谁会去记这种东西？如果有在线帮助，你可以在15秒内找到答案。</p>
<p>实际上，还有更差的问题，等会儿我会谈到的。</p>
<p>现在我们要谈到有趣的部分了：面试时提哪些问题。我的面试问题清单来自于我去微软公司找第一份工作的经历。这里实际上有几百个微软面试问题。每个人都有偏爱的问题。你也可以发展一套自己的面试问题以及面试的个人风格，这样你就可以比较容易地作出<strong>雇佣/不雇佣</strong>的决定。以下是我成功使用过的一些面试技巧，</p>
<p>在面试前，我读一遍应试者的简历，然后在一张纸片上随便写以下我的<em>面试计划</em>。这个计划实际上就是我要问的问题清单。以下是一个例子（用来面试程序员的）：</p>
<ol>
    <ol><strong>
        <li>介绍
        <li>应试者参加过的项目
        <li>无法回答的问题
        <li>C语言函数
        <li>你满意吗？
        <li>设计问题
        <li>挑战
        <li>你还有什么问题？ </strong></li>
    </ol>
</ol>
<p>在面试前，我非常，非常当心，避免自己先入为主。如果在面试前你就已经想当然地认为，一个麻省理工的博士一定是一个有头脑的人。那么在接下来的一小时的面试时间内，无论那个麻省理工的博士说什么都不能改变你的最初印象。如果在面试前你就认为这个应试者是个傻瓜，那么他面试时说什么也无济于事。面试就象一个非常精巧的天平。一小时的面试结束后就要对一个人下结论是不容易的（但是你又必须在面试结束后得出结论）。一些不起眼的细节可能会影响最后的结论。如果你在面试开始前对于应试者有了一点了解的话，就好比天平的某一端加上了重重的砝码。这样面试本身就会变得没有用处了。以前有一次在面试前，一个招聘负责人跑进我的房间说，&#8220;你肯定会爱上这个家伙的!" 对一个男孩? 天哪，这简直让我发疯。我本来应该说，&#8220;嗯，如果你这么确定我会喜欢他，为什么你不干脆雇佣他,何必让我浪费时间来面试？&#8221;但是那时我还太年轻幼稚, 所以还是面试了那个人。当这个家伙开始说一些蠢话时，我对自己说，&#8220;哇塞，这应该是个例外情况，也许是大智若愚。&#8221;我开始带着玫瑰色眼镜看他了。于是我以说<strong>&#8220;雇佣&#8221;</strong>结束了面试，虽然他是一个糟糕的面试者。接下来发生了什么事？除了我，其他的面试官都说，<strong>不要雇佣</strong>这个人。教训是，不要听别的人的话，在面试应试者前不要四处打探这个面试者的情况。最重要的是不要和别的面考官谈论应试者，除非你们都已经作出了独立的判断。这才是科学的做法。</p>
<p>作为面试步骤的第一步，<em>介绍</em>的目的是让应试者放轻松。我通常花30秒钟，讲一下我是谁，接下来面试会如何进行。我总是使得应试者确信，我们关心的是他（她）<em>如何</em>解决问题的，而不是他（她）的最终答案是对还是错。顺便说一下，面试时，你不要和应试者隔着一个桌子坐着，否则在你和面试者之间就有了一个障碍，并且暗示着一种比较正式严肃的气氛，这样应试者就很难放松了。更好的办法是把桌子靠墙放着，或者和应试者坐在桌子的同一边，这样有助于应试者放松。只有应试者不会因为紧张而表现失常，你才能更有效的进行面试.</p>
<p>第二步的内容就是问应试者最近做了些什么项目。对刚毕业的学生, 如果有论文就问问论文, 没有的话, 就问问他们做过什么很喜欢的大作业.例如，有时候我会问一下，&#8220;你最喜欢上学期哪门课程？不一定要和计算机相关的。&#8221;事实上，如果应试者回答的课程和计算机没有关系，我会比较高兴。有时候你会发现这个计算机系应届生选择了尽可能少的计算机相关课程，但是却选修了很多和音乐相关的课程。但是他（她）却说最喜欢的课程是《面向对象数据库》。哼哼，不错啊. 不过如果你直接承认你喜欢音乐胜于计算机, 而不是在这儿胡说八道的话, 我会更高兴一点。</p>
<p>当面试有工作经验的人时，你可以让他们谈一下前一份工作。</p>
<p>我问这个问题的目的是在寻找一样品质：<em>热情</em>。在应试者谈到他（她）最近做过的项目时，你观察到以下迹象都是不错的： </p>
<ul>
    <li>谈到他们做过的项目时变得热情洋溢；他们的语速更快，语言更生动活泼。这说明他们对某些东西有兴趣，有热情（因为现实中有许多人对所做的项目根本漠不关心呢）。即使他们激动地表达对做过的项目的负面感情，这也是一个好的信号。&#8220;我曾经为上一个老板安装Foo Bar Mark II，但他是个傻瓜！&#8221;表现出热情的人就是我们要雇佣的人。差的应试者对工作根本就不关心，所以根本不会激动。一个非常好的信号是当应试者很激动地谈论上一份工作，以至于暂时忘记了他们正在被面试。有时候应试者刚开始面试时表现的很紧张 -- 这是很正常的现象，所以我通常忽略不计。但是当他们谈到单色计算艺术（Computational Monochromatic Art）时，这个家伙变得极端兴奋, 一点都不紧张了。不错，我喜欢这样的应试者，因为他们关心他们做的事。（什么是单色计算艺术？拔掉你的电脑显示器的电源就可以看到了）
    <li>能认真地去解释事情。某些人被我拒掉的原因就是他们不会用普通人能明白的语言去解释他们做过的项目。很多工科专业的人总是以为所有人都知道Bates理论（译者注: Bates Theorem，一种经济学的理论）或者Peano公理组（译者注: Peano's Axioms，数论中的一些定理）是什么。如果应试者开始满口行话了，让他们停一停，然后你说，&#8220;能帮我个忙吗？就是为了练习一下，你能把刚才说的用我老祖母也能理解的话说一遍吗？&#8221;但即便如此, 有些人还是继续用那些术语, 而且根本没法让人明白他们在说什么。天哪！
    <li>如果这个项目是一个团队项目，看看他们是否在有承担领导责任的迹象？一个应试者可能会说：&#8220;我们用的是X方法，但是老板说应该是Y，而客户说应该是Z。&#8221;我会问，&#8220;那么<em>你</em>怎么做的？&#8221;一个好的回答可能是&#8220;我设法和团队中别的人开了个会，然后一起搞出个办法...&#8221;坏的回答看起来象，&#8220;嗯，我什么也<em>不能</em>做。这样的问题我解决不了。&#8221;记住，<strong>聪明并且能完成工作</strong>。要搞清楚某人是否能<strong>完成工作</strong>的一个办法就是看看他（她）过去是否倾向于完成任务。事实上，你可以主动要求他们给你个例子证明他们能担任领导作用，完成任务。－例如克服公司的陈规陋习。 </li>
</ul>
<p>现在我们谈谈清单上的第三款，<em>无法回答的问题</em>。这很有趣。这个主意的关键在于问一些不可能有答案的问题，就是想看一下应试者怎么办。&#8220;西雅图有多少眼科医生？&#8221;&#8220;华盛顿纪念碑有多重？&#8221;&#8220;洛杉机有多少加油站？&#8221;&#8220;纽约有多少钢琴调音师？&#8221;</p>
<ul>
    <li>聪明的应试者猜到你不是要测验他们的专业知识，他们会积极地给出一个估计。&#8220;嗯，洛杉机的人口是七百万；每个人平均拥有2.5辆轿车...&#8221;当然如果他们的估计完全错误了也没有关系。重要的是他们能积极地试着回答问题。他们可能会试着搞清楚每个加油站的储量。&#8220;嗯，需要四分钟给一个储油罐加满油，一个加油站有十个油泵每天运行十八个小时...&#8221;他们也可能试着从占地面积来估计。有时, 他们的想法的创造力让你吃惊. 而有时, 他们直接要一个LA的黄页去查。这都是好迹象。
    <li>不聪明的应试者则被难住了。他们目瞪口呆地望着你，好像你来自火星。你不得不提示：&#8220;嗯，如果你想建立一个象洛杉机那么大的城市，你需要建立多少个加油站？&#8221;你还可以提示他们：&#8220;加满一个储油罐要多长时间？&#8221;不过，这些榆木疙瘩脑袋还是只会坐在那里发呆，你得拖着他们往前走才行。这类人不会解决问题，我们可不要这样的人。 </li>
</ul>
<p>关于编程问题，我通常要求应试者用C语言写一些小函数。以下是我通常会出的题目： </p>
<ol>
    <li>将一个字符串逆序
    <li>将一个链表（linked list）逆序
    <li>计算一个字节（byte）里有多少bit被置1
    <li>搜索给定的字节（byte）
    <li>在一个字符串中找到可能的最长的子字符串，该字符串是由同一字符组成的
    <li>字符串转换成整数
    <li>整数转换成字符串（这个问题很不错，因为应试者要用到堆栈或者strev函数） </li>
</ol>
<p>注意，通常你不会希望他们写的代码多于5行，因为你没有时间理解太长的代码。</p>
<p>现在我们来详细看一看其中几个问题: 第一个问题: 逆序一个字符串。我这辈子还没有见过那个面试者能把这题目一次做对。所有的应试者都试图动态生成缓冲区，然后将逆序的字符串输出到该缓冲区中。问题的关键在于，谁负责分配这个缓冲区？谁又负责释放那个缓冲区？通过这个问题，我发现了一个有趣的事实，就是大多数认为自己懂C的人实际上不理解指针和内存的概念。他们就是不明白。这真叫人吃惊，无法想象这种人也能做程序员。但他们真的就是！这个问题可以从多个角度判断应试者：<br></p>
<ul>
    <li>他们的函数运行快吗？看一下他们多少此调用了<em>strlen</em>函数。我曾经看到应试者写的strrev的算法竟然只有O(n^2) 的效率，而标准的算法效率应该是O(n)，效率如此底下的原因是因为他们在循环中一次又一次调用<em>strlen</em>函数。
    <li>他们使用指针运算吗（译者按：原文为pointer arithmetic，指的是加减指针变量的值）？使用指针运算是个好现象。许多所谓的&#8220;C程序员&#8221;竟然不知道如何使用指针运算（pointer arithmetic）。当然，我在前文说过我不会因为应试者不掌握一种特定的技巧而拒绝他。但是，理解C语言中的指针不是一种技巧，而是一种与生俱来的才能。每年一所大学要招进200多个计算机系的新生，所有这些小孩子4岁就开始用BASIC语言在Atari 800s写冒险游戏了。在大学里他们还学Pascal语言，学得也很棒。直到有一天他们的教授讲了指针的概念，突然，<em>他们开始搞不懂了</em>。他们就是不能再理解C语言中的任何东西了。于是90%的计算机系学生转系去学政治学。为了挽回面子，他们告诉朋友，他们之所以转系是因为他们计算机系英俊貌美的异性太少。许多人注定脑子里就没有理解指针的那根弦。所以说理解指针是一种与生俱来的品质，而不是一种单纯的技巧。理解指针需要脑子转好几个弯，某些人天生不擅长转这几个弯。</li>
</ul>
<p>第三个问题可以考考面试者对C的位运算的掌握，但这是一种技巧，不是一种品质，所以你可以帮助他们。有趣的等他们建立了一个子函数用来计算byte中为1的位的数目，然后你要求他们优化这个子函数，尽量加快这个函数的运行速度。聪明的应试者会使用查表算法（毕竟这个表只有 256个元素，用不了多少内存），整个表只需要建立一次。跟聪明的应试者讨论一下提高时间／空间效率的不同策略是十分有意思的事情. 进一步告诉他们你不想在程序启动时初始化查询表。聪明的面试者可能会建议使用缓冲机制，对于一个特定的byte，只有在第一次被查询时进行计算，然后计算结果会被放入查询表。这样以后再被查询时直接查表就行了。而特别特别聪明的面试这会尝试有没有建立查询表的捷径，如一个byte和它的置1的bit数之间有没有规律可循？&nbsp;</p>
<p>当你观察应试者写C代码时，以下一些技巧会对你有帮助： </p>
<ul>
    <li>事先向应试者说明，你完全理解，没有一个好的编辑器光在纸上写代码是困难的，所以你不在乎他们手写的代码是否看上去不整洁。你也完全明白没有好的编译器和调试器，很难第一次就写出完全没有bug的程序，所以请他们不必为此担心。
    <li>好程序员的标志：好程序员写完&#8220;{&#8221;符号后，通常立刻跟上&#8220;}&#8221;符号，然后再在当中填上代码。他们也倾向于使用命名规则，虽然这个规则可能很原始。如果一个变量用作循环语句的索引，好程序员通常使用尽可能少的字符为它命名。如果他们的循环语句的索引变量的名字是CurrentPagePositionLoopCounter，显而易见他们写代码的经验还不够多。偶尔，你会看到一个C程序员写下象<strong>if (0==strlen(x))</strong>一样的代码，<em>常量</em>被放在==的左边。这是非常好的现象。这说明他因为总是把＝和＝＝搞混，已经强迫自己养成这种习惯以避免犯错。
    <li>好的程序员在写代码前会订一个计划，特别是当他们的代码用到了指针时。例如，如果你要求逆序一个链表，好程序员通常会在纸的一边画上链表的草图，并表明算法中的索引指针当前移动到的位置。他们不得不这样做。正常人是不可能不借助草图就开始写一个逆序链表的程序的。差的程序员立刻开始写代码。 </li>
</ul>
<p>不可避免的，你会在他们的程序中发现bug，于是我们现在来到了第五个问题：<strong>你对代码满意吗？</strong> 你可能想问，&#8220;好吧，bug在哪里？&#8221;这是来自地狱的一针见血的问题，要回答这个问题可要大费口舌。所有的程序员都会犯错误，这不是问题。但他们必须能找到错误。对于字符串操作的函数，他们通常会忘记在输出缓冲区加上字符串结束符。所有的函数，他们都会犯off-by-one错误（译者按：指的是某个变量的最大值和最小值可能会和正常值差1)。他们会忘掉正常的C语句结尾的分号。如果输入是零长度字符串，他们的函数会运行错误。如果malloc调用失败而他们没有为此写好错误处理代码，程序会崩溃。一次就能把所有事情做对的程序员非常,非常,非常地少.不过要是真的碰上一个的话, 提问就更有意思了. 你说,"还有Bug"。他们会再仔细地检查一遍代码。这个时候, 观察一下他们内心是否开始动摇了, 只是表面上勉强坚持说代码没有问题。总之，在程序员写完代码后，问一下他们是否对代码满意是个好主意。就像Regis那样问他们！（译者按，Regis Philbin是美国ABC电视网的游戏电视节目主持人，他的口头禅是&#8220;这是你的最后的答案吗？&#8221;）</p>
<p>第六部分：关于设计的问题。让应试者设计某样东西。Jabe Blumenthal，Excel的原始设计者，喜欢让应试者设计房子。Jabe说，曾经有一个应试者跑到白板前，画了一个方块，这就是他的全部设计。天哪，一个方块！立刻<strong>拒绝</strong>这样的家伙。你喜欢问什么样的设计问题？</p>
<ul>
    <li>好的程序员会问更多的信息。房子为谁造的？我们公司的政策是，我们不会雇佣那些在设计前不问为谁设计的人。通常，我会很烦恼我得打断他们的设计，说&#8220;事实上，你忘记问这个房子是给谁设计的了。这个房子是给一群长颈鹿造的。&#8221;
    <li>笨笨的应试者认为设计就像画画，你想画什么就画什么。聪明的应试者明白设计的过程是一系列艰难的权衡。一个很棒的设计问题是：设计一个放在街角的垃圾箱。想一想你得做多少权衡！垃圾箱必须易于清空，但是很难被偷走；易于放进垃圾，但是碰到狂风大作，里面的垃圾不会被吹出来；垃圾箱必须坚固而便宜。在某些城市，垃圾箱必须特别设计，以防恐怖分子在里面藏一个定时炸弹。
    <li>有创造力的应试者会给出有趣而独特的设计。我最喜欢的问题之一是<em>为盲人设计一个放调味品的架子</em>（译者按：原文为spice rack，老外的厨房里有个专门放调味品的架子,上面放了很多小罐罐，里面装了各种各样的调料）通常许多应试者的建议是把布莱叶文（一种盲人使用的文字）刻在放调料的罐子上，这样文字会卷起来而变形。我碰到一个应试者，他的设计是把调料放在抽屉里，因为他觉得水平地感知布莱叶文比垂直地做更方便。（试试看！）这个答案这样有创意，使我震惊！我面试了有一打得程序员，从来没有人想到过类似的答案。这样有创意的答案确实跃过了普通人考虑问题的条条框框。仅仅因为这个答案太有创意了，而且应试者别的方面还过得去，我雇佣了这个应试者，他现在已经成为Excel团队中一个优秀的项目经理了（译者按，本文作者曾在微软工作过）。
    <li>总是争取一个<em>确定的了结</em>。这也是<strong>完成工作</strong>的特质的一部分。有时候应试者会犹犹豫豫不能作出一个决定,试图回避困难的问题，留着困难的问题不作决定就直接向下进行,这很不好。好的应试者有一种推动事情自然地前进的倾向，即使你有意把他们拖回来。如果关于某个话题的讨论开始原地打转变得没有意义了，好的应试者会说，&#8220;嗯，我们可以整天谈论这个，但是我们得做点什么。为什么我们不开始...&#8221; </li>
</ul>
<p>于是我们来到了第七部分，<strong>挑战</strong>。这部分很好玩。在面试中留心一下, 当面试者的回答绝对的百分之百毫无争议时, 你可以说: " 嗯, 等一下等一下." 然后花上两分钟玩一下魔鬼的游戏（译者按，原文为devil's advocate，魔鬼代言人指的是违背自己的良知，为错误邪恶的观点辩护）. 记住一定要在你可以肯定他正确时和他争论。</p>
<pre wrap="">这个很有意思.  <br></pre>
<ul>
    <li>软弱的应试者会屈服。那我就和他说<strong>拜拜</strong>了。
    <li>坚定的应试者会找到一个办法说服你。他们会以肯尼迪总统的口才来说服你，&#8220;也许我误会了你的意思，&#8221;他们这样开头，但是正文仍是坚定地站稳立场。这样的人我就<strong>雇佣</strong>。 </li>
</ul>
<p>不得不承认，面试双方的地位并不是平等的。有可能应试者由于害怕你的权力而不敢于你争辩。<strong>但是</strong>，好的应试者有足够的热情和勇气坚持正确的观点，他们由于热切希望说服你而会暂时忘记正在被面试。这样的人就是我们要雇佣的人。</p>
<p>最后，可以问一下应试者有什么想问的。一些人喜欢看看应试者这时是否会问一些聪明的问题。这是市面上流行的面试书籍的标准技巧。我个人不在乎应试者问什么，因为这时我已经做了决定。麻烦在于，应试者也许已经见了5、6个人，进行了好几轮面试，他们可能很累了，以至于不能为每轮面试都准备一个聪明而独特的问题。所以如果他们没有可问的，没关系。</p>
<p>我总是留下面试的最后5分钟来推销我的公司。这很重要。<em>即使我不打算雇佣眼前这个应试者。</em>如果你幸运的找到一个很棒的应试者，你当然愿意做任何事情说服他（她）来你的公司。即使他们不是好的应试者，你也要尽力让他们为Fog Creek公司激动，这样面试结束时他们会对Fog Creek公司留下一个很好的印象。记住，应试者并不仅仅是可能的雇员，他们也是顾客，也是我们公司的推销员。如果他们觉得我们的公司很棒，他们也许会推荐朋友来面试。</p>
<p>啊哈，我记得我说过我会给出一些应该避免的非常不好的反面的试题例子。 </p>
<p>首先，避免不合法的问题。有关种族，宗教，性别，出生国，年龄，服役记录，是否老兵，性取向，生理障碍的问题都是<strong>不合法的</strong>。即使他们的简历说他们1990年在军中服役，也不要问有关问题。也许这会让他们愉快地谈论在海湾战争中的经历。但是你的问题还是不合法的。如果简历上写着他们上过Technion in Haifa, 不要问他们是否是以色列人, 即使只是为了闲谈, 因为这是违法的. 下面有一个很好的不合法的例子。<a href="http://www.job-interview.net/Guide/SPstep4.htm"><u><font color=#0000ff>点击这里</font></u></a>有很多关于什么是违法的讨论。（但是这个网站的其余问题够愚蠢的。）</p>
<p>其次，不要在问题中给予应试者暗示，我们公司喜欢或者不喜欢什么样的员工。我能想到的一个例子是问应试者是否有小孩或者是否结婚了。应试者也许会想我们不喜欢有家庭拖累的员工。</p>
<p>最后，不要问那些脑筋急转弯的题目，例如6根火柴怎么拼出4个三角形。象这样的灵机一动的问题是不能看出应试者是否有&#8220;有头脑/完成工作&#8221;的品质。</p>
<p>面试与其说是科学不如说是艺术。但是只要你记住<strong>有头脑/完成工作</strong>这个原则，你就可以应对自如。有机会就问问你的同事他们喜欢的面试问题和答案。这是我们公司员工午饭时热衷的话题之一。</p>
<p><font color=#808080><font size=2><span style="FONT-FAMILY: SimSun">本文最先用英文出版，题为</span> </font></font><a href="http://www.joelonsoftware.com/articles/fog0000000073.html"><font color=#0000ff size=2><u>The Guerrilla Guide to Interviewing</u></font></a></p>
<img src ="http://www.cppblog.com/SmartPtr/aggbug/30729.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/SmartPtr/" target="_blank">SmartPtr</a> 2007-08-24 08:47 <a href="http://www.cppblog.com/SmartPtr/articles/30729.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>[转] 正则表达式30分钟入门教程</title><link>http://www.cppblog.com/SmartPtr/articles/30723.html</link><dc:creator>SmartPtr</dc:creator><author>SmartPtr</author><pubDate>Thu, 23 Aug 2007 22:59:00 GMT</pubDate><guid>http://www.cppblog.com/SmartPtr/articles/30723.html</guid><wfw:comment>http://www.cppblog.com/SmartPtr/comments/30723.html</wfw:comment><comments>http://www.cppblog.com/SmartPtr/articles/30723.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/SmartPtr/comments/commentRss/30723.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/SmartPtr/services/trackbacks/30723.html</trackback:ping><description><![CDATA[<h2 id="contents"><span style="font-style: italic; font-size: 10pt; font-family: Tahoma; font-weight: normal;">
版本：v2.21 (2007-8-3) 作者：</span><a style="font-style: italic; font-family: tahoma; font-weight: normal;" href="http://www.unibetter.com/members/deerchao.aspx">deerchao</a><span style="font-style: italic; font-family: Tahoma; font-weight: normal;"> 来源:</span><a style="font-style: italic; font-family: tahoma; font-weight: normal;" href="http://www.unibetter.com/">unibetter大学生社区</a><span style="font-style: italic; font-family: Tahoma; font-weight: normal;">&nbsp;</span></h2>
<br>
<h2 id="mission">
本文目标</h2>
<p>
30分钟内让你明白正则表达式是什么，并对它有一些基本的了解，让你可以在自己的程序或网页里使用它。</p>
<h2 id="howtouse">
如何使用本教程</h2>
<p>
最重要的是——请给我<em> 30分钟</em> ，如果你没有使用正则表达式的经验，请不要试图在30<em> 秒</em> 内入门。当然，如果你是超人，那自然得另当别论。</p>
<p>
别被下面那些复杂的表达式吓倒，只要跟着我一步一步来，你会发现正则表达式其实并没有你
想像中的那么困难。当然，如果你看完了这篇教程之后，发现自己明白了很多，却又几乎什么都记不得，那也是很正常的——我认为，没接触过正则表达式的人在看
完这篇教程后，能把提到过的语法记住80%以上的可能性为零。这里只是让你明白基本的原理，以后你还需要多练习，多使用，才能熟练掌握正则表达式。</p>
<p>
除了作为入门教程之外，本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。就作者本人的经历来说，这个目标还是完成得不错的——你看，我自己也没能把所有的东西记下来，不是吗？</p>
<p>
文本格式约定：专业术语&nbsp;元字符/语法格式&nbsp;正则表达式&nbsp;正则表达式中的一部分(用于分析)&nbsp;用于在其中搜索的字符串&nbsp;对正则表达式或其中一部分的说明<a href="http://www.unibetter.com/deerchao/zhengzhe-biaodashi-jiaocheng-se.htm" id="clearButton" accesskey="c" onclick="return clearFormats();">清除格式</a></p>
<h2 id="introduction">
正则表达式到底是什么？</h2>
<p>
在编写处理字符串的程序或网页时，经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说，正则表达式就是记录文本规则的代码。</p>
<p>
很可能你使用过Windows/Dos下用于文件查找的通配符(wildcard)，也就是*和?。如果你想查找某个目录下的所有的Word文档的话，你会搜索*.doc。在这里，*会被解释成任意的<a href="http://www.unibetter.com/deerchao/zhengzhe-biaodashi-jiaocheng-se.htm#reference" title="参考">字符串</a>。和通配符类似，正则表达式也是用来进行<a href="http://www.unibetter.com/deerchao/zhengzhe-biaodashi-jiaocheng-se.htm#reference" title="参考">文本</a><a href="http://www.unibetter.com/deerchao/zhengzhe-biaodashi-jiaocheng-se.htm#reference" title="参考">匹配</a>的工具，只不过比起通配符，它能更精确地描述你的需求——当然，代价就是更复杂——比如你可以编写一个正则表达式，用来查找所有以0开头，后面跟着2-3个数字，然后是一个连字号&#8220;-&#8221;，最后是7或8位数字的字符串(像010-12345678或0376-7654321)。</p>
<p id="match">
正则表达式是用于进行文本匹配的工具，所以本文里多次提到了在字符串里搜索/查找，这种说法的意思是在给定的字符串中，寻找与给定的正则表达式相匹配的部分。有可能字符串里有不止一个部分满足给定的正则表达式，这时每一个这样的部分被称为一个匹配。匹配在本文里可能会有三种意思：一种是形容词性的，比如说一个字符串匹配一个表达式；一种是动词性的，比如说在字符串里匹配正则表达式；还有一种是名词性的，就是刚刚说到的&#8220;字符串中满足给定的正则表达式的一部分&#8221;。</p>
<h2 id="getstarted">
入门</h2>
<p>
学习正则表达式的最好方法是从例子开始，理解例子之后再自己对例子进行修改，实验。下面给出了不少简单的例子，并对它们作了详细的说明。</p>
<p>
假设你在一篇英文小说里查找hi，你可以使用正则表达式hi。</p>
<p>
这是最简单的正则表达式了，它可以精确匹配这样的字符串：由两个字符组成，前一个字符是h,后一个是i。通常，处理正则表达式的工具会提供一个忽略大小写的选项，如果选中了这个选项，它可以匹配hi,HI,Hi,hI这四种情况中的任意一种。</p>
<p>
不幸的是，很多单词里包含hi这两个连续的字符，比如him,history,high等等。用hi来查找的话，这里边的hi也会被找出来。如果要精确地查找hi这个单词的话，我们应该使用\bhi\b。</p>
<p>
\b是正则表达式规定的一个特殊代码（好吧，某些人叫它元字符，metacharacter），代表着单词的开头或结尾，也就是单词的分界处。虽然通常英文的单词是由空格或标点符号或换行来分隔的，但是\b并不匹配这些单词分隔符中的任何一个，它<strong> 只匹配一个位置</strong> 。（如果需要更精确的说法，\b匹配这样的位置：它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w）</p>
<p>
假如你要找的是hi后面不远处跟着一个Lucy，你应该用\bhi\b.*\bLucy\b。</p>
<p>
这里，.是另一个元字符，匹配除了换行符以外的任意字符。*同样是元字符，不过它代表的不是字符，也不是位置，而是数量——它指定*前边的内容可以连续重复出现任意次以使整个表达式得到匹配。因此，.*连在一起就意味着任意数量的不包含换行的字符。现在\bhi\b.*\bLucy\b的意思就很明显了：先是一个单词hi,然后是任意个任意字符(但不能是换行)，最后是Lucy这个单词。</p>
<p>
如果同时使用其它的一些元字符，我们就能构造出功能更强大的正则表达式。比如下面这个例子：</p>
<p>
0\d\d-\d\d\d\d\d\d\d\d匹配这样的字符串：以0开头，然后是两个数字，然后是一个连字号&#8220;-&#8221;，最后是8个数字(也就是中国的电话号码。当然，这个例子只能匹配区号为3位的情形)。</p>
<p>
这里的\d是一个新的元字符，匹配任意的数字(0，或1，或2，或&#8230;&#8230;)。-不是元字符，只匹配它本身——连字号。</p>
<p>
为了避免那么多烦人的重复，我们也可以这样写这个表达式：0\d{2}-\d{8}。
这里\d后面的{2}({8})的意思是前面\d必须连续重复匹配2次(8次)。</p>
<h2 id="testing">
测试正则表达式</h2>
<p>
如果你不觉得正则表达式很难读写的话，要么你是一个天才，要么，你不是地球人。正则表达式的语法很令人头疼，即使对经常使用它的人来说也是如此。由于难于读写，容易出错，所以很有必要创建一种工具来测试正则表达式。</p>
<p>
由于在不同的环境下正则表达式的一些细节是不相同的，本教程介绍的是Microsoft .Net 2.0下正则表达式的行为，所以，我向你介绍一个.Net下的工具<a title="转到RegexTester的官方网站（英文）" href="http://www.dotnet2themax.com/blogs/fbalena/PermaLink,guid,13bce26d-7755-441e-92b3-1eb5f9e859f9.aspx">Regex
Tester</a>。首先你确保已经安装了<a href="http://www.microsoft.com/downloads/details.aspx?displaylang=zh-cn&amp;FamilyID=0856eacb-4362-4b0d-8edd-aab15c5e04f5" title="转到下载.Net Framework 2.0的页面">.Net Framework 2.0</a>，然后<a href="http://www.unibetter.com/deerchao/downloads/RegexTester.zip" title="从www.unibetter.com下载Regex Tester, 75KB">下载Regex Tester</a>。这是个绿色软件，下载完后打开压缩包,直接运行RegexTester.exe就可以了。</p>
<p>
下面是Regex Tester运行时的截图：</p>
<p>
<img src="http://www.unibetter.com/deerchao/images/RegexTester.jpg" alt="Regex Tester运行时的截图"></p>
<h2 id="metacode">
元字符</h2>
<p>
现在你已经知道几个很有用的元字符了，如\b,.,*，还有\d.当然还有更多的元字符可用，比如\s匹配任意的空白符，包括空格，制表符(Tab)，换行符，中文全角空格等。\w匹配字母或数字或下划线或汉字等。</p>
<p>
下面来试试更多的例子：</p>
<p>
\ba\w*\b匹配以字母a开头的单词——先是某个单词开始处(\b)，然后是字母a,然后是任意数量的字母或数字(\w*)，最后是单词结束处(\b)（好吧，现在我们说说正则表达式里的单词是什么意思吧：就是几个连续的\w。不错，这与学习英文时要背的成千上万个同名的东西的确关系不大）。</p>
<p>
\d+匹配1个或更多连续的数字。这里的+是和*类似的元字符，不同的是*匹配重复任意次(可能是0次)，而+则匹配重复1次或更多次。</p>
<p>
\b\w{6}\b 匹配刚好6个字母/数字的单词。</p>
<table cellspacing="0">
    <caption>
    表1.常用的元字符</caption>
    <tbody>
        <tr>
            <th>
            代码</th>
            <th>
            说明</th>
        </tr>
        <tr>
            <td>
            .</td>
            <td>                    匹配除换行符以外的任意字符</td>
        </tr>
        <tr>
            <td>                    \w</td>
            <td>                    匹配字母或数字或下划线或汉字</td>
        </tr>
        <tr>
            <td>
            \s</td>
            <td>                    匹配任意的空白符</td>
        </tr>
        <tr>
            <td>                    \d</td>
            <td>                    匹配数字</td>
        </tr>
        <tr>
            <td>                    \b</td>
            <td>
            匹配单词的开始或结束</td>
        </tr>
        <tr>
            <td>                    ^</td>
            <td>                    匹配字符串的开始</td>
        </tr>
        <tr>
            <td>                    $</td>
            <td>                    匹配字符串的结束</td>
        </tr>
    </tbody>
</table>
<p>
元字符^（和数字6在同一个键位上的符号）以及$和\b有点类似，都匹配一个位置。^匹配你要用来查找的字符串的开头，$匹配结尾。这两个代码在验证输入的内容时非常有用，比如一个网站如果要求你填写的QQ号必须为5位到12位数字时，可以使用：^\d{5,12}$。</p>
<p>
这里的{5,12}和前面介绍过的{2}是类似的，只不过{2}匹配只能不多不少重复2次，{5,12}则是重复的次数不能少于5次，不能多于12次，否则都不匹配。</p>
<p>
因为使用了^和$，所以输入的整个字符串都要用来和\d{5,12}来匹配，也就是说整个输入必须是5到12个数字，因此如果输入的QQ号能匹配这个正则表达式的话，那就符合要求了。</p>
<p>
和忽略大小写的选项类似，有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项，^和$的意义就变成了匹配行的开始处和结束处。</p>
<h2 id="escape">
字符转义</h2>
<p>
如果你想查找元字符本身的话，比如你查找.,或者*,就出现了问题：你没法指定它们，因为它们会被解释成其它的意思。这时你就必须使用\来取消这些字符的特殊意义。因此，你应该使用\.和\*。当然，要查找\本身，你也得用\\.</p>
<p>
例如：www\.unibetter\.com匹配www.unibetter.com，c:\\Windows匹配c:\Windows。</p>
<h2 id="repeat">
重复</h2>
<p>
你已经看过了前面的*,+,{2},{5,12}这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码，例如*,{5,12}等)：</p>
<table cellspacing="0">
    <caption>
    表2.常用的限定符</caption>
    <tbody>
        <tr>
            <th>
            代码/语法</th>
            <th>
            说明</th>
        </tr>
        <tr>
            <td>                    *</td>
            <td>
            重复零次或更多次</td>
        </tr>
        <tr>
            <td>                    +</td>
            <td>                    重复一次或更多次</td>
        </tr>
        <tr>
            <td>                    ?</td>
            <td>                    重复零次或一次</td>
        </tr>
        <tr>
            <td>                    {n}</td>
            <td>                    重复n次</td>
        </tr>
        <tr>
            <td>                    {n,}</td>
            <td>                    重复n次或更多次</td>
        </tr>
        <tr>
            <td>
            {n,m}</td>
            <td>                    重复n到m次</td>
        </tr>
    </tbody>
</table>
<p>
下面是一些使用重复的例子：</p>
<p>
Windows\d+匹配Windows后面跟1个或更多数字</p>
<p>
13\d{9}匹配13后面跟9个数字(中国的手机号)</p>
<p>
^\w+匹配一行的第一个单词(或整个字符串的第一个单词，具体匹配哪个意思得看选项设置)</p>
<h2 id="charclass">
字符类</h2>
<p>
要想查找数字，字母或数字，空白是很简单的，因为已经有了对应这些字符集合的元字符，但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办？</p>
<p>
很简单，你只需要在中括号里列出它们就行了，像[aeiou]就匹配任何一个英文元音字母，[.?!]匹配标点符号(.或?或!)(英文语句通常只以这三个标点结束)。</p>
<p>
我们也可以轻松地指定一个字符范围，像[0-9]代表的含意与\d就是完全一致的：一位数字，同理[a-z0-9A-Z_]也完全等同于\w（如果只考虑英文的话）。</p>
<p>
下面是一个更复杂的表达式：\(?0\d{2}[) -]?\d{8}。</p>
<p>
这个表达式可以匹配几种格式的电话号码，像(010)88886666，或022-22334455，或02912345678等。我们对它进行一些分析吧：首先是一个转义字符\(,它能出现0次或1次(?),然后是一个0，后面跟着2个数字(\d{2})，然后是)或-或空格中的一个，它出现1次或不出现(?)，最后是8个数字(\d{8})。不幸的是，它也能匹配010)12345678或(022-87654321这样的&#8220;不正确&#8221;的格式。要解决这个问题，请在本教程的下面查找答案。</p>
<h2 id="negation">
反义</h2>
<p>
有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外，其它任意字符都行的情况，这时需要用到反义：</p>
<table cellspacing="0">
    <caption>
    表3.常用的反义代码</caption>
    <tbody>
        <tr>
            <th>
            代码/语法</th>
            <th>
            说明</th>
        </tr>
        <tr>
            <td>                    \W</td>
            <td>                    匹配任意不是字母，数字，下划线，汉字的字符</td>
        </tr>
        <tr>
            <td>                    \S</td>
            <td>                    匹配任意不是空白符的字符</td>
        </tr>
        <tr>
            <td>                    \D</td>
            <td>                    匹配任意非数字的字符</td>
        </tr>
        <tr>
            <td>
            \B</td>
            <td>                    匹配不是单词开头或结束的位置</td>
        </tr>
        <tr>
            <td>                    [^x]</td>
            <td>                    匹配除了x以外的任意字符</td>
        </tr>
        <tr>
            <td>                    [^aeiou]</td>
            <td>
            匹配除了aeiou这几个字母以外的任意字符</td>
        </tr>
    </tbody>
</table>
<p>
例子：\S+匹配不包含空白符的字符串。</p>
<p>
&lt;a[^&gt;]+&gt;匹配用尖括号括起来的以a开头的字符串。</p>
<h2 id="alternative">
替换</h2>
<p>
好了，现在终于到了解决3位或4位区号问题的时间了。正则表达式里的替换指的是有几种规则，如果满足其中任意一种规则都应该当成匹配，具体方法是用|把不同的规则分隔开。听不明白？没关系，看例子：</p>
<p>
0\d{2}-\d{8}|0\d{3}-\d{7}这个表达式能匹配两种以连字号分隔的电话号码：一种是三位区号，8位本地号(如010-12345678)，一种是4位区号，7位本地号(0376-2233445)。</p>
<p>
\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}这个表达式匹配3位区号的电话号码，其中区号可以用小括号括起来，也可以不用，区号与本地号间可以用连字号或空格间隔，也可以没有间隔。你可以试试用替换|把这个表达式扩展成也支持4位区号的。</p>
<p>
\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字，或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题：<strong> 使用替换时，顺序是很重要的</strong> 。如果你把它改成\d{5}|\d{5}-\d{4}的话，那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配替换时，将会从左到右地测试每个分枝条件，如果满足了某个分枝的话，就不会去管其它的替换条件了。</p>
<p>
Windows98|Windows2000|WindosXP这个例子是为了告诉你替换不仅仅能用于两种规则，也能用于更多种规则。</p>
<h2 id="grouping">
分组</h2>
<p>
我们已经提到了怎么重复单个字符（直接在字符后面加上限定符就行了）；但如果想要重复多个字符又该怎么办？你可以用小括号来指定子表达式(也叫做分组)，然后你就可以指定这个子表达式的重复次数了，你也可以对子表达式进行其它一些操作(后面会有介绍)。</p>
<p>
(\d{1,3}\.){3}\d{1,3}是一个简单的IP地址匹配表达式。要理解这个表达式，请按下列顺序分析它：\d{1,3}匹配1到3位的数字，(\d{1,3}\.}{3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次，最后再加上一个一到三位的数字(\d{1,3})。</p>
<p>
不幸的是，它也将匹配256.300.888.999这
种不可能存在的IP地址(IP地址中每个数字都不能大于255。题外话，好像反恐24小时第三季的编剧不知道这一点，汗...)。如果能使用算术比较的
话，或许能简单地解决这个问题，但是正则表达式中并不提供关于数学的任何功能，所以只能使用冗长的分组，选择，字符类来描述一个正确的IP地址：((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。</p>
<p>
理解这个表达式的关键是理解2[0-4]\d|25[0-5]|[01]?\d\d?，这里我就不细说了，你自己应该能分析得出来它的意义。</p>
<h2 id="backreference">
后向引用</h2>
<p>
使用小括号指定一个子表达式后，<strong> 匹配这个子表达式的文本</strong> (也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下，每个分组会自动拥有一个组号，规则是：从左向右，以分组的左括号为标志，第一个出现的分组的组号为1，第二个为2，以此类推。</p>
<p>
后向引用用于重复搜索前面某个分组匹配的文本。例如，\1代表分组1匹配的文本。难以理解？请看示例：</p>
<p>
\b(\w+)\b\s+\1\b可以用来匹配重复的单词，像go go, kitty kitty。首先是一个单词，也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b)，然后是1个或几个空白符(\s+)，最后是前面匹配的那个单词(\1)。</p>
<p>
你也可以自己指定子表达式的组名。要指定一个子表达式的组名，请使用这样的语法：(?&lt;Word&gt;\w+)(或者把尖括号换成'也行：(?'Word'\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容，你可以使用\k&lt;Word&gt;,所以上一个例子也可以写成这样：\b(?&lt;Word&gt;\w+)\b\s+\k&lt;Word&gt;\b。</p>
<p>
使用小括号的时候，还有很多特定用途的语法。下面列出了最常用的一些：</p>
<table cellspacing="0">
    <caption>
    表4.分组语法</caption>
    <tbody>
        <tr>
            <th colspan="2">
            捕获</th>
        </tr>
        <tr>
            <td>                    (exp)</td>
            <td>
            匹配exp,并捕获文本到自动命名的组里</td>
        </tr>
        <tr>
            <td>                    (?&lt;name&gt;exp)</td>
            <td>
            匹配exp,并捕获文本到名称为name的组里，也可以写成(?'name'exp)</td>
        </tr>
        <tr>
            <td>                    (?:exp)</td>
            <td>                    匹配exp,不捕获匹配的文本，也不给此分组分配组号</td>
        </tr>
        <tr>
            <th colspan="2">
            零宽断言</th>
        </tr>
        <tr>
            <td>
            (?=exp)</td>
            <td>                    匹配exp前面的位置</td>
        </tr>
        <tr>
            <td>                    (?&lt;=exp)</td>
            <td>
            匹配exp后面的位置</td>
        </tr>
        <tr>
            <td>                    (?!exp)</td>
            <td>
            匹配后面跟的不是exp的位置</td>
        </tr>
        <tr>
            <td>
            (?&lt;!exp)</td>
            <td>
            匹配前面不是exp的位置</td>
        </tr>
        <tr>
            <th colspan="2">
            注释</th>
        </tr>
        <tr>
            <td>                    (?#comment)</td>
            <td>                    这种类型的组不对正则表达式的处理产生任何影响，用于提供注释让人阅读</td>
        </tr>
    </tbody>
</table>
<p>
我们已经讨论了前两种语法。第三个(?:exp)不会改变正则表达式的处理方式，只是这样的组匹配的内容不会像前两种那样被捕获到某个组里面。</p>
<h2 id="lookaround">
零宽断言</h2>
<p>
接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西，也就是说它们像\b,^,$那样用于指定一个位置，这个位置应该满足一定的条件(<a href="http://www.unibetter.com/deerchao/zhengzhe-biaodashi-jiaocheng-se.htm#reference">断言</a>)，因此它们也被称为零宽断言。最好还是拿例子来说明吧：</p>
<p>
(?=exp)也叫零宽度正预测先行断言，它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b)，匹配以ing结尾的单词的前面部分(除了ing以外的部分)，如查找I'm singing while you're dancing.时，它会匹配sing和danc。</p>
<p>
(?&lt;=exp)也叫零宽度正回顾后发断言，它断言自身出现的位置的前面能匹配表达式exp。比如(?&lt;=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分)，例如在查找reading a book时，它匹配ading。</p>
<p>
假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了)，你可以这样查找需要在前面和里面添加逗号的部分：((?&lt;=\d)\d{3})*\b，用它对1234567890进行查找时结果是234567890。</p>
<p>
下面这个例子同时使用了这两种断言：(?&lt;=\s)\d+(?=\s)匹配以空白符间隔的数字(再次强调，不包括这些空白符)。</p>
<h2 id="negativelookaround">
负向零宽断言</h2>
<p>
前面我们提到过怎么查找<strong> 不是某个字符或不在某个字符类里</strong> 的字符的方法(反义)。但是如果我们只是想要<strong> 确保某个字符没有出现，但并不想去匹配它</strong> 时怎么办？例如，如果我们想查找这样的单词--它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样：</p>
<p>
\b\w*q[^u]\w*\b匹配包含<strong> 后面不是字母u的字母q</strong> 的单词。但是如果多做测试(或者你思维足够敏锐，直接就观察出来了)，你会发现，如果q出现在单词的结尾的话，像<strong> Iraq</strong> ,<strong> Benq</strong> ，这个表达式就会出错。这是因为[^u]总要匹配一个字符，所以如果q是单词的最后一个字符的话，后面的[^u]将会匹配q后面的单词分隔符(可能是空格，或者是句号或其它的什么)，后面的\w*\b将会匹配下一个单词，于是\b\w*q[^u]\w*\b就能匹配整个Iraq fighting。负向零宽断言能解决这样的问题，因为它只匹配一个位置，并不<strong> 消费</strong> 任何字符。现在，我们可以这样来解决这个问题：\b\w*q(?!u)\w*\b。</p>
<p>
零宽度负预测先行断言(?!exp)，断言此位置的后面不能匹配表达式exp。例如：\d{3}(?!\d)匹配三位数字，而且这三位数字的后面不能是数字；\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。</p>
<p>
同理，我们可以用(?&lt;!exp),零宽度正回顾后发断言来断言此位置的前面不能匹配表达式exp：(?&lt;![a-z])\d{7}匹配前面不是小写字母的七位数字。</p>
<p>
一个更复杂的例子：(?&lt;=&lt;(\w+)&gt;).*(?=&lt;\/\1&gt;)匹配不包含属性的简单HTML标签内里的内容。(&lt;?(\w+)&gt;)指定了这样的前缀：被尖括号括起来的单词(比如可能是&lt;b&gt;)，然后是.*(任意的字符串),最后是一个后缀(?=&lt;\/\1&gt;)。注意后缀里的\/，它用到了前面提过的字符转义；\1则是一个反向引用，引用的正是捕获的第一组，前面的(\w+)匹配的内容，这样如果前缀实际上是&lt;b&gt;的话，后缀就是&lt;/b&gt;了。整个表达式匹配的是&lt;b&gt;和&lt;/b&gt;之间的内容(再次提醒，不包括前缀和后缀本身)。</p>
<h2 id="commenting">
注释</h2>
<p>
小括号的另一种用途是能过语法(?#comment)来包含注释。例如：2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)。</p>
<p>
要包含注释的话，最好是启用&#8220;忽略模式里的空白符&#8221;选项，这样在编写表达式时能任意的添加空格，Tab，换行，而实际使用时这些都将被忽略。启用这个选项后，在#后面到这一行结束的所有文本都将被当成注释忽略掉。</p>
<p>
例如，我们可以前面的一个表达式写成这样：</p>
<pre class="regex">      (?&lt;=    # 断言要匹配的文本的前缀<br>      &lt;(\w+)&gt; # 查找尖括号括起来的字母或数字(即HTML/XML标签)<br>      )       # 前缀结束<br>      .*      # 匹配任意文本<br>      (?=     # 断言要匹配的文本的后缀<br>      &lt;\/\1&gt;  # 查找尖括号括起来的内容：前面是一个"/"，后面是先前捕获的标签<br>      )       # 后缀结束<br>    </pre>
<h2 id="greedyandlazy">
贪婪与懒惰</h2>
<p>
当正则表达式中包含能接受重复的限定符时，通常的行为是（在使整个表达式能得到匹配的前提下）匹配<strong> 尽可能多</strong> 的字符。考虑这个表达式：a.*b，它将会匹配最长的以a开始，以b结束的字符串。如果用它来搜索aabab的话，它会匹配整个字符串aabab。这被称为贪婪匹配。</p>
<p>
有时，我们更需要懒惰匹配，也就是匹配<strong> 尽可能少</strong> 的字符。前面给出的限定符都可以被转化为懒惰匹配模式，只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复，但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧：</p>
<p>
a.*?b匹配最短的，以a开始，以b结束的字符串。如果把它应用于aabab的话，它会匹配aab和ab（为什么第一个匹配是aab而不是ab？简单地说，因为正则表达式有另一条规则，比懒惰／贪婪规则的优先级更高：最先开始的匹配最有最大的优先权——The Match That Begins Earliest Wins）。</p>
<table cellspacing="0">
    <caption>
    表5.懒惰限定符</caption>
    <tbody>
        <tr>
            <td>                    *?</td>
            <td>                    重复任意次，但尽可能少重复</td>
        </tr>
        <tr>
            <td>                    +?</td>
            <td>                    重复1次或更多次，但尽可能少重复</td>
        </tr>
        <tr>
            <td>                    ??</td>
            <td>                    重复0次或1次，但尽可能少重复</td>
        </tr>
        <tr>
            <td>                    {n,m}?</td>
            <td>                    重复n到m次，但尽可能少重复</td>
        </tr>
        <tr>
            <td>
            {n,}?</td>
            <td>                    重复n次以上，但尽可能少重复</td>
        </tr>
    </tbody>
</table>
<h2 id="regexoptions">处理选项</h2>
<p>上面介绍了几个选项如忽略大小写，处理多行等，这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项：</p>
<table cellspacing="0">
    <caption>表6.常用的处理选项</caption>
    <thead>
        <tr>
            <th>名称</th>
            <th>说明</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>IgnoreCase(忽略大小写)</td>
            <td>匹配时不区分大小写。</td>
        </tr>
        <tr>
            <td>Multiline(多行模式)</td>
            <td>更改^和$的含义，使它们分别在任意一行的行首和行尾匹配，而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.) </td>
        </tr>
        <tr>
            <td>Singleline(单行模式)</td>
            <td>更改.的含义，使它与每一个字符匹配（包括换行符\n）。 </td>
        </tr>
        <tr>
            <td>IgnorePatternWhitespace(忽略空白)</td>
            <td>忽略表达式中的非转义空白并启用由#标记的注释。</td>
        </tr>
        <tr>
            <td>RightToLeft(从右向左查找)</td>
            <td>匹配从右向左而不是从左向右进行。</td>
        </tr>
        <tr>
            <td>ExplicitCapture(显式捕获)</td>
            <td>仅捕获已被显式命名的组。</td>
        </tr>
        <tr>
            <td>ECMAScript(JavaScript兼容模式)</td>
            <td>使表达式的行为与它在JavaScript里的行为一致。</td>
        </tr>
    </tbody>
</table>
<p>一个经常被问到的问题是：是不是只能同时使用多行模式和单行模式中的一种？答案是：不是。这两个选项之间没有任何关系，除了它们的名字比较相似（以至于让人感到疑惑）以外。</p>
<h2 id="balancedgroup">
平衡组/递归匹配</h2>
<p>
注意：这里介绍的平衡组语法是由.Net Framework支持的；其它语言／库不一定支持这种功能，或者支持此功能但需要使用不同的语法。
</p>
<p>
有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构，这时简单地使用\(.+\)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式，懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等，比如( 5 / ( 3 + 2 ) ) )，那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的，配对的括号之间的内容呢？
</p>
<p>
为了避免(和\(把你的大脑彻底搞糊涂，我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把xx &lt;aa &lt;bbb&gt; &lt;bbb&gt; aa&gt; yy这样的字符串里，最长的配对的尖括号内的内容捕获出来？
</p>
<p>
这里需要用到以下的语法构造：</p>
<ul>
    <li>(?'group') 把捕获的内容命名为group,并压入堆栈</li>
    <li>(?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容，如果堆栈本来为空，则本分组的匹配失败</li>
    <li>(?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话，继续匹配yes部分的表达式，否则继续匹配no部分</li>
    <li>(?!) 零宽负向先行断言，由于没有后缀表达式，试图匹配总是失败</li>
</ul>
<p>
如果你不是一个程序员（或者你是一个对堆栈的概念不熟的程序员），你就这样理解上面的三种语法吧：第一个就是在黑板上写一个
"group"，第二个就是从黑板上擦掉一个"group"，第三个就是看黑板上写的还有没有"group"，如果有就继续匹配yes部分，否则就匹配
no部分。</p>
<p>
我们需要做的是每碰到了左括号，就在黑板上写一个"group"，每碰到一个右括号，就擦掉一个，到了最后就看看黑板上还有没有－－如果有那就证明左括号比右括号多，那匹配就应该失败。
</p>
<pre class="regex">&lt;                         #最外层的左括号<br>    [^&lt;&gt;]*                #最外层的左括号后面的不是括号的内容<br>    (<br>        (<br>            (?'Open'&lt;)    #碰到了左括号，在黑板上写一个"Open"<br>            [^&lt;&gt;]*       #匹配左括号后面的不是括号的内容<br>        )+<br>        (<br>            (?'-Open'&gt;)   #碰到了右括号，擦掉一个"Open"<br>            [^&lt;&gt;]*        #匹配右括号后面不是括号的内容<br>        )+<br>    )*<br>    (?(Open)(?!))         #在遇到最外层的右括号前面，判断黑板上还有没有没擦掉的"Open"；如果还有，则匹配失败<br>&gt;                         #最外层的右括号</pre>
<p>
平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的&lt;div&gt;标签：&lt;div[^&gt;]*&gt;[^&lt;&gt;]*(((?'Open'&lt;div[^&gt;]*&gt;)[^&lt;&gt;]*)+((?'-Open'&lt;/div&gt;)[^&lt;&gt;]*)+)*(?(Open)(?!))&lt;/div&gt;.</p>
<h2 id="more">
还有些什么东西没提到</h2>
<p>
我已经描述了构造正则表达式的大量元素，还有一些我没有提到的东西。下面是未提到的元素的列表，包含语法和简单的说明。你可以在网上找到更详细的参考资料来学习它们--当你需要用到它们的时候。如果你安装了MSDN
Library,你也可以在里面找到关于.net下正则表达式详细的文档。</p>
<table cellspacing="0">
    <caption>
    表7.尚未详细讨论的语法</caption>
    <tbody>
        <tr>
            <td>
            \a</td>
            <td>                    报警字符(打印它的效果是电脑嘀一声)</td>
        </tr>
        <tr>
            <td>                    \b</td>
            <td>                    通常是单词分界位置，但如果在字符类里使用代表退格</td>
        </tr>
        <tr>
            <td>                    \t</td>
            <td>                    制表符，Tab</td>
        </tr>
        <tr>
            <td>
            \r</td>
            <td>                    回车</td>
        </tr>
        <tr>
            <td>                    \v</td>
            <td>                    竖向制表符</td>
        </tr>
        <tr>
            <td>                    \f</td>
            <td>
            换页符</td>
        </tr>
        <tr>
            <td>                    \n</td>
            <td>                    换行符</td>
        </tr>
        <tr>
            <td>                    \e</td>
            <td>                    Escape</td>
        </tr>
        <tr>
            <td>                    \0nn</td>
            <td>                    ASCII代码中八进制代码为nn的字符</td>
        </tr>
        <tr>
            <td>                    \xnn</td>
            <td>                    ASCII代码中十六进制代码为nn的字符</td>
        </tr>
        <tr>
            <td>
            \unnnn</td>
            <td>                    Unicode代码中十六进制代码为nnnn的字符</td>
        </tr>
        <tr>
            <td>                    \cN</td>
            <td>                    ASCII控制字符。比如\cC代表Ctrl+C</td>
        </tr>
        <tr>
            <td>                    \A</td>
            <td>
            字符串开头(类似^，但不受处理多行选项的影响)</td>
        </tr>
        <tr>
            <td>                    \Z</td>
            <td>                    字符串结尾或行尾(不受处理多行选项的影响)</td>
        </tr>
        <tr>
            <td>                    \z</td>
            <td>                    字符串结尾(类似$，但不受处理多行选项的影响)</td>
        </tr>
        <tr>
            <td>                    \G</td>
            <td>                    当前搜索的开头</td>
        </tr>
        <tr>
            <td>                    \p{name}</td>
            <td>                    Unicode中命名为name的字符类，例如\p{IsGreek}</td>
        </tr>
        <tr>
            <td>
            (?&gt;exp)</td>
            <td>                    贪婪子表达式</td>
        </tr>
        <tr>
            <td>                    (?&lt;x&gt;-&lt;y&gt;exp)</td>
            <td>                    平衡组</td>
        </tr>
        <tr>
            <td>                    (?im-nsx:exp)</td>
            <td>
            在子表达式exp中改变处理选项</td>
        </tr>
        <tr>
            <td>                    (?im-nsx)</td>
            <td>                    为表达式后面的部分改变处理选项</td>
        </tr>
        <tr>
            <td>                    (?(exp)yes|no)</td>
            <td>                    把exp当作零宽正向先行断言，如果在这个位置能匹配，使用yes作为此组的表达式；否则使用no</td>
        </tr>
        <tr>
            <td>                    (?(exp)yes)</td>
            <td>                    同上，只是使用空表达式作为no</td>
        </tr>
        <tr>
            <td>                    (?(name)yes|no)</td>
            <td>                    如果命名为name的组捕获到了内容，使用yes作为表达式；否则使用no</td>
        </tr>
        <tr>
            <td>
            (?(name)yes)</td>
            <td>                    同上，只是使用空表达式作为no</td>
        </tr>
    </tbody>
</table>
<h2 id="contact">联系作者</h2>
<p>好吧,我承认,我骗了你,读到这里你肯定花了不止30分钟.相信我,这是我的错,而不是因为你太笨.我之所以说"30分钟",是为了让你有信心,有耐心继续下去.既然你看到了这里,那证明我的阴谋成功了.上这种当的滋味还不错吧?</p>
<p>要投诉我,或者觉得我其实可以做得更好,或者有任何其它问题,欢迎来<a href="http://www.cnblogs.com/deerchao/archive/2006/08/24/zhengzhe30fengzhongjiaocheng.html">我的博客</a>进行讨论.</p>
<h2 id="reference">
一些我认为你可能已经知道的术语的参考</h2>
<dl><dt>字符</dt><dd>程序处理文字时最基本的单位，可能是字母，数字，标点符号，空格，换行符，汉字等等。</dd><dt>字符串</dt><dd>0个或更多个字符的序列。</dd><dt>文本</dt><dd>文字，字符串。</dd><dt>匹配</dt><dd>符合规则，检验是否符合规则，符合规则的部分。</dd><dt>断言</dt><dd>声明一个应该为真的事实。只有当断言为真时才会对正则表达式继续进行匹配。</dd></dl>
<h2 id="resources">
网上的资源及本文参考文献</h2>
<ul>
    <li><a href="http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/jscript7/html/jsreconintroductiontoregularexpressions.asp">
    微软的正则表达式教程</a></li>
    <li><a href="http://msdn2.microsoft.com/zh-cn/library/system.text.regularexpressions.regex.aspx">System.Text.RegularExpressions.Regex类(MSDN)</a></li>
    <li><a href="http://www.regular-expressions.info/">专业的正则表达式教学网站(英文)</a></li>
    <li><a href="http://weblogs.asp.net/whaggard/archive/2005/02/20/377025.aspx">关于.Net下的平衡组的详细讨论（英文）</a></li>
    <li><a href="http://www.oreilly.com/catalog/regex2/">Mastering Regular Expressions (Second Edition)</a></li>
    <li><a href="http://validator.w3.org/check?uri=referer">Validated XHTML 1.0 Strict</a></li>
    <li><a href="http://jigsaw.w3.org/css-validator/check/referer">Validated CSS 2.1</a></li>
    <li><a href="http://www.mozilla.com/">推荐使用Mozilla FireFox浏览</a></li>
</ul>
<h2 id="updatelog">更新说明</h2>
<ol>
    <li>2006-3-27 第一版</li>
    <li>2006-10-12 第二版
    <ul>
        <li>修正了几个细节上的错误和不准确的地方</li>
        <li>增加了对处理中文时的一些说明</li>
        <li>更改了几个术语的翻译（采用了MSDN的翻译方式）</li>
        <li>增加了平衡组的介绍</li>
        <li>放弃了对The Regulator的介绍，改用Regex Tester</li>
    </ul>
    </li>
    <li>2007-3-12 V2.1
    <ul>
        <li>修正了几个小的错误</li>
        <li>增加了对处理选项(RegexOptions)的介绍</li>
    </ul>
    </li>
    <li>2007-5-28 V2.2
    <ul>
        <li>重新组织了对零宽断言的介绍</li>
        <li>删除了几个不太合适的示例，添加了几个实用的示例</li>
        <li>其它一些微小的更改</li>
    </ul>
    </li>
    <li>2007-8-3 V2.21
    <ul>
        <li>修改了几处文字错误</li>
        <li>修改/添加了对$,\b的精确说明</li>
        <li>承认了作者是个骗子</li>
        <li>给RegexTester添加了Singleline选项的相关功能</li>
    </ul>
    </li>
</ol>
<br> <img src ="http://www.cppblog.com/SmartPtr/aggbug/30723.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/SmartPtr/" target="_blank">SmartPtr</a> 2007-08-24 06:59 <a href="http://www.cppblog.com/SmartPtr/articles/30723.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>