斯坦福-CS106A-B-L-X-编程入门笔记-十三-

斯坦福 CS106A/B/L/X 编程入门笔记(十三)

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P15:[22]CS 106L Fall 2019 - Lecture 14_ Inheritance (Screencast) - GPT中英字幕课程资源 - BV1Fz421q7oh

好,那么我们今天要讲的内容,之前我们讲过移动语义、拷,贝语义、类结构的基础知识。今天我们要讲两个内容,来,充实我们对面向对象编程和C++的知识,一个是命名空间,另一个是继承。

这两者不一定是C++特有的概念,特别是继,承。因此,我们会讲一些特定于C++的内容,然后你们可以,选择在106B课程或108课程中深入学习更多相关内容。但是首先,在第3张幻灯片中。

这是最后四节课中的第一节,提醒一下,我们第10周没有课,所以你们可以享受真正的,死周,至少是我们这边没有课程。如果我们看看目前为止的进展,见第4张幻灯片,这是我们,在课程开始时向你们展示的进度图。

你们可以看到到目前,为止已经学到了多少东西,我们已经涵盖了所有的基础知,识,讲解了标准模板库。在面向对象编程中,我们做了一些,调整以适应Avery的时间表,所以我们去掉了多线程的部分。

讲解了特殊成员函数和移动语义,特殊成员函数包括拷贝,赋值运算符和构造函数。今天我们将讲解继承,这个星期,四我们将讲解模板类,下个星期二我们将讲解RAII和智能,指针。

最后一节课我们会让你们投票选择你们想要讲解的,主题。所以可以是,我们可以讲多线程,可以讲makefiles,可以讲,区块链,也可以讲你们感兴趣的任何内容。我们将在本周晚些时候或下周初发出调查问卷。

请关注。好的,如果你们查看标有“上次操作符复习”的幻灯片,我希,望你们点击那个链接,它会带你们到cppreference。com上。

的“操作符重载”页面。我想提及这个页面作为你们的参考,Avery已经讲了操作符,重载、拷贝、移动函数,你们会记得里面有几个难点,其中,之一是,例如,如果你在这个页面上使用控制F或命令F,找。

到赋值运算符,你会注意到它实际上有一个赋值运算符的,示例模板。具体来说,我们提到的一个难点是检查这不是自我赋值,即,检查this是否不等于对其他的引用。因此,如果这是你通常会忘记的内容。

那么这个网站是一个,很好的参考。如果你正在编写自己的类,你可以回去了解,你的自定义拷贝或移动运算符重载应该满足哪些假设,以,便与C++库中的其他类良好配合。所以,这是一个很好的参考。

你还可以从这里注意到其他一些内容,如果你滚动到像关,系运算符这样的部分,你会发现对于许多相似的运算符,例,如大于和小于,或者加一和减一,可能不是这些,但你会注,意到很多运算符是基于其他运算符定义的。

这是另一个很好的风格建议,如果你在编写自己的类时,当,你定义一个运算符时,通常会基于其他运算符来编写它们,再次提醒,这个页面将有示例模板,说明你的运算符重载应,该满足哪些假设。所以,是的。

自己看看这个资料,真的很有用,只是想让你们。

知道它在那里。

太棒了。好了,回到幻灯片上。我想回顾的另一件事是有人问了一个很好的问题,关于为,什么特别要学习移动(move)。所以,我们学习了复制(copy)。为什么移动函数存在?

有人能告诉我移动函数相比复制函数、复制构造函数和赋,值函数的好处是什么吗?是的,去说吧。它将数据从左值(L value)移动到右值(R value),这样可,以节省大量内存。你不会创建额外的副本。没错。

没错。因此,移动的整个目的其实就是为了避免使用不必要的额,外内存。我们上次给出的答案是这样的,嗯,是的,移动确实完全是,为了效率,当你在处理像物联网设备这样的内存有限的小,型设备时,这非常有用。

但我认为值得提到的是,移动实际上非常有用,不仅仅是在,那些边缘情况中,而是在你编写的普通程序中也是如此。举个例子,当C++11引入移动语义时,这对社区来说确实是,一件非常激动人心的事。原因就是这样。

假设你有一个你写的程序。C++11发布后,你只需点击重新编译,它的速度会提高一倍,这就是移动语义所做的。原因是,因为很多C++库的内置类型在C++11发布时都写了,移动构造函数和赋值函数。

这意味着你代码中的每个等号,之前是复制赋值或构造函,数,现在只需用新的C++版本重新编译,就会变成移动赋值,或构造函数。正如Avery上次所展示的,它使代码速度提高了50%,这是巨,大的。

所以这就是我们想花整节课来教的原因。这也是我们确实鼓励你们在自己的代码中使用的,特别是,当你定义自己的类时。对于大多数你们会遇到的类(不是自己定义的),它们通常,已经定义了移动函数。太棒了。好了。现在。

如果你们看看写着“挑战模式”的幻灯片,有多少人记,得关于“所有const”的讲座?有多少人记得最后的这个挑战问题?你不一定需要做过它。我只是想看看这是否看起来很熟悉。好吧。好的。这是你的机会。

我会给你们两分钟时间。和一个伙伴讨论一下。根据斯坦福荣誉规范,千万不要看下一个幻灯片,因为它们,绝对没有答案。和一个伙伴讨论一下,问问自己,这五个const关键字分别。

在做什么?它们在说什么?和一个伙伴讨论一下。给我两分钟,我会找到一个屏幕共享的方式,以便我们可以,稍后进行代码操作。是的。不用担心。不用担心。是的。是的。是的。一定会有的。好的。所以是const。

我不知道它之前是什么。好的。const。两个。好的。好的。那这个呢?然后听剩下的幻灯片。好的。所以。我们走吧。我们有很多时间。所以我们看看吧。我去。好的。我还有一些其他的内容在课堂上。

你可以确保你给出那些,要点。然后,如果你有东西要和小组分享,首先,你应该和小组讨,论你要做什么。哦,等等。你能给我们一个更宽的视图吗?这是概念的参数。这只是一个指向的点。

记住我们谈过的不改变它所指向的东西吗?是的。哦,你不能这样做。哦,Anna。是的。在共享屏幕之前,你尝试让两者都显示出来。我只是,是的。是的,是的。谢谢。这是一个很好的建议。指针不在你的屏幕上。好的。

好的,让我们重新整理一下。好的,是的,谢谢。Victoria。是的,好的。是的,所以Victoria也提供了一个很好的选项。如果你们去Piazza,我发布了讲义幻灯片,在那里有一个,Zoom会议链接。

请加入那个链接,让我知道你们是否能看到我的屏幕。问号,问号。

一旦你能看到我的屏幕,请举手。是的,它应该在我发布的讲义幻灯片下面。哦,好吧。我听到一些人进来了。好吧,我再给你们30秒。看看你们能否解决。你好,欢迎。

如果你点击链接,它应该会自动填充会议ID。哦,我明白了。好的,如果可以的话,你也可以尝试与其他人一起看,如果,需要时间的话?但如果不行,你应该能用你的Stanford ID作为ID。谁还看不到Zoom?

好的,你愿意看看别人的肩膀吗?向你旁边坐着的人自我介绍一下。我希望你们今天结束时至少认识这个班上的一个人,今天,结束时。好的,太棒了。是的,谢谢你们的耐心。我很高兴我们都能再次看到屏幕。好的。

那么谁来逐一讲解。所以你可以给我解释一下,如果你们看到我的屏幕,这个,const关键字在做什么?有什么想法?你们讨论了什么?是的,Byron。完全正确,完全正确。所以正如Byron所说。

它表示作为一个参数,这个函数接受,一个const指针。这个下一个const在说什么?随意喊出来。是的,Victoria。完全正确。所以总的来说,这个函数接受一个指向常量整数的常量指,针作为参数。是的。

Byron。完全正确,这是一个完美的总结。所以对于麦克风来说,这意味着指针始终指向内存中的同,一位置,并且该内存无法被修改。完全正确。然后同样适用于返回值。这个下一个 const 关键字在说什么?

是的,Mason,继续吧。完全正确。继续,这个下一个 const 在说什么?完全正确。再一次,这意味着指针和内存都被指向。它不能被修改。然后最后,这个是最难的一个。

这个在函数末尾的最后一个 const 在说什么?是的,Eva。它没有改变任何东西。完全正确。所以这是让很多人感到困惑的事情。所以,函数末尾的 const 关键字整体上表示这是一个常,量成员函数。

这意味着如果你编写一个成员函数并将其声,明为 const,这个函数不能改变该实例中的任何私有变,量。一个清晰的说法是,如果你记得Avery谈到 this 关键字,并且其他人提到该类的特定实例。

那么这个函数不能改变,这些私有变量。太棒了。所以这是一个有点棘手的问题。事实证明,这实际上并不是在选择使用哪一种。这些是基于你想要什么返回值,想要什么参数,以及是否希。

望函数能够修改类的私有变量的三种不同功能。所以,是的,这也是在作业2中出现的内容。每当你编写自己的函数时,总是要检查你的两个问题。我希望它是 const 吗?我希望它通过引用传递吗?诸如此类。是的。

Elliot。在这个例子中,param 到底是什么?是的,它看起来像是被解引用了。像是,&param 成为 const 指针吗?好问题。所以这回到 & 在参数列表中的作用。

这是 C++ 中一个棘手的区别。在这种情况下,& 只是表示我们通过引用传递。所以,就像我们通常所做的,& 只是表示我们通过引用传,递。所以 param 只是你传递的参数的术语。所以。

明确一下,我们在参数部分做的是,我们通过引用传,递一个 const 指针到 const int。所以在这种情况下,引用实际上是不必要的,因为我们不需,要通过引用传递一个指针。

但如果你仍然困惑,可以课后再讨论。但这是一个很好的问题。太棒了。好的,今天有一些快速公告。作业2将在本周四截止。也就是说,不要忘记你有迟交天数可以使用。如果你填写了介绍调查,每个人都有四个迟交天数。

我会举行办公时间。我不能进行正常的周四办公时间,但我发布了其他时间。如果你需要,请查看 Piazza。作业1的成绩将在今天或明天发布。评分员的截止日期是今天。所以我们会尽快把成绩发给你。

如果你对你的成绩有任何问题,务必来找我们。我们很乐意查看你的代码或解释,如果你只是感兴趣的话,我可以做得更好吗?我们也很乐意这样做。然后,第三次作业将于本周四发布。再说一次。

你只需要完成这门课中的三项作业中的两项。所以,如果你完成了第二次作业,并且做了第一次作业,你,就完全完成了。祝贺你。好吧,完全完成是在你周四提交的时候。好的,是的。有人有任何问题吗?对不起。

我说得太快了。我在尽量弥补一些丢失的时间。是的。故意的。确切的。这是正确的链接。是的,好问题。还有其他问题吗?我刚刚意识到我一直忘记丢掉糖果。好吧。那么,我们会继续处理这个问题。好的。让我们看看。好的。

在这种情况下,我们今天会讨论命名空间。好的,所以命名空间有点有趣。如果我们看看到目前为止我们一直在使用的内容,你们已,经在使用命名空间,无论是有意还是无意。所以你们会记得我们写的代码中。

我们使用了很多 STD 标,准,无论是你们在其他课程中看到的 using namespace ,STD,还是使用显式的 using 声明,例如 using STD ,CL 等等。实际上。

我们在编写自己的类时也见过作用域解析。所以当 Avery 和你们一起实现 string vector 时,你,们会记得,当我们在 。cpp 文件中声明实现时,我们必须。

写类似 string vector: 的东西,然后是我们试图编写,的函数。所以这实际上是我们看到作用域解析在实践中的另一个地,方。我们实际上会在最后理解为什么这是必要的。那么。

C++ 中为什么会有命名空间呢?这种动机来自于我们在标准库中所工作过的内容,我们已,经看到它倾向于使用很多常见的名称,比如 string 或 ,maxcount

假设我们想定义一个自己的类,它也有一个看起来逻辑上,名为 count 的函数。我们该怎么做?如果我们想同时使用标准库和我们自己的库,我们需要一,种方法来区分我们使用的是哪种 count

这可能比最初看起来更复杂。所以我们实际上将查看一段看似非常无害的代码。

所以让我们看看这个。大家能看到代码吗?好的。似乎看不到。好的,太棒了。所以让我们看看这段代码。我们在这里做的是,我们有一个 int 类型的向量。我们所做的只是计算值 1 在该向量中出现的次数。

然后最后,我们会输出计数。这样我们可以双重检查它是否按预期运行。我们会得到手动计数 2。完美。假设这是我们正在编写的某个非常大型程序的一部分。假设在大约一亿行代码后,我们仍然在主函数中实现它,因。

为我们还不知道分解。但没关系。所以,经过了十亿行代码后,假设我们想使用标准算法库的,计数函数。这可能是我们之前见过也可能没见过的东西。但是在算法库中,有一个叫做 count 的算法,你可以传递。

一个迭代器到你想要计数的容器中。它会返回出现的次数。所以,我们可以做的一个事情是,比如说,好的,来吧,你知,道吗?我不喜欢我们的手动计数。让我们试试使用 C++ 库,它需要一个第一个迭代器。

所以我们要说 v。begin,一个第二个迭代器,即 v。end,以,及第三个,值 1。这看起来挺好的。这看起来挺好的。所以我们可以尝试运行这个。完美。我们看到,好的,它返回 2。我们知道,好的。

我们在上面声明了使用 namespace std。这就是为什么我们不需要指定这是 std 的 count。我们已经在之前指定过了。所以我们可能会有一个问题,那就是,好的,假设,这又是一,个庞大的代码库。

在这中间有十亿行代码。而我没有意识到我实际上之前有一个手动计数。所以忽略那个错误。自然的,过了一段时间,我确实想使用这个算法计数,它已,经被标准库为我们定义了。你们觉得会发生什么?你们的直觉猜测是什么?

再次,当我写这个的时候,我在想,好的,我已经声明了使用, namespace std。所以我不需要指定它是 std 的 count。你们认为会发生什么?你们觉得会有多少?实际上,我不会这样做。

我不会说多少会编译,因为我们在共享屏幕。它在那儿显示了一个错误。所以让我们实际运行一下,看看会发生什么。因为这有点违反直觉。而且错误信息,如果你在现实生活中遇到它,真的没有意义,所以啊。

这是我们看到的错误。对象类型 int 不是一个函数或函数指针。Count 不能被用作函数。好的,发生了什么?有人能解释一下他们认为发生了什么吗?为什么会抛出那个错误?所以再一次,这就是它抛出的错误。

是的,我回来了。正是,正是这样。所以在这种情况下,我们所做的是完全合理的。我们声明了一个叫做 count 的变量,这对于我们要做的事,情来说是一个完全合理的变量名。然后在我们代码中的某个时刻。

我们想要使用算法 count,作为程序员,我们期望它能按我们预期的方式解析,因为 ,count 是一个变量。对不起,手动计数是一个变量。而算法计数是一个函数。但是编译器看到的是这个局部变量 count。

它认为你正在尝试调用这个局部变量的函数操作符,即那,两个括号。它会说,你不能对一个整数如 1 做这样的操作。所以这就是发生的情况。这就是命名空间的一件棘手的事情。所以如果我们想解决这个问题。

一个非常简单的方法就是,即使我们已经说过使用命名空间 std,你也必须指明这是 ,std count。这样就能解决问题。

这样就会如我们所期望的那样打印出两个2。

没错。但这实际上是一个更好的例子,说明为什么我们在这门课,程的早些时候说我们更倾向于不使用 using namespace ,std。事实上。

这相当于对于那些使用过 Python 或 JavaScript。

的人来说。所以再次说明,这被称为命名空间。到目前为止,我们一直在使用标准命名空间,std:在 Python 中有一个类似的概念。我们说 import random。然后要使用该库中的某些东西。

我们必须说出库名和其中,的函数。在 Python 中,我们也说像 from random import * 这样,的做法也是相当糟糕的,因为你的代码中塞满了许多你不,需要的代码。

这里为什么我们不喜欢使用 using namespace std 的原。

因完全相同。相反,我们更喜欢使用的是,正如你可能在作业代码中看到,的,比如 using std:vector,using std:cout,using ,std:到目前为止,这说得通吗?有什么问题吗?

关于命名空间的另一件事,给命名空间下个定义,它听起来,就像它的字面意思。它是一种方法,让你将代码行或你声明的类分组到一个单,一的命名空间中,以便以后引用。所以我们实际上可以编写自己的命名空间。

到目前为止,我们只使用了标准命名空间。但我们在这里可以使用的语法是 namespace。然后让我们把我们的命名空间称为 lecture。然后语法是我们可以写一些东西,比如,让我们实际上声明。

我们自己的 count 函数。所以我们会说,让我们声明一个名为 count 的函数,它接,受一个 vector 并返回其中元素的数量。为了清晰起见,让我们把它改成类似的东西。

所以如果我们想使用我们刚刚在自己的命名空间中定义的, count 函数,你们觉得我们应该怎么做?所以 count,然后让我们说 lecture count。你认为语法应该是什么?所以不是 std:

count v。begin,语法应该是什么?没错,完全正确。所以正如我们所期望的,我们将使用我们新定义的命名空,间 lecture,而不是标准的。然后我们可以从那里调用 count 到 v 和 n。

所以如果我。

们尝试运行它,完美。

我们得到了我们所期望的结果。是的,Brian,请说。如果你滚动到代码的最顶部,你必须进行 count include ,vector algorithm。是的。

即使你已经做了 using namespace std,为什么你不需要,做 using std:count?啊,事实上,我们确实做了。你说的是下面这个算法吗?是的。它就在那里。

所以你不需要在上面再写一遍。是的,没错。所以这是一个很好的问题。为什么我们不需要在这里声明 using std:count?事实证明,我们完全可以。我们可以使用 std:count。然后这样一来。

在这里我们不再需要指定。哦,实际上,对不起。我收回刚才的话。所以我们确实需要再次指定,即使我们在上面提到使用,因,为我们有一个局部变量,它优先于我们在上面声明的使用,声明。是的,很好的问题。是的。

还有其他问题。所以即使 std 是一个命名空间,你仍然需要使用包含吗?是的,那么命名空间和包含之间有什么区别?所以可以把命名空间看作是两个不同的步骤。首先,你需要让代码知道其他代码的存在。

所以为了做到这一点,你使用这个包含语句来将所有的向,量代码从云端(可能,并不是真的)带到你运行的程序中。然后你使用命名空间来标记,在我正在使用的这个程序中,的所有代码中,我想使用哪一部分?

所以这是两个不同的步骤。首先,你必须包含代码本身。然后第二,你需要定义你想使用的代码部分。是的。你不需要为向量使用包含,因为你已经在那儿了。确切地说。所以在这种情况下,我们已经实际编写了代码。实际上。

你可以把这些代码写到一个单独的类中,然后包含,那个类。但在这种情况下,你已经在那个文件中包含了代码。是的,很好的问题。很棒。还有其他关于命名空间的问题吗?是的,Yizu。是的,实际上,是的,正是这样。

所以实际上,std 向量只包含了向量的一个特定实现。但是代码仍然需要知道你想使用的向量实际上是来自那个,包含括号向量类的向量。是的,所以它首先需要通过使用包含,包含语句把它引入范,围内。

然后它需要用名称标记它,以便知道它在使用哪个向量。是的,所以我们实际上可以完全编写自己的类。实际上,我们可以编写类似 class our own vector 的内,容,然后进行所有的正常公共私有操作。

然后那会接受是模板类型名称 t。然后,实际上,我们可以在这里引用我们自己实现的向量。所以再次,我会在之前调试错误。但本质上,一旦你在自己的命名空间中声明了它,你可以通。

过做类似 lecture vector 的操作来使用你自己的向量。所以这是 100% 有效的代码,前提是它确实能编译。是的,我会回去更新那里的语法。这是个好问题。我会再次发布更新。

因为我仍然看到困惑的表情。但是的,我会再跟进这个问题。是的,还有其他关于命名空间的问题吗?

另一个关于命名空间的事情是,如果你想在命名空间中有,命名空间,这类小的棘手细节。我们没有涵盖这些,因为我们认为如果你遇到这种情况,我,们希望你具备足够的信心去自己查找这些细节。

但这是 C++ 中命名空间的一个总体概述。但确实,我们希望 equip 你的是学习如何查找所有奇怪的,小细节。但如果您对此有疑问,也请随时向我们提问,您之后也可以,在网上查找。好的,继续说。

对于命名空间,如果你熟悉 JavaScript,它,在 JavaScript 中也是同样的情况。在 JavaScript 中,我们使用 require 关键字。然后同样,要调用一个函数。

我们必须说库名加上我们想要,使用的函数。所以在 C++中,完全是一样的。在这种情况下,两个冒号,作用域解析就是我们确定正在使,用哪个库的方式。

好的,所以我们可能会有的一个问题是回到我们编写自己,的类的时候,为什么我们必须在所有成员变量的定义前面,标注类的名称?事实证明,我们必须这样做的原因有点类似。

这是因为我们需要让编译器知道我们正在为哪个类定义函,数。因此,到目前为止,对于您的所有项目,您所看到的是有两,个文件。有类名。h 和类名。cpp。事实证明,您实际上可以将您的。

cpp 文件命名为您想要的,任何名称。您可以将其命名为而不是字符串向量。您可以肯定地将其命名为不是字符串向量。cpp。并且您仍然可以包含所有相同的代码,并且它会编译良好,所以当您编写自己的。

cpp 实现时,我们需要作用域解析的,原因是让编译器知道您正在尝试实现哪个头文件。所以这是一个小细节,因为我知道,我总是忘记添加作用域,解析,因为它看起来像是字符串向量。cpp。

当然,它是针对字符串向量的。好的,是的。在我们继续之前,关于命名空间还有最后的问题吗?命名空间还是作用域解析运算符?是吗?那么我们应该只在我们的。cpp 文件中使用命名空间吗?

所以这是一个常见的风格建议,就是只在。cpp 文件中使用, using namespace standard,而不是在头文件中。您甚至不必在。cpp 文件中使用它,但您说得对,您确实不。

希望在头文件中使用它。这是因为如果您的类被任何其他类调用或包含,那么您就,是在强制那个类完全导入整个标准命名空间。所以在。cpp 中做会更好。即使在。cpp 中,您仍然可以有选择地选择您正在使用的 。

using。但是在那里使用 using namespace 是可以的。是的,并且您只是要小心像我们之前展示的那些错误。是吗?比如 100 个语句?是的,没错。这是一个很好的观点。比如。

如果我们只是使用 std:cl 好几天呢?这是一个很好的问题。这又回到了风格的概念。所以在那种情况下,作为程序员,您 100%可以自由地使用 ,using namespace standard。是的。

所以所有这些都有点像它们自己的取舍,但无论什么,是合理的,无论什么是常识。是的,布莱恩?使用 std: 的动机是无论如何都不要使东西混乱。但是如果我们正在做 #include

我假设算法,是很大的,对吧?那么在那个时候,我们可以这样做吗?是的,所以这是一个很好的问题。所以这实际上很有趣。所以我得再次检查编译器实际上是如何处理井号包含与使,用语句的。我最初的猜测是。

即使它包含了所有文件,实际上也不会。让我再检查一下。让我回复您,以免给您任何错误的信息。但肯定存在某种效率,不包含整个命名空间。我得再回复您确切原因。好的,是的,所以我有这个,还有尤西的问题。

还有其他问题吗?好的,太棒了。所以再次,我们要转向继承。而且再次,我们的目的不是让你们对如何使用继承有一个,广泛的理解,诸如此类。那是您从像 108 这样的课程中会获得的东西,或者我相信。

他们有时仍然会在 A、B 或 X 中涵盖它。所以如果您对构建自己的类感兴趣,我肯定会看一下一般,的继承。但如果您想在 C++ 中构建,这些就是事情。这些是您需要知道的核心事情。好的。

所以先给你们一个关于继承是什么的总体概述,其动,机来自一个熟悉的问题。所以再一次,假设我们有一个文件流和一个字符串流。并且它们两个都在上面定义了这个插入运算符。并且假设我们有另一个类。

我们想要找到一个打印语句,它,对流做一些额外的事情,然后最终调用那个流插入运算符,然后对于字符串流也是同样的事情。它对那个流做同样的事情,然后调用唯一的区别是它必须,在这两个流上调用不同的插入运算符。

你们会如何解决这个问题?对于如何解决这个问题,您最初的直觉是什么?所以再次,我们更愿意只有一个我们可以在 if 流和 i 字,符串流上调用打印的函数。我们知道您可能会想到什么?是的。模板?对,完全正确。

好主意。所以我们之前已经看到过,好的,这似乎是一个熟悉的问题,为什么我们不只用一个模板?完美。所以我们只是定义某种流类型名称,然后让编译器为我们,创建这两个单独的函数。所以这个星期四。

我们实际上将涵盖一种叫做模板类的东,西,这是类似的。这又回到了这个问题,您会看到我们对继承做了什么。继承所做的与模板所做的似乎几乎有点类似。所以这个星期四,我们将讨论为什么我们想要使用模板而,不是继承。

或者其中之一。所以这是为了给你们介绍另一种解决这个问题的方法。所以再次,我们之前看到的模板是它使用了一种隐式接口,如果您还记得艾弗里的讲座,我们在问,这个函数代表我们,假设了什么样的事情?

所以没有一个明确的列表。但在这种情况下,它假设的一件事是,好的,无论是什么流,它上面都定义了某种插入运算符。作为一个附注,我们这个星期四还将涵盖的另一件事是,实,际上,在 C++20 中。

有一种方法可以明确说明您的模板需,要哪些运算符或函数。所以这实际上是非常令人兴奋的。但那会是下一次讲座的内容。所以这是一个插曲。模板是一个很好的解决方案。结果是,还有一种更通用的解决方案。

适用于其他面向对象,编程语言,这就是继承和接口。所以你可以这样理解,就像在模板中我们有一个隐式接口,一样,实际上还有一种叫做显式接口或仅称为接口的东西,它是C++中最简单的继承形式。

所以它是C++中继承的一个子集。所以它的工作原理是,你们中有多少人熟悉Java?在这里的Java。好的,完美。所以你们可能在Java中见过这样的情况,比如我们定义自,己的接口类。

使用关键字interface,类的名字。然后稍后,我们定义一个实现那个接口的类。在C++中,你可以镜像Java中完全相同的概念。所以在C++中,对应的做法是声明一个叫做drink的类,将你。

想要作为接口函数的函数声明为虚拟的,即关键字,virtual,然后稍后声明你的类T,其中冒号等同于实现,drink。到目前为止有任何问题吗?没有。好的。为了更清楚地说明C++语法。

需要注意的关键点是首先在接,口类中使用virtual这个词。然后注意在那个虚拟函数中,有一种我们之前没见过的独,特语法。我们说virtual void,函数的名字,等于0。

所以等于0的作用是强制任何继承自叫做drink的类必须实,现一个make函数,否则它不能被视为一个真正的类,编译器,将会抛出一个错误。所以这,还是,属于术语的一部分。为了让你们熟悉。

如果你们以后去阅读有关C++中继承的内,容,这被称为纯虚函数,pure表示继承类必须定义该函数。所以总结一下,在C++中,没有像Java中那样的interface关,键字。相反,如果你想定义一个接口。

如果你想使你的类成为一个,接口,方法是确保你的类只包含纯虚函数。所以这只是一个定义。要成为接口,类是一个只包含纯虚函数的类。然后如果你想实现一个接口作为另一个类,那么那个类必,须定义所有这些纯虚函数。

否则它将无法编译。是的,这对大家都明白了吗?这是一个关于继承的奇怪概述。是的,是的。观众成员3。所以我们之前遇到的问题是我们在类中写了太多实例。所以这实际上是一个很好的点。

所以接口确实没有解决我们开始时提出的问题。是的,这确实是一个完美的点。所以接口实际上解决了一个与我们引入接口时不同的问题,接口解决了这样一个问题,比如说茶和咖啡有不同的make,函数。在这种情况下。

你可以使用drink来指定饮料的总体设计,然后将如何实现make函数的细节留给两个不同的类。但这完全正确。我们之前的问题是两个函数是一样的。所以我们想要一种避免重复编写相同函数的方法。是的。

所以我们在解决几个不同的问题。是的。所以这样,我们不能?绝对可以。这是个很好的问题。确切地说。所以我们不能创建,你不能做类似 drink D equals new ,drink 的事情。

因为这是一个接口。所以唯一可以定义的类是那些没有纯虚函数的类。是的,很好的问题。然后是布莱恩。所以我已经写了那个。在继承类中,我们能否有一些公共函数在那里实际定义,而,其他函数没有?很好的问题。

那么我们能否有某些函数在其他函数中定义而其他没有?这是个很好的问题,以至于未来的我回到过去告诉过去的,我创建一个幻灯片。那么如果我们确实想定义某个函数呢?这是个很好的问题。是的。

这实际上是 C++ 中称为抽象类的东西。再次,这涉及很多术语。这只是为了让你们熟悉这些词汇,因为这些是特定的 C++ ,术语。但是如果你见过继承,概念应该感觉有些熟悉。所以如果一个类有至少一个纯虚函数。

那么它就被称为抽,象类。回到之前的问题,确切地说,抽象类不能被实例化。所以只有那些没有纯虚函数的类才能被实例化。例如,假设我们有一个基类。那么在那个类中,我们可以有一个纯虚函数,这意味着任何。

继承它的类必须实现那个函数。我们可以有一个非纯虚函数,这意味着基类也实现了 foo,但是继承类可以用它们自己的实现覆盖那个函数。最后,我们可以有我们自己的普通函数,返回生命的意义。是的。

到目前为止有任何问题吗?这些问题很好。是的?你能回到你认为关键词 public 的幻灯片吗?是的,很好的问题。所以我们看到它写着 class T colon public drink。

所以 public 关键字实际上是三种可能的访问控制符中的,一种。我写了关于它们的幻灯片,但实际上我预见到我们不会在,下一节课之前覆盖它们。

所以这三个词是 public、private 和 protected,你可能,以前见过。在这种情况下,这就是说 drink 中的所有公共函数在 T ,中仍然是公共函数。

drink 中的所有受保护函数在 T 中仍然是受保护函数。而 drink 中的所有私有函数 T 是无法访问的。所以 public drink 实际上是你在 C++ 中处理类时最常,用的关键字。实际上。

如果你不指定任何访问关键字,它会默认为 ,public。所以这是你最常见的情况。更少见的是你会看到类似 class T colon protected ,drink 这样的东西。

这表示将 drink 的所有公共函数转换为 T 中的受保护函,数。是的。是的,这是个很好的问题。是的,布莱恩?杰瑞说的某些内容。实际上,Jerry 说如果你没有它,它会默认为私有。谢谢。谢谢。不,不。

不。这是百分之百正确的。所以实际上,涉及到类时,一切默认为私有。这就是类的一个主要优点。涉及到结构体时,这里实际上会有一个例子。所以在这里,当你做 struct B 从另一个结构体继承时,这。

实际上默认为公有。这就是结构体和类之间的一个关键区别。是的,谢谢你提醒我这一点。好的,太棒了。所以,回到之前的问题,现在知道我们可以有类来实现可以,被多个类继承的相同函数,解决我们问题的另一种方法是。

有一个通用的基类来实现一个通用的打印函数,然后有两,个类,iStream 和 iFstream,它们只需从该基类继承,因此,无需重新实现该函数。是的,到目前为止这样说有道理吗?

我们同时解决了两种不同的问题。太棒了。好的,作为最后一点,当涉及到成员变量时,虽然不是函数,你可以把它们想象成在这个例子中,它们的作用与函数类,似,除了当你重新定义一个成员名称,即使它是不同的类型。

它会使任何以前写的成员名称“隐藏”。所以是的,在这种情况下,如果你尝试在结构体 B 上调用 ,A,你唯一能看到的 A 是 double A。你甚至不会知道 int A 的存在。

这只是关于继承的最后一点小小的难点。接下来剩下的内容,我们将在下次上课时开始讲解。实际上继承部分没剩下多少了。但要记住的大概念是纯虚函数,然后我们将在下一节课上,讲解公有、私有和保护。是的,太棒了。

谢谢大家在屏幕分享问题中耐心等待,祝你们度过一个愉,快的星期二和星期三。还有,来拿一根果冻棒吧。有五种不同的口味,你可以拿走或者付款。哦,是的,我不知道。你要一个吗?所以你认为,这个你叫什么?果冻棒。

一根果冻棒。我认为那可能是,哦,果冻条,抱歉。果冻条,有很多种吃果冻的方式。哦,是的。是的,谢谢。是的。你从哪里得到这些的?啊,从一个亚洲市场。还有,我刚刚在来这里之前把其他课程的代码弄好了。

我非常自豪。哦,我的天哪。是的,它非常有利。

好的。需要帮忙清理吗?啊,不需要。不过谢谢你。好的。谢谢。周四见。

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P16:[21]CS 106L Fall 2019 - Lecture 15_ Inheritance and Template Classes - GPT中英字幕课程资源 - BV1Fz421q7oh

好的,大家讨论的时候,我们为了让视频中的人跟上进度,一开始我们发了一些食物,只是开玩笑,我的意思是,我们,确实发了,但没关系,我知道有些人不能来上课,所以我们。

让大家考虑一下纯虚函数和非纯虚函数之间的区别,因此,今天我们将完成上次没有完成的继承部分,然后进入模板,类和概念,也就是说,重新审视模板的整体概念,但现在是,应用在面向对象的环境中。

所以今天非常令人兴奋,好的,那么谁能告诉我纯虚函数和非纯虚函数之间的区别,举手,好的,Brian,说说吧。纯虚函数等于零,而非纯虚函数是非纯虚函数。这完全正确,还有什么其他的不同之处吗?比如。

你的子类可能实际上会定义纯虚函数,因为,你在应,用虚函数,但纯虚函数对每个类都是不同的,而对于非纯虚,函数,你可以说,它只是一个通用的定义,但子类可以选择,使用它。完全正确,是的。

所以为了让后面的人能听到,如果有任何,人听不见的话,功能上的区别,语法上的区别就是这些,功,能上,纯虚函数必须由继承该类的任何类来实现。非纯虚函数是,你说,啊,我在这个类中实现了一个通用的,解决方案。

但如果我的派生类想要重写这个函数,它们可以,实际上有一个问题,就是,好的,比如说,正如我们之前看到,的,抽象类就是任何具有至少一个纯虚函数的类,换句话说,它有至少一个尚未定义的函数。所以我们上次看到的。

啊,是的,抽象类也可以拥有正常类,的一切,它可以有自己的变量,它可以有自己的非虚函数,像这个 bar。所以,有时出现的问题是,等一下,我觉得我在其他地方看,到过人们实际上重写了这个 bar 函数。

换句话说,非虚函,数。我认为适当的回答是,重写非虚函数是合法的,但不道德。

是的,为了澄清,当你设计自己的类时,如果你希望你的函,数可以被重写,你需要声明它为虚函数。如果你希望强制它可以被重写,你可以通过添加等于零来,声明它为纯虚函数。好的,到目前为止有问题吗?有吗?

等于零除了像 x 等于零那样的标记外还有其他作用吗?绝对有,你可以把它想象成将指针设置为 null 的等效物,是的,它有点像函数的等效物。好问题。是的,Brian?你在说什么,像你在做什么?啊。

那确实是一个打字错误。谢谢。我在跟随上面的等于零,但谢谢你发现了这个问题。是的,好问题。好的,关于上次没有讲到的一些术语,我们到现在为止一直,在使用这些词,但为了正式定义,当我们说基类时,它是被。

继承的类。也就是父类或超类。派生类是继承的类。所以我们稍后会看到一个例子,但这只是为了让你知道,如,果你在阅读继承的文档时,基类和派生类是你最可能看到,的术语。好的,关于实现你自己的类和继承关系。

有几件事要注意,比如说,如果你想设计你自己的类。所以有几个方面需要记住。关于构造函数,作为一种良好的实践,你在从另一个类继承,你的类时,你的构造函数中要包括父类构造函数作为初始,化列表参数之一。

所以具体来说,我们稍后也会看到一个例子。但要记住,当你实现你自己的构造函数时,你要调用前一个,类的构造函数,以便你可以使用它已经为你创建的任何构,造函数。然后,如果需要。

你也可以添加你在类中添加的任何新变量,同样,关于析构函数,也有类似的规则。在这种情况下,无论是编写你自己的类还是从一个类继承,时,关键是如果你打算让你的类可继承,你基本上总是将析,构函数设为虚拟。

这样做的原因是,否则可能会出现一些非常棘手的内存泄,漏,这些泄漏不容易预见。举个例子……,啊,是的。一般来说,一个类的析构函数是否为虚拟是判断它是否打,算被继承的一个好标志。这就是为什么会这样。

所以在这种情况下,我们有一个基类,然后我们有一个从基,类继承的派生类。问题是,假设一个用户想要创建一个类型为基类的指针B,指向一个新的派生函数。这整行代码就是继承变得棘手的地方。就是。

当你在处理不同的指针和不同的对象时,如何解释不,同的继承类?这正是我们在这门课程中跳过的内容。所有这些都是继承的棘手细节,你将在CS108课程中学习,或者在线阅读更多内容。但对了。

我们不会覆盖那部分内容。但本质上,如果我们在这里删除B,我们只调用基类析构函,数而不是派生类析构函数。因此,在派生类中分配的任何内存现在都会变成悬空的。所以,是的,这就是这一政策的原因。

所以这些只是两件风格上的事情,以防你发现自己编写自,己的类时。到目前为止有任何问题吗?是的,这些大多是定义性的或只是一些事实,以防你发现自,己想编写自己的类时。好的,所以另一个人们问过的问题是。

私有、保护和公共之,间的区别是什么?这又是关于同一主题的一些杂项事实。私有意味着任何私有的成员或变量只能由这个类访问。保护意味着保护的成员或函数可以被这个类或任何派生类,访问。

然后公共意味着任何人都可以访问。举个例子,假设我们有一个名为drink的基类,其中foo是公,共的,bar是保护的,baz是私有的。那么这意味着drink类本身可以访问这三个变量。

假设 tea 继承了 drink。那么 tea 可以访问公共和保护成员。而 rock,虽然与 drink 完全无关,只能访问公共成员,也,就是 foo。是的,所以这算是更多的定义。

到目前为止有任何问题吗?

太棒了。所以在这种情况下,给你们。完美。好的,我认为我们回到录制状态了。这很有趣。哎呀。所以这是今天晚些时候的内容。好的。所以假设。我们要做的是我只是想演示一下如何使用声明自定义类的,语法。

看看你是否想在自己的代码中使用这个。所以再次,我们上次看到为什么我们使用特定的 using 语,句而不是使用标准命名空间。假设我们有一个叫 drink 的类,我们正在实现它。

而类的语法就像你们到目前为止实现的任何头文件一样。你可以有一个公共部分。你可以有一个私有部分。假设在我们的公共部分,我们只是想保留默认构造函数。假设我们还想要另一个构造函数,我们传入一个字符串作。

为口味。我们想要什么口味的饮料?然后我们将实现它。我们来维护一个私有字符串 flavor。然后在这里,我们当然只需初始化我们的 flavor 变量。注意这里我们使用了 Avery 讲过的初始化列表。

哎呀。然后假设我们想要一个纯虚函数。所以任何继承 drink 的类必须实现一个叫 make 的函数,这进一步意味着,在我们的 int main 中,我们不能做诸如, drink D = 等等 的操作。

原因是,因为,如果我们实际完成它,你会看到编译器会抱,怨,这是因为我们有一个纯虚函数,这意味着这个类不能被,实例化。所以假设我们知道我们希望这个类是可继承的,那么作为,我们的好习惯。

我们希望声明我们的析构函数为虚函数。我们还会将它设置为虚析构函数。太棒了。好的。所以在这种情况下,我们来实现一个类 T,它也会有一些公,共变量。假设它也使用默认析构函数。好的。

所以根据我们所说的构造函数,我们应该如何编写 T 构造,函数?对不起。这应该实际上写作 class T 继承自 public drink。你认为我们应该如何编写这个 T 构造函数?

假设它接受一个字符串作为口味。这里应该写什么?是的。使用它。完美。你认为这个语法是什么?陷阱问题。或者说,它可能是有点直观的。它可能有点直观的。是的。所以这完全正确。

所以我们需要做的就是使用已经定义在 drink 上的构造,函数。这有意义吗?大家跟上了吗?点赞。点踩。你能再讲一遍这一行吗?是的。绝对可以。是的。所以这行代码的意思是,我们想为这个 T 类定义另一个构。

造函数,因为我们希望用户能够传入他们想要构造的 T 的,类型。因此,他们会传入一个字符串表示的类型。我们想强调的一点是,在创建派生类的构造函数时,比如在,这个例子中是 T。

我们理想的做法是总是要在该构造函数,中调用基类的构造函数。这是因为基类已经有了一种接口或使用成员变量的方式,你希望继承这个类时能够复用这些方式。是的。这是个好问题。语法方面。

我们再次使用了 Avery 讲座中的初始化列表。太棒了。是的。这有意义吗?是的。所以下划线类型只是一个风格上的选择。我本来可以直接叫它类型。在这种情况下,它仍然会起作用,因为在初始化列表中,它。

知道哪个是这个类型,哪个是参数。所以,是的。为了避免混淆,我们实际上可以保持这样。很好。是的。到目前为止的问题都很好。是的。这段时间内输入了相当多的代码。所以类的语法对大家来说有意义吗?好的。独特的是。

如果你查看 106B 或 X 作业中的 。h 文件,你,会发现它实际上使用的正是这种语法。只是通常定义了更多的函数,你不会特别注意到类的语法,以及公共和私有语法。是的。

所以我们正在做的是在这个文件中定义一个类。通常定义类的地方是在它们自己的头文件中。

所以在这种情况下,恰好我们在主 。cpp 文件中定义了类。

。是的。这有意义吗?是的。所以每次你想使用父类中的某个方法时,只需在方法前加,冒号和空格,然后调用父类中的方法?或者调用父类中的任何方法?是的。这个冒号实际上只是构造函数特有的。它只用于初始化列表。

我想这是两节讲座前的内容。是的。是的。是的。有一种方法。假设 make 实际上不是一个纯虚函数。假设它实际上是一个有。假设我们定义它为 make does get a cup 或类似的东西。

有一种方法可以使用作用域解析来访问父类的函数。但我们不会涵盖这个内容。所以如果你感兴趣,可以找我们讨论或查找相关资料。是的。但这是你可以做的事情。太棒了。好的。是的。所以如果我们继续。所以,再次。

如果我们希望 T 可以被继承。我们可以做类似的事情。所以,再次,我们将析构函数设为虚函数。然后,在 drink 中,make 被声明为纯虚函数。这意味着在 T 中,我们必须实现那个函数。

我们也可以将其声明为纯虚函数。然后交给另一个函数来实现。是的。我很高兴。测试得很好。测试得很好。嗯。好的。好的。所以如果我们要实现 make,比如说。所以在这一点上,比如说我们只是想把这个函数做成。

比如说,我们希望所有的 T 以相同的方式创建。所以我们会做一些像。 创建它。从 T 类中创建 T。完美。然后实际上,这就像是,为 T 取一个杯子。好的。到目前为止这有意义吗?

所以我们所做的是声明了一个名为 drink 的类。我们还声明了一个名为 T 的类,它继承自 drink。我们涉及的四件事是。构造函数的使用情况如何?析构函数的使用情况如何?

然后像虚函数这样的东西如何在两个类中实现?是的。嗯。所以在 T 的情况下。所以在这种情况下,由于 T 不再有任何纯虚函数,我们理,论上可以做一些像 TT 的事情。然后让我们用像红色这样的口味来初始化它。

所以。对。所以这实际上也需要自己的口味变量。然后我们可以做一些像 T。make 的事情。所以如果你想象这是另一个。嗯。所以 drink 和 T 是一种矫揉造作的例子。但你可以想象,假设这两个类是流类。

那么这可以是流类的某种接口。而这可以是它的实际实现。所以这实际上会是像打印到流中的东西。嗯。正是如此。所以在这种情况下,我们会调用 T。make。原因是因为 drink 实际上没有定义 make 函数。

好问题。是的。嗯,很好的问题。那么为什么 T 的析构函数是虚函数?实际上,它不必是虚函数。我本来打算声明两个更多从 T 继承的函数。但我们可能没有时间去做。所以,是的,我现在实际上要跳过这部分。所以。

是的,假设 T 实际上是一个最终类。我们不希望 T 被继承。那么绝对我会去掉虚函数。嗯,好问题。事实上,由于这个原因,我甚至不需要写它。但我只是为了注释的目的留在那里。嗯,Eva。绝对是的。绝对是的。

所以问题是,再次问,如果我们有两个 make 函数,一个在,父类中?假设这个函数是像,从 drink 类中创建 T。那么绝对可以。我们可以调用 T。make。事实上,我相信我们可以实际运行它。

看看它打印了什么。所以,再次,我们用 T 初始化它。那看起来很熟悉。对。好的。它打印了一个奶昔。它打印了一个奶昔。相当不错的 make 函数。好的。这就是 C++ 的未定义行为。完美。

并且它说从 tea 类创建了 T。但是如果您愿意,有一种方法可以访问父类的 make 函数,要做到这一点的方法是使用名称空间解析。所以应该是——好吧,我得记住语法。

但应该是类似 drink 或 tea 这样的东西。不,应该是 T。drink。我认为是这样的。我们看看是否正确。从 drink 类创建了 T。好了。好的。是的。所以,绝对是。老实说。

这是一个比这长得多的主题。所以,是的,如果看起来有点仓促,我道歉。是的,这些只是一些特定的 C++特性。如果您查看 CPP 参考——如果您在 C++中搜索继承,您会——,您会深入了解所有奇怪的情况。

比如,如果您从这个创建一,个类但想访问那个中的东西等等。是的。所以这绝对是一个非常大的主题。并且有很多——是的。你们问了很好的问题。是的。还有其他问题吗?在我们继续之前?是的。对。我不记得了。

您能重复一下为什么调用虚析构函数是好的吗?是的。是的。所以我们想要调用虚析构函数的原因是因为我们想要保证。

——如果我们回到这里。我们想要保证,比如说,如果用户出于某种原因声明——是的,这进入了一些奇怪棘手的继承细节。但是比如说,您想要声明一个派生类型的对象但声明一个,指向基类型对象的指针。

虚函数确保如果您说删除 B,它将转到派生类的——派生类,的析构函数而不是基类的析构函数。是的。所以在删除的情况下以及析构函数被调用的任何其他地方,在这种情况下——所以,是的。

我们强制析构函数为虚函数的原因只是为了防止内存泄漏,所以在构造函数中,没有——是的。在构造函数中,实际上没有内存问题。是的。我们可以再讨论。是的。如果我们感到困惑。是的。好的。好的。好的。是的。

在我继续之前还有问题吗?没有。好的。所以我上次提到的一件事是我给了你们这个问题。问题是我们有——让我们看看。我们有一个针对 F 流的打印语句,还有一个针对字符串流,的打印语句,我问你们,你们会怎么解决?

我们最初的直觉是,哦,等等。我们不是学过模板吗?这不正是他们试图解决的那个问题吗?绝对是。模板是这个问题的完美解决方案。然后我就说,好吧。但是 JK,这里有另一种方法来做。这里是显式接口。所以再一次。

记住,模板是隐式接口,而这些是显式接口。所以一个自然的问题是,再一次,什么时候使用每一个?所以事实证明,这实际上是一个更大的问题,什么时候使用,被称为静态多态性而不是动态多态性的东西?

在继承部分这将是一个常见的反复出现的问题,因为这是,一个非常大的部分。我不会详细讲解多态性是什么。你可以把它理解为编译器如何能够接受不同类型,并对每,种类型使用相同的代码。所以这有点像你对模板的熟悉感。

它可以使用相同的模板代码,并根据你传入的类型选择使,用哪个模板代码。所以模板和我们刚才讨论的内容之间的区别在于,模板,你,可能还记得,这是一点小细节,但是模板的工作方式是,当,你编写某个东西时。

假设我们有一个向量模板,然后在我们,的代码中我们写了类似“vector of ints”的东西,那么编,译器在编译时会实际创建一个全新的模板函数,只要我们,有类型名称T,它会字面插入“int”。因此。

模板被称为静态多态性,在编译时生成所有实际不同,的代码片段。在这种情况下,继承实际上是一种动态多态性。换句话说,在比如说,基类指针B等于新建派生类的情况下,它不会知道你指的是哪个类,直到运行时。

直到你实际运行,代码时。所以没有办法提前确定什么代码在什么地方有效。它只会在运行时知道,啊哈,B实际上是基类的类型,即使它,指向一个派生对象。所以,如果那些细节不明白也没关系。要记住的一般概念是。

模板在编译时进行它们的多态性,而,派生类在运行时确定它们的所有类型。所以,这就是何时使用每种方法的主要区别。对吗?去做吧。那些将会有所不同。静态的,是的。完全正确。静态与动态通常意味着编译时与运行时。

是的。但这是一个好问题。所以作为你们的参考,如果你们遇到类似之前的问题,你们,有两个打印语句,但它们实际上指的是相同的代码,只是有,两种不同的类型,你们现在有两个解决方案。你们可以创建两个不同的类。

或者使用模板。这就是何时使用每种方法的一个想法。当你非常关心使运行时快速时使用模板。换句话说,让所有工作发生在编译时。或者当,例如,假设我们的两个类像是,再次,比如说,像岩,石和茶。换句话说。

它们没有共同的基类。在这种情况下,继承相同类的解决方案实际上并不适用。然后,是的,派生类你希望在节省编译时间时使用。实际上,还有一个使用派生类的好处。再次澄清一下,我们在这里使用派生类的方式是。

假设我们,有一个名为drink的类,并且我们有两个子类叫做茶和咖啡,那么如果drink类已经定义了make函数,那么茶和咖啡就不,需要定义自己的make函数。

这就是我所说的在这里作为替代方案使用派生类的意思。明白吗?这样我们使用派生类的方式是否和使用模板时相同?Victoria,你能总结一下吗?是的,绝对可以。绝对可以。正是如此。正是如此。所以对于模板。

编译器必须创建每一种类型,而在类中只有,一个函数。只是运行时必须确定使用哪个函数。在这种情况下,只有一个函数。正是如此。是的。所以派生类的另一个好处是你会注意到,使用模板时,实际。

上每次都是在复制和粘贴代码。每次只是将 T 替换为另一个变量类型。因此,你的代码中可能实际上有六个不同版本的相同模板,一个用于 int,一个用于 double。从程序员的角度来看,这只是一个模板。

但是当你在代码中编译时,将会有多个模板版本。这就是所谓的代码膨胀。所以这是不使用模板的另一个优势。话虽如此,我们也熟悉为什么模板使用起来很好。所以这是另一个关于何时使用每种方法的情况。

这是一个很好的参考。好的。是的。有人有任何问题想在讲座中澄清的吗?是的。我们也可以将讨论放到线下进行。好的。是的。为了给你们提供一些背景,首先,我们没有涵盖的内容,就,是这就结束了继承的部分。

我们没有涵盖多态性是如何工作的。我们也没有涵盖像 base pointer B = new derived 这样,的那些棘手的细节,诸如如何解析你所引用的函数。是的。这些都是继承的一些棘手细节。

为了给你们提供一些背景,比如如何放置这次讲座,可以将,继承视为如果你正在尝试设计自己的大型系统,其中需要,不同类型的不同类。比如说你要设计一个访问系统,你需要像学生类型和课程,类型。

这时你就要开始考虑继承,思考哪些类型是其他类型的类,型?例如,我如何构建我的整体系统?这就是面向对象编程的整体概念:你如何与所有这些不同,类型(如学生、课程或作业)一起工作,使它们协同工作。所以。

如果你的项目不涉及那种大型系统设计,那么请记住,这一点。但如果你不理解它,也不要担心。是的。如果你想设计那种大型系统,这就是用到的地方。好的。棒极了。这是另一张幻灯片。

我想我们已经有了几个关于类型转换的问题。你应该如何进行类型转换?我实际上会留给你们以后阅读。简而言之,现代 C++ 中,最佳实践不再是像圆括号 int B ,或 int 圆括号 B 这样的做法。

正如你们可能在许多其他课程中看到的,最佳实践实际上,是使用被称为静态转换的方式。所以,是的,我会把这个放在那里作为参考,但现在为了节,省时间我不会进去看。但是,是的。

这是一个关于如何将事物从一种类型转换到另,一种类型的说明。好的。目前有没有人有任何问题需要回答?好的。好。是的,这对继承的讲解确实像是速成课程,尤其是你之前没,有见过它的话。所以尽量理解你能理解的部分。

祝贺你在那些你不理解的,地方。好的。很好。在这种情况下,公告。所以作业一的成绩。好的。Avery,纠正我一下。它们都发布了吗?所以有一位课程组长本周遇到了紧急情况。是的。所以他们还没有完成。

但他们很快就会完成评分。否则,我认为其他人的成绩已经全部评分完毕了。是的。所以如果你去 paperless。stanford。edu,你应该能够看到,你的作业反馈。再次说明。

我们很愿意坐下来讨论任何反馈,或者如果你想,深入了解你的代码,或者看看你如何改进它。是的。一定要联系我们。我们很乐意讨论这个问题。是的。顺便提一下,我认为到目前为止所有已经评分的同学都通,过了。所以耶。

所有人都通过了。太棒了。是的。所以其他行政事务。作业二今天到期,但记住你可以在整个学期内使用四天的,延期天数,如果你还没用过的话。然后,如果你完成了作业,恭喜你。你已经正式完成了这门课程。

你已经完成了所有要求,我们几乎可以保证你会通过。然后作业三已经发布了。作业三,我放了。是的。所以它已经发布了。PDF上的截止日期不正确。是的。所以我现在要改一下。用这个来说明。是的。然后我还会加一个。

我要添加一个小的说明,因为我在作业中添加了移动语义,所以我要添加一个关于移动语义的特别说明。是的。这是一个相当具有挑战性的作业,但它也是其中一个。我认为这是最有趣的作业。它结合了所有的主题。

它不仅结合了课程后半部分的主题,还重新讲述了一些算,法和模板的内容。所以。是的。是的。你会看到真的。你会在那个作业上工作。这是一个非常酷的作业。是的。材料涵盖到今天的内容。Anna会讲解模板类。

那将涵盖作业三的材料。好的。是的。从后勤角度来说,作业三将在这门课结束讲座之后到期。也就是说,我知道我。我们可能还会在最后一周继续有办公时间,如果你想继续,做这个作业或者过来和我们讨论。

然后我们会把成绩发给你们。是的。如果你有任何问题,请像往常一样在Piazza上发帖。这个。11点钟。哦!是的。应该是。是12月。是的。是。是星期四。是的。是的。不,其实是明年2020年到期。所以。

有很多延迟日期。太棒了。好的。所以,是的。所以,我们要讲的最后一个大主题就是模板类。好消息是,你们实际上对所有的模板类都非常熟悉。不是完全熟悉。但是在语法上,它与我们看到的模板函数非常相似。事实上。

为了让你们回忆一下,因为我又查看了一遍,模板,是在第四周,这感觉好像是很久以前的事了。所以,回到我们的STL模板讲座,你们还记得使用我们最喜,欢的计数函数吗?好的。是的。所以。

为了回顾一下我们之前看到的模板,逻辑是。好的。我们有一个对整型向量非常有效的函数。但是它做了很多假设。那么,我们如何去除这些假设,并把它们放到模板中呢?所以,第一个假设是。好的。它不一定是整型向量。

它可以是任何数据类型的向量。我们说。 好的。好的。这很不错。然后我们说。好的。不。我们可以做得更好。它不一定是数据类型的向量。它可以是任何数据类型的集合。但这时候我们遇到一个问题。

就是并不是所有的集合都能,以线性方式进行迭代。例如,我们看到的映射和集合,它们没有按照顺序递增的概,念。所以,我们找到的解决方案是。好的。相反,我们将使用我们最喜欢的迭代器来遍历函数。然后。

这又做了最后一个假设。实际上,我们为什么需要它成为一个集合呢?实际上,我们可以使用任何迭代器的范围,统计值在该范围,内出现的次数。好的。这对你们来说熟悉吗?点头表示是的。好的。好的。太棒了。所以。

总结一下,我们用函数模板所做的就是描述如何构建,一组相似的函数。所以,在这里我们能够构建一个函数,该函数可以计算某个,值在任何其他东西范围内出现的次数。所以。

这是一个基于你传入的迭代器类型和数据类型的家,族。猜猜类模板是什么?事实上,你会发现它的语法也非常相似。所以,我们来看一下模板类是如何工作的。

这会有点有趣,因为我很确定。

是的,我所有的代码还在里面。好的。所以,我实际上很快。哦,不要看,不要看,不要看,不要看。好了。好的。所以,假设我们从一个函数开始,比如,一个看起来像这样,的类。为了让你了解发生了什么。

我们现在要做的是实际实现你,在第二次作业中使用的优先队列。不完全是这样。实际上我们不会实现 compare 函数部分,因为它需要一些,额外的细节。但我们现在要做的是把这个优先队列。

从现在只处理 int ,的状态。它要求只传入 int 类型。我们将把它改成一个模板化的类,以便它可以是任何类型,的优先队列。所以,这有很多代码。所以,快速总结一下它在做什么。

我们有我们的 main 函数。我们将创建我们的优先队列类型。现在它只定义了一个 int 向量。所以,我们甚至不需要做任何事情,因为我们没有进行任何,参数化。我们推送了 3、5 和 7。

然后 cout 了它。然后在我们的类优先队列中。所以,再次注意我们使用这个类语法来定义一个类。再说一次。我们有构造函数和析构函数。我们将实现的三个函数是你在第二次作业中可能使用过的,三个函数:

top、pop 和 push。不,我在开玩笑。和 push。所以,是的。不要在意这些实现的东西。实际上挺有趣的。如果你看看斯坦福库中的优先队列实现,它使用了我们在,这里使用的方式。

也就是使用一种称为堆的数据结构。但不要在意这个。我们实际上现在只是把它简化一下。

所以,这就是我们的类。然后我们还定义了一个输出运算符。

所以,如果我们现在运行这个。哎呀,我们还在运行简单继承。给我一点时间。来自 drink 类的 T。如果我们把它设为活动项目。我就做这个。

意外的。啊,对了。

你说什么,你不认识结束注释符号?完美。它输出了 735。好的。输出的顺序很奇怪只是因为我们打印的方式。所以,不要在意那个。我们只要确保它能工作。好的。所以,完美。所以,我们定义了这四样东西。

top、pop、push 和输出。然后我们有一些用于实现类的私有变量。所以,这就是这个类。再次说明,这个类目前只允许是 int 类型的优先队列。所以,你们对如何将它语法上变成模板化类有什么直觉?

你们认为我们会写什么?是的,Zach。好的。好的。告诉我,告诉我,告诉我,告诉我该怎么输入。我会输入。只要大写 T。T 前面需要什么?输入它。好了。因此,这将是第一个区别。所以。

函数模板和类模板之间的一个区别实际上只是我们,模板化的变量类型。在这种情况下,它将是一个类。不,我收回刚才的话。对不起。实际上,我们在这里不会做的事情,但你可以在其他地方做,的是。

你实际上可以让类本身成为一种模板变量类型。所以,比如说,我们定义了另一个东西,比如说我们定义了,一个结构体像 node。然后那个 node 有一个 int priority 和一个 int 数据。

之类的。然后我们可以做的是,我们可以在类上定义我们的模板类,在这种情况下,我们可以将 node 作为类型传入。但在这种情况下,我们可以坚持。它一定要是一个类吗,你可以做到吗?不一定。所以,实际上。

让我们做类吧。所以,一个类将包括任何东西,包括像标准库这样的东西。因此,它将包括向量,诸如此类的东西。因此,在这种情况下,由于我们想做一个字符串的向量,我,们将使用类。是的,你需要澄清吗?是的。

因为我几周前在想这个问题。我认为类和类型做了类似的事情。我认为有一个边缘情况,如果你想做像嵌套模板这样的事,情,如果你想做一个模板的模板,那么那里会有一些奇怪的,情况。大多数人不会编写类的模板模板。

因此,对于大多数用途,类和类型名称类是完全相同的。好的。这实际上是一个很好的澄清。很好。好的。因此,你可能记得从作业分发单中,我们有一些东西也写了。

class container 等于 std vector of t。所以,这句话的意思是,它说,好吧,这个类模板接受两个参,数,第一个是某个类 t,第二个我们称之为 container。

这句话的意思是,默认情况下,除非另有指定,否则将其设,置为 t 的向量容器。好的。到目前为止这有意义吗?到目前为止关于模板化的任何问题吗?棒极了。好的。那么。

我们还需要做什么才能将这个类完全转换为模板类?所以,现在,我们已经声明了我们的模板变量,但我们是否,在任何地方使用了它们?好的。那么,我们的代码中需要在哪里更改?是的。完美。正是这样。好的。完美。

我们看到一个 int 在那里。还在哪里?好问题。我们需要改变构造函数吗?不。是的。不。这是一个非常好的问题。因此,在这种情况下,由于我们在构造函数中没有做任何特,殊的事情,我们仍然可以使用默认的。

它将调用我们传入的任何类型的默认构造函数。因此,如果,例如,我们传入的 t 是一个向量,那么它将能,够调用默认的向量构造函数。是的。好问题。是的。我们还需要在代码中的其他地方修改,以便将其完全转换。

为模板函数、模板类?完美。完全正确。然后还有一个地方。你看到了吗?完美。完全正确。所以,在这种返回类型的顶部。太棒了。是的。所以,这个 t 堆的向量实际上与我们传递的内容没有关系。

这只是我们在底层实现优先队列的方式。是的。所以,我们其实可以选择用其他方式实现,比如链表,这样,我们就不会使用向量了。我们会使用不同的东西。是的。这是个好问题。这个向量与这个向量无关。是的。好问题。

所以,是的。实际上,这是个好问题,因为我们并没有在任何地方使用容,器。确实如此。在这个函数或我们现在实现的方式中,没有地方实际使用,容器。但这算是一种接口。难道不能把容器作为保存堆的东西吗?是的。

我们可以。在这种情况下,我们可以。是的。我们不一定要这么做,但完全可以。太棒了。很酷。那么,在这种情况下,我们现在可以使用。所以,就这些。你们刚刚对一个类进行了模板化。所以,是的。太棒了。

这感觉很像你们模板化一个函数的方式,这很好。所以,现在我们可以使用它了。注意,我们将其命名为 priority_ 单字母 Q 以避免拼写,错误,因为 Q 很难拼写。不,其实也还好。所以。

正如我们在作业中看到的,为了证明我们模板化的类,确实有效,在作业中,我们使用了一个字符串的向量,我们,只是没有定义字符串。是的,谢谢。完美。好的。所以。是的。做得好。做得好,你们。是的。不,捕捉得很好。

捕捉得很好。是的。所以,完美。所以,在这种情况下,我们现在做的是优先队列的类型不再,存储整数。它存储的是字符串的向量。所以,让我们选择这三个非常随意选择的词。然后看看。

对。所以,这是我们最后一次需要做的更改,而不是。是的。所以,实际上,我们想做的是一个每个循环,因为这是一个。

字符串的向量。在这种情况下,我有点偷懒,因为我知道每个只是大小为 1,所以,我们可以直接访问零索引。但我们创建了自己的模板化类并使用了它。做得非常好。这实际上是我们结束的地方。所以,我们还有一分钟。

所以,我实际上会尝试讲解概念,因为,再次强调,概念我们,早在模板讲座中和 Avery 一起讨论过。而且,你们看到的是。对。所以,当我们有一个模板时,我们做了各种隐含的假设。所以。

Avery 给你们的一点提前预览是,在 C++20 中,有一,些称为概念和约束的东西。因此,约束是定义模板后,语法中的内容,你说“requires”,然后提供一系列布尔值。在这种情况下,仅就术语而言。

概念是这些约束的一个名称,集。因此,C++20 中的概念的作用是,它允许我们将那些隐式接,口转换为显式要求。所以,现在,如果你尝试在编译时运行这段代码,如果你传,入的东西没有,例如,输入迭代器。

那么你的代码实际上会,抛出一个错误,表示好的,我甚至不需要你运行这个。我已经知道这不会成功。这非常重要,因为现在,与其收到像“__first 不等于 ,__last”这样的非常模糊的错误信息。

使用 C++20 的概念,你将收到更清晰的错误信息,如“其类型没有输入迭代器”,所以,这就是概念的整个想法。同样,这些可以与类模板、函数模板,甚至类模板的成员函,数一起使用。因此。

任何定义在模板化类上的函数。而且,STL 已经定义了一堆概念。因此,这些只是命名要求的集合。一个概念可能看起来像这样。从 requires 派生,is base of,和 is convertible。

因此,这些都是 C++20 中出现的内容。超级激动人心。你可以编写自己的概念。你可以使用并创建自己的概念。所以,这就是一个非常简要的概述。如果有的话,我会在下节课开始时专门讲一下这个。

但你们已经完成了面向对象编程。所以,祝贺你们。还有一件事。所以下周还有两节课,星期二和星期四。请来。我星期四不会在这里。我总是有一个传统,就是在最后一节课时与我的班级自拍,所以,那将是星期二。

如果你想成为自拍的一部分,请来。我告诉大家,如果你不在那张照片里,我不会记得你。如果我在街上看到你,我会忽略你。明白了吗?哦,对了。所以,我想我有两个在这门课上的学生,但他们不再来了。所以,我不知道。

他甚至不记得他们是谁。好了。总之,是的。确保你星期一来。不,是星期二。视频中的人,请星期二来。太棒了。是的。祝你们周末愉快。如果你们想,可以多拿一些果冻。是的。然后见到你们最后两节课。

谢谢。你们有什么想法?你有问题吗?

是的。

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P17:[23]CS 106L Fall 2019 - Lecture 16_ RAII and Smart Pointers (Screencast) - GPT中英字幕课程资源 - BV1Fz421q7oh

你看到的这瓶水,我用完了Itoen,所以现在里面装的是水,第二件事,实际上我明天会提到面试的事情。接下来,我今天确实有一个面试,我被问到了关于RAII的内,容。所以这是一个重要的话题。

这是你必须了解的最重要的C++习惯用法。好的。所以我们今天要讨论这个话题。在我们开始之前,我想让你看看这个例子。这只是一个非常简单的函数。问题是,这个函数里有多少个代码路径?代码路径的意思是。

当你执行这个函数时,控制流有多少种,不同的方式可以进行并退出函数?这讲得通吗?你能通过这个函数多少种方式?你可以通过这个函数并退出的不同路径有多少条?是的,例如,一条路径可能是,你。

一条路径可能是你完全跳过if语句,直接返回。这是一条代码路径。这段代码里有多少条代码路径?好的,我听到的是三条。还有其他答案吗?有趣的问题,没有额外信息。六条,有趣。好的。

我想知道你们这些数字从哪里来的。好的,有趣。好的,谁认为是1?好的,没有人说1。2条?2条?好的,那么3条?4条?有趣。超过4条?超过10条?好的,好吧,你们都错了。答案绝对是超过10条。但首先。

我们先来看一下最明显的三条路径,好吗?所以这个问题最常见的答案是三条。我们快速浏览一下。所以你从开始处开始,好吗?你评估它,结果是假的。所以我们评估另一个表达式,结果也是假的。所以我们返回。

这是一条代码路径,好吗?第二条代码路径,我们开始这个函数。这部分返回真。当这个返回真时会发生什么?是的,确切地说,短路。由于短路,你不会评估其他部分。哦,不,实际上,如果第一个是假的,第二个是真的。

那么你,评估这一部分,然后退出,好吗?第三部分就是你说的那样。如果这个是真的,那么我们不需要评估第二部分。所以你评估这一部分,返回,好吗?所以三条代码路径。我只是好奇,第四条是什么?

对于那些回答四条的人,第四条是什么?哦,好吧,我明白了。还有其他代码路径吗?你提到六条。好的,那么还有其他代码路径吗?嗯?是的,好的。好的,那么它们来自哪里?提示是。

你可以假设这些调用是像原子操作一样的。就像你只是进行函数调用。不要过多考虑调用中发生了什么。有没有其他方式可以在返回语句之外退出这个函数?你可以抛出错误,对吧?你可以抛出异常。没有任何额外信息的情况下。

这段代码可能在多少个地方,抛出异常?所以答案是23减去3,即20。这段代码在20个不同的地方可能会抛出异常。好,让我们快速浏览一下代码,好吗?所以员工 e,在这里可能会抛出异常吗?好的。

那么在这里调用了什么函数?在这里调用了一个函数。是的,正如你所说,拷贝构造函数被调用了,如果输入无效,那么这可能会抛出异常。好的,这是一个地方。抛出异常的下一个地方是哪里?E。title。

那里可能会抛出异常。好的,还有其他地方吗?即使这是等于等于,理想情况下这可能有效,但你实际上不,知道 e。title 返回什么。它可能返回一个字符串。它也可能返回不是字符串的东西,然后用户重载了等于等。

于。明白了吗?不知道你为什么想这样做,但可能是可能的。所以等于等于也可能抛出异常。同样的情况,stally 可能抛出异常,大于号也可能抛出异,常,如果用户重载了这些运算符。好的。

你没有被告知任何额外信息。或运算符可能抛出异常吗?所以你实际上可以重载或运算符。我不知道你为什么要这样做,但你可以重载或运算符。这是另一个可以抛出异常的地方。好的,那这些呢?这里可以抛出异常吗?好的。

你可以在这里抛出异常多少次?所以 e。first,你可以在那里抛出异常。E。last,你可以在那里抛出异常。还有其他地方可以抛出异常吗?好的,每次调用流插入运算符时,你都可以抛出异常,对吗?你可以。

假设你尝试将某些内容打印到 cout,然后你的对,象无效。所以这些也可能抛出异常。一、二、三、四、五。这些都可能抛出异常。还有其他地方吗?好的,是的,e。first 可以抛出异常,e。

last 也可以抛出异,常。加法运算符可能是用户定义的,对吗?因为即使这是一个字符串,那么字符串加字符串,好,这是 ,C++ 的事情,不会抛出异常。但你可能没有字符串,这可能不是一个字符串,加上一个字。

符串。那么用户定义了,加法运算符。所以你可以在那里抛出异常,也可以在那里抛出异常。明白了吗?还有一个地方。返回,返回调用了什么?返回调用了字符串的拷贝构造函数。我认为字符串确实有,对于某些值。

字符串确实会抛出异常,对吧?所以答案是至少23。好问题,可能会超过23吗?哪些函数可能抛出异常?我们还没有讨论析构函数,对吗?所以有很多析构函数在进行。有这个,我认为有一个析构函数。我不确定其他的。

当这个员工对象到达这里时,也会被析构。所以这也可能抛出异常。通常我们假设析构函数不会抛出异常,因为如果析构函数,抛出异常,会发生一些奇怪的事情,明白了吗?所以如果你把这些都加起来。

至少有 23 条代码路径,对吧,三个明显的,再加上 20 条由于异常导致的。现在你可能会想,嗯,那又怎么样呢?有 23 条代码路径,这有什么关系呢?好,跟进的问题。假设你有这段代码,问题是。

你能保证这段代码没有内存泄,漏吗?哦,我的错,那不是 delete,那应该是 E。好,把这个改成 E,对不起。我复制粘贴错了,这应该是 E。所以它在删除指针。我可以做一个标记吗?把这个改成 E。好了。

好吧,你在这里调用 new,所以我们在堆上分配了一些东西,我们对它进行了一些操作。好,这些应该是箭头。对不起,我昨晚做这个时很晚了。好,这些应该是箭头,这些应该是箭头,这些都应该是箭头,好。

这里有内存泄漏吗?所以,我在这里分配了堆上的东西。我能保证在这个函数结束时,我已经释放了内存吗?对于那三条正常的代码路径,没问题,因为你调用了 ,delete result。

你在这里调用了 delete eat。所以当你调用 new 时,你在堆上分配了内存。当你调用 delete 时,你释放了堆上的内存,对吧?所以对于我们讨论的那三条正常控制的代码路径,这没问,题。

那其他的 20 条呢?例如,如果我在这里调用 new,然后如果我尝试做 E arrow, title,如果这抛出异常,我们就会退出这个函数,甚至不,会到达 delete。这样说清楚了吗?对吧?

为什么这 20 条代码路径重要,因为如果我们经过这 20 ,条其他代码路径,它们可能不会到达 delete E,这样就会,有内存泄漏。所以这是个大问题,考虑到你现在拥有的工具,你实际上不。

知道怎么解决这个问题,对吧?是的,有问题吗?为什么呢,因为这只是一个函数。这仅仅是构造了一个新的泄漏。那么仅仅有这个 delete,是不是不好?是的,我认为 106b 讲过内存泄漏,对吧?

106x 讲过内存泄漏。等等,Cynthia 没有讲过内存泄漏?什么?好,嗯。好,是的,这实际上是个很好的普遍性问题。为什么内存泄漏总体上不好?有几个不同的原因。

其中之一是实际上有大量的安全漏洞是基于内存可以泄漏,的事实。我现在不详细讨论这些,但如果你搜索一下,你会意识到这,种不确定行为通常是非常糟糕的。另一个你可能更熟悉的实际问题是,比如说,在这里我们只。

创建了一个新的员工。但假设我们恰好调用这个函数一百万次,比如在 Google ,这样的大型公司,他们每天有超过一百万次交易。在这种情况下,假如你没有创建这个指针,你的计算机会把,它当作无法再分配的内存。

这意味着如果你调用它一百万次,而你的计算机没有超过,一百万次的空间,比如说一个字节的空间,那么你的计算机,将会用尽RAM。这就是内存泄漏出错的一个例子。

还有很多其他大小不一的解决方案来处理内存泄漏的问题,这是一个有效的问题,但确实,它是编程中的一种大经典规,则,说的是我们不希望出现内存泄漏。好吧,我们要不要快速回顾一下什么是内存泄漏?

只是一个简单的定义,对吧?好吧,因为如果你现在正在做瓦片操作,你不希望瓦片中出,现内存泄漏。你们现在都在做瓦片操作,对吧?好吧,不要在瓦片中出现内存泄漏。是的,所以内存泄漏是当你。

知道你通常在栈上声明变量,它们的生命周期是那个函数调用,对吧?然后当函数完成时,那块内存就会被释放。如果你使用关键字new,就像你们在处理列表节点时那样,它们是在堆上分配的,所以这些对象会持续存在。

好帽子。所以它们的生命周期会在函数完成后仍然存在。问题是,因为如果你在堆上声明东西,它不会自动被释放。你必须自己释放它。想象一下如果我们不断尝试分配更多的内存,然后如果你,继续在这里分配内存。

这个指针将在函数完成后不再存在,但指针最初指向的是在堆上分配的内存,对吧?所以如果函数完成了,你不再有这个指针。你只是有在堆上分配的内存,你无法访问,但操作系统不知,道你必须释放它。

所以delete关键字的作用是,它本质上告诉你的操作系统,可以重新使用那块内存。否则,你的系统将假定它仍在使用中。我不这样认为,对吧?我不这样认为。我说过“是”吗?

我认为这是谷歌浏览器变得非常慢的原因之一。所以如果你打开很多标签页,然后如果你不完全退出谷歌,浏览器,它会随着时间变得越来越慢。是的,我记得我在某处读到这个原因是因为它有大量的内,存泄漏。现在。

这不是特别重要。它仍然可以工作,但只是你会有内存被浪费。它不能再被使用,但也没有被回收,明白吗?所以这就是为什么内存泄漏是不好的。是的,所以这里的问题是你不能保证这个函数没有内存泄,漏。

因为如果代码在这里的任何地方抛出异常,从这里到这,里,如果在这里的任何地方抛出异常,那么你将跳过结果,删除E这一行,对吧?所以这个函数可能有内存泄漏,特别是如果抛出了异常。好吧,我们怎么解决这个问题?

更普遍的关注点是,通常在C++中,有一些资源,你可以获取,它们,但一旦获取了它们,你必须记住稍后释放它们,以便,其他程序或函数能够使用那块内存,对吧?所以堆内存就是一个例子。

你可以使用new来获取堆内存,然后可以使用delete来释放它。是否有其他资源,如果你获取了它,你必须记得释放它?有一个大资源,我们在 Windows Excel 中讨论过。PDF 文件?对,嗯哼?

正是如此,流,明白吗?在流中你什么时候获取东西?例如,文件流就是一个例子。它们获取什么?文件流在打开文件时获取某物,对吧?当它调用 。open 时,它拥有那个文件的使用权。它可以从文件中读取。

然后当你读取完文件时,你必须记得,做什么?你必须关闭文件,对吗?好,所以是的,这些是其他一些例子。文件,当你打开一个文件时,你必须记得关闭它,好吗?类似地,还有其他东西。这些东西叫做锁。

这里有 110 的人吗?没有,好吧,其实,Anna 可能会在下次讨论多线程,但锁本,质上是当你有多个程序尝试同时运行时。你有时需要一个锁来防止它们同时竞争同一个数据结构。

所以如果它们都试图编辑同一个数据结构,你需要使用锁,来防止一个在另一个写入时访问。这样说清楚了吗?所以锁做的就是这些不同的函数,它们可以尝试获取一个,锁,实际上就是它们拥有了锁的使用权。

其他线程不能做任何事情。然后当它们用完时,它们必须解锁,好吗?你可以想象如果你锁住了某物但没有解锁会有多糟糕,对,吗?其他的,是的,套接字,我们不会讨论太多套接字。所以作为一个快速插曲。

我们不会深入讨论异常,但异常只,是你将控制权和信息转移到一个叫做异常处理器的东西的,方式,好吗?你在 106B 中使用过异常,throw 关键字?对,好。你从未需要编写这个 try-catch。

try-catch 的东西,但我,认为主文件的起始代码中有一个 try-catch,以便如果你,在这里抛出异常,它能捕捉到这种异常,好吗?这是在 106A 中教授的,如果你用 Java 学过。

隐约记得这个,也许?好,所以,是的,这是处理异常的一种方式。我们不会过多讨论异常,因为 106B 会教这个,而且它们也,很容易弄清楚。这实际上只是一堆语法,所以我们不会深入探讨。但是异常安全的概念。

你必须小心如果抛出异常,这是面向,对象编程中一个非常重要的概念。好,所以在这里,我们不能保证这个 delete,我必须更改所,有这些,这个 delete 在函数退出时被调用,因为你可能在,这里退出函数。

这样说清楚了吗?有任何问题吗?你有什么问题?是吗?异常是否仍然连接到程序中?不,因为你可以在抛出时退出函数,但如果你将其包装在 ,try-catch 中。

如果你可以将任何代码包装在 try-catch ,语句中,如果你抛出某物,那么控制流会立即转到这些 ,catch 语句中的一个,具体取决于你抛出了什么异常。如果像这样,异常会抛出当前函数。

但接着它会去下一个标,签吗?实际上,你可以在这里放任何东西,比如抛出语句,如果你,抛出异常,那么这些中的一个会捕获它,具体取决于是什么,类型的异常。明白了吗?是的,我们不会深入探讨它们如何工作。

因为它们真的很容,易用 Google 查找。异常没有什么太难的。这只是语法问题。但是如何保护以应对异常的思想非常重要。那么我们如何保证一个类释放资源,不管是否有异常发生?所以即使在这里抛出异常。

我还是要确保释放我在这里分,配的堆内存。明白了吗?顺便提一下,还有一个叫做异常安全的概念,其中一种解决,这个问题的方法是你可以说,这些函数是不允许抛出异常,的,明白吗?你可以告诉一个函数你不能抛出异常。

那就是我们在 no throw 关键字中做的事情。记得那个 no throw 关键字吗?我在几节课前有点困惑?是的,有些函数绝对不应该抛出异常,因为这会导致未定义,的行为。例如。

如果你在析构函数中抛出异常,那么你会有点疑惑,这个对象到底是已经被销毁了还是仍然存在,对吧?如果你在析构函数中半途而废,那是什么意思?是的,因为这实际上没有真正的逻辑意义,你不能在析构函,数中抛出异常。

交换函数、移动构造函数也是一样,你不允许在这些函数,中抛出异常。好吧,这就是所谓的 no throw 异常保证。还有其他较弱的保证类型,如强异常、基本异常、无异常,今天。

我们想尝试编写满足基本异常保证的代码,在这种情,况下,即使抛出异常,你的程序仍然处于有效状态,你仍然,可以从中恢复,明白吗?好吧,另一种避免这样问题的方法是完全避免异常。

这段代码来自 Google 的风格指南。所以 Google 不使用异常,至少对于 C++ 是这样。好吧,他们的理由其实很有趣。你可以在这里阅读更多内容,你可以查看链接,但原因是他。

们在最初开发 C++ 时忘记了这一点,所以他们甚至不打算,去碰这个问题,明白吗?处理异常困难的地方在于,如果你的代码中有些地方处理,异常不当,那么如果你编写新代码,你真的不能抛出异常。好吧,明白了吗?

所以一些公司实际上禁止使用异常,但异常非常强大,因此,你仍然应该学习如何使用异常。是的,有问题吗?嗯嗯,对,因为这里的原因就是如果你允许异常,你不能保,证释放这个资源。是的,嗯嗯,没错。例如。

这样你就不能安全地使用文件流,因为如果抛出异常,流可能仍在访问某些文件。是的,这很糟糕。所以如果你想使异常可用,你必须确保你能够处理这个问,题,明白吗?好的,我们来讨论一下如何处理这个问题。好的。

这是一种叫做RAII的技术。它代表的是资源获取即初始化。好的,我有一个旁注吗?好的,是的,我这里有一个旁注。父亲是C++。我忘了怎么念他的名字。这是他对为什么叫RAII的解释。好的。

我将很快解释RAII是什么,你会发现这其实不完全准,确,但也算有点道理,好吗?一个更好的名字是SVRM。另外,M不应该被突出显示。R应该被突出显示。作用域基础内存管理。所以,作用域在这里非常重要。

我们将使用作用域的思想来自动释放内存。我最喜欢的名字是CADR,C-A-D-R-E,CADR,CADRE?CADRE,是的,CADRE。是的,它代表构造函数获取,析构函数释放。仅从这个。

你能猜到RAII是什么意思吗?嗯,是的。这是最好的名字了。简而言之,这意味着什么?好的,是的,嗯。所以,当你创建对象时,你知道对象创建时会有构造函数,当对象超出作用域时会有析构函数,好吗?

因为析构函数总是在你超出作用域时被调用,你可以把释,放资源的代码放在析构函数中。这有意义吗?好的,所以,总结一下,如果你回到这里,如果你把删除操作,放在这里,问题在于即使内存超出作用域,它可能也不会被。

调用。所以,更好的做法是你可以把释放资源的代码放在析构函,数中。这样,无论如何,如果你退出函数,资源将会被释放。问题,是的,嗯,嗯。这是个好问题。所以,你通常会,有一些方法可以解决这个问题。

有一种方法是你可以设置自己创建作用域。你知道你实际上可以把大括号放在那里,它会创建自己的,作用域吗?是的,所以,你有时候会想,为什么有时候会看到随机的大,括号在那里?原因是因为那些创建了一个内部作用域。

如果你在其中声明对象,它会在函数结束时释放。你需要小心在析构函数中进行释放,同时在其他地方进行,释放,因为那样你可能会遇到问题,比如,你是否会释放两,次?你是否只释放了一次?是的,所以,嗯,继续。哦。

是的,所以,通常你可以仍然只在析构函数中进行释放,但你可以潜在地调整作用域。这有意义吗?好的,我知道这真的很无聊。你们看起来很无聊,但这是一个非常重要的,哦。这是一个非常重要的例子。好的。

这与RAII无关,但既然我们谈论了糟糕的缩写,这里,有另一个,我是认真的。这是一个实际的习语。它叫做指针到实现,他们决定把它命名为PIMPL。潜在的最后一个话题,如果你们真的感兴趣的话。是的,所以。

实际上,在类本身中,你会看到PIMPL,它确实有,意义。这意味着指向实现的指针,所以你实际上会在代码中看到 ,PIMPL。好的,如果你在任何代码库中使用 Control-F,你会看到 ,PIMPL。

如果你输入 PIMPL,你会看到它出现很多次。好的,你永远无法摆脱它。酷,RAII 的想法是,如果你获取了一个资源,你应该总是在,构造函数中进行。如果你释放资源,你应该在析构函数中进行。明白了吗?

理由是,你要把它放在构造函数中,这样就不会有半有效状,态。一旦构造函数完成,资源就可以使用了。然后析构函数,你总是在它超出作用域时调用析构函数,所,以你应该把资源的释放放在析构函数中。举个例子。

你在 CS106B 中学过这段代码,对吧?这有点熟悉吗?对,你正在打开一个文件,然后逐行读取,打印,然后关闭它,这符合 RAII 吗?为什么不符合?嗯,对,你会发现你在这里获取资源,不是在构造函数中。

而,是在一个单独的函数调用中获取的。然后你释放资源,不是在析构函数中,而是在一个单独的函,数调用中释放的。这很有趣,因为,比如说,你有时会忘记调用关闭,但如果你,忘记调用关闭,你的代码仍然可以工作。

好吧,谁实现了流库,在析构函数中,他们会为你关闭文件,如果你还没有关闭它的话。嗯,对,所以这是正确的 RAII 代码。文件流库已经符合 RAII 规范。只是为了向后兼容,他们提供了一个打开和关闭函数。

以免,破坏现有代码。但是你不应该使用打开和关闭。嗯?不,你根本不应该使用它。你应该立即在创建 if 流时直接传入文件名,这样在这个,对象创建时,if 流是有效的,好吗?这也确保了没有无效的代码。例如。

如果你在这里插入一些东西,那可能会无效,因为你,还没有实际获取资源。所以,把它放在这里,你可以保证每当你使用输入时,它在,这个点是有效的。然后我们不需要关闭调用,因为流的析构函数会释放它,好,吗?

有一学期,我和一个讲师争论,他们决定如果你忘记调用关,闭,就扣一分。然后我说,好吧,你不应该调用关闭,好吗?是的,所以,你不应该调用关闭。如果你调用关闭也没关系,但实际上,if 流类是符合 RAII。

规范的。所以即使你抛出异常,比如在这里,你也能保证即使你离开,这个函数,流仍然会关闭。好的,这是另一个例子。我们不想多谈锁,但互斥锁基本上就是一个锁。所以,你可以说,好吧,我想锁定锁,修改数据库。

然后解锁,数据库。这符合 RAII 吗?不符合,因为你实际上有一个获取和释放的函数调用。好的,那么,有什么更好的方法?你可以使用一种叫做锁管理器的东西。好的,锁管理器真的很可爱。它只有一个功能。

它基本上获取锁,然后它基本上一直持有锁,直到锁保护器,被析构,然后它释放锁。它没有做其他任何事情,只是获取锁,持有锁直到它的析构,函数被调用,然后释放锁。这样,即使你在这里抛出异常。

你也可以保证始终释放锁。如果你们上升到110,你们肯定会使用锁保护器。这是实际的 C++ 代码。好,酷的事情。我今天有个面试,他们真的问我,为什么使用锁保护器?我当时想,哦,是的,我该拿出我的幻灯片吗?

好,实际上,人们确实,这实际上是一个超级重要的事情,所,以锁保护器非常重要。好,我想讨论这个吗?好,当然。你认为锁保护器是如何实现的?它看起来是这样的。它实际上有两个方法,一个构造函数,接受锁。

并保存锁,记,得锁住锁,然后在其析构函数中解锁锁。问题,嗯?这个锁实际上是这样做的,对吧?哦,好,好,这是一个好的,好的,让我做一个例子。所以假设安娜,好吧,为了模拟一个多线程程序,假设安娜。

和我都在尝试访问这个糖果罐,好吗?你的工作,安娜的工作是计算里面有多少块糖果。我的工作是从罐子里取糖果。我想确保艾弗里有一块新的。好,所以想象我们两个同时尝试去做,对吧?所以你在尝试计算。

而我在尝试拿东西,对吧?是的,所以这种情况会破坏数据结构的完整性,因为你在尝,试计算东西,而我在尝试从中移除东西。是的,所以。这是一种情况。是的,锁的作用就像是一个许可条。是的,很酷。比如说。

谁拿到这个许可条,然后其他人就必须等到我释放,它。把它想象成一个许可证。你必须拿到许可证才能对数据结构进行操作,然后每次完,成后,你必须释放它,以便其他人可以使用这个许可证,好,吗?

为什么不使用 RAII 锁保护器,而可能导致你的解锁函数,永远不会被调用是如此危险,因为这意味着如果,假设,只,要一个线程在解锁函数调用之前退出,那么没有其他线程,可以再次访问它。是的,所以类比是。

没有人能进去。是的,所以类比是,如果我尝试拿这个,然后我突然绊倒,摔,倒,躺在那儿,那么这个罐子会在那里,但安娜永远无法拿,到它。安娜会一直等到永远。哦,是的。哦,我只是想说 RAII,但继续。是的。

所以,对,这是一个好的事情。当程序结束时,锁系统,或者说,一切都会像那样被重置,嗯,是的。所以我们可以重新启动你的程序,但是。对,不,你真的不想重新启动谷歌,对吧?正是如此。是的,所以是的。

确保遵循这些,以便,即使我摔倒,这个程,序也能返回到这里,其他人仍然可以接手程序。当你说“永恒”时,你通常指的是,像,持续时间。问题,嗯?所以,比如,我们有,还是说?是的,当你调用 unlock 时。

其他线程就能访问,嗯哼。在这种情况下,对的。所以如果你需要这个,那就是下一个问题,我们可以定义一,个值超范围,这样那个构造就会被调用,你可以让其他线程。

访问它。我可以试着输入它。

那打开了,嗯,好吧。快完成了。哦,快速插个话。互斥量是特殊的。它们不能被复制,也不能被移动。对,因为想象一下,如果你尝试复制它,那就会破坏整个系,统的完整性。对,如果我制造多个许可证。

那就违背了这个目的。如果你能够把它移动到其他地方,那也违背了这个目的。所以互斥量不能被复制,这就是为什么你必须使用这个引,用的原因,这也是初始化列表很重要的原因。回到正题。好,嗯。总结一下。好。

所以问题在于内存,这里是去年发布的一篇文章标题。

只要看日期。是的,这只是一个半心半意的愚人节笑话,但其中确实有些,真相。好,C++ 将不再有指针,我会很快讲解这个将内存管理变成,自动的 RAII 想法。

好,所以如果你查看 C++ 的风格指南,它说你不应该显式,调用 new 和 delete。就像你不应该调用 open 和 close,你也不应该调用 lock, 和 unlock。

你不应该显式调用 new 和 delete。

所以,轮到安娜了。这实际上非常重要,这也是我们今天结束时讨论的最后一,个内容。就是说,不是现在,而是在我完成我的幻灯片之后。哦,你在利用你的时间。是的,不。好,只想再提一点关于自动内存管理的事情。

人们总是对 C++ 进行批评,因为它没有内存管理,但一个,反对的论点,你可以告诉你的 Python 朋友,是自动内存管,理有时并不是一个好事,好吗?例如,在 Java 中,Java 有一个垃圾回收器。

垃圾回收器的问题在于你实际上不知道对象何时会被释放,没有确定对象何时会被释放。对象会被释放,但你不知道何时会发生。这是不确定的。与 RAII 不同,RAII 保证析构函数会在对象超出作用域时,被调用。

这时对象将被释放,好吗?所以,这就是为什么 Java 不好的一个原因。好,我讲完了。哦,是的。我应该保留这些注释记录吗?好,太棒了。所以,这就是为什么艾弗里和我都认为,这是最重要的讲座,之一。

因为这是现代 C++ 从每一代 C++ 中出现的最大风,格变化之一。所以,我认为这是从 BRX 学到的东西到实际行业中使用的,最大风格差异之一。它回到了你刚刚看到的整个事情,即现在使用现代 C++,你。

几乎不会使用 new 或 delete。这很疯狂。而我们不需要这样做的原因就是我们接下来要讲的内容。所以,作为刚才讲座的简要回顾,我们到目前为止已经看到,文件读取和锁的操作可能不符合 RAII 原则。

在这种情况下,这两个命令是打开和关闭,或者锁定和解锁,我们修复它的方法是创建一个新的 C++ 对象,确保在析构,函数中释放资源。在这种情况下,它是一个包装对象。在这里,IF 流已经是一个包装对象。

在这种情况下,一个新的类叫做锁保护,正如 Avery 说的,它唯一的作用是在构造函数中获取锁,在析构函数中释放,锁,确保它总是被释放。所以,还有一个地方可以应用这个方法。你们已经见过这个了。那么。

多少人对此感觉熟悉?有人见过这个吗?好的,完美。这是符合 RAII 原则的吗?不符合。好的。正是如此。原因是,因为正如我们之前看到的,如果在处理我们创建的,指针时抛出了异常,我们不能保证 n 被删除。

那么,大家有什么想法关于我们如何修复这个问题,从一般,的角度来说?大声说出来。是的,Brendan。是的,不,稍微大声一点。那么,我们如何确保它在退出时?在什么析构函数中?啊,我们可以。

所以这是一个好的,然而请注意,在这种情况,下,我们想要删除的不是节点本身。我们并没有对节点调用 delete。我们要对什么调用 delete?对指针,确切地说。所以,按照相同的思路。

修复的方法是将其包装在另一个 ,C++ 对象中,这个对象为我们完成所有这些逻辑。在这种情况下,完全相同的思路。我们将把它包装在一个叫做智能指针的 C++ 对象中,它将,确保它为我们做相同的事情。所以。

今天我们没有时间,但我会在讲座后发布代码。你不想编辑以删除代码吗?不,不,我们没有时间做这个。所以,我们没有时间,但讲座后,我会发布代码。它基本上只是向你展示这些包装类并没有什么神奇的地方,本质上。

它们做的只是,在构造函数中获取资源,在析构函,数中释放资源。所以,如果我们愿意,完全可以自己构建这些包装类的实现,但幸运的是,C++ 已经为我们内置了一些。你们将在 C++ 代码中看到这三个术语。

特别是 unique ,pointer 和 shared pointer,几乎无处不在。我们将一个一个地介绍这些。结果是,还有许多其他智能指针。在之前提到的 boost 库中的这些额外库中也有。

你们还可能看到一个叫做 auto pointer 的东西,但它实,际上在 C++ 11 中已被弃用,并在 C++ 17 中完全移除。所以,是的,基本上你永远不想使用 auto pointer。

但这实际上是在 C++ 98 中引入的,这表明 C++ 创建者早,在很久以前就开始考虑 RAII 和异常安全代码的想法。那么,unique pointer 是什么?独占指针。

你可以基本上将其视为指针的锁守卫等同物。因此,它将像许可单一样唯一地拥有其资源,正如Avery所,说,并且在对象被销毁时,它将在析构函数中删除该资源。我们很快会回到无法复制的问题。所以,再次说明。

以前我们有这个显式的new和delete,现在,我们可以使用C++中的一个类,称为独占指针,它保证当对,象本身超出作用域时,内存将全部释放。有人知道为什么这不能被复制吗?类似于Avery说的,嗯。

后面那位。完全正确,再说一遍。如果你可以复制它,那么它将不会,正是这样,它不会唯一,地拥有资源。为了让你明白为什么这会是一个很糟糕的事情,假设我们,尝试复制独占指针。所以。

假设我们有一个指向堆上某些数据的独占指针。并且假设我们能够复制它。所以,我们有一些复制y,它仍然指向堆上的相同数据。然后假设y超出作用域并删除了堆数据,因为,再次,任何时,候析构函数被调用。

它会删除它所指向的内容。那么x会发生什么呢?好吧,如果我们然后尝试解引用x,或者当x超出作用域时,当它尝试删除堆上的数据时,这些数据已经不再存在。因此,你会崩溃,这被称为双重释放,实际上还会引发一系。

列其他安全漏洞。我们感到遗憾。好的。所以,这就是为什么这个类,独占指针,不允许复制。另外,作为复习,你如何告诉一个类不允许复制?我们如何在语法上做到这一点?是的,请继续。所以,这是其中一种方法。

你可以在拷贝构造函数和析构函数中手动告诉它,去死。事实证明,还有一种方法可以在语法上做到这一点。我实际上没有写出语法,但Avery实际上在今天之前的幻灯,片中展示过,对于我相信是锁守卫的实现。所以。

你会看到你可以,正如你可以让构造函数设置为默认,一样,你也可以将构造函数设置为删除,这样它就会被删除,你可能会看到很多时候,如果你去探索你的Stanford代码,你会看到很多类。

他们做了叫做删除拷贝构造函数和拷贝,赋值的操作。是的,所以,这只是你如何删除的语法方面的内容。但这会是个问题,因为如果,比如说,你一直在处理代码,你,可能会意识到,好吧。

有时候我确实希望有多个指针指向同,一个对象。比如说,我为什么只能对一个对象有一个指针?这几乎看起来违背了指针的目的。似乎不够灵活。是的,好吧,我没有复制这些更改,但我们的答案是C++已经。

预见到了这一点。实际上还有另一种智能指针的类,称为共享指针。简要背景是,共享指针的目的是执行相同的RAII合规性,以,确保资源始终被删除,但在这种情况下,该资源可以被任意,数量的共享指针共享。

只有在没有指针指向它时才被删除,例如,这可能的语法用例是,我们声明一个指向新 int 的,共享指针 p1,然后,您会注意到,我们使用了 Avery 提到,的内部作用域。

其中我们使用两个大括号来表示这应该是,一个单独的作用域。在这种情况下,我们声明另一个共享指针,并注意到要声明,另一个共享指针,我们使用了拷贝构造函数。所以,现在我们有了 p1 和 p2。我们到达这行时。

p2 超出了作用域,然后我们到达这行时,p1 超出了作用域,最后,现在没有指针引用那个新 int,因此,int 被释放了。这样说清楚吗?有没有人对共享指针有任何问题?好,所以,你可能会问的一个问题是。

是的,再次重要的是,你要声明共享指针的方式是,在创建第一个指针后,使用拷,贝构造函数声明所有后续的指针。所以,一个问题可能是,这些是如何实现的?为什么 unique pointer 不做同样的事情?

事实证明,有一种称为引用计数的技术,它本质上遵循了你,的直觉,即在我们的共享指针类中,我们有一个计数器来跟,踪对同一堆数据存在多少个指针,因此,实际上,你的做法,是每次拷贝构造函数或拷贝赋值被调用时。

计数器增加一,每次任何析构函数被调用时,计数器减少一,最后,只有当,引用计数为零时,才释放堆数据。所以,在我们之前的例子中,啊,哎呀,好吧,所以,在我们之,前的例子中,当 p1 被声明时。

我们将引用计数设置为一。在这里,拷贝构造函数被调用,因此我们将其增加到二。在这里,p2 的析构函数被调用,因此我们将其减少回一,最,后,在这里,p1 的析构函数被调用,因此减少到零,因此,我。

们知道可以安全地删除堆内存。对这一点有任何问题吗?没有,太棒了。好,所以,我会完成这部分内容,然后,我们可以在最后一讲,中完成剩余的内容。首先,请注意我们之前的例子,其中我们使用了 unique 。

pointer 来处理我们之前不符合 RAII 的函数,这仍然有,效。我们可以使用共享指针,实际上,这个函数仍然可以完美地,工作。为什么会这样?为什么我们可以在这里使用共享指针,并且它仍然有效?是的。

Zach,继续。完全正确,正是如此。对于共享指针,你不必有多个引用,但它确实允许你有多个,引用。完全正确。太棒了,然后,最后,实际上在 C++ 中还有一种称为弱指针,的东西。它的作用类似于共享指针。

但它不会增加你的引用计数。所以,它允许你有另一个引用,但不会增加你的引用计数,这没关系,因为有一些其他的实现细节,弱指针。我们不会深入探讨这些内容,因为这只是额外的材料,但如,果你有兴趣使用这样的东西。

现在你知道它的作用是什么,可以查看 C++ 文档,你会理解如何使用它的细节。好,所以,我会在下一次讲解这个最后的部分。

在我们离开之前,有一件事我们要做,Avery 星期四会不在,城里,所以,如果 Avery 想解释一下的话。所以,我可以和大家一起拍个自拍,所以,请。

嗨。嗨。对,你看不到,你在这里,有多少人,15?15 人中的 15 人,好吧,是的。所以,学期开始的时候,我在想,好吧,这张自拍会很难,但,实际上并没有比普通自拍更难,因为在讨论班上,人们通常。

都会到场。好吧,往前站。你觉得我们应该在哪里拍照?这里?对。这里?还是那边?我们可以让大家坐在这一部分。对对,这样我们可以拍到高度。但这边有点亮。哦,我明白了,我明白了。哦,好吧,这样就好。大家。而且。

我觉得,哦,是的。好吧,你们,笑一笑。如果你能看到相机,相机就能看到你。好吧,大家都在相机里吗?确定,大家都在相机里吗?是的,我会拍很多照片,其中一张大家的眼睛会睁开。好吧,一张搞笑的照片?来吧。等等。

你想做吗?不,我不想做。好吧,认真一点的照片。认真一点的照片?认真,好吧,搞笑的照片。快速问一下,你带这个只是为了自拍吗?不,不,是为了自拍。好吧,好,酷。好吧,再来一张。你对,怎么说呢。

你的期末考试有什么感觉?走吧。期末考试?我不参加。期中考试。好吧,期中考试,当然。绝对不参加,绝对不参加。好吧,好吧,谢谢。好的,做得很好。非常感谢。好吧,在你们离开之前,关于最后一节课的后勤安排。

传统,上我们会发送一个调查问卷,或者说我们,而不是我,我们,会发送一个调查问卷,询问你们希望我在最后一节课上讲,什么内容。这可以是任何东西。可以是多线程,可以是区块链。

不知为何,一直都很受欢迎。

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P18:[24]CS 106L Fall 2019 - Lecture 17_ Multithreading (Screencast) - GPT中英字幕课程资源 - BV1Fz421q7oh

好的,完美。

好的,我再给一分钟。和上次一样,他们没有能从 VGA 接到笔记本电脑的适配器,所以我们要通过屏幕共享来做。所以我把链接发到 Piazza 上了。实际上课程开始不需要任何屏幕。所以这将是一个惊喜演示。

是的,感谢你们所有人在介绍表格中提交的糟糕的爸爸笑,话。是的,我保证所有这些都来自你们。我觉得这个特别元。所以我想把它用作最后一个。好的。好的,酷。所以这次讲座挺令人兴奋的。所以首先。

欢迎来到 CS106L 的最后一次讲座。这是一段美好而漫长的旅程。但是,我们会讲到的。但首先,感谢你们为最终讲座主题投票。如果能来点鼓声就好了。好的,谢谢。最终选定的主题是多线程。实际上非常有趣。

除了一个人之外,每个人实际上都选择了多线程作为他们,想要涵盖的主题。所以我很高兴。紧随其后的是制作文件,我们今天没时间讲了。所以相反,我会发布一个 PPT,里面有我们本来要讲的关于,制作文件的内容。

所以是的,如果你们感兴趣可以看看。好的。好的。所以因为这是最后一次讲座,我想同时我们也尝试一些有,趣的东西。所以我能找三个志愿者吗?好的。我看到两个。还有一个。好的。好的。好的。准备好了吗?

你们要到前面来。这可能会占用课程开始的前五分钟左右。好的。所以在我们今天进入线程之前,我真的想让你们了解一下,为什么多线程到底重要?所以稍微介绍一下背景,很多时候当人们想到多线程时,他。

们实际上想到的是多处理。他们想到像数据中心里的很多并行计算机之类的东西,在,那里他们可以同时运行操作。但多线程有不同的用例。例如,假设我们只有一台只有一个 CPU 的计算机。

在那种情况下我们为什么要做多线程?因为在那种情况下,似乎你仍然一次只能运行一个线程。那么多线程的意义是什么?所以为了给为什么即使在那种情况下多线程仍然有用提供,一种动力,我们要做这个现场演示。好的。

所以今天的演示,将会发生的是,让我看看,谁想当第一个,写作者?好的。伊娃。所以伊娃的任务是在黑板上写 1 到 10 这些数字。好的。所以你先别开始。你先别开始。这个的运作方式,然后,好的。

假设我们还有另一个想当另,一个写作者的人。好的。完美。扎克将成为第二个写作者,他也试图在黑板上写 1 到 10 ,这些数字。事情是这样的,我将会是决定谁先开始写的控制者。所以我会告诉Eva。

当我说出Eva的名字时,她将开始写字,并且会尽可能快地写,直到我让她停下来,然后我会让Zach,开始写。好。准备好了吗?所以让我们来做个演示。哦,对了。抱歉。所以你只有一块粉笔。

因为我们只有一块来打败你。好吧。Eva。做得很棒。这很简单,还是很难?竖起大拇指。这有点尴尬。好。尴尬,但还算不错。好。所以我们又回到了这样一个问题:我们为什么要使用多线,程呢?现在。

我们有两个不同的线程,但在任何时刻只有一个人能,控制粉笔。现在我们要引入一个随机因素。好。Brian。所以你的角色是随机数生成器。因此,Eva作为第一个写字的人,当她到达数字二时,她必须,停下来。

等待你给她一个随机数。然后她将写下那个随机数,然后继续写到10。好。不过,Brian,你非常,非常,可以说是有点小故障。假设你花很长时间才想到你要说的随机数。这是一个很重要的问题。好。让我们再来一次。

Eva。太短了。再来一次。再来一次。更长一点。更长一点。你今天很困。好吧。Eva。好。所以整个概念是,Eva必须等待Brian的输入,而Zach本可以,在这段时间内写字。所以。

这就是为什么在一个CPU中使用多个线程的目的是什,么呢?有人能告诉我吗?一个进程必须等待某些东西。另一个进程可以继续进行,而第一个进程在等待。完全正确。所以这真的就是关键思想。做得好。

让我们为我们的现场演示参与者鼓掌。是的。一个很常见的问题是,你可能会问,实际中有多少时候你必,须等待某些东西呢?结果是比你想象的要多得多。例如,每当你从文件或磁盘中读取时,你必须等待磁盘被找,到。

这涉及到许多物理方面的因素。或者说你在向网站发出请求时,你必须等待网站响应。这些延迟时常发生。即使是在我们之前见过的简单的C输入中,我们也必须等待,用户输入。所以等待实际上在很多地方都会发生。

这也是为什么多线程而不是多进程是有意义的原因。所以是的。这就是我们今天讨论的大致范围。回到我们的幻灯片。你们中有多少人见过多线程或者听说过它?好的。好的。明白了。所以实际上不少人了解这个概念。

那么我就相对快速地讲解这一部分吧。对于那些不熟悉的人来说,线程的概念就是你刚刚看到的,那样。其实过去,在这门课上,对你们中许多人来说,你们写的所,有代码都是顺序执行的。这意味着每当你执行一行代码时。

下一行代码会跟着执行,以此类推。线程的作用就是并行化执行。在单个 CPU 的情况下,它们并不真正并行。并不是两个操作同时发生。但你可以想象结果可能会感觉像是并行的。稍后我们会更详细地讨论这个问题。

以图形化的表示来看,你可以想象我们有一个程序正在运,行。假设这个程序生成了两个新线程。比如说我们演示中的两个写线程。同时,我们的主程序仍然继续运行,就像之前一样。然后在某个时刻,所有线程重新汇合。

我们的程序输出我们想要的结果。多线程有几个棘手的地方。你们可能已经能猜到一些可能会出现的问题。但为了清楚说明,假设在我们的程序中,我们将变量 A 设,为 2,将变量 B 设为 1。

然后假设我们生成的一个线程将 A 设置为 5。这个线程的工作就是将 A 设置为 5。然后另一个线程说,好的,我的工作是将 A 加到 B 上,然,后将结果赋值给 B。这里的问题是什么?

这里的潜在问题是什么?可能会有什么问题?对。正是这样。具体来说,这里可能有哪两种结果?结果可能是 B 加 2 或 2。对,你说对了。确切地说。在这种情况下,棘手的部分是我们不知道 B 最终会变成什,么。

因为这取决于哪个线程先执行。这就是所谓的数据竞争,或与竞态条件相关的概念。这在 CS 110 中你们会非常熟悉。对,如果你们继续上这些课程的话,不是 107,只是 110。但我们这里只会讲解基础知识。

好,到目前为止对多线程的基本概念还有什么问题吗?好。那么这就引出了锁的概念。我们实际上已经见过锁。Avery 已经简要解释过它们的作用。我们在 RAII 中也见过它们。之前我们有一个叫做互斥量的锁。

我们可以锁定和解锁它,我们意识到,哦,实际上有一个更好的解决方案,就是将它,包装在锁保护器中。这自动确保锁在项被销毁时总是会被解锁。

所以我想做的就是再给你们展示一下,C++ 中多线程是如,何工作的。所以要做到这一点,我们实际上要回到我们最喜欢的标准。

库。所以我要点击这个链接。太棒了。原来多线程是程序的一个核心特性,实际上它是 C++ 标准,库下的核心标题之一。所以在这个链接上,我们看到了它的五个主要部分。并且每个部分都有几件我想强调的事情。

所以第一个是原子标题。从这个原子头文件中要记住的是,如果您在程序中使用多,线程,您不想使用的是整数、布尔值、字符串之类的东西,您实际上想要使用的是这种原子等价物。所以您会注意到,如果您查看这个链接。

不是布尔值,他们,有一种称为原子布尔值的东西。而不是整数,他们有一种称为原子整数的东西。实际上,这所做的是告诉我们,这是一个类,应该保证在单,个变量类型中没有数据竞争。为了更清楚一点,这到底是什么意思?

您会记得,比如在我们的例子中,我们有 B += A。

原来这个简单的加法运算实际上在底层是一系列的多个指,令。所以实际上,虽然它看起来是一行代码,B += A,但在底层,它所做的实际上类似于获取 A 的值,将 A 加到 B 上,用 。

B 的新值替换 B 的值,诸如此类。所以实际上有一系列不同的事情要做才能执行这一行代码。

。所以原子类,哦,无限递归。

让我们看看。是的,好了。好的。所以这个原子类为我们所做的是,C++ 的设计者已经为我。

们提供了一个类,向我们保证基本上我们期望的原子操作,换句话说,一次完成所有操作的操作,实际上是一个原子操,作。所以例如,如果我们要使用,如果说 B 和 A 是原子整数,那么要添加 B += A。

我们可以使用这个称为原子取加的方,法。实际上,这就相当于加号。所以是的,所以您可以回去阅读这些。再次强调,如果您想自己使用多线程编程,这些是您想要使,用的变量和函数。我想在库中引用的其他几件事。是的。

对。是的。所以另一件事是,即使您在编写多线程程序时,也不一定必,须将每个操作都设为原子操作。所以实际上只有在事情执行的顺序确实重要的情况下。在那种情况下,那就是您想要使用原子操作的地方。

但在您程序的其他情况下,也许没关系,您不必在那里使用,它。是的。好的。所以 STL 中的线程类就是我们刚刚处理的。所以我们稍后实际上会有一个关于它的代码演示。所以我现在先跳过这个。我想提一件事。

所以上次我们有一个很好的问题,就是我们引入了锁守卫,这个概念,有人问了一个问题,这可能吗?所以似乎存在一个奇怪的问题,即锁保护器只有在离开作,用域时才会解锁,但你可能想要在函数内部进行解锁和加,锁。

我们之前说过,你可以使用这种作用域操作符,只需插入两,个大括号来形成一个新作用域。这实际上是一个完全有效的解决方案。结果是还有另一种解决方案。所以我想在上次的答案基础上再补充一点。

锁保护器是你可以用来包装锁的包装类之一,并且仍然符,合 RAII 标准。实际上还有另一种类型的保护器,叫做唯一锁,就是这个。它的功能实际上是我们上次寻找的那种,它允许你在声明,锁保护器时。

可以在函数内部加锁或解锁,但它保证当保护,器离开作用域时,它会确实解锁。所以它做的事情和锁保护器一样,只是你可以在中间加锁,或解锁,但它仍然保证你的锁在作用域结束时会被解锁。所以是的,我想提到这一点。

因为有人对这个问题提出了很,好的问题。所以实际上库中确实有可以做到这一点的东西。嗯,到目前为止对这两个类有什么问题吗?是的,这部分是让你了解标准库中的内容,以便你知道在编,写自己的程序时要寻找什么。

然后这部分将更具概念性,比如,如何处理多线程程序?是的,它们为什么有趣?是的,好的,让我们看看。所以这最后两个,我不会深入讨论条件变量,你将会经常使,用。如果你继续上更高阶的 CS 课程。

你可以把它们看作是两,线程之间的通信方式。条件变量允许你发信号或等待信号。所以是的,条件变量可以看作是两线程在运行时相互通信,的方式。这个未来类有点有趣。有没有人使用过 JavaScript?好的。

有一点。好的,是的。所以在 JavaScript 中,有一个 await 的概念,或者说异,步函数的一般概念。结果是 C++ 也有这个。它就在这个未来类中,位于最底部。所以是的,只是提醒你一下。

如果你需要异步函数,你可以,使用未来类。对于异步函数的简要概述,你可以把它们看作是一种启动,自己的线程并等待该线程响应的方式。所以通常的做法是,你会做一些像是,在一个新线程中发送,网络请求。

所以它会构建网络请求并在一个独立的线程中发送它。只有在收到响应后,它才会返回到你的原始线程并带回响,应。所以这就是异步函数的想法。是的,如果你之前没听说过也没关系。

这是在像 JavaScript 这样的语言中常见的东西。是的。对这方面有任何问题吗?

没有。好的,完美。

在这种情况下,我们将做的是,是的。

好的。所以这张幻灯片列出了我所说的内容。再次提到,锁保护与唯一锁。有几种不同类型的所谓互斥量,你可以使用。是的。其中一个有趣的类型是递归互斥量,它表示一个线程可以,多次获取该线程的锁。

你可以把它想象成拿着一块粉笔,但可以对这块粉笔声称,拥有权利多达七次。然后要释放对粉笔的拥有权,你需要说,啊,我释放这块粉,笔的拥有权七次。这就是递归互斥量的概念。再次,旨在给你一个广泛的了解。

让你知道你可以使用和尝,试的东西。好的。现在,我认为我们将进入比较有趣的部分,即看到多线程的,实际应用。在我进入代码之前,有人对多线程、锁、数据竞争等概念,有任何问题吗?好的。是的,布莱恩。

当你实现计时器时,你需要使用这个吗?是的。所以当你实现计时器时,你需要使用这个吗?实际上,如果你继续学习所有系统类,你实际上会实现多个,计时器,这有点有趣。但是是的,绝对需要。

计时器的工作方式类似于这个。根据你实现它们的方式,它们可能会使用我们提到的条件,变量,这些变量依赖于等待信号或发出信号给其他线程等,是的。是的,对。是的。这是个很好的问题。在这种情况下,我们仍然。

使用原子类。所以再说一次,为了记录,问题是原子类,使用原子变量和,原子方法如何实际影响这个场景?在这种情况下,它实际上没有影响。原因是,因为在这种情况下,我们调用的唯一原子操作是加。

法等于本身或像 A 等于 5 本身。但我们没有保证像 A 等于 5 然后 B 加等于 A 这样的操,作是整体原子的。所以在这种情况下,使用那个类仍然会导致我们得到 B 的,任意值。

你可以想象它可能更重要的地方是在一些情况下,这是一,种你在 CS110 中肯定会看到的情况。这是一种被称为“你有牛奶吗”的情况,一般概念是,假设你,和一个室友住在一个房子里。你有一天回到家,打开冰箱。

发现牛奶用完了。然后你当然去商店买一瓶。但问题是,当你去买牛奶时,你的室友回家了,检查了冰箱,看到冰箱开着,看到是空的。然后室友想,好吧,我最好去商店买两瓶。然后你们现在有了两瓶牛奶。

这比你想要的要多得多。所以在这种情况下,你会发现问题的原因是你检查是否需,要某样东西,然后真正满足这个需求的过程不是原子的。因此,你可以通过类似的方式使用原子操作来确保它们是,原子的。在那种情况下。

特别是情况,我相信你仍然不能做到,因为,这不是像加法等于那么基本的操作。是的。但这保证了,例如,当你检查 A 的值时,当你存储 B 的值,时,它仍然是一致的。是的。是的。好问题。好问题。是的。

还有其他问题吗?

好的。好的。那么在这种情况下,我们来看看线程是如何工作的。

所以我们要去这个文件。

好的。所以在这里,我有一个文件,它不多,但我已经导入了一些,我们需要的库。所以这个想法是,我会写一点,并在进行中解释我在做什么,线程的想法是这样的。假设我们想要有两个线程。我们将使用它们的方式是。

再次使用这个线程类,叫它们 ,Thread1 和 Thread2。你使用线程的方式是,你可以将它们想象成工人。你可以将它们想象成早些时候为演示自愿参与的人。所以线程总是需要它要做的工作。在这种情况下。

假设我们要定义一个叫做 greet 的函数。这些线程的唯一工作就是运行那个函数,然后完成后返回,所以在这种情况下,我们会说 greet,然后我们实际上来写,那个 greet 函数。

第二个线程也会做相同的事情。所以我们来写一个函数 greet,它会接收一个 int i,作为,线程的 ID。所以我们叫它 int ID。它所做的就是,假设它会输出,大家好,我的名字是,好的。

然后它就做这些。回到声明线程的语法,所以每次你声明一个线程时,你都要,告诉它要做什么工作,在这个情况下就是 greet 函数。然后在语法上,你只需要在之后传递参数给那个函数。在这种情况下,只有一个参数。

ID,所以我们把它叫做 ID 1,然后对于这个 greet 函数,我们叫它 ID 2。在这里,我们实际上可以做的就是,你可以想象线程在你的,程序中如何工作的,一旦你在程序中声明了一个线程,它会。

立即开始尝试执行它的工作。所以一旦你声明了 std thread, thread 1, greet, 1,它,会立即开始尝试输出它想要输出的内容。同样当你声明线程 2 时也是如此。

所以我们来看会发生什么。啊,好吧,实际上,在我们这样做之前,最后我们来看看所有,绿色完成的情况。好的,所以有人猜测一下如果我现在尝试运行程序会发生,什么?这有点棘手。具体来说,你认为它会输出任何内容吗?

除了线程的问候和所有问候完成之外的内容?它可能连这些也不会输出,因为你使用了一个未编码的标,识符。这是个很好的观点。这是确切的问题。这是确切的问题。更好。好的。好的。这是热身环节。你们做得很好。好的。

所以让我重新陈述我的问题。有多少人认为,“大家好,我的名字是 1”会被输出?每个人都觉得这一定是个难题问题。有多少人认为,您好,我的名字是 2 将会被输出?好的,是的,这公平。如果你不认为 1 会。

那么 2 也不会。所以实际上,你们的直觉,在某种意义上,哦,给我一秒钟。

为什么是 Greek comma 1?是的,这只是声明线程的语法。

不,我不是故意这样做的。

啊,是的。好的,这个原因是因为,回答 Brian 的问题,之所以是 。

Greek comma 1,仅仅是因为从语法上讲,将任务传递给工,作线程的方式是列出任务的名称,然后在逗号后面列出该,函数所需的所有参数。是的,这只是语法问题。好的,这有点有趣。

所以这里有很多事情发生。首先要注意,好的,我们确实打印了来自我的线程的问候语,并且我们确实打印了所有问候完成。但所有问候完成发生在任何的,您好,我的名字是 之前。而且进一步来说。

这两个输出的顺序非常混乱。所以现在先忽略这些,集中在这两个上面。为什么所有问候完成发生在任何两个“您好,我的名字是” ,的输出之前?啊,是的,确切的。更一般的想法是,我们不能忘记主函数本身仍然是一个运。

行中的线程。所以如果你记得我们之前的图示,我们有两个线程分支出,去,但主函数仍然在中间运行。是的,这就是一般的想法。实际上,我之前的问题是个陷阱问题。

数据竞争的难点在于它们是不可预测的。所以我们实际上不能预测主函数是否会完全结束,才会出,现任何输出,或者是否不会。为了使其更清楚,我们可以使用,让我确认一下我是否正确,地得到语法。我们可以使用。

这是 STD 线程类中的一个函数。它允许我们告诉特定的线程休眠一定的秒数。所以让我们让它休眠五秒钟,以确保安全。所以现在我期望的是,主线程在这两个线程可以执行它们,的输出语句之前完全返回。

所以让我们测试一下这个假设。

好的,完美。有人能告诉我为什么添加睡眠语句实际上帮助我更有保障,地使这两个输出不打印吗?

为什么会这样?绝对正确。完全正确。所以再说一次。因为我们迫使这两个线程等待,我们确保主函数先结束。而当主函数结束时,这两个线程也会超出作用域。正如 Byron 所注意到的那样,我们在主函数中声明了这两。

个线程。所以一旦主函数结束,这两个线程也超出作用域。

这就是为什么我们得到这个错误,终止调用没有活动异常,它抱怨这两个线程由于某种未知原因被终止。

好的。是的。到目前为止,有人有任何问题吗?关于基本线程使用的?没有。好的。所以就像多个东西,同时作为字符串字符串?是的。所以这是个好问题。你能再澄清一下你的问题吗?所以把多个东西放进去。

因为这次我注意到你没有,当你运,行它的时候,就像上次你运行它的时候,你有像 T2ERMI、。

T2 terminate,因为这两个进入了 C。正是如此。是的。像你还在试图查看其他东西时,所以你看到它如何能够自,动一次接受多个东西?是的。所以这是个好问题。所以问题是,为什么这些输出会重叠呢?

为什么,这个 terminate 为什么会和这个“我的名字是 ,2C”重叠?所以答案是,再次查看 C out。

你认为这一行是一个原子操作吗?谁认为是的?谁认为不是?好的。能有人解释一下为什么吗?是的。所以,实际上即使只看这一行,我们会发现实际上有多个操,作在进行。注意到即使显式地。

我们在这一行有三个不同的、类似的,操作。而且记住,这是因为这个插入运算符返回的是流本身。这就是允许我们改变 C out 表达式的原因。所以正是如此。所以我们不能保证这一行会一次性执行完。

所以实际上我要问你们,我们如何才能保证所有这些 C ,out 会在同一时间发生?你们认为我们可以使用什么?说得大声点。一个锁。好的。我不知道这是否大声一些,但就这样吧。我们绝对需要一个锁。好的。

所以我们可以做一些像我们有一个,这叫做互斥量。所以我们叫它一些互斥量。然后我们如何声明这个互斥量?一个想法可能是,我们可以说像这样。这引起任何人的注意吗?哦,好的。梅森,绝对是的。是的,正是如此。

所以我们如何使它符合 RAII?完美。正是如此。所以,再次,锁保护器的语法。再一次,如何使其符合 RAII 的想法是,将其包装在另一个,可以处理析构函数和构造函数的类中。在这种情况下。

语法是使用 STD lock guard。结果是锁保护器实际上是一个模板类,因为它可以接受我,们讨论过的递归互斥量。它可以接受定时互斥量。所以这就是为什么我们必须使用互斥量并且必须是 STD。

我也可以在上面声明 STD mutex,但,然后我们将其称为 ,lock guard。然后在括号中指定我们要保护的锁。在这种情况下,就是这个互斥量。太棒了。然后注意到我不需要做任何显式的解锁。

因为当锁保护器,超出范围时,会为我处理这个问题。好的。现在谁认为 C outs 会很好看?好的。好的。有多少人认为 C 输出看起来不会好看?好的。好的。有几个。好的。不错。不错。

我认为在这种情况下我们总是开的玩笑是,啊,关于,关于,投票,美国的投票比例,比如十个人里有三个,类似这样。好的。太棒了。所以,啊,对。所以我们实际上仍然没有解决主函数先结束的问题,这就。

是为什么这仍然有重叠,但您会注意到不管怎样,“你好,我,的名字是一”和“你好,我的名字是二”确实完美执行了。所以实际上,是的。所以那个 T 实际上来自这个,这个终止消息。所以实际上。

为了使其不那么令人困惑,并且因为这很重要。

,让我们实际上修复那个消息。所以再一次,问题是现在我们的主线程,在它的两个线程能,够自己结束之前就完成了。所以我们解决这个问题的方法是,我们必须告诉主线程在,我们希望。

在我们希望它结束之前明确等待它的线程完成,运行。所以我们这样做的方式是,我们,那个语法就是,我们获取,我们想要等待的线程。然后这是一个叫做 join 的函数。所以您可以这样想,啊,好的。

我们希望线程一重新加入主,线程,主线程和线程二重新加入主线程,然后主线程才能继,续执行。所以会发生的是,这个主函数实际上会在这里等待,直到线,程一完成,并在这里等待直到线程二完成,然后才继续执行。

其他任何行。好的。所以让我们看看现在会发生什么。

啊,我们终于有了我们期望的程序。好的。所以问题,只是为了确认理解,这两行可以交换吗?“你好,我的名字是一”和“你好,我的名字是二”。谁认为它们实际上可能以相反的顺序结束?谁认为它们不能。

必须是一然后是二。

好的。是什么,为什么,解释一下为什么您认为它们必须是这个顺,序?啊,好的。是的。所以这是从我们编写的方式来看完全有效的、有效的假设,所以这是一种棘手的事情,这就是为什么我想提出来。而且对不起。

大多数问题通常不是故意***难的问题。这个也不是。所以是的,所以这实际上是一个非常常见的误解,这也是为,什么提出来非常重要。所以即使主线程在等待线程二加入之前等待线程一加入,线程本身可以在任何时候结束。

所以另一种演示方式是假设这个主函数实际上有,比如说,让我们做很多非常,非常繁重的工作在中间。当它在做那个繁重的工作时,很可能在某个时候这两个线,程完成了。所以实际上这个 join。

这个 join 调用所做的实际上是,您可以想象成只是在任何时候检查这个线程是否完成的布,尔值?如果是,那么好的,我可以立即返回。如果不是,那么我必须在这里等待直到它完成。所以是的,所以在这种情况下。

这两个线程可以在任何时候,执行。但确实主线程在线程一完成之前不会检查线程二是否完成,这是否有意义?因为这有点儿,这有点儿难。很好。好的,我看到赞成的手势。非常好。好的。是的。

我们正慢慢地涵盖所有最基本的线程方面。所以这非常好。好的。所以我还想强调的是,至今我们只使用了两个线程,但实际,上这可以与任何数量的线程一起工作。假设,我们在顶部有一些常量。假设我们想要运行10个线程。

那么在这里,我们可以使用像向量这样古老的东西,而不是,显式声明每个线程。所以我们实际上可以有一个线程的向量。线程就像其他类一样,你可以使用我们学过的数据结构。所以我们称之为线程的向量。

然后为了按预期初始化它们,对于i小于k个线程,我们将执,行threads。push_back。然后和之前一样,在这种情况下,如果我们想的话,我们可,以使用到目前为止在这个课程中学到的统一初始化器,但。

也不是必须的。所以这个默认初始化器,就像之前一样,我们传入函数和id,然后最后一次,我们要等待所有线程返回。所以我们将执行像std:thread t for threads,然后,t。join。好的。

大家明白我刚才做了什么吗?是的,好的。所以为了再次测试理解,如果我们不是在这里有一个单独,的for循环,似乎,哦,为什么我们有两个for循环?如果我们在这里直接执行threads[i]。

join会发生什么?会有什么问题?是的。正是如此。所以再大声一点,这将调用一个线程,然后等待它结束,然,后另一个线程,然后等待它结束。你会注意到这听起来像是串行执行,就是非并行化的执行。

所以你实际上写的就是与我们迄今为止编写的串行程序完,全一样的程序。正是如此。所以这就是为什么我们首先要启动所有线程,以便它们可,以同时执行。然后在最后,我们要测试所有线程是否已经加入。是的,Byron。

这是一个很好的问题。所以是什么控制了每个线程的执行顺序,基本上,是什么控,制了线程的执行顺序?这实际上是大量研究的主题。是的,事实上,如果你继续上系统课程,这也是你将广泛学,习的主题。

所以这是一个非常关键的问题。大致的原理是,你的CPU中有一个调度器负责调度哪些线程,同时运行。为了让你对这如何工作有一个感觉,你可以想象实际上每,个线程在CPU上获得的时间片通常非常短。

你可以想象每个线程,线程一得到一毫秒,然后线程二得到,一毫秒,然后线程一再次得到一毫秒,以尽可能多地执行。然后,调度程序的角色,实际上是一个独立于任何线程的特,殊内核线程。

负责决定接下来要运行哪个线程以及如何在,其他线程运行时保存每个线程的状态。是的,这些算法如何最佳调度不同线程,这实际上是很多研,究的主题。是的,这是个好问题。是的,有什么事吗?是的,实际上。

这正是我接下来要问的问题。所以这是个好问题。是的。那么我们为什么会有这个线程和?那么这个和有什么重要的呢?或者它是否根本不重要?是的。是的。所以你可以想象一下,如果我们没有这个和会发生什么,你。

会记得每次。首先,你会注意到它有一个错误信息,提示删除复制构造函,数。但这就是原因,因为记住,当我们不进行引用传递时,我们,在传递什么?复制。是的,更大声一点。是的,完全正确。通过复制传递。

那么当我们声明一个新线程时会发生什么?我们刚才说,当我们声明一个新线程时会发生什么?对了。所以正是这样。所以如果他们没有删除复制构造函数,那么在这里发生的,情况是,我们实际上会启动10个新线程。

因为我们刚复制了,10个新线程。是的,完全正确。所以这就是为什么我们需要通过引用传递。而这个&符号只是表示我们通过引用而不是通过值进行访,问。是的,很好的问题。好的。所以我们可以看到。

运行一下以确保我们正确运行了。

那太完美了。

好的。我认为那是系统错误。我实际上不确定那是从哪里来的。我得查看一下。但好的,这就是我们关于多线程和C++的速成课程。你们到目前为止有任何问题吗?是的,继续说。是的。

多线程和多进程之间的区别就像我们上课开始时的,演示一样,这实际上也有更详细的答案。它们确实有不同的优点和缺点,例如进程比线程隔离得更,多,等等。完全正确。所以多进程。

你可以将其想象为多线程的一个更大版本,但,具有一些额外的特性。我们可以课后再讨论这个问题。是的。所以,如果你在线搜索多线程和多进程之间的区别,你会得,到一些答案。但这实际上是一个比较细致的问题。是的。

你有问题吗?对的。是的。有没有工具可以查看同时运行的多个线程?在GDB中确实可以查看线程。不过我忘记了确切的语法。在GDB中确实可以做到。我在想是否有其他的软件。我几乎可以保证有。

但我也可以在讲座后再看看,看看是否能找到什么。但绝对可以在GDB中检查。是的。对的。所以这会检查所有正在运行的进程。你说它还检查不同的线程吗?好的。好的,完美。所以看起来这是否是一个Linux命令?

好的。好的。是的,正是如此。H top。H top。哦,top也是。哦,是的。好的。当然了。当然了。明白了,你使用的。是的,绝对的。是的。谢谢。谢谢。这是一个很好的参考。是的。布莱恩,你也有问题吗?

太棒了。是的。所以讲座后我会再次把这段代码发布到网站上。所以我们只剩下四分钟了。我本来打算多讲一点我们的AI。

但我大部分时间会浏览一下,告诉你们需要注意的事项。如果你们感兴趣,可以回去深入了解。但首先是公告。这是我们最后一节课。所以我想给大家一个掌声,感谢你们作为优秀的学生,整个,学期都坚持下来了。

给自己一个鼓励吧。希望你们现在都能互相认识了,并且在街上见到时会打招,呼。是的。然后也代表我和艾弗瑞,课后保持联系,随时给我们发邮件,即使我们的Piazza可能会停用,但我们心中的Piazza永远。

有效。所以,是的,随时发邮件给我们。我们也很希望保持联系。是的。我们也会在街上向你们打招呼。是的。所以。所以,再次感谢你们。所以我会再次简要概述一下这些幻灯片中的重点。如果你们想回顾RAII。

大体思想是,再次强调,我们使某物,符合RAII的方式是使用一个包装类,这个包装类处理在对,象超出作用域后销毁的逻辑。我们上次看到,有两种方法可以使用指针来实现这一点。我们可以使用独占指针或共享指针。

我想在你们心中,重新考虑一下这两者之间的区别?这里有一个稍微复杂的点,就是如何创建独占指针或共享,指针?这里有一种创建方法。但实际上,课堂的后半部分的主要 takeaway 是,这实际上。

不是你想要声明独占指针或共享指针的方式。事实上,你想要声明它的方式是使用这两个内置的智能指,针创建函数。创建函数不是一个官方术语。它们只是没有官方术语,所以我造了一个。具体而言,有这两个函数。

make_unique 和 make_shared。每当你尝试创建一个新的独占指针时,你要做的是使用 ,make_unique 这个类型或 make_shared 这个类型,而不是。

传入这个 new node。然后,实际上所有接下来的幻灯片都是关于为什么这样做,的。而且这实际上非常有趣。如果有人感兴趣的话,我可能会在第十周的某个时间主持,一个简短的讲座来谈谈这个话题。

因为这确实是一个非常,有趣的内容,实际上涉及到竞态条件。所以它非常与我们学习的多线程相关。但你也可以浏览这些幻灯片。不过,最重要的是你总是希望使用 make_unique 或 ,make_shared。

而不是明确地调用 new node。

而且,可以告诉程序员的是,在 C++ 中,我们现在几乎不会,调用 new 或 delete。即使在处理原始指针时,即使我们仍然想使用像 int* 这,样的东西。

这个 int* 几乎不会来自我们明确调用 int* n, = new int。它总是会是对另一个现有引用的引用。这是其中一个重要的收获。再说一次,现代 C++ 最大的新概念之一是我们基本上已经。

摆脱了 new 和 delete,这非常棒,因为它消除了很多过去,困扰 C++ 的内存泄漏问题,并且导致了很多安全漏洞。

因此,有一个问题在论坛上有人问,接下来我该怎么做?我怎么继续学习更多的 C++?如果我想学习新的东西会发生什么?首先,继续提升你的 C++ 技能的最佳方法就是使用 C++。无论是你想建立的个人项目。

还是你去工作的地方,或是在,你的课程中,是的,多使用 C++。使用现代 C++。然后,如果你真的感兴趣,还有很多 C++ 的阅读材料你可,以参考。我会推荐的有这五本书。并不是说你必须读完这五本书。

而是看哪一本对你有吸引力。例如,"Effective Modern C++" 提供了现代 C++ 关键新,特性的一个很好的概述。另一个好的在线资源是赫布·萨特,他是 C++ 的共同创始,人之一。

他写了一个非常好的在线博客。所以如果你搜索“赫布·萨特 C++ 博客”,你会找到他的博,客。这个博客解释了很多 C++ 的独特特性和一些需要注意的,复杂点。所以,谢谢大家。祝贺你们完成了。哦,天哪。

是的。从物流上讲,我们下周仍会为第三次作业提供办公时间,如,果你感兴趣的话。是的。我可能会举行一个小讲座来完成第二部分,因为这部分非,常有趣。但你们做得很好。祝你们有一个愉快的感恩节。

祝你们有一个愉快的感恩节。祝你们期末考试好运。等一下。是的。对于三重指针,我们可以做像 unique、shared、shared ,这样的吗?一个 unique、shared、shared 指针。是的。

你完全可以。你完全可以。所以你会得到一个独占指针,指向一个共享指针,指向另一,个共享指针。

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P19:[01]CS 106L Fall 2020 - Guest Lecture_ Template Metaprogramming - GPT中英字幕课程资源 - BV1Fz421q7oh

我通常会这样做,是的。

让我们看看,幻灯片,我想Ethan或Nikhil可以分享它们。我现在就把它们上传上去。好的,非常好。然后在代码方面,我觉得你可以直接打开一个空的Qt ,Creator项目。我有点犹豫分享某些东西。

因为我们今天会使用一些相当,新的C++17的内容。所以,我不是很确定你的编译器是否支持这个,可能支持,也可能不支持。

但如果不支持,我实际上会使用这个网站来演示大量代码,如果你修过CS107,你可能以前使用过这个网站。当你在这里输入代码时,它实际上会生成你可以看到的汇,编代码。你今天不需要理解汇编代码。

尽管我会突出一些我们将要,做的汇编代码中的有趣内容。好的,这个网站的URL在这里。这个链接应该可以让你访问编译器。是的,实际上,它支持C++17,也支持C++20编译器。

好的,那我们开始吧。

好的。所以,是的,我叫Avery。我去年是Windows Excel讲师之一,我非常期待今天和你们。

讨论模板元编程。总体议程是,我们将从一些激励示例开始,这些示例将介绍,我们如何在类型上进行计算的概念。你们编写的普通程序,程序的总体概念是你将处理数据,进,行一些计算,然后返回数据的处理版本。但相反。

我们将改变我们的计算模型,开始考虑在类型上进,行计算。而不是处理像这三个变量这样的值,我们实际上会对这些,类型进行计算,例如int、double、引用、const等。然后我们将讨论元函数。

它们像函数一样,但实际上不是函,数。然后我们将一起实现两个元函数。一个叫做identity,仅仅是让你们准备好了解什么是元函,数。然后我们实际上会学习一些更复杂的模板规则,这些规则。

将帮助我们实现另一个叫做isSame的元函数。最后,我们将用constexpr总结一切,这是C++中的一个非常,新的发展。好的,那么事不宜迟,我们开始吧。所以,作为一个总体免责声明。

今天的目标是尝试介绍一些,更高级的模板概念。我们将介绍至少两个或三个。然后我们还将看到这些模板概念实际上是如何有用的。毫无疑问,今天的代码将感觉非常不自然,你可能会感到我,们编写的代码。

C++的设计者并没有打算让你以这种方式使,用C++。它会感觉非常hacky。这实际上是很贴切的,因为C++的这一部分是无意中的。它是通过偶然发现的。在2003年,我认为。

一些C++程序员在C++会议上展示了这个,奇怪的程序,它完全不是预期的使用方式,但做了一些有趣,的事情。好的,这导致现在它变得非常有用,你会在STL中看到它的,应用。好的,现在,TMP,模板元编程。

你可能会问,这段代码看起来很奇怪。我会写 TMP 代码吗?答案是可能会。如果你在实现库,那么你肯定会看到 TMP 代码。好的,所以我会给你一些 PyTorch 的例子。我会给你一些 STL 的例子。

这些例子大量使用了 TMP 代,码。然而,无论你最终用 C++ 做什么,当你尝试调试模板错误,信息时,你可能会看到一些 TMP 代码。所以理解这些错误信息的含义是非常有用的。

我相信 Ethan 和 Nikhil 已经给你展示了一些例子,你尝,试编译一个程序时,它生成了大量的错误信息,今天我们实,际上会看看这些错误信息的含义。因为这样,有些人可能不会编写 TMP 代码。

我希望你们关,注 TMP 的高层次直觉,即 TMP 是什么,我们为什么需要 ,TMP,而不是具体的语法细节。好吧,现在我们把这个问题放到一边,来谈谈一个激励例子。

所以我实际上会开始尝试实现,打开编译器。好的,所以这里是这个想法。激励例子是我们想要实现一个类似这样的函数。所以我们来包含 vector,包含 deck,包含 set。好的,为了让我们的生活更轻松。

我们使用命名空间 std。我将首先创建一个名字的向量。所以我将创建一个名字的向量,假设名字是 Avery,稍后你,会明白这点的重要性。我们再加上 Ethan、Nikhil 和 Anna。

所以我们有一个名字的向量。我想做的是实现这个函数。所以这个函数叫做 distance,它基本上接受两个迭代器。所以假设我们实际上可以从这个向量中获得两个迭代器。

假设我们有一个指向 Anna 的 Anna 迭代器。我们可以做 find names。begin,end,然后是 Anna。所以这会找到元素 Anna 并返回一个指向 Anna 的迭代器。

我们也可以返回一个指向 Avery 的迭代器,find ,names。end 和 Avery。好的,所以这个例子现在有意义吗?所以我们基本上是在创建一个向量,从这个向量中获取一。

些指向 Anna 和 Avery 的迭代器。这应该是 Avery。然后我们要做的是编写一个叫做 distance 的函数,它接,受两个迭代器。它基本上返回这两个迭代器之间的距离。所以在这个例子中。

这个颜色现在并不令人烦恼。颜色稍后会有用,但现在还不重要。所以我要做的是切换到暗模式,这样你看不到颜色。

好了。好的。那么这个 distance 函数会返回什么呢?这个 distance 函数返回四,因为第一个迭代器指向第一,个元素。这个 Avery 指向最后一个元素。那么它们之间的距离是 一、二、三。

所以你会将第一个迭代器递增三次以到达最后一个迭代器,好的,所以这是我们要实现的函数。标准库中已经有一个 distance 函数。你可以实际运行程序,它会返回三。好的。好的。

所以这基本上是我们今天要用的总体例子。让我们快速实现一个距离的示例。我们称之为我的距离,这样就不会有冲突。在我输入这些内容时,想一想您会如何实现距离。这里的距离,我们希望它尽可能通用。

这样即使我们将向量,更改为牌组,或者将牌组更改为集合,我们都可以通过传入,任意迭代器来使用这个距离函数,有点像 STL 算法本身。那么让我们写出一个模板。我们将传入一个任意的迭代器类型。

然后距离返回一个大小 T,它接收两个参数,距离,第一个,迭代器和最后一个迭代器。好的,现在让我们想一想我们将如何实现这个。所以让我们想一想我们将如何实现这个。所以实现这个的一种方法。

我可能会稍微回顾一下迭代器,但实现这个的一种方法是做类似于最后一个减去第一个的,操作。所以如果这是,让我们看看,如果这是一个向量,回想一下,向量,从最强大到最不强大有不同类别的迭代器。

最强大的迭代器是随机访问迭代器。

我实际上有关于这个的一张幻灯片,我想。所以我实际上有关于这个的一张幻灯片,我们回想一下有,不同类别的迭代器。最强大的迭代器是随机访问迭代器,它允许您向前和向后,任意移动迭代器。所以您可以做 += 3。

然后迭代器向前跳三步。好的,这与其他类型的迭代器相反,在那里它们不允许随机,访问,您必须逐个逐个地递增这些迭代器。那么随机访问迭代器,哪些集合具有随机访问迭代器?向量、牌组、数组。而您知道的其他集合。

映射、集合、列表,它们没有随机访,问迭代器。它们具有前向迭代器或其他不太强大的迭代器。所以这是来自 Stiopa 的一个问题,他问,如果我们正在做,这个模板函数,对吧,我们如何确保它实际上是一个迭代器。

类型而不是像整数或字符串?是的,好问题。那么,让我们看看。这个问题有两个部分。第一是我们如何确保它是一个迭代器类型?以前,当您编写这些函数,这些模板函数时,您会,我认为使,用的术语是隐式接口。

如果模板函数内部的代码,如果它对于模板类型不起作用,例如,如果我们传入的不是迭代器,而是向量,那么会发生,的是,当编译器试图弄清楚您的迭代器类型是什么。所以,现在,它确定它的类型实际上是一个向量。

然后这里发生的是,我们应该得到一个编译器错误。哦,我们应该得到一个编译器错误。好的,那不是一个非常有用的编译器错误。嗯,我们可以在减号下看到,如果您转到减号函数。从最后一个减去第一个返回。对吗?哦。

是的。哦,是的,就是这样。就是这样。是的,是的。所以在这里您可以看到,那个隐式接口,那个类型必须满足,它必须有一个减号函数。无论它是什么类型都必须有一个减号函数。因为向量没有减号函数。

它不满足这个隐式接口。好的,但在这里你还应该问另一个问题,那就是,这只有在,迭代器是随机访问迭代器时才有效。好的,所以我们实际回到这一点。好的,迭代器和重新迭代器。这个。

我的距离函数只有在它是随机访问迭代器时才有效。

好的,因为回想一下,只有随机访问迭代器才有减号。

所以,为什么这会是一个问题?嗯,如果我们把它改成一个双端队列(deque),这仍然会有,效。仍然有效,因为双端队列有随机访问迭代器。所以当它调用我的距离函数时,它能够进行这个减号操作。

但假设我们把它改成一个集合(set)。然后我们会得到一个编译错误,因为集合迭代器没有减号,好的,因此,基于此,这个函数适用于随机访问迭代器,但不,适用于所有其他类型的迭代器。解决这个问题的一种方法是。

好的,这不适用。但你可以用暴力破解的方法来尝试找出距离。创建一个结果。然后不断递增第一个迭代器。所以在第一个迭代器不等于最后一个迭代器时,保持递增,然后计算你需要递增多少次,然后返回结果。例如。

如果这是一个向量(vector),那么我们将从第一个迭,代器开始。所以迭代器到安娜(Anna),递增三次。所以是三次。如果这是一个集合,如果是集合的话,因为集合是有序的。

所以安娜和艾弗里(Avery)应该实际上是相邻的,这就是为,什么当你运行这段代码时,它返回一。我们称之为集合是有序的。好的。现在因为我们实现了这个,所以这段代码不假设迭代器类,型是随机访问迭代器。对。

对,现在我们只是在对这些迭代器进行递增操作。所有迭代器都允许你递增它们。所以这段代码适用于所有类型。是的。所以如果我们把函数保持这样,这段代码将适用于任何类,型。这段代码有什么缺点?有人有想法吗?

为什么这段代码可能不理想?考虑一下效率。对。我们尝试的第一个方法非常高效。但总是假设我们会这样做有什么问题?所以,在之前我们写最后减去第一个,这非常高效。如果迭代器是随机访问迭代器。

这将以 O(1) 时间计算距,离。你可以想象你有两个迭代器。你只是在计算它们之间的距离。所以这是非常快的。如果它是一个随机访问迭代器。如果它不是随机访问迭代器,那么你必须这样做。

现在的代码将始终运行这个慢版本,即使它是随机访问迭,代器。现在,这段代码适用于任何类型的迭代器。它将执行这个 O(n) 代码,其中 n 是距离。它将始终运行这个 O(n) 代码。这似乎很可惜,对吧?

因为如果它是一个随机访问迭代器,你可以实现这个函数,更快。现在,如果你实际查看 STL 中距离函数的实现。

你会看到,如果你查看复杂度,这将会说明。它会说明距离函数是线性时间 O(n)。但是如果输入迭代器是随机访问迭代器类型,那么复杂度,是常数的。所以某种程度上,STL 实现达到了两全其美,对吧?

它的做法是,如果一般情况下,它会运行这个 O(n) 版本,这个版本在一般情况下有效。但是如果迭代器具体是随机访问迭代器,它会识别出来。然后它会运行这个更快的版本。所以这是我们面临的问题。

我们必须能够处理它,处理这个模板类型,而你对它一无所,知。你必须以某种方式能够提取出一些关于它的信息。我们需要确定它是否是随机访问的。其次,一旦你确定它是随机访问的,你就可以确定,好的,我。

应该运行这段代码还是那段代码?我们应该根据是否是随机访问来运行快速版本还是慢速版,本?这是问题。如果它不是随机访问,这行代码甚至无法编译。第一个,last 减去 first 甚至无法编译。

这将成为我们尝试编译这段代码时的问题。所以这是我们试图在最后实现的目标。我们将尝试做类似这样的事情。Category 等于任意类型,无论是什么样的迭代器。然后我们将进行一些类似这样的逻辑处理。

如果 category 是随机访问,则返回 last 减去 first。否则你就运行这个慢速版本。好的,在今天的讲座结束时,我们将编写看起来像这样的代,码。我们将能够找出如何获取有关它的信息。

这个模板类型是,什么,是什么样的迭代器,然后能够检查迭代器是否是那种,类型。好的,有一种类型分发的概念,即使你有一个模板函数,它,能够接受任何不同的类型,但我们实际上可以根据确切的,类型来实现这个函数。

这结合了模板的灵活性,并能够尝试弄清楚确切的类型,并,可能基于此进行优化。好的,这就是我们在最后要达到的代码目标。这只是一个问题,对吧,我认为有相当充分的理由说明我们,不这样做。但从理论上讲。

这个问题是否可以通过重载来解决?你可以想象,如果你让所有的随机访问迭代器继承自随机,访问迭代器,而所有其他的继承自其他迭代器,那么你可以,对这两种类型进行重载,并使用类型解析来解决它。当然。是的,是的。

那么我们来看看。是的,这是一个很好的观点。所以我不认为这学期的 106L 课程中对继承进行了太多讲,解。但你确实提出了一个很好的观点,即为什么不这样做?你可以用像一个版本这样的重载。

即如果它是一个随机访,问迭代器的迭代器,然后另一个函数用于一般情况。我会说我们不这样做的原因有两个。一个是需要某种继承层次结构来实现那种逻辑。这使得类型系统有些不灵活,因为假设你想要,不仅根据迭。

代器的类型,输入,输出进行分发。例如,我们也可能会根据输入是否为 double、底层容器是,否存储 doubles、存储 ints 来进行分派。如果你有不同的维度,想要根据类型有不同的实现,这样做。

不一定会有效。是的,模板相比于继承的一个重要改进是它更灵活,如果你,有多个维度想要使用模板,模板在这方面更为灵活。所以总结一下,我们希望能够做出决策,不仅仅是基于基本,类型,也许还要基于其他许多特征。

好吧,你能回答关于这段代码的任何问题吗?显然这段代码还不能编译。那么我们实际要搞清楚这意味着什么。我们怎么能实现这样的功能?

好的,我们继续。我想我有点落后于时间了。

所以我会稍微加快速度。好的,我们来谈谈类型上的计算。

请注意这里。是的。所以我们实际上要讨论类型上的计算,因为我们面临的根,本问题是需要能够获取模板类型的一些信息。好的,所以我们必须在类型 IT 上进行操作。

我们将引入的是我们将使用类型计算的概念。

我们要做到这一点,将进行并排比较。计算值是你现在大多数编写的程序。然后我们还将看到如何对不同类型进行类似的计算。所以你计算的变量实际上是类型。在正常的计算中,当你处理值时,你可以将这些值存储在变,量中。

所以我们有一个存储三的变量 S。在计算方面,你可以存储类型,这些不是变量,但它们像是,类型别名。你可以在这个变量 S 中存储一些类型。好的,这就是值与,类型之间的比较。好的,根本上,如果你试图存储变量。

这些是两种方法。这些是值和类型的存储方法。你可以使用先前值的值创建新的值。所以我们可以创建一个使用之前变量 S 的变量 triple。对于类型,你可以创建一个新类型,它是一些旧类型。

但你可以尝试在其上应用一些操作。所以在值的方面,我们将其乘以三。在类型方面,你实际上可以取一个现有的类型,比如类型 ,S,然后用 const 和引用装饰它。

所以 CL 引用这里是类型 const S 引用,const int 引用,好的,最常见的做法是将值传递到函数中,然后得到返回值,好的,现在很难想到如何对类型做这件事。

所以这是我们今天将引入的一个重要概念。你可以将类型传递给我们称之为元函数的东西。所以一个元函数是这样的。你可以将它们传递给元函数。然后你可以从这些元函数中获得类型。注意语法有点奇怪。

这里有些东西与模板有关。现在我们将深入探讨为什么会有这样的语法。在值方面,您可以对现有变量进行比较并获得布尔值。所以您可以评估某些布尔表达式。在类型方面,您可以做类似的事情。您可以将类型传递到元函数中。

并且注意,在这里我们从元函数中获得了一个值。我们通过将两种类型传递到元函数中得到了一个布尔值。好的,在这里尝试专注于元函数的概念。我们仍然能够将类型传递到元函数中,并且我们可以从这。

些元函数中获得类型或值。然后最后,在值方面,一旦您计算了这些布尔值,您可以根,据这些布尔表达式更改控制流。例如,如果等于 true,那么您可以退出您的程序。当您在类型上进行计算时,您可以做类似的事情。

这里的这些等于是一个常量布尔表达式,代码将变为 0 或, 1。然后您可以根据这些表达式更改编译器生成的代码。那么这到底意味着什么?我稍后会更详细地介绍。但我希望您大致了解的概念是。

我们在值上能做的大多数,事情。在类型上有类似的事情我们可以做。您可以更改控制流,您可以更改,您可以获取变量,您可以,将它们传递到函数中。对于在类型上能做的每件事,都有一个类似的对应。在值上能做的每件事。

在类型上也有类似能做的事情。在类型上也有您能做的事情。好的,我在下一张幻灯片上停下来回答问题。因此基于此,我不期望您完全理解这些元函数的上下文是,什么。我们稍后会处理。

但是一些旧的东西您之前应该已经看到过,他们在这里使,用了语句。这些是类型别名。紫色的东西,这边的这个类型,这是一个成员类型。对。当您尝试访问集合的迭代器类型是什么类型时,您会使用,看起来像这样的语法:

iterator 来能够访问某个类的成,员类型。您在这里看到的这个类型是一个成员类型。一些新的东西您之前应该没有看到过,蓝色的那些我们在,这里看到的。这些是元函数。它们的行为像函数。

但语义看起来有点不同。您不应该,专家应该是新的。我们将在今天讲座的最后谈论那个。然后还有这些静态成员值。106B 和 106L 已经开始不涵盖静态是什么。而且这不是超级重要。

但这里的想法是这里的这个值是一个实际的值。那是某个类的一部分。然后然后这个值是一个静态值,这意味着我们不必创建一,个对象就能够访问该值。这个值属于这个类。

我也会在接下来的几张幻灯片中更详细地介绍静态是什么,是的,我不想在这里停下来回答问题,因为这只是一个预览,您应该还不完全理解这三个概念是什么。让我们谈谈元函数。所以快速回顾一下,模板类型。

模板类型是当您尝试编写模板类时可以使用的。在编写模板类时,你需要指定你的任意模板类型是什么。然后,当你实际创建一个 int 类型的向量时,你在类中实,现的就是 T。然后,编译器所做的就是进入你的向量。

并将其中每个 T ,的实例替换为你实例化时指定的模板类型。所以在这里它会是 int。所以它就是你之前使用过的那些经典模板类型。结果是,你不仅可以使用模板类型,还可以使用这些模板值,在模板参数中。

你实际上可以放置实际的值。所以记住,这里的 n 不是一个类型。它是 size_t。这是允许的。整个实现看起来完全一样。你可以在实现中使用这个值 n。

一个实际可以做到这一点的 STL 类是 array 类,你可以,在这里看到。这些是固定大小的数组。这本质上是一个 C++ 数组,但它更智能,知道它的大小。我相信它是在 C++ 11 中出现的。

所以这只是一个模板类型的回顾,然后再加一点额外的内,容。你还可以使用模板值。我们覆盖了这个幻灯片,每次你实例化向量时,你都会给它,一些类型 int 或 double。编译器所做的就是取这里的代码。

并将每个 T 实例替换为,你实例化时指定的类型 int。所以你会得到这个代码,或者 double。你会得到这个代码。一个元函数。我们来谈谈什么是元函数。在一个非常抽象的层面上,元函数是一个“函数”。

它对某些,类型和/或值进行操作。所以这类似于函数的参数,它输出一些类型和值,这类似于,返回值。这是对元函数的一个非常抽象的看法。它本质上是一个函数,它作用于输入并返回输出,就像普通,函数一样。

关键是输入和输出可以是类型和/或值。好的,但这是一个非常抽象的定义。我们实际来弄清楚什么是元函数。下一行可能会让你感到惊讶。元函数是一个结构体。我们一直在讨论的这个元函数,它是一个结构体。

它有公共成员类型和字段,这些取决于模板类型和值是如,何实例化的。这些模板类型和值就是输入。然后,元函数,也就是结构体,会创建这些公共成员类型和,字段。这就是元函数的输出。元函数,即结构体。

会查看其模板类型和值作为输入,然后,将成员类型和字段作为结构体的一部分,这代表了输出。好的,所以输入来自模板类型。结构体所做的就是将函数的输出放入这些公共类型和字段,中。好的,这就是抽象的。

我不想过多地关注这个定义,因为当你看到一个示例时会,更有意义。哦,这里有一个很好的图示,将常规函数与元函数进行比较,常规函数,你传入参数,得到返回值。对于元函数,你传入模板类型作为模板参数的一部分。

然后它将生成一个称为 type 的成员类型。这是输出。或者它将放置一个静态成员,称为值,这将是返回的值。不想过多地停留在这个话题上,因为它非常抽象。让我们进入一个具体的定义。我们将编写一个身份函数。

它的作用是接受一些输入。输入可以是一个类型,也可以是一个值。它所做的就是输出完全相同的类型或值。所以它并不会做什么特别有趣的事情。它只是返回它自身。我们如何使用这个元函数?嗯。

它有点像我们之前看到的语法。对。你不是调用,而是使用结构体。你将把输入作为模板类型或模板值的模板值传入。然后你将尝试访问它的成员类型或静态成员变量。好的。成员类型称为type。成员变量称为values。

按照惯例,你总是使用type如果你要返回一个类型。如果你想返回一个值,你总是使用value。所以这就是这些元函数将如何使用的方式。让我们实际尝试实现它。这些元函数是结构体。

这些结构体的模板类型是元函数应,该获取的输入。所以身份元函数的输入应该是一个类型。这就是为什么我们将其模板化,使用某种类型T。然后在值方面,我们将输入放在它上面。所以我们将基于输出对其模板化。

我们将获取任何给定的,输入。我们将对其进行一些操作,并将其作为输出。左侧的输出是,我们将输出放在一个别名和一个成员类型,上,称为type,在变量侧的右侧。我们将输出V作为静态值。

作为称为value的静态成员。通过查看这个例子,你应该能够看到我们如何基于这个实,现得到这个用法。对。你在这里传入T。当你传入T时,这个结构体会用T实例化为int。它将T替换为这里的int。

这就是为什么当你写identity:type时,它将访问type成,员,这里我们将其替换为int。所以这就是为什么整个过程给你一个int。类似地。当你传入3时,编译器用V等于3实例化这个结构体。

所以我们有一个身份结构体,其中V等于3。然后当你尝试调用identity:value时,它会为你检索这个,值。好的,让我在这里暂停一下,因为我相信你们对这有什么问,题。有一个问题是我提前准备好的。

让我们看看是否有人会问那个问题。但除此之外,还有其他问题我可以回答这个例子。这个目标是向你们展示我们如何获取元函数的输入,如何,获取元函数的输出,身份函数本身并没有做任何有用的事,情。所以我的问题是。

是否有必要像这样编写两个函数来访问,类型名或值,或者有没有某种简洁的方法来将这两者结合,起来?我明白了。所以你是说我就像是左右两边,我们能把它们合并成一个,吗?对的。嗯,我认为这可能会有点挑战。

因为我觉得这可能会有点挑,战,因为这样的话你需要一种方法来判断这里是否有一个,类型,这里是否有一个值。我相信有一些技巧可以做到这一点,比如你可以设置一个,类型名称 T 和一个 int。

然后可以用默认参数,如果你只,提供类型,那么你可以忽略值。如果你提供值,那么你可以忽略类型,然后在这里,你可以,根据类型或值来访问你需要的那个。所以我认为这可能是可以做到的。是的。一般来说。

他们会将这些命名得不同,以避免对 T 和我们,使用的内容产生混淆。是的,好问题。我还有一个问题。当然。我有点不明白。所以所有这些都发生在编译时。对的。是的,那么它能做多少?如果我们有一个 for 循环。

会发生什么呢?比如一个 for 循环运行三次,而在 for 循环内部。我们有一个像是 using K 等于对 K 的引用。对的。这能做到吗?它会有三重引用吗?是的,是的。是的。好问题。

所以现在我们一直在编写一个比较简单的函数。你不需要做太多的类型。如果你想让这个元函数稍微复杂,一点,我们将不得不引入一些新的技术,以便实际改变 T ,是什么。好的。如你所提到的,编写一个 for 循环。

记住,在这个结构体中,你实际上并不在任何函数中。这目前是在结构体声明中。所以你实际上没有一个函数。所以你不能真正有像 for 循环这样的东西。你可以有变量。你不能有任何类似的东西。是的。

我们的结构体中还没有函数。所以你不能使用到目前为止你知道的大多数技术。例如,如果你想要返回像是,如果你想使用这个示例,但不,是返回 V。你想要返回像是斐波那契数列。

你不能在这里编写 for 循环来计算斐波那契数列,因为你,在这里没有真正的函数。你实际上只是有一些需要在编译时计算的表达式。我们将会看到一个技术。是的,我们只能在结构体内部使用 using 关键字。

如果你想要使用。你可以使用 using 关键字。记住,结构体有点像类。所以你可以在类声明中放入任何东西,但当我们尝试计算,这个类型时,你不能通过编写函数和普通函数来确定这个,类型是什么。

因为编写普通函数。这需要在运行时执行某些操作。有一种方法是使用 constexpr,但我会把这个留到后面。是的,但我确实同意目前这种技术感觉非常有限。现在你能做的不多,因为在类型方面。

我们能做的只是把 T, 作为类型。如果你想用 T 做更复杂的事情,我们就得引,入一种新技术。这说得通吗。是的。好的,是的,稍后我会更深入地探讨你的问题,因为会有一。

些关于你能用这个类型 T 做什么的新进展。但是,呃,哦,是的。所以我认为有人会问的问题是为什么我们在这里需要一个,静态的。我们在这里需要一个静态的原因是,请注意,当我们使用这,个元函数时。

我们从不实例化一个结构体。所以正因为如此,我们不想创建一个 identity 类型的对,象。我们真的只是想用这个元函数本身。所以通过在这里设置一个静态的,我们的意思是这个值属。

于整个结构体而不是特定的对象。对。通过这样做,现在,我们就不必调用构造函数来创建一个 ,identity 结构体然后调用它的值字段,我们可以简单地使,用这个符号。

因为这里的值字段属于 identity 。好的。还有其他我可以回答的问题吗。我知道我的时间有点紧。有人问我们是否可以禁用这些的构造函数 是的,嗯,你可,以禁用构造函数。嗯,你可以禁用构造函数。

虽然我认为一般来说,如果你知道你要使用这些元函数。对于这些有一种惯例,就像这些是元函数。不要试图不要试图创建这些元函数本身的实例。

所以我猜你可以通过使用 constructor = delete 通常来,禁用构造函数,因为使用这个的人知道这是元函数。我不知道,可能有也可能没有必要这样做。我不确定 STL 是否会这样做。

STL 可能会这样做只是为了,防止你可能做一些事情。是的。好的。还有其他问题吗。所以是的,这是一种有趣的看待结构体的方式。对,我们没有像预期的那样使用它们。我们没有把结构体当作它原本的用途来使用。

它原本是类似于类的,但现在我们用结构体来模拟函数的,样子。并且这个函数可以接受和返回类型或值。好的,所以总结一下,元函数是一个结构体,它将其模板类,型和值视为参数,并将返回值作为公共成员。

我们从不创建这个结构体的实例。好的,这就是为什么公共成员是静态的。我想我已经问过她问题了。所以我想我要继续另一个例子。让我们写一个叫做 is_same 的元函数,is_same 要做的是。

它将接受两个类型或两个值,并将要么真要么假放入静态,成员 fair_value 中。所以记住这里,这两个的输出都将是值。和之前一样,我们可以接受两个类型或两个整数,然后你会。

有一个叫做 value 的静态字段。所以在右边,这很容易实现,你只需要说 V = W 。这就是这个叫做 value 的字段的值。所以如果你放 3 和 3,这将是真的。如果你把三和四放在一起。

这将是错误的。所以这一部分很容易实现。另一方面,这一部分有点难,对吧,左边,因为你需要检查 T, 是否等于 U,而我们实际上没有一种方法来做到这一点。实际上。

实现这一点的整个要点是能够检查 T 是否等于 U,所以你还不知道有任何可以检查两个类型是否相等的构造,因此,我们需要解决这个问题。

这就引出了模板推导。好吧,这是一种更高级的模板技术,我现在将简要介绍一下。

。有一个概念叫做模板特化。模板特化意味着你可以有通用模板以及专用模板。一旦我们做一个例子,这将会有意义。这是一个非常著名的特化示例。STL 实现的向量有一个通用实现。它也有一个专用实现。

通用实现正是你所知道的。这是一个使用数组的实现,如果需要的话会调整大小。布尔向量的实现被特化为更节省空间。而不是使用一个存储一个布尔值的数组。如果你这样做,我认为每个布尔值需要大约八字节。所以。

它所做的是将所有布尔值存储在一个位数组中。所以这样更节省空间。每个元素占用一个位。好吧。而 C++ 允许你做的是,如果你实现一个通用模板,你可以,为特定类型专用这个模板。

注意这里模板类型 T 已经消失了。我们将说,如果你有一个布尔值的向量,那么选择了专用实,现。所以再总结一下,如果你实例化一个布尔值的向量,你得到,的是底下那个。如果你实例化一个整数的向量。

你得到的是上面的那个。好吧。到目前为止有意义吗?Josh,要实例化专用向量,你只需要做 std:vector 布尔,值,它会自动选择专用实现。这很糟糕,因为专用版本实际上不符合向量的所有类型要,求。

特别是,你不能通过引用来编辑它的元素,因为没有变量。这只是一个位。所以这是个灾难,非常糟糕。这就像是最好的主意,但却是地球上最糟糕的执行。对吧。对。所以这实际上很有趣。斯坦福图书馆。

斯坦福图书馆就像是 STL 之上的一层。而 Julie 所做的,我觉得很酷,是她写了一些逻辑,以便如,果你尝试实例化一个斯坦福布尔向量,实际上得到的是一,个布尔 Deck。因为布尔向量是,像是。

谁想出这个主意的试图很聪明,但,结果证明这是个坏主意。所以人们往往避免布尔向量。Stefano 也有个问题,问如果模板在第二个示例中没有任,何作用,那模板有什么意义?比如,为什么不直接删除那个呢?是的。

是的。好问题。所以这里的模板确实只是为了强调这些都是模板类。只是它们在不同程度上进行了专用化。所以这是一个通用的模板。我们只是说这个模板没有使用任何模板类型。它仍然是一个模板类。

因为它是所有这些模板类的一部分,但这个特别的模板没有使用任何模板参数,因为我们将用,布尔值来替换它们。好的。我认为我的重点是要强调这些都是模板类。这个不只是覆盖了这个。现在,你可以部分专用化一个模板类。

所以,在顶部的我们有一个完全通用的哈希表。在底部的我们有一个完全专用的哈希表。所以在这里我们将键和值专用化为整数。你不能实际上进行半途而废的专用化。例如,你可以专用化一个参数。

所以另一个类型v仍然可以是某种任意类型。你还可以更复杂一些,你可以编写这些额外的模板。这个模板只有在你的第一个类型可以匹配到K星时才会匹,配。所以那个类型必须是一个指针。所以我们有不同程度的专用化。

编译器将尝试匹配所有这些模板中最专用的一个。如果没有一个适用,那么你会默认使用更通用的一个。好的,现在实际上没有人以这种方式专用化哈希表。我只是做一个示例。但你可以想象一些优化。

如果你的键或你的值非常好。你可以以更专用化的方式存储你的哈希表。好的,在你的作业中,你可以尝试专用化哈希表类。好的。总之,我跳过了一些幻灯片,如果你想了解更多关于这些模,板规则的内容,你可以阅读它们。

有很多奇怪的模板规则。我希望你明白的一般概念是,当你编写所有这些模板时。编译器会对所有这些模板进行排序,然后会逐一尝试这些,模板,直到找到一个有效的模板。这种排序将从最专用到最通用。

所以它会先尝试最具体的模板,然后才尝试较少成功的模,板。如果它失败了。如果它尝试了一个模板而失败了。这完全没问题。它会按照顺序尝试下一个,直到你看到没有一个有效,这种,情况下你会得到一个编译器错误。

或者如果有类型问题。现在如果没有有效的模板。你实际上已经看到了,如果没有有效模板时的错误信息,当,你编写模板类时。你会看到的错误信息是,哦,它尝试了,我认为你会看到类,似“哦,它尝试了这个。

然后尝试了这个,然后尝试了这个”,这样的信息。如果你查看编译器输出的错误信息,你可以看到它说,它尝,试了这个不工作。它尝试了这个。它不工作。它尝试了这个。它不工作。结果表明向量有超过10种不同的专用化。

所以当你在使用向量时做错了什么,你实际上会看到一大,堆错误消息,关于哦,你做错了什么。你做错了什么,它通过告诉你第一个尝试失败来证明你做,错了。它尝试了第二个也失败了。它尝试了每一种可能。都不成功。

好的,这就是为什么你会收到错误消息,因为它尝试了每一,种特化。我们快没时间了。我确实想快点结束这个。我们在这个小的代码片段中。花两三分钟时间尝试弄清楚为什么这个实现有效。我得给大家再一分钟。

尝试弄清楚这个、这些如何工作。可能值得讨论这两个例子,为什么第一个给你假,第二个给,你真。有人想试试吗?聊天里有一些人。我可以试试。你可以试试。这有点奇怪。就像是,如果你传入的是相同的类型。

那么它会默认使用 ,is same,因为它有 TT 这表示相同类型,但如果你有两种,不同类型,它们会使用类型名 T 类型名,你,我认为,可能,对,对,完美。所以,嗯。

我们在这里利用编译器会在转到通用的之前,始,终检查最特化的这一事实。好的,这一点非常重要。在第一个例子中,当类型不同的时候。它首先尝试匹配底部的那个,它会尝试,好的,我可以把 T ,作为 int 吗?

不行,这不起作用。我可以尝试把 T 作为 double 吗?不行,这也不起作用。所以这个失败了,它默认使用这个。这就是为什么当你运行第一个时你会得到假,因为值字段,这里是假的。当 T,什么时候。是的。

第一个的澄清,应该是 is same 括号 T 逗号 U。是的,好问题。所以实际上我也曾对这个感到困惑很久。事实证明,如果你写的是通用模板,你不需要在这里写类型。

所以你是否特化是根据是否有尖括号来区分的。对于底部的例子,如果 T。所以如果两个都是 ints,编译器会首先尝试匹配底部的那,个,它尝试 T 等于 int,结果成功了。所以当你创建这个类时。

你得到的是底部那个,这就是为什,么如果你尝试访问它的值字段你会得到真。在回答问题之前,只是想说一下。所以以后作为参考,你可以阅读幻灯片以获取参考,但,我。

们实际上可以使用相同的技术来实现许多其他不同的功能,每当你需要一个如果-否则语句时,你可以使用这个技术,这将给你一个如果-否则语句。例如,你可以实现 is pointer。好的,几乎相同的实现。

它会尝试匹配底部的那个。这个只有在你实际选择 T 为匹配指针的类型时才会成功,这样,底部的那个将为真。否则,它会默认为旧的那个,会是假的。类似地,我们可以做的是,除了返回一个值,你实际上可以。

返回一个类型。所以有一个叫做 remove_const 的元函数。它的作用是尝试匹配一个常量。如果它成功匹配了常量,那么它会将 T 作为它的类型。所以注意这里的常量已经没有了。如果它失败了。

不能匹配常量,那么它也会将 T 作为类型,所以如果你尝试去除一个不包含常量的东西的常量,那么,它会返回那个值。好的,你可以看到,这种技术基本上用于实现一个巨大的 ,if-else 语句。

如果你需要一个 if-else-if-else-if-else-if-else 语,句,你基本上可以做同样的事情,只需做很多不同的特化。确保你写它们的顺序是有意义的。

然后这基本上就是一个巨大的 if-else 语句。这个通用的就是巨大的 else,如果没有任何特化的能工作,简单总结一下。是的,继续吧。在这个例子中,如果里面的内容在两种情况下都是相同的。

为什么我们需要两个不同的呢?是的,好问题。所以我们这里需要不同的原因是底部的一个会做稍微不同,的匹配。当你尝试匹配底部的一个,例如 const T,并且我们尝试将。

其匹配到这里的 const int 时,这里的 T 不是 const ,int,而是 int。好的,所以我们在看这部分,并且尝试将其匹配到尖括号中,的内容。我们得到的 T 实际上是 int。

这就是为什么当你在这里放,置它时,我们得到 int 的类型。嗯,上面虽然主体是相同的,但它尝试匹配的内容有些不同,所以在这里,如果你尝试匹配,这里说 T 是整个内容,而不。

仅仅是 const 后面的内容。谢谢。是的,没问题。好的,我想时间快没了,所以我想快点完成这个。如果你需要离开,我们已经涵盖了我想介绍给你的最重要,的内容。

这就是你可以在编译时实现 if-else 语句的方法,这非常笨拙,但它有效,并且非常有用。好的,我会稍微超时,以便我们可以把所有内容都完成。但是,是的,这是一种技巧。

我们利用编译器的模板匹配规则来实现 if-else 语句。我们通过利用模板匹配规则来实现 if-else 语句。

好的,让我快速总结一下。

那么我们如何实现这个距离函数?让我快速实现这个距离函数。

好的,所以我们来实现这个距离函数,我要做的是,首先尝,试实现这两行代码。Category 等于无论什么类型的迭代器。我们怎么实现呢?所以结果是我们还没有编写这个元函数,但 STL 里有很多。

非常有用的元函数。有一个叫做 iterator_traits 的元函数。所以使用 category 等于 std:iterator_traits。这是一个元函数。它的作用是你可以输入一个迭代器类型。

这非常酷。这个元函数有很多不同的返回值。好的,它不仅仅有一个返回值。它有很多不同的返回值。你可以使用不同的名称来访问每个返回值。所以我们想要的名字是迭代器,迭代器编译失败。哦,对了,这里有独立的作用域。

所以这是之前 Ethan 或 Nikhil 介绍的类型名称错误。因为我们在这里放入了一个迭代器类型,我们有一个依赖,的模板类型。所以我们在这里只需放置一个类型名称。

这是 C++ 中唯一有用的编译器错误信息。所以这是一个元函数。它有很多返回值。我们将特别选择一个称为迭代器类别的返回值。发生的情况是,类别是一个代表某种类型的类型,它实际上,是一个常量类型。

你可以将其视为一个常量,它告诉你这个迭代器属于哪个,类别。然后我们可以在这里实际使用我们自己的 is_same 实现,好的,是的,我们可以使用我们自己的实现。我们也可以使用 STL 的实现。

你将传入类别,并检查它是否等于随机访问迭代器,这里有,一个类型,有一个常量,称为随机访问迭代器标记。这基本上是如果这是一个随机访问迭代器,它将是什么类,别。不要忘记获取返回值,即双冒号值。Avery。

一些学生想知道你能否重复一下为什么需要类型名,称。当然。所以我不太确定类型名称是如何引入的。但原因是因为有一个叫做依赖模板类型的东西,当你试图,在这里放入一个模板参数并尝试访问成员类型时,你必须。

放一个类型名称,以便 C++ 不会被角括号或其他东西混淆,老实说,我教的方式是,这是 C++ 中唯一有用的错误信息,如果你收到错误信息,它会明确告诉你如何修复它,然后就,去修复它。

所以我们在这里的教学方式就是你必须这么做,因为有一,些,它只是为了消除一些奇怪的编译器解释。你就这么做了。原因是因为它是一个依赖类型,也就是在双冒号之后,它是,一个控制依赖类型的模板参数。

而这种特定的操作序列会导致一些编译歧义,除非你修复,它。我必须坦白。我几乎不理解为什么我们需要在这里使用类型名称。但我知道编译器会准确告诉你为什么。所以如果你收到那个错误,就写类型名称。无论如何。是的。

所以这基本上是实现。但请注意,它仍然无法编译。好的,如果我们放入 vector,它会编译。如果我们放入 vector,这会编译。这会有效。是的,这编译了。好的。我想要的,其实。

我会向你展示接下来的很酷的东西。但如果我在这里放一个 set,它不会编译。原因是,因为在编译时,编译器会发现这个表达式是假的。所以你将永远不会运行第一个。你将始终运行第二个,这是我们想要的。

问题是这段代码仍然在代码中。对。这行仍然在代码中。而这行无法编译。如果你传入的是 set 迭代器,因为迭代器之间不能进行减,法操作。这就是我们有这个编译器错误的原因。好的。

所以问题是我们不仅需要能够确定要运行哪个分支,我们还需要一种方法来完全从源代码中移除另一个分支。即使这行代码永远不会被运行,我们也不能在源代码中保,留它。如果它在源代码中,代码就不能编译。好的。

那么最后我们要介绍的是如何在编译时更改源代码?

哦,实际上,我有一张幻灯片讲这个。是的。所以其他分支和编译,即使我们知道那个分支永远不会被,运行。我们需要一种方法来移除这些有问题的代码,如果我们能,找出哪些部分不会被运行。

我们知道 if 语句永远不会被运行。所以我们必须完全将其从代码中移除。好的,在 C++ 17 之前。所以注意我们现在是 2017 年。你必须使用这个 enable if,这可能是我最喜欢的函数之,一。

这是你能找到的最 hacky 的函数。它会故意生成替代失败以欺骗编译器。如果你感兴趣,课后问我。这真的很有趣。但这也有点复杂。所以 C++ 17 引入了一个叫做 if constexpr 的东西。

if constexpr 做的是在编译时计算布尔表达式。然后根据它是 true 还是 false,它会用实际运行的代码,替换整个 if else 分支。这真的很酷。它在更改源代码。

所以我们实际上加上 constexpr。

if constexpr,注意错误消失了。好的,所以现在这还不够酷,因为我想给你展示实际生成的,源代码。所以我们实际去设置里。

把它切换到浅色模式。我不知道为什么深色和浅色会给你不同的东西。但在浅色模式下,你可以看到一些真的很酷的东西。所以如果你不理解汇编也没关系。老实说,我现在也几乎不理解它。但如果我们去看看。

我觉得这些中有一个是我们想要的。是这个吗?对,就是这个。好的,所以这是我的距离函数。好的,这非常好,因为它会突出显示每行代码如何转化为汇,编。这里有一件事非常明确,那就是注意第 15 行和第 16 行。

它们没有被突出显示。所以在汇编代码中,这两行甚至不属于汇编代码的一部分,这就是为什么一旦你添加了 constexpr,你不会得到编译,器错误,因为当编译器将这个表达式计算为 false 时,它。

会完全移除这部分代码。所以这就是为什么在汇编代码中,你只有这些行。好的,然后只是为了演示,如果你做一个向量,你会得到另,一个分支。是的,你会得到另一个分支,你可以自己查看。如果你去看看那个函数。是的。

超级短的函数,因为它只有一行。你可以看到它调用的是减法运算符。所以注意这部分代码完全从源代码中移除了。如果你在 C++ 17 中,只是想给你展示一些酷的东西,你可。

以清楚地看到这是一个 while 循环,因为有很多跳转。如果你现在正在做二进制炸弹的练习,那就有一个测试。有一个 J 等于。这里有一个跳转,它显示了这段代码正是这段代码。好的,很酷。基本上就是这样了。

我想讲的内容就这些了。我们超出了些许时间,不过没关系。我们实现的最终代码是这样的,其中 if constexpr 让你,在编译时确定这个表达式,它会实际改变 if 语句或运行,的分支。因此。

你可以在 if 语句中放置不编译的代码,只要 if 语,句能够帮助你确定那行代码是否编译。非常感谢。这真是一次非常有趣的讲座。我还有两张幻灯片。所以,是的,关键点是,最终生成的代码是这两种。然后。

模板元编程就是你在修改程序的实际源代码,constexpr if 可以让你打开或关闭代码中的不同部分,等等。关键要点是,模板让你处理变量类型,元函数让你修改这些,类型或查询信息。

constexpr if 给你灵活性来修改源代码,这允许你根据类型优化代码。你可以想象这种代码在 STL 算法库中随处可见,根据迭代,器的类型,它会基于该类型进行优化。就是这样。哦。

CMP 是在哪里出现的?你大约两周前学过 std:move。我不认为 Ethan 和 Nikhil 曾经深入研究过 std:move ,的参数和返回值,但你可以看到。是的,你可以看到 std:

move 的返回值是你需要基于元函,数获取的那个值和类型。这是一项非常有趣的练习,去弄清楚为什么。这是一个你必须考虑的奇怪边界情况。好吧,所以 STL 的 std:move 中有一个名为 。

remove_reference 的元函数。其次,如果你做任何机器学习,PyTorch 有一个索引选择函,数。我在 CS224n 中经常使用这个函数。注意你会看到 enable 这里的情况也是一样的。

这本质上是他们优化的一种方式,如果你的张量是 float ,类型与其他类型的比较。所以很酷的优化。好吧,就是这样。接下来去哪里。有一个很酷的讲座,受到我的讲座的启发很大。去看看吧。

C++20 引入了一个叫做概念的东西。概念的目标是让一切变得不那么奇怪。让整个代码,一切我们今天学到的东西,更自然,而不是像,黑客一样。所以如果你想进一步学习,可以看看概念。概念是非常非常酷的。好的。

很棒。感谢你们让我超时。这很典型,当我教这个课程时。我认为到最后,我们就放弃了 50 分钟的讲座,改成了 70 ,分钟的讲座。无论如何,很酷。我可以回答任何问题吗?在回答问题之前。

我想感谢大家今天的到来。这是一次非常有趣的讲座,所以谢谢大家。让我们都解开静音,为 Avery 鼓掌吧。谢谢。太好了。是的,我认为 Avery 会在这里回答你们的问题。我能在聊天中留下的一个问题是。

如果在你的作业中使用 ,hash if 会发生什么,以及为什么当它设置为零时会使代,码失效?我相信那是一个预处理指令。它在一个更基本的层面上工作,也就是在你开始编译之前,将文本传递到计算机中。

所以你可以避免它。这是一种不同的避免方式。但使用预处理指令不是很灵活,因为你只能做非常简单的,事情。是的。实际上,今年夏天,我尝试改进测试工具,实际上使用模板,元编程。但我没能在时间内完成。

以便向你们发布。所以你们必须处理旧版本。但是下个学期的课程将会得到华丽的模板元编程版本。太棒了。这就是为什么我们依赖 Avery 来编写我们的作业,因为我,们不知道如何做 TMP。是的。

我可以回答任何问题吗?我有一个问题。当然。你能讲讲 enable if 吗?我花了一些时间阅读有关它的内容。我还是不明白它是如何工作的。所以了解一下会很有帮助。当然。啊,有六张幻灯片。是的。

实现看起来是这样的。所以看几秒钟。实际上,我让你们看几秒钟,然后我会回答其他问题。然后尝试思考一下为什么,这个是如何有用的。有趣的部分是,注意到 enable if 的通用版本没有类型。

所以它有点像下面那个。下面那个是特定类型,其中如果 B 是布尔值。所以如果 B 为真,那么它将匹配下面的那个,你将会有一,个叫 type 的成员。好吧,所以如果 B 在这里为真,那么整个东西都会给你它。

如果 B 为假呢?那么你会得到垃圾,因为那个类型甚至没有定义,你会得到,一个 schema 错误。是的,你会得到一个编译器错误。然后,启用条件在这个上下文中使用,所以注意到这个和布,尔值相同。

这个在这里和布尔值相同。对。好的。但是如果,如果相同在这里返回假值,那么整个函数不会编,译失败吗?因为那里没有定义类型。是的,完全正确。所以,如果在这里相同为假,那么 enable if 就没有一个。

叫 type 的成员。所以这一行将会编译失败。好吧,这里就变得非常hacky了。我们故意让这一行不编译,因为我跳过了一张幻灯片,因为,它太复杂了,但是有一个叫 SFINAE 的模板规则。

替换失败不是错误,当你尝试替换一个类型,失败时,编译,器会忽略它。但我很惊讶这样的错误,比如说,根本没有放入类型。是的,它只是把它视为 SFINAE 错误,而不是像总体的终止,编译器错误。是的,是的。

然后,你必须以特定的方式使用它,你必须把 enable if ,放在头文件中。在模板之后,因为那样被认为是模板替换错误。是的,这里有趣的地方在于这一行。如果它为真,它什么也不会做。

就只是给你一个空的返回。好吧,这个东西你本可以直接在这里写 void,一切都会很,好,但我们故意在这里添加了额外的内容,以便如果这个内,部布尔值为假,这将会生成一个编译器错误。

我们故意制造一个编译器错误,以便编译器会决定忽略它,这就是为什么它叫做启用如果(enable if)。如果它为真,如果它真的为真,那么这个函数存在。如果它为假,那么这个函数不存在。这说得很好。是的。

我得承认,在我理解模板元编程之前,我遇到过启用,如果(enable if),我只是很难弄清楚这一行到底在做什么,好的,但现在你知道,为什么你会在这里得到编译器错误,是因为它甚至没有类型。正是这些时候。

你会希望自己写的是动态类型语言。是的,你告诉自己,这值得为了速度的提升。是的,你会看到一些编程语言尝试,嗯,我对其他编程语言,了解不多,也许是 Rust,也许是 Go。Rust 不做这种事情。

Rust 没有,Rust 在整体上对奇怪的模板情况了解较少,他,们一般尽量避免这些,至少我知道的情况是这样,但再次强,调,我对 Rust 极为不熟悉。好的,是的。你只能在 C++ 中遇到这种疯狂的情况。

哦,引用一下,我该如何,我要如何在 Rust 中复制我的基,于 SFINAE 的 C++ 代码。有一种叫做特化(specialization)的东西,它让你用更优。

雅的方式实现这一点。它和 SFINAE 完全不同。

而且它仅在 Nightly Rust 中可用,因此不太可靠。你可以使用特性(traits),它们基本上类似于概念,(concepts),对吧,这解决了所有问题,而且方式要好得多,对不起。

特性(traits),不是接口(interfaces)。有趣的是,Google 推荐你避免模板编程。是的。所以基本上,如果我没记错的话,在 Rust 中,特性(trait),让你在类上贴标签。

而无需通过继承系统。它让你只为类编写函数,并匹配特定的特性,这让你可以绕,过整个编译时检查类的问题,因为你可以定义一个符合需,求的特性。是的,是的。事实上,你在这里看到的这些模板,这些元函数,实际上是。

类型特性(type traits)库的一部分。太棒了。所以,对不起。是的,所以你现在知道如何自己实现这些元函数,但实际上,有一个完整的库。有一个巨大的库,里面包含了你可能需要的每一个元函数,例如。

如果你想检查类型是否为浮点数,你可以在这里找到,然后你可以实际查看它是如何实现的。不,你实际上可以查看它是如何实现的。是的,有趣的是,你基本上从这些低级的构造开始,比如 is, same。

然后你可以逐渐构建到更复杂的构造,比如 is ,floating point,然后不知怎么的,你能够获得这些。你可以检查它是否有拷贝构造函数,我觉得这很酷。你可以检查它是否有虚析构函数。

你可以检查它是否有。是的,各种各样的东西。有一个问题,抱歉。编译时宏中的字符串文字哈希是什么意思?是的,是的。嗯,让我看看,嗯,已经有一段时间没有写这个了,所以让我,给你展示一下。

编译时宏中的字符串文字哈希。所以,你会创建一个,创建一个类似宏的东西。然后,嗯,是的,所以发生的事情是,你会创建一个宏。然后,每次你想要哈希那个宏,每次你想要哈希一个字符串,时,你将字符串包裹在宏周围。

所以,让我打出来。所以你会做一些事情,你会创建一些宏,它可以是一个实际,的宏,也可以是一个称为哈希字符串的模板元编程宏。然后,每当你在代码中想要哈希一个字符串时,你就把它包,裹起来。你好。然后。

这需要在编译时用实际的哈希码47374替换每一个实,例。是的,关键思想,我记得做这个很难让它编译成功。让它工作并不难,让它编译成功是非常困难的。是的,我刚刚发布了一个不使用TMP的旧版本链接,但基本。

上他们写了一堆互相接口的宏来实现某种字符串哈希算法,我会说,第二个会更容易理解,一旦你上过一门七的课程,因为有很多编译器相关的内容,如果你更深入地理解编译,器的工作原理会有帮助。是的。

尤其是如果你查看Ethan提到的那个链接,其中有很,多位移操作。所以我认为一旦你上过一门七的课程,研究这个会更有意,义。如果这种无聊的东西对你有吸引力,考虑参加一个编译器,课程。有一些很不错的。

我的意思是,每个人都知道在这个领域应该学些什么,但像,高级编译器课程有一些非常有趣的东西。是的,我还没有上编译器课程,但我会在某个时候去上。是的,我没上。是的,是的。

我决定出于某种原因参与了那一季度的229,跳过了编译器,课程。所以,我没有上编译器课程,但我可能会去上编译器课程。是的,但无论如何,这些关于操作系统和编译器的课程都很,酷。

你会了解到很多关于编译器的工作原理。然后这将大大帮助你理解C++,真正理解C++。了解性能的带来,查看汇编代码有很大帮助。我最喜欢的C++ YouTuber之一。是的,有C++ YouTuber。

你不敢相信,有C++ ,YouTuber,Jason Turner,他的视频总是使用这个编译器探,索器,他会向你展示代码是什么样的,汇编是什么样的,告,诉你哦,是的,实际上有性能提升。

我们通过写这个而不是那个节省了两个汇编指令,类似这,样的东西。是的,通过查看课程来想象优化的样子很有帮助。好,还有其他问题吗?你看到C++的短视频了吗?哦,真的吗?哦,哇,我实际上从未见过那个。

有人在聊天中提到,甚至提到过C++短视频,我不认为它们,真的存在。我希望它们不存在。不存在。使它成为替代作业或一个好点子。让我停止录制。

斯坦福大学《CS106L: C++编程| Stanford CS106L C++ Programming 2019+2020》中英字幕(豆包翻译 - P2:[1]CS 106L Winter 2020 - Lecture 2_ Streams - GPT中英字幕课程资源 - BV1Fz421q7oh

这有点烦人。

工作太多了。

没事,没事。我明白了,我明白了。

这是全屏。

太棒了。好吧,我会在一分钟内开始。这个给你,好吗?好的。大家好。欢迎回来。耶。那么,你们的第一周怎么样?课程确定了吗,一切都安定下来了吗?太棒了。好吧,今天开始下雨了。我担心你们有些人可能不会来。

但雨停了,我很高兴你们都,来了。今天,我们将讨论流。为了提供背景,我这里大约有100张幻灯片。上个季度,我有300张,一个学生在反馈表上写道这是他们,经历过的最压力的体验之一。所以,是的。

今天会非常非常可管理。好的,我们要讨论流。今天我们要涵盖的内容是,首先进行流的高层次概述以及,流存在的原因。然后我们将讨论最基本的流类型,字符串流。接下来我们将讨论状态位,并讨论IO流。最后。

如果时间允许,我会做一个两分钟的流操作符介绍。

它们不是特别重要。好吗?但在此之前,我意识到Keith没有讲解C++中的字符串是什,么,所以我将给你们一个关于C++字符串的快速速成课程。好的?那么,在大多数语言中,你怎么表示一个字符串?

你怎么创建一个字符串?对。当然,对。所以字符串,一些变量名,比如str等于,然后你可以在这里,声明你想要的内容。比如说hello world。好吗?所以,是的,这会创建一个字符串对象,字符串。

然后字符串,本身保存hello world。所以如果你尝试输出,它会将字符串打印到cout。字符串的一些奇怪的特性。让我们看看。关于字符串的一点特别之处是,如果你说你想获取一个单,一的字符。

有人知道怎么做吗?这和Java和Python一样。你怎么获取一个单一的字符?是的。对,好的,对。在Java中,你会使用。carat,对吧?是的,嗯嗯。在Python中呢?你索引它,对吧?好的。

在C++中,你做索引的操作,比如说你想获取第一个字符。这会打印什么?e,对吧?因为别忘了,字符串是零索引的,就像其他数据结构一样。所以如果你打印1,你会得到e。不同于Python,你不能做奇怪的。

像负一这样的操作。你不能那样做。在C++中,你只能基于实际的索引来索引。还有什么?你怎么修改一个字符?假设我想把hello world中的e换成其他字符。对,嗯嗯。酷,对。在Java中。

如果你想修改一个字符,你必须做这样的操作。字符串会从0到1做一个子字符串,再加上新字符,比如i,再,加上另一个子字符串到其他地方。但在C++中,这非常好。在 C++ 中,你可以将这个字符串视为一个数组。

所以你可以直接这样做,好吗?在 Java 中,因为字符串对象更安全,所以你不能这样做。但在 C++ 中,你可以将字符串视为字符数组,并且可以直,接这样设置。明白了吗?有问题吗?是的,有问题,是的。

好问题,是的。当你索引一个字符串时,每一个字符都是字符。所以你应该将其设置为字符。是的,这确实是个很好的观点。尤其是如果这是你第一次使用 C++,你可能会尝试这样做,然后你会遇到编译错误。是的。

编译错误也很困惑。所以,好问题。好了。最后我想简要提一下,如果你传入——好吧,我在决定如何引,导 C 字符串。这个字面量,其实是一个 C 字符串。C++ 中有两种字符串。一种是 C 字符串。

另一种是 C++ 字符串。明白了吗?C 字符串的类型是 char*,或者 char[]。而 C++ 字符串是一个真正的字符串。明白了吗?通常,总是使用 C++ 字符串。要使用 C++ 字符串。

基本上声明一个名为 str 的字符串,对象。明白了吗?好了。今天我们就讲到这里。其他的内容你会在周五跟 Keith 学到。

好。我们来谈谈流。那么流为什么存在呢?嗯,我们通常希望流能够与外部设备进行交互。有很多外部设备,你的程序可能需要与它们交互。一些最常见的,比如键盘。所以控制台和键盘是你最常见的设备。事实上。

在 CS106B 中,你已经见过 cout。Cout 是一个连接到控制台的流。所以如果你打印一些内容到 cout,你会在控制台上看到它,这些流也与键盘连接。所以如果你输入字符。

这些是连接的——键盘连接到一个流,内部。你也可以处理文件。我们今天不会讲文件,但 Keith 明天会讲文件。明白了吗?所以明天你会学习如何从文件中读取内容。你会学习如何写入文件。

这些也都是通过流来完成的。我们不会讲这两个,因为这两个相对高级。你可以写——你可以让不同的程序同时运行,然后它们相互,发送消息。它们在相互通信。这使用了一个叫做管道的概念。

你会在 CS110 中学习这个。非常酷,但基本上你还可以使用流将内容发送到其他程序,最后,流也用于网络编程。我们不会讲这个,但你可以参加 CS144。你将学习如何将内容发送到流,然后流会处理将内容发送。

到其他服务器。明白了吗?流的酷炫之处在于它是一个统一的接口,对吧?几乎所有的——有一个流。流可以做各种各样的事情,并且使用流的方式对所有四种,都非常相似。好的?例如。

你们都知道如何将内容打印到 cout。你们基本上对其他操作也是做同样的事情。好的?我个人喜欢称之为鳄鱼运算符。它是两个小于号。对吧?这就是你如何打印到 cout 的方式。

这也是你如何打印到其他程序的方式。好的?所以流的重点在于它提供了一个统一的接口来处理各种交,互。所以假设你有一个名为 date 的对象。我们还没有讲到对象,但我假设如果你来自 Java 或 。

Python,你知道这些对象是存在的。你如何打印——假设你想将这个对象打印到控制台上。你会怎么做?嗯,有两个主要步骤你必须完成。首先,你需要将程序中的信息变量类型对象转换为字符串。

计算机没有直接、简单的方法来知道,将其转换为流后会,变成什么。所以这是你必须管理的第一件事,类型转换。然后第二部分是你必须实际将那个字符串写入控制台。明白了吗?好的。你认为哪个操作更难?有什么猜测吗?

哪个似乎更难?好的。是的,类型转换似乎有点复杂,对吧?你需要读取字段并进行处理。但是你知道怎么做吗?如果我给你一个对象,你能将它转换为字符串吗?可能可以,对吧?你可以写一个表格。一个是一月,二是二月。

只需在那儿有一个表格。你可以读取月份,找出是哪个月份,找到日期,打印出来,将,日期放入字符串中。好的?所以这有点复杂,但你们都知道怎么做。另一部分,实际写入控制台,那比较复杂,对吧?比如。

你如何实际操作控制台?对吧?你可能甚至在 Java 中也没有学过这些内容,对吧?你还没有学会如何将内容显示到控制台上。好的?所以第二部分是难点。流的目标——好的,我再举一个例子。

假设你想从文件中读取一个双精度浮点数。那么有两个步骤。你首先必须从文件中读取字符串表示,因为,记住,文件只,是一个字符的集合。然后你将其转换为双精度浮点数。好的?你可能能做到这一点。这有点麻烦。

你首先要找到小数点的位置。你必须找到所有字符。你需要将它们全部转换为数字。但你能做到这一点。你如何从文件中读取一个字符?这有点复杂。所以,是的。再次,这两个挑战就是这样。

流的重点在于它提供了一个统一的接口来处理整个过程。不用担心你如何读取或写入内容,你可以将流视为字符的,缓冲区。好的?不要担心流如何实际接收你的字符并将它们放入控制台或,你正在写入的任何文件中。

你可以将整个过程视为一个大的数组。明白了吗?然后,交互流的接口是通过输出和输入运算符。明白了吗?所以,是的。例如,如果你在向 cout 打印,你只需将内容放入 cout 中,然后,cout 会将字符。

比如 3。14,写入 cout 的缓冲区中,之后,cout 缓冲区会将缓冲区中的内容转移到你的控制台,中。明白了吗?反方向也是一样的。你其实不知道你的流是如何从文件中读取到缓冲区的,但。

你知道里面有一个缓冲区,你可以从中读取内容。明白了吗?所以你只需要关注这个操作。流本身会处理其他所有事情。明白了吗?目前有什么问题吗?这有点像是一个高级的概念。这比较抽象。

你实际上还不知道这是如何工作的,所以我们会很快举个,例子。明白了吗?首先,我们来谈谈字符串流。字符串流很简单,因为它们不依赖于任何东西。明白了吗?它们基本上只是做这个转换而已。明白了吗?

如果你向字符串流缓冲区写入内容,这些内容就会保留在,那里。不会转移到其他地方。明白了吗?

所以让我们做一个例子。你们都把这段代码调出来了吗?如果没有,那也没关系。你可以找一个旁边的人,他有调出来的代码。我喜欢讲座有互动性,所以边做边填充内容。正如我在这里写的,我写了很多内容。

你可以看看下面写了什么,但你可以调用这些函数看看发,生了什么。继续跟着做。尝试输入一些代码。你也可以尝试不同的东西。如果你注意到有什么意料之外的情况。明白了吗?首先,我们将尝试使用字符串流做一些操作。

我们需要做的第一件事是如何构造一个字符串流?构造字符串流有两种类型。有输入字符串流和输出字符串流。我们先创建一个输出字符串流。好的,所以我们刚刚声明了一个输出字符串流。实际上。

我们稍后会学习为什么这是一个错误。是的,所以我们声明了一个输出字符串流。实际上,为了让它更有趣,我会先在字符串流中放入一些内,容。比如说 ito 和 greenT。好的,所以现在你应该想象一下。

一旦你创建了字符串流,你就有了一个字符串流,并且它的缓冲区中已经包含了这,些字符。明白了吗?好的,这是一个输出字符串流,这意味着你可以向字符串流,中写入内容,但不能从中读取。好的。

那么假设我向其中写入一些东西。比如我写 16。9 盎司。实际上,在我们这样做之前,先试着运行一下这个代码。好的,所以流有一个特殊的操作,你可以基本上将缓冲区中,的内容转储出来,创建一个字符串。例如。

我可以做类似 oss。str 的操作。这个方法将缓冲区中的内容转换成一个字符串。好的,这样理解了吗?那么你期待这里打印出什么?Ito 和 greenT,因为我们构造了一个字符串流。在它的缓冲区中。

你开始时是这些字符,我们只是立即将缓,冲区转换回字符串。所以它应该仍然是 ito 和 greenT。好吧,目前有任何问,题吗?好,现在我们实际上可以向 oss。str 写入内容。例如。

我们可以写入 16。9 盎司,然后我们可以再次尝试打,印 oss。str。你期望打印出什么?是吗?好问题。所以当你声明。问题是,当你创建一个字符串流对象时,它是否有自己的缓,冲区。

或者当你创建多个字符串流对象时,它们是否共享同,一个缓冲区?答案是每个字符串流都有自己的缓冲区。所以你可以看到我们实际上有两个流在运行。我们有 ostringstream。这是一个字符串流。

我们还有一个 cout。Cout 也是一个流。这两个缓冲区不。它们不会互相共享内容。所以我们可以向 cout 打印内容,而我们的 oss 仍然在进,行操作。好的,好问题。是的,好问题。

你为什么会使用流而不是字符缓冲区?正如我们很快会看到的,写入内容是很好的,但真正的强大,之处在于从流中读取内容。流本身会自动为你找到字符。它还会进行类型转换。例如,如果你想从缓冲区中读取一个整数。

这有点困难。你必须找到空格的位置。你必须将字符转换成一个整数。如果你使用流,你可以直接读取整数,我会很快展示这个。这是个很好的问题。每当我们教你们东西时,询问为什么使用这个而不是其他。

类似的东西总是好的。这是一个值得持续提问的好问题。好吧,你能猜测这里打印了什么吗?有任何猜测吗?好吧,如果不确定,你总是可以运行代码。

让我们看看发生了什么。

好的,那么刚刚发生了什么?是的。好的,所以长度相同。有什么变化?正是这样。16。9 盎司,我们基本上覆盖了里面的缓冲区。

所以这是一个非常重要的概念,就是缓冲区本身。

如果你查看这些幻灯片,你会注意到我有一个箭头,它会显,示当前位置。这决定了所有内容的读取和写入位置。当你写入内容时,位置指针会一直向下移动。最初,当你构造一个字符串流时,它最初从开始处开始,这。

就是为什么我们覆盖了缓冲区。

好的?你不需要记住这些,但如果你想从字符串流的末尾开始,你,可以做类似 stream, stream 的操作。你可以提供一个常量,称为 A-T-E,代表末尾。

字符串流,它未定义。

真的吗?这应该能工作。好吧,这很奇怪。哦,字符串流。哦,搞定了。谢谢你。是的,好吗?所以你可以这样做,然后如果你尝试运行这个,位置指针从,最末尾开始。而且,当然,这个文件没有找到。

明白了吗?这可能是我们最初想要的。现在位置指针从末尾开始。

好吧,还有其他问题吗?嗯?一、二。是的,它们被表示为字符。你指的是哪一张幻灯片?是这张吗?第18张,好。这张幻灯片?是的。所以不要忘记流的抽象在这里。这就是流的抽象。所以这个字符串表示。

其实是缓冲区中的写入内容,字符被,存储在那个缓冲区里。你在这里看到的变量,就是你在程序中写入缓冲区的内容。

。我想我知道你的意思了。你的意思是,比如说,在这里我们可以直接写16。9吗?我们可以尝试这样做吗?是的。数字到字符串的转换,它只是将其转换为字符串并存储在,那个字符串里,无论那个字符串是否在内存中。

好问题。所以字符串流或任何流的一个很酷的功能是它们会为你进,行转换。所以你可以放置任何支持插入的类型,它会在放入之前将,任何类型转换为字符串。这样会有效。所以如果我们改为16。9。

它们会被转换成单个字符并放入,缓冲区中。明白了吗?很酷。好吧。所以字符串流没有太多复杂的,但它们很好玩。让我们尝试一个输入字符串流。我们从相同的内容开始。实际上,让我们做16。9盎司。明白了吗?

所以输入字符串流的好处是它们为你做了转换。明白了吗?所以假设我想读取开头的内容。所以16。9。然后我想在我的程序中实际使用它。明白了吗?我可以做的是直接用一个双精度浮点数变量。

我们还可以创建一个叫做单位的字符串。然后我们可以直接提取双精度浮点数,再提取单位。明白了吗?所以注意会发生什么,我们创建一个输入字符串流,它有这,些字符在缓冲区中。位置从开始处开始。当你调用这个操作时。

字符串流会将位置向下移动尽可能,远,直到遇到空白字符,然后抓取它经过的字符,并将其转,换成你所要求的类型。所以这里,位置从开始处开始,然后向下移动,读取尽可能,多的内容,直到遇到空白字符。

然后将它读取的字符转换为,你要求的类型。所以在这里,它将16。9直接转换为一个值为16。9的双精度,浮点数。明白了吗?到现在为止怎么样?这有意义吗?好吧,为了证明它是双精度浮点数。

我们尝试打印amount除,以2。你期望打印什么?8。45,正是这样。好的,给你这个。8。45。好的,重要的是你读入的类型很重要。好的,如果我把这个转换成字符串,哎呀,这仍然会工作,除,非你做ISS。

它尝试抓取一个字符串,它抓取16。9而不是将,其转换成字符串。好的,这就是为什么这个amount除以2不工作,因为amount,是一个字符串。快速问题,如果我这样做呢?然后我打印出来,好吧。

你期望打印出什么?我听到有人说了答案。8,对,因为它向前移动,尝试提取尽可能多的字符,这些字,符对读取为整数有意义。它看到小数点,然后就像,好吧,小数点不能是整数的一部,分,所以我们必须在这里停止。

然后它抓取16,将其转换为整数,放入amount,所以你在这,里得到8。unit是什么?unit会是什么?好的,我听到。9和16。9。答案是。9,原因是位置已经移动到这个位置。它尝试读取16。

所以位置移动到那个位置。然后当你尝试做这个操作时,它读取到下一个空白字符,所,以它读取这个。明白了吗?所以试试看,它可能会工作。

是的,所以8,然后是一个空格,接着是。9。

问题?是的?

好问题。所以你问的是,当你进行错误操作时,分隔符是什么?好的,是的,好问题。所以如果你在读取一个字符串时,如果你在提取东西到字,符串中,那么分隔符将是一个空格。明白了吗?事实上,不只是任何空格。

这是一个幻灯片,显示了什么是空白字符分隔的标记。如果我们尝试读取,假设这些是字符串,如果你尝试一个一,个地读取字符串,第一个会是标记。它会跳过反斜杠n,这表示换行,点后面是一个句号。

它将单个句号读取为标记3。它跳过这些反斜杠键,这是一个制表符。一堆换行符。它跳过所有内容,只读取下一个。明白了吗?是的,它读取一个空白字符分隔的标记。还有其他问题吗?后面有人有问题吗?好的,太棒了。

好的,所以类型确实很重要。如果你尝试,第二部分的回答是,如果你正在读取各种其他,类型,而不是字符串,那么它会读取尽可能多的内容,只要,类型仍然有意义。所以如果你读取一个整数,1和6作为整数是有意义的。

但句,号不再有意义,所以它在这里停止,读取6作为整数。明白了吗?现在,这应该引起很多问题,比如说,好吧,它试图读取整数,如果不能怎么办?那应该是你立刻想要思考的问题。如果不能怎么办?

我们很快会讨论这个问题。你有问题?好的,正是如此。好的,太棒了。是的,所以简单回顾一下。如果你,分隔符操作,它将变量转换为某种字符串字符形式,插入到缓冲区中,就是这样。所以你可以链式操作这些操作。

所以如果你链式操作,它首先执行第一个操作,然后执行第,二个操作。如果你链式调用底部的那个,它会读取到第一个,然后读取,到第二个。他们确实有一个关于为什么这样有效的问题,对吗?比如说,当我学习这门课时。

我总有一个问题,就是,这到底,是做什么的?这看起来像一个非常奇怪的符号。你不能用其他对象这样做,对吗?比如说,你不能读取到一个 vector 中。你不能读取到其他对象中。为什么这样有效?具体来说。

为什么你可以这样链式调用?好吧,实际上,我现在有点想回答这个问题。这些叫做运算符,我们稍后会更多地讨论运算符。但这些运算符是特别定义的,来说明如果你用这个运算符,和流以及其他对象一起使用会发生什么?

在这里,无论是谁编写了流库,都说好吧,你应该将右边的,对象转换成字符,并将其插入到左边对象的缓冲区中。明白了吗?显然,这只有在左边的对象有缓冲区时才有意义。为什么可以链式调用?通过某种操作顺序。

它先执行左边的操作,这个左边的操作,变成并返回 OSS 本身。所以实际上这里发生的事情是,假设我们尝试链式调用这。

个结果。这会起作用。发生的事情是,在这个初始操作完成后,这将返回 ISS 本,身。所以这将折叠成这个。鳄鱼运算符返回流本身,这就是你可以链式调用它们的原,因。有任何问题吗?

很好。好吧。我们跳过吧。我们回答了这些问题。有没有一种流可以同时进行插入和提取?有的。这只是叫做常规流,而不是 I 流或 O 流。你可以在网上搜索一下。这很有趣。这些东西叫做流定位函数。它们比较底层。

它们有点无聊。基本上,你可以根据需要移动位置指针。你可以很容易地在网上搜索它们。你可以弄明白它们是怎么工作的。好吧。如果你愿意的话,你实际上可以访问缓冲区本身。我们不会教授这个,因为那需要理解指针。

但只是供参考,这些东西是存在的。很好。好吧。

信不信由你,你实际上已经足够了解来实现你第一个斯坦,福库函数了。所以第一个斯坦福库函数,字符串转整数。字符串转整数做什么?它接受一个字符串,将其转换为整数。如果转换失败,它会抛出一个异常。现在。

不要担心抛出异常的部分,你可以和你身边的人一起,讨论。尝试想办法写出这个函数。你已经足够了解如何编写这个函数了。而且它只有四行长。开始吧。我没有放那个原型。真糟糕。好吧。所以每个人也必须做这个。好吧。

我们可以继续吗?好的。那么,有什么想法吗?有人想自愿提出一个想法吗?有任何想法吗?有任何想法吗?我给你一个提示。你需要字符串流。那么你觉得第一行是什么?好的。声明一个字符串流。你在参数中放什么?S?

好的。很好。所以我们将S传入,创建一个字符串流,其缓冲区字符来自S,好的。接下来是什么?对不起?好的。在这里声明一个整数,因为我们将其转换为整数。所以是整数结果。接下来。是的。读取。

从字符串流中读取一个整数。最后。好的。是的。所以C out将其打印出来,但这里这是一个函数,所以我们,只需返回结果。好的?是的。很简单,对吧?好的。所以在这里输入代码。所以我在下面写了一个函数。

字符串转整数测试,它让你输,入不同的整数,并为你调用函数。好的?不要担心这个是如何工作的。你下周会学习这个。但是是的。是的。所以在主函数中,尝试调用字符串转整数测试。我写了一个小错误。

我忘了声明这个函数的原型,所以你需要复制它,放在最上,面。好的?我忘了声明原型。所以确保你这样做。然后你可以尝试调用这个函数,你会看到类似的弹出信息。

。它会提示你输入一个整数,比如30。好的。然后它说,你输入了30。我将把它转换为整数。这仍然是30。30的一半是15。好的?所以在这里我们可以看到它成功地读取了字符串30,将其,转换为整数,并除以2。

明白了吗?好的。我们试试其他的。7。好的。所以你输入了7。那一半就是3。所以在这里你可以看到它将字符串形式的30和7转换为整,数,然后对其进行除以2,打印出一半的值。好的?到目前为止明白了吗?好的。

你可以测试一下吗?试着破坏程序。尝试做任何你能做的事情。只要试图让它做错什么事。好的?这并不难。是的?好的。所以如果你输入A,它认为你输入了0,0的一半是0。是的。所以这确实破坏了程序。

还有其他可以破坏程序的方式吗?字符,字母,当然。是的。这也破坏了程序。还有其他方式吗?回车?好的。是的。所以如果你输入回车,它会认为是0。

哦,是的。哦,好问题。你可能不会得到0。请注意一下。大家好。如果你输入回车或输入错误的内容,你可能不会得到0。明白了吗?原因是——Keith讲过了吗?如果你声明整数但没有初始化,它们只是随机垃圾。好的?

所以这个结果不一定是0。它是你声明时计算机里随机的垃圾数据。所以当你打印出来时——如果它不能读取一个整数,这一行,什么也不做。然后当它返回结果时,返回的是结果中原本的垃圾数据。明白了吗?

这些是未初始化的值。

它们不好。它们会引发严重的安全问题。问问Anna吧。对。尝试一些其他有趣的东西。如果我输入8,也就是一个有效的整数,然后我再输入另一,个整数会怎么样?发生了什么?也许我应该试试,比如9和3。

它基本上只读取了第一个。它忽略了其他的。明白了吗?如果我输入一个无效字符然后再输入一个9会怎么样?发生了什么?对。

它执行了第一个,发现哦,这个是错的,所以它什么也没做,明白了吗?

所以我们会尝试修复这个问题,因为——对。这就是通常发生的情况,如果你尝试使用cin而不学习如何,使用流。基本上,在你的第一次作业中,你会通过所有正常的测试,然后当他们尝试使用错误的输入来测试你的程序时。

你会,失败所有的测试。所以在CS106B中不要使用cin。明白了吗?或者考试中。我们来看。我有很多窗口。

好的。这是第一次尝试。问题是,如果这个操作失败了会怎么样?我们来谈谈状态位。有四个位表示流的状态。明白了吗?它们用颜色编码标识,如这里所示。有一个好位、一个失败位、一个EOF位和一个错误位。明白了吗?

快速概述一下。好位意味着一切正常。失败位意味着之前的操作失败了。EOF位意味着之前的操作达到了你拥有的缓冲区的末尾。而错误位意味着发生了一些坏事。挺容易理解的。嗯,除了EOF和失败。明白了吗?

所以你会注意到,如果发生失败,或者其他任何位被激活,所有未来的操作都会被冻结。

明白了吗?这是什么意思?这意味着如果这个操作失败了,那么失败位就会被激活。你可以尝试做一些随机的操作,比如有效的、无效的,随便。

什么。所有这些操作都不会有任何效果。所以这些位非常重要。它们指示流现在发生了什么。明白了吗?那么一个位可能被激活的常见原因。第一个,没有什么特别的。如果其他位都没有激活,那么好位会被激活。失败位。

最常见的是类型不匹配。这是什么意思?就是我们刚刚做的,对吧?当你尝试读取一个整数,但它不能读取整数,因为它不是整,数。明白了吗?如果发生这种情况,失败位会被激活,未来的操作会失败。EOF。

当你到达缓冲区的末尾时,我们稍后会看到。然后错误位很少被激活。如果错误位被激活,那么发生了一些非常非常糟糕的事情,程序可能内部发生了错误。你可能无法从错误位问题中恢复过来。

我们可以从失败位和EOF中恢复过来。现在,关于状态位的重要事项,这一点并不直观。良好和失败不是对立的。明白了吗?这些也是需要思考的哲学性问题。良好和失败不是对立的。好吧?良好和失败不是对立的。

当你拿到冬季学期的考试成绩时,请记住良好和失败不是,对立的。明白了吗?所以,仅仅因为你想检查是否失败,并不意味着这实际上是,良好的。所以,即使你检查了考试成绩,你没有失败,这也不一定意,味着你做得很好。

明白了吗?良好和失败不是对立的。良好和坏不是对立的。是的,但我们最关心的是失败和EOF,这意味着你很少需要,检查良好,因为良好不是失败的对立面。良好不是EOF的对立面。

明白了吗?酷。让我们尝试查看状态位。为了做到这一点,我为你写了一个可爱的函数,叫做print ,state bits。明白了吗?你可以快速读取它。它的作用是打印状态位。明白了吗?需要注意的是。

我使用了一个叫做转向运算符的东西。你不需要真正理解它。Keith实际上会教这个。Keith是唯一会教这个的讲师。是的。基本上,转向运算符是一个非常简短的if-else语句。明白了吗?可以理解。

有些人讨厌它,因为它不像if-else语句。但Keith真的很喜欢它。所以,是的,你会学到更多关于它的内容。它的好处在于,如果你需要做这样的事情,你可以用一行代,码来替代。明白了吗?有趣。

我应该告诉你一个有趣的事实吗?不。我会留到下次再说。酷。酷。让我们看看。好的。所以我们要做的是在这个函数内部尝试一下。让我们尝试打印状态位。所以print state bits。

你必须传入你想查看状态位的流。所以这里我们想查看ISS。明白了吗?所以我将在这里打印状态位。我将在读取之后打印状态位。我想就是这些了。明白了吗?所以我们将尝试在构造之后立即打印状态位,并在你尝试。

从中读取之后立即打印状态位。明白了吗?好的。所以尝试输入这些代码并运行程序,看看你得到什么。

好的。让我们尝试一个常规操作。137,Keith最喜欢的数字。好的。这有意义吗?最初,良好位是打开的。读取之后,EOF位是打开的。这有意义吗?好的。直观上有意义,对吧?最初,一切都是良好的。

当你读取某些内容时,你到达了缓冲区的末尾,EOF位是打。

开的。是的。当然。所以基本上就是这四行代码。但你调用了下面定义的print state bits函数。你传入你想查看状态位的流。明白了吗?还有其他问题吗?问题?是的。是的。这通常会打印得很好。

唯一的例外是构造函数如果以某种方式失败了。我不知道构造函数怎么会在这里失败。如果你传递了什么奇怪的东西,也许会失败。明白了吗?好吧?一切都好吗?

好的。现在,我希望你继续测试这个,尝试所有其他的错误输入。看看错误位会发生什么变化。我给你大约30秒钟。只管测试你能想到的任何东西。看看你能得到什么样的错误位。我会给能够开启错误位的同学一个特别奖。

因为我还没有,找到如何做到这一点的方法。明白了吗?我还没有找到如何做到这一点的方法,而不是真的崩溃计,算机。所以看看你能否做到这一点。

哦,抱歉。这里,这里,这里。这是代码。这是代码。是的,问题?好问题。你想尝试一下吗?是的。这不起作用。不起作用?好的。

哦,好的。我不确定。差不多吗?我想它给你的是整型最大值。这里。这是整型最大值。是的,所以我想它将其解释为整型最大值,是的。所以它可能能够感知到你超过了整型最大值。相当酷,对吧?很聪明。好的。

你尝试了什么?有人能开启失败位吗?好的,你尝试了什么?故障的相反。是的,你可以看到失败位已经开启了。还有其他的吗?是的,嗯哼。对的。当它判断将会失败时,它会停止。它返回到开始的地方,并且说,哦。

我失败了。所以如果你输入一个字符,它会查看第一个字符,并且说,哦,这不合法。它会将位置指针移动回到开始的地方。当缓冲区为空时,它能够感知到,并且,它就会停下来,不做,任何操作。回到操作之前的地方。

操作开始之前的地方。是的,明白了吗?需要注意的一点是,即使你输入了空字符串,EOF位还没有,开启。EOF只有在你尝试读取超出末尾时才会开启。然后它会开启EOF。明白了吗?这是非常常见的错误。

如果你测试EOF,你需要小心,这也是我不推荐检查EOF的原,因。好的,还有其他的吗?有人能在最后开启良好位吗?是的,嗯哼。所以8a。注意到良好位在最后开启了。好的,现在问题是,你是否能够检查错误?

鉴于你知道这些位,你能检查是否发生了错误吗?所以在操作之后,你怎么知道字符串转整数是否成功?你得到什么?对不起?好的。确切地说,你会得到EOF位。明白了吗?你会得到EOF位,特别是,你不会得到失败位。

你不会得到失败位,在最后你会得到EOF位。明白了吗?是的,但不能仅仅是EOF,因为你还需要更关注这个边缘情,况。你不能有失败,并且你想要EOF。对了,有问题吗?

当然。当你处理文件时,这一点更为重要,当你尝试从文件中读取,时。有时你会写这个条件。当我们还没有到达文件末尾时,尝试读取一个字符。明白了吗?所以你会一直读取。你会一直读取。你会一直读取。

然后发生的事情是,如果你检查EOF,它实际上不会在执行,一次后完成,最终会失败。所以,取决于你如何构建循环,它有点像是一个多出的错误,如果你必须等待EOF,这种情况会超出最后一个字符。明白了吗?

取决于你如何构建循环,但通常你会遇到这种方式的错误,我现在没有示例,但我会找一个示例并发布在Piazza上。

明白了吗?好吧,我们可能会超出几分钟的时间,但我希望这没问题。

因为这很酷,对吧?这真的很酷。不是吗?

好吧。好吧,我会诚实地说。流不是我最喜欢的主题,但它们是我最炫的幻灯片,所以我,必须讲解它。

好吧,顺便说一下,一种方法是检查EOF标志是否开启。另一种我更喜欢的方法是尝试读取一个字符。确保失败标志没有开启。然后尝试读取一个字符。确保失败标志开启。这有意义吗?

让我打出来,然后我们来理解为什么这样做有意义。好吧,我要尝试这个操作。如果失败标志开启,那么这意味着发生了很严重的问题。抛出一个异常。我将抛出一个称为域错误的异常。不要太担心它是什么。

它只是抛出一个异常。错误。这个检查不够,因为我们在示例中看到你可能仍然有好的,标志,这样不好,对吧?所以我们还可以检查。好吧,你还要检查是否尝试读取另一个字符。保持ISS。ISS。ISS。fail。

现在我们确实想要失败。所以如果它没有失败,那么抛出STD域错误。明白了吗?再看一遍这段代码,然后向你旁边的人解释。超出了五分钟。没关系,对吧?这是缓冲时间的好处。是的。更好的错误信息?好吧。好吧。

这是一个好的观点。好吧。好吧。为了测试你的理解,这个错误信息应该怎么写?什么是更具信息性的错误信息?开头没有有效的整数。好吧。第二个错误信息说了什么?以及一个有效的。好吧。或者甚至是,因为从技术上讲。

你可以有一个有效的整数,然后一个无效的整数。所以我想你可以重新措辞为,比如说,有有效整数之后还有,其他内容。很酷。是的。棒极了。所以,是的。这就是错误位如何有帮助,以及你如何能够检查。检查错误。

有问题吗?是的。ISS。哦,EOF?是的。好的问题。所以,是的,你可以在这个例子中这样做。是的,这样会有效。所以你也可以这样做。我更喜欢另一个方法的原因是,那个方法在我们稍后涉及,输入输出流时更好。

输入输出流中,文件结束符(EOF)比较少被使用。是的。因为有些情况你输入了一堆内容,但它只读取了一小部分,是的。所以我个人不喜欢使用文件结束符(EOF)。但在这个例子中。

你可以肯定地使用文件结束符(EOF)。那样会有效。好的。我只是想让它在所有地方更一致。哦,这里你想检查文件结束符(EOF)是否开启。如果开启了,那么你需要抛出错误。不,其实,不,抱歉。如果没有开启。

那么你需要抛出错误。酷。我们时间不多了,但因为课实际上在2:50结束,所以我可以。

稍微多讲一点,对吧?好的。如果你需要离开,随时离开。

但,好的。

所以,作为一个例子。是的。这是我们做的例子。

此外,作为一个快捷方式,这个东西,有人提醒我,这个操作,返回什么?是流,对吧?好的。有趣的是,这个流可以被重新解释为布尔值,其中流本身被,转换为真,如果失败标志位(fail bit)关闭。好的。

这有点让人困惑。本质上,我的意思是,我们可以直接将这个表达式放到这里,而不是检查这个。所以,这个操作返回流,该流被转换为是否失败标志位,(fail bit)关闭。所以,我们可以把这个放进去,然后这样做。

好的。然后在这里,不是读取单个字符,像 char remain,我们可,以使用 ISS,读取单个字符,确保它是关闭的。如果它是开启的,所以如果这是成功的,那么它返回这个。另一种考虑方式是。

这个操作返回 true 如果失败标志位,(fail bit)没有开启,并且返回 false 如果失败标志位,(fail bit)开启。好的。这只是写法更紧凑的一种方式。

当你明天在106b课程中学习文件流时,你会再次看到这种,习惯用法。好的。你可能会在文件相关的上下文中看到它。是的,你可能会在这种上下文中看到它。我只是想指出,这等同于失败,是指能够不失败。酷。

我不能谈论这些。太可惜了。好的。好吧。所以,讲座实际上结束了。这实际上是可选材料,所以如果你想留下来听,这完全没问,题,好吗?如果学生有问题,我很介意你们回答问题。是的。我有一个问题要问大家。

还有其他人无法从网站上下载源代码吗?好的。好的。所以,只是前面的一些人。是的,好的。听起来不错。好的。这是可选材料,所以如果你想离开,随时离开。但实际上,我会一直到2:50。我会在学期中重复很多次这个。

所以,我会继续讲。你不必听这些内容。这是可选的。这可能对你的作业没有帮助,但它会给你一些在鸡尾酒会,谈论的好话题。好吧。正在缓冲中。

好的。所以,是的,问题。主函数。主函数?好的。嗯哼。是的。好点子。原因是我忘记在顶部声明函数原型了。所以,直接把这行复制到最上面。复制这行,你会得到更大的那一行。好问题。你们都有2点30的课吗?

因为我有点难过。好的。好的。所以,我们要尝试这个实验。我们将尝试运行。我们将打印到cout cs。然后我们将打印106。我们将打印l,然后打印endl。好的。我还不会执行刷新操作。

我还不会执行endl操作。好的。所以,基本上,它将尝试。这将是cout。所以,我只是尝试打印cout。它会做一些无意义的工作。然后它会尝试打印106,做更多的无意义工作。我们可以去掉这些无意义的工作。

它将打印l,做更多的无意义工作,然后在最后。让我们做换行。好的。所以,这些无意义的工作大约需要两到三秒钟。你期待打印出什么?好的。这有点难以解释。所以,你可能在想它会先打印cs,过几秒钟,打印106。

过几,秒钟,打印l,过几秒钟,空格,换行。好的。对。这很直观。这实际上发生了吗?哦,抱歉。调用错了东西。在这里调用了。你不需要输入这些,但我会把它们输入出来。好的。看看发生了什么。

好的。那么,发生了什么?是的。

它一次性打印了所有内容。正是如此。好的。这就是缓冲区的酷炫之处。当你把东西发送到cout时,它会把内容放入缓冲区,但不会,立即将缓冲区中的内容转移到控制台。有什么猜测为什么会这样吗?

为什么它不会每次都有机会就转移?嗯哼。是的,正是如此。读写操作很昂贵。它们有点慢。好的。所以,如果你每次都这样做,那会使程序变慢。事实上,cout是一个缓冲流。还有另一个流叫做cerror。

它用于打印错误信息。我拼写对了吗?E-E-R?哦,S-T-D。S-T-D。好的。

好的,还有另一个流叫做cerror。它用于打印错误信息。你可以想象错误信息不是那么重要,没有缓冲。每次你发送内容,它会直接移到控制台。好的?大家都看到了吗?C-S,立刻。然后106,立刻。

然后L,立刻。没有等到所有东西完成才打印。好吧,为了好玩,我这里有一个东西。它叫做。哦,不,我把东西移除了。呃,好吧,好吧。没事,没事,没事。Cout。Endle at。EndleEachTime。

好吧,我要调用函数 EndleEachTime,它返回所需的时间。如果我们尝试一下,让我们看看 EndleEachTime 做了什么。

。EndleEachTime 是。好吧,这是为了。不用担心这个 chrono 是什么。它基本上是一个计时器。要注意的是这个。那是干什么的?好吧,大家。我想大家都同时说了,但。对,你打印前100个整数。

包括零,每个整数在每行打印一次,关于 Endle 有趣的是,Endle 是一个换行符加上一个叫做, Flush 的东西。有人想猜测一下 Flush 的作用吗?是吗?那就是它的作用吗?

其实是一个模板控制台?完全正确。好吧,Flush 触发。告诉它将内容打印到控制台。那么这会做什么呢?它每次都按行打印到控制台。

与这个相比。相同的操作,但这里,你在每次换行符后没有 Flush,而是,在最后只 Flush 一次。好吧,你认为哪个更快?第二个,对吗?好的。为了公平起见,我的电脑有点怪,所以我不太确定。 哦。

不。哦,我知道了。我知道原因了,我知道原因了。我不能立即打印这个。我必须先调用函数。所以 int a 等于那个,int b 等于 Endle at a。所以我们先调用两个函数,然后打印它们所花的时间。

所以 a 和 b。

不要把你的变量命名为 a 和 b。而且。对,所以你可以看到第二种方法明显更快。

好吧,现在公平地说,快了多少?可能快了一点,但不会快得太多。出于习惯,你还会看到我输入 Endle,但如果你去一个被称,为 Stack Overflow 的毒性在线论坛,如果你写的代码中。

包含 Endle,别人只会关注那一行。所以只是提醒一下,如果你在高性能领域,不要过多使用 ,Endle。话虽如此,如果你不在高性能领域,尽管 Stack Overflow ,上的用法。

可能还是在每行末尾使用 Endle 更好。这种经验尤其在你在 106B 时,作业中将 Endle 放在每行,末尾几乎是最佳实践,因为如果不这样做,有时会导致很难,调试的错误。

所以如果你在处理 106B 作业时遇到奇怪的打印错误,请,检查是否在每行末尾添加了 Endle,因为这很可能是原因,有时你会说,哦,我正确打印了所有内容,但在控制台上没,有显示出来。好吧,答案是。

你是否尝试使用 Endle?你会看到控制台上的内容。现在,有些操作会自动触发刷新。其中之一是如果你调用 C in,它会自动刷新 C out。这很有道理,因为用户在 C out 打印完所有内容之前,实。

际上无法输入任何东西。

酷。就这样了吗?我还应该继续吗?我有点想多说一点。有问题吗?是的,好吧。让我关闭所有这些标签页。

我有点烦躁。好的。是的。哦,好问题。好问题。好问题。你看,我有这些很棒的动画。浪费了。浪费了。好的。操控符。这些只是特殊的关键词。如果你将它们插入到字符串中,它们会改变字符串的行为。

Endle 是这些字符之一。本质上,如果你输入 Endle,它基本上就像是,嘿,打印一个,新行并且刷新字符串。好的。这是一个常量吗?这些是常量吗?这是个好问题。或者说,它们像是枚举,对吗?我认为是枚举。

枚举?好的。是的。我们下次会讨论枚举是什么。但这些就像是常量。好的。所以有不同的操控符实现,它们需要枚举。确切地说,是的。我们稍后会讨论如何编写操控符。好问题。所以 Endle 是一个操控符。

Flush 是一个操控符。使用操控符,你可以做一些奇怪的事情。空间、填充,你可以让一切看起来很华丽。这些很容易 Google,所以我们不会多谈。有问题吗?实际上,结果是操控符是函数。哦,这些是函数?

是的。哦,酷。好的。所以我猜操控符的作用是,它检查,哦,这是一个函数。让我调用它。这可能就是它的作用。是的,问题?是的。

哦,好的问题。确实有一个最大大小,你实际上可以获取大小。实际上有一种方法可以获取大小。有一种类似的限制最大大小的方法,是的。如果你覆盖它,会发生什么?安娜,你知道答案吗?我猜是未定义行为。是的。

还有其他问题吗?我觉得我差不多了。好吧,有些人离开了,在我给你们布置作业之前,所以这有,点没意义,但我想,哪个比较好?这个有点酷,但它使用了向量。这个有点无聊。不,好的,不。这个还挺有趣的。好的。

所以给定一个输入流,看起来像这样,你要么进行错,误检查,要么打印结束时间。所以有一个开始时间,有一个持续时间,有一个结束时间。基本上,打印结束时间。解析它,打印结束时间。好的?所以你将需要使用流。

字符串流。是的,所有操控符都适用于所有流吗?比如说,你的查询语言是否适用于网络流?它是否在网络流中除了 cout 之外?好问题。并非所有操控符都有效。例如,对于输入流,对于输出流,不,对不起。

不是输出流。对于输入流,你不能使用 ndl。你不能使用 ndl,因为你不能真正读取到那个。你不能真正读取新行。但是,我的意思是,即使对于输出的混合呢?文件,我认为,会有效。对于 ndl,文件会有效。

因为它是相同的概念——我不是在谈,论 ndl。所以其实,这有点有趣。我们隐藏了一些类的细节。结果发现,所有这些不同的流都属于不同类型的类。

有一个 oStream 类,oStringStream 是它的一个特定子集,因此,根据操控符的定义位置,它可以应用于所有 ,oStreams,或者只应用于,例如,oStringStream。

这将在编译时检查吗?再说一遍?这将在编译时检查。这将在编译时检查。还有,对,检查一下。所以如果你去 C++ 文档,它会告诉你你可以使用哪些。所以是的,你可以全部 Google 这些。当然,不要记住这些。

真正地,你如何使用这些是,当你突然想到,“哦,我的输入,太丑了。”,我想让它变得漂亮。然后你 Google oStringStream 操控符,然后出现了这个,图表,你查看你想要的。明白了吗?

不要记住这些。没有人会记住这些。

所以就这样。明白了吗?我想我已经差不多准备好了。没有什么。我有一个问题要问你,我之前甚至见到你之前就有了。所以 Avery 简短提到过,不仅仅有 oStreamStream 和 。

iStreamStream。还有一个 StringStream,它结合了输入和输出。所以有人能告诉我为什么,如果有东西同时做这两件事,我,们为什么还会使用输入流、iStreamStream 或输出流?

为什么不在任何地方都使用 StringStream?你们觉得怎么样?是的,我们继续。好的。记忆丧失,因为你以为它会假定你要同时流它们,对吧?所以你可能不想做两者。是的,等等。所以如果我可以用另一种说法。

那就是,如果你知道你只想,做一个,那么你就不需要那个可以同时做这两者的。是的,完全正确。那么你们觉得怎么样?类似的东西吗?是的。一样的东西?完全一样。是的。所以我想把它用 C++ 的哲学来表述。

虽然我们可以在任何,地方使用 StringStream 并且它在功能上完全正常,实际,上没有任何区别。嗯,不,那有点儿不对。有一个轻微的性能差异和一个轻微的内存差异。所以这回到一个风格点。

就是明确声明意图。这是我们想指出的六个要点之一,就是当你在 C++ 中编写,代码时,你要明确声明你想用对象做什么。另一个部分是,使用更具体的一个会有一些轻微的性能好,处。所以这有点直观,但再次提醒你。

对于所有不同的选项,你,要问自己,我想要使用每一个选项做什么?我想要用什么?像是一个更通用的选项。好的。然后我们基本上完成了,但我没有覆盖的是基本上。是的,我下次会讲这个。基本上。

它向你展示了为什么CN是个噩梦。好吗?而像这样的无害代码,这就是我假设如果你不了解字符串,你会写的代码。这不起作用,特别是因为鳄鱼运算符只读取空格分隔的标,记。所以我们将在下周二讨论这个。好吧?下次。

我们要做的是讨论类型。我们将讨论现代C++类型,包括对、更复杂的类型。我们将学习auto。然后我们还将实现getInteger,这可能是斯坦福库中最难,实现的函数。好的?下次见。

是的,没问题。

posted @   绝不原创的飞龙  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示