10-2 到底应不应该返回对象?

前几讲里我们已经约略地提到了返回对象的问题,本讲里我们进一步展开这个话题,把返回对象这个问题讲深讲透。 F.20 《C++ 核心指南》的 F.20 这一条款是这么说的 [1]: F.20: For “out” output values, prefer return values to output parameters 翻译一下: 在函数输出数值时,尽量使用返回值而非输出参数 这条可能会让一些 C++ 老手感到惊讶——在 C++11 之前的实践里,我们完全是采用相反的做法的啊! 在解释 F.20 之前,我们先来看看我们之前的做法。 调用者负责管理内存,接口负责生成 一种常见的做法是,接口的调用者负责分配一个对象所需的内存并负责其生命周期,接口负责生成或修改该对象。这种做法意味着对象可以默认构造(甚至只是一个结构),代码一般使用错误码而非异常。 示例代码如下: MyObj obj; ec = initialize(&obj); … 这种做法和 C 是兼容的,很多程序员出于惯性也沿用了 C 的这种做法。一种略为 C++ 点的做法是使用引用代替指针,这样在上面的示例中就不需要使用 & 运算符了;但这样只是语法略有区别,本质完全相同。如果对象有合理的析构函数的话,那这种做法的主要问题是啰嗦、难于组合。你需要写更多的代码行,使用更多的中间变量,也就更容易犯错误。 假如我们已有矩阵变量 A、B 和 C,要执行一个操作 ​ R=A×B+C 那在这种做法下代码大概会写成: error_code_t add(matrix* result, const matrix& lhs, const matrix& rhs); error_code_t multiply(matrix* result, const matrix& lhs, const matrix& rhs); error_code_t ec; matrix temp; ec = multiply(&temp, a, b); if (ec !...

March 13, 2024 · zlzong

11 为函数和函数参数定义类型

本小节我们来学习函数类型的定义,以及对函数参数的详细介绍。前面我们在讲object例子的时候见过简单的函数定义,在那个例子中我们学习了如何简单地为一个参数指定类型。在本小节你将学习三种定义函数类型的方式,以及关于参数的三个知识——即可选参数、默认参数和剩余参数。接下来我们开始学习。 2.8.1. 函数类型 (1) 为函数定义类型 我们可以给函数定义类型,这个定义包括对参数和返回值的类型定义,我们先来看简单的定义写法: function add(arg1: number, arg2: number): number { return x + y; } // 或者 const add = (arg1: number, arg2: number): number => { return x + y; }; 在上面的例子中我们用function和箭头函数两种形式定义了add函数,以展示如何定义函数类型。这里参数 arg1 和 arg2 都是数值类型,最后通过相加得到的结果也是数值类型。 如果在这里省略参数的类型,TypeScript 会默认这个参数是 any 类型;如果省略返回值的类型,如果函数无返回值,那么 TypeScript 会默认函数返回值是 void 类型;如果函数有返回值,那么 TypeScript 会根据我们定义的逻辑推断出返回类型。 (2) 完整的函数类型 一个函数的定义包括函数名、参数、逻辑和返回值。我们为一个函数定义类型时,完整的定义应该包括参数类型和返回值类型。上面的例子中,我们都是在定义函数的指定参数类型和返回值类型。接下来我们看下,如何定义一个完整的函数类型,以及用这个函数类型来规定一个函数定义时参数和返回值需要符合的类型。先来看例子然后再进行解释: let add: (x: number, y: number) => number; add = (arg1: number, arg2: number): number => arg1 + arg2; add = (arg1: string, arg2: string): string => arg1 + arg2; // error 上面这个例子中,我们首先定义了一个变量 add,给它指定了函数类型,也就是(x: number, y: number) => number,这个函数类型包含参数和返回值的类型。然后我们给 add 赋了一个实际的函数,这个函数参数类型和返回类型都和函数类型中定义的一致,所以可以赋值。后面我们又给它赋了一个新函数,而这个函数的参数类型和返回值类型都是 string 类型,这时就会报如下错误:...

March 13, 2024 · zlzong

12 编译期多态:泛型编程和模板入门

相信你对多态这个面向对象的特性应该是很熟悉了。我们今天来讲一个非常 C++ 的话题,编译期多态及其相关的 C++ 概念。 面向对象和多态 在面向对象的开发里,最基本的一个特性就是**“多态” [1]**——用相同的代码得到不同结果。以我们在 [第 1 讲] 提到过的 shape 类为例,它可能会定义一些通用的功能,然后在子类里进行实现或覆盖: class shape { public: … void draw(const position&) = 0; }; 上面的类定义意味着所有的子类必须实现 draw 函数,所以可以认为 shape 是定义了一个接口(按 Java 的概念)。在面向对象的设计里,接口抽象了一些基本的行为,实现类里则去具体实现这些功能。当我们有着接口类的指针或引用时,我们实际可以唤起具体的实现类里的逻辑。比如,在一个绘图程序里,我们可以在用户选择一种形状时,把形状赋给一个 shape 的(智能)指针,在用户点击绘图区域时,执行 draw 操作。根据指针指向的形状不同,实际绘制出的可能是圆,可能是三角形,也可能是其他形状。 但这种面向对象的方式,并不是唯一一种实现多态的方式。在很多动态类型语言里,有所谓的**“鸭子”类型 [2]**: 如果一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被当作鸭子。 在这样的语言里,你可以不需要继承来实现 circle、triangle 等类,然后可以直接在这个类型的变量上调用 draw 方法。如果这个类型的对象没有 draw 方法,你就会在执行到 draw() 语句的时候得到一个错误(或异常)。 鸭子类型使得开发者可以不使用继承体系来灵活地实现一些“约定”,尤其是使得混合不同来源、使用不同对象继承体系的代码成为可能。唯一的要求只是,这些不同的对象有“共通”的成员函数。这些成员函数应当有相同的名字和相同结构的参数(并不要求参数类型相同)。 听起来很抽象?我们来看一下 C++ 中的具体例子。 容器类的共性 容器类是有很多共性的。其中,一个最最普遍的共性就是,容器类都有 begin 和 end 成员函数——这使得通用地遍历一个容器成为可能。容器类不必继承一个共同的 Container 基类,而我们仍然可以写出通用的遍历容器的代码,如使用基于范围的循环。 大部分容器是有 size 成员函数的,在“泛型”编程中,我们同样可以取得一个容器的大小,而不要求容器继承一个叫 SizeableContainer 的基类。 很多容器具有 push_back 成员函数,可以在尾部插入数据。同样,我们不需要一个叫 BackPushableContainer 的基类。在这个例子里,push_back 函数的参数显然是都不一样的,但明显,所有的 push_back 函数都只接收一个参数。...

March 13, 2024 · zlzong

12 使用泛型拯救你的any

在前面的小节中我们学习了any类型,当我们要表示一个值可以为任意类型的时候,则指定它的类型为any,比如下面这个例子: const getArray = (value: any, times: number = 5): any[] => { return new Array(times).fill(value); }; 这个函数接受两个参数。第一个参数为任意类型的值,第二个参数为数值类型的值,默认为 5。函数的功能是返回一个以 times 为元素个数,每个元素都是 value 的数组。这个函数我们从逻辑上可以知道,传入的 value 是什么类型,那么返回的数组的每个元素也应该是什么类型。 接下来我们实际用一下这个函数: getArray([1], 2).forEach(item => { console.log(item.length); }); getArray(2, 3).forEach(item => { console.log(item.length); }); 我们调用了两次这个方法,使用 forEach 方法遍历得到的数组,在传入 forEach 的函数中获取当前遍历到的数组元素的 length 属性。第一次调用这个方法是没问题的,因为我们第一次传入的值为数组,得到的会是一个二维数组[ [1], [1] ]。每次遍历的元素为[1],它也是数组,所以打印它的 length 属性是可以的。而我们第二次传入的是一个数字 2,生成的数组是[2, 2, 2],访问 2 的 length 属性是没有的,所以应该报错,但是这里却不会报错,因为我们在定义getArray函数的时候,指定了返回值是any类型的元素组成的数组,所以这里遍历其返回值中每一个元素的时候,类型都是any,所以不管做任何操作都是可以的,因此,上面例子中第二次调用getArray的返回值每个元素应该是数值类型,遍历这个数组时我们获取数值类型的length属性也没报错,因为这里item的类型是any。 所以要解决这种情况,泛型就可以搞定,接下来我们来学习泛型。 2.9.1. 简单使用 要解决上面这个场景的问题,就需要使用泛型了。泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。 还拿上面这个例子中的逻辑来举例,我们既要允许传入任意类型的值,又要正确指定返回值类型,就要使用泛型。我们先来看怎么改写: const getArray = <T>(value: T, times: number = 5): T[] => { return new Array(times)....

March 13, 2024 · zlzong

12-1 c++中的模板

1. Template的基本语法 1.1 Template Class基本语法 1.1.1 Template Class的与成员变量定义 我们来回顾一下最基本的Template Class声明和定义形式: Template Class声明: template <typename T> class ClassA; Template Class定义: template <typename T> class ClassA { T member; }; template 是C++关键字,意味着我们接下来将定义一个模板。和函数一样,模板也有一系列参数。这些参数都被囊括在template之后的< >中。在上文的例子中, typename T便是模板参数。回顾一下与之相似的函数参数的声明形式: void foo(int a); T则可以类比为函数形参a,这里的“模板形参”T,也同函数形参一样取成任何你想要的名字;typename则类似于例子中函数参数类型int,它表示模板参数中的T将匹配一个类型。除了 typename 之外,我们再后面还要讲到,整型也可以作为模板的参数。 在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 T。可以说,这个 T是模板的精髓,因为你可以通过指定模板实参,将T替换成你所需要的类型。 例如我们用ClassA<int>来实例化模板类ClassA,那么ClassA<int>可以等同于以下的定义: // 注意:这并不是有效的C++语法,只是为了说明模板的作用 typedef class { int member; } ClassA<int>; 可以看出,通过模板参数替换类型,可以获得很多形式相同的新类型,有效减少了代码量。这种用法,我们称之为“泛型”(Generic Programming),它最常见的应用,即是STL中的容器模板类。 1.1.2 模板的使用 对于C++来说,类型最重要的作用之一就是用它去产生一个变量。例如我们定义了一个动态数组(列表)的模板类vector,它对于任意的元素类型都具有push_back和clear的操作,我们便可以如下定义这个类: template <typename T> class vector { public: void push_back(T const&); void clear(); private: T* elements; }; 此时我们的程序需要一个整型和一个浮点型的列表,那么便可以通过以下代码获得两个变量:...

March 13, 2024 · zlzong

13 编译期能做些什么?一个完整的计算世界

上一讲我们简单介绍了模板的基本用法及其在泛型编程中的应用。这一讲我们来看一下模板的另外一种重要用途——编译期计算,也称作“模板元编程”。 编译期计算 首先,我们给出一个已经被证明的结论:C++ 模板是图灵完全的 [1]。这句话的意思是,使用 C++ 模板,你可以在编译期间模拟一个完整的图灵机,也就是说,可以完成任何的计算任务。 当然,这只是理论上的结论。从实际的角度,我们并不想、也不可能在编译期完成所有的计算,更不用说编译期的编程是很容易让人看不懂的——因为这并不是语言设计的初衷。即便如此,我们也还是需要了解一下模板元编程的基本概念:它仍然有一些实用的场景,并且在实际的工程中你也可能会遇到这样的代码。虽然我们在开篇就说过不要炫技,但使用模板元编程写出的代码仍然是可理解的,尤其是如果你对递归不发怵的话。 好,闲话少叙,我们仍然拿代码说话: template <int n> struct factorial { static const int value = n * factorial<n - 1>::value; }; template <> struct factorial<0> { static const int value = 1; }; 上面定义了一个递归的阶乘函数。可以看出,它完全符合阶乘的递归定义: ​ 0! = 1 ​ n!=n×(n−1)! 除了顺序有特定的要求——先定义,才能特化——再加语法有点特别,代码基本上就是这个数学定义的简单映射了。 那我们怎么知道这个计算是不是在编译时做的呢?我们可以直接看编译输出。下面直接贴出对上面这样的代码加输出(printf("%d\n", factorial<10>::value);)在 x86-64 下的编译结果: .LC0: .string "%d\n" main: push rbp mov rbp, rsp mov esi, 3628800 mov edi, OFFSET FLAT:.LC0 mov eax, 0 call printf mov eax, 0 pop rbp ret 我们可以明确看到,编译结果里明明白白直接出现了常量 3628800。上面那些递归什么的,完全都没有了踪影。...

March 13, 2024 · zlzong

13 TS中的类,小心它与ES标准的差异

虽然说类是 ES6 中新增的概念,但是在这里讲 TS 中的类,是因为在语法的实现上 TS 和 ES6 规范的,还是有点区别。在学习本节课之前,你要确定你已经详细学习了ES6标准的类的全部知识,如果没有学习,建议你先学习下阮一峰的《ECMAScript 6 入门》,学习完后再来学习本节课你会发现,一些同样的功能写法上却不同。 2.10.1. 基础 类的所有知识我们已经在 ES6 中的类两个课时学过了,现在我们先来看下在 TS 中定义类的一个简单例子: class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } getPosition() { return `(${this.x}, ${this.y})`; } } const point = new Point(1, 2); 我们首先在定义类的代码块的顶部定义两个实例属性,并且指定类型为 number 类型。构造函数 constructor 需要传入两个参数,都是 number 类型,并且把这两个参数分别赋值给两个实例属性。最后定义了一个定义在类的原型对象上的方法 getPosition。 同样你也可以使用继承来复用一些特性: class Parent { name: string; constructor(name: string) { this.name = name; } } class Child extends Parent { constructor(name: string) { super(name); } } 这些和 ES6 标准中的类没什么区别,如果大家不了解ES6标准中类关于这块的内容,建议大家先去学习ES6类的知识。...

March 13, 2024 · zlzong

14 类型推论,看TS有多懂你

在学习基础部分的章节时,我们讲过,在一些定义中如果你没有明确指定类型,编译器会自动推断出适合的类型;比如下面的这个简单例子: let name = "lison"; name = 123; // error 不能将类型“123”分配给类型“string” 我们看到,在定义变量 name 的时候我们并没有指定 name 的类型,而是直接给它赋一个字符串。当我们再给 name 赋一个数值的时候,就会报错。在这里,TypeScript 根据我们赋给 name 的值的类型,推断出我们的 name 的类型,这里是 string 类型,当我们再给 string 类型的 name 赋其他类型值的时候就会报错。 这个是最基本的类型推论,根据右侧的值推断左侧变量的类型,接下来我们看两个更复杂的推论。 3.1.1 多类型联合 当我们定义一个数组或元组这种包含多个元素的值的时候,多个元素可以有不同的类型,这种时候 TypeScript 会将多个类型合并起来,组成一个联合类型,来看例子: let arr = [1, "a"]; arr = ["b", 2, false]; // error 不能将类型“false”分配给类型“string | number” 可以看到,此时的 arr 的元素被推断为string | number,也就是元素可以是 string 类型也可以是 number 类型,除此两种类型外的类型是不可以的。再来看个例子: let value = Math.random() * 10 > 5 ? 'abc' : 123 value = false // error 不能将类型“false”分配给类型“string | number” 这里我们给value赋值为一个三元操作符表达式,Math....

March 13, 2024 · zlzong

15 类型兼容性,开放心态满足灵活的JS

我们知道JavaScript是弱类型语言,它对类型是弱校验,正因为这个特点,所以才有了TypeScript这个强类型语言系统的出现,来弥补类型检查的短板。TypeScript在实现类型强校验的同时,还要满足JavaScript灵活的特点,所以就有了类型兼容性这个概念。本小节我们就来全面学习一下TypeScript的类型兼容性。 3.2.1 函数兼容性 函数的兼容性简单总结就是如下六点: (1) 函数参数个数 函数参数个数如果要兼容,需要满足一个要求:如果对函数 y 进行赋值,那么要求 x 中的每个参数都应在 y 中有对应,也就是 x 的参数个数小于等于 y 的参数个数,来看例子: let x = (a: number) => 0; let y = (b: number, c: string) => 0; 上面定义的两个函数,如果进行赋值的话,来看下两种情况的结果: y = x; // 没问题 将 x 赋值给 y 是可以的,因为如果对函数 y 进行赋值,那么要求 x 中的每个参数都应在 y 中有对应,也就是 x 的参数个数小于等于 y 的参数个数,而至于参数名是否相同是无所谓的。 x = y; // error Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number' 这个例子中,y 要赋值给 x,但是 y 的参数个数要大于 x,所以报错。...

March 13, 2024 · zlzong

16 函数对象和lambda:进入函数式编程

本讲我们将介绍函数对象,尤其是匿名函数对象——lambda 表达式。今天的内容说难不难,但可能跟你的日常思维方式有较大的区别,建议你一定要试验一下文中的代码(使用 xeus-cling 的同学要注意:xeus-cling 似乎不太喜欢有 lambda 的代码😓;遇到有问题时,还是只能回到普通的编译执行方式了)。 C++98 的函数对象 函数对象(function object)[1] 自 C++98 开始就已经被标准化了。从概念上来说,函数对象是一个可以被当作函数来用的对象。它有时也会被叫做 functor,但这个术语在范畴论里有着完全不同的含义,还是不用为妙——否则玩函数式编程的人可能会朝着你大皱眉头的。 下面的代码定义了一个简单的加 n 的函数对象类(根据一般的惯例,我们使用了 struct 关键字而不是 class 关键字): struct adder { adder(int n) : n_(n) {} int operator()(int x) const { return x + n_; } private: int n_; }; 它看起来相当普通,唯一有点特别的地方就是定义了一个 operator(),这个运算符允许我们像调用函数一样使用小括号的语法。随后,我们可以定义一个实际的函数对象,如 C++11 形式的: auto add_2 = adder(2); 或 C++98 形式的: adder add_2(2); 得到的结果 add_2 就可以当作一个函数来用了。你如果写下 add_2(5) 的话,就会得到结果 7。 C++98 里也定义了少数高阶函数:你可以传递一个函数对象过去,结果得到一个新的函数对象。最典型的也许是目前已经从 C++17 标准里移除的 bind1st 和 bind2nd 了(在 <functional> 头文件中提供):...

March 13, 2024 · zlzong