本文以 UNIX 环境为主,结合一些技术博客和 <<C++ Primer>> 做一些总结和整理。

编译过程

单文件

这里的单文件指的是单独的 .cpp/.c 文件,因为 .h 文件只是进行一些变量的声明,是不需要进行编译的。

  • 预处理(预处理器 cpp ): 预处理器cpp将对源文件中的宏进行展开

    1
    2
    3
    $ gcc -E hello.cpp -o hello.i
    // or
    $ cpp hello.cpp -o hello.i
  • 编译(编译器 gcc/g++ ): gcc将c文件编译成汇编文件,然后编译成机器码。(编译器将 .i 文件编译成汇编文件 .s)。

    1
    $ gcc -S hello.i
  • 汇编(汇编器 as ): 汇编器将汇编文件编译成机器码 (可以通过 nm -a hello.o 查看机器码文件)

    1
    2
    3
    $ gcc -c hello.s -o hello.o
    // or
    $ as hello.s -o hello.o
  • 链接(连接器 ld ): 链接器ld将目标文件和外部符号进行连接,得到一个可执行二进制文件。

    1
    $ gcc hello.o -o hello

头文件

我们在第一步当中可以看到,这一步的作用就是把宏进行了展开。那么我们的头文件也是在这里被以宏的方式引入到了 hello.cpp 当中。那么我们下面展开介绍一下 #include与头文件中的一些注意事项。

#include

#include 是c语言的宏命令,会在第一步(预处理)中起作用。会将后面那个头文件完完整整的引入到当前的文件当中。而且仅是做替换,而不会有其他的副作用。

1
2
3
// math.h
int add(int a, int b);
int del(int a, int b);
1
2
3
4
5
6
// main.cpp
#include "math.h"
int main() {
int c = add(2, 3);
int d = del(3, 2);
}

经过第一步预处理以后:

1
2
3
4
5
6
7
# main.i
int add(int a, int b);
int del(int a, int b);
int main() {
int c = add(2, 3);
int d = del(3, 2);
}

而对于头文件的声明当中 " "< > 是有区别的,如果头文件名在 < > 中,就会被认为是标准头文件。编译器会在预定义的位置查找该头文件,如果是 " " 就认为它是非系统头文件,非系统文件查找通常开始于源文件所在路径。

头文件保护符

头文件保护符是为了保证头文件在一个 .cpp 文件当中被多次引用不会出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// file1.h  
class file1
{
};

// file2.h
#include "file1.h"
class file2
{
};

// file3.h
#include "file1.h"
#include "file2.h"

上面file3.h的代码展开以后就变成了

1
2
3
4
5
6
7
8
9
10
11
12
// file1.h展开的内容  
class file1
{
};

// file2.h展开的内容
class file1
{
};
class file2
{
};

class file1 被引用了两次,导致编译器报错。这时就可以加上头文件保护符来解决这个问题。

1
2
3
4
5
6
7
8
9
// file1.h  
#ifndef _FILE1_H_
#define _FILE1_H_

class file1
{
};

#endif // !_FILE1_H_
1
2
3
4
5
6
7
8
9
10
// file2.h  
#ifndef _FILE2_H_
#define _FILE2_H_

#include "file1.h"
class file2
{
};

#endif // !_FILE2_H_
1
2
3
4
5
6
7
8
// file3.h  
#ifndef _FILE3_H_
#define _FILE3_H_

#include "file1.h"
#include "file2.h"

#endif // !_FILE3_H_

这时因为 _FILE1_H_ 只出现了一次,就不会出现重定义的问题。

注意事项

头文件中需要区别 声明定义 两个概念。声明因为不涉及到内存的分配,所以是允许多次出现的,而定义则会进行内存的分配,所以定义只能出现一次。而在头文件中是只允许出现声明和一些特殊的定义的( 类定义, 值在编译时已知的 const 对象和 inline 函数)

  • 值在编译时就已知的 const 对象:
    如:const char c = 'c' 这个是在编译时就已经确定值的,之后程序不能改变。
    const char *c = 'c' 是不可以的,因为指针不是在编译时确定值的。
    并且全局的 const 对象是没有 extend 的声明的,所以只对当前 .cpp 文件有效。所以将它放在头文件中进行引用后,仅对引用文件有效,而对其他文件不可见。所以不会出现重定义。

  • inline:因为在函数的调用执行过程当中,我们需要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。如果一个运行时间很长的函数,那么这些调用代价也是可以忽略的。不过对于一些简单的函数,这些调用代价是很昂贵的。我们就可以使用 inline 函数,它会像宏定义一样进行代码的替换,而不进行调用过程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    using namespace std;
    inline void swap(int *a, int *b){
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
    }

    int main(){
    int m, n;
    cin>>m>>n;
    cout<<m<<", "<<n<<endl;
    swap(&m, &n);
    cout<<m<<", "<<n<<endl;
    return 0;
    }

    在编译器遇到函数调用 swap(&m, &n) 的时候就会进行替换,并用实参代替形参。

    1
    2
    3
    4
    int temp;
    temp = *(&m);
    *(&m) = *(&n);
    *(&n) = temp;
  • class:对于类的定义成员函数是可以写在定义体内的,这样的话编译器会默认这个函数是 inline 的,也可以声明在定义体内然后在外面进行实现。

多文件

多文件互相依赖的情况,只需要先单独将各文件编译成 .o 文件,然后 link 一下就行了。

1
2
3
4
5
6
7
8
9
// Circle.h

#ifndef CIRCLE_H
#define CIRCLE_H
class Circle
{

};
#endif
1
2
3
4
5
6
7
// Circle.cpp
#include "Circle.h"

#include <iostream>
using namespace std;

...
1
2
3
4
5
6
7
8
9
10
// main.cpp
#include "Circle.h"
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
...
}

进行编译:

1
2
3
$ g++ -c Circle.cpp -o Circle.o
$ g++ -c main.cpp -o main.o
$ g++ main.o Circle.o -o main

如果有修改,每次只需要对增量文件进行编译就行了。如果项目比较大,可以使用 makefile 文件来进行自动化编译。(后面会有文章继续介绍 makefile 和其他的自动化编译)

Comments

⬆︎TOP