5 c++如何工作
源文件就是文本文件转化到可执行的二进制文件或者程序,然后源文件会传到编译器,然后编译成二进制文件(binary)
#include<iostream>
int main()
{
std::cout << "hello world " << std::endl;
std::cin.get();
}
5.1include
首先这其中· #include称为预处理文件,任何以#开始都是预处理文件,当编译器收到一个源文件时,它第一件事就是预处理你所有的预处理指令。这就是为啥·叫预处理,因为整的发生在编译之前。 这里用include,include就是找到一个文件,这里是找一个叫iostream,该文件里所有内容会被复制到目前这个文件里。 你include的文件被称为头文件,我们之所有include头文件iostream是因为我们需要一个函数(function):cout的申明。cout可以打印东西到控制台console,然后我们有了这个main函数,main函数相当重要。
5.2main
main函数被称作入口点(entry point)也就是说我们运行程序时,计算机会从该函数里的代码开始执行。 这其中main的定义是int,然而我们没有返回一个整数(integer)。 这是因为main是个例外,你不需要从主函数返回任何类型的值,如果你不返回任何值,它会返回0,这只适合于main函数
5.3 <<
这些向左的带角的括号,看起来像左移位符号(bit shift),其实是被重载的符号,你得把它想成一个函数,我知道他看起来像运算符,但实际上,运算符就是函数,所以这种情况下和cout.print一模一样 这里目的即使把hello world 传入 cout,cout把它打印在控制台里。然后传入endl,end line告诉控制台前进到另一行。
5.3 linker链接器
1首先 include iostream,即使预处理,会在编译文件之前被评估。即使把iostream中内容拷贝到这个文件里。 2当我们头文件被评估后,我们的文件会被编译,这个阶段,编译器会把我们的c++转化成实际的机器码。 所有cpp文件都会被编译,而头文件不会,只有cpp,头文件在预处理被include到头文件里,那是他们被编译的时候,所以我们有一堆cpp文件被编译,而且它们是一个个被单独被编译的。每个cpp文件会被编译成一个object文件,如果扩展名的话,用Windows编译器是.obj,当有了一个个obj时候,也就是cpp被编译过后的结果,我们得有办法把他们联系起来,组成exe。这时我们得朋友linker(链接器)就有用武之地 你可以在linker标签下看它的设定,但基本就是把所有obj拿来的然后联系起来 linker的作用是把所有obj连成exe, error list 的工作原理是,解析(parse)output窗口,找error关键字,然后从哪找信息,放入list,功能简单,output功能比较全面 此处双击问题即可到发生错误的地方。 当你单独编译一个文件,linking显然不会发生,因为你单独一个文件。 如果我们有多个c++文件呢? 假设我们有打印hello world 到控制台,但不想用cout,用自己的logging函数,这就需要包装logging函数
我们来写log函数,会接受cstring作为参数,取名,message,并打印message到控制台。 此时我们可以把log放到另一个文件 新建一个log.cpp会发现cout不是STD的member,这基本上说不懂cout是啥,这要是没有吧cout声明include进来。 c++中每个symbo都需要某种声明,cout是main里include文件里定义的一个函数,也就是iostream 把#include剪切入log.cpp再次编译成功了 回到main我想调用log函数,ctrl+f7编译失败,不知道cin,加入#include,会发现log还是找不到,我们把函数从一个文件移动到另一个。 发生错误的原因是它识别不出log,所以我们需要通过一个声明(declaration)来解决。声明从字面意思,就是声明log是一个存在的东西。有点像承诺,告诉编译器有一个函数叫log,编译器就真的相信了。因为编译器根本不关心resolving(解析)log到底被哪定义的。 这里有两个词,declaration(声明)和definition(定义)申明就是这个symbol真的存在,定义是说这个函数到底是什么,这个函数的主体。 这中间有个疑问,如何能确定有这个函数,这时候link就有用了。 当我们文件被编译后,linker就会去找log的定义,然后跟我们main里调用联系起来。 如果找不到定义,就会得到linker error 我们可以看到两个obj因为,每个编译cpp都会产生obj,然后linker会串成exe
7 C++链接器linker是如何工作的
linking是从c++源码到二进制可执行文件的一个过程。 我们编译后会通过一个叫做链接的过程,链接的主要工作是找到每个符号和符号的位置并把它们链接在一起。 我们需要一种方法将这些文件链接到一个程序。即使没有外部文件里的函数,比如你已经把一个程序写在一个文件里了。应用程序仍需要知道入口点在哪里
7.1案例
在vs中我们有一个非常简单的项目,只包含一个源文件math.cpp. 在这里有两个函数log和multiply,multiply实际上调用了log函数,打印出multiply这个单词到控制台。然后返回a*b 编译有两个阶段-编译和链接。 你可以对其进行区分,如果你按ctrl-F7或者按下编译按钮,只有编译会发生链接完全不会发生。然而如果你build你的项目或者按F5运行它会编译然后链接。 如果你按crl+f7可以发现没发生任何错误 如果你按build会发现得到一个链接错误,缺少入口函数,我们的主函数。 因为我们的编译分为两个阶段,如果编译中出现错误,错误会以c开头,如c243. 如果链接中出现问题会以LNK开头 你在文件的属性,可以知道配置类型是exe,exe必须要有一个入口点。 在一个文件中即可build成功。 如果这些函数存在多个文件中。如把log函数加入log.cpp中,再执行会报错,因为找不到名为log的函数 所以把声明加入math.cpp中可以发现 编译成功
10 头函数
在C++基础上,头文件传统上是用来申明某些函数类型,以便用于整个程序中。在链接器章节中我们知道,为了让我们知道函数和类型的存在,我们需要某种声明。 我们需要一个共同的地方存放函数声明,只是声明,没有实际的定义,没有函数的主体。
10.1案例
假设在主文件生成一个c++文件定义了Log函数 main.cpp
#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.cpp中使用了Log函数,我们拿到一个错误,log函数不再该文件中,该文件不知道log函数的存在。我们在main中可以正常使用,在log.cpp中会报错。所以log.cpp到底需要沙漠才不会报错。 如何才能告诉它,log函数存在,只是区别于别的地方。 这时是函数申明的使用之地。 加入函数声明后可以正常使用按ctrl+f7可以编译。对项目编译也可以看到链接成功,因为可以找到log函数。 我们告诉了log.cpp那个函数存在某处,如果我们创建另外一个文件呢?我们创建一个别的文件,然后使用这个log函数呢? 这意味这个void log声明可以到处复制黏贴吗? 答案是的,你确实需要怎么做,但是有一个方法可以让一切简单一点。 就是头文件
10.2头文件
头文件一般被include到cpp文件里面,基本就是复制和黏贴进cpp文件里。我们创建一个头文件,上图中的文件夹都是过滤器并不是真正的文件夹。我们可以在资源文件下创建一个头文件。其实就是无论在什么地方创建头文件都没关系 此时我们将头文件剪切放在这里。我们的头文件log.h,我么可以在任何想用log函数的地方include它,然后显然他会帮我们做哪些我们不想人工做的事。 其实我们还可以include“log.h”到main.cpp,main.cpp包含实际的函数定义,所以我们并不需要它,我们反正可以调用log。但加进去也不会报错。
如果我们在main中调用initLog会报错所以我们需要去log头文件把函数签名加进去。
void Log(const char* message);
void initLog();
此时可以在main中调用
10.3pragma once
pragma once意思是只include这个文件一次,是一种头文件保护符 他所作的是防止我们把单个头文件多次include到一个单一翻译单元中。 其实不会防止我们把头文件include到程序各处,只防止到一个翻译单元里,也就是一个单独的cpp文件,原因是我们不小心把一个文件多次include到一个翻译单元中。我们会得到一个重复的错误,因为我们会多次复制和黏贴那个头文件。一个比较好的办法是我们创建一个Struct 我们可以创建一个struct player{},如果我们把这个文件,两次应付得到一个翻译单元,并且没有头文件保护。他会真的include这个文件两次,也就是我们会有两个structs,他们有相同的名字player。 如果我们去掉#pragma once 并如下图引入两次Log.h。 我们会得到一个player structure重复定义的问题。 也许会觉得自己不会干这个种事,但是c++种会出现套娃的问题。 比如有一个头文件player include log,然后player 又被inlude到其他文件。假如我们创建一个common也包含log.h。如下图把pragam once 注释掉 此时main中也会出现同样问题。 如果log.h不再一个文件夹中,比如它目前存在log.cpp的上一级目录中,可以使用…/来返回上一级目录 iostream“”也可以用来告诉编译器位置,可以在任何地方用,我们可以把iostream换成引号,将完全没有问题。所以方括号只用于编译器include路径,引号用于所有。但我一般只喜欢将它用于相对位置,还不如方括号。
25构造函数
25.1为什么需要构造函数
构造函数是每次在实例化时运行 这里实例化Entity并为他分配内存,实际上没有初始化那个内存,如果在main函数中访问X会报错 未初始化局部变量 初始化init函数会有很多多余的操作也不清爽。
25.2构造函数
构造函数是一种特殊的方法,每次构造一个对象都会调用的方法。构造函数没有返回类型且名称必须与类的名称相同。在java和python中会初始化为0但是c++不会,需要手动初始化所有基本类型,否则会设置为留在内存的其他值。 当写一个同名方法,叫函数重载,即有相同的函数名,但不同参数对应不同的函数版本
25.3不使用构造函数
25.3.1方法1放入private
例如一个类只有静态方法,我们只想让其使用静态方法,不希望创建实例,放入private可以发现不能访问构造函数
25.3.2方法2使用delete
34const
const基本就是做出承诺,承诺某些东西是不变的。 它只是一个可以绕开的承诺,你可以绕开承诺 这个承诺好处是可以简化很多代码
34.1 常数
这里是无法改变的,做出承诺从语法上说这个整数是一个常数
34.2指针
当你声明一个指针,没有用const会在堆上生成,这里我可以做两件事,逆向引用a,然后将它设为一个值比如2,然后我们可以做另一件事就是重新分配实际的指针,这样他就指向别的地方,比如&MAX_AGE为了绕开const限制 这里我们可以做两件事,我们可以改变指针内容,我们也可以改变指向的地址
1const 放在*前 不能修改指针指向内容
const int* a=new int
这里尝试改变a指向内容会发生错误,当改变a本身是不会报错的 2 const放在*后 我可以改变a的指向内容,但我不能把实际指针本身重新赋值,指向别的东西 这两个效果是一样的,关键是 * 前还是 * 后 3 *前后都有 不能改变a的指向内容,也不能把实际指针本身重新赋值,指向别的东西
34.3类和方法
放在函数名后
这个方法不会修改任何实际类,我们不能修改类成员变量 不能再setter中用函数名后的const,这样我们不能修改类中的成员变量,通常用在GetX中 如果m_x是一个指针,想让它保持不变,这样会写三个const,这意味着返回了一个不能被修改的指针,指针内容也不能被修改,这个方法承诺不修改类中变量
34.4引用
引用没有指针本身和指向内容的区别了,因为引用就是内容 如果把const去掉会报错,因为无法确定GetX函数不会写入Entity类,有时候会写两个GetX一个带const,一个不带
34.5mutable
当函数为const,但你有些调试的参数可以用mutable。 mutable允许函数是常量方法,但可以修改变量
|