01 堆、栈、RAII:C++里该如何管理资源?

今天我们就正式开启了 C++ 的学习之旅,作为第一讲,我想先带你把地基打牢。我们来学习一下内存管理的基本概念,大致的学习路径是:先讲堆和栈,然后讨论 C++ 的特色功能 RAII。掌握这些概念,是能够熟练运用 C++ 的基础。 基本概念 堆,英文是 heap,在内存管理的语境下,指的是动态分配内存的区域。这个堆跟数据结构里的堆不是一回事。这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏。 C++ 标准里一个相关概念是自由存储区,英文是 free store,特指使用 new 和 delete 来分配和释放内存的区域。一般而言,这是堆的一个子集: new 和 delete 操作的区域是 free store malloc 和 free 操作的区域是 heap 但 new 和 delete 通常底层使用 malloc 和 free 来实现,所以 free store 也是 heap。鉴于对其区分的实际意义并不大,在本专栏里,除非另有特殊说明,我会只使用堆这一术语。 栈,英文是 stack,在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”(last-in-first-out 或 LIFO)。 RAII,完整的英文是 Resource Acquisition Is Initialization,是 C++ 所特有的资源管理方式。有少量其他语言,如 D、Ada 和 Rust 也采纳了 RAII,但主流的编程语言中, C++ 是唯一一个依赖 RAII 来做资源管理的。 RAII 依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理。对 RAII 的使用,使得 C++ 不需要类似于 Java 那样的垃圾收集方法,也能有效地对内存进行管理。RAII 的存在,也是垃圾收集虽然理论上可以在 C++ 使用,但从来没有真正流行过的主要原因。...

March 13, 2024 · zlzong

01 导读:Hello~TypeScript

同学你好,我是Lison。很高兴你对TypeScript感兴趣,或许你对TypeScript了解还不多,或许还有很多疑问,比如: 学 TypeScript 是不是就不需要学 JavaScript 了? Vue 用 TypeScript 改写发布 3.0 后是不是不用 TypeScript 不行? TypeScript 靠谱吗? … 诸如此类疑惑,导致你一直对它犹豫不决,那么本节我将代替 TypeScript 向你做一个自我介绍。 同学你好,我是 TypeScript,如果你觉得我是 JavaScript 的孪生兄弟,或者觉得我是前端圈新扶持起来的太子,那你可能对我是有点误解了。其实我并不是一个新的语言,用大家公认的说法,我是JavaScript的超集,你可以理解为,我是加了一身装备铭文的进化版 JavaScript。JavaScript 有的,我都有,而且做得更好。JavaScript 没有的,我也有,而且我是在很长一段时间内不会被 JavaScript 赶上的。 虽然我作为超集,但是我始终紧跟 ECMAScript 标准,所以 ES6/7/8/9 等新语法标准我都是支持的,而且我还在语言层面上,对一些语法进行拓展。比如新增了枚举(Enum)这种在一些语言中常见的数据类型,对类(Class)实现了一些ES6标准中没有确定的语法标准等等。 如果你是一个追赶技术潮流的开发者,那你应该已经将 ES6/7/8/9 语法用于开发中了。但是要想让具有新特性的代码顺利运行在非现代浏览器,需要借助Babel这种编译工具,将代码转为ES3/5版本。而我,可以完全不用 Babel,就能将你的代码编译为指定版本标准的代码。这一点,我可以说和 JavaScript 打了个平手。 另外我的优势,想必你也略有耳闻了那就是我强大的类型系统。这也是为什么造世主给我起名TypeScript。如果你是一名前端开发者,或者使用过 JavaScript 进行开发,那么你应该知道,JavaScript 是在运行的时候,才能发现一些错误的,比如: 访问了一个对象没有的属性; 调用一个函数却少传了参数; 函数的返回值是个字符串你却把它当数值用了; … 这些问题在我这里都不算事。我强大的类型系统可以在你编写代码的时候,就检测出你的这些小粗心。先来简单看下我工作的样子: interface 定义的叫接口,它定义的是对结构的描述。下面的 info 使用 ES6 的新关键字 const 定义,通过 info: Info 指定 info 要实现 Info 这个结构,那 info 必须要包含 name 和 age 这两个字段。实际代码中却只有 name 字段,所以你可以看到 info 下面被红色波浪线标记了,说明它有问题。当你把鼠标放在 info 上时,VSCode 编辑器会做出如下提示: 如果上面这个小例子中你有很多概念都不了解,没关系,Lison 在后面的章节都会讲到。...

March 13, 2024 · zlzong

02 TypeScript应该怎么学

如果你看过了本专栏的大纲,那你应该会有一种,哇,官方文档里列出的知识基本都讲了,这个专栏太细了的感觉。这一个小节我会教给大家如何去自学TypeScript。虽然你在学习本专栏的时候,Lison会手把手的带着你学习TypeScript的语法和实战。但我还是想给你讲讲如何自学TypeScript,在授你以鱼之前也会授你以渔的,这样TypeScript即使更新了,你也能毫无压力地迎接它的新特性。好,接下来让我们开始吧。 1.2.1 学会看文档 英文官方文档始终是及时更新的。但即便是官方的文档,有一些更新在更新日志里写了,而新手指南里却没有及时同步更新,所以有时看指南也会遇到困惑,就是文档里写的和你实际验证的效果不一样。遇到这种问题,首先确定你使用的TypeScript版本,然后去更新日志里根据不同版本找对这部分知识的更新记录。如果找到了,看下这是在哪个版本做的升级;如果你不放心,可以把TypeScript版本降到这个版本之前的一个版本,再验证一下。 TypeScript 是有一个中文文档的,但是这个文档只是对英文文档的翻译。官方文档中的小疏漏,这个文档也没有做校验,而且更新是有点滞后的。在写本专栏的时候,TypeScript最新发布的版本为3.4,但是中文文档还是在3.1。所以想了解TypeScript的最新动态,还是要看英文官方文档的。不过我们还是要感谢提供中文文档的译者,这对于英文不是很好的开发者帮助还是很大的。 1.2.2 学会看报错 我们在前面的例子中展示了 TypeScript 在编写代码的时候如何对错误进行提示。后面我们讲到项目搭建的时候,会使用 TSLint 对代码风格进行规范校验,根据 TSLint 配置不同,提示效果也不同。如果我们配置当书写的代码不符合规范,使用 error 级别来提示时,会和 TypeScript 编译报错一样,在问题代码下面用红色波浪线标出,鼠标放上去会有错误提示。所有如果我们使用了TSLint,遇到报错的时候,首先要区分是 TSLint 报错还是 TS 报错,来看下如何区分: 上面这个报错可以从红色方框中看到,标识了 tslint,说明它是TSLint的报错。后面括号里标的是导致这条报错的规则名,规则可以在 tslint.json 文件里配置。关于 TSLint的使用,我们会在搭建开发环境一节讲解。示例中这条报错是因为 no-console 这个规则,也就是要求代码中不能有 console 语句,但是我们在开发时使用 console 来进行调试是很常见的,所以你可以通过配置 TSLint 关闭这条规则,这样就不会报错了。但我们应该遵守规范,当我们决定引入 TSLint 的时候,就说明这个项目对代码质量有更高的要求,我们不应该在书写代码遇到TSLint报错就修改规则,而是应该根据规则去修改代码。 上面这个报错可以从红色方框中看到,标识了 ts,说明它是 TypeScript 编译器报的错误。在我们书写代码的时候,通过强类型系统,编译器可以在这个阶段就检测到我们的一些错误。后面括号里跟着的 2322 是错误代码,所有的错误代码你可以在文档的错误信息列表中查看。不过你一般并不需要去看文档,因为这里都会给你标出这个错误码对应的错误提示,而且这个错误信息根据你的编辑器语言可以提示中文错误信息。很明显这个错误是因为我们给 name 指定了类型为 string字符串 类型,而赋给它的值是123数值类型。 上面两种是在编写代码的时候就会遇到的错误提示。还有一种就是和 JavaScript 一样的,在运行时的报错,这种错误需要在浏览器控制台查看。如果你调试的是 node 服务端项目,那你要在终端查看。来看这个例子: 当我在代码中打印一个没有定义的变量时,在书写代码的时候会做提示,且当程序运行起来时,在浏览器控制台也可以看到报错。你可以打开浏览器的开发者工具(Windows系统按F12,Mac系统按control+option+i),在 Console 栏看到错误提示: 红色语句即错误信息。下面红色at后面有个文件路径main.ts,蓝色框中圈出的也是个文件路径,表示这个错误出现在哪个文件。这里是出现在main.ts中,问号后面的cd49:12表示错误代码在12行,点击这个路径即可跳到一个该文件的浏览窗口: 在这里我们就能直接看到我们的错误代码被红色波浪线标记了,这样你修改起错误来就很明确知道是哪里出错了。 1.2.3 学会看声明文件 声明文件我们会在后面讲。我们知道原来没有 TypeScript 的时候,有很多的 JS 插件和 JS 库,如果使用 TypeScript 进行开发再使用这些 JS 编写的插件和库,就得不到类型提示等特性的支持了,所以 TypeScript 支持为 JS 库添加声明文件,以此来提供声明信息。我们使用 TypeScript 编写的库和插件编译后也是 JS 文件,所以在编译的时候可以选择生成声明文件,这样再发布,使用者就依然能得到 TypeScript 特性支持。一些 JS 库的作者已经使用 TypeScript 进行了重写,有些则是提供了声明文件,一些作者没有提供声明文件的,大部分库都有社区的人为他们补充了声明文件,如果使用了自身没有提供声明文件的库时,可以使用npm install @types/{模块名}来安装,或者运用我们后面讲到的知识自行为他们补充。...

March 13, 2024 · zlzong

02 自己动手,实现C++的智能指针

上一讲,我们描述了一个某种程度上可以当成智能指针用的类 shape_wrapper。使用那个智能指针,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性。这一讲我们就来进一步讲解,如何将 shape_wrapper 改造成一个完整的智能指针。你会看到,智能指针本质上并不神秘,其实就是 RAII 资源管理功能的自然展现而已。 在学完这一讲之后,你应该会对 C++ 的 unique_ptr 和 shared_ptr 的功能非常熟悉了。同时,如果你今后要创建类似的资源管理类,也不会是一件难事。 回顾 我们上一讲给出了下面这个类: class shape_wrapper { public: explicit shape_wrapper(shape* ptr = nullptr) : ptr_(ptr) {} ~shape_wrapper() { delete ptr_; } shape* get() const { return ptr_; } private: shape* ptr_; }; 这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。但它缺了点东西: 这个类只适用于 shape 类 该类对象的行为不够像指针 拷贝该类对象会引发程序行为异常 下面我们来逐一看一下怎么弥补这些问题。 模板化和易用性 要让这个类能够包装任意类型的指针,我们需要把它变成一个类模板。这实际上相当容易: template <typename T> class smart_ptr { public: explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) {} ~smart_ptr() { delete ptr_; } T* get() const { return ptr_; } private: T* ptr_; }; 和 shape_wrapper 比较一下,我们就是在开头增加模板声明 template <typename T>,然后把代码中的 shape 替换成模板参数 T 而已。这些修改非常简单自然吧?模板本质上并不是一个很复杂的概念。这个模板使用也很简单,把原来的 shape_wrapper 改成 smart_ptr<shape> 就行。...

March 13, 2024 · zlzong

03 右值和移动究竟解决了什么问题?

从上一讲智能指针开始,我们已经或多或少接触了移动语义。本讲我们就完整地讨论一下移动语义和相关的概念。移动语义是 C++11 里引入的一个重要概念;理解这个概念,是理解很多现代 C++ 里的优化的基础。 值分左右 我们常常会说,C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories): 我们先理解一下这些名词的字面含义: 一个 lvalue 是通常可以放在等号左边的表达式,左值 一个 rvalue 是通常只能放在等号右边的表达式,右值 一个 glvalue 是 generalized lvalue,广义左值 一个 xvalue 是 expiring lvalue,将亡值 一个 prvalue 是 pure rvalue,纯右值 还是有点晕,是吧?我们暂且抛开这些概念,只看其中两个:lvalue 和 prvalue。 左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有: 变量、函数或数据成员的名字 返回左值引用的表达式,如 ++x、x = 1、cout << ' ' 字符串字面量如 "hello world" 在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。 反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有: 返回非引用类型的表达式,如 x++、x + 1、make_shared<int>(42) 除字符串字面量之外的字面量,如 42、true 在 C++11 之前,右值可以绑定到常左值引用(const lvalue reference)的参数,如 const T&,但不可以绑定到非常左值引用(non-const lvalue reference),如 T&。从 C++11 开始,C++ 语言里多了一种引用类型——右值引用。右值引用的形式是 T&&,比左值引用多一个 & 符号。跟左值引用一样,我们可以使用 const 和 volatile 来进行修饰,但最常见的情况是,我们不会用 const 和 volatile 来修饰右值。本专栏就属于这种情况。...

March 13, 2024 · zlzong

03 VSCode揭秘和搭建开发环境

这节课我们要做的就是在砍柴之前先磨刀,学习如何借助VSCode愉快高效地开发TypeScript项目,我们来一步一步让VSCode对TypeScript的支持更强大。如果你已经习惯了使用别的编辑器,那你也可以自行搜索下,本节课提到的内容在你使用的编辑器是否有对应的替代品。 1.3.1 安装和基本配置 如果你还没有使用过VSCode,当然先要去官网下载了,下载安装我就不多说了,安装好之后,我们先来配置几个基本的插件。 (1)汉化 如果你英语不是很好,配置中文版界面是很有必要的,安装个插件就可以了。打开VSCode之后在编辑器左侧找到这个拓展按钮,点击,然后在搜索框内搜索关键字"Chinese",这里图中第一个插件就是。直接点击install安装,安装完成后重启VSCode即可。 (2)编辑器配置 有一些编辑器相关配置,需要在项目根目录下创建一个.vscode文件夹,然后在这个文件夹创建一个settings.json文件,编辑器的配置都放在这里,并且你还需要安装一个插件EditorConfig for VS Code这样配置才会生效。配置文件里我们来看几个简单而且使用的配置: { "tslint.configFile": "./tslint.json", "tslint.autoFixOnSave": true, "editor.formatOnSave": true } tslint.configFile用来指定tslint.json文件的路径,注意这里是相对根目录的; tslint.autoFixOnSave设置为true则每次保存的时候编辑器会自动根据我们的tslint配置对不符合规范的代码进行自动修改; tslint.formatOnSave设为true则编辑器会对格式在保存的时候进行整理。 (3)TypeScript相关插件 TSLint(deprecated)是一个通过tslint.json配置在你写TypeScript代码时,对你的代码风格进行检查和提示的插件。关于TSLint的配置,我们会在后面讲解如何配置,它的错误提示效果在我们之前的例子已经展示过了。 TSLint Vue加强了对Vue中的TypeScript语法语句进行检查的能力。如果你使用TypeScript开发Vue项目,而且要使用TSLint对代码质量进行把控,那你应该需要这个插件。 (4)框架相关 如果你使用Vue进行项目开发,那Vue相关的插件也是需要的,比如Vue 2 Snippets。 Vetur插件是Vue的开发辅助工具,安装它之后会得到代码高亮、输入辅助等功能。 (5)提升开发体验 Auto Close Tag插件会自动帮你补充HTML闭合标签,比如你输完<button>的后面的尖括号后,插件会自动帮你补充</button>; Auto Rename Tag插件会在你修改HTML标签名的时候,自动帮你把它对应的闭标签同时修改掉; Bracket Pair Colorizer插件会将你的括号一对一对地用颜色进行区分,这样你就不会被多层嵌套的括号搞晕了,来看看它的样子: Guides插件能够帮你在代码缩进的地方用竖线展示出索引对应的位置,而且点击代码,它还会将统一代码块范围的代码用统一颜色竖线标出,如图: 1.3.2 常用功能 (1)终端 在VSCode中有终端窗口,点击菜单栏的【查看】-【终端】,也可以使用快捷键 ”control+`“ 打开。这样可以直接在编辑器运行启动命令,启动项目,边写代码边看报错。 (2)用户代码片段 一些经常用到的重复的代码片段,可以使用用户代码片段配置,这样每次要输入这段代码就不用一行一行敲了,直接输入几个标示性字符即可。在VSCode左下角有个设置按钮,点击之后选择【用户代码片段】,在弹出的下拉列表中可以选择【新建全局代码片段文件】,这样创建的代码片段是任何项目都可用的;可以选择【新建"项目名"文件夹的代码片段文件】,这样创建的代码片段只在当前项目可用。创建代码片段文件后它是一个类似于json的文件,文件有这样一个示例: { // Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description....

March 13, 2024 · zlzong

04 容器汇编 I:比较简单的若干容器

上几讲我们学习了 C++ 的资源管理和值类别。今天我们换一个话题,来看一下 C++ 里的容器。 关于容器,已经存在不少的学习资料了。在 cppreference 上有很完备的参考资料([1])。今天我们采取一种非正规的讲解方式,尽量不重复已有的参考资料,而是让你加深对于重要容器的理解。 对于容器,学习上的一个麻烦点是你无法直接输出容器的内容——如果你定义了一个 vector<int> v,你是没法简单输出 v 的内容的。有人也许会说用 copy(v.begin(), v.end(), ostream_iterator(…)),可那既啰嗦,又对像 map 或 vector<vector<…>> 这样的复杂类型无效。因此,我们需要一个更好用的工具。在此,我向你大力推荐 xeus-cling [2]。它的便利性无与伦比——你可以直接在浏览器里以交互的方式运行代码,不需要本机安装任何编译器(点击“Trying it online”下面的 binder 链接)。下面是在线运行的一个截图: xeus-cling 也可以在本地安装。对于使用 Linux 的同学,安装应当是相当便捷的。有兴趣的话,使用其他平台的同学也可以尝试一下。 如果你既没有本地运行的条件,也不方便远程使用互联网来运行代码,我个人还为本专栏写了一个小小的工具 [3]。在你的代码中包含这个头文件,也可以方便地得到类似于上面的输出。示例代码如下所示: #include <iostream> #include <map> #include <vector> #include "output_container.h" using namespace std; int main() { map<int, int> mp{{1, 1}, {2, 4}, {3, 9}}; cout << mp << endl; vector<vector<int>> vv{{1, 1}, {2, 4}, {3, 9}}; cout << vv << endl; } 我们会得到下面的输出:...

March 13, 2024 · zlzong

04 八个JS中你见过的类型

这小节你学习起来会很轻松,这是你正式接触 TypeScript 语法的第一节课,是最最基础的语法单元。这节课我们将学习在 JavaScript 中现有的八个数据类型,当然这并不是 JavaScript 中的所有数据类型,而是现在版本的 TypeScript 支持的基本类型,在学习基础类型之前,我们先来看下如何为一个变量指定类型: 为一个变量指定类型的语法是使用"变量: 类型"的形式,如下: let num: number = 123 如果你没有为这个变量指定类型,编译器会自动根据你赋给这个变量的值来推断这个变量的类型: let num = 123 num = 'abc' // error 不能将类型“"123"”分配给类型“number” 当我们给num赋值为123但没有指定类型时,编译器推断出了num的类型为number数值类型,所以当给num再赋值为字符串"abc"时,就会报错。 这里还有一点要注意,就是number和Number的区别:TS中指定类型的时候要用number,这个是TypeScript的类型关键字。而Number为JavaScript的原生构造函数,用它来创建数值类型的值,它俩是不一样的。包括你后面见到的string、boolean等都是TypeScript的类型关键字,不是JavaScript语法,这点要区分开。接下来我们来看本节课的重点:八个JS中你见过的类型。 2.1.1 布尔类型 类型为布尔类型的变量的值只能是 true 或 false,如下: let bool: boolean = false; bool = true; bool = 123; // error 不能将类型"123"分配给类型"boolean" 当然了,赋给 bool 的值也可以是一个计算之后结果是布尔值的表达式,比如: let bool: boolean = !!0 console.log(bool) // false 2.1.2 数值类型 TypeScript 和 JavaScript 一样,所有数字都是浮点数,所以只有一个number类型,而没有int或者float类型。而且 TypeScript 还支持 ES6 中新增的二进制和八进制数字字面量,所以 TypeScript 中共支持二、八、十和十六四种进制的数值。...

March 13, 2024 · zlzong

05 容器汇编 II:需要函数对象的容器

上一讲我们学习了 C++ 的序列容器和两个容器适配器,今天我们继续讲完剩下的标准容器([1])。 函数对象及其特化 在讲容器之前,我们需要首先来讨论一下两个重要的函数对象,less 和 hash。 我们先看一下 less,小于关系。在标准库里,通用的 less 大致是这样定义的: template <class T> struct less : binary_function<T, T, bool> { bool operator()(const T& x, const T& y) const { return x < y; } }; 也就是说,less 是一个函数对象,并且是个二元函数,执行对任意类型的值的比较,返回布尔类型。作为函数对象,它定义了函数调用运算符(operator()),并且缺省行为是对指定类型的对象进行 < 的比较操作。 有点平淡无奇,是吧?原因是因为这个缺省实现在大部分情况下已经够用,我们不太需要去碰它。在需要大小比较的场合,C++ 通常默认会使用 less,包括我们今天会讲到的若干容器和排序算法 sort。如果我们需要产生相反的顺序的话,则可以使用 greater,大于关系。 计算哈希值的函数对象 hash 就不一样了。它的目的是把一个某种类型的值转换成一个无符号整数哈希值,类型为 size_t。它没有一个可用的默认实现。对于常用的类型,系统提供了需要的特化 [2],类似于: template <class T> struct hash; template <> struct hash<int> : public unary_function<int, size_t> { size_t operator()(int v) const noexcept { return static_cast<size_t>(v); } }; 这当然是一个极其简单的例子。更复杂的类型,如指针或者 string 的特化,都会更复杂。要点是,对于每个类,类的作者都可以提供 hash 的特化,使得对于不同的对象值,函数调用运算符都能得到尽可能均匀分布的不同数值。...

March 13, 2024 · zlzong

05 TS中补充的六个类型

上个小节我们学习了八个JavaScript中常见的数据类型,你也学会了如何给一个变量指定类型。本小节我们将接触几个TypeScript中引入的新类型,这里面可能有你在其他强类型语言中见过的概念,接下来让我们一起来学习。 2.2.1 元组 元组可以看做是数组的拓展,它表示已知元素数量和类型的数组。确切地说,是已知数组中每一个位置上的元素的类型,来看例子: let tuple: [string, number, boolean]; tuple = ["a", 2, false]; tuple = [2, "a", false]; // error 不能将类型“number”分配给类型“string”。 不能将类型“string”分配给类型“number”。 tuple = ["a", 2]; // error Property '2' is missing in type '[string, number]' but required in type '[string, number, boolean]' 可以看到,上面我们定义了一个元组 tuple,它包含三个元素,且每个元素的类型是固定的。当我们为 tuple 赋值时:各个位置上的元素类型都要对应,元素个数也要一致。 我们还可以给单个元素赋值: tuple[1] = 3; 这里我们给元组 tuple 的索引为 1 即第二个元素赋值为 3,第二个元素类型为 number,我们赋值给 3,所以没有问题。 当我们访问元组中元素时,TypeScript 会对我们在元素上做的操作进行检查: tuple[0].split(":"); // right 类型"string"拥有属性"split" tuple[1].split(":"); // error 类型“number”上不存在属性“split” 上面的例子中,我们访问的 tuple 的第二个元素的元素类型为 number,而数值没有 split 方法,所以会报错。...

March 13, 2024 · zlzong