本立道生:必备的基础知识

news2024/11/24 17:32:58

通过前面两节课的内容,我带领大家熟悉了一下 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++ 编译流程

如果看过 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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/154504.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】5.5 遍历二叉树和线索二叉树

5.5.1 遍历二叉树 遍历定义 顺着某一条搜索路径巡访二叉树中的每个结点,使得每个结点均被访问依次,而且仅被访问一次(又称周游)。访问的含义很广,可以是对结点作各种处理,如:输出结点的信息&a…

Centos7开启SSH连接配置

1、查看是否已安装openssh-server: [rootlocalhost ~]# yum list installed | grep openssh-server 如果有信息说明已安装了openssh-server,如果输出没有任何结果,说明没有安装。 2、安装openssh-server(如果已安装&#xff0c…

微信小程序(学习笔记篇)

基本项目结构 pages用来存放所有小程序的页面utils 用来存放工具性质的模块(例如:格式化时间的自定义模块)app.js小程序项目的入口文件app.json 小程序项目的全局配置文件app.wXss小程序项目的全局样式文件project.config.json项目的配置文件sitemap.json用来配置小…

买卖股票的最佳时机 II -数学推导证明贪心思路 -leetcode122

问题说明来源leetcode 一、问题描述: 122. 买卖股票的最佳时机 II 难度中等1941 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。 在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可…

Spark Core----RDD详解

为什么需要RDD 分布式计算需要: 分区控制(多台机器并行计算,将一份数据分成多份,在不同机器上执行)Shuffle控制(不同分区数据肯定需要进行相关的关联,不同分区进行数据传输叫Shuffle控制&…

分享77个NET源码,总有一款适合您

NET源码 分享77个NET源码,总有一款适合您 NET源码下载链接:https://pan.baidu.com/s/1vhXwExVAye5YrB77Vxif8Q?pwdzktx 提取码:zktx 下面是文件的名字,我放了一些图片,文章里不是所有的图主要是放不下...&#xf…

Html 3D旋转相册制作

程序示例精选 Html 3D旋转相册制作 如需安装运行环境或远程调试&#xff0c;见文章底部微信名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Html 3D旋转相册制作>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习…

zabbix监控主机

zabbix官网 zabbix分为zabbix server&#xff08;zabbix服务端&#xff0c;用来展示监控的&#xff09;和zabbix-agent&#xff08;zabbix客户端用来收集数据的&#xff09; zabbix-agent客户端有两种工作模式&#xff0c;被动模式&#xff08;由zabbix服务来采集数据&#xff…

二十二、Kubernetes中Pod调度第四篇污点(容忍)调度详解、实例

1、概述 在默认情况下&#xff0c;一个Pod在哪个Node节点上运行&#xff0c;是由Scheduler组件采用相应的算法计算出来的&#xff0c;这个过程是不受人工控制的。但是在实际使用中&#xff0c;这并不满足的需求&#xff0c;因为很多情况下&#xff0c;我们想控制某些Pod到达某…

魔方爱好者快来康康,困难的平面魔方来了!

前言和效果图我今天看到一个网站&#xff0c;就是关于魔方的&#xff0c;里面二阶魔方引起了我的兴趣。https://rubiks-cube-solver.com/2x2/进去后你们可以看到&#xff0c;二阶魔方的平面展开图&#xff0c;复原也更加困难。虽然是英文的&#xff0c;但我还是玩得不亦乐乎。好…

查看GPU使用情况和设置CUDA_VISIBLE_DEVICES

文章目录一、简介二、查看GPU状态和信息三、使用3.1临时设置&#xff08;临时设置方法一定要在第一次使用 cuda 之前进行设置&#xff09;3.2python 运行时设置3.3永久设置四、参考资料一、简介 服务器中有多个GPU&#xff0c;选择特定的GPU运行程序可在程序运行命令前使用&am…

企业舆情监控排查什么,TOOM讲解企业舆情监控工作方案?

互联网时代&#xff0c;企业舆情发生因素很多&#xff0c;如果不能及时监控解决&#xff0c;就会引发无限舆情发展&#xff0c;进而影响到企业品牌声誉&#xff0c;引发企业信用危机&#xff0c;所以就需要做好企业舆情监控&#xff0c;接下来我们简单了解企业舆情监控排查什么…

CMMI的五个级别及其特征简述

CMMI 一共分五个级别&#xff0c;一级最低&#xff0c;五级最高&#xff0c;一般企业初次认证CMMI从三级开始。 1、CMMI一级&#xff0c;完成级。在完成级水平上&#xff0c;企业对项目的目标与要做的努力很清晰。项目的目标得以实现。一般来说&#xff0c;公司的初始阶段就…

【C进阶】指针笔试题汇总

家人们欢迎来到小姜的世界&#xff0c;<<点此>>传送门 这里有详细的关于C/C/Linux等的解析课程&#xff0c;家人们赶紧冲鸭&#xff01;&#xff01;&#xff01; 指针笔试题前言一、题1&#xff08;一&#xff09;题目&#xff08;二&#xff09;答案及解析&#…

【Python】函数——模块与函数的导入

概述 为了方便使用函数&#xff0c;我们可以将函数存储在称为模块的独立文件中&#xff0c;再将模块导入到主程序中&#xff0c;导入一个模块需要使用import语句。 导入整个模块 模块是扩展名为.py的文件 1、导入某个模块 语法为&#xff1a; import module_name 2、导入…

[oeasy]python0043_八进制_oct_octal_october_octave

八进制(oct) 回忆上次内容 什么是 转义&#xff1f; 转义转义 转化含义\ 是 转义字符\n、\r是 转义序列 还有什么 转义序列 吗&#xff1f; \a是 响铃\b 退格键\t 水平制表符 tab键\v、\f 实现喂纸不回车 通过 16进制数值 转义 \xhh输出 (hh)16进制对应的ascii字符 如果我们不…

Portainer使用docker compose搭建nacos并初始化MySQL、Portainer stack搭建nacos并初始化MySQL

Portainer使用docker compose搭建nacos初始化MySQL、Portainer stack搭建nacos初始化MySQL新建stack(堆栈)添加stack(堆栈)名称添加docker-compose规则配置环境变量上传初始化sql文件找初始化sql文件nacos初始化mysql-schema.sql文件内容上传sql文件到初始化挂载目录部署stack(…

打印机不打印故障简单排除方法

日常工作中经常会遇到打印机不能打印的情况&#xff0c;那么又没有专业的技术人员在场帮忙的情况下我们也可以自己动手简单的处理一下故障&#xff0c;可以尝试以下的方法进行简单的故障排除&#xff1b; 一、使打印机处于联机状态&#xff0c;如果打印机没有处于联机状态&…

医疗影像工具LEADTOOLS 入门教程: 检测和提取 MICR - 控制台 C#

LEADTOOLS是一个综合工具包的集合&#xff0c;用于将识别、文档、医疗、成像和多媒体技术整合到桌面、服务器、平板电脑、网络和移动解决方案中&#xff0c;是一项企业级文档自动化解决方案&#xff0c;有捕捉&#xff0c;OCR&#xff0c;OMR&#xff0c;表单识别和处理&#x…

基于STM32或STC的手势控制MP3语音播放器的设计

一. 系统设计框图 区别于传统设计中的按键开关控制&#xff0c;本设计可以实现通过手势控制MP3播放器。采用STM32或STC15单片机和PAJ7620手势模块&#xff0c;能够识别九种手势&#xff0c;分别为上下左右前后&#xff0c;顺时针&#xff0c;逆时针&#xff0c;挥动。在本设计…