打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
当算法工程师在谈论C ,他们在谈论什么?

作者:王金戈

来源:https://zhuanlan.zhihu.com/p/82895086

本文已经作者授权,转载请联系原作者

高强度使用了一年的C++,又适逢读完了Scott Meyers的《Effective Modern C++》,很想写下这篇文章,说说我对C++的看法。

从Java到C++

最初接触C++是在大一夏季学期,三周时间入门“C++面向对象编程”。虽然C语言学得不错,但这门课还是让我头晕目眩,不知所云。既没理解面向对象的思想,也没看懂C++稀奇古怪的语法。为数不多的印象就是可以用cout替代printf,用引用替代指针,仅此而已。

后来,我与C++渐行渐远,因为有了另一个美好的邂逅——Java。我对Java是一见钟情,简洁的语法,纯粹的面向对象特性,丰富的类库,广泛的应用领域,都让我爱不释手。与之相比,C++为了兼容C而作的妥协,前后不一的设计理念,奇怪的标准库,越来越诡异的新特性,实在让人难以接受。

若干年后,成为研究生的我由于算法研究的需要,不得不重拾C++。起初,拷贝和赋值的概念让我迷惑了好一会儿。我无法理解为什么C++设计这么细致的控制手段,值传递和引用传递,拷贝构造和移动构造,左值和右值等等等等。这些在Java中都是不存在的,所有对象都是引用类型就好了,为什么要有那么多规则?

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

一切为了性能

有人说,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++》这本书,与几年前读《Effective Java》的感受完全不同,C++这部似乎每页都写满了两个字——“性能”。作者对性能的挖掘已经到了匪夷所思的地步,一丝一毫都不放过。下面我们就谈谈“C++程序员的自我修养”,探讨C++编程中需要注意的地方。

C++程序员的自我修养

  • 内存管理

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

  • 数据传递方式

首先,一定要熟悉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<T>(name));}

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

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

  • 了解标准库

了解标准库不只是会用,而是理解其内部实现原理。比如,vector内部是数组、list内部是双向链表、unordered_map是哈希表、map是红黑树等等。当然,基本的数据结构常识是必不可少的,以免频繁插入数组,或是频繁通过下标访问链表元素的情况发生。此外,vector、unordered_map、map如何动态扩容,是否需要预先分配空间,何时使用emplace_back代替push_back,如何区分push_back的两种重载,这些细节问题都是值得我们注意的。

  • 向C++11、C++14、C++17靠拢

从C++11开始,标准委员会每3年会颁布新的标准,绝不拖延。目前的最新版是C++17,明年会颁布C++20。现在已经是2019年了,可很多人还在用C++98写代码。要知道,Ubuntu 16.04默认的gcc版本是5.4,该版本已经支持了C++14的全部特性。所以,大部分开发者使用的电脑上应该至少可以利用到C++14的新特性。

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

  • 切记,避免过早优化

著名计算机科学家Donald Knuth说过一句名言:“过早优化是万恶之源。”初时不解,现在深有体会。程序的扩展性依赖于早期的合理设计,但不意味着我们应该在刚开始就设计出一套复杂的系统。软件工程中的敏捷开发正是针对这一问题给出的解决方案,快速迭代,随时重构,才能开发出良构的软件。我们不是神仙,没人能预知今后的变化,为不存在的需求设计复杂的架构,是一种愚蠢的做法。

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

结语

最后,本文的目的其实是引导大家去学习真正的C++知识,不是看我们这些博客,而是学习一手资料。

我推荐《C++ Primer》、《Effective Modern C++》和cppreference,第一本书学习C++基础,第二本进阶,第三个是C++官方文档,用来平时检索。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
C++类对象的拷贝构造函数
vector用完后需要释放嘛?
c 三五法则(适合学完一遍不怎么清楚的童鞋)
【博文连载】C 中的“引用”
对于拷贝构造函数和赋值构造函数的理解
C 面试题之浅拷贝和深拷贝的区别
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服