本立道生:必备的基础知识-创新互联

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

目前累计服务客户上千余家,积累了丰富的产品开发及服务经验。以网站设计水平和技术实力,树立企业形象,为客户提供做网站、成都网站建设、网站策划、网页设计、网络营销、VI设计、网站改版、漏洞修补等服务。创新互联始终以务实、诚信为根本,不断创新和提高建站品质,通过对领先技术的掌握、对创意设计的研究、对客户形象的视觉传递、对应用系统的结合,为客户提供更好的一站式互联网解决方案,携手广大客户,共同发展进步。

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

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

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

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

C++ 编译流程

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

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

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

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

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

在上图中,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++ 编码之旅就会少一些疑惑,多一些勇往直前的勇气。

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

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


分享题目:本立道生:必备的基础知识-创新互联
链接分享:http://ybzwz.com/article/djiech.html