博客主页 😁
分类

后端技术

下的文章

Count:

计 7 篇
206
如何在 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,跳出当前函数,回到调用这个函数的位置箭头所指向的代码是还没有运行而将要运行的代码。
188
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头文件如果我们在这个标签上右击,选择打开包含文件夹,可以看到它其实位于我们的计算机中某处。这就是关于头文件的一些事情。
165
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文件里,下一节会单独来讲头文件中的函数声明。
284
CPP学习DAY3
无标签
CPP学习DAY3
分类: C++
简介:变量当我们创建一个变量时,它会被存储在内存中的两个地方之一:栈或堆。C++ 中的原始数据C++ 中的原始数据基本上构成了我们在程序中存储的任何类型的数据的基础。在 C++ 中,不同变量之间的唯一区别就是大小—这个变量占用内存大小的多少,这实际上是这些原始数据类型之间的唯一区别。声明一个新变量的语句如下:变量类型 变量名 [= 变量值]变量类型 变量名 [= 变量值]如int variable;或int variable = 8;int代表整数,它允许我们在给定的范围内存储一个整形数字。按照传统,整形数据的大小为4字节(byte),实际上,数据类型的实际大小取决于编译器。这是一个带符号的整数,所以其可以存储的数字范围是±21亿。可是为什么是21亿呢,实际上21亿并不准确,大概是21亿上下。我们来看看这20亿是怎么来的。一个字节是8比特(bit),也就是说,4个字节就是32bits。因为这个数据类型是带符号的,也就是说它有正负,包含了一个符号,也就是负号,而负号会占用1bit,所以我们留给实际数字只有31位。每个bit可存储两个值,也就是0和1,所以一共有2^31,也就是约21亿(实际精确结果为 2,147,483,648,但是要留一位给0,所以应是2,147,483,647,粗略记为21亿就可以了)。那么有没有办法只存储整数而不存储复数,也就是让32bits都用于存储数字呢?答案是当然有!我们可以这样定义一个整型变量:unsigned int variable;这里的 unsigned 意思是无符号数,也就是我们将用来存储符号的那 1 bit 也用于存储数字了。那么 2^32 是多少呢?当然是两倍,也就是约 42 亿了。那么我们还有哪些数据类型呢?下面列出其它的数据类型:数据类型内存大小char1 byteshort2 byteint4 bytelong4 bytelong long8 byte…………当然还有很多的数据类型及其变化形式,但最基本的是这5个。char 通常是用来存储字符的,我们可以给它赋值 65 ,也可以给它赋值 'A'(注意这里用的是单引号)。那我们可以给一个 int 类型的变量赋值字符吗?当然也可以。与字符 A 所绑定的数字就是 65。既然数字是字符,字符是数字,那么他们之间又有什么联系呢?我们先以 char 为例,看看下面两行代码输出的分别是什么:char a = 65; char b = 'A'; std::cout << a <<std:: endl; std::cout << b <<std:: endl;事实上,第一行输出的是 A,第二行输出的也是 A。因为我们在使用 char 作为 a 和 b 的数据类型,就是在告诉 cout 我们传入的是一个字符而不是数字。如果我们将 char 换成 short 又会出现什么情况呢?short a = 65; short b = 'A'; std::cout << a <<std:: endl; std::cout << b <<std:: endl;运行一下可以看到,输出了两个 65。这是因为我们告诉了 cout 我们传入的是一个字数字而不是字符。从上面的例子中可以看出,c++ 的数据类型并不局限于官方给出的建议用法,实际的使用方法还是取决于程序员。因此在这里想强调的是:数据类型之间唯一的真正区别是,当我们使用改数据类型创建一个变量的时候,将会分配多少内存。如果我们想定义小数呢?那么我们可以采用 float 和 double 这两种数据类型:float a = 5.5; double b = 5.5;但是你以为这样就结束了吗?还没有!尽管这里我们使用了 float 数据类型,但当我们在VS中将鼠标悬停在 a 变量上,我们可以发现它仍然被定义为了double类型。那么我们如何定义一个真正的 float 类型的变量呢?我们只需在所赋值后加一个 f(大小写均可)就可以了。float a = 5.5f;float 和 double 前也可以加 long 之类而组成变化形式,这里不多加讨论,只给出这两个数据类型所占的内存大小:数据类型内存大小float4 bytedouble8 bytebool 型是一个较为常见的类型,它的值只有两个:true或者flase。但是如果我们尝试这样定义: bool a = true; 那么我们将会得到一个 1 的结果。因为计算机中没有对或错的说法,只有 0 和 1。所以基本上 0 就代表 false(bool a = false; 的输出是0),而 0 以外的任何东西都被认为是 true。bool 型只占用 1 byte 大小。什么?为什么是 1 byte 而不是 1 bit?事实上,它确实只占用 1 bit,但当我们需要从内存中获取或存储我们的 bool 时,我们没有办法寻址到每一个 bit,我们只能寻址到 bytes。正因如此,我们无法创建只有 1 bit 的内存空间。但是,我们可以将 8 个 bool 类型的值存放在1 byte 里面。开头讲过,实际上数据类型所占空间大小取决于编译器,那么我们要怎样来检查编译器对某个数据类型的定义呢?我们可以用 sizeof() 进行检查://下面代码输出的是bool类型所占用的内存大小 std::cout << sizeof(bool) << std::endl; //也可以不要括号,写成: std::cout << sizeof bool << std::endl;现在,有了这些原始类型,我们可以将其转换为指针(pointer)或者引用(reference)。指针可以通过在数据类型旁边写一个星号来声明,而引用则是在数据类型旁边加上一个&符号。int* a = 1234; //这是指针 bool& b = true; //这是引用指针和引用是一个非常大也及其复杂的问题,所以会在后面的章节单独进行学习。
195
CPP学习DAY2
无标签
CPP学习DAY2
分类: C++
简介:链接器的工作方式当我们将源代码编译成 obj 文件后,需要通过链接器将这些文件连接起来,使之生成我们所需要的可执行文件。那么在这个过程中,链接器究竟做了哪些工作呢?链接的主要工作是找到每个符号和函数的位置,并将它们链接在一起。之前我们提到过,每一个 cpp 源代码文件实际上就是一个 translation unit,他们之间无法相互沟通,彼此之间还没有建立联系。当我们将程序写在多个 cpp 文件中时,就必须使用链接器了。链接器除了找到每个符号和函数的位置外,还有一个很重要的功能,就是找到程序的入口点(Enter point),也就时我们所说的main函数。当我们 build 一个项目时,实际上是执行了两个步骤:编译和链接。在VS中,假设我们有一份 cpp 代码如下:#include <iostream> void Log (const char* message){ std::cout << message <<std::endl; }当我们执行编译命令时(快捷键:CTRL + F7),我们会发现编译成功完成,没有报错;但当我们执行 build 或者运行(快捷键:F5)时,会产生一个 linking error,其大意时"没有找到程序入口点"。这是因为上面的代码中并没有包含 main 函数,在链接时链接器找不到程序的入口点,就无法知道程序应当从哪一步开始。但是入口点并不一定必须是 main 函数,实际上我们可以自行定义程序的入口点,这可以在链接器中修改,但绝大多数情况我们无需改动,使用 main 作为程序的入口点即可。下面我们用一个例子来小结一下:首先我要声明一个用于打印的函数,将其写在 Log.cpp 文件中:#include <iostream> void Log(const char* message) { std::cout << message << std::endl; }这里我们可以尝试去编译,会发现我们编译成功了!(即使里面没有main函数);然后我们想写一个简单的两数相乘的程序,命名为 Math.cpp :#include <iostream> void Log(const char* message); int Multiply(int a, int b) { Log("Multiply"); return a * b; } int main() { std::cout << Multiply(5, 8) << std::endl; std::cin.get(); }此处的 void Log(const char* message); 是告诉编译器我们有一个名为 Log 的函数存在(尽管我们并没有给出其详细定义),而编译器也会相信缺失有一个Log 函数存在,这样在下面的 Multiply 函数中才能使用 Log 函数。将两份文件编译完成后,我们会得到两个 obj 文件,这两个文件在生成时会被 linker 所连接。如果我们将上述 Log.cpp 文件中的 Log 改成 Logr 或别的什么名字,我们再编译时会发现这两个文件依旧会编译成功。这说明这两个文件之间没有联系的。但如果我们按下生成又会发生什么呢?我们会得到一个 Linking error,会告诉你无法解析的外部符号。这就体现出 Linker 的功能了。linker 会在每个 obj 文件之间建立联系,从而生成最终的程序。重新回到上面的例子。如果我们将写在 Multiply 函数中的 Log("Multiply"); 注释掉,那么链接器将不会报错,因为在编译过程中,编译器识别到我们并没有使用 Log 函数,那么在链接时将不会去寻找和 Log 有关的信息。然而如果我们不是注释掉Log("Multiply"); 而是将 main 函数中的输出语句注释掉,那么链接器依旧会报错。可是我们既然将输出语句注释了,说明我们并没有使用 Multiply 函数,自然也就没有用到 Log 函数,那么怎么会报与 Log 有关的链接错误呢?这是因为我们在此文件中声明的 Multiply 函数中使用到了 Log 函数,而编译器无法确定这个声明的函数是否会在其他地方被使用,所以一同进行了编译,那么链接器自然就会去找包含在 Multiply 函数中的信息。而在之前我们注释掉 Log 语句时,编译器发现并没有使用到 Log 函数,自然就不会将它放入这个 translation unit 的编译结果中。这里需要注意的两个关键词是“使用”与“定义”。那么如何解决后者的问题呢?我们可以在 Multiply 函数的定义前加上 static ,即static int Multiply(int a, int b) { Log("Multiply"); return a * b; }这样我们这个函数的使用范围(作用域)就是当前文件了,也就是说其他的文件无法与之建立连接。这样我们在编译此文件时,如果发现此函数没有在当前 Translation unit 中被调用,即便是声明了此函数,也不会对其进行编译。这样我们的链接也就能顺利通过了。在这里我们再强调一点:函数的返回类型,函数名、参数类型及参数个数都是十分重要的。如果我们将上例中的 Log.cpp 文件中的 void Log 改成 int Log 并添加代码 return 0;,一样会出现链接错误。因为在 Math.cpp 文件中,链接器试图去寻找一个返回类型为 void 的名为 Log 的函数,当然是找不到的,所以会出现错误。参数同理。还有一种链接错误就是当我们有相同符号,也就是有两个名称相同的函数具有相同的返回值和相同的参数,此时我们的linker不知道要链接到哪个函数,此时就会出现链接错误。而当这两个相同函数出现在同一个代码文件中,即使没有发生链接,编译器也能够通过报错来告诉我们此处出现了两个相同函数。也许你会说这么傻的错误怎么会发生,但是请考虑以下情况:我们先写一个头文件 Log.h#pragma one void Log(const char* message) { std::cout << message << std::endl; }再写文件1:#include <iostream> #include "Log.h" int main() { Log("hello"); }再写文件2:#include <iostream> #include "Log.h" void IntLog () { Log("world!"); }我们可以看到,这三个文件中 Log 只被定义了一次,但是如果我们将它进行生成,那么我们仍会得到链接错误!这就要重新说回 #include 的作用了:#include 就是将后面文件中所有的代码复制到此处。所以实际上我们的文件1和文件2是这样:文件1:#include <iostream> void Log(const char* message) { std::cout << message << std::endl; } int main() { Log("hello"); }文件2:#include <iostream> void Log(const char* message) { std::cout << message << std::endl; } void IntLog () { Log("world!"); }这样就很明显了,我们将 Log 函数定义了三次,链接器当然会报错了。那么我们要怎样避免这样的错误呢?方法一:在 Log.h 文件中对 Log 函数的定义前添加 static 即可,也就是说,被复制过去的对于 Log 函数的定义仅在当前文件中生效。这样就可以解决链接方面的问题了。方法二:在 Log.h 文件中对 Log 函数的定义前添加 inline 即可,也就是说,被复制过去的仅仅是 Log 函数的定义的内部的文件仅在当前文件中生效。拿文件1举例:方法1:#include <iostream> stastic Log(const char* message) { std::cout << message << std::endl; } int main() { Log("hello"); }方法2:#include <iostream> int main() { std::cout << "hello" << std::endl; }还有一种方法,就是将其写作一个单独的 Translation unit,然后在头文件中仅保留对此函数的声明(再次区分定义与声明的区别!)。最后再重复一次:链接器(Linking)的作用就是要将编译过程中生成的所有对象文件(.obj)链接起来。它还会导入我们所需要的其他的一些库文件,例如 C运行时库、C++标准库、平台API以及其它的一些东西,这是非常常见的。同时也有不同类型的链接:静态链接和动态链接 。这些内容在后续的学习中将会被涉及。
博客主页 CHI's blog 今春不见桃花
闽ICP备2022003806号 闽公网安备35012102500456号 本站由又拍云提供CDN加速/云存储服务 本站已运行 1 年 233 天 16 小时 6 分 自豪地使用 Typecho 建站,并搭配 MyDiary 主题 Copyright © 2022 ~ 2024. CHI's blog All rights reserved.
打赏图
打赏博主
欢迎
欢迎
欢迎访问CHI's blog
欢迎您来评论,但首次评论需经过审核才能显示,之后就不用啦^_^
搜 索
足 迹
分 类
  • 默认分类
  • 相册
  • 随想录
  • 技术向
  • 读书笔记
  • 生活小记