typedef有什么用_我是怎么用C++恰饭吃的

学习这件事不在乎有没有人教你,最重要的是在乎你自己有没有觉悟和恒心。

时间过得真快啊,转眼间,我已经使用C++写了一年的项目了。又适逢我读完了几本书,包括Scott Meyers的《Effective》系列和《Effective Modern C++》以及《C++ Templates》,还有一些其他的。很想写下这篇文章,谈谈我对C++的看法。

a1cae4b3665720010a6a03a35f9aa3a0.png


0. C++为什么多规则

我第一次接触C++是在大二的某个学期,之所以是某个学期,是因为我实在记不起来是上学期还是下学期了,反正是一门选修课。我自认为我C语言学的还可以,便选修了这门课,每周一节课,入门“C++面向对象编程”。

虽然C语言学得不错,但这门课着实让我头晕目眩,不知所云。一个学期下来,既没理解面向对象的思想,也没看懂C++稀奇古怪的语法。为数不多的印象就是可以用cout替代printf,用引用替代指针,仅此而已。

后来,我与C++渐行渐远,因为有了另一个美好的邂逅——Java。我谈不上对Java是一见钟情的,但是简洁的语法,纯粹的面向对象特性,丰富的类库,广泛的应用领域,还是让我爱不释手。

与之相比,C++为了兼容C而作的妥协,前后不一的设计理念,奇怪的标准库,越来越诡异的新特性,实在让人难以接受。于是我与两位同学用一个暑期的时间用Java开发了一套不知名的管理系统,参加了一个不知名的什么大赛,一人获取了400元的奖金。是的,我把钱记得很清楚是。

后来,成为研究生的我,由于科研需要,便很少再使用Java,更别在提什么C++了。再后来,听到去百度工作的我亲爱的郭师兄说起自己在用的Python,我的内心也没有泛起丝毫的波澜,尽管后来我也使用Python。并真心觉得这个语言好用。

多年之后,由于工作需要,我不得不重拾C++。起初,拷贝和赋值的概念让我迷惑了好一会儿。我无法理解为什么C++设计这么细致的控制手段,值传递和引用传递,拷贝构造和移动构造,左值和右值等等等等。为什么要有那么多规则?

今天,我可以肯定地说出答案:为了性能。

有人说,C++是个全能语言,底层、算法、软件、后端都能做。但都能做就意味着都做得不好,专业性和通用性不可兼得。C++开发软件、做Web后端显然是不合适的,没有大量的类库支撑,开发过程困难重重。

而C++在算法领域则是一骑绝尘,SLAM、深度学习,都得用C++写,即便你用了TensorFlow的Python API,它内部也是用C++实现的。之所以如此,是因为算法通常是计算密集型应用,好的实现和差的实现在性能上千差万别,算法研究者必须用一种能够精确管控性能的编程语言,这种语言就是C++。

很多人以为C++比Java快是因为Java必须运行在虚拟机上,没有C++编译成机器码来得直接。这种观念不是没有道理,但不免浅薄。现在的Java有了JIT(Just-in-time compilation,即时编译)优化,性能已经越来越接近C++,所以性能差距并不体现在虚拟机上。

C++和Java,以及其它高级语言,如C#、Python的真正区别是,只有C++支持手动管理内存并指定数据传递方式。所谓手动管理内存,不仅仅是通过指针访问特定地址的内存数据,更在于由用户决定对象分配在栈上还是堆上,这一点至关重要。

Java的对象全部分配在堆上,而我们知道堆内存是不连续的,所以Java程序需要频繁访问不连续的内存,这会破坏缓存的有效性,从而导致性能下降。再来看数据传递方式,Java所有的变量都是引用,变量赋值和参数传递都是对引用的拷贝,这在一定程度上避免了初级C++程序员容易犯的冗余拷贝的问题,但同时也扼杀了灵活性,毕竟有时候,你真的需要拷贝对象,而不仅仅是传递引用。

不过,C++提供的灵活性并不一定能转化为高性能,糟糕的C++代码可能比同样功能的Java代码速度更慢,因为你很容易陷入无谓的拷贝和构造中去。作为C++程序员,无疑要十分清楚每行代码背后的隐藏含义,这也注定了写好C++并不容易

《Effective Modern C++》这本书,每页都写满了两个字——“性能”。作者对性能的挖掘已经到了匪夷所思的地步,一丝一毫都不放过。下面我们就谈谈“C++程序员的自我修养”。


1. C++程序员的自我修养

内存管理

直接声明对象还是用new创建对象?前者在栈上申请空间,后者在堆上申请空间。处理器对栈内存的操作一般快于堆内存,除非真的必须动态管理内存,建议都在栈上声明对象,这也可以避免忘记delete导致的内存泄露。

如果要动态分配内存,请不要把内存处理的细节暴露给你的“用户”,将内存处理的细节封装起来,有很多方法,模板和Pimpl均是不错的选择。同时在封装的时候注意不要产生内存泄漏。

数据传递方式

首先,一定要熟悉C++11提供的五种拷贝控制函数,包括拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符、析构函数。它们都是隐式调用的,首要任务就是弄清楚它们各自被调用的场合,然后是它们的功能。

给定一个赋值语句或函数调用语句,要准确地分析出这句代码调用了哪种拷贝控制函数。这期间,需要理解右值和右值引用,否则移动构造和移动赋值就无从谈起。

有了这些基础后,我们才能针对特定的场景,选择最合适的函数参数。比如,addName函数用来把参数存入names_容器,从下面三种方式中选择最合理的方式:

// Version 1, by-value copy name to the function.void addName(std::string name) {    names_.push_back(std::move(name));}// Version 2, provide two overloads, one with const reference, another with rvalue reference.void addName(const std::string& name) {    names_.push_back(name);}void addName(std::string&& name) {    names_.push_back(std::move(name));}// Version 3, use universal reference to integrate lvalue reference and rvalue reference.template<typename T>void addName(T&& name) {    names_.push_back(std::forward(name));}

这里没有最优解,版本2和版本3最节约性能,但可能会导致源码或二进制码体积较大,版本1最简洁,但会多出一个移动构造。取舍的关键在于具体的应用场景,假如对象的移动构造非常廉价,完全不耗费性能,那就选择版本1,否则就选择版本2或3。

说到这里,便又涉及到另一个问题,你是否清楚对象拷贝构造和移动构造的代价?对于自定义类,如果没有手动声明这些构造函数和赋值运算符,编译器会为我们自动生成。但编译器自动生成的这些函数长什么样?如果我们需要自定义这些函数,应该遵循什么规则?答案很繁琐,但我们必须逐一理清。

熟悉标准库

“不熟悉C++标准库,任何人都称不上是高生产力的C++程序员。”但是熟悉标准库是多么艰难的一件事啊!《C++标准库》这本书绝对应该是你必备的案头书。

标准库不只要会用,而是理解其内部实现原理。比如,vector内部是数组、list内部是双向链表、unordered_map是哈希表、map是红黑树等等。当然,基本的数据结构常识是必不可少的,以免频繁插入数组,或是频繁通过下标访问链表元素的情况发生。

此外,vector、unordered_map、map如何动态扩容,是否需要预先分配空间,何时使用emplace_back代替push_back,如何区分push_back的两种重载,这些细节问题都是值得我们注意的。

最朴素的方法就是去阅读STL源码,可能是一个痛苦的过程,首先这里边涉及到数据结构、泛型编程等众多知识。光数据结构和泛型编程可能很多程序员一辈子都没有深刻过,我们最大的障碍在于心中的怯弱。

向C++11+靠拢

从C++11开始,标准委员会每3年会颁布新的标准,绝不拖延。现在已经是2020年了,可很多人还在用C++98写代码。

从现在开始,动起来吧,了解auto关键字,使用nullptr替代NULL,使用using替代typedef,尝试使用constexpr和noexcept关键字,使用lambda替代std::bind,使用std::thread或std::async替代pthread。这些改变会使代码变得更清晰、更高效。

切记,避免过早优化

“过早优化是万恶之源。”初时不解,现在深有体会。程序的扩展性依赖于早期的合理设计,但不意味着我们应该在刚开始就设计出一套复杂的系统。

软件工程中的敏捷开发正是针对这一问题给出的解决方案,快速迭代,随时重构,才能开发出良构的软件。我们不是神仙,没人能预知今后的变化,为不存在的需求设计复杂的架构,是一种愚蠢的做法。

之所以要强调这点,是因为过去犯的此种错误太多。看过好的软件架构,就想用到自己的项目中来,殊不知别人也是经过反复迭代才到了今天的地步,绝不是一开始就成型的。一个软件,只有完美贴合其所处的应用场景,才是好的设计,生搬硬套设计模式是没用的。


最后,本文的目的其实是分享一下个人的在学习和工作中的一点思考,所有的东西都是真正地在项目中沉淀下来的,引导大家去学习真正的C++知识,学习一手资料。

a1e56f7c98d9dff20357d33d3c137917.png

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注