C++学习:编译

编译

为了编译 C++ 程序,我们使用 C++ 编译器。C++ 编译器按顺序遍历程序中的每个源代码 (.cpp) 文件。

它会检查您的代码以确保它遵循 C++ 语言的规则。如果没有,编译器会给你一个错误(和相应的行号)以帮助确定需要修复的内容。编译过程也将中止,直到错误被修复。

将源代码转变为可执行的二进制主要需要两个步骤,第一是编译,第二是链接。编译主要是将源代码转变为中间形式,obj,目标文件,然后目标文件经由链接器连接,生成了可执行代码。

对于编译过程而言,首先要做的事情是预处理,预处理对所有的预处理项进行相应处理,然后进行分词和语法处理,将人比较容易看懂的代码生成编译器比较容易处理的语法树,然后将所有的代码要么生成常量,要么生成指令。一旦生成了语法树,就可以进行代码生成了。

对于C++语言,每个.cpp文件就是一个翻译单元,每个.cpp文件都会被翻译成一个.obj文件。

Main.cpp

#include <iostream>

void Log(const char* message);

int main()
{
	Log("hello,wrold");
	
	std::cin.get();

	return 0;
}

Log.cpp

#include <iostream>
void Log(const char* message)
{
	std::cout << message << '\n';
}

例子中的两个源代码log.cpp和main.cpp大小都不大,但是生成的.obj文件体积较大.
在这里插入图片描述

在这里插入图片描述

这个原因主要是#include 所导致的。如何理解#include这个预处理,创建一个新的小的cpp文件,math.cpp

int Multiply(int a, int b) {
	int result = a * b;
	return result;
}

这个cpp文件没有任何的#include或者其他的预处理项。来看下编译的第一步,预处理。常见的预处理项包括#define,#include,#if, #endif等。先看一下最常见的#include。#include非常简单,它指定了你想要包含的文件,预处理器打开那个文件,阅读里面内容然后要包含的内容贴贴到文件中。看一个例子,创建一个新的头文件,EndBrace.h,在这个文件中就只有一个右大括号

在这里插入图片描述

按ctrl+F7进行编译报错

在这里插入图片描述

左边的大括号没有被匹配。因为我们确实将右括号给删了。

接下来,用#include预处理来处理
在这里插入图片描述
在这里插入图片描述

现在编译的话,完全没问题。原因就是预处理的时候,编译器就是取打开了EndBrace.h,然后将它里边的内容拷贝到了math.cpp,这样的话,大括号就匹配完成了。

在这里插入图片描述

接下来看的更细致一点,修改一下VS的配置,让它输出编译的过程中产生的预处理的结果。右键点击项目,选择属性,将预处理到文件的“否“,改成“是“。

ctrl+f7重新编译,此时可以在debug目录下,发现math.i。打开math.i,可以发现内容如下:
在这里插入图片描述

通过上面的操作,应该能够理解#include的作用。

最后看一下#include 的效果。可以看到Math.i多达五万多行,而这么多代码,主要就是iostream的内容。

在这里插入图片描述

接下来,恢复配置,不再输出预处理代码,预处理代码都删去;剩下的cpp代码。

int Multiply(int a, int b) {
	int result = a * b;
	return result;
}

观察一下目标文件。如果直接使用VS打开math.obj,那么可以看到都是二进制。这时候可以修改一下配置,让生成obj的同时输出asm,好看一点。可以通过项目 -> 属性 -> c/c++ -> 输出文件

在这里插入图片描述

此时,可以生成.asm文件,打开ams文件

在这里插入图片描述

先不要太在意这些汇编代码细节,这个例子讲编译器优化。在上面的Line2的代码中,可以看到首先将变量a保存在寄存器eax中,接下来看到 imul 指令,这里就是将变量a和变量b进行相乘,并且将结果保存在eax中。

然后将eax中的值放在变量result中,最后将result的中又放回了eax。因为eax存放的是函数的返回值。

上面看上去重复的动作主要是因为math.cpp中声明并返回了result变量。这就是如果我们不要求编译器进行优化,那么,编译器产生的代码会很慢。

可以修改一下math.cpp,看一下生成的asm,进行一下对比:


int Multiply(int a, int b) {
	return a*b;
}

生成的汇编代码会得到精简但是不明显。

再看一个例子,如果如下修改math.cpp:

int Multiply() {
	
	return 2 * 5;

}

此时可以看到,直接将10移入了eax。这里主要是想说明 常量折叠 (constant folding)的概念。也即,如果代码中有常量,那么在编译的时候就已经计算出了这个常量,并不会将计算遗留到运行时。

在这里插入图片描述

下面看一个函数调用的例子

const char* Log(const char* message) {
	return message;
}


int Multiply(int a, int b) {
	
	Log("multiply");
	return a * b;

}

这里的Log并没有什么用,只是用来演示一下函数调用。当查看汇编码时,可以看到在对应于multiply函数的汇编码中,有call指令,以及之下有imul指令

在这里插入图片描述

Log函数名字之后跟着的乱码和@@ 称为函数的签名,便于链接器定位到函数。这里的代码其实有冗余,Log函数基本没有任何作用。我们可以试一下优化

在这里插入图片描述

如果遇到和RTC1冲突,可以修改

在这里插入图片描述

这样的话, 可以看到生成的汇编码将 call Log完全地给优化掉了

在这里插入图片描述

通过以上的例子,对编译器的工作应该有了基本的了解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值