作者 | 梁唐
大家好,我是梁唐。
今天是国庆假期的倒数第二天,之前几天基本上都在给大家发福利,今天写篇文章,聊点生活吧,就聊聊老梁最近最头疼的事情。
最近让老梁最头疼的事情有两件,一件是团队调整,一直跟了很久很熟悉的老板跑了,第二件事是新来的老板给了我们一个新的系统,老梁拿来一看是C++写的,差点没晕倒。读源码有一点读文言文的感觉,单独看每一个字都认识,组合到一起就歇菜了。
毫不掩饰地说,老梁的C++水平其实很一般,甚至可以说是很差。差到什么程度,差到完全没有项目经验,除了ACM比赛里用到的那些语法,其他一无所知。这也没啥不好意思的,毕竟术业有专攻,老梁也没做过C++的开发,大半的精力都投在了算法上,不是研究各种数据结构,就是研究模型效果、拟合,C++只剩下能刷leetcode了。
但是你们知道吗,当年老梁去找工作的时候,可是冲着C++的开发岗位去的。投了好几个公司给我调剂了,安排到Java去了,老梁当时还腹诽:这帮公司也太水了,Java这么不优雅的语言也能用?
因为那时候不懂,想法非常单纯,C++运行速度无敌,其他语言都是渣渣。Python是牛皮,但是运行速度能看么?尤其看不起用Java做项目的,觉得Java太二了,一切面向对象,用个实例都得new一下,指针也没有。用个maven各种循环依赖,一千行代码两千行xml配置。
直到后来实打实地干了几个月Java工程师之后,被现实狠狠地教育了,老梁发现Java其实很优雅,优雅到老梁甚至thinking in Java都没看完,就照着前人留的代码,就可以把代码写得有模有样,甚至还获得了晋升的资格,成了老板眼里技术扎实的后生。
再到后来,老梁多混了几年江湖,对各方面认知稍稍深了一些之后,对这个问题有了更深的看法。其实如果大家有所关注前沿技术的话也能发现一点端倪,很多大型项目的核心源码都在逐渐被替换,有的被替换成Java,有的被替换成Python,也有的被替换成Go。
被替换的原因也很简单,无非就一点,C++很难。很难在于它的范式、语法糖很多,多到C++的作者也不敢说自己精通C++。在于它写出的代码很容易藏有漏洞,程序员们需要面临大量性能优化、内存管理等问题。上古程序员留下的大型工程就像是地雷阵一样可怕,谁也不知道怎么就爆了,以至于《程序员的呐喊》一书中有这么一条:


关于祖传的C++工程,有各种各样的笑话,堪比苏联笑话合集,大家感兴趣可以去网站上搜索一下,绝对有趣。
所以如果你要问老梁,是C++还是not C++,老梁会很坚定地告诉你,快逃。
玩笑归玩笑,但其实老梁最近在硬着头皮读C++代码的时候,心情很复杂,因为曾经老梁也是C++的拥趸,为了C++也没少黑Java(好像C++拥趸大多都是Java黑)。但C++天花乱坠般的代码读起来觉得痛苦也是真的,觉得很多语法糖看起来莫名其妙或者很难理解也是真的。
看着看着,莫名的有一种心生悲悯的感觉。会忍不住地想,要是C++的语法能简单点,让写代码和读代码的人都不那么费劲该有多好。
老梁举几个例子,比如C++的面向对象当中,有一个概念叫做虚函数。在C++当中只有声明了是虚函数的函数或者方法可以被子类继承和覆盖,C++正是通过虚函数实现了多态。比如维基百科里有这么一个例子:
# include <iostream>
# include <vector>
using namespace std;
class Animal{
public:
virtual void eat() const { cout << "I eat like a generic Animal." << endl; }
virtual ~Animal() {}
};
class Wolf : public Animal{
public:
void eat() const { cout << "I eat like a wolf!" << endl; }
};
class Fish : public Animal{
public:
void eat() const { cout << "I eat like a fish!" << endl; }
};
class GoldFish : public Fish {
public:
void eat() const { cout << "I eat like a goldfish!" << endl; }
};
class OtherAnimal : public Animal{
};
int main(){
std::vector<Animal*> animals;
animals.push_back( new Animal() );
animals.push_back( new Wolf() );
animals.push_back( new Fish() );
animals.push_back( new GoldFish() );
animals.push_back( new OtherAnimal() );
for( std::vector<Animal*>::const_iterator it = animals.begin();
it != animals.end(); ++it) {
(*it)->eat();
delete *it;
}
return 0;
}
在Animal这个类当中使用了virtual关键字标记了eat方法和析构函数两个方法是虚函数,正因此当我们执行程序的时候会得到:

如果我们把virtual关键字去掉,得到的结果就会是:

老梁个人感觉这里的设计非常无厘头,完全没有必要加virtual这个关键字,直接像Java一样,全员默认virtual不行么,子类如果也实现了同名的方法就覆盖,如果没有就用父类方法不是也可以吗?增加virtual这个概念,除了增加学习和使用成本之外,老梁没想出其他的优点。尤其是再叠加多继承的时候,更是混乱到离谱……
再比如指针和引用,也很令人困惑,其实这两者几乎都是可以通用的。但当你发现同样一个项目当中有的人用指针有的人用引用的时候,搞不好真的会崩溃……
再比如C++的内存管理,给使用者带来的困难也很大。因为要自己控制实例和内存的释放,对于开发者来说这是一个非常巨大的挑战。尤其是在一些大型系统当中,我们很难知道到底有哪些下游用到了我们的对象,想要设计出很好的释放机制非常困难,有些时候甚至是无解的。
后来C++为了解决这个问题,也学习Java提供了自动GC的功能,但问题来了,内存管理本身就是C++的一大特色之一。如果也像Java一样自动GC了,那么为什么不去使用Java呢,使用C++的意义在哪里?
像是这样的问题还有很多,C++性能高则高矣,但是为了追求性能放弃了很多,不但放弃了很多,还大大提升了使用者的门槛。并不是所有开发者都是jeff dean,有大佬说C++写出了地球上最赚钱的软件office,但老梁特地问了微软的同学,office的核心代码已经很多年没有更新了,谁也不敢改,谁也不知道改了哪里会出问题,已经成了没有人能读懂超出人类理解能力的怪物……
其实也不只是C++,其他语言也有这个毛病,一个大型的工程改起来无比的麻烦,谁也不知道代码当中到底嵌套了多少耦合。明明只改了A的逻辑,结果BCD全挂了是常态。所以很多公司宁可多花点时间重写项目,也不愿意去改,不但有很多问题,而且阅读源码本身也不是一件容易的事。
对语言特性感兴趣的同学,可以去学一下Go。Go语言和C++完全是两个极端,Go语言的设计理念就是一切从简,能砍掉的功能绝对不添加。也很少支持面向对象,抽象方法、虚函数这些统统没有,Go语言的关键词和语法糖也是老梁见过的这些语言当中最少的,所有的语法对于有编程基础的同学来说一个下午就可以学完。
学习完Go语言,再来反思C++的很多设计,就会发现大众对于C++的一些诟病,以及C++当下面临的问题和挑战其实都能从深层次找到原因的。
当然,老梁说这些并不是为了把C++喷死,毕竟存在即合理,C++诞生至今四十多年屹立不倒,仍然是世界上最流行的编程语言之一,也同样有内在原因的。我们作为学习者和使用者,只是武断地给出判断这个好那个垃圾是没有意义的,认真地学习和研究其中的设计精髓提升自己的技术能力才是关键。毕竟我们要做的是工程师,而不是乐评人。
好了,今天的文章就聊到这,老梁继续去啃C++ primer了……