我读研期间学的 Windows C++ 开发,毕业第一份工作很挑,非 Windows C/C++ 不做,后来做了几年 Windows C++ 技术负责人,再后来又转行做 Linux C++,又做了几年 Linux C++ 主程,再后来做了 Java 开发,如今作为负责人,根据业务做技术选型,需要哪种语言就用哪种语言。但是我最喜欢的还是 Windows C++,C++ 学好之后真的能对你的技术基础起的非常好的促进作用。
分享下我个人经验和感受吧
写在前面的话
在大多数开发或者准开发人员的认识中,C/C++ 是一门非常难的编程语言,很多人知道它的强大,但因为认为“难”造成的恐惧让很多人放弃。
我从学生时代开始接触 C/C++,工作以后先后担任过 C++ 客户端和服务器的开发经理并带队开发,至今已经有十多年了。虽然时至今日哪种编程语言对我来说已经不再重要(我目前主要从事 Java 开发),虽然时至今日哪种编程语言对我来说已经不再重要(我目前主要从事 Java 开发),其妙无穷,就像武侠小说中的“九阳神功”一样,有了这个基础,您可以快速学习任何语言和编程技术。
目前 C/C++ 的应用领域
需要注意的是本文不细分 C 与 C++ 的区别,通常情况下,C++ 可以看成是 C 的一个超集,在古典时期,可以认为 C++ 就是 C with classes。虽然如今的 C++从功能层面上来看,离 C 越来越远了;但是从语法层面来上来看,大多数 C++ 语法还是与 C 基本一致的,所谓 C++ 的面向对象特性,如果细究 C++ 类方法的具体语法还是 C 的过程式语法。当然,面向对象是一种思想,语言本身对其支持的程度固然重要,能否熟练地使用则是更要看开发者的水平了。
C 语言目前主要用于像操作系统这样一类偏底层的的应用开发,包括像 Windows/linux等这样的大型商业操作系统,和嵌入式操作系统、嵌入式设备上的应用。还有一些开源的软件,也会选择C开发,这些系统主要优先考虑程序执行效率和生成的可执行文件的体积(C 代码生成的可执行文件体积相对更小),当然还有一些是历史技术选型问题,这类软件以 redis、libevent、nginx,目前像国内的电信服务商所使用的电话呼叫系统一般也是基于一款叫 freeswitch 的开源 C 程序做的二次开发。
C++ 面向对象的语法与 C 相比较起来,在将高级语言翻译成机器二进制码的时候 C++ 编译器在背后偷偷地做了大量工作,生成了大量的额外的机器码,而这种机器码相对于 C 来说是不是必须的。例如,对于一个 C++ 类的实例方法,编译器在生成这个方法的机器码时,会将函数的第一个参数设置成对象的 this 指针地址,以此来实现对象与函数的绑定。正因为如此,许多开发者会优化和调整编译器生成的汇编代码。
C++ 与操作系统平台
从上面的介绍可以看出,与 Java、python 等语言相比,C/C++ 语言是运行在离操作系统最近的一种高级语言,因此其执行效率也比较高,但是有得必有失,也因为如此,所以 C/C++ 这门语言存在如下特点:
C/C++ 整套的语法不具备“功能完备性”,单纯地使用这门语言本身提供的功能您无法创建任何有意义的程序,您必须借助操作系统的 API 接口函数来达到相应的功能。当然,随着 C++ 语言标准和版本的不断更新升级,这种现状正在改变;而像 Java、python 这类语言,其自带的 sdk 提供了各种操作系统的功能。举个例子,C/C++ 语言本身是不具备网络通信功能的,必须使用操作系统提供的网络通信函数(如socket系本身是不具备网络通信功能的,必须使用操作系统提供的网络通信函数(如socket系整的网络通信功能。我在读书的时候常常听人说,QQ、360 安全卫士这类软件是用 C/C++ 开发的,但是当我学完整本 C/C++ 教材以后,仍然写不出来一个像样的窗口程序。许多过来人应该都有类似的困惑吧?其原因是一般 C/C++ 的教材不会教你如何使用操作系统 API 函数的内容。
正因为 C/C++ 语言需要直接使用了操作系统的接口功能,这就造成了 C/C++ 语言繁、难的地方,如操作内存方面不当容易引起程序宕机,不同的操作系统的 API接口使用习惯和风格也不一样,接口函数种类繁多,开发者如果想开发跨平台的程序,必须要学习多个平台的接口函数和对应的系统原理。
在应用层开发,直接使用操作系统的接口的函数,往往执行效率高,控制力度大,您的开发能力仅仅限制于操作系统本身,Java 这类语言,很多功能即使操作系统提供的,如果 Java 虚拟机不提供,开发人员也无法使用。正如著名的编程大师 Charles Petzold 说的:
“显而易见,究竟用哪种方式编写应用程序最好,其实并无一定之规。应用程序本身的特性应该是决定采用何种编程工具的最主要因素,但是无论将来你采用什么样的编程工具,通过了解操作系统 API 从而深入理解操作系统的工作原理,这本身就有很重要的意义。操作系统是一个非常复杂的系统,在 API 之上加一层编程语言并不能消除其复杂性,最多不过是把复杂性隐藏起来而已。说不定什么时候,这种复杂的那一面迟早会蹦出来拖你的后腿,懂得系统 API 能让你到时候更快地挣脱困境。 在基本操作系统API之上的任何软件层或多或少都会限制你使用操作系统的全部功能。比如,你或许发现采用 Visual Basic 来编写你的应用程序非常理想,但是就有那么一两项非常基本的功能 Visual Basic 无法支持。往往这个时候你得非要调用基本 API 。作为直接使用操作系统 API 的程序员,我们的活动空间完全由 API 来规范,再没有什么其他方式比直接调用 API 更有效、更灵活多样了。”
总结起来,C/C++ 语言的开发核心是建立在直接调用操作系统 API 的基础上的,优点是执行效率高、发挥空间大;缺点是,需要经过系统深入的学习,学习周期长,编写代码较复杂、容易出错。
Linux C++ 与 Windows C++ 领域之争
我之所以把这一个标题单独列出来,是想纠正现在很多 C/C++ 新人和初学者的一些的不当认识,一般有以下几种观点:
1.Linux C++ 开发就是后台开发,而 Windows C++ 开发就是客户端开发;
2.后端开发比客户端开发(前端)高级,因此后端开发行业薪资水平比客户端开发薪资要高;
3.我只学 Linux,不学 Windows。
我相信对于 80 和 90 这一代的开发者来说,当初接触计算机并进入软件行业,都是从接触 Windows 开始的。时至今日,大数据、人工智能等各种新技术方兴未艾,移动互联网如火如荼。但是无论是 Linux 还是 Windows,尤其是 Windows,还是我们大多数人工作、学习、娱乐使用最多的操作系统,我们每天都会使用上运行在其上的各种软件。我们使用这些软件像喝水、呼吸空气一样自然,所以很多人就忽视了这类软件的“基础作用”。对于 Windows 上的软件开发由于其发展了很多年了,这些领域也比较成熟,一般不再招初中级开发,而是需要水平较高、经验较丰富的高级开发者,这让很多人造成了“Windows C++”开发市场需求已经很小了的错觉。试问,PC QQ 部门这些年对外招了多少人?
另外,Linux C++ 和 Windows C++ 一样,没有孰高孰低之分,只是两种不同的操作系统而已,不要觉得在 Linux 下敲命令就比在 Windows 的图形化界面点击鼠标达高级。图形化界面之于命令行,是人们对更高级、更方便的工具的追求的必然结果。Linux C++ 也不一定就是后台开发,Windows C++ 也不一定就是客户端开发;所谓的服务器与客户端是个相对的概念,即谁给谁提供服务,提供服务的我们认为是服务端(后台),被服务的我们认为是客户端(前台)。而 Windows 作为后台服务的应用也比比皆是,如笔者之前所在的某交易所的服务器后台都是 Windows 下的 C++ 程序;另外如一些游戏类的服务器端,也不少是 Windows 的。
借用《UNIX 编程艺术》这本书的观点,Windows 和 Linux 的哲学理念不一样,Windows 是假设你不会操作,它教你如何操作,而 Linux 是假设你会操作然后进行操作;根据这个理念,Windows 一般普通人用的多,而 Linux 程序员用的多。从编程的角度来说,Windows 的代码风格是使用所谓的匈牙利命名法,而 Linux 使用的短小精悍的连字符风格,例如同一个表示屏幕尺寸的整型变量,Windows 上可能被命名为 iScreen 或 cxScreen ,而 Linux 可能是 scrn;再例如 Windows 上创建线程的函数叫 CreateThread, ,Linux 下叫 pthread_create。有时候,我觉得 Windows 的,匈牙利命名法反而更容易理解代码。
这里既然提到前端(客户端)开发和后端开发,这里不得不提一下,这二者没有优劣之分。其侧重点和开发思维是不一样的,前端(客户端)开发一般有较多的界面逻辑,它们是直接与用户打交道的,因而一款客户端软件的好坏很大程度上取决于其界面的易用性和流畅性,开发者只要把这一端的“一亩三分地”给管理好即可;而后端服务,对于普通用户是透明的,开发者的程序必须尽量体现“服务”这个字眼,即更有效地为更多的客户端服务,这就要求兼顾请求响应的正确性、及时性和流畅性,由于服务软件也是运行在某台物理机器上的程序,鉴于 CPU、内存、网络带宽资源有限,而服务程序一般是长周期运行的,因此必须合理的分配和使用资源(如尽量回收不再使用的各种资源),开发者应从全局考虑,不能在某个“客户端”这一棵树上“吊死”。
从个人的职业发展来看,建议从事客户端开发的读者适当地了解一下服务器开发的思路,反过来也建议从事后端开发去学习一下客户端开发,二者相得益彰。从个人的技术提高来说,也是很有帮助的。例如您要学习一套开源的软件代码,如果您熟悉客户端和服务器的基本开发和调试技巧,您可以更好地学习它。而在工作上,一个项目,往往是由客户端和服务器程序组成,如果您都熟悉,您可以站在一个更高的角度去审视它、规划它,这也是架构师的基本要求之一。
最后就是很多读者关心的客户端和服务器的薪资问题,这个没有绝对的谁高谁低,因人而异,因能力而异,因岗位而异。
如何看待 C++ 11/14/17 新标准
C++ 开发者有个不成文的规定就是,即使您对 C++ 很熟悉,也不要在简历上写上您精通 C++,原因很简单—— C++ 这门语言包含的东西实在太多了,没有人能真正“精通”所有。C++ 既支持面向对象设计(OOP),也支持以模板语法为代表的泛型编程(GP)。而且新的 C++ 标准和遵循 C++ 新标准的编译器也参出不穷,这些年,C++ 变化越来越大,越来越快,从最初业界和开发者翘首以盼的 C++11 标准,历经 C++14、C++17 到今天的 C++20,这门语言与之前的版本差别越来越大,更多原来需要使用第三库的功能也被陆续添加到 C++ 标准库中。以致于C++之父 Bjarne Stroustrup 也开始对这门语言表示担忧:
在 C++11 开始的基础建设尚未完成,而 C++17 基本没有在使基础更加稳固、规范和完整方面做出改善。相反,却增加了重要接口的复杂度(原文为 surface complexity,直译“表面复杂度”),让人们需要学习的特性数量越来越多。C++ 可能在这种不成熟的提议的重压之下崩溃。我们不应该花费大量的时间为专家级用户们(比如我们自己)去创建越来越复杂的东西。(还要考虑普通用户的学习曲线,越复杂的东西越不易普及。)
当然,我们不用有这种担忧,毕竟我们既不是 C++ 标准委员会成员,也不是 C++ 编译器开发厂商。就我个人经验来说,对于 C++11、C++14、C++17 乃至 C++20,我们学习它们的准则应该是以实用为主,也就是说我们应该学习其实用的部分,至于新标准提到的一些高级特性和各种复杂的模板,我们大可不必去了解。我们并不是做学术研究,我们学习 C++ 是为了投入实际的生产开发,所以应该去学习 C++ 新标准中实用的语法和工具库。关于C++11常用一些知识点,这里也简单地给读者列举一下:
auto 关键字、for-each 循环、右值及移动构造函数 + std::forward + std::move + stl 容器新增的 emplace_back() 方法、std::thread 库、std::chrono 库、智能指针系列(std::shared_ptr/std::unique_ptr/std::weak_ptr)(智能指针的实现原理一定要知道,最好是自己实现过)、线程库 std::thread + 线程同步技术库
std::mutex/std::condition_variable/std::lock_guard 等、lamda 表达式(JAVA 中现在也常常考察 lamda 表达式的作用)、std::bind/std::function 库、其他的就是一些关键字的用法(override、final、delete),还有就是一些细节如可以像 JAVA 一样在类成员变量定义处给出初始化值。
C++语言基础与进阶
基础
这里说的基础不是狭义上的 C++ 语言基础,而是包括 C++ 开发这一生态体系的基础,笔者认为的基础有:
1.C++ 语言本身熟练使用程度
2.前面也介绍了单纯的 C++ 您啥也干不了,您必须结合一个具体的操作系统平台,所以您得熟悉某个操作系统平台的 API 函数,比如 Linux,以及该操作系统的原理。这里说的操作系统的原理不局限于您在操作系统原理图书上看的知识,而是实实在在与系统 API 关联起来的,如熟练使用各种进程与线程函数、多线程资源同步函数、文件操作函数、系统时间函数、窗口自绘与操作函数(这点针对 Windows)、内存分配与管理函数、PE 或 ELF 文件的编译、链接原理等等。
3.网络通信,网络通信在这里具体一点就是 socket 编程。这里的 socket 编程不仅要求熟练使用各种网络 API 函数,还要求理解和灵活运用像三次握手四次挥手等各种基础网络通信协议与原理。关于 socket 编程实践,《TCP/IP网络编程》这本书是非常好的入门教材。
说了这么多,您可能会觉得很抽象。笔者在这里举个具体例子,假设我们现在要开发一个类似电驴这样的软件。软件界面如下图:
如上图所示,假设我们的操作系统选择 Windows,使用语言我们使用 C++,这就要求您必须熟悉 C++ 常用的语法,如果您还不熟悉,您就需要补充这方面的知识。
在熟悉 C++语法的前提下,从这款产品实现技术来看,我们的目标产品分为 UI 和网络通信部分。下面将详细介绍这两部分:
UI 部分
对于 UI 部分,我们的认识是这需要使用 Windows 的窗口技术。我们可以直接使用原生的 Win 32 API 来制作自己的界面库,也可以选择一些我们熟悉的界面框架,如 mfc,wtl、duilib、wxWidgets 等。无论您是在阅读别人的这样的项目还是需要自己开发这样的项目,在确定了这款软件使用的 UI 库(或者使用原生 Win 32 API),您就需要对 Windows 的窗口、对话框、消息产生、派发与处理机制需要了解,同样的道理,如果不熟悉您需要补充相关的知识(关于这一点,下文不再赘述)。
接着,根据上图中的软件功能,大致分为三大模块,即资源、下载和分享。这三大块是可以使用一个 Windows Tab 控件去组织,这个时候您需要了解 Windows Tab 控件的特性。
1.对于资源模块,本质上是一个窗口中嵌入了一个浏览器控件(WebBrowser 控件),那么您需要了解这一个功能点的相关知识。当用户点击了某个列表中某个具体的资源,可以对齐进行下载。这就又涉及到 WebBrowser 控件与 C++ 宿主程序的交互了,那么如何实现呢?可以选择使用 ActiveX 技术,也可以使用 javascript 与 C++交互技术。
2.再来看下载模块,当产生一个下载操作时,界面上会产生以下下载列表,每个列表项会实时显示下载进度、下载速率等参数,同时正在下载的项目也可以被暂停、停止和删除。那么这又设计到 ListView 控件的相关功能,以及 ListView 如何与后台网络通信逻辑交互。
3.分享模块是将本地资源分享到服务器或者给其他用户。界面左侧是对文件系统的一个快照,那么这又涉及到如何遍历文件系统(了解枚举文件系统的 API),右侧也是一个 ListView 控件,这里不再赘述。
网络通信部分
网络通信部分,主要有两大块,第一个是程序启动时,与服务端的交互;第二个就是文件下载与分享的 P2P 网络。您在阅读或开发的过程中,如果对这些技术比较陌生,您需要补充这些知识,具体的也就是 socket 的各种 API 函数,以及基于这些 API 逻辑的组合。当然可能也会用到操作系统平台所特有的网络 API 函数,如 WSAAsyncSelect 网络模型。
再一点,网络通信部分如何与 UI 部分进行数据交换,是使用队列?全局变量?或者相应的 Windows 操作平台提供的特殊通信技术,如 PostMessage 函数、管道?如果使用队列,多线程之间如何保持资源的一致性和解决资源竞态,使用 Event、CriticalSection、Mutex、Semaphore 等等?
当然,笔者这里只列举了这个软件的主干部分,还有许多方方面面的细节需要考虑。这就需要读者根据自己的情况,斟酌和筛选了。您想达到什么目的,您就去学习和研究相关的代码。
总结起来,可以得到如下公式:
一款C++软件 = C++语法 + 操作系统API函数调用
进阶
如果您达到了我上面说的三点后,可以再找一些高质量的开源的项目去实战一下。需要注意的是最好找一些没有复杂业务或者您熟悉其业务的开源项目(如开源的 IM 系统),如果你不熟悉其业务,不仅要学习其业务(软件功能),还需要再去学习它的源码,最后可能让我们迷失了最初学习这款软件的目的。
学习这些项目的同时,读者应该先确定自己的学习目的,如果您的目的是学习和借鉴这款软件的架构,那么先从整体去把握,不要一开始就迷失在细枝末节中,这类我称之为“粗读”;或者,您的目的是学习下开源软件的在一些细节上的处理与做法,这个时候,您可以针对性地去阅读您感兴趣的模块,深入到每一行代码上去。
学习开源软件存在一种风气,许多新手喜欢道听途说,一听别人说这个软件不好,那个软件存在某某瑕疵就放弃阅读它的打算了。然后到了实际开发中,因为心中没有任何已有软件开发问题的解决方案,产生挫败感,久而久之就对本来喜欢的 C/C++ 开发失去了兴趣。学习的过程是先接触,再熟悉,再模仿,再创造。不管什么开源项目,在您心中没有任何思路或者解决方案时,您应该先接触熟悉,不断模仿,做到至少心中有一套对于某场景的解决方案,然后再来谈创新谈批判、改造别人的项目。
我个人学习一套陌生的开源项目时,总是喜欢将程序用调试器正常跑起来,然后再中断下来,统计当前的线程数目,然后通过程序入口 main 函数从主线程追踪其他工作线程是如何创建的;接着,分析和研究每个线程的用途以及线程之间交互的,这就是整体把握,接着找我感兴趣的细节去学习。
这里我以学习 redis 为例,将 redis 源码从官网下载下来以后,使用您喜欢的代码阅读器管理起来,我这里使用的是 Visual Studio,如下图所示:
在大致了解了 redis 有哪些代码模块以后,我们把代码拷贝到 linux 平台,然后编译并使用 gdb 调试器跑起来。如下图所示:
然后使用按 ctrl + C 将gdb中断下来,输入info threads查看当前程序的所有线程:
接着挨个使用 thread + 线程编号 和 bt 命令去查看每个线程的上下文调用堆栈:
然后对照每个线程的上下文堆栈,搞清楚其逻辑,并结合主线程,看看每个线程是在何时启动、端口在何时启动侦听的等等。等做完这一步,关于 redis-server 的框架也基本清楚了。
端口在何时启动侦听的等等。等做完这一步,关于 redis-server 的框架也基本清楚了。
最后,如果对 redis-server 源码中各种数据结构和细节感兴趣,我们可以进一步深入到具体的代码细节。
当然,不熟悉 gdb 的读者看笔者这段操作流程比较困难,这是正常的,说明如果想通过调试去研究 redis 这一款开源软件,您需要去补充一点 gdb 调试的知识。这就是我上文中所说的,针对性地补缺补差。
小结
关于 C/C++,暂且就讨论这么多。最后再强调一遍,C++ 是一门讲究深度的语言,其“深度”不是体现在会多少 C++ 语法,而是能够洞察您所写的 C++ 代码背后的系统原理,这是需要长期不断的积累的,没有速成之法。反过来一旦学成,可以快速地学习其他语言和框架。个人觉得,如果自主创业或者想在二三线城市长期发展的读者,C/C++ 应该是优选语言,有了它作为基础,您可以跳出依赖各种环境和框架的窠臼,快速地学习和开发您想要的软件,完成您想要的业务产品。
网友评论