C++是如何工作的?
首先来看一个最基本的C++程序段。
#include <iostream>
int main()
{
std::cout << "HelloWorld" << std::endl;
std::cin.get();
}
第一行 #include 的含义是预处理的意思,这条语句的作用是将一个名为iostream的文件拷贝到源代码中这个文件通常被称为“头文件”,之所以要拷贝这个文件,是因为后面要使用到的cout、cin就存在于这个文件中(其实如果不怕麻烦,直接把iostream中的内容手动拷贝到源文件中也可以实现同样的效果),注意:在源代码实际编译之前,这段预处理代码就被执行了。
接下来看main函数,main函数的返回值类型是int,main函数比较特殊,不强制要求写明返回值,这里默认的返回值是0,当然也可以写明返回值,但是这样做没什么意义。
main函数体中,实现了输入(cin)和输出(cout)两个功能,"<<"符号看上去像左移运算符,实际上它被称为“重载运算符”,可以被理解为是一个函数。
代码编写完毕后,编译器会将代码编译成机器能够识别的机器码,可以在VS studio中进行编译选项配置,X64表明编译代码的目标平台,如果想修改运行平台,可以在该栏目中配置,另外解决方案配置中有Debug和release两种选项,在Debug模式下,程序的编译、运行速度会比release慢一些,因为release模式下会对程序进行一些性能方面的优化,而Debug模式则会默认关闭这些优化,但是关闭优化的好处就是我们可以调试代码。
项目中的每个CPP文件都会被编译,但是头文件不会被编译,因为头文件的内容在预处理时就被包含进来了,源文件被编译时,包含进来的头文件也一起被编译了。每个cpp文件都被编译成了一个目标文件,如果使用的是vs编辑器,生成文件的后缀是.obj,有了这些目标文件以后,需要把这些文件合并成一个可执行文件,此时就需要使用链接了,链接就是将所有的obj文件粘在一起,合并成一个exe文件。下面看这个例子,我新建了一个名为main.cpp的源文件,为了让程序看起来简洁一些,我把打印功能单独封装到一个Log函数中,为了防止程序报错,我在源文件main.cpp中声明一下Log函数,虽然我没有真正地实现Log函数,但是下面这段代码可以正常通过编译!
void Log(const char* message);
int main() {
Log("Hello world");
}
但是却无法正常运行!
这是因为在编译阶段,编译器只关心当前的源代码是否符合规范,是否包含词法错误、语法错误、静态语义错误,而不关心当前源文件与其他源文件的关联关系是否正确,这就是为什么虽然没有实现Log函数,但是main.cpp文件却通过了编译,因为编译器发现虽然我们使用了一个“陌生”的函数Log,但我们提前声明了Log函数,这符合C++的规范,因此编译放行,生成了main.obj文件,但在执行阶段,情况就复杂了一些,程序需要被真正地执行,但是程序此时发现Log函数没有函数体,因此就报错了,下面我们简单实现一下Log函数,我再新建一个源文件log.cpp,
#include<iostream>
void Log(const char* message) {
std::cout << message << std::endl;
}
此时程序就可以正常运行了!但是问题是,main.cpp是如何找到在log.cpp中定义的Log函数的位置呢?这时链接器就登场了!
编译器将每个cpp源文件单独编译程.obj文件,main.obj就包含了Log函数的声明信息,log.obj包含了Log的定义信息,
运行时,链接器就将这两个obj合并成一个.ilk文件,因此程序运行时就可以准确定位到声明函数的具体位置了!
下面详细介绍一下编译器和链接器的工作原理。
C++编译器
我们编写的代码实际上就是一个普通的文本文件,C++编译器需要做的就是将文本文件转换成一种被称为目标文件的中间格式,这些目标文件随后被传递给链接器,链接器会完成所有的链接工作。
在生成目标文件时会执行多个步骤,首先需要预处理代码,预处理完毕后,会进入词法分析、语法分析,将英语化的源代码整理成C++编译器可以理解的形式,会生成一个被称为抽象语法树的东西,编译器的最终任务是将所有代码转换为常量数据或指令。
首先在C++中,没有文件的概念(这与java语言有很大不同),文件只是向编译器提供源代码的一种方式,简单来说,如果我们创建的文件后缀名为.cpp文件,C++编译器会默认地将其视为C++文件进行编译,但是如果我们创建的文件名后缀为.happy(我自己乱写的一个后缀名),只要我告诉编译器这是一个C++文件,那么编译器依然会按照C++的规范去编译它,比如:
然后在控制台这样去编译执行它,依然可以执行成功!(但是平时工作中最好不要这样做)
PS D:\cproject\C_Study\demo1> g++ -x c++ -o happy .\demo.happy
PS D:\cproject\C_Study\demo1> .\happy.exe
happy
先来看看编译的第一阶段——预处理,常用的预处理指令包括include、define、if\ifdef、pragma,先来看include指令,这个指令很简单,预处理器会将include包含的文件打开、读取全部内容,并将其粘贴到文件中,就是简单的粘贴复制!用一个简单的例子看一下:
我在main.cpp中写了如下代码:
int add(int a,int b) {
return a + b;
#include "kuohao.h"
然后再kuohao.h中写:
}
看起来很古怪吧?kuohao.h头文件中只要一个右括号,但是没关系,我们的main.cpp依然可以正确编译并运行!因为 #include "kuohao.h"这段代码就相当于把kuohao.h中的内容原封不动地粘贴到了指定位置。
我们还可以让编译器输出一个文件,该文件包含所有预处理器的处理结果,我们可以在属性页中修改选项,将“预处理到文件”修改为“是”,
再次编译 main.cpp,可以看到生成了:
这个文件就是预处理后的C++代码,打开这个文件可以看到:
#line 1 "D:\\cproject\\C_Study\\demo1\\main.cpp"
int add(int a,int b) {
return a + b;
#line 1 "D:\\cproject\\C_Study\\demo1\\kuohao.h"
}
#line 4 "D:\\cproject\\C_Study\\demo1\\main.cpp"
观察完效果后,记得把选项重新设置为“否”,否则编译将不会生成obj文件了!!!
接下来我们打开生成obj文件观察一下:
全部是二进制文件,无法看懂,我们可以在vs设置一下,将“汇编程序输出”设置为如图所示:
输出目录里会生成asm汇编代码文件:
打开这个文件,就能看到相应的汇编代码:
最下面的"add@YAHHH@Z"这段代码,代表的是函数签名,链接器就靠函数签名去寻找函数的。
C++链接器
每个文件都被编译成一个obj文件,这些obj文件无法相互联系和作用,如果obj文件之间存在引用关系,就需要进行一个被称为链接的过程,链接的主要任务是找到每个符号和函数的位置,并将它们链接到一起,即使源文件中都没有使用其他文件,链接器也需要确定main函数的位置,比如如下代码:
#include <iostream>
void log(const char* message) {
std::cout << message << std::endl;
}
int add(int a, int b) {
log("hello");
return a + b;
}
这段代码可以正常编译,但是在运行时就会发生如下链接错误!提示程序没有“main函数”,意味着程序没有入口点。
默认情况下,每个exe程序的入口点都是main函数,但是实际上,我们可以指定一个程序的入口点,这可以在属性页中设置,因此C++程序的入口点不必一定是main。
原文地址:https://blog.csdn.net/qq_31841383/article/details/144287286
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!