以非耍流氓的方式讨论C++中的面向对象编程

老规矩,先解释一下标题,我承认有点标题党,其实就是为了抓人眼球,希望通过这种标题吸引你点进来看看。我有自信这是一篇好文章。我曾经写过一篇很有名的博客:《为什么C语言不会过时》 很多的网络媒体都进行了转载。我相信这篇博客的质量应该比《为什么C语言不会过时》还好。因为它会颠覆你对C++面向对象编程的传统的认知。

再详细解释一下什么是非耍流氓。其实C++面向对象编程的问题,很多大牛都讨论过,但是因为人家是大牛,所以文字描述居多,很少给出代码。人家毕竟是阅近天下**, 心中早已无码。我个人认为讨论编程的问题,如果你不给出代码就是在耍流氓。 而一些C++初学者,倒是愿意给出一大堆关于什么dog继承animal的类似的源代码,但是这样讨论面向对象编程有些肤浅,片面,甚至是把人带向了错误的方向。所以我这篇文章首先要基于源代码的方式讨论面向对象,而且介绍现代方式的面向对象编程,你会惊讶的发现,无论是从理念上,还是实现方式上,它都和传统意义的面向对象编程截然不同。

我刚开始接触C++的面向对象的时候,首先接触的例子就是duck是animal, student是一个people等等那一套。这种例子铺天盖地,充斥于各种C++的各种初级书本中。我相信大部分人和我都有一样的记忆。当时感觉这个面向貌似没什么难的。但是当自己写程序的时候,大部分时间都根本没怎么使用到继承。这个是第一个层次。

Class Animal{
   Virtual Speak() = 0;
}
Class Duck: Animal{
   Virtual Speak(){cout<<"quack"<<endl;
}

慢慢地开始接触设计模式,这个就有点难度了。这里没有什么鸭子,学生之类的东西了。基本上都是一些很抽象的东西。比如说,策略是一个基类,命令是一个基类等等。当时看的云山雾罩,不得所以。只是机械的记住几个名字,几个类关系图而已。

再慢慢地,开始意识到一些本质的东西。继承是面向对象编程的核心实现方式,其思想本质是把变化的东西封装到子类中,而客户只针对一个基类的指针来调用:

Class Base{
   Virtual Do{}= 0
};
Class Implement1: Base{
   Do(){do it in this way};
}
Class Implement2: Base{
   Do(){do it in thatway};
}

//Client code
Base* Ibase;
Ibase->do(); //

如果你仔细研究设计模式,尤其是建造模式和行为模式,你会发现它们都符合这个基本的想法。例如工厂方法模式,我们把Do 函数换成Factorymethod方法就OK了,换句话说,我们把对象生成方法的变化下推到子类中,使得这种变化不会影响到客户的代码。再举了例子,策略模式,你把Do函数换成AlgorithmInterface()就可以了。我们把算法的变化封装到不同的子类中。当运行的时候调用不同的子类,客户端的代码也不受影响。顺便说一句,这种思想后面还有两个非常凡尔塞的原则。一个是里氏替换原则:如果S是T的子类型,则类型T的对象可以用类型S的对象替换(即类型T的对象可以用子类型S的任何对象替换(即向上转换))而不更改程序的任何期望属性(正确性,任务执行等)。其实就是我们能用Implement2替换掉IBase, 客户的程序不受影响。另外一个就是反向依赖原则:高层模块不应依赖低层模块,低层模块也不应依赖高层模块,两个都应该依赖抽象层。抽象不应该依赖细节,细节应该依赖抽象。 这个其实更简单, 换成人话就是一旦Do()这个玩意定下来,客户就去调用它,子类就去实现它。换句话说,客户不依赖于子类了,而是客户和子类都依赖于Do()这个玩意。这里很多人把Do()这个玩意也叫做接口,抽象,基类,契约等等。 看明白了吗?其实这两个原则其实说的都是一回事。

如果上面我说的你都明白,那么我恭喜你。你对传统意义的面向对象编程已经基本了解了,而且23个设计模式对你来说就不那么难懂了。

在传统的面向对象编程中,你会发现另外的两个问题,1) 什么应该是基类?2)如果基类需要变化怎么办? 而恰巧正是这两点,直接击中了传统面向对象编程的痛点。让我们一个一个说。 首先什么应该是基类?关于这个问题,C++面向对象里面有一个经典的案例,那就是:椭圆应不应该是圆的基类。我这里只是简单的介绍一下,详细的讨论可以参考后面的参考文献1。 长话短说,欧几里得认为圆形就是椭圆,但是C++里面你却不能这么用,这是因为椭圆有两个圆心,但是圆却只有一个圆心。 没有办法直接使用继承。请看源代码;如果使用了继承,你会发现圆这个类有三个圆心。而且setLongRadius在圆形里面也没什么实际的意义。这明显违背了里氏替换原则。

Class ellipse{
   setLongRadius(float);
   setShortRadius(float);
   pair<float, float> center1;
   pair<float, float> center2;
}

Class circle{
   setRadius(float);
   pair<float, float> center;
}

问题出在哪里,到底是欧几里得错了? 还是Bjarne Stroustrup(C++之父)错了?大部分人的第一反应都是一定是我错了。然后针对这个问题,层出不穷的解决方案就出来了。但是无非就三种方法,让基类变弱,让子类变强。或者再建造一个更抽象的shape基类,然后让ellipse和circle都继承于它。但是无论是哪种方法,都相当的别扭和不自然。

如果如何在开始的时候设计类继承体系已经很成问题了,那么未来的变化更是大麻烦。 这里有另外一个例子。比如我们现在设计了一个面向对象的继承体系如下图:

Class Animal{
   walk();
   speak();
}

现在People和Dog继承于它。 貌似非常地完美,没有任何问题。但是两个月以后,我们需要新加入一个类鲨鱼。问题就来了。鱼能继承于Animal吗?不能,因为调用Shark.walk()的方法没有办法解释。但是鱼明明就是一种Animal吗?没关系,面向对象是不会错的,C++语言也是没错的,只是我分类的方法不对。我应该再加入一个哺乳动物类,鱼类,然后把swim这个行为放到鱼这个类中。看着不错,虽然改变类体系这种事情会对现有的系统带来巨大的冲击和影响(几乎所有过去的代码需要改写和重新编译。)但是你相信现在的分类是完美的。但是两个月以后,鲸鱼来了。没关系,我们可以使用双继承去解决这个问题。(一旦使用了双继承,基本上就是代表你的设计需要一个补丁了。)鲸鱼同时继承哺乳动物类和鱼类,也算暂时过关了。但是两个月以后,鸭子来了。这个时候,我估计你快疯了。这个能walk, speak 和swim的东西到底应该放到哪里呢?真没想到,你会被一个鸭子伤害到。 其实你不是个例。 Linux之父炮轰过C++,指出任何现在看起来伟大,光明正确的类设计,两年以后都会有各种问题,那个时候修复起来成本很大,因为整个的系统实现都是基于现有的类设计架构,非常难于修改。同时继承关系是一种很强的耦合关系,这种强耦合使得整个程序变成了一大坨,牵一发而动全身。通过我们上面这个类似玩具的例子,我希望你能对这句话有所体会。

传统面向对象编程(我是指那种上来就去设计复杂的类的分类和继承体系)的问题在于把人类日常科学的分类方法照搬到编程领域,这是不对的。再详细点说,is-a关系并不能完美地适用于编程领域,is-substituable(可替代关系)更适合。在Python语言中,这种思想得到了贯彻和体现。Python语言中有著名的(duck type)鸭子类型。只要你叫起来象鸭子,走路像鸭子,那么你就可以替代鸭子。 至于你本身是什么,我不care。

我们再论is-substituable(可替代关系),现代面向对象的本质到底是什么呢?是构造一个分类准确,漂亮并且复杂的分类体系吗?当然不是!编程的最终目的就是完成一个任务,这个任务需要多个对象配合,换句话说就是各个对象能够彼此发送消息(A对象给B对象发消息就是A对象调用B对象的某个方法。)现代面向对象编程的本质就是轻对象分类体系(少用继承),而重视可替代关系的表达和实现。有了这种可替代关系的加持,对象之间就能更好的彼此合作(对象间调用彼此方法),从而更好地完成一个任务。

一旦我们把is-a关系换成is-substituable(可替换关系),我们就完成了面向对象编程部分的思想改造,但是不要忘记,C++是一门静态语言,不是动态语言。就语言技术本身,我们还不能像Python语言那样直接地支持动态数据类型(鸭子类型)。 但是感谢C++的generic编程。这给现代C++语言打开了一扇新的大门,使得我们不用使用继承也能支持泛型和多态(编译期间的)。不仅如此,现代的C++语言对泛型的支持更安全,对多态的支持更高效。可以这么说,C++11以后,尤其是C++/14/17/20的不断进步和演化,为我们的现代面向对象编程提供了强大的语言技术支持,使得我们完成了现代面向对象编程部分的工具改造


思想和工具都改造完成,我们该上码了。这里我们对一个形状求面积。 我会首先给出基于传统面向对象编程的实现,然后再给出现代面向对象编程的实现。

首先是设计一个形状的基类, 然后派生出三角形, 椭圆,圆形等等。

Class Shape{
   virtual getArea() = 0;
}
Class Rectangle: public Shape{
   setLength(float);
   setHight(float);
   virtual getArea(){return length* hight;}
}
Class Triangle: publich Shape{
   ....
}
float getShapeArea(Shape* ptr_shape){
   return ptr_shape->getArea();
}

代码相当标准和传统,这里不过多解释。请注意,使用传统的面向对象编程,你就会遇到上面我们讨论的所有的面向对象的问题。圆形是不是椭圆?点是不是个形状等等。

好吧,现在我们用现代面向对象编程的思路去解决这个问题。看下面的代码:

Template<typename T>
float getTArea(T t){
   return t.getArea();
}

请注意,并不是使用模板了就实现现代化了,而是这段代码背后的思想。现在我们已经不需要T一定是Shape类型了。相反地,只要在计算面积这个事情上,某个类型T有可替换性,那么它就可以上。 注意到没,我们再也不需要关注椭圆是不是圆形这个问题了。而且以后就算是出现了3D的对象,只要它在计算面积这个事情上有可替换性,那么我们的代码就不需要更改,你只需要不断的加入新类就行了。就这样。

比起Shape基类,T也是类型安全的。因为在C++编译并且实例化这个函数模板的时候,编译器会检查实例化的T类型支不支持getArea()这个函数。在C++20中,我们更是引入了concept的概念。使得对类型的静态检查变得更加简单和方便。

template <typename T>
concept supportArea = requires(T v)
{
    v.getArea()
};
Template<supportArea T> //supportArea is concept
float getTArea(T t){
   return t.getArea();
}

不仅不再需要纠结圆形是不是椭圆这个问题了,我们还可以在可替换性上再前进一步。你想得到面积,我给你面积就得了,至于我怎么算的,你不用担心。并不是我有getArea()函数才有可替代性,而是我把面积返还给你我就有可替代性。假设有个圆形,我们想求面积,但是目前圆形中还没有实现getArea()方法,没关系,我们可以用一个矩形替换圆形来计算面积。这个理解起来很简单。我们可以把一个圆形转换成一个矩形,参考下图

有了现代C++语言特性,实现起来也不难。参看源代码:

Template<Typename T> 
float getTArea(T t){
  if constexpr(Is_Circle<T>::value){
     auto getArea = [](auto& t){
           Rectangle r;
           r.setLengh(pi*t.Radius);
           r.setHigth(t.Radius);
           return r.getArea();
          };
      return getArea();      
  }
  else{
     return t.getArea();
  }  
}

这段代码用到了很多的C++14/17的特性,比如if constexpr,lambda和 type_trait等等。限于篇幅我不多解释了。这里简单介绍一下思想:首先构建一个Is_Circle<T> type_trait类。然后判断T是否是一个圆形,如果是,那么构建一个矩形,然后把矩形的长设置为pi*r,把高度设置为r。然后利用矩形的求面积公式计算圆形的面积。如果不是圆形,那就直接调用类型T自己的getArea。

也就是说,在求面积这件事上,只要在语义层面上满足可替换性,那么一个圆甚至可以被一个矩形所替代,而且我们也彻底摆脱了有没有getArea()函数这种语法层次的的限制。怎么样,够酷吗?

如果这个例子你读懂了,一次类似的例子还可以阅读参考文献2《Linux多线程服务端编程》。第一章里面有一个使用variadic template实现Observer设计模式的例子。书中的一个观点令我印象很深:设计复杂的分类和继承体系就是“叠床架屋”,不如直接基于对象编程(不使用继承,直接使用各种对象彼此协作),这样才能“拳拳到肉”。我深以为然。

这里必须要再提一句,可替代性的度量不仅依赖于某个函数本身的语法和语义限制,它还依赖于调用这个函数的前后条件的限制。也就是pre-condition和post-condition也要同时满足客户的要求。这么说有点抽象,再举个例子,现在有两个鸭子类,调用一个鸭子类的speak方法,它返回“quack”,调用另外一个鸭子类的speak方法,它返回“姐,好久没来了!”。单从speak方法本身,语法和语义都貌似符合可替代性。但是很明显,客户对调用speak方法后的post-codition的期待是完全不一样的。所以在这种情况下,貌似完美的可替代性是不成立的。

最后总结一下:

1)现代的面向对象设计并不是基于Is-a关系设计复杂的类继承体系。而是基于可替代关系构建一个对象间能够高效彼此协作的系统。

2) 可替代关系的理解是基于语义的,而不是基于语法的。例如有的时候语法上不可替代,但是语义上可替代就行。例如圆形即使没有getArea函数,它也可以被矩形替代(在计算面积的时候)。而有的时候,即使语法上完全可以替代,但是postcondition不满足,也不满足替代关系。例如两种不同的鸭子。

3)利用现代C++的语言特性,尤其是基于generic programming语言特性实现上面介绍的基于可替代关系的设计意图。

4)现代面向对象设计并不完全排斥对象,那种没有附加成本的基于数据抽象的对象还是推荐的。例如把一个点对象和数值对象抽象成一个圆形是推荐的。这种对象是更方便我们编程,而且没有任何附加成本。我们反对的是设计复杂的类分类和继承体系。

参考文献:

  1. Inheritance — Proper Inheritance and Substitutability, C++ FAQ (isocpp.org)
  2. Linux多线程服务端编程 (豆瓣) (douban.com)

C++中的未定义和depends

先立个flag, 从此以后有三不聊:不聊政治,不聊宗教,不聊教育孩子。这三个主题有个共同的特点:大家都有自己的观点,大家都认为自己的观点对,但是其实谁的观点还都没啥用!而且这些话题插嘴的门槛还特别底, 这就是为啥容易吵吵起来的原因了。

所以我决定聊点C++语言,不好意思,这个热心网友想插嘴还不太容易。 今天主要聊聊我最近发现的C++的一个现象:那就是模板看起来很难, 其实不难; 异常看起来很简单,其实很难;多线程看起来很难,其实TMD更难。

首先聊模板,talk is cheap,show me the code。下面这段眼花缭乱的代码要是读懂还真不容易,不过他难就难在了语法上,其实就是利用编译期间,模板实例化的时候的SFINAE特点,来判定一个类是否有serialize这个函数。就这么简单。我知道你想骂人了,好端端的你就直接说中文就好,为啥总拽洋文呢? 因为这个词一旦翻译成中文,你要是看到了就想打人了。 这个单词就是“替换失败不是错误”这几个词的首字母缩写。不知道你怎么想,我个人觉得,同样是不懂,但是英文说出来更牛逼一点,是不?


template struct hasSerialize{
typedef char yes[1];
typedef yes no[2];
template struct reallyHas;
template static yes& test(reallyHas* /unused/) { }
template static no& test(…) { /* dark matter */ }
static const bool value = sizeof(test(0)) == sizeof(yes);
};

std::cout << hasSerialize::value << std::endl;

至于语法上花了呼哨的,其实就是一个习惯的问题,一旦你习惯了这种语法,你再看hello world啥的还觉得别扭呢! 好了总结一下,模板编程大部分都是语法上叠屋架床的,其实语义层面相当直接和简单。抓住了语义,那就是一拳到肉。至于语法开始麻烦点,大不了面向stack overflow编程就好。对了提醒一下,一般stack overflow上分为提问区和解答区,抄代码的时候一定要从解答区copy! 看看,这就是一拳到肉!

聊完了模板,现在聊聊异常,介绍C++异常的时候通常都会提到一个小函数,这个函数就是assert。这个函数简单的不得了,就是输入一个判断bool的表达式,如果表达式为false, 那么assert就终止整个程序。好吧,上代码:
assert(2+2==4);

就这么简单吗?对,就这么简单。那么下一个问题就来了,判断表达式判断啥?,assert该在哪里用呢?嗯~~, 这个depends。

我知道你又想打人了,刚刚明白了上面比较复杂的语法,现在好不容易遇到一个简单的,就是想知道在哪里用,你还说depends。这种感觉就像你把500块钱刚给了大街上一位向你招手的女孩,女孩收下钱后马上对你说你是一个好人。这就是一种想打人的感觉。 好吧,我换一种说法,assert就是用来验证是否违背了(invariant)不变性的时候用的。你现在感觉好点了吗? OK,那么什么又是(invariant)不变性呢? 举个例子吧,你今年20,再过一年你21,你遵守了(invariant)不变性,如果你今年20,过一年你还是20,那么你破坏了(invariant)不变性。来吧,打我啊!

没错,你没看错,(invariant)不变性其实是一个更大的概念,它就是指程序在语义上面是正确的,过了一年长一岁,这个在逻辑语义上是对的。 或者在一个更大的范围内是正确并且稳定的。例如,王健林为了让你完成一个小目标,给你转了一个亿。 在王健林的钱离开他的账号,但是还没到你的账号的时候,你们之间的(invariant)不变性就被打破了,直到钱转到你的账号上,你们之间的(invariant)不变性就又恢复了。不变性并不是指的“不变性”。验证这种(invariant)不变性其实是对编程以外的领域知识要非常了解的。例如:对一个人员管理系统,你应该用下面的assert语句。问题是,你真的确定100是正确的吗?你对全世界人口的极大值了解吗?
assert(age>0)
assert(age<100),

哪里会破坏(invariant)不变性呢,这个在多线程里面最多了。例如如果我们有两个线程分别计算你的岁数,每个线程把你的年龄加上0.5,这个时候如果同步的不对,就会发生data race。例如一个线程读入20,加上0.5后,还没写入到内存的时候,另外一个线程切入,读入20,加上0.5后,写回到内存。这个时候第一个线程再次回归,把自己计算得到的20.5再写一遍内存。最后两个线程结束后,内存里保存的就是20.5,而不是21了。这个时候你已经违背了(invariant)不变性了。

除了容易发生违背了(invariant)不变性以外,多线程另外一个问题就是语法和语义都很难,大量的模板推高了使用线程的语法的难度。语义上,哪怕一个简单的锁,什么时候用?哪里用?用不用?通通depends。 原子编程的内存模型又把语义方面的知识推到了CPU的instrument那个层次上去了。

上面说到多线程非常容易破坏数据的(invariant)不变性,首先这种破坏通常很难调试,因为他们并不是每次复现。另外破坏数据的(invariant)不变性还有一个更糟糕的结果,那就是程序的行为“未定义”。

C++新手看到“未定义”这一个词通常会有一种乐观的情绪。他们刚被“面向对象”糊弄到编程这个领域,既然是“未定义”,那么也许是“洞房花烛夜”也保不齐。一般C++老手会告诫新手,未定义通常是“洞房花烛夜-不举”。而10年以上的老鸟会说:未定义通常是“洞房花烛夜-新娘不举”。20年以上的骨灰这个时候一般是不说话的,他们一般45度斜头上望,眼睛里留下两行浊泪,嘴里嘟囔着:“那一年,我洞房花烛夜-新娘很举”。

希望你们能通过这个故事记住“未定义”这个词。对这个词到底有多坏一定要有充足的想象力。说简单点,标准委员会也不知道某些行为会坏到什么程度,那就叫做“未定义”吧。就像一个人长的丑得他妈都不愿意看,那么这种丑就叫做“未定义”。

总结一下:
1)模板在唬人的语法下面其实挺简单的,理解了它到底要干什么,那就一拳到肉了。
2)遇到depends的时候,通常意味着你需要更深入的领域知识了。
3)能不用多线程就不用,必须要用就用成熟的线程库和模型,例如boost线程池库,生产者-消费者模型,消息传递模型等。
4)如果你对“未定义”这个词感触不深,那么说明你还没到“那一年…”

我的女朋友漏电了–论C++中的失败(failure),缺陷(bug)和异常(exception)

—–赵岩

先做个广告置入,如果喜欢这篇文章,你可以到 zhaoyan.website/blog 去查看于此类似的C/C++文章。

我承认有点标题党了,不过这真的是一篇写软件的文章,所以如果你已经抽出了一张面巾纸,那么趁早再把它完美的放回去。这篇软件文章很软,源代码不多,而且大部分都是伪代码。所以很适合所有人看。我特别推荐年轻的初学者,把纸巾放回去后,继续看下去。如果把这几个概念理清楚,对未来的工作非常有帮助。

先说失败(failure)。常见的软件的失败主要分为三种,编译失败,运行失败,结果失败。下面通过一个程序的例子来说明:
—————————————
int l1 = 0;
ll= 23; //compile fail
int a[10];
a[10] = l1; //run time fail
if(l1=1){
… // result fail.
}
——————————————
你可以把编译失败理解成世界上最聪明的一堆程序员在帮你审核你的代码。如果审核失败,他们会提醒你在造成严重错误之前修改你的问题。单从这一点看,C++也比一些解释性的语言更加安全。例如源码中你想把变量l1修改一下。但是由于笔误写成了ll。C++的编译器会对你大吼的。但是Python一声也不吭,看你运行时候的笑话。

运行失败是指你已经有了可执行程序,当你运行它的时候,它crash了,挂了,死了,kick the bucket了。一个你最经常看见的输出就是访问越界内存带来的Segmentation Fault。所以请记住:男人头,女人腰,还有越界内存。这三样东西永远不要摸,否则会挂掉。

比起结果失败,你会发现运行失败是多么美好的一件事。上面的if语句是C++中非常著名的一个结果失败的例子。你的程序编译正常,运行正常,就是结果不对。而且你还不知道原因所在。另外一个常见的原因就是C++的数值计算溢出。这在C++中是非常著名的“未定义”。 C++标准委员会乐观的认为如果我“未定义”,那么天才的编译器程序员会发明一种最聪明的方法来解决这个问题。但是实际的情况是:天才的编译器程序员是最懒惰的程序员!如果你“未定义”,那么他们就什么也不干。结果就是到底发生什么,鬼知道!!!

如果你还没看懂,没关系!我再给你举了生活的例子。一个程序员根本就没有女朋友。这个就类似于编译时失败。一个程序员有个女朋友,但是他第一天就去摸人家女孩子的腰。有些地方还真是和尚摸得,你却摸不得,结果女朋友愤然离去,这可以对应运行时错误。最后一种情况当然就是结果错误了。程序员有了一个漂亮的女朋友,第一天他去摸人家的腰,女孩子不仅没有生气,还娇羞的把头靠在程序员的肩旁上,对他说:“讨厌了,你刚才把人家摸怀孕了啦!”

现在的问题就是:如何能尽早的发现女朋友是否环孕?不对,我说错了,我是想说如何能够更早的发现失败?

首先,常见的失败的原因主要有两种,一种是缺陷(bug),另外一种是异常(exception, error)。好多人搞不清楚,所以我上段代码:
———————————
String key = name;
If(key = “Yan”){
handle = openFile(key);
salary = handle.read();
}
———————————
请注意,我说的缺陷就是bug的含义。也就是常说的虫子。缺陷主要有两种,设计缺陷和实现缺陷。上面的代码中,用名字来做为查找键值就是一种明显的设计缺陷。如果你在办公室喊一声“狗蛋”,会有好多同事答应的。另外一种实现缺陷我们上面介绍过,key==“Yan”写成了“key=Yan”。这些都会造成程序的失败。
那么我们如何能尽早的发现bug呢?一个很牛逼的工具就是unit test。不过C++也提供另外一个有力的工具,那就是assert。Assert用法是最简单的。下面我们看看它是如何发现我们的缺陷的。
—————————————
PrintSalary(string name, int salary){
Assert(name == “Yan” && salary<=200,000);
}
————————————-

Assert的基本理念可以应对成一句话:“反常必有妖”。上面这段代码的含义是如果发现Yan赚的钱超过了20万,那程序一定是哪里出现问题了。就像你发现你的名字出现在福布斯富豪榜上了,那富豪榜一定是印错了。你最好复查一下计算薪水的代码。如果你再阅读一下上面计算salary的代码,你就会发现是重名的设计缺陷和if的实现缺陷才让Yan的salary那么多。

关于assert,有三点需要说明,第一就是在哪里放assert和用assert验证什么? 一个基本的原则就是函数的各种输入和输出值,但是具体上则完全是根据具体的问题和程序员的经验。你可以把assert想象成传感器。要检验一辆车的质量,一个有经验的工程师知道哪里需要放传感器,用传感器检测什么。而工程师的女朋友会发现:这个颜色我不喜欢!

关于assert 的第二点就是在程序发布版中,assert是失效的。这个很好理解。一个车出厂前才需要用各种传感器来发现各种缺陷,一但发现缺陷,就应该找到缺陷的原因并修正它,直到零缺陷,你才应该把车出厂去买才对。如果用户去买车,发现车上都是传感器。这会影响用户的速度和体验;同时就算是用户发现车漏油这个问题,你让普通用户做什么呢?所以说assert只是工程师用于产品出厂前检测缺陷的,而不是用于最终产品的。当然,零缺陷只是传说,所以车厂有召回,软件公司有补丁。

Assert的第三点是现在c++支持static _assert了。它可以在编译的时候就发现问题,这印证了我前面说过的,编译失败要好于运行失败。

———————————–Static_assert(sizeof(int)>2)
————————————–

现在我们回来看看计算薪水的代码,看看OpenFile这一段,这里并不是缺陷,而是会发生异常。文件打不开有太多原因,权限不够,名字不对,别人给删掉了。所有这些原因都不是我们的程序能完全控制的。虽然这造成了程序的失败,但是这既不是设计缺陷,也不是实现缺陷,而是一种异常。

对于异常,我们应该用try catch捕捉到这种异常,然后根据上下文做出正确的反应。常见的异常多发生在IO,网络链接和用户交互上。它和assert一样,如果要用好很大程度上取决于具体的问题和程序员的经验。与assert不同的是,它也必须存在于产品的发布版本中。在车的例子中, 车漏油是缺陷,但是车胎扎了就是一种异常。在平时使用的时候,你也应该时刻准备着捕捉到这种异常,并做出正确的反应。这是它和缺陷最大的不一样的地方。
—————————————–
Assert(car doesn’t leak);

Try{
Drive on a nail;
}
Catch(“flat tire”){
Replace spare tire;
}
——————————————–
没看懂吗?没关系。让我们继续程序员和女朋友的故事吧。我们上文提到的程序员自从把女朋友摸环孕后有点怕怕了。他决定从日本买一个型号为“苍井-玛丽亚”的女朋友。拿到货后很快发现女朋友不仅漏气,而且漏电!漏气这个事,明显属于异常范围的。因为即使正常的使用,漏气也会正常地发生。所以一般厂家会提供一些胶水来应对这种情况。

但是漏电这明显是一个bug,厂家是应该用传感器在出厂前就发现并修正这一问题的。也就是说厂家是不应该把这一产品出厂来买的。你需要向厂家报告这一缺陷(bug)。正常的情况下,你会收到厂家的教科书式的回复:“It’s not a bug, It’s a feature!”。我还真的承认。女朋友漏电这个事,人家说是feature(特性),Make sense(没毛病)!

异常是一个很大的topic,感兴趣的可以查看我的两本书《C语言点滴》《Drop of knowledge of C++》 。里面有很多的关于异常的介绍。

从程序员和女朋友的故事中,我希望你能记住以下几点:
1)编译失败好于运行失败,运行失败好于结果失败。
2)失败通常由缺陷和异常造成。
3)对于缺陷(bug),用assert和单元测试在开发期间尽早发现并修正。
4)对于异常(exception),用try catch 在产品使用的全程捕捉并处理。
5)程序员就不应该找女朋友!

C++模版从精通到精神分裂

by – 赵岩

先做个广告置入,如果喜欢这篇文章,你可以到zhaoyan.website/blog 去查看于此类似的C/C++文章。
这是一篇写软件的文章,但是很硬,提前预警一下,女生不要看!

所有写C++的文章,如果没有源代码都是在耍流氓。闲话不说, May the source be with you!

https://gist.github.com/zhaoyan/5d39a57e64f2929a95704c6253d35530

这是一个教科书般经典的例子。介绍C++的继承和多态。 这里唯一需要重点强调的是:对函数LetAnimalTalk和vector<Animal*> va 来说,我们可以想象他们是客户。通过继承把变化封装到基类的后面,这样使用基类接口的客户就不需要改动! 对客户来说,无论基类后面怎么变化,你都影响不到我。例如,如果现在有一个经理狗加入了项目团队,你的LetAnimalTalk函数是不需要任何改变的。

So far so good! 现在看看引入模版后,发生了什么?

https://gist.github.com/zhaoyan/c5acca04eda622984f2288c06eb7b0a2

基本的应用场景是这样的。对于animal, 你可以用字符串来表示他的ID, 如果你想developer是不应该享有字符串名字的,那么你也可以用整型数来表示他的ID。上面整个的程序,如果你把main中换成下面的样子,除了猫会有点意见,其它一切都没有问题!

https://gist.github.com/zhaoyan/d728d68b5b976d053fa47a310f076768

上面模版继承的一个最明显的弊端是语法变得更加臃肿和复杂了。例如,你不可能在子类中直接引用基类的变量了。如果你引用他,必须使用Animal<T>::id_这样的语法。背后的原因是当编译器编译Cat的时候,Animal是根本不存在的!具体的细节你可以看后面的参考文献1。

另外的一个问题就是虚函数的效率问题。这种多态是发生在程序运行的时候,主要通过虚函数表进行调用分发。所以有一定的效率损失。下面我们再看另外一个例子。 由于猫不喜欢整型ID的名字,所以我们这里完全去掉这个feature,重点关注如何利用模版实现多态。
https://gist.github.com/zhaoyan/c429cb015c556ad23a103a2068540fa6

这段代码中,有几点注意一下:
1) 多态已经不需要用指针了,我们可以用引用来支持多态。

2)在函数LetAnimalTalk中,pa.talk()到底调用那一个 talkImplement是在编译的时候就决定了。这是一种静态多态技术。所以没有效率的损失。

3)但是函数LetAnimalTalk现在必须是模版函数,同时我们也失去了用vector同时保存Cat 和Developer的能力。这是效率提升带来的灵活性的损失!

4)这个是CRTP模式,更多介绍看参考文献2。翻译过来就是“好奇地不断追问自己!”这应该是一种精神分裂的明显的初期症状了。

目前为止,我们介绍了三个例子。还都遵循着一个基本的IS-A的逻辑关系。也就是说,Cat是一个Animal,Developer也是一个Animal。下面介绍三个IMPLEMENT-BY的逻辑关系。第一个例子完全没有使用继承。

https://gist.github.com/zhaoyan/5d6f49d35aada5ff851e79244a5cd6e1

1) 这段程序中,通过模版参数,在编译的时候就把不同的talk行为的实现方式传递给Animal类。这个方法在STL中运用的相当广泛。具体的例子像STL中的map类 template<
class Key,
    class T,
    class Compare = std::less,
    class Allocator = std::allocator<std::pair >
> class map
其中, std::less就类似于我们上面的SayMiao。

2)由于导入的是某种行为,所有再叫做Cat就不合适了,所以这里把类的名字叫做SayMiao

为了实现IMPLEMENT-BY关系,我们也可以使用私有继承:

https://gist.github.com/zhaoyan/c7c02387f4b0867991ad837ae4fb82e7

1) 有没有被
template< typename T>
class Animal: private T{
这样的语法惊到!没关系,我们慢慢来。首先私有继承不是IS-A的关系。而是IMPLEMENT-BY的关系。关于什么时候使用私有继承,什么时候使用组合(composition)
。请看参考文献3。

2)这种方式是Parameterised inheritance, 也是一种常见的设计模式,请看参考文献4

OK,最后的问题是,既然私有继承可以,共有继承行不行?在一个分裂的病人眼中,没啥是不行的!

https://gist.github.com/zhaoyan/07a8c0eb8dadd6eb1e3c7372ef2629a8

1) 这就是在Modern C++ design中提到的Policy-Based design。一个小提示是:现在在Animal中已经不需要talk这个函数了

上面我一共给出了六个程序。到 https://www.onlinegdb.com/ 把这六段代码拷贝进去,根据自己的理解和问题修改一下。“纸上得来终觉浅,绝知此事要运行”。 这其实是陆游给广大程序猿的一句忠告。 如果你有足够的耐心,你可以慢慢地深入的体会,这里好玩的东西还挺多的。由于篇章关系(主要是再展开我也不会了!)我就不多说了。

以上这六段程序,分别代表着六种不同的语法方式,表达出两种最基本的设计模式 IS-A还是IMPLEMENT-BY。首先,没有什么优劣之分,在不同的场景下,各有优缺点。另外,C++的模版完全不同于传统的C++编程。他的语法和想表达的语义有非常明显的分裂趋势,非常容易把传统的C++程序猿也搞分裂了。正所谓范型是C++最大的坑,但是不跳此坑,不足以谈人生!

如果没有看懂就算了!你完全可以说:“这个人已经疯了!” 这个我在标题中已经承认了!

参考文献

1)https://eli.thegreenplace.net/2012/02/06/dependent-name-lookup-for-c-templates

2)https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

3)https://isocpp.org/wiki/faq/private-inheritance#priv-inherit-vs-compos

4)https://blog.feabhas.com/2014/06/template-inheritance/

C/C++语言的五个层次

今天我们聊聊C/C++语言的五个层次。 这已经是C语言系列文章的第四个了,前面的三个文章分别为:

为什么C语言不会过时?
什么教材适合零基础的C语言学习者?
为什么C语言很难?

首先是第一个层次。在这个层次你知道基本的语法,能写个hello world,给女朋友打印个心形,读写个文件啥的。要是经历过正统的本科计算机教育,还可以构造个链表,整个vector和map 往里赛个整数,字符串啥的!总之,大部分人这个时候基本上就以为C/C++语言也就这样了,没啥可学了呢。在自己的简历中,非常自信地写上了“精通C/C++语言”。

然后,你开始参加工作了,开始做一个实际点的C++项目。这种项目无论大小,只要是实际上还有点用处的,你一定是踩过if(a=1)这种坑,一定吃过3/4等于0这种亏。你也一定经历过程序昨天还好好的,在老板面前demo的时候就 segmentation fault了。同时,你开始有了疑问,啥时候需要用static,啥时候需要用const。什么时候使用模版的特化和偏特化 ?什么时候使用函数指针?声明和定义的区别?编译单元又是什么东西?你有点对自己的C++语言水平不自信了。你开始对数组和指针小心翼翼了。你突然发现stack overflow原来是这么有用的一个网站。 恭喜你,你现在开始进入第二个层次了。

经过如干时间,你终于搞清楚了上面这些问题的答案。现在你也基本可以编写一个实际的 C++ 的小型项目了。这个时候你最大的困扰是为啥你的C++语言总是用的像C语言一样。现在你的年龄也有点大了,你妈妈不时催促你找个对象,你也开始认真思考什么是面向对象。 假设你现在有这样一个场景:一个人,一颗心,一个牙刷,这个人的媳妇,老妈和朋友,你如何设计一个类了?如果你写出这样的类定义,恭喜你,你开始进入到第三个层次了。这个时候你开始充分利用面向对象的语言来熟练的描述现实中的各种关系了。同时以前你怎么都读不懂《设计模式》现在能读出点滋味了。

class Person{
    Heart heart;
    Unique_prt<Brush> brush;
    Person* wife;
    Person& mother;
    LendMonday(Person* friend); //this is a function
}

好了,我们再接再厉。在第四层中,你开始实现一个编译器了。你开始了解g++中各种优化开关背后的含义,你开始查看编译器产生的Intermediate represent语言, 这个时候,以前困惑你的char+int为什么要把char先变成int这个问题,终于不再困惑你了。你开始明白为什么需要this指针了。你知道了大头和小头。你进入第四层了。 这里需要澄清一下,这个层次并不是说自己了解编译器,而是自己参与实现编译器。目前C++主流的编译器就三个,g++, Clang, VC++。 如果你再对编译器的要求苛刻点,可以把VC++去掉。那么问题是,你是这些编译器的核心开发人员吗?如果不是,对不起,你还没有进入第四层呢。

都开始玩编译器了,才进入第四层,那第五层到底是个啥?第五层是制定C++语言的标准了。这个层次大约有100多个人。不定期开个会制定新的C++标准。这里是一张会议照片。

我强烈怀疑这些人能不能写出来一个没错任何错误的hello world。因为你从标准里根本读不到“CPU”,“内存”,“简单”,“喜欢”这些词。只有什么“右值引用”,“浅拷贝”,“模版元”,“类型推导”这类的词语。我曾经误打误闯地搞了一年多的编译器,所以必须硬着头皮,读过几页的C++标准,我可以用我的性别担保,它们真的很难读,不服来战下面这段:
Unless otherwise indicated (8.2.2), a prvalue shall always have complete type or the void type. A glvalue shall not have type cv void. [ Note: A glvalue may have complete or incomplete non-void type. Class and array prvalues can have cv-qualified types; other prvalues always have cv-unqualified types. See Clause 8. — end note ]

总结一下:整个的过程是这样,标准对编译器说,你听我的。编译器对C++语言说,你听我的。C++对实际问题说,你听我的。实际问题对程序员说,你听我的。作为程序员,你有两个选择。你可以说,我都精通C++语言了,我谁也不听。那你就可能一辈子停留在精通这个阶段。如果你选择,我听!那么好,你仔细听,你会慢慢地开始听见C++语言怎么说,编译器怎么说,标准怎么说。

真的别再说自己精通C++语言了,上面照片里那一百多人都不敢说他们精通C++语言,这是因为他们知道C++有多大,有多复杂!写完这篇文章,我信心满满地说:我了解C++语言,并且掌握一些。你呢?

为什么C语言不会过时?

赵岩 http://zhaoyan.website

这是C语言系列博客的第3篇,如果对前2篇感兴趣,可以点击下面的链接:
什么教材适合零基础的C语言学习者?
为什么C语言很难?

评价任何一门编程语言,都是招人骂的。 永远是这样。就像是春寒料峭的季节, 街上穿棉袄和穿单衣的擦肩而过,双方一定是同时在心里出现了两个字:“傻逼!”这个在心理学上有个专业的名字:叫做“二逼”现象!

那我为啥还要做这个挨骂的事呢?作为《C语言点滴》《drop of knowledge of C++》书籍的作者,《C语言新思维,第二版》的译者。我觉得我有责任系统的介绍一下这本语言,他的特点,还有他的未来。这个问题对很多刚刚踏入程序猿这个行业的新手至关重要。因为他们有深深的担忧,万一C语言就像Fortran,perl语言那样过时了怎么办?

先上一个表,这个就是著名的TIOBE语言排行榜。目前它是一个最权威的一个语言流行度的排行榜,从这个排行榜上看,你会得到一个最直观的结论。Java和C都在下降,而下降的部分被第三名以后的语言所瓜分。

对所有的编程语言,他们的最后的目的其实就是两种:提高硬件的运行效率和提高程序员的开发效率。遗憾的是,这两点是不可能并存的!你只能选一样。在提高硬件的运行效率这一方面,C语言没有竞争者!举个简单的例子,实现一个列表,C语言用数组int a[3],经过编译以后变成了(基地址+偏移量)的方式。对于计算机来说,没有运算比加法更快,没有任何一种方法比(基地址+偏移量)的存取方法更快。C语言已经把硬件的运行效率压缩到了极致。这种设计思想带来的问题就是易用性和安全性的缺失。例如,你不能在数组中混合保存不同的类型,否则编译器没有办法计算正确的偏移量。同时C语言对于错误的偏移量也不闻不问,这就是C语言中臭名昭著的越界问题。C语言自诩的“相信程序员”都是漂亮的说辞,它的唯一目的就是快,要么飞速的运行,要么飞速的崩溃。C语言只关心程序飞的高不高,不关心程序猿飞的累不累。就是这样!

现在来看看那些非C的语言,他们的长处都在于提高程序员的开发效率上。或者支持动态的列表,或者支持安全的列表。但是加入任何的中间层,加入任何的安全检验,它不可能比(基地址+偏移量+无检验)的方式更快。这个世界上不存在“开发容易,运行快”的语言,开发容易毕竟来源于对底层的一层一层又一层的包装。就像是这个世界上根本就不存在“我就看看,什么也不买”,不存在“我就抱抱,什么也不干”。

现在回答两个最普遍的问题:硬件这么便宜了,有必要让软件更快吗?有这种疑问的人大部分都是网吧的固定客户,他们理解的计算机只在电脑城,他们理解的计算只是游戏和播放硬盘中的小电影。不要玩个游戏开个挂就乐得不行不行的,别忘了还有全实景仿真,还有3D渲染,还有自动驾驶。人在开车的时候,每秒要收集60个不同的物体,然后根据这60个物体的不同组合和反映来做20个最重要的决定。然后从这20多个决定中选一个执行。所以就算用上最快的硬件,自动驾驶现在还不敢说能像人那样开车。就算是自动驾驶成功了,下一步还要自动飞行呢?因为我们老早就预言了:你咋不上天呢!所以说:计算速度永远是不够的!因为新的应用会越来越复杂,越来也实时。对了!我还忘了一个更重要的限制:计算的能耗!NASA飞行器上的CPU最多就是32位的,说出来你可能不信,国际空间站上没有一个CPU是64位的,我猜一个最主要的原因是航天员不爱看硬盘小电影吧。

另外一个流行的疑问是:我可以发明一种同样快的语言,但是没有C语言那么多的坑。想法是可以的,而且还真巧有这个语言,真巧它的名字叫D语言,真巧没有太多的人用!这是因为一个基本的事实。现在有太多,太多太多的C代码,他们大部分都在正常工作,就像Linux, Window, MacOS,Unix,Vxworks。你没有看错,这些操作系统的内核都是C,我虽然不确定C在Window中所占的比例,但是我相信微软的人不会傻到用C#去全部改写一个操作系统的内核。你想让这些人去用你的全新的语言,这就不是“有点”很傻,很天真了!而且有些代码,我们根本就不能改!NASA一个简单的5个CPU飞控软件编写完毕后,要进行一种“全覆盖”测试。如果CPU A坏了会发生什么?如果CPU A,B坏了呢?如果CPU A,C坏了呢。。。。?如果你愿意,你可以做个简单的数学组合。测试完毕后,别说重写,就算加个注释都不行。因为主管payload的大妈会非常严肃的质问你,为什么你上报的东西数量增加了,但是质量没有增加?你需要和她详细的解释:硬件和软件是不同的,硬件是那种摸起来硬硬的东西,但是软件不是那种摸起来软软的东西。看着大妈鄙夷的眼神,这个时候你会非常后悔自己手欠加入的哪一行注释。你还别不当真,这个是NASA的真实故事。

哪为什么C语言还下降这么多呢?很简单,有些任务本身就不是C语言的。我上学的时候还用C语言编过窗口界面呢?然后很快微软的人就推出了MFC,就是一大堆宏把底层的C windowAPI包装了起来。再后来这个技术也过时了。因为微软的人认识到,带有窗口的应用程序说到底不是C语言的本职工作,再这么一层一层包下去就有露馅的危险,于是他们发明了一个全新的语言C#来负责这个任务。Java也是这样,突出网络,易用,安全,跨平台。无论是Java, c#还是python, 他们都有意避开提高硬件的运行效率这个问题,因为这个问题上没办法和C竞争,也无法撼动Linux, Unix,GNU tool这些已有C代码的位置。剩下的就只是提高程序员的开发效率上大作文章。这对C语言是好事,把自己不善长的东西去掉,让自己跑的更快!

伴随着嵌入和实时系统的兴起,AI,机器人,自动驾驶等。这些都是C语言的核心应用,而且在这种应用上面,C语言没有竞争者。所以我感觉C语言会稳定在自己核心的应用中,并开始逐步回升。但是Java语言我个人不乐观。小型和灵活性上,Python更胜一筹。一行python代码后,你根本不知道自己还是不是duck类型?平台领域,每个平台都推出自己专属的语言。Windows会继续支持C#,苹果偏爱Swift, Android推出Kotlin,Google用go。Java宣称自己可以自由到每家做客,但是无论是到谁家,都会发现客厅里面坐着一个亲儿子,这个时候自己这个干儿子多多少少有点尴尬。所以我猜测,最后Java会稳定在对跨平台有严格要求的,大型非实时应用上。

最后说点闲话,C++不会淘汰C语言。有了对象后你会发现再简朴的对象也耗费资源,而且有了对象以后,总是不由自主的去想继承这个事,一但继承实现了,你会发现继承带来的麻烦远超过你的想象。Java的发明人James被问到如果可以从新设计Java语言的话,第一个要做什么事?他说:“去掉对象”!作为一个已婚,有两个孩子的程序猿,我感同身受。如果大家感兴趣,我可以再写一个博客,聊聊C++和C的真实区别所在。

如果你看到这里,还什么都没记住。那就只记住一点:没人能预测未来。
--------------------------
全世界只需要五台电脑 -IBM创始人
640K内存足够了 -微软创始人
没必要在家里用电脑-DEC创始人
--------------------------
如果再有人对你说C语言已经过时了,最好自己思考一下,能求真最好,如果不能,至少要做到存疑。

什么教材适合零基础的C语言学习者?

赵岩 http://zhaoyan.website

最近收到一个微博网友提出的这个问题,下面我简单地回答一下。
我没有读过很多的C语言教材。有一本谭浩强老师的《C语言程序设计》,貌似是很经典的,你可以考虑一下。我在哈尔滨工业大学教C语言的时候,使用的是工大苏小红老师的《C语言程序设计(第一版)》,个人感觉还可以。这本书目前好像出第二版了,但是我个人还是推荐第一版。

简单地说,这个问题就回答完了,下面再啰嗦几句,你要有时间就看看,没时间也看看,因为下面的话更有价值。我想说的是,教材就这么回事,大部分教材内容都差不多。也就是说,如果你看教材A没明白,那么教材B你也看不明白。教材的主要作用就是,教会你一些基本的概念,然后由这些基本的概念开始,你要开始简单的实践并产生一些问题,并尝试解决这些问题,这是你进步的开始。

下面再啰嗦几句,有没有时间都要看看!下面的话才是最重要的。既然你要开始学习C语言了,我要告诉你道理,知识和技能这三者之间的区别。这三者中,道理,有的时候以段子或者名人名言等面目出现,它是目前微信圈最流行的。它的最主要的特点就是一个字:贱!产出贱,传播贱,收获贱。大师们一不小心憋出一个段子,“像富人那样思考,追寻梦想去奔跑!”,然后按几下键传到微博,微信上。你打开手机按一下键,看到段子后热血沸腾,感觉整个人都不一样了!当然了,出去奔跑是有点累的,所以最后你决定转发一下,证明自己对人生有了崭新的理解和认识。不过你有没有想过,这个世界上最终是能量是守恒的。你按一下键获得的东西,这种东西到底有什么用?

段子流行后面的真实原因是人都是懒惰和想投机取巧的。举个例子来说,要想健康说起来是很简单:少吃,多动,早睡就好了。但是这三点做起来真是太TM的难了!你一边享受着自己喜欢的但是不健康的生活方式,一边深深的自责并瘫痪在床上翻看着手机。这个时候一位满面红光的养生专家发段子了,他说你不用担心,你可以把吃出来的病再吃回去的,只要你每天吃5种不同颜色的食物就可以了!这个时候长相甜美的营养专家也发帖了,她说你只要饭后吃一个蓝色的小药丸就可以了,只要988,五脏六腑乐哈哈!这个时候仁波切也发帖了,他说:南无无量天尊,该吃吃,该喝喝,啥事别忘心里搁!看完这些你心情大好,豁然开朗的同时并且还有点饿了。最后你妈妈来了,她让你别看手机并且早点睡觉。你不耐烦地说知道了。看到没,如果再有人告诉你什么21天从入门到精通的话,你要提高警惕。骗子成功的主要原因就是我们都想不劳而获,切记!

下面说说知识,这个东西你要获得,你就必须要付出成本了。无论是自学还是上学,你都要付出相当的金钱成本和时间成本。所以老师和学校的存在,你也可以理解为一种是半强制的措施,保证你能完成这个相对无聊,枯燥的学习过程,而不至于轻易地半途而废。无论你是通过在线学习,还是自己的兴趣驱动,我都真心希望你,坚持下来一个完整,系统的学习周期。 在学习知识的过程中,好的老师和好的书会让你少走些弯路。好的老师就是我!好书包括《C语言点滴》和《C程序设计新思维(第二版)》,前一本书会回答你看教材时产生的各种问题,后一半书介绍了一个完整的C语言的生态系统。前一本书深入,后一本书广阔。另外,别指望着通过看几本书精通C语言,记住一点,知识和技能是不一样的。

和知识相比,技能不可能通过看书获取,因为技能的获取完全靠自己的实践。所以100%带有强制性,例如你的毕业论文,你公司的任务等。 你要忍受任务带来的压力,要面对现实中各种复杂的问题和挫折。实验,思考,分析,解决。在这个令你最劳累的过程中,你失去了头发,获得了技能。技能的获取没有捷径,拿C语言来说,别人都说C语言坑多,解决这个问题的方法其实非常简单,就是每个坑你就踩一遍!顺便告诉你,这个过程中你是没时间,也没心情去转发段子的!

不过我要恭喜你,根据能量守恒定律,失去头发而获得的技能是最值钱的。在你未来的生活中,你要靠你的技能来赚取你的工资,你的技能的水平也决定了你的生活水平。举例来说,现在美国家庭的平均年收入是6万美元,而一个有技能的C/C++程序猿的年薪在8万~10万之间(一般公司的保守值,大公司在12万左右)。这个数字并不是再鼓励你,相反它是在警告你,学习C语言的路上,其实没有多少人能坚持到最后!当你的第一个hello world程序编译的时候,你面对满屏幕的错误和警告,再一次问问自己,是要继续和世界打招呼,还是转行去当专家和大师!

为什么C语言很难?

赵岩 http://zhaoyan.website

前两天,有个学校要给高中生开一门编程的课,大家讨论用什么编程语言授课,大部分人推荐用JAVA, 一部分说用Python,但是几乎所有的人都说:C语言太难了!

作为《C语言点滴》的作者,《C语言新思维》译者,这种语言坑我一般都绕开的,因为无论我说什么,没私心也显得有私心。但是“C语言太难”这句话太扎心,搞得我有点伤心。(文章排比对账,我只服自己!)我决定站出来为C语言说两句公道话!首先我个人必须承认,大家认为的都没错,相比较另外两种语言,C语言确实是最难学的。但是C语言为什么难?我认为锅不能C语言一个人背!背后还有三个主要的原因,那就是:面对任务难,开发环境难,底层难。

首先是面对任务难。我不认为用{}就比用缩进难!声明一个变量的类型就比不声明一个类型难!需要编译一遍就比直接运行难!就像你认为英语难,还分he, she;还有a, the。老外却不这么想。换位思考一下,下面这个牌匾也会让老外抓狂的。

说到底,这种语法级别的难度比较其实没有任何意义,只是一个使用习惯,一个小程序python用10行,C语言可能用20行,就因为多敲了10行你就难受的不要不要的,我只能对你说:要身残志坚!

不同与JAVA和python,C语言面临的任务几乎都是要求实时,高速或者是嵌入的。例如医疗,军事,飞控,航天,金融等领域。举个栗子,NASA大部分软件要基于三个不同的时钟系统,自转(公转)时间,CPU的晶振时间和原子钟时间。一秒要分成500份,基于2毫秒的基础进行操作同步;同时用全球的原子钟时间均值对所有时钟系统调整。在这种环境下,JAVA那种“大约一分钟以后”的虚拟机管理方式一定是不行的。 所以我在NASA工作所接触的软件,几乎都是C语言编写的。可想而之,这种软件的开发难度,当你阅读这种程序代码的时候,你说C语言太难了,这是否有点不公平?

其次是开发环境难。C语言一开始就和UNIX(LINUX)有不解之缘,它们是伴生的系统。所以要想发挥C语言的全部威力,最好的开发环境就是UNIX(LINUX)系统。但是问题来了,UNIX(LINUX)系统里的各种开发工具,每一个都不是省油的灯。它们设计的最初目的就是效率,而不是易学性。再举个栗子,gcc的各种编译开关就很复杂了,make系统为了解决gcc的部分问题,自己随之带来了更大的问题。git目的就是帮你保存历史备份,但是你会发现你经常会串改历史,或者干脆迷失在历史中。就连最简单的一个编辑器VIM,头一个月内,你最多的使用体验就是“恨不得拽自己的头发把自己提溜起来。”

好吧,外面的世界太凶险!让我们回到Windows妈妈哪里。虽然Windows的大部分内核都是C语言写的,但是它对C语言的支持缺最差。Why?如果你用Window的编译器去编译C语言,你会发现变量必须要写到函数的开头。它是唯一一个只支持到C89标准的编译器。Windows本身不想去抢这份实时,高速,嵌入的市场,老老实实做消费电子市场就好,这种市场要求开发容易,发布快。所以C#语言和后面的.Net平台才是它发展的重心。像玩LEGO那样的编程,你需要做的就是把一个个控件拽到窗口上,用鼠标来编程!所以还是算了吧,毕竟你也不想你在做飞机的时候,飞机上控制降落的电脑突然蓝屏了吧!所以如果你是一个C程序员,你唯一能做的就是在linux下使用哪些臭名昭彰的难学的工具。这笔账难到也要算到C语言的头上吗?

最后是底层难。这必须要要聊聊C语言两个最受诟病的特性,位操作和指针。这两个概念本身很简单。但是通过这两个概念,它把很多底层操作系统的知识和体系结构的知识都暴露了出来。指针指向地址后,马上引入了一大堆内存管理知识。什么是堆?什么是栈?这个地址在内存的那个区域?这个区域可以修改吗?这个区域自动回收吗?指针指向函数后,又引入了一堆操作系统知识,什么是回调函数啊?什么是事件驱动啊?以及位操作后面的二进制,溢出,浮点数精度等等一系列的问题。我用手指指向了一本《相对论》,然后就有人跑过来对我说,你这个手指头太难了!

如果编程只是你的业余爱好,使用那种语言真的无所谓。大部分初学者面临的任务规模下,三种语言的开发难度都差不多。 就是打个招呼,英语的“hello”,中文的“你好”,或者是日语的‘牙买碟’,我实在看不出这有什么难度上的区别。但是如果你立志要当一名高水准的程序员,C语言你是逃避不开的。或者编程序是你的饭碗,你也要认真考虑一下C语言。语言的易学性在就业上是一把双刃剑。如果一个公司招聘C程序员,你第一个反应就是他为什么不去招聘满大街的JAVA程序员?你面临的一定不是什么图书管理系统,也一定不是一个什么网站。想明白了这一点,就完全有理由要一个高价钱!

C语言很难,要逃避这种难,却很难!C语言很简单,要理解这种简单,却不简单(文章排比对账,我只服自己!)

《21st Century C, 2nd Edition》译者序

最近非常有幸的接受了人民邮电出版社胡俊英和陈冀康编辑的邀请,翻译《21st Century C, 2nd Edition》这个书,这是一本非常经典的C语言著作,目前已经是第二版了。计算机书出了很多年,大家对其自有判断,最简单的办法就是根据书名,90年代末期出版过几本比较经典的计算机图书,书名为:《**入门到精通》,《21天学会**》等。不过很快大家就开始借用这种书名,最后搞得有些良莠不齐。更有甚者,最近出现了好多,《**从入门到放弃》, 《**从入门到入院》 系列丛书,彻底颠覆了以前程序员中这么神圣的书名。好在还有O’Reilly出版社的动物丛书,目前还都是品质和经典的象征。经典到有的时候在圈内人们都忘了书名,只记得动物的名字。例如Perl语言的 “骆驼书”以及Git的“蝙蝠书”等等。

当完成最后一个字的录入,作为这本书的译者,应该系统的给这本书做个总结了。首先,这是一本经典的C语言图书,amazon有50多个评论,评分达到4分。这个骄人的成绩主要来自于本书的两个优点:第一个优点就是系统性和大局观。C语言最开始作为开发UNIX操作系统的工具,它和UNIX操作系统有着不可分割的关系。无论UNIX派生的POSIX标准以及GNU运动,C语言都是其核心的开发语言和工具。所以如果想真正发挥C语言的威力,那必须要把这个语言放到一个更大的编程环境中去。这本书通过对POSIX标准库,GNU编译器,Shell 脚本,Make,GIT以及文档和测试等一系列内容的介绍,建立了一个高效整合的开发环境,C语言作为这个环境的核心开发语言,通过各种开发工具和库的配合,将开发环境的优点淋漓尽致地发挥出来,从而能显著地提高你的开发效率。

第二个优点就是新思维和反规则。作为物理学的爱好者,我用物理来做一个类比 。牛顿创立了经典力学和万有引力。正当我们认为物理学已经完胜的时候,爱因斯坦在一边幽幽地说了一句:“光会拐弯”。在爱因斯坦的结论在观测日全食得到验证以后,这位天才自信心爆棚并宣称:“一切都是可以通过计算来确定的”,这个时候研究量子力学的波尔却传给了他一个纸条说:“上帝掷骰子!”。也许,我是说也许,我们一直都相信的规则或者答案过于片面。 就像有一天我6岁的女儿小米粒问我,“我们人类从那来啊?”我说:“有人说是猴子变得,有人说是神创造的,你信哪个都可以?你告诉爸爸,你信那个啊?”我的女儿想到没想就回答到:“是神把猴子变成人的!”说完她飘然而去。

我们人类总是有一种倾向,一旦形成了自己的某些规则,那么就会自然地排斥和否定另外的反规则。而这本书的难能可贵之处就在于,它不仅提出了C语言的一些反规则,而且通过一些例子证明这些反规则是合理的。例如,我们可以建造高效和准确的宏,我们可以不需要斤斤计较对内存的使用,哪怕有点内存泄漏,我们可以用goto,但是对switch却完全可以放弃等等。现代物理有一个反物质学说,当物质和反物质遇见,二者会立即湮没,并爆发出巨大的能量。这里我借用一下:当你熟悉了规则,同时也理解了反规则,这个时候你的心中就没有了规则。剩下的就是巨大的能力。此时小李飞刀已经不带刀,此时无招已经胜有招。

俗话说:“优点不说没不了,缺点不说不得了。”下面说说本书的缺点,那就是:对每一部分的内容并没有详细地介绍。所以你不要指望着阅读完本书,你能熟练地使用shell脚本,写出复杂的makefile并通过Git高效地与人协作。坦白的说,这也并不算是缺点。这本书的目的就是告诉你,当你想干XXX的时候,有YYY工具你可以用,而YYY工具的基础用法是ZZZ。 当你发现ZZZ并不够用的时候,你可以去找专门的介绍YYY的书。这个时候你会发现完整介绍YYY的书,它的厚度足以挡住狙击步枪射出的子弹。客观点说,这也算不上啥缺点。没办法,任何和UNIX相关的东西,都有吓人的学习曲线。你想要“会当凌绝顶,荡胸生层云”,那么曲线必须要很陡才行!

最后说说本书面相的对象:首先,这本书并不是教材,虽然本书的后面有一个简短的介绍C语言的附录,我没有骗你,它确实是很简短。请注意我的用词,我只是说“简短”,并没有说“简单”。所以如果你是一个C语言的初学者或者是零基础,这本书并不适合你。

这本书的面相对象是:有一定C语言基础的高年级学生,或者是一些使用C语言作为主要开发语言的工程师。对于高年级的学生,它们缺乏的是一种对大的编程环境的认识。 而对于使用C语言的从业人员,这本书会让你对C语言有不一样的认识,它对你多年使用C语言形成的习惯和风格提出了挑战,让你有一种“原来C语言也可以这些用”的赞叹!然后让这些反规则去湮没你心中存在多年的规则,从而爆发出巨大的能量!

有的时候,书和读者之间是讲点缘分的。 有的时候读者会问什么是好书?我认为:你看懂的,有收获的都是好书。有些人看见花,有些人看见草,而我只是把你带到草地上。最后,我为本书做了一个网站: http://zhaoyan.website/xinzhi/c21/book.php 里面对每一章都有我的观点,推荐的补充内容等。对于本书的“每部分内容过于简短”这一缺点,我做了一些有益的补充和修正 。Enjoy It!

赵岩
02/11/2017

《C语言点滴》读者来信及回复

赵岩老师您好!我是一位来自四川的大二的男同学,是一名C语言的初学者,那天我冒着大雨在成都书店看C方面的书,最后买了您的那本《C语言点滴》(现在想想冒着大雨还是真值得哈:),您的书实在很有趣,我并不是计算机专业的学生,但您的书让我能了解了很多其他的东西,生动而有趣。我那天晚上花了一晚上去看一个关于 列指针和行指针的程序,看了半天头疼,翻开您的书看了指针那一章,我一下子就开窍了! 非常感谢您用生动的语言让我这样自己抱着各种书本自学C语言的学生体会到了轻松的味道! 我是一名自学者,现在学完了C语言的入门内容,我尝试编写了闹钟程序和通讯录程序,但是我现在有点迷茫,我的目标其实一方面是想学些编程方面的东西来自娱自乐,但是更多的方面其实是有那么一天我能帮我爸编写一个期货方面的软件,也就是说,我学C其实是想编写软件吧! 我想请教老师,如果我的目标是编写软件,我现在应该去学习和锻炼哪方面的内容,在您书内有一张这样的图,关于以后学习的方向,我是不是应该朝着“产品开发”方向去学习呢?我有没有必要学习其他的语言呢?

————————————回复———————————-
首先是感谢你喜欢这本书。尤其是冒着大雨,我猜一定是买完本书以后,又把书顶在头上跑回了家。最开始出版的时候,我就告诉陈冀康编辑书价要尽量便宜,陈编辑补充说再便宜也要塑封皮。如何分辨女孩背的是否是山寨包,一到下雨天立马就知道了,顶头上的都是山寨包!塞到衣服里的都是正品。为了提高本书在下雨天的销量,不得不佩服,编辑就是高!

我本来想写点鸡汤文鼓励你一下。但是现在人已经不再看完鸡汤文就跟打了鸡血般兴奋了!所以还是说点白话。总结为三个建议:

第一,我个人感觉不需要学习其他的语言,你需要把C/C++两种语言精通即可。我说的精通是指深入的精通,不仅需要知道每个语言的特性,还要知道为什么要有这个特性?这个特性最常用在什么地方?等等深层次的问题,而不要仅仅停留在语法层面上。 就像我书中介绍的C语言的函数指针,彻底了解它,你就会知道C#中一定有对应的东西存在,只不过叫做“代理”了。它们背后的概念和思想才是最有价值的,就是模拟出一个接口,来提高系统的模块化。至于在C#中如何定义一个代理,我写不出来。但是有一台联网的电脑,写出来也就是一分钟的事。

第二,多动手。以英语为例,你一定有这个体会,你可以看懂一篇文章,现在让你说一遍这篇文章的大意,你立马傻眼。背后的原因在于知识分为被动知识和主动知识。那些看到就明白,自己却不会用的是被动知识。对你有用的其实是主动知识,而且人的主动知识是被动知识的一个很小,很小的子集。要把被动知识变为主动知识,唯一的途径就是去使用,去动手。用过几次,错过几次,才能变成你的主动知识。切记,切记,切记!

第三,有没有想过为什么人一磕瓜子就停不下来,而一看书就不行?这是因为磕瓜子把一个大任务分解为很多小任务,而每个小任务都有回报!而看书恰好相反。所以在学习的路上,按照磕瓜子的战略去学习。不要上来就想编一个期货软件,今天写个hello world,明天打印个心型,后天在心上加个箭。。。。。。。

最后,说点题外话。这个世界并不是按照程序在运行。你发慢慢发现原来小成靠勤,中成靠命,大成靠爹!C/C++语言可能改变不了你的命运,但是你磕你的瓜子,毕竟也是带壳的!如果学习C/C++语言能带来些快乐,那就做一个快乐的程序员吧!(结尾上碗鸡汤,老磕瓜子人也容易上火。)