编译与预编译 头文件与源文件
编译与链接
编译
1、编译就是把文本形式源代码(.c/.cpp)翻译为机器语言形式的目标文件(.obj)的过程。
2、 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制。
链接
链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码(.exe)的过程。
静态链接和动态链接
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
(1)静态链接
在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2) 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
预编译
预编译
预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段主要处理#开始的预编译指令。
何时需要预编译
总是使用不经常改动的大型代码体。 程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个“预编译头”。
预处理的主要内容
预处理包括:宏定义、文件包含、条件编译。
宏定义
宏定义是C语言提供的三种预处理功能的其中一种。宏定义和操作符的区别是:宏定义是替换,不做计算,也不做表达式求解。宏定义又称为宏代换、宏替换,简称“宏”。
格式:
#define 标识符
字符串其中的标识符就是所谓的符号常量,也称为“宏名”。预处理(预编译)工作也叫做宏展开:将宏名替换为字符串。掌握"宏"概念的关键是“换”。一切以换为前提、做任何事情之前先要换,准确理解之前就要“换”。
即在对相关命令或语句的含义和功能作具体分析之前就要换:
例:#define Pi 3.1415926把程序中出现的Pi全部换成3.1415926
注:宏定义过程中需要换行在行尾打“\”
说明:
(1)宏名一般用大写
(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义
(3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
(6)可以用#undef命令终止宏定义的作用域
(7)宏定义允许嵌套,例:
#define WIDTH 80
#define LENGTH (WIDTH+40)
(8)字符串( “ ” )中永远不包含宏,例:
printf(" “); scanf(” ");
(9)宏定义不分配内存,变量定义分配内存。
(10)宏定义不存在类型问题,它的参数也是无类型的。
带参数的宏定义
格式:
#define宏名(参数表) 字符串
例如:#define S(a,b) a*b
area=S(3,2);
第一步被换为area=a*b; ,
第二步被换为area=3*2;
类似于函数调用
说明:
(1)实参如果是表达式要注意括号问题
(2)宏名和参数的括号间不能有空格
(3)宏替换只作替换,不做计算,不做表达式求解
(4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
(5)宏的参数不存在类型,也没有类型转换。
(6)宏展开使源程序变长,函数调用不会
(7)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、值传递、返回值)。
文件包含 #include
在C语言中文件包含是指一个源文件可以将另一个源文件的全部内容包含进来。
该命令的作用是在预编译时,将指定源文件的内容复制到当前文件中。
文件包含有两种格式,分别是:#include “file” 和 #include< file >
这两格式的区别在于:
1.使用双引号,系统首先到当前目录下查找被包含的文件,如果没找到,再到系统指定的"包含文件目录"(由用户在配置环境时设置)去找。
2.使用尖括号:直接到系统指定的"包含文件目录"去查找。通常使用双引号比较保险。
条件编译
1.条件编译指令将决定哪些代码被编译,而哪些是不被编译的。
2.可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
条件编译指令
#if、#else、#elif和#endif指令
(1)
#if表达式
//语句段1
#else
//语句段2
#endif
如果表达式为真,就编译语句段1,否则编译语句段2。
(2)
#if表达式1
//语句段1
#elif表达式2
//语句段2
#else
//语句段3
#endif
如果表达式1真,则编译语句段1,否则判断表达式2;如果表达式2为真,则编译语句段2,否则编译语句段3
#ifdef和#ifndef
(1)
#ifdef的一般形式:
#ifdef宏名
//语句段
#endif
如果在此之前已定义了这样的宏名,则编译语句段。
(2)
#ifndef的一般形式:
#ifndef 宏名
//语句段
#endif
如果在此之前没有定义这样的宏名,则编译语句段。
#else可以用于#ifdef和#ifndef中,但#elif不可以。
头文件和源文件
后缀为 .h 的文件是头文件,内含函数声明、宏定义、结构体定义等内容。
后缀为 .c 的文件是源文件,内含函数实现,变量定义等内容。而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。这样分开写成两个文件是一个良好的编程风格。
头文件和源文件本质
在编译器眼中其实.c文件和.h文件并没有什么本质的区别,都是同样的文件,并不是说编译头文件的时候就会去找同名头文件,.h文件的内容会被完全替换源文件中的include “xxx.h”。为什么多了一个.h的文件大概是为了方便区分。那为什么常见自己写的源文件中会看到所写的include “xx.h”,引入同名头文件呢,这句话并不是多余的,将同名头文件引入过来后,此句include会被头文件中的内容完全替换,也就多了很多声明,当函数A需要调用函数B的时候就无需再考虑到代码顺序的问题,是一个很好的习惯。
怎样自己写一个头文件并应用
在aaa.h里定义了一个函数的声明,然后在aaa.h的同一个目录下建立aaa.c,aaa.c里定义了这个函数的实现,然后是在main函数所在.c文件里#include这个aaa.h 然后我就可以使用这个函数了。
几个小问题
//a.h
void foo();
//a.c
#include “a.h” //这句话是要,还是不要?
void foo()
{
return;
}
//main.c
#include “a.h”
int main( )
{
foo();
return 0;
}
u 如果 a.c 中不写,那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起?
其实.h和.c/.cpp没啥关系
从C编译器角度看,.h和.c皆是浮云,换句话说,就是.h和.c没啥必然联系。.h中一般放的是同名.c文件中定义的变量、数组、函数的声明,需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用。因为#include"xx.h" 这个宏其实际意思就是把当前这一行删掉,把xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用xx.c 中函数的地方,都要在使用前声明一下子),所以用#include"xx.h" 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
上述问题实际上是说,已知头文件“a.h”声明了一系列函数(仅有函数原型,没有函数实现),“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”