| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 程序的编译和预处理~收藏了回家慢慢看 -> 正文阅读 |
|
[C++知识库]程序的编译和预处理~收藏了回家慢慢看 |
目录 1. 程序的翻译环境和执行环境在ANSIC的任何一种实现中,存在两个不同的环境。 2. 详解编译+链接2.1 翻译环境?组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。 2.2 编译本身也分为几个阶段:
gcc 指令的一般格式为:?
C程序?目标文件和可执行文件?结构 目标文件和可执行文件可以有几种不同的格式,有ELF(Excutable and linking Format,可执行文件和链接)格式,也有COFF(Common Object-File Format,普通目标文件格式)。 虽然格式不一样,但具有一个共同的概念,那就是?段(segments),这里段指二进制格式文件中的一块区域。 linux下的可执行文件有三个段:(?可用?nm?命令查看目标文件的符号清单?)
??预处理 (?gcc -E?)
所以当我们无法判断?宏定义是否正确?或?头文件包含是否正确?时,可以查看已编译后的文件来确认问题。比如:hello.c 中第一行的 #include<stdio.h>命令告诉预处理器读取系统头文件 stdio.h 的内容,并且把它直接插入到程序文本中,结果就得到了另一个C程序,通常是以 .i 作为文件扩展名。在该阶段,编译器将 C 源代码中的包含的头文件如 stdio.h 编译进来,用户可以使用 gcc 的选项 -E?进行查看。
使用 gcc?-E 参数完成。 预处理会干什么事情:
?处理完成之后看看我们的 Hello.i,发现原来8行代码现在变成了接近700行,因为将 <stdio.h> 的文件被替换进来了,在最后几行找到了我们自己 Hello.c 的代码: 使用系统默认的预处理器 cpp 完成。 预处理除了使用 GCC -E 参数完成之外,我们还可以使用系统默认的预处理器 cpp 完成。如下所示 我们看看Hello.ii的代码: 虽然 Hello.i 和 Hello.ii 的代码对应的行数不同,但是内容却是一模一样的,只是中间空行的数量不同而已。 OK ,接下来,继续向编译出发。 编译 (?源文件?转换成?汇编代码?)gcc -S 编译是将?源文件?转换成?汇编代码?的过程,具体的步骤主要有:词法分析 ---> 语法分析 ---> 语义分析及相关的优化 ---> 中间代码生成 ---> 目标代码生成(汇编文件.s)。 具体生成过程可以参考《编译原理》。在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。 用户可以使用?-S 选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
注意:gcc 命令只是一个后台程序的包装,会根据不同的参数要求去调用预编译编译程序cc1(c)、汇编器 as、连接器 ld。 使用 gcc?-S 参数完成。 查看 Hello.s 发现已经是汇编代码了。 使用系统默认的编译器 cc1 完成这个过程。 前面的预处理命令? 倒数第二个命令就是? 并没有? 看上图第二条, 有可执行权限,那为何不试试能不能用来编译? 好像没有什么报错,迫不及待的看看? 发现和? 汇编
使用 gcc?-c 参数完成。 其实也可以查看下 Hello.o 的内容: 只是乱码罢了。要是想看,我们可以使用 hexedit, readelf 和 objdump 这三个工具。 hexedit 只是个将二进制文件用十六进制打开的工具,我们执行:
可以看到: 最右边是源文件被翻译成可见字符,点.表示的都是不可见字符。这样看当然没有多大实际意义,但是一些输出的字符串 Hello World,包括整个文件的类型 ELF 都是可以看到的。 readelf 和 objdump 我们后面再说。 使用系统默认的汇编器as完成。 hexedit 看看 : 使用 cmp 命令比较 Hello.oo 和 Hello.o 只有极少数字符不同。可能也是格式问题。 总结:上面的过程中,我们已经将 Hello.c 源程序经过预处理、编译、汇编阶段变成了二进制代码,这三个过程我们都是用两种方法完成的,一种是 GCC + 参数的方法,另一种是使用系统默认的预处理器,编译器,汇编器。这两种方法都达到了我们的目的,最后给它加上x权限。然后运行
链接
hello 程序调用了一个 printf 函数,它是每个 C 编译器都会提供的标准C库中的一个函数,printf 函数存在于一个名为 printf.o 的单独预编译好了的标准文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中,链接器(ld)就负责处理这种合并,结果就得到 hello 文件,他是一个可执行目标文件(简称:可执行文件),可以被加载到内存中,有系统执行。
在链接中,函数和变量统称为符号(symbol),函数名或变量名就是符号名(symbol name)。可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(symbol table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是它们的地址。符号表中所有的符号分类:
链接过程主要包括了地址和空间分配、符号决议和重定位。符号决议有时候也叫做符号绑定、名称绑定、名称决议,甚至还有叫做地址绑定、指令绑定,大体上它们的意思都一样,但从细节角度来区分,它们之间还存在一定区别,比如“决议”更倾向于静态链接,而“绑定”更倾向于动态链接,即它们所使用的范围不一样。 看代码:
test.c
?如何查看编译期间的每一步发生了什么呢?
1. 预处理 选项 gcc -E test.c -o test.i 2.3 运行环境程序执行的过程: 3. 预处理详解? 3.1 预定义符号
这些预定义符号都是语言内置的。
|
属 性 | #define定义宏 | 函数 |
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非 常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执 行 速 度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境 里,除非加上括号,否则邻近操作符的优先级可能 会产生不可预料的后果,所以建议宏在书写的时候 多些括号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副 作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法 的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递 归 | 宏是不能递归的 | 函数是可以递归的 |
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假
定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一
个机器内存大写,我们需要一个数组能够大写。)
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
编译指令:
gcc -D ARRAY_SIZE=10 programe.c
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件
编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常见的条件编译指令
?
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
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方
一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
如果找不到就提示编译错误。
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
注意按照自己的安装路径去找。
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
如果出现这样的场景:
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
或者:
#pragma once
就可以避免头文件的重复引入。
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 2:35:13- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |