前言
本系列也是慢更系列,主要收纳一些还不够单独成系列的C++的杂项问题,或是一些与C++有关,但不属于核心知识的一些旁系问题。
关于C++与C的关系
「学C++要先学C吗?」
「C++和C是不是完全不同的两个语言?」
「这个语法是C的还是C++的?」
这是一系列笔者被问到非常多次的问题,原本笔者认为这不是个什么大不了的问题,就是一个发展史嘛,聚焦到当前遇到的问题才是最重要的。但后来发现这件事纠结的人非常多,而且笔者观察到,不同的人对待这个问题的态度非常不一样,甚至还存在站队、引战的问题。
因此,笔者决定将这个问题收纳为C++杂谈系列的第一个问题,希望能像读者详细解释一下整件事情的来龙去脉,带大家了解一下二者的爱恨情仇。
首先,我们需要了解一下C++的诞生史。
C++的诞生
早在1972年,美国的贝尔实验室中,有一个大神名为Dennis Ritchie,我们就暂且称他为「D神」。D神当时的任务是要开发一款操作系统(OS, Operating System),而如果直接用汇编指令来开发OS那太痛苦了,所以D神对当时已有的一个语言动了心思,这门语言是B语言。
B语言虽然不像汇编那么「反人类」,但它还是比较基础,所以,D神就决定为B语言扩充一些特性(例如变量、函数、指针等),并且删除了一些不需要的特性,让它更适合去开发OS。D神觉得,这门语言既然是从B语言发展来的,就给它取了B的下一个字母,叫做「C语言」(还有一种说法是取自BCPL, Basic Combined Programming Language中,B的下一个字母C,当然这个可能无从考证,毕竟C语言的下一代没有叫D语言或者P语言)。
1973年,D神又对C语言进行了一些扩充,不过那时C语言就是实验室里单纯用来开发OS的,那个OS才是他们的主要目标。
1978年,C语言的有一个版本发布,它更加标准化,并且成为了独立的开发语言(而并不单纯是OS开发的附属品了),这一标准我们称之为「Standard C」。同年,D神和他的小伙伴们开发的OS也问世了,这个OS被命名为「Unix」。
到了1979年,有另一位大神,叫Bjame Stroustrup,我们姑且称他「B神」。B神来到了贝尔实验室,自然是要学习C语言的。与此同时,这位B神他希望能给C语言提供一些面向对象(OOP, Object Oriented Programming)的语法能力,于是他为C语言带来了「类」的语法,并且把这种扩充后的C语言叫做「C with class」,也就是带类的C。
1983年,贝尔实验室的另一个大神名为Rick Mascitti(我们称他R神),建议将B神研究的这个带类的C,叫做「C++」,也就是给C自增了一下的意义。R神的意见被大家接受,从而这个语言正式命名为「C++」。
1989年,ANSI(American National Standards Institute,美国国家标准学会)和ISO(International Standards Organization,国际标准化组织)成立了一个联合部门,用于标准化C++。不过标准化的进程可谓是一波三折,中间出了各种各样的问题,大家可以脑补一下~
直到1994年,这个联合部门发表了第一个C++标准化草案。但同年,另一件事情让C++标准化的发布又受阻了。有一个大神,名为Alexander Stepanov,我们叫他A神。A神利用C++模板,编写了一套几乎纯编译期的代码库。这套模板库的出现是史无前例的,正好C++还并没有官方的模板库(只有一些从C继承来的运行时函数库,连类都没封装的那种),所以那时候有人提议,把这一套模板库也纳入C++标准库的一部分。那自然,又要进行各种审理、改善的工作。后来这套模板库被命名为STL(Standard Template Library,标准模板库)。
这样一直到1998年,C++的第一套标准才问世,也是我们现在俗称的C++98标准。
C++其实就是新版C
故事听完了,相信大家应该能发现,其实一开始C++就没想成为一门新的语言,而是为了去扩展C语言的。换句话说,C++其实就是C语言的2.0版,C++才是C的自然进化。只不过由于C++扩充的内容非常多,对于一部分人来说,原本的C就够用了,于是,原始C又被分离出了一条分支,单独维护了。
用一个不是特别恰当的例子来比喻,C语言就相当于Windows 10,C++就相当于Windows 11。而Win11就是新版Windows,是Win10的下一个版本,只不过由于很多人还不能立刻切到Win11,所以还在继续使用Win10。那么这个时候微软也要按照自己的约定,在2025年之前,继续维护Win10。
所以C和C++的关系跟这个很像,它们之间的关系非常紧密,这跟其他C系扩展语言(例如Java、C#等)是有着本质区别的。
所以,我们应当把C++理解为C语言的一个分支,而现行C语言就是这个项目的master分支。这也就意味着,如果master分支上修复了什么bug、制定了什么新标准等等,其他分支是要跟进的(类比分支对master的fetch动作)。事实上确实是这样,C++的每一个标准都是包含了前一个C分支的各种规范的,例如说C++20标准就包括了C18标准,详细的关系请看下图:
就像C语言在C18标准上定义的所有东西,C++20里都是囊括在内的。因此,C++从一开始的诞生,就决定了它不可能脱离C语言独立存在。
C++的标准库并不等价于STL
前面的故事也提到了STL,一开始,C++是直接拿着C的标准库来用的,但是由于引入了命名空间、重载函数等特性,C++对一部分C标准库进行了扩充和改造。举例来说,在math.h
中,定义了下面这几个求绝对值的函数:
int abs(int);
long labs(long);
double fabs(double);
然而有了C++的重载函数特性,我们就没必要用前缀来区分了,直接用参数类型来区分就好。因此,在C++标准库中,对它们进行了改造,并定义在了cmath
头文件中:
#include <math.h>
namespace std {
int abs(int);
long abs(long);
double abs(double);
}
类似的改造还有很多,因此,上面这些改造后的,以c
开头头文件的这些,才是真正意义上原始的C++标准库。
当引入了STL后,STL所提供的所有能力也被纳入了标准库当中,因此,实际上C++的标准库并不单纯是STL,还包括扩充后的C标准库。只不过随着后来C++新标准的不断完善和扩充,二者之间的界限逐渐模糊,大家也就不再区分了。
所以说,狭义的STL一开始就是个第三方库,只不过被标准委员会看上了,将其纳入了C++标准库的一部分而已。而现在普遍大家口中的STL,是广义的STL,它指的就是C++标准库。
只不过,狭义的STL跟标准库有一个非常本质的区别,就是自闭环性。标准库里面的很多功能、接口是需要操作系统来提供的(例如标准输出stdout
),如果真的来实现这些功能,需要用到汇编,或者操作系统API。但狭义的STL是一个彻头彻尾的模板库,完全由C++语言编写。
换句话说,你不能只用C来实现C的标准库(同理,不能只用C++来实现原始C++标准库),但你却可以只用C++来实现STL(狭义的)。
再来回答那几个灵魂问题
学C++之前要先学C吗?
要!必须要!因为C++就是C来的,它包含了C的语法语义、内存管理模式、构建方式等等几乎所有的内容。所以C是C++的根基,要学C++肯定要会C,毋庸置疑。
我知道有一些资料上会说,没必要先学C,直接学C++即可。从学习方法上来说,你可以不用说,我第一只段只看纯C的,学会以后第二阶段再来看C++;而是说我直接系统化来攻克C++的内容。但是,就结果来说,如果有一天你把C++学完了,你就会发现,其实C的东西你也都学过一遍了。
所以,这个问题的答案是:想学C++的话,可以不用先刻意去学纯C,但是你不可能跳过C。或者换个说法,可以不用「先学C」,但是你不能「不学C」。
而笔者个人的建议,还是先学一下纯C,毕竟它内容少,更容易掌握。同时,C对C++来说是基础,先把基础打牢,一定是有好处的。上手直接学C++真的很容易劝退!因为C++真的很复杂~~
C++和C是不是完全不同的两个语言?
如果你是出于好奇,问出「C和C++是不是两种语言」的话,前面篇幅已经把C和C++的关系解释得很清楚了,C++不会脱离C的,所以,C语言的那一套理论、方法一定可以在C++上使用,只不过是推荐还是不推荐的区别罢了。
但我知道,这个问题更多场景是在引战,或是说,一些C++的爱好者去鄙视C语言的方式,有或是一些传统C语言的拥护者对C++的排斥、吐槽的方式。
在实际生产开发中,我们可能会根据项目需要,只去使用C++的一个子集罢了。然而在C++使用者里其实是有一个「鄙视链」的存在。笔者以前曾经也经历过,就是当我掌握了很多C++提供的上层工具以后,我就会去鄙视那些只用基础语法的人,嘲笑他们「用C++在写C代码」。举例来说,当我知道了std::string
的存在以后,我就很鄙视那些到处写char *
的人。当我知道了std::shared_ptr
的存在以后,我就很鄙视那些到处new
和delete
的人。
但后来我发现,陷入这种鄙视链中,靠向下鄙视来保持自己的优越感,这是一件蠢到爆的事情!首先,大家的需求不同,因此衡量的标准自然也不同。比如说,我给一个写嵌入式内核的人去安利工厂模式有多么多么先进,那估计我会被打。他们在意的就是性能以及代码的通透性,多余的分层会大幅降低可用性。他们用C++而不用C,可能单纯就是为了「用引用代替指针」「把函数封装在结构体中」「封装一些初始化操作在构造函数中」等等这些非常简单的目的,但更多关注的就是底层交互,所以他们用C++在写C风格的代码这就是一件非常非常正常的事情!这时候如果我拿着我的OOP设计模式、SFINAE法则、STL工具等这些技能去鄙视他们,全然不顾人家对底层交互、程序性能、通透性等的需求就吐槽人家,那我会觉得我自己才是真的蠢……
另一方面,C++提供了这些高级语义、高度封装的工具等,使用起来其实并不难,毕竟人家都做好高层封装了,就是为了让你用着舒服的。所以会用这些工具,本身不是什么值得骄傲的事,更重要的是,我们能不能搞明白它们是怎么设计的,怎么实现的,如果出了问题应该怎么解决。只有这样,我们才能真正发挥出C++的优势,能上能下。
这个语法是C的还是C++的?
同理,如果真的是出于好奇,那么你可以把这个语法在纯C中写一下,看看能不能编译通过,是否在纯C下支持就可以很轻松地判断出它是不是C语法了。
也同理,请大家不要排斥「在C++中写C语法」这件事,或者觉得这样做很LOW之类的。首先明确自己的需求,要的是高性能、高度可控,还是说要高可读性、高层语义等。如果有一个工具(比如说STL提供的类、函数,或者第三方库提供的)本身就很符合你的需求,那么直接使用自然是无可厚非的。但如果没有,或者说,你认为自己手动实现的才更符合自己的需求,那么就去搞吧!一定会用到底层的,或者说更「偏C」的语法,不要排斥它,C语法本身就是C++语法的子集,没必要硬纠结和站队。自己人何苦为难自己人!
关于开发环境
关于IDE
「到底哪个IDE好?」
「用vim是不是才是专业的?」
「VSCode该遭鄙视吗?」
关于这些问题,笔者还是那句话!适合自己的才是最好的,何必在意别人怎么说呢……
不过有关IDE,笔者的建议是,IDE固然好,因为可以帮我们提高效率,但是,你要清楚IDE做了些什么,假如没有IDE的话,你能不能手动把这些东西搞出来。
举例来说,我们给IDE的工程项目里添加了一个cpp文件,然后再点击「build」,为什么这个文件就自动被编译链接进去了?这就说明,IDE中添加一个新文件,并不仅仅是添加一个文件这么简单,它还把这个文件加到了对应项目工程的配置列表中。这样在构建项目的时候,就会进行编译,并参与链接。
再比如说,我通过系统工具(比如yum、apt、brew等)安装的一些第三方库,为什么安装好以后,直接include
它的头文件就可以构建成功?IDE做了什么事?它是去哪里扫描头文件的(默认的-I
参数是什么)?又是去哪里扫描链接库的(默认的-L
参数是什么,-l
参数是什么)?这些如果没有IDE的帮忙,我们还能正确构建吗?
如果你清楚了所有IDE为你做的事情,并且也能不靠IDE,完全自己搞出来的话,这时,你再去选择适合你的IDE来帮忙提效,这当然是无可厚非的事。但如果你并不知道这些操作是什么,只依靠IDE的话,那我认为你其实还是处于一知半解的状态,这个阶段并不适合去纠结哪个IDE更好。
关于操作系统
「学C++,买Mac还是Windows PC?」
「开发C++一定要用Linux吗?」
「不会Linux命令能不能学C++?不会DOS命令能不能学C++?」
首先我们要搞清两件事:
- 开发环境≠构建环境≠运行环境
- 影响构建方式的主要是系统架构,而非操作系统
先来解释第一点。「开发环境」指的是代码在哪里写,你输出的结果应该是一组代码。所以,这个阶段只与编辑器有关,我们选一些有高亮的、有自动补充的、有静态检测的环境有助于我们提效。那么这个阶段其实跟用什么OS没关系,更多依赖于编辑器。如果你直接用记事本(或者vim)就能写代码,那真的随意了,基本是电脑就行(emmm…其实不是电脑都行,比如iPad…)。
「构建环境」指的是,代码在哪里进行编译、链接。这一阶段主要依赖编译期,gcc、clang、msvc等行为都不同。编译环境的不同决定了编译出的「程序」不同。
「运行环境」指的是,上一阶段的程序在哪里运行,这里主要依赖OS,OS决定了程序加载为「进程」的方式,以及执行进程的方式。
所以这3个环境是可以分离的,并不一定要在同一台设备上运行。假如说我们做后台开发,那么运行环境一定在服务器上,这时候服务器的OS才是主要考虑的。如果说服务器用的是Linux,那么我们就得构建成Linux上可以运行的程序。
但并不是说,只有Linux系统才能构建Linux程序,我们用macOS、甚至Windows作为构建环境时,也可以构建成Linux上可执行的程序,所以构建环境上,只要编译期、环境变量和其他构建相关的工具配置妥当,就可以进行构建,本身与OS也无关。
所以,开发环境无需考虑OS,更多的应该是考虑开发者的熟悉程度、编辑器的支持、编辑的插件支持情况等。
再来解释第二点。刚才我们提到了运行环境的OS和构建环境的配置情况,而这里OS其实是次要条件,主要考虑的是系统架构(也就是微指令集)。我们知道构建环境是负责把源代码转换成机器指令的,那么转换为哪种机器指令,就取决于执行环境的系统架构。例如「AMD-64」「PowerPC」「Aarch-64」等。
目前服务器端使用AMD-64架构的居多,也有部分使用PowerPC架构的。而桌面端则是AMD-64架构为主,少部分使用Aarch-64架构的。这里面最经典的就是Mac,在2006年以前,Mac使用的事PowerPC架构,在2006年换为Intel芯片后,变为了AMD-64架构(注意,这里的AMD-64也叫Intel64或x86-64或x64。而x86架构指的是IA32架构,只是由于IA32架构的机器几乎被淘汰差不多了,因此现在很多人说「x86架构」其实指的是AMD-64架构,而并不是IA32架构)。
所以,如果你要把Mac当做构建环境的话,一定要考虑运行环境的架构,如果运行环境是AMD-64架构的,而构建环境是搭载了苹果自研芯片的Mac,那么则无法使用,因为苹果自研芯片属于Aarch-64架构。但如果是是Intel芯片的Mac则可以使用,因为它是AMD-64架构的。(使用交叉编译工具是可以解决一部分跨架构问题,但它并不好用,而且无法正确生成可执行文件,还是需要在目标架构上进行一次构建的。)
另外不仅仅是Mac,市面上也有一些搭载了骁龙8cx芯片的PC,它们虽然是Windows系统,但其实仍然是Aarch-64架构的。
所以总结一下OS的问题:开发环境不用纠结,你习惯哪个就用哪个,你喜欢的工具哪个支持就用哪个。构建环境要跟运行环境架构匹配即可,其余的,就纠结纠结性能(会影响构建速度)和成本吧。
而至于Linux命令、DOS命令这些,直接参与C++代码构建的与这些无关,它们只是启动器罢了。对于一个已经安装好C++构建工具的Linux或DOS,用的都是构建工具的指令,这已经不属于Linux/DOS指令的范围了。要说唯一的区别,应该就是安装的区别,因为你要在Linux上安装gcc/clang,肯定是需要用一些Linux命令的;同理,要在Windows上安装gcc/clang,肯定也是需要用一些DOS命令(或PowerShell命令)的。
所以结论就是,一点都不会肯定不行,但是不用太精通,只要能正确安装并使用构建工具即可。