萌新开了一个C++的坑,跟着Cherno大神一起学习C++。
本身也刚入门,理解较为浅显,但是会把自己认为重要的内容挨个儿记录下来!
视频链接在这儿,真的非常良心的up!感谢!
目录
5. C++是如何工作的
# 后面的是预处理语句,编译器看到这句,就先处理这个预处理语句,在实际编译发生之前就被处理了。Include的意思,是它需要找到一个文件,然后将这个文件里所有代码拷贝到现在的文件里,通常被叫做头文件。需要这个头文件,我们才能调用这个文件里的函数cout和cin。
Main函数是程序的入口,当运行程序时,计算机从这个函数开始执行代码,然后程序一行一行地执行代码。Main函数是int型,比较特殊不一定需要返回值;若不返回,系统会默认返回0,这个只对main函数适用。
<< 叫做重载运算符,并没有更多实际意义,可以理解为一个函数,实际上与print函数一样?另一种理解是,将字符串helloworld推送到cout流中,然后打印到终端,然后再推送一个行结束符endl,endl告诉终端跳到下一行。
cin.get函数是等待我们按下enter健,在前往下一句代码之前等待, 这个时候程序暂停执行。而我们已经没有下一行了,所以程序返回0,意味着代码执行完了。
Ctrl+F7可快速编译当前文件,编译会对每一个C++文件产生一个.obj文件;而build,会对整个项目的c++文件进行编译
Error list部分信息缺失,具体bug调试要看output窗口。
在C++中,任何的符号都需要声明;需要调用同项目里的函数,需要void这个函数,来让c++知道这个函数是存在的。
链接器会找到正确的函数定义在哪,将函数定义导到log函数中,让我们在main.cpp中调用;如果找不到函数定义,将会出现链接错误。链接器无法解析这个符合,链接器的工作是链接函数
6. C++编译器是如何工作的
C++编译器是干什么的?我们需要文本转化为实际应用程序的方法,从文本到可执行的二进制程序,我们基本有两个主要操作:编译和链接。编译的工作是将我们的文本文件转换为目标文件的中间格式obj,这些obj文件可以传递到链接。编译器做了如下几个事:1. 预处理我们的代码,当预处理完成,我们的文件会被记号化和符号化,将C++语言整理成编译器真正能够理解和推理的格式,也就是所谓的抽象语法树。编译器将我们的代码转换成常量数据或指令,然后开始实际生成代码,这些代码是我们的CPU将执行的代码。 我们还得到了其他各种数据,比如一个存储所有常量,变量的地方。
CPP文件被称为翻译单元,实际上C++不关心文件,文件只是提供给编译器源代码的一种方式,你需要告诉编译器你输入的是什么类型的文件,以及编译器应该如何处理它。.cpp文件,C++会把它当成c++文件;.c文件编译器会把.c文件当成C语言文件;.h文件编译器会当成头文件。
我们提供给编译器C++文件,编译器将把文件变成一个翻译单元,翻译单元会生成一个obj文件;有时在cpp文件中包含其他cpp文件,变成一个大的cpp文件,你会得到一个翻译单元,一个obj文件。这就是为什么在术语上把cpp文件和翻译单元分开。
Obj文件很大,是因为 包含了iostream。我们常用的预处理语句是include,define,if,ifdef,pragma。#include指定了你想要包含的文件,预处理器打开了文件,阅读所有内容,粘贴到你写的文件中。如下,头文件中只有一个“}”,我们就可以把大括号的位置用头文件代替,因为它只是复制黏贴。
修改预处理器设置把预处理操作可视化,重新编译,会发现一个.i文件。
可以看到源码文件中把.h文件中“}”放进来了
#define预处理器只进行搜索INTERGER,并替换为后面的内容,源码正常。
If预处理语句可以让我们包含或排除基于给定条件的代码。
若改成if 0,VS将会淡出下面的函数,显示这是被禁用的代码;源码中也没有这些代码。
#include <iostream>,整个预处理文件有6万多行。
Obj文件中,是一些人类看不懂的代码。
修改输出文件设置,可以得到汇编语言
这里,改成a*b的时候,汇编语言的代码会小很多。
举了一个log函数的例子,大概意思就是调用log函数会在汇编语言里有一个call,但是log这个函数实际上没有什么用。如果在优化里选择了max speed,在汇编语言里会直接不生成那句call。
7. C++链接器是如何工作的
编译完成后,就是链接的过程。链接需要找到每个符合和函数在哪并把它们连接起来。当每个cpp文件被翻译成一个单独的目标文件作为翻译单元,它们彼此之间并没有关系不能交互。当我们把程序分割成多个C++文件,我们需要一种方法把这些文件连接在一起,这就是链接器主要目的和工作。即使文件里没有外部函数,在一个cpp文件中写完整个程序,应用程序仍然需要知道入口(main函数)在哪,所以在实际运行程序时,C++运行库会说,这是main函数,我要跳到这里执行代码,这实际上才是这个应用的起始位置,所以仍需要main函数链接起来。
在vs中ctrl+F7,仅仅是编译没有链接。只有在bulid项目或F5运行,才会编译,然后链接。如下,一个没有main函数的cpp文件会正常编译;
但是build的时候,就会提示以LNK开头的链接错误。
而以C开头的,是语法错误。
我们需要知道这种不同类型的错误来得知bug出现在哪个阶段来进行修改。绝大多数入口点是main函数,但入口点其实可以自己设置。
故意打错Log函数:
发现原函数仍可以正常编译,因为编译过程确实没问题;
但是会生成一个LNK的错误提示,因为找不到Log函数。
我们把Log注释掉,发现build正常,因为我们从来没有调用过Log函数。
然而,Log和Multiply函数是嵌套关系,主程序中注释了Multiply函数实际上就不会调用Log函数,但是这个时候还是会提示编译错误。因为虽然这个文件中我们不用Multiply函数,但我们可能会在另一个文件中使用它,所以连接器需要链接它。
而如果我们告诉链接器,我们只会在这个文件中使用它,我们可以去掉这种连接的必要性,我们可以通过static来实现。
同样,调用的函数的类型和参数必须与被调用的函数一致,否则同样出现链接问题。
另一种常见的链接错误是重复符号。函数或变量有相同名字和签名,链接器会不知道链接到哪一个。
转移下位置,编译是成功的;
但是build就会失败,因为链接器不知道连接哪个Log函数
一个典型例子:
将Log函数编程一个头文件,然后再Log.cpp和Math.cpp中分别include,在build中发生了连接器错误。这是因为include函数会在两个cpp文件里定义两次Log函数,这样链接器就不知道连接哪个Log函数了。
解决办法是标记Log函数为静态,这意味着在链接Log函数时,Log函数只能是内部函数,这意味着在Math.cpp和Log.cpp中的log函数只能文件的内部函数,它们会有自己版本的log函数,对其他的obj文件都不可见。
Inline和定义修改不写了。。。用到再回头学。
8. C++变量
一个CPP程序,我们希望能够使用数据,编程的大部分内容实际上都是在使用数据,将数据存储在某个地方。当我们创建一个变量时,它将被存储在内存中:堆和栈。变量会占用内存,是我们实际存储数据的地方。C++中,我们有很多原始数据类型,这些基本数据类型基本上构成了我们在程序中存储的任何数据,每钟数据类型都有特定的用途…(有点乱)不同变量类型之间的唯一区别是大小,就是这个变量会占用多少内存。
Int表示整数,它让我们在给定的条件下存储一个整数。如果我们想声明一个变量,输入这个变量的类型,起个名字,然后给一个值。后一项是可选的,不用马上给它赋值。
整数通常是4个字节的数据类型,数据类型的实际大小取决于编译器,不同编译器会有不同。Int数据类型实在一定范围内存储整数,因为是4个字节,这是一个有符号整数,储存的值从负20亿到正20亿,比这个数更大的就需要更换数字类型。变量的类型大小与它能存储多大的数字直接相关。Int是4个字节32位,其中1位是符号,这样数字只剩31位。2^31≈2147483648,有一位是0,因此准确来说int可表示的范围是-2147483648 ~2147483647。同样,我们可以使用无符号整数位,把符号位也作为数据的一部分,这就是unsigned int,可以表示42亿个十进制数字
通常就是5个数据类型,还有char(1字节),short(2字节),int(4字节),long(通常4字节,取决于编译器),long long(8字节);这五个前面都可以加unsigned来移除除符号位,可以让你设置一个更大的数字。通常char是用来存储字符的,但实际上字符和数字是一样的例如A就是65,程序员对某些数据类型做出假设时,如果传入一个char,本意就是character,通常就是希望真的分配一个字符。
可以看到,给char类型的a赋值‘A’或者65,程序输出的都是A。因为cout传入一个char到cout,cout会把char类型的数据当成字符而不是一个数字
可以看到,当改成short类型,cout不再把它当作一个字符,而是会打印出数值。
数据类型的使用取决于程序员,有一些既定的约定,但不用死记。通过以上是想让我们明白大小是这些数据类型之间唯一的区别,创建变量时通过数据类型将分配多少内存。
浮点型我们有float(4字节)和double(8字节),同样你可以long double这种。
但实际上,这样定义的是一个double类型的浮点数;为了区分一个小数是float还是double,float类型的小数需要在后面增加一个f,大小写都可。
还有数据类型bool(1字节),意思是boolean,代表true或false
理论上,bool类型只需要1bit去表示0和1,但我们处理寻址内存时,也就是我们需要从内存中找回我们的bool变量的值时,没有办法去寻址只有一个bit位的内容,只能寻址字节,所以我们不能创建只有一个bit位的变量,因为我们需要能够访问它。但我们可以在一个byte内存储8个bool数。
我们如何知道一个数据究竟多大?毕竟它依赖于编译器,有没有什么地方可以查吗?在C++中调用sizeof操作符。
自定义类型都是基于这些基本的类型创建的,在这些基本类型基础上,我们可以创建任何的类型,并存储我们的任何数据。现在,有了这些原始数据类型,我们还可以把它们转换为指针或引用,指针加*,引用加&
9. C++函数
函数就是我们写的代码块,,用来执行特定的任务。在我们学到class类的时候,这些块统称为方法。但是函数,准确来说不是属于C++类里面的东西。使用函数是非常常见的,避免代码重复。我们把我们要做的事情写成一个函数,多次调用它。
写一个两数相乘的函数,首先定义这个函数的返回值类型,因为是相乘,所以定义为int型。
函数也可以不一定要提供参数
也可以让函数什么类型都不返回,使用void来声明函数,void意思是什么也没有。
会到最初的例子,我们通过定义一个result变量来存储Multiply的结果
但是,在多次调用的时候,我们经常忘记修改一些细节,导致程序输出有问题,且并不好找。
我们可以修改乘法函数的定义,只有乘数和被乘数的变化,计算完以后打印,不需要返回值。
主函数也会变得极其简洁
*每次我们调用函数,编译器生成一个call指令,为了调用一个函数,我们需要创建一个堆栈结构,我们需要把参数推进堆栈,我们还需要一个叫做返回地址的东西压入堆栈,然后我们要做的是跳到二进制执行文件的不同部分,以便开始执行我们的函数指令。为了将push进去的结果返回,我们得回到最初调用函数之前。就像在内存中跳跃来执行函数,跳跃和执行都需要时间,会减慢我们的程序。划*重点是因为,假设编译器决定保持我们的函数作为一个实际的函数,并不做内联inline工作。提到这些是为了说明,不必将每一行代码写成函数,这样会拖慢我们的程序。函数的主要目的是防止代码重复,需要一些经验就能知道什么时候要写函数。
main函数的返回类型是int,但是没有返回值,也没有return。但是有且只有main函数可以没有返回值,其他指定了返回类型的函数必须有返回值
我们通常还将函数分解为声明(declaration)和定义(definition)声明通常存储在头文件中,我们在翻译单元或CPP文件中编写定义。
10. C++头文件
我们有两种不同文件类型的概念,一种是想C++一样,编译的编译文件一个翻译单元,这种就会有头文件的概念。头文件远不止是为了声明一些你要声明的,然后再多个CPP文件中使用,随着C++学习的深入,有些工作确实需要头文件才能工作。头文件通常用于声明某些类型的函数,以便它们能够被使用在你的程序中。我们需要某些声明存在以便知道哪些函数和类型可供我们使用。如果我们在一个文件中创建函数,而我们想在另一个文件中使用它,C++不会知道这个函数存在当我们编译另一个文件的时候,我们需要一个公共的地方来存放东西,只是声明没有定义,因为我们只能定义函数一次。一旦我们需要一个公共的地方来存储声明,没有实际的定义,没有函数体,只是一个地方来表明,这个函数是存在的。
Log函数定义在main.cpp中,log.cpp不知道Log函数是什么因此有报错。
那么我们怎么知道log函数是确实存在的只是定义在别处?这也就是说,需要函数声明。
增加如下这句,没有函数体,表示它是函数的声明,告诉C++确实有这么一个函数。
我们确实告诉C++了有这么一个函数,但是当我们新建另一个文件,我们就需要再复制一次这个声明。然而使用头文件会变得更简单。头文件通常包含在CPP文件中,复制黏贴,把头文件的内容放入CPP文件,通过#include预处理器指令来实现。
#pragma
以#开头的东西被称为预处理器命令或预处理器指令,意味着它将在实际编译此文件前被先处理。Pragma本质上是一个被发送到编译器或预处理器的与处理指令。Pragma表示只包括这个文件一次。Pragma once监督这个头文件,阻止我们单个头文件多次被包含并转换为单个翻译单元。这并不妨碍我们将头文件放在程序的多个位置,而只是说放在一个翻译单元,一个CPP文件。因为如果我们不小心多次包含了一个文件,并转换成一个翻译单元,我们会得到复制错误,因为我们会复制黏贴整个头文件多次。我们将通过我们创建的结构体来演示这一点。
我们把#pragma once注释掉,并在log.cpp文件中include两次,发现报错:重新定义结构体。
Include的工作原理是复制黏贴到其他文件,所以你可以创建一串的头文件,所以你其实可以让player的头文件包含log,或者第三个头文件包含其他别的include等等。
这里用一个common的头文件来展示。仍然得到player被重复定义的提示;log函数被include了两次。
当我们取消了pragma once的注释,error消失了,因为它识别到了那个log.h已经被include,没有include两次。
实际上还有一个办法来实现监督,它比pragma更意义,虽然pragma once看起来更简洁。
所谓的头文件保护符
这个在以前经常被使用,只是现在有了新的预处理语句Pargma once。几乎每个编译器现在都支持pragma once,不只是visual studio,还有gcc,clang,msvc。
头文件有 < 和 ‘之分。当我们编译程序时,我们需要告诉编译器确切的include路径,这是我们电脑里文件夹的路径,含有需要包含的文件。如果我们需要include的文件在其中某一个文件夹里,我们就使用尖括号<来告诉编译器搜索包含头文件的文件夹。而引号‘则通常用于包含相当于当前文件的文件夹。尖括号只用于编译器包含路径,引号可以做一切,但我一般只用它在相对路径下,主要还是用尖括号。
Iostream比较特殊,它没有拓展名。它实际上就是一个文件,只是它没有扩展名,是写C++标准库的家伙决定这么干的,用于与C#进行区分。我们可以通过vs看到它的源码和地址。
11. 如何在VS中调试代码
Debug可以帮助我们理解程序在计算机中是怎么运转的。调试的两大重点是断点(break point)和读取内存(reading memory)。那么调试的意义是什么?debug意思是de bug清除错误。我们需要诊断我们的代码错在哪?电脑永远是对的,debug是为了找出你的错误。我们可以设置断点在我们的程序的任何代码上,程序运行到这儿的时候,它将暂停,挂起执行线程,让我们看一下这个程序的state。这个state指的是内存,我们可以暂停这个程序,然后看下在它的内存中发生了什么。一个运行中的程序所需的内存时相当大的,包括你设置的每个变量,调用的函数等等;在程序中断后,内存数据实际上还在,能查看内存,对诊断你的程序出问题的原因非常有用。你可以看到每一个变量的值,来看是否符合预期;还可以单步逐行运行你的代码,比如在第5行设置断点,它只会运行到第6行。还可以step into函数内,看看函数会运行到哪里。
使用F9或者在左边单击就能打上断点。
确保要处于debug模式,因为release模式编译器实际上会修改代码,断点可能永远不会生效
逐语句Step into的意思是进入到这行代码上的函数里面,我们可以看到函数实际每一步都干了什么,快捷键 F11
逐过程Step over就是从当前函数跳到下一行代码,快捷键F10
跳出Step out是跳出当前函数,回到调用这个函数的位置,快捷键shift+F11
通过Step into,我们可以看到message已经被赋值为Hello World!,这就是读取内存。
黄色箭头指向的代码行代表该代码还没有被执行。可以看到指针在第6行,但a的值目前不是8,
Autos,locals,watchs是三个对我们很重要的窗口。Autos自动窗口会向你展示可能对你重要的局部变量或变量。Watchs可以让我们监控我们想要的值,目前由于未完成初始化,都还是无用的,随着程序的进行,这些值将会更新显示内存中的值。
内存,它是一个视图,可以查看我们程序的内存,这叫做内存视图,我们可以按照如下的方式把内存窗口也调出来。
最左边的是内存地址,中间是以十六进制表示的实际值,右边是ASCII码对这些数字的解释
输入&a来显示变量a存储在程序的内存中的位置,没有完成初始化,所以a的值还是随机的,但这是一个明确的值,这就是调试模式的好处。调试模式会减慢我们的程序,编译器会让程序做一些额外的事情让我们调试变得更轻松。
这个例子中a的值是随机的意味着他是未初始化的栈内存。编译器知道我们准备做一个变量,但我们还没有初试化它,所以它要做的就是先用随机值把它填满。好处是当我们在调试代码时,一旦出现问题我们查看内存时,发现变量值是随机的,我们就知道我们没有初始化过这个变量,这就是问题所在。但做这样额外的事情会减慢程序的速度,我们不想再release模式中这样做,但这在调试中很有用。
同样,可以在watch窗口,使用十六进制显示,这意味着a当前正在栈内存初始化。
我们现在按下F10;
可以在watch窗口,看到a的值变成了8。内存里2个数字等于1个字节,这也就是为什么我们用十六进制来看,每两个十六进制数与1个byte对齐,这样我们就能分辨这8个十六进制数字是4字节内存。
这就是我们现在做的,我们暂停了程序,查看程序的状态state,读取它的内存。
我们看到string被赋了值,char*实际上是一个指针,指向内存地址0x00007ff75ee3abb
- 而这个地址存储的值,是Hello的ASCII码。
我们如果想继续看到Log函数,只需要再打一个断点在Log,然后F5,这样程序就会在下一个断点处停下。
可以看到c的值已经变成了o,但是是红色,这部分内容以后再讲。
一个程序就是由内存组成的,甚至是指令指针,在我们的程序中这些都存储在内存中,所以说我们的内存是最重要的。通过设置断点,我们可以暂停程序,在给定的时间点和代码行检查一下,看看我们所有的变量是什么,这对我们调试代码非常有用。
12. C++条件与分支(if语句)
来看看条件语句,if语句,分支语句在C++中是怎么工作的。
我们一般需要一个特定的条件进行评估,根据评估结果来决定我们想要执行什么代码。先是对实际condition的评估,然后是基于这个条件语句评估后的分支语句,结果为真跳到源代码的某个部分,结果为假我们跳到源代码的另一部分。当我们开始一个应用程序时,整个应用程序及其所有模块加载到内存中,基本上所有这些指令组成了我们的的程序,现在都存储在内存中,当我们有了条件语句所产生的分支,我们基本是在告诉电脑,提到这一部分内存开始吧,在那里执行我们的指令。由此,在内存和分支之间跳跃实际上会拖慢程序,很多程序优化就是优化分支语句。
在整数或者在大多数原始的数据结构中,如果你要检查两个数,比如两个整数是否相等,基本上是在获取他们的四个字节的内存,比较每个字节,为了让这2个整数是相等的,内存的每一位都必须相同。
我们可以通过插入断点,然后查看某行代码的反汇编
这个反汇编的视图在你debug时非常有用,一方面可以用于源代码无法找到错误原因只能求助于调试CPU指令(尽量避免这个情况)。这样可以快速看到编译器实际生成的代码,而不是把所有的东西都输出到一个文件中。
(解读了一下这段汇编语言的代码)jne = jump not equal(如果不等的话就跳出),je=jump equal(如果等的话就跳出)bool类型的数据其实用1位就能表示,但是必须要用一个字节。实际上,0就是false,不等于0就是true。
所以if语句到底在做什么?他只是在检查这个布尔数据值是数字0嘛?如果是0,就不执行if语句;如果不是0,就会跳到if语句内,这就是为什么在判定条件里不需要写 == true这样的话。所以if(1)程序就会打印Hello World了,if(0)就什么也不做。
当If语句里只有一行指令,可以不需要大括号,也可以把程序放一行,但是这样在debug时会有问题,分不清在if判断还是执行语句
If语句同样可以放在判读指针是否为空;如下当ptr有内容的时候显示ptr的内容;ptr为空指针的时候就不执行。
else if实际上并不是C++关键字,实际上是分开的,如下;写成else if实际上跟把if判断和执行放一行是一个意思。
编程实际有两种,一种是数学编程,做一些运算类的编程;另一种是逻辑编程,全是逻辑。在未来我们学会些更好的代码的时候,会在很多情况下需要用到if语句,但大佬可以用一些数学计算代替,而不是作比较再通过分支语句来处理,因为这样会降低很多程序的速度,这被认为是糟糕的代码。
13. Visual Studio的最佳设置
这些文件夹不是文件夹是过滤器,世纪上是一种虚拟组织方案,这些过滤器划分好了源代码。
点击显示所有文件,可以看到真实的文件夹目录,这个时候就变成了新建文件夹。
Build以后,可以看到exe文件的地址
修改VS中的输出目录和中间目录的设置。
我们可以查看各种宏代表的是什么
14. C++循环
for循环或while循环。循环是我们写代码时,需要多次执行同样的操作。Helloworld我们可以复制粘贴5次,也可以把参数放进函数,调用函数5次;或者我们可以使用循环,可以让代码连续运行5次。
For循环分为三段,第一部分是变量的声明,要在这里声明变量(一般是i,iterate)第二部分是条件,循环5次就是i<5;最后一部分会在for循环的下一次迭代之前被调用,i++ 和 i+=1 和 i=i+1是一个意思,指的是起始于0的变量i增加1,for循环的每一次迭代,都会增加1,然后我们写for的循环体,
For循环的三个声明,第一段是开始for循环时会运行一次,第二段是比较后的bool类型,它将在下一次for循环结束前进行评估,第三段在for循环的最后被运行。所以其实可以把第一段放前面,第三段放后面。
同样,可以把中间段修改,因为我们并没有改变程序的行为,只是用不同的写法重写了代码。
另外,for循环在做遍历数组之类的事情上也非常有用。
While循环和for循环类似,只是没有开头的申明以及后面的语句,仅有条件语句。
那while循环和for循环我们该如何选择?这就看你是否需要一个变量,实际上并没有区别,主要看习惯。有一种习惯约定是如果你有一个已经存在的确定的条件,只是想做一些比较那么用while循环。例如游戏循环,只要变量running的值为true你就想让这个游戏持续循环,因为条件是不变的,不需要在每次循环之后改变这个条件,不需要在循环前先声明这个条件变量,我们可以将之前的变量或者函数调佣后的结果拿来用,不需要保持更新或者初始化某些东西。然而,当我们处理数组的时候,一个确定长度的数组,例如数组有10个元素,我将会使用for循环,因为我想让代码循环10次,因此我想要跟踪某些变量,仅仅循环10次,而且这些变量可用于处理数组中的元素,因为如果我想要处理10个元素长度的数组中的元素时,我需要一个偏移量或者叫索引的东西来处理元素。对于i这个变量,我们要跟踪它,从0 1 2 3 4当我们的迭代进行的时候,这和数组的索引完全一致,非常适合。
还有一种do while语句已经不常用了。实际上它就是不管怎样都会先运行一次,即使condition=false。
15. C++控制流语句
控制流语句与循环语句一起工作,它让我们可以更好地控制这些循环的实际运行,主要有三个continue,break,return。
Continue只能在循环中使用,continue表示进入这个循环的下一个迭代(如果有的话),如果没有,那循环就会结束。Break主要用于循环中,也出现在switch语句中,意思是要跳出(中止)循环。Return是最有力量的,会脱离你的函数,会退出这个函数。函数可能需要一个返回值,如果只有return,它使用与void函数,如果函数需要返回值的话,需要为它提供一个值。
Continue加这儿不会改变行为,本身就会执行
设置一个条件,i整除2的时候,直接进入下一个循环(不会执行Log,直接i++),我们可以看到log程序只在奇数位执行,设置断点也可以看到这一点。
Break语句,会完全跳出for循环。
Return
这里main函数必须要一个返回值,所以要return 0,在这里return 0直接退出程序了,因为std::cin.get()没有发挥作用。
Return函数可以用在任何地方,但像如下这么做,retrun 0后面的句子不会在任何情况下被触发,这就是所谓的死代码。有些编译中会限制用户写这种代码。
16. C++指针
指针可能是整个系列中最重要的一集。这里讲的是原始的指针,不是只能指针。
对计算机来说,内存就是一切,内存是编程中最重要的一件事。当年编写一个应用程序并启动它时,所有的程序都被载入到内存中,所有的指令告诉计算机在你写的代码中要做什么,所有这些都被加载到内存中,CPU就是这样访问你的程序并开始执行它的指令的。当你创建一个变量,当你从磁盘中加载数据时,所有东西都存储在内存中,如果没有内存,那就神恶魔也做不了了,而指针对于管理和操纵内存非常重要。
指针是一个整数,一种存储内存地址的数字。假设有一条街,街旁边一排的房子。这就是计算机中的内存,这就是计算机的内存。他只是一条线性的线,在这条捷航的每一所房子都有一个号码和地址。回到计算机,这条直线上的每一幢房子都有一个地址是一个字节,它是一个字节的数据,我们显然需要一种方法来寻址所有的byte,就是街上所有的房子。因为假设某人在网上订了东西,想要送货上门,它需要被送到正确的房子里,或者有人需要把东西从他们的房子里送出去,无论哪种,你需要能够从这些房子的内存字节中读写。指针就是这些地址,这些地址告诉我们房子在哪里。我们在代码中做的几乎搜友事情都是在内存中读写,你完全有可能写一个不适用指针的C++程序,但指针是非常有用的工具。因为内存是你拥有的最重要的东西,是计算机能提供的最重要的资源,能够对内存有更多的控制是至关重要的。再次强调下,一个指针只是一个地址,是一个保存内存地址的整数。类型与这些无关,类型只是我们为了让生活更容易而创造的某种虚构。所有类型的指针都是保存内存地址的整数。
指针其实不需要类型,如果我们给指针一个类型,我们只是说,这个地址的数据,被假设为我们给的类型,除此之外没有任何意义。它只是一些我们写出来让代码实现变得更容易的工具。
我们设置一个void类型的指针,void以为这我们不关心我们的代码中的指针类型,只是需要保存一个地址。我们给这个指针的的内存地址是0,0实际上不是一个有效的内存地址,内存地址不会一直到零,零是无效的,意味着这个指针是无效的。无效指针是可以接受的,但0不是一个有效的内存地址,我们不能从内存地址0重读取或写入,这么干程序会崩溃。
我们也可以这么干
或者C++关键字 nullptr
我们建立了第一指针,它是无类型的,内存地址是0,完全无用,但可能是你能写的最简单的指针。进一步,我们定义一个整型变量var的值为8,定义一个void类型的指针,&var来查看他的内存地址打入断点。可以看到var的值被设为8,指针ptr的值为0x00000042cdaff734,把这个地址放到内存中查询,可以看到0x00000042cdaff734这个内存地址对应的值为我们设置的8。
以上就是指针的工作方式,指针是一个保存地址的变量,一个整数。指针就像其他变量一样,它不是保存变量a这样的值本身,他保存着一个内存地址,内存地址也是一个整数值。这个整数有多大,这个指针有多大,取决于很多东西,可能是32位整数,也可能是16位,不重要反正是个整数。可以看到,换成double类型没有任何区别。
我们可以通过*+指针变量来逆向操作,这个*ptr等同于var。这里有一个报错,因为编译不知道这个指针指代的数字的大小。Short(2字节),int(4字节),long long(8字节)编译器不知道这需要多少字节的数据。这个时候就需要类型了,我们要告诉编译器这是一个整数,就是int型四字节。只需要把void*改成int*就能解决这个问题。
计算机给我们分配了8字节(char 是1字节,一共8个)的内存,并返回一个指向那块内存开始的指针,然后使用堆关键字memset的函数,用我们制定的数据填充一个内存块。第一个参数是内存块开始的指针,第二个参数是取值比如1,最后一个是大小,那么在这个情况下应该填入多少字节。
当我们知道指针名的时候,可以直接在地址里输入指针名,会直接指向按个地址。
可以用delete[] 删除指针数组
指针本身也是变量,存储在内存中,所以我们可以得到双指针或三指针,是指向指针的指针。
我们可以看到,二级指针ptr存储的值是一级指针buffer的地址,VS里会一层一层显示指针所存储的值。
所以!指针只是存储内存地址的整数。
17. C++引用
指针和引用是被大量翻来覆去提及的两个关键字,计算机现在用它们做的事情几乎是一样的。从语义上来说,我们如何使用和书写是有一些细微的区别的。但根本上,引用通常只是指针的伪装,它们只是在指针上的语法糖(syntax sugar),让它更容易阅读,更容易理解。
引用是一种我们引用现有变量的方式,不像指针,你可以随便创建一个新的指针变量,设置它让它等于空指针或类似的东西。但不能对应用这样做,因为引用必须“引用”已经存在的变量,引用本身并不是新的变量,并不占用内存,没有真正的存储空间。
如果我想给这个变量创建一个引用,在它的变量类型后面加一个&,这个&符号实际上是变量声明的一部分。在指针里,使用&符号来实现获取变量的内存地址。这里不一样,这里的&是变量类型的一部分,不是在变量旁边的,所以要注意区别
现在我们所做的就是创造了一个叫做别名的东西,因为这个ref“变量”,它只是一个引用,ref变量实际上不存在,编译之后只会得到变量a。引用,只是我们可以在源代码中编写,以使我们的生活更简单的东西。(就是起个别名)
这个例子中,我们没有把它作为一个指针或者一个引用传递,它会复制5这个值到函数中,复制会创建一个全新的变量value,而没有达到我们想使a递增的效果。
修改一下以上代码, 我们用int定义这个Increment函数并给它一个return值,可以看到在执行到Icrement(a)的时候,a的值没有变化,有变化的Increment函数中的返回值,这就没有起到我们希望函数递增的作用。
我们需要做的是通过引用来传递变量,如何通过将这个变量传递到函数中来修改它呢?实际上,我们可以不把实际值5传递给函数,可以把a变量的内存地址传递过去,因为我们可以在这个函数中可以写入该内存地址,因为我们已经将该内存地址传递给函数了。
修改后的函数以指针value(要修改数据的内存地址)作为输入。由于运算顺序的优先级不同,*value++表示将value+1,再取value+1的内存地址的值,这显然不是我们想要的;而(*value)++代表先取指针value所指向的值,将(*value)+1。主函数中,我们以&a(a的指针,也就是a的内存地址)作为输入,这样就实现了a的递增。
然而,我们可以通过引用来使得这一切变得更简单。Int &value引用a之后,a就跟value一样了,非常方便快捷地达到了目的。
关于引用,另一件重要的事情是,一旦声明了一个引用,你不能改变它引用的东西。
如下的代码,原本建立了a的引用ref,然后觉得引用错了又想重新引用b,但这样写是不对的,这样会将b的值赋值给a。
这意味着当你声明一个引用时,需要把它进行赋值(也就是int& ref = a),不能不赋值,因为它必须引用一些东西。为了达到刚才的效果,使用指针的方式来指引就能达到。
18. C++类
面相对象编程实际上是只一种你可以采用的方式来写你的代码,C语言,Jave主要是面相对象的语言,你不能写任何其他类型的代码。C++有点不同,它没有强加给你一种特定的风格。C语言实际上并不支持面向对象编程,因为为了面向对象编程,需要有一些概念比如类和对象,但如果你想使用面向对象的话,C++会添加所有这些功能。
简单来说,类只是对数据和功能组合在一起的一种方法。例如在游戏里,我们可能想要一些代表角色的东西,那我们需要什么样的东西来带边角色呢?显然我们需要一些数据比如角色在游戏中的位置,角色可能拥有的某些属性,比如移动速度,3D模型等,所有这些数据都需要存储在某个地方,我们可以为所有这些创建变量。但是,它们只是一堆没有组合在一起的变量,是无组织的被存放在我们代码里,所有这一切都变成了如此多的代码,难以维护。
我们通过类来简化它们。我们可以创建一个叫做Player类,它包含了所有我们想要的数据,编程一种类型。我们使用class+名字实现,这里的名字必须是唯一的,因为class是类型,我们新建的是一个新的变量类型。看起来很像函数,但是结尾的大括号要加分号。
由类类型构成的变量成为对象(object),新的对称为实例(instance)。
调用类中的参数时,player不能访问在类中声明的私有成员。
这是因为“可见性的原因”,当你创建了一个新的类,可以选择指定类中的内容可见性。默认情况下,一个类中所有东西都是私有的,这意味着只有类中的函数才能访问这些变量。然而,我们希望从main函数中访问这些变量,所以我们要做的,就是在这里把它设为public。public意味着我们可以在类之外的任何地方访问这些变量,
现在我们很好的清理了代码,把所有变量都放在了一个地方,这些变量集合可以代表一个player,所以我们把它很好地分组了,现在我们有了这些数据,假设我们想让player做一些事情,比如移动,我们要写一个函数来改变X和Y的变量值,我们可以把它写成单独的函数。我们通过引用传递,因为我们要修改Player的对象。xa和ya是player移动的距离。最后在Main函数中调用Move函数,设置调用对象,x,y的值。
更进一步,类实际上可以包含函数,我们可以将move函数移动到类中,类内的函数被称为方法(method)。我们把move函数放进Player里,这样就消掉了第一个调用player实例的参数。Main函数中也可以直接调用类中的函数。
这两种方法并没有什么不同,但是后一种会让代码更简洁,更易于维护。
类允许我们将变量分组到一个类型中,并为这些变量添加功能,我们在这个类里有数据和处理这些数据的函数,这就是所谓的类了。类虽然可以让代码更简洁,但如果不使用类搞不定的事儿,使用类也一样搞不定,类不会给我们任何新功能。类本质上只是语法糖,可以让我们组织代码,更易于维护。
19. C++类与结构体对比
我们有两个术语,结构体struct和类class。看起来有点相似,那我们是应该使用结构体还是选择类?实际上,基本没有区别,只有一个关于可见度的小区别。默认情况下,类是私有的。如果你不指定修改任何可见性,那么类里面的参数默认就是私有的。然而在结构体中,默认值是public。技术上讲,这就是类与结构体唯一的区别。
Struct结构体在C++中继续存在的唯一原因,是因为他希望与C保持向后兼容性,因为C代码没有类但有结构体,如果我们突然去掉整个结构体关键字,我们会失去兼容性,这样C++编译器不知道什么是struct。
实际上结构体和类没有区别,只是多写一个public的区别。Cherno自己,当使用Plain old data时,他喜欢尽可能地使用struct,pod是一种只表示变量的结构,例如是数学上的向量类。这个与之前的class Player有区别,这个类似于纯数学上的运算,而Player你需要增加很多功能比如键盘控制,3D渲染等等。这种时候倾向于使用结构体。
另外的场景是继承(inherit)。他不会在struct中使用继承。如果我们要有一个完整的类层次结构,或者某种继承层次结构,我们将使用类。因为继承是一种增加另一层次复杂性的东西,我只希望我的结构体是数据的结构。
如果你尝试混合使用这些类型,比如你有一个叫做a的类和一个叫做b的结构体,这个b结构体继承自a,某些编译器会警告说你继承了一个类,但你是一个结构体。
综上,如果我只想表示一些数据,那么用结构体;如果我想要一个大量功能的整个类,比如一个游戏世界,一个player,或者其他可能有需要继承的东西,我们将使用类。
20. 如何写一个C++类
写一个基本的log类。这个log类是我们管理日志信息的一种方式,我们如果想我们的程序打印消息或信息到控制台,这通常用于调试。如果我们想知道发生了什么,只需要将事物的状态打印到控制台,应用程序中的控制台就像一个放信息的地方,我们可以用它来打印发生了什么,这也几乎可以保证,代码在正确工作的东西。控制台基本上是内置在操作系统中的东西,我们几乎可以保证它总是有效的。Log类函数可以根据需要写的复杂或简单,但它对调试和开发非常重要,花时间在上面是绝对值得的。
准备先写一个只有警告、错误但不跟踪消息的Log类。首先搭好整个框架,在main函数里构建需要怎么使用Log函数。实例一个Log对象,设置log等级,打印warn级别的内容。
设置私有变量,增加前缀m,这样我们就能知道,在类代码中哪些是成员变量,哪些是局部变量
针对变量的Public与方法的public分开了,为了看起来更整洁;需要有数字代表错误,警告和信息跟踪,这里设为了0,1,2.
设置warning函数,作用是打印message的内容,前面增加[WARNING]:,同样的方法也用于设置ERROR和INFO;增加一个if条件,只在LogLevel级别大于等于Error时才进行打印。
实际运行结果可以看到,由于设置了LogLevel,因此只打印了Error和Warning级别的信息。
21. C++中的静态(static)
Static关键字在C++中有两个意思,一个是在类或结构体外部使用static关键字,另一种是在类或结构体内部使用static。类外面的static,意味着你声明为static符号,链接将只是在内部,意味着他只能对你定义它的翻译单元可见。然而类或结构体内部的静态变量static意味着该变量实际上将于类的所有实例共享内存,该属性与实例无关,是所有对象共有的。类似的事情也适用于类中的静态方法,在类中,没有实例会传递给该方法。
给变量增加s前缀来表示这个变量是静态的。这里的static的意思是,这个变量只会在这个翻译单元内部链接,静态变量或函数意味着,当需要将这些函数或变量与实际定义的符号链接时,链接器不会在这个翻译单元的作用于之外寻找那个符号的定义。
我们在另一个函数中定义一个同名的变量,可以看到程序是可以正常运转的;而删除了static就不行,因为我们不可以有两个同名的全局变量。
有一种解决方法是,我们将其中一个变量声明为extern int,这表明这个变量是在这个cpp文件外部的变量,链接器会在外部翻译单元去查找这个变量。
而如果,我们在Static文件中增加static,类似于表明这个变量是static.cpp私有的,在运行main函数的时候,就会报错无法找到s_Variable。
相同的用法也用在function中。
在类或结构体外使用static,只意味着当你声明静态函数或静态变量时,它只会在它被声明的CPP文件中被看到。如果你想在头文件中声明一个静态变量,并将该头文件包含在两个不同的C++文件中,那就是在两个翻译单元中都声明了相同的s_variable变量为静态变量。如果你不需要变量是全局变量,你就需要尽可能多地使用静态变量,因为一旦在全局域下声明东西的时候,链接器就会跨翻译单元进行链接。基本来说,全局变量是有些不好的,会引起bug,最好是让函数和变量标记为静态的,除非你真的需要它们跨翻译单元链接。
22. C++类和结构体中的静态
在一个类或结构体中的静态,在几乎所有面型对象的语言中,静态在一个类中意味着特定的东西,如果把它和变量一起使用,这意味着在类的所有实例中,这个变量只有一个实例。如果我创建一个名为Entity的类,我不断创建Entity实例,我仍然只会得到那个变量的一个版本。意思是,如果某个实例改变了这个静态变量,它会在所有实例中反映这个变化,因为这里只有一个变量,尽管有一堆实例,所以通过类实例来引用静态变量是没有意义的,因为这就像类的全局实例。静态方法也是一样,无法访问类的实例。
如下,我们用Entity结构体定义了函数并打印,符合我们预期。
如果我们让变量是静态时,事情就会改变。首先第二个实例会报错初始值设定太多,因为x和y被设定了两个初始值。
同时需要再外部定义静态变量X和y的作用域,否则链接器识别不到外部符号x和y。
语法问题解决以后,我们看到输出的两个还是5和8。
当我们让这些X和Y静态时,我们让这两个变量在Entity类的所有实例中只有一个实例,这意味着当我改变第二个Entity实例的X和Y时,它们实际上和第一个完全一样,它们指向的是相同的内存,不同的实例但是它们的x和y是相同的,所以我们这样e.x e.y是没有意义的。这就像我们在名为Entity的命名空间中创建了两个变量,他们实际上并不属于类。从这个意义上说它们可以是private也可以是public的,它们仍然是类的一部分,而不是命名空间。当你创建一个新的类的实例或类似的东西时,它们与任何分配无关了。所以其实上一段代码干的事情跟如下是一样的。
当然这也是有用的,当你想夸类使用变量时,你只需要创建一个全局变量,或者不使用全局变量,而是使用一个静态全局变量,他是在内部进行链接的,它不会在你的整个项目中都是全局的。这样做会有相同的效果,那么我们为什么要在类中使用静态呢?
原因是,把他们放在Entity中是有意义的。假如你有一条信息,想在所有Entity实例之间共享数据,或者将它存储在Entity类中是有意义的,因为它与Entity有关。要组织好代码,你最好在这个类中创建一个静态变量,而不是一些静态的或者全局的东西到处乱放。
静态方法不能访问非静态变量,会报错对非静态成员的非法引用。
原因是静态方法没有类实例,本质上,你在类中写的每一个方法,每个非静态方法总是获得当前类的一个实例作为参数,这就是类在幕后的实际工作方式,平时看不到,他们通过隐藏参数发挥作用。静态方法不会得到那个隐藏参数,静态方法在类外部编写方法相同。非静态变量属于对象,静态方法不是对象的一部分,所以无法访问对象中的变量。
如下的代码,本质上是非静态类方法在编译时的真实样子
而我们把这个实例去掉,这正是我们将static关键字添加到类方法时所做的。
23.C++中的局部静态(Local Static)
你可以在局部作用域中使用static来声明一个变量,局部静态会有更多的含义,声明一个变量我们需要考虑两种情况:变量的生存期和变量的作用域。生存期指的是变量实际存在的时间,在它被删除之前,它会在我们的内存中存在多久。而变量的作用域是指我们可以访问变量的范围,如果在函数内部声明一个变量,我们不能在其他函数中访问它,因为我们声明的变量对于我们声明的函数是局部的。局部静态变量允许我们声明一个变量,它的生存周期基本上相当于整个程序的生存期,而它的作用范围被限制在这个函数内。你可以在任何作用域中声明这个,刚才只是用函数举个例子,这并不仅仅局限在函数内部。这就是为什么函数作用于中的static和类作用于中的static之间没有太大区别,因为生存期实际上是相同的。唯一的区别是,在类作用域中,类中的任何东西都可以访问这个静态变量;而如果在函数作用于中声明一个静态变量,那么它将是哪个函数的局部变量,对类来说也是局部变量。
如下,我们在函数中声明了一个静态变量。这意味着当我们第一次调用函数时,这个变量将被初始化为0,所有对函数的后续调用实际上不会创建一个全新的变量。
我们可以看下差别:i是非静态的时候,每次调用Function函数都会创建一个新的变量,把i从0加到1;而当我们把i作为静态变量的时候,每次调用函数都会在上一次基础上增加1,就有了12345的输出。
二号的做法,其实跟把int i=0移到函数外面是大致一样的。但是区别点是,i在函数外部定义的时候,i的值会被外部影响。而如果你只希望这个变量在函数内部变化的话,你就应该选择在局部作用域下声明成static,这样外部就无法修改这个变量。
另一个例子是如果你有一个单例类,单例类是只存在一个实例的类。如果我想创建一个单例类而不是用局部静态作用域,我就要创建静态的单例实例,可能是个指针。我们可以在Get方法中新建一个实例而不用再在外部声明一个实例。可以看到右边的代码比左边简洁很多。
如果去掉static,那么这个单例会在栈上创建,当代码运行到后面一个大括号,函数作用域结束时就会被销毁,这就是第一个严重错误,尤其还是在引用它的时候。如果只是复制(删除引用)当然不会有问题,而我们是返回一个对它的引用。而通过添加静态,将它生存期延长到永远,这意味着每次,我们第一次调用Get,它实际上会构造一个单例实例,在接下来的时间,它只会返回这个已经存在的实例,
24. C++枚举
ENUM是enumeration的缩写,它是一个数值集合,如果你想要给枚举一个更实际的定义,他们是给一个值命名的一种方法,我们不用一堆叫做a,b,c的整数,我们可以有一个枚举数,它的值是a,b,c与整数对应。它还能帮助我们,将一组数值集合作为类型,而不仅仅是用整形作为类型。当让也可以给它复制任何整数,或者限制那些值可以赋值。它只是一种命名值的方法,当你想要用整数来表示某些状态或某些数值时,它非常有用。然而您希望为他们指定一个名称,以便代码更易于阅读。总的来说,枚举数就是一个整数,会在你的代码中看起来有些不同,还能让你的代码保持干净一些。
如下的代码中,A B C没有分组,在后面你可能会有一个D,或者重新声明A,然后就乱了。我们本质上希望能定义一个类型,只能是这三个数中的一种,而且能够把他们组合起来,这正是我们可以使用枚举的地方。
我们利用枚举法,把类型规定为A,B,C三种。
枚举法中,默认A=0,后面的B和C是累加;如果设置了A的初试值,那么B和C依然是累加。
我们同样可以指定枚举的数据类型
不可以指定为float,因为float不是整数,枚举法要求变量必须为整数
这就是枚举法,它是给特定的值命名的一种方式,这样你就不必在各种地方,处理各种整数。
Log函数中,尅一看到我们可以把Error,Warning,Info使用枚举法归类。
使用int的话,所有int数都可以被放进LogLevel中,而通过设置类型为Level,限制你的代码只允许使用Level枚举数。
我们发现Error有报错,这是因为Error既被用在等级变量又被用在函数里,C++不知道哪个是正确的。
所以我们要给枚举里更改变量名,让它和函数有区别。