C++编译器无法捕捉到的8种错误

C++是一种复杂的编程语言,其中充满了各种微妙的陷阱。在 C++ 中几乎有数不清的方式能把事情搞砸。幸运的是,如今的编译器已经足够智能化了,能够检测出相当多的这类编程陷阱并通过编译错误或编译警告来通知程序员。最 终,如果处理得当的话,任何编译器能检查到的错误都不会是什么大问题,因为它们在编译时会被捕捉到,并在程序真正运行前得到解决。最坏的情况下,一个编译 器能够捕获到的错误只会造成程序员一些时间上的损失,因为他们会寻找解决编译错误的方法并修正。

那些编译器无法捕获到的错误才是最危险的。这类错误不太容易察觉到,但可能会导致严重的后果,比如不正确的输出、数据被破坏以及程序崩溃。随着 项目的膨胀,代码逻辑的复杂度以及众多的执行路径会掩盖住这些 bug,导致这些 bug 只是间歇性的出现,因此使得这类 bug 难以跟踪和调试。尽管本文的这份列表对于有经验的程序员来说大部分都只是回顾,但这类 bug 产生的后果往往根据项目的规模和商业性质有不同程度的增强效果。

这些示例全部都在 Visual Studio 2005 Express 上测试过,使用的是默认告警级别。根据你选择的编译器,你得到的结果可能会有所不同。我强烈建议所有的程序员朋友都采用最高等级的告警级别!有一些编译提示在默认告警级别下可能不会被标注为一个潜在的问题,而在最高等级的告警级别下就会被捕捉到!(注:本文是这个系列文章的第 1 部分)

1)变量未初始化

变量未初始化是 C++ 编程中最为常见和易犯的错误之一。在 C++ 中,为变量所分配的内存空间并不是完全“干净的”,也不会在分配空间时自动做清零处理。其结果就是,一个未初始化的变量将包含某个值,但没办法准确地知道 这个值是多少。此外,每次执行这个程序的时候,该变量的值可能都会发生改变。这就有可能产生间歇性发作的问题,是特别难以追踪的。看看如下的代码片段:

  if (bValue)

       // do A    else        // do B

如果 bValue 是未经初始化的变量,那么 if 语句的判断结果就无法确定,两个分支都可能会执行。在一般情况下,编译器会对未初始化的变量给予提示。下面的代码片段在大多数编译器上都会引发一个警告信息。

  int foo ()
  {
      int nX;
      return nX;
  }

但是,还有一些简单的例子则不会产生警告:

  void increment (int &nValue)
  {
      ++nValue;
  }
  int foo ()
  {
      int nX;
      increment (nX);
      return nX;
  }

以上的代码片段可能不会产生一个警告,因为编译器一般不会去跟踪查看函数 increment ()到底有没有对 nValue 赋值。

未初始化变量更常出现于类中,成员的初始化一般是通过构造函数的实现来完成的。

  class Foo
  {
  private:
      int m_nValue;

  public:
      Foo ();
      int GetValue () { return m_bValue; }
  };

  Foo::Foo ()
  {
      // Oops, 我们忘记初始化m_nValue 了    }
  int main ()
  {
      Foo cFoo;
      if (cFoo.GetValue () > 0)
          // do something        else           // do something else    }

注意,m_nValue 从未初始化过。结果就是,GetValue ()返回的是一个垃圾值,if 语句的两个分支都有可能会执行。

新手程序员通常在定义多个变量时会犯下面这种错误:

int nValue1, nValue2 = 5;

这里的本意是 nValue1 和 nValue2 都被初始化为5,但实际上只有 nValue2 被初始化了,nValue1从未被初始化过。

由于未初始化的变量可能是任何值,因此会导致程序每次执行时呈现出不同的行为,由未初始化变量而引发的问题是很难找到问题根源的。某次执行时, 程序可能工作正常,下一次再执行时,它可能会崩溃,而再下一次则可能产生错误的输出。当你在调试器下运行程序时,定义的变量通常都被清零处理过了。这意味 着你的程序在调试器下可能每次都是工作正常的,但在发布版中可能会间歇性的崩掉!如果你碰上了这种怪事,罪魁祸首常常都是未初始化的变量。

2)整数除法

C++中的大多数二元操作都要求两个操作数是同一类型。如果操作数的不同类型,其中一个操作数会提升到和另一个操作数相匹配的类型。在 C++ 中,除法操作符可以被看做是 2 个不同的操作:其中一个操作于整数之上,另一个是操作于浮点数之上。如果操作数是浮点数类型,除法操作将返回一个浮点数的值:

  float fX = 7;
  float fY = 2;
  float fValue = fX / fY; // fValue = 3.5

如果操作数是整数类型,除法操作将丢弃任何小数部分,并只返回整数部分。

  int  nX = 7;
  int nY = 2;
  int nValue = nX / nY;   //  nValue = 3

如果一个操作数是整型,另一个操作数是浮点型,则整型会提升为浮点型:

  float fX = 7. 0;
  int nY = 2;
  float fValue = fX / nY;
  // nY 提升为浮点型,除法操作将返回浮点型值
  // fValue = 3.5

有很多新手程序员会尝试写下如下的代码:

  int nX = 7;
  int nY = 2;
  float fValue = nX / nY;  // fValue = 3(不是3.5哦!)

这里的本意是 nX/nY 将产生一个浮点型的除法操作,因为结果是赋给一个浮点型变量的。但实际上并非如此。nX/nY 首先被计算,结果是一个整型值,然后才会提升为浮点型并赋值给 fValue。但在赋值之前,小数部分就已经丢弃了。

要强制两个整数采用浮点型除法,其中一个操作数需要类型转换为浮点数:

  int nX = 7;
  int nY = 2;
  float fValue = static_cast<float>(nX) / nY;  // fValue = 3.5

因为 nX 显式的转换为 float 型,nY 将隐式地提升为 float 型,因此除法操作符将执行浮点型除法,得到的结果就是3.5。

通常一眼看去很难说一个除法操作符究竟是执行整数除法还是浮点型除法:

 z = x / y;  // 这是整数除法还是浮点型除法?

但采用匈牙利命名法可以帮助我们消除这种疑惑,并阻止错误的发生:

  int nZ = nX / nY;     // 整数除法    double dZ = dX / dY; // 浮点型除法

有关整数除法的另一个有趣的事情是,当一个操作数是负数时 C++ 标准并未规定如何截断结果。造成的结果就是,编译器可以自由地选择向上截断或者向下截断!比如,-5/2可以既可以计算为-3也可以计算为-2,这和编译 器是向下取整还是向 0 取整有关。大多数现代的编译器是向 0 取整的。

3)=  vs  ==

这是个老问题,但很有价值。许多 C++ 新手会弄混赋值操作符(=)和相等操作符(==)的意义。但即使是知道这两种操作符差别的程序员也会犯下键盘敲击错误,这可能会导致结果是非预期的。

 // 如果 nValue 是0,返回1,否则返回 nValue    int foo (int nValue)
  {
      if (nValue = 0)  // 这是个键盘敲击错误 !            return 1;
      else           return nValue;
  }

  int main ()
  {
      std::cout << foo (0) << std::endl;
      std::cout << foo (1) << std::endl;
      std::cout << foo (2) << std::endl;
      return 0;
  }

函数 foo ()的本意是如果 nValue 是0,就返回1,否则就返回 nValue 的值。但由于无意中使用赋值操作符代替了相等操作符,程序将产生非预期性的结果:

  0   0   0

当 foo ()中的 if 语句执行时,nValue 被赋值为0。if (nValue = 0)实际上就成了 if (nValue)。结果就是 if 条件为假,导致执行 else 下的代码,返回 nValue 的值,而这个值刚好就是赋值给 nValue 的0!因此这个函数将永远返回0。

在编译器中将告警级别设置为最高,当发现条件语句中使用了赋值操作符时会给出一个警告信息,或者在条件判断之外,应该使用赋值操作符的地方误用 成了相等性测试,此时会提示该语句没有做任何事情。只要你使用了较高的告警级别,这个问题本质上都是可修复的。也有一些程序员喜欢采用一种技巧来避免= 和==的混淆。即,在条件判断中将常量写在左边,此时如果误把==写成=的话,将引发一个编译错误,因为常量不能被赋值。

4)混用有符号和无符号数

如同我们在整数除法那一节中提到的,C++中大多数的二元操作符需要两端的操作数是同一种类型。如果操作数是不同的类型,其中一个操作数将提升自己的类型以匹配另一个操作数。当混用有符号和无符号数时这会导致出现一些非预期性的结果!考虑如下的例子:

  cout << 10 – 15u;  // 15u 是无符号整数

有人会说结果是-5。由于 10 是一个有符号整数,而 15 是无符号整数,类型提升规则在这里就需要起作用了。C++中的类型提升层次结构看起来是这样的:

  long double (最高)
  double   float   unsigned long int   long int   unsigned int   int               (最低)

因为 int 类型比 unsigned int 要低,因此 int 要提升为 unsigned int。幸运的是,10已经是个正整数了,因此类型提升并没有使解释这个值的方式发生改变。因此,上面的代码相当于:

  cout << 10u – 15u;

好,现在是该看看这个小把戏的时候了。因为都是无符号整型,因此操作的结果也应该是一个无符号整型的变量!10u-15u = -5u。但是无符号变量不包括负数,因此-5这里将被解释为4,294,967,291(假设是 32 位整数)。因此,上面的代码将打印出4,294,967,291而不是-5。

这种情况可以有更令人迷惑的形式:

  int nX;
  unsigned int nY;
  if (nX – nY < 0)
      // do something

由于类型转换,这个 if 语句将永远判断为假,这显然不是程序员的原始意图!

C++编译器无法捕捉到的 8 种错误

5)  delete  vs  delete []

许多 C++ 程序员忘记了关于 new 和 delete 操作符实际上有两种形式:针对单个对象的版本,以及针对对象数组的版本。new 操作符用来在堆上分配单个对象的内存空间。如果对象是某个类类型,该对象的构造函数将被调用。

Foo *pScalar = new Foo;

delete 操作符用来回收由 new 操作符分配的内存空间。如果被销毁的对象是类类型,则该对象的析构函数将被调用。

delete pScalar;

现在考虑如下的代码片段:

 Foo *pArray = new Foo[10];

这行代码为 10 个 Foo 对象的数组分配了内存空间,因为下标[10]放在了类型名之后,许多 C++ 程序员没有意识到实际上是操作符 new[]被调用来完成分配空间的任务而不是 new。new[]操作符确保每一个创建的对象都会调用该类的构造函数一次。相反的,要删除一个数组,需要使用 delete[]操作符:

  delete[] pArray;

这将确保数组中的每个对象都会调用该类的析构函数。如果 delete 操作符作用于一个数组会发生什么?数组中仅仅只有第一个对象会被析构,因此会导致堆空间被破坏!

6)  复合表达式或函数调用的副作用

副作用是指一个操作符、表达式、语句或函数在该操作符、表达式、语句或函数完成规定的操作后仍然继续做了某些事情。副作用有时候是有用的:

x=5

赋值操作符的副作用是可以永久地改变x的值。其他有副作用的 C++ 操作符包括*=、/=、%=、+=、-=、<<=、>&gt;=、&=、=、^=以及声名狼藉的++和—操作符。但 是,在 C++ 中有好几个地方操作的顺序是未定义的,那么这就会造成不一致的行为。比如:

  void multiply (int x, int y)
  {
      using namespace std;
      cout << x * y << endl;
  }

  int main ()
  {
      int x = 5;
      std::cout << multiply (x, ++x);
  }

因为对于函数 multiply ()的参数的计算顺序是未定义的,因此上面的程序可能打印出 30 或 36,这完全取决于x和++x谁先计算,谁后计算。

另一个稍显奇怪的有关操作符的例子:

  int foo (int x)
  {
      return x;
  }

  int main ()
  {
      int x = 5;
      std::cout << foo (x) * foo (++x);
  }

因为 C++ 的操作符中,其操作数的计算顺序是未定义的(对于大多数操作符来说是这样的,当然有一些例外),上面的例子也可能会打印出 30 或 36,这取决于究竟是左操作数先计算还是右操作数先计算。

另外,考虑如下的复合表达式:

  if (x == 1 && ++y == 2)
      // do something

程序员的本意可能是说:“如果x是1,且y的前自增值是 2 的话,完成某些处理”。但是,如果x不等于1,C++将采取短路求值法则,这意味着++y将永远不会计算!因此,只有当x等于 1 时,y才会自增。这很可能不是程序员的本意!一个好的经验法则是把任何可能造成副作用的操作符都放到它们自己独立的语句中去。

7)不带breakswitch语句

另一个新手程序员常犯的经典错误是忘记在 switch 语句块中加上 break:

  switch (nValue)
  {
      case 1: eColor = Color::BLUE;
      case 2: eColor = Color::PURPLE;
      case 3: eColor = Color::GREEN;
      default: eColor = Color::RED;
  }

当 switch 表达式计算出的结果同 case 的标签值相同时,执行序列将从满足的第一个 case 语句处执行。执行序列将继续下去,直到要么到达 switch 语句块的末尾,或者遇到 return、goto 或 break 语句。其他的标签都将忽略掉!

考虑下如上的代码,如果 nValue 为 1 时会发生什么。case 1 满足,所以 eColor 被设为 Color::BLUE。继续处理下一个语句,这又将 eColor 设为 Color::PURPLE。下一个语句又将它设为了 Color::GREEN。最终,在 default 中将其设为了 Color::RED。实际上,不管 nValue 的值是多少,上述代码片段都将把 eColor 设为 Color::RED!

正确的方法是按照如下方式书写:

  switch (nValue)
  {
      case 1: eColor = Color::BLUE; break;
      case 2: eColor = Color::PURPLE; break;
      case 3: eColor = Color::GREEN; break;
      default: eColor = Color::RED; break;
  }

break 语句终止了 case 语句的执行,因此 eColor 的值将保持为程序员所期望的那样。尽管这是非常基础的 switch/case 逻辑,但很容易因为漏掉一个 break 语句而造成不可避免的“瀑布式”执行流。

8)在构造函数中调用虚函数

考虑如下的程序:

  class Base
  {
  private:
      int m_nID;

  public:
      Base ()
      {
          m_nID = ClassID ();
      }

      // ClassID 返回一个 class 相关的 ID 号        virtual int ClassID () { return 1;}
      int GetID () { return m_nID; }
  };

  class Derived: public Base
  {

  public:
      Derived ()
      {
      }
      virtual int ClassID () { return 2;}
  };

  int main ()
  {
      Derived cDerived;
      cout << cDerived.GetID (); // 打印出1,不是2!        return 0;
  }

在这个程序中,程序员在基类的构造函数中调用了虚函数,期望它能被决议为派生类的 Derived::ClassID ()。但实际上不会这样——程序的结果是打印出 1 而不是2。当从基类继承的派生类被实例化时,基类对象先于派生类对象被构造出来。这么做是因为派生类的成员可能会对已经初始化过的基类成员有依赖关系。结 果就是当基类的构造函数被执行时,此时派生类对象根本就还没有构造出来!所以,此时任何对虚函数的调用都只会决议为基类的成员函数,而不是派生类。

根据这个例子,当 cDerived 的基类部分被构造时,其派生类的那一部分还不存在。因此,对函数 ClassID 的调用将决议为 Base::ClassID ()(不是 Derived::ClassID ()),这个函数将m_nID 设为1。一旦 cDerived 的派生类部分也构造好时,在 cDerived 这个对象上,任何对 ClassID ()的调用都将如预期的那样决议为 Derived::ClassID ()。

注意到其他的编程语言如C#和Java会将虚函数调用决议为继承层次最深的那个 class 上,就算派生类还没有被初始化也是这样!C++的做法与这不同,这是为了程序员的安全而考虑的。这并不是说一种方式就一定好过另一种,这里仅仅是为了表示 不同的编程语言在同一问题上可能有不同的表现行为。

结论

因为这只是这个系列文章的第一篇,我认为以新手程序员可能遇到的基础问题入手会比较合适。今后这个系列的文章将致力于解决更加复杂的编程错误。 无论一个程序员的经验水平如何,错误都是不可避免的,不管是因为知识上的匮乏、输入错误或者只是一般的粗心大意。意识到其中最有可能造成麻烦的问题,这可 以帮助减少它们出来捣乱的可能性。虽然对于经验和知识并没有什么替代品,良好的单元测试可以帮我们在将这些 bug 深埋于我们的代码中之前将它们捕获。

英文原文:ALEX  编译:伯乐在线 — 陈舸

云平台:诸神之战

编者注:Michael Driscoll是Metamarkets的联合创始人兼CTO。他最近发表了一篇文章, 文章介绍了目前最有竞争力的几个云平台:Amazon Web Services、微软的Azure、Google App Engine以及EMC Virtual Private Cloud,并分析了各自的优势。作者认为云平台的实质就是计算、存储以及网络的虚拟化(抽象),谁赢下了云平台之争,就会像操作系统之争一样成为下一个 时代的新霸主。而一旦这个霸主确立,技术就会向着更高层面的抽象继续演进,那时候应用即服务型的初创企业将会百花齐放。

很多年以前情况就已经很显然——必须找到某种方式让程序员与硬件的复杂性无缘……这一层东西就是操作系统

—安德鲁·谭宁邦《操作系统(英文)》

云 就是企业新的操作系统,服务就是新的应用。云为下一代的服务提供庇护,从Pinterest到Instagram,从foursquare到AirBnB 都在腾云驾雾。这厢,为上一代的桌面应用提供接口的微软Windows和苹果MacOS X已如日落西山(注:这个结论也许下得太早,这些老牌操作系统本身也在蜕变),那厢,为上述服务提供必需的计算、存储和网络的Amazon之类供应商正冉 冉升起。

相对于上一辈,云这个操作系统更灵活,容错能力更高。云的这两个优势源自它的两个招牌特性:虚拟化和分 布式。因为虚拟化,失效的硬件可以升级或被换出,虚拟流程可移植到新机器,几乎不会对最终用户产生任何影响。因为分布式,云可以散布在成千上万个商品化的 设备中,服务的计算和带宽需求可通过灵活伸缩来满足,磁盘存储限制几乎已成时代逆流。

云在其他方面也带来了新挑 战,更确切地说又把我们带回了前PC时代的客户/服务器关系(注:又一次印证了螺旋式上升的哲学原理)。如果说云是更强大的主机的话,那么今天的客户端则 比昔日的哑终端更加智能。新的客户机是智能手机、平板电脑,以及更加现代的web浏览器,其本地缓存与计算能力乃富交互应用之精髓。不过,管理客户机与服 务器之间以及跨越不同类别客户机之间的状态会给开发人员带来复杂性。

这个计算新世纪打开了新的市场与机遇之门。这场决定战役的主题是:谁会成为云计算王国的标准平台?

这个赌注很大,不仅仅因为效用计算的开支增长迅速。跟以往操作系统之争一样,这也是一场生死攸关的战争,争夺的焦点是运行于云平台之上的应用之控制权。

Amazon Web服务:王座无可争议

Amazon的Web Services(AWS)初步取得领先。AWS在其核心的弹性计算云(Elastic Compute Cloud)之上提供了一组不断扩展的周边设备,如块存储、负载均衡以及内容交付网络。2011年,AWS的增长率为80%,收入估计将近10亿美元,云 服务的市场前景究竟有多大由此可略见一斑。在最近跟22位CEO的会面中我问他们有多少位在用AWS:每个人都举手。

Microsoft Azure:微软的双刃剑

伟大的桌面颠覆正在发生。桌面应用正一个接一个地被同等的云服务取代,办公、财会,甚至照片编辑均如此。这些服务吸收了诸如跨设备无缝同步(Evernote)、社交、实时协作(Zendesk、Google App)等云的服务器中心架构天生就支持的特性。

对 于桌面的传统霸主微软来说,云平台当然是个威胁。但机遇并存。从目前占统治地位的桌面过渡到新的云前沿,微软也拥有其战略杠杆。微软可帮助早已经熟悉其编 程API和运行环境的桌面开发者扩展其应用。与此类似,让微软的客户群平滑地演进到云服务当中相对也比较容易。所以说,微软携着Azure进入云平台领域虽然有些晚但它有居高临下的优势。微软的问题在于如何挥舞好这把双刃剑才能既可杀敌又能免于自残。

Google的App Engine:失速的平台

在为消费者开发应用即服务方面,Google是早期的领导者,Google的App套件最为人所熟知的。但是Google在为开发者开发自己的云平台App Engine方面的成就则略微逊色。跟操作系统一样,如果能够为应用开发者提供合适的工具,云平台就可以成功。

与微软的Azure和Amazon的Web Service相比,App Engine的开发者工具更高级,更面向Google,约束更多。只支持选定的编程语言,后端存储也存在限制。结果是App Engine在严肃的开发者那里的被接受程度相对较低。

尽管Google在创建自己的云服务方面的能力很强,借助着Android,它在客户端生态系统的存在也不断增强,但如果Google希望在云平台方面与别人一决高下的话,它需要换挡才行。

EMC的优势:数据信任的力量

EMC 是最后一个有竞争力的大玩家。跟Amazon、微软和Google不一样的是,EMC跟消费端IT的距离很遥远,主要面对的是企业。EMC的根本优势是数 据。它的服务器已经为一些全球最大型的企业的宝贵数据提供托管。这是一项很强大的战略优势,因为数据既是云价值的核心,也是其漏洞的来源。

云从本质上来讲属于数据操作系统(如Tim O’Reilly对互联网的评价一样)。云服务的价值在于连接—通过电子邮件、社交媒体、业务仪表盘等与整个星球颤动的数字脉搏相连。

不过数据不断增加的连接性也带来了漏洞风险;消费者隐私被侵犯的事件一定会越来越多。对于企业来说,很难评估数据安全性方面的敏感性。数据的价值独一无二,是不可替代的企业资产,其损失不可以金钱来进行衡量。

EMC已经赢得了要求严苛的企业的信任感,让他们放心将自己的数据交给EMC。EMC可以凭借这种信任发挥优势。相对于Amazon的公有云,EMC提供虚拟私有云(VPC),EMC很聪明,它意识到“公有云”这个词对于企业高管来说跟“公共厕所”和“公共泳池”差不多。

风云变幻,定局待时

云平台的王者之争至关重要,因为这跟其他的平台之争一样,这场决斗的结果也是胜者为王,赢家通吃。一旦某一初创企业将自己的数据放在了Amazon的S3存储服务上,那么它再将自己的Hadoop集群部署在Amazon上也将是一个划算的选择(带宽成本更低),同样,跟其他已经在Amazon上面的服务提供商的协作也会更好。

被锁定在一个平台之中的风险促发了某些云计算方面开源标准的发展,最值得注意的是OpenStack,这个标准已经得到了IBM、Rackspace以及HP的支持。

谁能够成为市场霸主取决于其发展势头、迁移成本、安全以及对开发者的友好程度(API设计、标准、文档)的综合较量。胜出者将会成为下一代原生云应用及服务基础性的操作系统,并将获得丰厚无比的物质回报。

无论谁将胜出,有远见的公司都能够继续享用云操作系统所赋予的存储、计算及网络虚拟化的好处,同时能够将自己的创造力集中在这个技术栈之上的地方。其结果将是应用即服务型初创企业的百花齐放,会对已有的技术公司和整个经济产生颠覆性的影响,好戏才刚刚开始。

完成比完美更重要?请三思而后行

“发 布或死亡”、“完成比完美更重要”、“只需发布”,最近我们发现类似的话成为越来越多团队的箴言。他们赞美达成带来的荣耀,却忽略了完成的目标是为创造价 值,而不是构建一个创业公司。这种“发布(ship)”心态可能带来危险的效果,并让许多创业者做着一些平庸、多余或不重要的工作。

发布至上的优势

为 什么这种达成至上的心态会变得如此普遍?其原因并不是他人的鼓动,而是这样做后带来的体验使然。当自己做的产品已经部署并且确实也有用户在使用时,给团队 带来一种正整体向上发展的感觉。有时候这也只是自己的遐想。当你想快速测试一个想法或小特性时,”达成”的想法或许会有用。不过有时也会得到错误的反馈。

发布至上的风险

这也是问题所在。当你正享受达成的快感时,实际你正面临这样两个风险。

→ 发布错误的东西

如果自己都不认可即将发布的产品或新特性,那就是一个错误的“发布”,结果只能是南辕北辙。就算你因此获得一些反馈,并通过及时纠正让自己学会变得更快,但这些反馈也可能让你误入歧途。

→ 发布一个平庸的产品

“发布至上”最大的问题是:很多时候品质得不到保障。因为发布的产品本身就考虑欠周全,所以即使便获得反馈,也不能反映出自己真实想法中存在的不足。

回归品质

再回归品质来看,实际经验表明,高品质的内容会带来惊人的回报。

从上面这个图形我们可以得知,投入的时间与精力对产品价值的影响。当你投入更多的时间和努力到工作中后,可能获得高达10倍的效益。额外工作回报率非常简单,竞争对手可能很早就已放弃。或许他们解决问题很容易,但使用的都是最基础的方法。

此前,乔布斯曾对品质有如下表述:

首次着手解决某个问题时,第一个解决方案通常是很繁复的,而大多数人也止步于此。但如果你继续尝试,通常会获得一些非常优雅、简单的方案。大多数人只是不愿意在这方面花费时间和精力。我们相信客户是聪明的,他们想要一个考虑周全的产品。

品质不等于行动迟缓

有人认为精力集中于品质,将导致的最大问题就是行动迟缓。实际的经验告诉我们,事实并非如此。很多时候快速发布产品后,一旦出现问题,将花费更多的时间进行弥补。这样实际上花费的时间,比一次做好所花时间更多。也就是我们常说的欲速则不达。

找到平衡点

Facebook提出的“完成比完美更重要”确实是一个不错的箴言。但如果将其作为创业者的普遍行动指导,恐怕不合适。事实上更深层次的东西,我们无法通过一句话获悉。

这就是为什么Dropbox用了18个月时间才最后发布。Evernote花了大约两倍的时间构建出原生客户端应用程序,以取代简单的移动网页版本。另外还有Square的品质困扰;Quora花三个月去找出的唯一有问题的那个页面等等,这些都不是偶然。

更好的产品需要经过深思熟虑,才能最终获得胜利。特别是在生态系统不存在转换成本,并能通过排行榜赢得市场的情况下,更是如此。

Android 4.0:该来的总会来的

业界消息称,华硕、三星、索尼等大量厂商都已经纷纷完成了Android 4.0操作系统的测试和认证工作,很快就会为旗下设备提供全面升级服务。Android 4.0其实早在2011年11月就已发布,但大多数厂商的旧设备均未获得升级,不过随着各家陆续公布升级计划,Android 5.0又很可能在5-6月份就露面,局促的更新换代进程让厂商们忙得颇为焦头烂额。

而且厂商们最优先考虑的肯定是开发新设备,然后才会为旧设备提供升级,所以大多数厂商都无法按照预定计划完成Android 4.0的升级。

华硕第一代Eee Pad Transformer已经享受到了Android 4.0.3,三星则在率先更新了Galaxy S II i9100之后又会陆续升级Galaxy Tab 8.9、Galaxy Tab 10.1、Galaxy Note。

索尼的Tablet P/S系列平板将在四月底获得Android 4.0,带来增强的用户界面和新功能。

除了Android,各家厂商也都在忙于Windows 8系统的相关设备开发,毕竟它会同时支持x86、ARM。

Android 4.0:该来的总会来的

开源者的信仰正在崩塌

GPL 协议的自由软件分享规定对于一些个人和企业是一种约束,这让许多开发者或者公司开始转向使用 Apache 许可协议,Apache 许可协议允许人们使用、修改开源代码,但没有要求使用者必须公开分享自己的源代码。这让许多开源运动支持者开始质疑,开源者的信仰是不是已经改变?

Sleepycat 软件公司 CEO Mike Olson 作为开源运动的先驱,却不是一个开源软件狂热分子。他对开源运动有自己的理解,并作出了与众不同的举动。

早在 90 年代 Linux 还未盛行时,Olson 在建立开源软件 Berkeley DB 资料库过程中作出了不小的贡献。而作为 Sleepycat 软件公司的 CEO,他利用一个类似于 GPL 的协议——对于 Linux 发展非常重要的自由软件协议,将这个资料库成功转入商业化。GPL 协议规定:如果你完善了某个自由软件并将它的代码应用在一个大型软件产品中,那么你需要将你的这个产品再共享给自由软件用户。

在 2009 年,Olson 建立了 Cloudera——第一个利用 Hadoop 牟利的组织,基于谷歌软件基础结构的开源数据运算平台——他用 Apache 许可协议替代了 GPL。Apache 许可证不要求使用者必须将软件再次共享给大众。你可能会觉得这样的许可协议一定会阻碍开源世界的发展,但 Olson 认为存在这样的不一样的声音是很有必要的。

Olson 绝不仅仅是唯一一个支持 Apache 许可协议的。据一组统计数据显示,当今世界上许多开源项目都在从有约束性的许可协议(比如 GPL 协议)阵营中转投宽松的许可协议(如 Apache 许可协议)阵营。并且,许多开源运动关注者——包括长期关注的专家 Matt Asay 和一名来自 RedMonk 对开发有兴趣的研究员 Steven O’Grady,都认为这样的改变将会给开源世界带来更好的发展,会有更多的人加入开源阵营。

“商业化的开源项目都开始向 Apache 许可协议模式转变,而 Olson 只是走在了这些人的前面,”多年来一直轻视 Apache 许可协议重要性的 Asay 指出,“他就是在幕后一直嘲笑我们这些 GPL 支持者的家伙。”

这场运动最大的力量来自于互联网巨头,包括 Facebook 和 Twitter,他们相对于传统的软件公司对开源有着非常不一样的态度。但随着这些巨头以及他们的项目源源不断加入到支持宽松许可协议的阵营中来,产生了 一大批利用开源获利的公司,如 Cloudera,他们避开了 GPL 以及其他的约束性许可协议,很可能吸引商业公司的注意。

相对于 Apache 许可协议,许多公司都担心 GPL 协议,担心 GPL 协议会让他们公布自己的私有代码。但是在 Apache 许可协议内,这种担心就没有必要。对于 Olson 以及其他人来说,这不仅仅是鼓励他们使用开源工具,也是非常有利于 Cloudera 这样的公司从自由软件中赚钱的。

Apache 许可协议受到青睐

Black Duck Software 一直在跟踪调查自由软件许可协议,根据其统计,约束性许可协议例如 GPL 仍然被广泛的使用。但现在,自由软件运动逐渐成熟,而且互联网改变了这场运动的经济状况,Black Duck 的统计表明,GPL 的影响力相对于许多如 Apache 这样的宽松类许可协议来说正在逐渐下降。

有数据显示,GPL 协议的项目使用率正在由 2008 年的 70% 下降到现在的 57%,而 Apache 以及 MIT(另一个宽松型的许可协议)的使用率则分别由5% 上升至 11%。

Free Software Foundation,由 GPL 产生的非营利性公司,它的一名许可协议服从工程师 Brett Smith 认为像 Black Duck 公司的统计数据具有误导性。“他们所得到的数据不是完全公开的,所以通过那些数据计算出来的数字无法表明什么”,他说,“以后 GPL 或者其他许可协议如何发展很难预料的。”但其他人,如 Redmonk 的 Steven O’Grady 和 OuterCurve Foundation 的执行董事 Paula Hunter, 一名微软后端开发开源的提倡者,他们认为他们看的东西发展趋势是和 Black Duck 公司看到的一样。(Black Duck declined to be interviewed for this story).

很明显的,最近几年很多开源项目都姿态鲜明地选择了 Apache 许可协议,包括云计算平台如 Hadoop、OpenStack、Cassandra 以及 CloudFoundry。另外,Node.js 遵循的是 MIT 许可协议。甚至一些著名的移动平台也加入了这一阵营,比如安卓手机操作系统就支持 Apache 许可协议,惠普之前则公布了开源的 Palm’s webOS 平台加入 Apache 许可协议阵营的时间表。

巧合的是,许多项目都是来自大的互联网公司。“相对于我们以前所看到的,他们对待开源有截然不同的态度” Steven O’Grady 说,“他们不再像以前那样重视代码了。这些公司在五六年前将代码视作私有——他们认为这些代码是与众不同的——但现在却公开了。”

包括 Facebook 以及 Yahoo 在内的通过 Hadoop 引导的公司,他们不以出售软件为主要业务。但那只是部分的情况。他们通过利用别人的开源软件建立业务运营,所以他们也愿意把这些再分享给别人而不索取回 报。但同时,他们知道其他人也是这么想的,所以他们知道,无论怎样,回报最终还是会到来的。

O’Grady 认为 Twitter 是另一个很好的例子,他们的项目是开源数据库 FlockDB 以及开发者工具包 Bootstrap。还有 Rackspace,他们的 OpenStack 是一个提供虚拟计算资源的亚马逊互联网服务平台。

这样的趋势不会停止,在开源项目周边,有无数企业如雨后春笋般出现,它们希望帮助世界各地采用该软件,然后从中牟利。Facebook 的数据库 Cassandra 催生了 Texas-based DataStax、Hadoop 催生的不仅仅有 Cloudera 还有雅虎的子公司 Hortonworks、Rackspace 围绕 OpenStack 建立了自己的服务部门,还有 Node.js 的管理员 Joyent,利用开源开发平台售卖软件以及为一些公司做服务。所以说,Mike Olson 并不是个例。

GPL 协议是否是毒药?

在 Olson 还在 Sleepycat 的时候,Berkeley DB 基于 GPL 协议获得了一个“强有力的非营利性版权”。你可以免费使用 Berkeley DB,但是如果你用了,你可能不得不用你自己的代码来付出代价。对于很多公司来说,这是令他们非常不安的一个规定。虽然他们想要用 Berkeley DB,但是他们不想放弃自己多年开发的软件。但 Sleepycat 提供了一个空子让他们来钻,如果你支付给 Olson 和他的公司一些费用,他们就能给你提供不同的许可协议来允许你持有你自己的代码不公开。它被称之为双重许可协议的技巧。

“GPL 协议是一种毒药,而我们能够卖给你解药。如果你不希望你的源代码受到 GPL 协议的影响,你可以购买一份不同的许可协议。”Olson 说,“这对我们来说,相当成功,但是我们永远没法成为一个年收入上亿的公司。我们的商业交易建立在我们的顾客被威胁的基础上:‘除非你给我钱,否则将危及 你的知识产权’,那不是开始商业行为的好方式。”

而在 Apache 许可协议下,Cloudera 完全改变了状态。本质上,你可以以你喜欢的任何形式使用自由代码——不需要贡献任何你自己的代码给其他共享者。而 Cloudera 则利用出售支持以及增加串联 Hadoop 的私有软件进行赚钱。这起到了在自由软件与非自由软件间搭建桥梁的作用!

“开源是我们所做的工作中很重要的一部分。我们有一半的工程师都工作在(Hadoop)开源项目上。”Olson 指出,“但更重要的是,我们需要将自己和其他市场区分开来,这样可以让用户有充分的理由认为我们是特别的。”

有些人称 Cloudera 是一个“开放核心(open core)”公司。项目的核心是开源的,而 Cloudera 所提供的周边的付费软件则不是开源的。但这个名字并不是一开始就有的,“开放核心(open core)”在公众的嘴里是一个否定的词语,因为它在暗指 Cloudera 是伪开源的。

Mike Olson 不关心别人怎么称呼 Cloudera,只要能带来利益,人们怎么称呼它都无所谓。

开源的实用主义

神秘的 Cloudera 很少谈到他们的收入,但他们的客户名单上却不一般,他们拥有包括 Groupon、Rackspace 以及 Samsung 这样的大客户。对于 Olson 来说,Cloudera 的发展已经超出科技领域冲进华尔街和生物医学领域了。这个公司成功最重要的部分,Olson 说,就是 Apache 许可协议。他理解人们为什么喜欢 Free Software Foundation 创始人 Richard Matthew Stallman 创建 GPL 协议,并且,在很多年前,GPL 对 Olson 来说也是好东西,但现在,这是新的时代了。

自由软件运动的精神领袖理查德·马修·斯托曼(Richard Matthew Stallman)

“我不相信政治或者宗教信仰会是稳妥的商业基础,”Olson 讲到,“如果你和早期的一些自由软件者聊天,所有的内容都是关于权利和责任的——我明白为什么 Richard 对那东西感兴趣——但如果你作为一名商人,你必须抓住用户、市场以及商业机会。GPL 确实能够给你赚钱的机会,但是还有其它的合作的许可协议能够让你赚的更多。”

Apache 的优点是很少会带给潜在客户威胁。“如果你希望软件被使用,你需要一个 Apache 许可协议,”国际法律公司 DLA Piper 的合作伙伴 Mark Radcliffe,一个专攻开源软件的家伙如是说,“在与之打交道的过程中,很少会遇到复杂的法律问题。”并且,一旦你有了用户,例如 Olson,那将会有大把的机会赚钱。

当开源他们的源代码时,很少遭遇复杂法律问题是许多互联网公司选择 Apache 许可协议的另一个原因。“将开源发展融入他们基本开源项目战略的原因有很多,发展趋势只重要的一方面,但你们不要担心这种情况会发生在 GPL 那里。”Radcliffe 讲到。换句话说,他们不用担心要开放自己不想开放的那些代码。

最重要的宗旨是基于 Apache 许可证下,开源代码与私有代码自由混合是被允许的。Olson 认为这是未来的发展趋势,而像 Oracle 以及 IBM 这样的巨头已经围绕开源软件项目建立了成功的商业模式。“我想,开源商业化的成功将会更像 IBM 或 Oracle 多一些,像 Red Hat、MySQL 或者 Sleepycat 少一些。”

他认为,即使没有类似 GPL 这样的协议存在,这些公司也将会继续促进开源项目的发展,因为他们现在开始意识到这些项目能带来的利益是可观的。“随着行业的逐渐成熟,我想人们已经认识到相互协作的价值,”他说,“你们不需要去吓唬或者逼迫他们。”

只是,这样的“开源”还是不是开源者最初所信仰的“开源”?(翻译/魏兵)