编译
1. 整体流程
- 预处理
#include
是一个来自C
语言的宏命令,在编译器进行编译之前把它后面所写的那个文件的内容,完完整整地、一字不改地包含到当前的文件中来,没有其它任何作用与副功能的。系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后再到 C++ 安装目录找。- 把同一个头文件多次用
include
包含进cpp
文件里,可能会出现不良后果。如果在头文件开头使用#ifndef...#endif
,那么在第一次include后,就已经有了相应的宏定义,第二次再试图include时,就不再满足#ifndef
的条件,也就不会再次执行include。#pragma once
也可以起到类似作用。
显然,在预处理的过程中,被include的文件的内容已经被复制到了对应的地方,因此编译过程中不会再处理被include的文件。
- 编译
这个过程就是把源文件转换成二进制的机器码,即生成
.o
或.obj
目标文件。- 目标文件中没有定义的函数和变量就视作一个个符号,并把这些符号放进符号表(symbol table)中,然后在链接过程中再去找具体的定义。
- 编译器在这个过程中针对源代码执行优化。
- 链接
在链接的时候,需要在
makefile
里面说明需要连接哪个.o
或.obj
文件,此时,连接器会去这个.o
或.obj
文件中找在 b.cpp 中实现的函数,再把他们build
到makefile
中指定的那个可以执行文件中。通常,C++ 编译器会在每个 .o 或 .obj 文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示 “redefined”或者multiple definition of ..
。
2. 注意事项
-
一个符号,在整个程序中可以被声明多次,但能且仅能被定义一次。
-
根据上述编译流程,如果被include的文件中有函数或变量的定义,那么当它被多个文件包含时,这些函数和变量就被定义了多次。在链接过程中就会出现类似“多次定义”的错误。但有三个例外
- const 对象的定义。因为全局的 const 对象默认是没有 extern 的声明的,所以它只在当前文件中有效,在链接时也不会为此类对象查找定义。
- 类(class)的定义。
- 头文件中可以写内联函数(inline)的定义。因为内联函数在预处理过程中就会被直接复制到函数被调用的地方,就像include的复制一样,也不会在符号表中添加符号。在编译和链接过程中,也就不会出现“找到一个符号的多个定义”的错误。
GCC编译器
Ubuntu中安装和切换多版本GCC编译器
- 安装GCC
# 只安装 GCC 软件包 sudo apt install gcc # 或者安装 build-essential 软件包,该软件包包含 GCC 及一系列开发工具,如 make、g++ 和dpkg-dev。 sudo apt install build-essential
other
ifndef, pragma once
#pragma once
- pragma once 一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。
- 你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。
- 不必担心宏名冲突了,也就会出现宏名冲突引发的问题。大型项目的编译速度也因此提高了一些。
- 缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。
- 这种方式不支持跨平台,不受一些较老版本的编译器支持.
#ifndef
- ifndef的方式受C/C++语言标准支持。它不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。
- 当然,缺点就是如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,但编译器却硬说找不到声明的状况——这种情况有时非常让人郁闷。
- 由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。
- 受C/C++语言标准的支持,不受编译器的任何限制.