C/C++相关概念和易错知识点(8)(预处理、编译、链接)

本文详细介绍了C语言程序从预处理、编译到链接的全过程,涉及预处理指令、头文件引用、条件编译、宏定义与替换,以及编译生成的.o和.exe文件的形成过程。

前言:这篇文章将从程序运行的顺序讲解预处理、编译、链接相关的知识点。重要分享预处理相关的知识和感想。后面可能会针对这三个知识点进一步分享

生成可执行程序全过程

.c文件在翻译环境中处理生成可执行程序。电脑是不能看懂我们写的代码的,所以需要翻译环境来将我们写的代码转换为可执行的程序,可以将翻译环境理解为我们日常生活中使用的翻译软件。在C语言相关概念和易错语法(1)中我就讲过我们写的代码转化为可执行程序之间需要经历的过程

简单从图中进行一些解释:

(1)在Linux环境下编译生成的是.o文件,在Windows环境下编译生成的是.obj文件。.o(.obj)文件已经是二进制文件了。同样,.exe文件也是二进制文件

(2)编译过程包括:预编译、编译、汇编,具体操作后面会着重讲解

(3)链接库里面存储的是与程序运行相关的函数,它保证我们可以正常生成.exe文件并能正常运行。

正文

编译过程:预编译、编译、汇编

一、预编译(预处理):

1.预处理进行的操作:预处理将所有(除#pragma)含#的指令、注释全部清除,包括#include,#define,#if等。具体处理方法是删去没必要的(如#if判断为假的代码块和注释),展开引用的头文件,生成.i文件,.i文件的内容还是C语言,我们依然能够看懂。

2.预处理相关的知识:

(1)展开头文件有两种表达方式:第一种是<xxx.h>直接到标准库的目录里去查找相关的文件,找到了就拷贝一份到该文件,找不到就报错;第二种是"xxx.h",先在我们所写的.c文件的同级目录下找是否有相应的头文件,如果没有,再到标准库里去找,再没有就报错。

我们由此可以知道为什么自己写的头文件要用"xxx.h"来引用,本质上是两种书写方式导致的查找方法不一样。但是,要注意尽量引用标准库的函数时包含的头文件用<>形式书写,这样可以有效提高代码运行速度,且易于区分。

(2)条件编译:条件编译和条件表达式相似,具体有如下几种

①分支表达:

定义判断:

注意事项:注意使用以上编译都要以#endif结尾保证结构完整。

注意条件编译(所有预处理指令)的执行先于main内部程序,所以要避免以下的错误:

因为#if后面的常量表达式为0(假)的话,#if与#endif之间的代码会在预编译期间被直接删除掉,在这里我们可以看到,#if后面的常量表达式在预编译阶段执行时,a还没有被赋值为10,所以为假,表达式结果是0,和下面效果一样:

③相关应用

以offset为例子,offset用于计算结构体的偏移量,offset其实是个宏定义,里面就有用到条件编译:

通过这里我们也可知道,在C和C++里面的offset并不完全相同。

(3)#pragma的两个常用之处

①  #pragma pack( num )

用于设置默认对齐数,#pragma pack( 1 )相当于没有对齐数这一概念了。

在一个程序里,经过实验,如果两个pragma pack间没有多余的代码(不含预处理指令),以后一个pragma pack为准;如果有多余代码,以第一个pragma pack为准。

针对上面代码举例子:


#include <stddef.h>

#pragma pack()

#include <stdio.h>

#pragma pack(1)


struct Stu
{

	char name[15];

	short age;

	float score;

}s1;


int main()
{
    printf("name偏移量是%d\nage偏移量是%d\nscore偏移量是%d\n", (int)offsetof(struct Stu, name), (int)offsetof(struct Stu, age), (int)offsetof(struct Stu, score));

	return 0;
}

结果是:

②  #pragma once

在打开一个.h文件时,开头的第一句话就是这个,意思是防止头文件重复引用,这样在预处理结束后的代码过于冗杂,甚至出现错误

它相当于以下的代码:本质上都是防止重定义或重引用

顺便一提#undef是删除宏替换的意思而不是“不定义”的意思,如果要用到“不定义”的功能,需要在#if用否定方式描述,再用#define来操作

试着解释一下以下代码:

注意事项:#pragma有的指令是针对编译器的,比如使用#pragma pack(1)来控制默认对齐数,使得结构体,共用体实质上取消对齐,这影响到分配内存的规则,所以这种含#的指令在预编译阶段是不会删除的。

(4)宏替换和宏定义

宏替换:

①需要注意的一点就是宏替换会直接把你设置的内容置换,而中间不会做任何处理,因此当看到#define MAX 1+1时,不能把它想象成#define MAX 2。如果你想要达到这个效果,要勤加括号,最好在替换的内容的最外层都加一个括号,#define MAX (1+1),这样能最大程度避免出现BUG。

②由于它的直接替换的特征,有下面一个非常易错的代码,同时也涉及指针的特点:

先看一下下面的代码:



#define INT int*

typedef int* INT_PTR;

int main()
{

	int* a, b;

	INT c, d;

	INT_PTR e, f;

	return 0;
}

其中很多人会误以为6个变量都是int*,其实不然。

只有a,c,e,f是int*,这个细节在于对指针表达的理解。在指针的章节我就分享过:*在不同的地方应该有不同的理解,有的时候int*要合在一起理解,有的时候要拆开随着变量作为一个整体去理解。这里拆开理解更好:

int    *a    ,    b

相信这样划分就能很好的理解为什么b是int而不是int*,因为只有一个*,被a占用了,所以b只是个int。宏替换的情况也是如此。那么如果我想要让b也是int*,则怎么处理呢?

这就要利用上面的e和f的处理方式了。INT_PTR被用于整个替换int*,相当于创建了两个INT_PTR的变量e和f,因此两个都是int*。

在这里,我们应该进一步了解到*(指针的符号)在C语言中的特殊性了,很多地方比较反直觉,需要在不同的地方对它进行不同的解读。

另外,不要将*作为宏替换末尾的符号,因为这往往达不到你想要的效果

*会被自动拆开作为INT替换的那部分

这里可以看到它被替换为* int*而不是int*

宏定义:

①两种书写符号#和##

#写在前面,相当于我们传什么,就把什么原样转换成字符串


##写在后面,举个例子:在我们传了type后,当type和其它字母(符号)相连时,type一般不会被替换,当在type后加上##时,它会和后面的字符连在一起并且会被我们传的type替换:

反例:

(5)C语言中还有一些预定义符号,如__DATE__,__TIME__,特征是中间的符号都是大写,左右分别由__划分,这类符号很多,要学会识别

二、编译,汇编,链接

1.编译

编译生成的是.s文件,它主要是将我们写的代码进行进一步检查(词法,语法,语义等分析),词法语法问题会在这个阶段被发现。同时,编译会将我们写的代码转化成汇编代码文件,这种文件的内容我们已经看不懂了。

2.汇编

生成.o(.obj)文件。这个阶段将汇编代码文件转换成了二进制文件,每一条汇编语句对应一条机器指令,不过这个时候生成的文件不止一个,需要经过链接才能最终生成可执行程序。

3.链接

它将汇编生成的文件结合链接库生成.exe文件,期间我们所写的函数会在这一步被定位。因此,如果没有定义函数,在这里会被找到。

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值