博客主页 😑
文章

Count:

计 25 篇
132
翻译——博客里应该写点啥(What to blog about)
无标签
翻译——博客里应该写点啥(What to blog about)
分类: 默认分类
简介:你应该创建一个博客,在互联网上拥有一个属于自己的小天地可以很大程度上安抚自己的心灵。但是,我们应该在博客里写点啥呢? 这个问题很容易让人陷入深思。对我来说,只写一些新的、唯一的、以前从来没说过的东西绝对是一种自我施压。这种心理陷阱毫无帮助,甚至会更加拖慢你前进的脚步。 你可以记录 TIL (Today I learned),或是用写作的方法描述你现在正在进行的项目,这两种类型的内容,你一定能写出来,而且可以在写作中感受到自己创作出文章的乐趣。今天我学了什么 Today I Learned就我的体验来说,一篇 TIL ,也就是一篇记录今天我学了什么的文章,是写起来最轻松的文章了。 如果你刚学了怎样去做一件事,立马把它记录下来! 称之为 TIL,意味着这并不是你向他人保证的要写一篇高质量的或是很深奥的教程。这只是表明:“我仅在这里声明:这是我的笔记,但你也可能在这里找到对你有用的东西”。 我也很喜欢这种很浅显易懂的内容。我写 TIL 的一部分原因是为了强调,即使你已经有了25年从业经验,你依然应该庆祝一下你新学到了一些及为基础的事情。 我那天刚学了 pdb 中的 “interact” 命令,这是我做的 TILThe pdb interact command | Simon Willison’s TILs。 我从2020年4月开始发表 TIL,迄今为止已经上传了346篇了,并且这些文章中的大部分我都没有用超过10分钟的时间来写。这是一种既快速,又能令自己满意的一种在线写作方法。 我收藏的文章展示在https://til.simonwillison.net这个网站上,而这些内容都来源于我的 simonw/tilGithub仓库。写下你的项目 Write about your projects如果你正在做一个项目,那你更要把它记录下来。 我建议你在对任何你创建的事情“做完”的定义之后再加一条“把它记录下来”。 像 TIL 一样,它避免了“独一无二”所带来的压力。即使你的项目与成千上万的其它项目重叠也无所谓,构建它的经验对于你而言是独一无二的。你应该用很少的段落和一张截图来解释(或是暗自窃喜)你所做的事情。 这个截图是极为重要的!你的项目会在十年后继续存在并运行着吗?我希望会这样,但我们都知道这件事很快就会变得一团糟。 比截图更好的方式是一段GIF动画!我用的是LICEcap来制作GIF的。如果有一段视频,那当然是最好不过的了,不过这需要你投入更多精力去制作它。 但在你记录你的项目时,往往会很容易被诱惑去跳过这一步(译者:就是懒的搞),但只要你这样做了,你便会遗漏掉这个项目中大量极有价值的东西。 这几天我一直在做一件事:我告诉自己,写点东西是我在构建这个项目的过程中必须要付出的事情。并且在最后,我都会感到我的付出非常值得。 去看看我的这个项目标签Simon Willison on projects吧,它是这类文章的例子。综上,这就是我对于书写博客的建议:写下你所学到的东西,也记录一下你创建的项目进程。原文链接: What to blog about (simonwillison.net)
670
当时光倒流——高中实习十日小记
无标签
当时光倒流——高中实习十日小记
简介:前些日子,我应高中老师的邀请,回到我的高中实习了一段时间。原本我只是抱着回去看看老师,看看母校的心态,但当我走进高一班级的时候,我突然有些恍惚,仿佛是灵魂脱离肉体并审视着自己。但此时,是在审视坐在面前的多年前的自己。2015年,我第一次踏上这片土地,2019年,第一次与这片土地告别。我与这片土地告别的那一刻,这片土地又封存了一个藉藉无名的莽撞少年,而它一并封存的,还有那个少年一生中二十分之一的各种情愫。之后我回来过一次,走进了我的母校,路过了我曾经就读的班级,也与当年的班主任打了照面。但那次回校,更像是一种仪式,就像是离开老家多年,如果不回去探望,就要被村里人议论。顾不上去回想当年种种,便匆匆离开了。此后这片土地,也被我封存起来了,我再也不会回到这里了。时光飞逝,如今我已然步入社会。说来也是,人生总是多变的,谁都没有办法预料到未来会发生什么,就好像我也没有预料到有一天我又会故地重游。可当我真的脚踏实地地站在校门前,一种迷茫感油然而生。我不清楚我该是以什么身份重新走进这扇校门,是学生,学长,还是恩师的好友,又或只是一名寒假志愿者。但当我走进教室的那一刻,这种迷茫感瞬间消散。这种身份标签并没那么重要,重要的是当下的感受。蒋勋曾说,当你以一种早已熟悉的身份看待事物的时候,往往并不会感受到其最真实的感受,而当以一个陌生的视角,把眼前的事情当作从未经历过的事情看待时,才能体会到真正直击心灵的感受。正是在我还未找到定位的时候踏进教室,才能发现学生们的那种极为简单且清澈的眼神。我在学校停留了十余天,这段时间正是春节前后。这个学生群体也较为特殊,是来自新疆的优秀初中生,通过选拔输送到内地(新疆人除了新疆外的地方都叫内地)的教学水平极高的高中培养。但因为距离新疆太原,所以只能在学校度过寒假。我和老师们,以及其他的志愿者们一起,管理他们的晚自修纪律,给这些高中生做大学生涯讲座,带他们备年货,包饺子,在学校的春节晚会上为他们准备节目,给他们组织团课教育活动,带他们去游乐场玩……这十来天的工作量说实话不算小,但是置身其中,一边做着组织领导的工作,一边又和他们一样以一个学生的角色参与其中,这让我忘记了抬起手腕看看时间,以至于悄无生息地到了离开的日子。志愿者们的最后一晚,志愿者们回到各自的班级中,与同学们挥手告别。大家相处十来天,对于学生而言,这个时间点正是刚打破陌生感,新鲜感未过,对我们的印象仅仅是发现了我们身上的优点,还没有到一种熟悉到厌倦的地步;而对于志愿者们,这段时间,是他们第一次为自己热爱的事业,为一群陌生的人倾注自己全部的心血,正值热情高涨的时候。在这个时间点,彼此告别之际,难免会流下泪水,以此为这段时光画上一个看似圆满的句号。我是第二天才要离开的。我没有和同学们告别,因为我觉得我的到来本身就是一件很突兀的事情,而离别也没有必要画上一个这么彻底的句号,轻描淡写地离开他们的视线在我看来是一个更好的选择。这十天对于我而言,有着另外的意义。我不是教育专业出身,所以关注点没有放在如何教育学生,我天生就没有这份热情,所以在这上面瞎研究可能只会败坏祖国的花朵。这十天对我而言,更像是去体验生活开拓眼界。成功的是,我跳出了原本的井。这十天里,我看到了身居高位的校长在与我们对话时总会身体微倾,也听闻了他在晚上工作到一两点后蜷在办公室的沙发上小睡到六点然后继续操劳。看到了我的老师在大年三十的晚上仍然在办公室里伏案辛劳。或许这是教育工作者的使命,但我看到的却是从他们身上散发出的一种我所难以触及的对于自己事业的认真和热爱。伟大这个词本身太过空虚,但从他们的所作所为上却能将这个词慢慢具象。我也看到了刚步入岗位的志愿者们的满腔热血,他们能为了策划活动争吵讨论到凌晨,然后互相协作保证活动的圆满举办,也会深入到学生中间去耐心聆听他们最真实的想法。回想到我的大学生涯,我想到我刚加入运动队时,也是一样充满热情。但不同的是,他们是为学生服务,这是一种比我更充实,影响更深厚的热情。我看到在老师们努力下,更有朝气,更追求上进的学生们,他们如今敢想敢拼,精神饱满,朝气蓬勃。“长江后浪推前浪”这句话虽然已经被用过无数次,但仍然是用来描述他们的话中的不二之选。我可能不会走上教育岗位,但是这几天的经历让我切身体会到教育事业的艰辛,也看到了教育者们的伟大。“硕果累累”不仅仅只需要播撒种子,最关键的还是日复一日不辞劳苦地浇水施肥,修枝剪叶。人生难能遇到贵人,这几天里,我的老师也与我聊了很多,带我见识了很多在我认知之外的事情。有些东西需要用很长的时间来消化吸收,这份经历于我而言是一笔宝贵的财富。山高水长,很幸运能遇到这样一群老师,在我成长的路上不断为我创造丰富阅历的机会,也很荣幸能够遇到这群志愿者们,能让我感受到人与人之间的多样性,从而能够更好地反思和提升自己。很开心能和这些同学们相处一周多的时间,让我重新感受到青春的无限生机。祝你,祝我,祝大家,前途似锦。今日一别,他日我们定会顶峰相见。——CHI
502
如何在 Visual Studio 中调试代码
无标签
如何在 Visual Studio 中调试代码
分类: 后端技术
简介:调试(debug)是编写程序过程中的重要步骤。调试的两大部分,一个是断点,另一个是读取内存。在大多数情况下,我们会同时使用这两个部分。换言之,设置断点的目的就是为了读取内存,断点是调试和在内存中查找的重要部分。那么,调试的意义是什么?Debug,就是为了从代码中清除bug。什么是断点?断点是调试器中会暂停(break)的点。我们可以将断点设置在我们程序中的任何代码行上。当执行到这一行时,程序会暂停。这时我们可以查看这个程序的状态(state),也就是程序的内存。我们可以暂停下来看看在程序的内存中究竟发生了什么。一个程序运行时所占用的内存是很大的,我们设置的每一个变量、所调用的函数等等。当我们中断程序后,内存数据实际上还在。查看内存对于诊断程序中出现的问题非常有用,通过查看内存,我们可以查看程序运行中变量的值。除此之外,我们还可以单步运行代码。假设我们将断点设置在第五行,我们可以选择单步执行代码,使程序运行第六行代码。我们还可以步入(step into)函数内,查看程序运行到哪里。在VS中,对当前行设置断点的快捷键是F9,或者我们可以点击代码行号左侧,为此行设置断点。同时,我们要确保我们的解决方案配置处于 debug 模式,因为如果我们处于 release 模式的话,我们的代码实际上会被重新编排,有可能程序在运行的时候永远不会击中断点。同时在调试器一栏中我们选择本地 Windows 调试器,以确保我们在运行时附加了调试器。逐语句(步入)step into:快捷键 F11 ,进入当前断点所指的语句逐过程 step over:快捷键 F10 ,进入下一行代码跳出 step out:快捷键 shift + F11,跳出当前函数,回到调用这个函数的位置箭头所指向的代码是还没有运行而将要运行的代码。
360
CPP学习DAY5
无标签
CPP学习DAY5
分类: C++
简介:头文件(Head files)头文件是什么?我们为什么需要它们?为什么它们在 C++ 中存在?你或许已经习惯了很多其他语言,比如 java 或者 c#,它们实际上没有头文件这个概念,也没有存在两种不同的文件类型的概念:一种是我们的编译文件,比如说cpp文件,也就是一个翻译单元,还有一种就是头文件,一个奇怪的文件。我们经常到处 include 它们,为什么它会存在?头文件实际上很有用,而且它们的用途不只是声明某种声明,以供你在多个CPP中使用而已。随着学习的继续,我们将要学习很多新概念,都需要头文件才能正常工作。在c++基础中,头文件传统上是用来声明某些函数类型,以便可以用于整个程序中。回想一下之前谈到的的c++编译器和链接器,为了让我们知道什么函数和类型存在,我们需要某种声明。比如说,我们在一个文件中创建函数,然后想要在另一个文件中使用,当我们尝试编译时那个文件时,C++甚至都不会知道它的存在,所以我们需要一个共同的地方来存放声明,而非定义。因为,记住,我们只能定义函数一次。我们需要这么一个共同的地方存放函数声明,只是声明,而没有实际的定义,没有函数的主体,只是一个地方说“嘿,这个函数存在!"假设在文件 Main.cpp 中我有个函数叫log,它将打印信息到控制台,它接受一个 const char "message",然后单纯cout该信息。#include <iostream> void Log(const char* message) { std::cout << message << std::endl; } int main() { std::cout << "Hello world!" << std::endl; std::cin.get(); }如果我继续创建另一个文件,我们将之命名为 log.cpp,然后也许这个文件有个什么要用到我们到 Log 的,并且会打印一句话到控制台void InitLog() { Log("hello hello hello"); }我们将会得到一个错误:这个 Log 函数并不存在于该文件,该文件不知道 Log 函数的存在,如果回到 main 文件中,Log 函数就在这里。如果如果我按CTRL加f7试图编译我们的 Main.cpp,可以看到编译成功,没有错误然后回到 Log.cpp,如果我们试图编译这个文件,我们获得一个错误,因为显然对于这个文件来说,Log 函数不存在所以 Log.cpp 到底需要什么东西才能不会有错误?我们该如何告诉它,Log 这个函数存在,只是它定义于别的什么地方?这就是函数声明用的用武之地!如果我们回到我们的 Log.cpp 代码这里,只需要简单的声明,Log 函数存在。如果我们回到main函数,然后看一下这个实际的签名,可以看到 Log 是一个返回 void、接受一个参数——也就是一个 const char 指针,所以这是函数签名,我们可以直接复制这个,回到 Log.cpp,粘贴进来,然后用一个分号结束它,就像下面这样:void Log(const char* message); void InitLog() { Log("hello hello hello"); }这个函数没有主体,正是说明这是一个函数的声明,我们还没有定义这个函数到底是什么样子,这个函数到底做什么,我们只是说,嘿,这边有一个函数叫 Log,返回类型是 void,接受一个 const char*,这个函数存在这时就可以看到我们的 Intellisence 的错误已经消失了,如果我按 control 加 f7 ,我们可以成功编译,如果我们右击 Hello world,然后点 build 来 build 我们的程序,可以看到他也链接成功,因为这是可以的,他找到了那个log函数。这样我们就找到了一个告诉 Log.cpp 那个 log 函数存在于某处的办法。那如果我创建另外一个文件呢?如果我创建了一个别的文件,然后需要用这个 log 函数呢?这意味着我也需要把这个 void log 声明到处复制粘贴吗?嗯,答案是是的,你确实需要这么做,但是呢,有一个办法可以让这一切简单一点,那就是使用头文件。头文件到底是什么?因为这是 C++,你可以做任何事情,所以头文件一般是那种被 include 到 cpp 文件里的,基本上我们做的就是,把头文件里的内容 copy and paste 到 cpp 文件里,我们通过 #include 这个 pre processor 语句来实现,所以 #Include 有从文件复制粘贴到其他文件的能力,这正是我们需要在这里做的。我们需要把这个 Log 的声明,复制粘贴到任何我们需要用这个 Log函数的地方,所以让我们去创建一个头文件。我将右击 header files ,请注意这些文件夹其实是过滤器,它们不是真的文件夹,我也可以在 source files 底下创建一个头文件,将来可能也会有一些教程,关于 visual studio 是怎么工作的,但现在我们需要知道的就是,无论在什么地方右击创建头文件都没关系,我将在头文件上面创建,因为这更合理一点,但实际上其实无所谓,我将创建一个头文件叫 Log.h:#pragma once void Log(const char* message);你会意识到它自动给你插入了一些代码,叫 #pragma once,我们等会很快会介绍他,在这里我将存放我们的声明,我将从 Log.cpp 把这个Log函数剪切,放入我们的头文件,现在呢,我们的头文件 Log.h,我可以在任何想用 Log 函数的地方 include 它,它会帮我们做那些我们不想人工做的事情,也就是复制粘贴这个到任何我们需要它的所有文件里。我不想自己去做这个复制粘贴的工作,所以就找了一个某种程度上让它看起来整洁一点,自动化一点的办法。回到 Log.cpp,你会看到我们也得到了一个错误,因为我们其实没有声明这个函数。但如果我输入 #include “log.h":#include "log.h" void InitLog() { Log("hello hello hello"); }看哪,我们没有任何错误,我们的文件可以编译。我们其实还可以做的是把它 include 到 Main.cpp,Main.cpp 包含实际的函数定义,所以其实他并不需要他,我们反正都可以调用Log,只是为了让你知道,我们把它include进来也不会造成什么问题,然后编译,这是完全没有问题的。回到 Log.cpp,你可以看到我们定义了这个超屌的函数 InitLog,然后除了 Log.cpp,没人知道它,如果想从Main里面使用它,我需要拿到它的声明,如果我在这里调用 InitLog:#include <iostream> void InitLog(const char* message) { std::cout << message << std::endl; } int main() { std::cout << "Hello world!" << std::endl; std::cin.get(); }我们会获得一个错误,这是因为他没有在任何地方被声明,所以让我们去 Log.h 把它的函数签名加进来,就像这样:#pragma once void Log(const char* message); void InitLog();要确保它和cpp文件里的实际签名一致,所以现在一切都看起来很好,然后我要去把这个 Log 函数拿到这个 Log.cpp 来,因为这样看起来更合理一点:#include <iostream> #include "log.h" void InitLog(const char* message) { std::cout << message << std::endl; } int main() { InitLog(); std::cout << "Hello world!" << std::endl; std::cin.get(); }回到Main.cpp,如果运行我们的程序,看我们实际上初始化了 Log 并打印了 Hello world! 到控制台。我们回到那个头文件,看一下那个pragma once到底是什么。这里我们有一个语句是看起来是visual studio给我们生成的,叫#pragma once,这是什么? 任何以一个井号开头的语句都被称为预处理命令,或者叫预处理指令。也就意味着他将在这个文件被编译之前被c++的pre processor评估,pragma其实是一个被输入到编译器或者说预处理器的指令,pragma once其实意思就是说只include这个文件一次,Pragma once, 是一种被称为header guard(头文件保护符),他所做的就是防止我们把单个头文件多次include到一个单一翻译单元里,现在我说话其实很小心的,你得明白,他其实不会防止我们把头文件include到整个程序的各处,只是防止include到一个翻译单元里,也就是一个单独的cpp文件,原因是如果我们不小心把一个文件多次include到一个翻译单元里,我们会得到一个重复的错误,因为我们会多次复制和粘贴那个头文件。一个比较好的办法来示意这种情况是我们创建一个结构体(struct)。比如说如果我在这里创建一个结构体叫做player:// #pragma once void Log(const char* message); void InitLog(); struct player ;我可以就让他空着,没关系,如果我将这个文件两次 include 到一个翻译单元,并且没有头文件保护符,他会真的include这个文件两次,也就是我会有两个结构体,他们有相同的名字—player,我们可以通过把这个pragma once注释掉来看一下这个例子。回到 Main.cpp,include Log.h两次:#include <iostream> #include "log.h" #include "log.h" //注意这里 int main() { InitLog(); std::cout << "Hello world!" << std::endl; std::cin.get(); }这时如果我试图编译我的文件,你可以看到我们拿到了一个 player structure 重复定义的错误,因为我们在重新定义这个 player struct,而我们只能定义一个叫 player 的 struct,struct 需要唯一的名字。所以你会问我为什么要include一个文件两次?这又回到了include到底是怎么工作的?记得吗,include的工作方法就是从一个文件复制粘贴到另一个文件,也就是说你可能会有一个一连串的 include,所以你可能有一个头文件叫player,他会include log,然后player又被include到其他的文件,然后可能那第3个文件会包含所有的include,所以如果我创建一个新的头文件,叫common,Common将会包含一些常用的 includes,比如它会include log。如果我回到log.h,确保program once被注释掉了,然后在log.cpp,我include log.h和common.h,如果我编译我的文件,猜猜会怎样?我仍然拿到那个错误,因为那个struct player被重新定义了,如果我们要解析预处理器到底做了什么,你可以看到他其实有把 Log include两次,然而回到Log.h,如果我们把注释去掉,并试图编译我们的文件,将不会有任何报错,因为它认识到log已经被include了,他不会第2次再include它。还有另外一种方式来添加一个头文件保护符,用传统的办法添加头文件保护符,其实是通过 #ifdef。我们可以做的就是输入#ifndef,然后我们可以来检查某种符号,比如说_log_h,然后我们会定_log_h,然后在我们代码的最后,输入 endif,这个将要做的就是首先他会检查这个log h这个符号是否存在,如果没有被定义,他将把以下代码include到编译里,如果这个被定义了,那这些都不会被include,会全部被禁用,当我们通过了,第1个条件检查,我们会定义log h,也就是说下一次我们运行这段代码时,它是已经被定义的状态,所以不会再重复。最简单的办法来描述这个东西就是复制粘贴整个文件,到我们的log.cpp文件,如果我们看一下它,我也把log.h注释掉,以及common.h,所以你们看在这里,第1次时,这一切都OK,他include了这个文件,一切正常,但第2次,他显示为灰色,因为log_h已经被定义了,所以这种形式的头文件保护符在过去经常使用,然而我们现在有这个新的预处理语句叫pragma once,所以我们现在大多数用它。从某种意义上来说,其实你用哪种都无所谓,但pragma once看起来更干净,所以在行业里这也是绝大部分人所用的,基本上现在所有的编辑器都支持pragma once,所以并不是只是适用于vs,gcc、clang、msbc,它们都支持pragma once。话说回来,如果你有看到老代码,或者别人写过到不同风格的代码,你会碰到这个ifndef头文件保护符,所以需要知道一下它是啥。你可能会发现,include语句间有一些差异,有些include语句用引号,有些include语句用方括号,所以到底啥情况?其实很简单,他们代表两件事,当我们编译我们的程序时,我们有办法告诉编译器某些include路径,这基本上就是在我们电脑里通往文件夹的路径,它们包含include文件。如果我们想要include的那个文件,在这些文件夹其中之一的话,我们可以用尖括号去让编译器在所有include路径里,去搜索这个文件,而引号用于include文件存在于该文件的相对位置,举例来说,如果我有一个文件叫做log.h,它存在于目前这个log.cpp文件的上一级目录,我可以用../来返回,而这会返回上一级目录,因为这是相对于本文件的位置,而对于方括号来说,永远没有说这个文件的相对位置,他们必须存在于所有include目录的某一个里,我们会在以后的学习中涉及到怎么设置include目录和这所有东西,但现在不需要涉及这么复杂,但这基本上就是他的工作原理。引号也可以用于include目录的位置,所以你其实可以在任何地方用引号,我可以把 iostream 这个尖括号换成引号,将完全没问题,所以尖括号只用于编译器的include路径,引号用于所有的路径。最后一件事,你可能发现这个iostream看起来不像个文件,因为它没有扩展名,就叫iostream,这是怎么一回事呢?其实,它是个文件,只是没有扩展名,写标准库的人决定这么干的。为了区分c的标准库和c++标准库,c标准库里的头文件一般都有.h扩展名,而c++没有,所以这也是一个区分哪些属于c标准库,哪些属于c++标准库的办法,就是是否有.h扩展名。iostream跟其他东西一样,就是个文件,其实,在vs里,右击它,点击打开文件,你可以看到它带领我们来到这个iostream头文件如果我们在这个标签上右击,选择打开包含文件夹,可以看到它其实位于我们的计算机中某处。这就是关于头文件的一些事情。
533
CPP学习DAY4
无标签
CPP学习DAY4
分类: C++
简介:函数(Function)函数就是我们编写的代码块,被设计用来执行某个特定的任务,当我们之后说到类(class)的时候,那些代码块被称为“方法(method)”,但在这里,是在明确地说某种不属于某个 class 的东西。对我们而言,分割代码以防止重复代码是很常见的。我们不希望多次写同一代码,因为这样做除了复制和粘贴大量的代码,并最终获得一团糟以外,也意味着,如果我们决定改变一些代码,我们就必须在所有粘贴原始代码的地方改变它,这对维护来说将会是一场灾难。所以我们可以做的就是写一个函数来做我们想做的,然后在我们需要的时候我们可以在代码中多次调用,。可以这么理解函数,有一个输入和一个输出(尽管这并非绝对必要的),我们可以提供特定的参数,然后这个函数可以为我们返回一个值。假设我们想把两个数相乘,我们想要写出一个函数来实现它,我要做的第一件事就是在这里写一个叫返回值(return value)的东西,这是这个函数会返回的类型。因为我们把两个整数相乘、当然将产生一个整数,所以我们返回值是int,我将给这个函数一个名字,在本例中,叫 Multiply,它会接受两个参数,它们是我们想要相乘的数字,我把它们叫做A和B。然后我将给函数一个函数体(body)它所要做的就是返回 a × b 。int Multiply(int a, int b) { return a * b; }所以你可以看到我们这里有一个函数,它接受两个参数,都是整数,然后仅仅返回这两个数字的乘积。其实我们并不一定要提供参数例如我可以不提供任何参数,然后返回 5 × 8。int Multiply() { return 5 * 8; }这仍然是一个返回一个整数的函数,但它只是不取任何参数。我们还可以告诉函数我们不想让它返回任何东西。我们通过写一个void来作为它的返回类型。void当然代表没东西,那么我们可以直接把结果打印到控制台。int Multiply() { std::cout << 5 * 8 << std::endl; }让我们回到原来的例子:我们有 int a和 int b,我们返回了这两个整数的乘积。int Multiply(int a, int b) { return a * b; }那么我们如何调用这个函数呢?调用一个函数很简单,让我们试着打印乘法的结果。首先,我要声明一个变量来存储这个结果,所以我输入int result = Multiply()。然后我们就用3和2,这样做的结果就是用这两个参数来调用Multiply函数,然后把返回值,也就是 a × b 的结果,存到这个result整型变量。然后我们可以通过控制台输出这个result让我们按F5来运行我们的程序,当它build好后,我们将会得到6int main() { int result = Multiply(3, 2); std::cout << result << std::endl; std::cin.get(); }这显然是 3×2 的结果。说的更详细些,假设我想要做一系列乘法,然后把它们都打印到控制台。如果我在没有函数的情况下做类似的事情那么它看起来会很乱。我需要重复这段代码,去复制粘贴几次,我把它们叫做 result2 和 result3。int main() { int result = Multiply(3, 2); std::cout << result << std::endl; int result2 = Multiply(8, 5); std::cout << result << std::endl; int result3 = Multiply(90, 45); std::cout << result << std::endl; std::cin.get(); }我们做 85,和 90 × 45 ,如果我运行上面这个程序,将得到一样的值。因为当我复制并粘贴这段代码时,我忘记更改输出语句中的变量了。这种情况其实经常发生,人们复制和粘贴代码块,然后忘记改变一个小细节,在某些情况下,你可能只是运行你的程序,甚至没有注意到不对,直到在之后某个地方造成错误。然而,如果你为它创建一个函数,这种问题就非常容易解决。让我们通过打印result2和result3来解决它:如果我运行下面的代码:int main() { int result = Multiply(3, 2); std::cout << result << std::endl; int result2 = Multiply(8, 5); std::cout << result2 << std::endl; int result3 = Multiply(90, 45); std::cout << result3 << std::endl; std::cin.get(); }我们会得到正确的结果。然而你可以看到,我实际上是多次复制了几乎相同的代码。如果我决定直接用数字相乘的写法来代替这个乘法函数,我必须在每个地方替换它: int result = 3 * 2; std::cout << result << std::endl; int result2 = 8 * 5; std::cout << result2 << std::endl; int result3 = 90 * 45; std::cout << result3 << std::endl;如果不想这么麻烦,我们就可以为它写一个函数。返回类型是void,因为它并不会返回什么给我们,它只会执行我们要求它做的事情。我们把它命名为 ”MultiplyAndLog“,然后思考我们想要的参数。所以这三段代码之间到底什么变化了?实际发生变换的只是两个相乘的数字,所以它们成为了我们函数的参数。到底这三段代码什么发生了变化,我们需要指定什么才能让这个函数执行我们想要的操作?让我们写下我们的参数,所以我们会用 int a 和b (你可以管它们叫任何东西)我们将把其中一段复制粘贴到这个函数中:void MultiplyAndLog(int a, int b) { int result = a * b; std::cout << result << std::endl; }当然,我将用我们的参数来替换3和2,这样我们就可以使用我们指定的参数来执行该函数里的乘法运算了,这会导致a*b发生,然后打印结果到控制台。所以现在,我不需要写很多次这些,我只需要简单地调用MultiplyAndLog,并传入参数所以比如说3和2,然后是8和5,然后是90和45,仅此而已。int main() { MultiplyAndLog(3,2); MultiplyAndLog(8,5); MultiplyAndLog(90,45); std::cin.get(); }这就是我们最后得到的样子,干净易读的程序。这是一个很简单的例子,但它很有效地证明了函数是非常重要的,我们的目标应该是把你的代码拆分成许多许多函数。但不要太过分,你不需要给每一行代码都准备一个函数,那样很难维护、你的代码看起来会很混乱很拥挤。实际上那样会让程序运行更慢。我们每次调用函数时,编译器会生成一个调用指令。这意味着,在一个运行的程序中,为了让我们调用一个函数我们需要为这个函数创建一整个 stack frame(栈框架),也就是说,我们得把参数之类的东西 push(推)到栈上。我们还需要一个叫做返回地址的东西,将其压到栈上,然后我们需要跳到我们程序的某个不同部分,以执行我们的函数里的指令。为了将我们 push进去的结果返回, 我们需要返回到最初调用函数之前的地方。所以这整个过程就是在内存中跳转来跳转去而为了执行函数指令。所有这些都需要时间,所以它减慢了我们的程序。上面所说的减慢原因的原因是,这都是建立在假设编译器决定保留我们的函数作为一个函数而不是将其内联(inline)。我们将在以后深入讨论 inlining之所以说这么多,是因为不想让你直接为每一行代码创建一个函数,这需要一点经验来意识到你什么时候需要一个函数。但基本上,如果我们正在多次做一个重复的任务,那么就可以为它创建一个函数。函数的主要目的是防止代码重复,我们不希望只是到处复制和粘贴代码。现在我们再回到我们的代码。你可能会注意到这个主函数有点奇怪,它说他的返回值是 int,然而却找不到return这个关键字。很明显我没有返回任何东西。所以如果我指定一个返回类型,我真的需要返回一些东西吗。试试看,在这个乘法函数中什么也返回,再按ctrl F7来编译该文件,我们将会得到一个错误,告诉我Multiply必须返回一个值。因此带有返回类型的函数实际上需要返回值吗?答案是肯定的!他们需要!主函数实际上是一个特殊的函数,主函数且只有主函数可以不用返回一个值,如果你没有指定返回值,它会自动假设你返回0。这只是现代c、c++版本的特点,为了让你的代码更简洁。有趣的是,所有这些必须返回一个值的规定实际上只适用于调试模式。如果我们在release模式编译,你会发现我们实际上并没有错误,这并不是说我们在这里做的是正确的,因为如果拿那个返回值做其他事,会得到未定义行为,只是编译器并没有对我们提示而已。然而,在调试模式下,当特定的调试编译标记(flags)被激活时,我们将会得错误,它将帮助我们调试代码,因为在任何时候你都不应该写一个函数,说了它会返回什么,但是并没有返回。这就是对函数的基本介绍。函数非常有用,每个程序都是由一系列函数组成的我们还通常将函数拆分为声明和定义。声明通常会存在头文件,而定义则会写在翻译单元里或者说cpp文件里,下一节会单独来讲头文件中的函数声明。
博客主页 CHI's blog 今春不见桃花
闽ICP备2022003806号 闽公网安备35012102500456号 本站由又拍云提供CDN加速/云存储服务 本站已运行 1 年 298 天 23 小时 58 分 自豪地使用 Typecho 建站,并搭配 MyDiary 主题 Copyright © 2022 ~ 2024. CHI's blog All rights reserved.
打赏图
打赏博主
欢迎
欢迎
欢迎访问CHI's blog
欢迎您来评论,但首次评论需经过审核才能显示,之后就不用啦^_^
搜 索
足 迹
分 类
  • 默认分类
  • 相册
  • 随想录
  • 技术向
  • 读书笔记
  • 生活小记