程序预处理、编译、链接流程

一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。

1 预处理之前的翻译处理

在预处理之前,编译器必须对该程序进行一些翻译处理。

1.1 编译器把源代码中出现的字符映射到源字符集。

该过程处理三字符序列和多字节字符。

如三字符序列:

int arr??(3??) = {1,2,3};

处理为:

int arr[3] = {1,2,3};

源文件字符集:源文件本身也是文本文件,所以源文件字符集是指源文件保存时采用哪种字符集编码。VC++下源文件默认是gbk编码,如果想要更改,可以通过 文件-高级保存选项 修改某个源文件的编码方式,似乎没有什么选项能够设置创建项目时的源文件编码,需要自己一个一个设。

编译器字符集:编译器在读取源代码文件时所使用的内部字符集决定了编译器如何把读入的源代码文件字节流进行转换,转换是指从一种字符集编码的字节经过解码再编码到另一种字符集编码的字节。当然编译器内部采用什么字符集不是我们所关心的,是编译器内部的事情。

执行字符集:编译器在编译时会将 字符/字符串 常量从上一步编码得到的字节转化为相应的字符集,转化为哪种字符集决定了程序在运行时这些字符串采用了哪种字符集编码,举个例子:

printf("你好");

程序在执行时 "你好" 这个字符串在内存中保存的是哪种字符集的编码呢?就由执行字符集决定。

那么,编译器到底会转化为哪种字符集呢?分2种情况:

1. 如果是窄字符/字符串”“(以char为单位),那么不同的编译器可能不一样。以VC++为例,它是由系统代码页决定的,比如在中文windows系统下就采用GBK编码。

2. 如果字符/字符串前有指定编码方式,那没什么好说的了,就采用指定的编码方式,如下:

char* s1     =  u8"hello"; //窄字符串,utf-8编码 (C++11)
wchar_t* s2  =  L"hello";  //宽字符串,utf-16编码
char16_t* s3 =  u"hello";  //宽字符串,utf-16编码 (C++11)
char32_t* s4 =  U"hello";  //utf-32编码 (C++11)

//'\x12' : \x后面接2个16进制数字,可表示一个窄字符char,多个\x连起来可表示一个utf-8字符,如"\xE4\xBD\xA0"

//L'\u1234' : \u后面接4个16进制数字,可表示一个utf-16宽字符

//U'\U12345678' : \U后面接8个16进制数字,可表示一个Utf-32

VC++里如果想要强行改变执行字符集,可以加上一句预处理:

//设置执行字符集为utf-8
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif

控制台输出乱码的问题解决

控制台的代码页默认是和系统代码页是一样的,中文windows下就是GBK,所以你用输出流函数(printf,wprintf,cout,wcout等)输出GBK编码的字符串肯定没问题,现在讨论2种情况:

输出宽字符串,如果直接输出是会乱码的。这里有一个函数:setlocale(),是用来程序运行时设置当前系统的区域信息。而在所有C++程序启动前,locale的默认设置setlocale(LC_ALL,”C”);会被执行,这个”C”肯定不是能支持中文的这样一个环境,而宽字符串在输出时会根据setlocale()的设置将其转为相应区域代码页的编码,当然就会出错了。所以我们可以手动设置一下locale信息就可以了:

#include <locale.h>
// 使用""的话就从当前系统获取代码页,当然也可以设成具体的,比如"chs"
int main()
{
    setlocale(LC_CTYPE,"");
    wprintf(L"你好");    // 这样就不会乱码了
    //...
}

对于窄字符串来说,是不会作编码转换的,所以如果你输出GBK编码的窄字符串,那是没有问题的,但如果是utf-8编码的窄字符串呢?显然又会乱码,这时只能修改控制台窗口的代码页了:

//经过测试发现没有乱码,但是鼠标点到字上时会变乱,可能需要设置字体

system("chcp 65001");
printf(u8"你好");

1.2 将物理换行转换成逻辑行

编译器定位每个反斜杠后面跟着换行符的实例, 并删除它们。也就是说, 把下面两个物理行(physical line):

printf("That's wond\
erful!\n");
转换成一个逻辑行(logical line):
printf("That's wonderful!\n");

注意,在这种场合中,“换行符”的意思是通过按下Enter键在源代码文件中换行所生成的字符, 而不是指符号表征\n。

由于预处理表达式的长度必须是一个逻辑行, 所以这一步为预处理器做好了准备工作。一个逻辑行可以是多个物理行。

1.3 特殊文本序列的处理

编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。这里要注意的是, 编译器将用一个空格字符替换每一条注释。因此,下面的代码:

int/*这看起来并不像一个空格*/fox;

将变成:

int fox;

而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。

2 预处理

预处理器查找一行中以#号开始的预处理指令。预处理包括宏替换、文件包含、条件编译、以及其它的一些预处理指令(如pragma)。

3 编译

编译器完成源代码到目标代码的转换。对于于强类型语言(如C语言)的编译器,其类型检查在编译阶段完成。

对于函数原型的声明,就是告诉编译器该函数的存在,也就是说,即使没有函数的定义,但有函数原型的声明,编译器也不会报错。当然,到了链接阶段,链接器会去查询函数的定义,此时就需要函数的定义了。

编译器将汇编或高级计算机语言源程序(Source program)作为输入,翻译成目标语言(Target language)机器代码的等价程序。源代码一般为高级语言 (High-level language), 如Pascal、C、C++、Java、汉语编程等或汇编语言,而目标则是机器语言的目标代码(Object code),有时也称作机器代码(Machine code)。

高级计算机语言便于人编写,阅读交流,维护。机器语言是计算机能直接解读、运行的。

对于C#、VB等高级语言而言,此时编译器完成的功能是把源码(SourceCode)编译成通用中间语言(MSIL/CIL)的字节码(ByteCode)。最后运行的时候通过通用语言运行库的转换,编程最终可以被CPU直接计算的机器码(NativeCode)。

5 链接

链接器能够将各文件以及需要的库的代码全部链接到一起。

本页共39段,1528个字符,3345 Byte(字节)