前言:我们都知道,一般我们所写的程序都是经过编译和链接后才生成可执行程序的,那编译和链接过程都干了些什么呢,今天我们来讨论这个问题。
本文中,我们的重点是:
程序的翻译和执行环境
C语言程序的编译+链接
预定义符号介绍
预处理指令#define
宏和函数的对比
预处理操作符#和##的介绍
命令定义
预处理指令#include
预处理指令#undef
条件编译
话不多说,正文开始~
我们的环境基础是VS2022和vscode(gcc),不同编译器可能会有所差异~
目录
注:以上的都是比较笼统的解释,来自《编译原理》的一本书的知识也不可能通过一篇博客就写完,还请读者谅解~~
1.程序的翻译环境和执行环境
我们编写程序在生成可执行程序之前要经历将我们的代码翻译成计算机可识别的机器语言,接着进入执行环境,用于实际执行代码。
我们的终点是来详细了解编译和链接过程:我们给出下面的一个程序:
每一个源文件都会经过编译器的单独处理生成目标文件
符号汇总:将每一个源文件中的全局符号都汇总起来,注意,这里一定是全局符号,像函数名,全局变量等都是全局符号,函数内部的变量不是全局符号,我么你在编译过程中不会执行源文件程序。
形成符号表:将每个源文件的符号汇总都分配一个地址形成各自的符号表,因为我们的每个源文件的编译过程都是独立的,所以每个源文件所包含的外部链接(包括外部函数等)的信息都会被分配给一个无效地址,即不会与其他源文件产生链接。(注意:static修饰的静态变量可以将修改外部链接属性为内部链接属性,就是因为没有加入符号表所致)。
符号表的合并和符号表的重定义:将每个源文件生成的符号表进行合并并检查相同符号,将无效地址统一查找替换为有效地址(也就是源文件在符号汇总时分配的地址),这一阶段可以检查对应的函数声明是否出现问题,跨源文件的符号信息是否都被成功的替换为有效地址。
所以,对上述的程序,我们可以给出他的符号汇总->形成符号表->符号表的合并和符号表的重定义的流程图示:
还要注意,我们的汇编代码并不是二进制代码,这和计算机语言的发展有关:
2.程序的执行过程
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)(这里也就是我们了解到的函数栈帧的相关知识,详情请移步函数栈帧的创建与销毁)存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
注:以上的都是比较笼统的解释,来自《编译原理》的一本书的知识也不可能通过一篇博客就写完,还请读者谅解~~
3.预处理详解
3.1 内置预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
//注意上述符号有些编译器可能会存在不支持现象
3.2 #define
3.2.1#define定义的标识符
#define MAX 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符,相当于将换行符转义)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
注意:define在定义时最后不要加上;,可能会出现错误
3.2.2 #define定义宏
这个点还是比较重要的,所以这里重点展开讲一下:
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
1.参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
2.宏的参数不会经过计算,直接回替换到需要使用宏的位置上去,所以我们要注意对一般的参数,在宏里使用尽量加括号,并且整体也要加上括号。
3.2.3 #define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
3.2.4 #和##
1.使用 # ,把一个宏参数变成对应的字符串。
我们来看一段具有相同功能的代码:
我们的想法是用宏定义来简化代码,但是我们看出这三个printf有三处是有差异的,我们可以用宏定义实现并且用#来实现输出对应的参数的字母(注意我们不加#numvs会直接报错):
2.##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
这个了解即可,实际场景一般用不到。
3.2.5 宏和函数的对比
宏通常被用于执行简单的计算
宏对比函数的优势:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。(函数要涉及传参和函数栈帧的开辟及销毁以及其主要的计算,可能会浪费更多的时间)
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。 宏是类型无关的。
但是宏也是有缺点的:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。
2. 宏是没法调试的(因为其在编译的过程中已经执行了相应的替换)。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
5.注意,宏是不能递归的,但是函数可以。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
我们给出如下的例子:
总结上来说,如果要实现的功能比较复杂,建议用函数,否则就用宏定义来实现。
拓展:内联函数inline
c和c++中引入了内联函数inline,既有函数的优点,又有宏的有点,这里不展开讲解,下次一定~~
3.3 #undef
功能:用于移除一个宏定义
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
3.4 命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个 程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器 内存大些,我们需要一个数组能够大些。)此功能需要在gcc环境下实现
3.5 条件编译
先给出一个引例:
常见的条件编译指令:
1. #if 常量表达式 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif 2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif
3.6 文件的包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方 一样。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
3.6.1 头文件包含方式
我们都见过,头文件的两种包含方式:
#include “filename”和#include<filename>
两种方式可以替换使用,但是还是建议标准头文件用<>,而我们自己写的头文件用“”。
但是多文件编译时不免会出现多次编译同一个头文件,这里我们有两种方式去处理:
1.条件编译
每个头文件的开头写:
#ifndef __TEST_H__ //如果没有被包含
#define __TEST_H__ //包含头文件
//头文件的内容
#endif //__TEST_H__
2.或者
#pragma once
4.金句省身
安静的去做,成功了再说
努力成为你最喜欢的那种人,就算不成功,至少你喜欢现在努力的自己。停止精神深处的无意义的内耗,你要留点精力去读书,去运动,去奔赴你想要的生活。
不要觉得独来独往很奇怪,沉淀的日子总是安静无声的,也不要怕努力了没有一个好结果,提前焦虑只会加重你的负累,还没到最后你怎么知道自己不行。
把消极的态度扔掉,把拖延的毛病改掉,认准了一条路就不要打听它有多远,只管大胆的向前走,你踏出的每一个脚印都会成为你日后的经验和底气。