前端开发者的现代 C++ 课:必备的基础知识
创始人
2024-06-02 21:10:40
0

通过前面两节课的内容,我带领大家熟悉了一下 Visual Studio C++ 开发环境的必备知识,虽然还有很多关于 Visual Studio 的重要知识没有介绍,但为了让你尽快进入 C++ 开发环节,及早获得开发程序的愉悦,我们暂时只介绍这些必备知识。

C++ 创建至今已有近 40 年的历史,经历了数次重大的变革,本贾尼老爷子自己也说现在的 C++ 就像一门全新的语言。不过 C++ 终究是基于 C 发展而来的,时至今日 C++ 还是和 C 保持着最大的兼容性。

当前的软件世界还有很多项目都是基于 C 或者早期版本的 C++ 构建而来的,随着时间的发展它们已经成为了当今计算机软件世界的基础设施,比如 Windows 操作系统、 SDK 、SQLite 、FFmpeg 等。我们使用现代 C++ 开发软件项目往往要依赖这些基础设施,如果只使用现代 C++ 特性而不了解底层原理,那么可能无法很好地使用这些基础设施。

从本节开始,我将带领你学习现代 C++ 语言及相关知识。首先介绍 C++ 的编译流程,了解这些知识之后你就知道 C++ 的头文件和源码文件是如何被编译成二进制可执行文件的了。接着会讲解一个应用程序运行期的内存布局,了解了这些知识之后,你就知道在代码中的那些变量到底存储在用户内存的哪个区域了。

这些知识都是开发 C++ 应用程序的必备知识,掌握了这些知识再去学习 C++ 的语法就会更加从容。

前端开发者的现代 C++ 课 - 刘晓伦liulun - 掘金小册专门为前端开发者“定制”的现代 C++ 编程指南。「前端开发者的现代 C++ 课」由刘晓伦liulun撰写,709人购买https://s.juejin.cn/ds/SVKwBRj/

C++ 编译流程

如果看过 C++ 项目的代码,你就会发现,项目中分为两种文件:头文件(.h)源码文件(.cpp)。不像 JavaScript 项目,只有 js 文件,为什么 C++ 项目要把代码写在两种不同的文件中呢?

这里不讨论 .mm、.hpp 等扩展名的 C++ 文件。

这要从 C++ 的编译流程说起。在 C++ 项目中,头文件和源码文件往往是成对儿出现的,我们把一对儿 .h 和 .cpp 文件称为一个代码单元,源码文件通过#include指令引入与自己对应的头文件,头文件和源码文件也可以通过#include指令引用另一个代码单元的头文件,以获得另一个代码单元的能力。

无论是头文件还是源码文件,最终都会被编译到可执行文件中,整个过程如下图所示:

在上图中,Class2 的头文件引入了(#include) Class1 的头文件,Class2 的源文件引入了 Class3 的头文件。

在 Visual Studio 编译程序时,第一个环节:预处理环节,就是用来处理这些引用关系的,在这个环节 Visual Studio 会把 #include "Class1.h"这行的代码替换成Class1.h文件中的全部内容。

注意:Class1.h中往往只包含类型、方法、变量的声明而不包含实现逻辑。不过 C++ 规定只要给出类型、方法、变量的声明,就可以使用它们,所以这里的作用只是让 Class2 得到 Class1 在头文件中声明的内容,得到了这些声明之后,编译器就不会出现语法错误(比如:使用了未定义的变量或方法)。此时的 Class2 是不知道 Class1 的具体实现的。

预处理环节除了完成#include指令的替换工作之外,还完成了条件编译指令#if #elif #endif的处理工作、常量的替换工作等。

完成预处理环节的工作之后,Visual Studio 开始执行编译环节的工作,在这个环节编译器经过词法分析、语法分析、语义分析、代码优化、汇编等过程把各个类(或程序单元)编译成机器指令。

在这个过程中,如果你的程序有一些编译器能发现的错误,Visual Studio 则会提示编译异常。

最终生成的机器指令被存放在一系列的 .obj 文件中,你可以在[YourSolutionDir]\x64\Debug 目录下找到这些文件。

完成编译环节的工作之后,Visual Studio 开始执行链接环节的工作,链接器会把上一个环节生成的所有 obj 文件,还有标准库的 lib 文件、第三方库的 lib 文件链接到一起,最终生成可执行文件或动态链接库(.exe 文件或.dll 文件)。

只有链接工作执行完成之后,Class1 的实现逻辑才和 Class2 的实现逻辑绑定到一起,Class2 才可以真正地访问 Class1 的方法。

由此可见,C++ 使用头文件和源码文件一定程度上起到了隐藏实现细节、控制访问权限的目的。之所以 C++ 要在两个文件中完成这项工作,主要是为了适应配编译器的要求。

预处理指令 #pragma once

现代 C++ 头文件中往往会用到了一个预处理指令:#pragma once这个指令告诉预处理器这个文件只会被处理一次

比如,Class1.h 引入了 Class2.h 和 Class3.h ,而 Class3.h 的头文件也引入了 Class2.h 的头文件,如下图所示:

前面我们说了,#include 语句会被替换成被包含的文件,那么如果不加处理的话,最终 Class1.h 中将包含两份 Class2.h 的内容。

如果 Class2.h 中包含#pragma once这个预处理器指令,则在预处理器处理 Class1.h 的头文件时,则只会包含一次 Class2.h 的内容,不会因为 Class3.h 也包含 Class2.h 的引用就会在 Class1.h 中创建两份 Class2.h 的内容。

这是现代 C++ 编译器新增的一个指令,这个指令出现之前,C++ 开发者都是通过如下方式来保证头文件不会被重复编译的。

#ifndef _FileA
#define _FileA
// code
#endif

这种方式书写起来繁琐,编译时也低效,所以推荐使用#pragma once指令。一般情况下,C++ 头文件中都会使用和加入 #pragma once 指令。

应用程序内存布局

一个应用程序在操作系统中运行时,它占用的内存一般分为以下几个区域。

  • 内核空间:用于存储操作系统和驱动程序为进程提供的临时机器指令和中间变量。
  • 映射段:用于装载或映射动态链接库,也常用于将文件内容映射到内存中。
  • 代码段:用于存放应用程序的机器指令,为了防止指令被其他程序修改,代码段是只读的。
  • 数据段:用于存储全局变量、静态变量(static)和常量数据(const)。
  • :用于存储应用程序运行过程中申请的内存空间,比如使用 malloc 方法或 new 关键字申请的内存。
  • :用于存储函数的局部变量、参数、返回值及调用者的上下文信息。

如下图所示:

一个进程真正的内存使用情况并不像上图中描述的这样规整,每个区块的大小差异巨大,而且不同类别的内存可能会交叉出现,不同的内存区间也可能是不连续的、碎片化的。你应该关注数据段、堆和栈这三个内存区域,后文中我们还会反复提到这些概念。

JavaScript 的解释引擎 V8 也遵循这个内存布局约定,但 JavaScript 并不遵循这个约定,因为 JavaScript 是运行在 V8 之上的,由 V8 定义 JavaScript 的内存布局模型。

栈与栈帧

我们先来简单介绍一下栈与栈帧的用途,如下图所示:

当你的程序进入 main 方法后,程序将在空间中创建一个栈帧,变量 a 和 b 保存在这个栈帧中;当 main 方法调用 method1 时,程序将在空间中创建第二个栈帧,变量 c 和 d 保存在第二个栈帧中;当 method1 调用 method2 时会执行类似的工作,当 method2 方法返回时,method2 的栈帧会被销毁,栈帧上保存的变量也会被销毁。method1 返回时也会执行同样的销毁工作。

如果你在 method1 中调用 method2 时传递了方法参数,很多时候这些参数也会被拷贝到 method2 的栈帧中,在 method2 中修改这些参数,只是在修改 method2 栈帧上的参数副本,并不会影响 method1 中的对应变量。

栈帧空间切分成了很多片,每个方法享有独立的内存空间,除非专门的设置(后文会讲:引用参数),一个方法不会更改另一个方法的栈帧内存。

空间中分配内存的变量不需要程序员手动销毁,栈帧销毁时栈帧上的变量会被自动销毁。

栈的总内存大小是固定的,而且非常小,在 Windows 操作系统中默认大小为 1M,当在栈空间中申请的内存超过栈的剩余空间时,将提示内存溢出错误。这也是为什么开发者要关注递归调用引发栈溢出的原因(递归调用会创建非常多的栈帧)。

操作系统会专门分配寄存器存放栈的地址,入栈、出栈都有专门的指令执行,所以操作栈上的内存效率非常高。

如果想让某个变量在函数调用结束之后仍然可用,那么可以在空间中为变量分配内存,使用 new 操作符就可以完成这项工作,new 操作符返回堆空间的地址(就是指针)。除非专门的设置(后文会讲:智能指针),开发者必须自己完成堆内存的释放工作,使用 delete 关键字可以完成这项工作(C++ 中的 delete 关键字与 JavaScript 中的 delete 关键字差异巨大)。

堆内存的大小与计算机系统中有效虚拟内存大小有关,比栈空间要大得多,大部分时候开发者都会把大对象或数组放到堆中

堆内存没有专门的优化,使用效率较低,且容易产生内存碎片。

总结

本节我们首先介绍了 C++ 的编译流程,编译工具在预处理环节替换了所有的 #include 指令,编译工具在链接环节把各个编译单元的二进制代码链接到一起,这样 C++ 的头文件和源码文件就正式被编译成二进制可执行文件了。

接着我们讲解了应用程序的内存布局,其中最重要的是:数据段、堆和栈。数据段用于存储全局变量和静态变量;堆用于存储应用程序运行过程中使用 new 或 malloc 申请的内存空间;栈用于存储函数的局部变量、参数、返回值及调用者的上下文信息。

这些知识是 C++ 开发的基础知识,掌握了这些知识之后我们再开启 C++ 编码之旅就会少一些疑惑,多一些勇往直前的勇气。

相关内容

热门资讯

离开旧爱,想坐慢车,看透彻了心... 离开旧爱,想坐慢车,看透彻了心就会是晴朗的 是哪首歌?分手快乐,梁静茹的一首老歌分手快乐 梁静茹 我...
火影忍者决斗场什么忍者能卡进墙... 火影忍者决斗场什么忍者能卡进墙里波风水门。1、利用瞬身术进竖核入墙内:首先选信尘择一个合适的墙壁,然...
只有再爱一次,才能忘记前男友吗... 只有再爱一次,才能忘记前男友吗?你可以多和你的好朋友谈心啊,多结交一些朋友一样可以让你淡忘他,不一定...
女的穿越时空回到古代朝鲜的言情... 女的穿越时空回到古代朝鲜的言情小说古灵的《替身》
求一部动漫,很久以前看的,忘记... 求一部动漫,很久以前看的,忘记了名字。是关于一个少女变身用扑克牌行窃的故事。圣少女(提醒:变身后穿黑...
好听的歌 劲爆的 伤感的 忧伤... 好听的歌 劲爆的 伤感的 忧伤的 都行求要听的 欢快 忧伤的都行 只要好听 分享下呗Gee-少...
汤圆创作里面容易签约嘛?难度怎... 汤圆创作里面容易签约嘛?难度怎么样?是特别好看的文文才会成功嘛?什么地方,没听说过。
异界小说 男主被冰封了 然后有... 异界小说 男主被冰封了 然后有一帮来拍电影的女的在洗澡的时候发现了他! 好像是这样的就记得这些还有点...
选择词语填在横线上(快来帮帮我... 选择词语填在横线上(快来帮帮我吧)调整 整顿 矗立 伫立1、调整 整顿2、耸立 伫立调整 整顿矗立...
朴灿烈香水百瑞德,灿烈用的是百... 朴灿烈香水百瑞德,灿烈用的是百瑞德哪种香味的香水?朴灿烈喜欢用的百瑞德香水是银色山泉这款,这款香水是...
精神分析的学习与自我成长的联系... 精神分析的学习与自我成长的联系?心理学专业人士来答卡伦 霍尼《神经症与人的成长》,如果你是专业的,你...
怎么就没一首我喜欢听的歌 怎么就没一首我喜欢听的歌你最喜欢听的歌,听久了也会腻。心情不要那么浮躁,就会找到你喜欢的那首歌
如果把地球直线挖通的话,人跳进... 如果把地球直线挖通的话,人跳进去会摔死还是会掉到另一端呢?应该会直接摔死,因为地球中心才是非常复杂的...
和男朋友分手,他居然说谢谢我给... 和男朋友分手,他居然说谢谢我给他上了一堂人生课,什么意思?虽然你们分手了,但是你们一起度过了一段人生...
带口哨的纯音乐 带口哨的纯音乐曲名:The Voyage艺人:The Mountaineering Club Orc...
禾葡兰的禾善基金的理念是什么? 禾葡兰的禾善基金的理念是什么?禾善基金是有什么理念呢?禾善基金是禾葡兰创办的互助基金会,基金会的救助...
名侦探柯南给工藤新一的挑战书每... 名侦探柯南给工藤新一的挑战书每集开头那段音乐,有完整的吗,不是剧场和动画那个!那首歌是仓木麻衣的《T...
路漫漫其修远兮的下一句 路漫漫其修远兮的下一句路漫漫其修远兮,吾将上下而求索”这句出自屈原的名作《离骚》。“路漫漫其修远兮,...
海伦凯勒的背景 海伦凯勒的背景 海伦·凯勒(Helen Keller)(1880年6月27日-1968年6月1日...
激情燃烧的岁月实际上就是中国人... 激情燃烧的岁月实际上就是中国人焕发出巨大的劳动热情2.咱们工人有力量的歌曲,焕发的是现代人的激情。以...