2017-09-21 22:53:44 +0000   |     java c compile   |   Viewed times   |    

C语言编译的两大特点

第一,C语言是局部编译

如果把C语言真正编译前的宏展开预处理,和转换成汇编语言的过程都算在编译过程里的话,C语言的编译过程主要分为两步:

  1. 把每一个.c源代码文件编译成二进制的.o目标文件。
  2. 再把多个.o目标文件链接成最终的可执行文件。

说C是局部编译,是因为.c源码编译成.o目标文件的过程是单对单的。就算多个文件之间相互有依赖关系,编译的时候,只包含了外部变量和函数声明的头文件即可,不需要提供外部变量和函数的实现。

比如a.c依赖b.c。其中的a()函数需要调用b()函数。编译a.c的时候,只需要在头文件b.h中声明了b()函数,然后在a.c中包含b.h头文件就可以。所以实际编译的时候,编译器只是得到了一定会有b()这个函数的承诺,但并不知道具体的实现。

/**
 * a.c 源码文件
 */
#include <stdio.h>
#include "b.h"

void a(void) {
    b();
}
/**
 * b.c 源码文件
 */
#include <stdio.h>

void b(void) {
    printf("I am b()!\n");    
}
/**
 * b.h 头文件
 */
void b(void);

编译a.cb.c两个文件,好的做法是,分别编译成a.ob.o两个目标文件。即使a.c依赖b.c中的b()函数,在编译a.o的时候,我们不需要向编译器提供b()函数的实现。

gcc -c a.c -o a.o
gcc -c b.c -o b.o

然后再把a.ob.o链接成一个可执行文件。链接之前在a.ob()函数的入口地址留空,就等链接的时候在填入。可以说a.ob.o是完全解耦的。

gcc -o run.exe a.o b.o

C语言之所以采取局部编译的方案,是一个历史问题。当初计算机的内存空间很有限,不允许编译的时候把所有依赖的库全缓存的内存里,统一编译。只读很有限的文件又不能避免模块间的循环依赖问题。所以采用局部编译的方式,让每个小文件都可以单独编译。

这也是为什么C语言需要引进.h头文件,虽然函数的入口地址可以暂时留空,但毕竟还是要检查一下调用的函数是否存在。所以.h头文件中的声明,实际是对编译器的一种承诺,a()函数调用的b()函数,在b.c文件里一定有。

但C语言需要头文件并不代表这是一个最优的解决方案。只是在局部编译的前提下,不得已而为之。到了像Java这样统一编译的情况下,就不再需要头文件。

第二,C语言是编译型语言

编译型语言的特点就是: 编译完(链接完成以后)直接得到的是一个二进制机器码的可执行文件。和编译型语言相对的一个概念就是 解释型语言。 区别就是,解释型语言编译完储存的不是机器码,而是一种中间码。最典型的就是Java的.class文件储存的是字节码。JVM在运行时加载.class文件以后,还需要进一步解析才能转换成本地平台的机器码执行。

所以Java语言跨平台的特性就是这么来的。因为不同的平台上使用的二进制机器码是不同的。C语言在一台机器上编译好的可执行文件,拿到另一台机器上,很可能使用的不是一套机器码。而不同的平台上可以安装不同版本的JVM,可以把字节码转换成相应平台的机器码。但JVM拿到的都是完全相同的.class文件中的字节码。

这两点上,Java语言和C语言完全不同

第一,Java是统一编译的

还是刚才的例子,

/**
 * A.java 文件
 */

/** A类要调用B类的b()方法 */
class A {
    public void a() {
        B b = new B();
        b.b();
    }
    public static void main(String[] args) {
        A a = new A();
        a.a();
    }
}
/**
 * B.java 文件
 */

/** A类要调用B类的b()方法 */
public class B {
    public void b() {
        System.out.println("I am b()");
    }
}

在编译A.java的时候,需要向编译器提供访问B.class文件的路径,

# 先把B.java编译成B.class
javac -d ~/java/bin B.java
# 然后编译A.java的时候,要给出访问B.class的路径
javac -cp ~/java/bin A.java

或者如果B.class文件没有实现编译好的话,也可以用-sourcepath选项直接提供B.java的源码位置,

javac -sourcepath ~/java/src A.java

所以,

JVM在编译A.java的时候,必须拿到它依赖的b()函数的具体实现。

这样就让编译Java程序 必须准备齐全所有需要的代码。编译起来更麻烦,但得到的好处是不需要再编辑.h头文件了。只需要保证文件名和类的命名空间保持一致,以及实际储存路径和package路径保持一致,这样虚拟机就能找到需要用到的全部组件。相当于自动帮我们生成了一份.h头文件。

第二,Java是解释型语言

这点在讲C语言是编译型语言的时候已经讲了,解释型语言的一大特点就是编译后储存的不是二进制机器码,而是中间代码,比如Java的字节码。这里不再重复。