C++ 里有很多关键字,初学时很容易被这些关键字绕的云里雾里。现在打算梳理一下 C++ 里比较常见的关键字。
extern
提起extern关键字,就不得不提到 C++ 的编译过程。编译过程大致分为如下几步
预处理
预处理阶段编译器会对#号开头的语句进行替换,比方说代码文件里有一行代码这么写,
#define SIZE 100
那么编译器会把文件里出现的 SIZE 统一替换成 100 , 除此之外,像宏函数,引入的头文件,都会做替换。这里有一点需要注意的,
#include
一个 C++ 程序必须有的引入头文件,这行代码的执行同样发生在预处理阶段,编译器会把引入的头文件整个复制到代码文件里,处理完之后,你的代码就和头文件的代码变成同一个文件了。这点很容易理解,但确实很重要。这是区分内部和外部的唯一途径。
预处理完成之后,会生成一个XXX.i的文件,这个文件就是全部替换完成之后的文本文件。
编译
随后,编译器需要对宏处理完成之后的文本文件进行编译,转换成汇编语言。编译报错就是发生在这个过程。所谓编译报错其实就是编译器无法把你的代码转换为汇编语言,可能是语法错误,也可能是命名冲突,抑或是其它问题。编译完成生成的文件是XXX.s文件。
汇编
编译完成后,全部 C++ 代码被转换成汇编语言。接下来要做的是把汇编语言转换成机器语言。编译器会根据不同平台生成不同的机器语言。转换结束后生成XXX.o文件并不能直接运行,需要将整个程序进行链接转换成可执行文件后才能执行。
链接
这个过程就是把所有XXX.o文件链接到一起,也就是把声明和定义链接到一起。这也就是为什么要把头文件和源文件分开,头文件主要是声明,源文件主要是定义。这样做的目的是引入一个头文件之后不需要把定义重新编译一遍,只需要引入声明,然后直接调用即可。
光文字比较枯燥,很难理解。接下来用一个例子演示一下全过程。
1 | //math.h |
1 | //math.cpp |
1 | //main.cpp |
写好三个文件之后,首先对main.cpp 和 math.cpp进行编译
1 | //编译都是以单文件为单位进行的 |
编译好之后,我们会得到main.o 和 math.o 两个文件。我们不需要在main.cpp里声明add函数是因为引入的头文件已经声明了,宏替换之后等于main.cpp文件也声明了。
完成编译后要把所有.o文件链接到一起。
1 | //将两个文件链接到一起 |
接下来我们会得到一个名为test的可执行文件,执行test就会输出期待的30。编译的过程大致就是这样。
了解编译全过程之后很容易就能理解什么是内部,什么是外部。引入了头文件就是内部,没有引入就是外部。
想调用外部的函数或者使用外部的变量,只需要声明一下,并加个extern关键字就可以啦。上面的示例是引入了头文件的,其实不引入头文件也是可以的。
1 | //math.h |
1 | //math.cpp |
1 | //main.cpp |
上面这样写也是符合规范的。这就是内部和外部的区别,也就是extern的主要用途。
static
上面提到的extern就是为了把函数或者变量提供给外部使用,那static就是把函数或者变量局限在内部使用。分清内部和外部,自然就理解了。static像一个缩小了作用域且是在定义时分配内存的全局变量。
inline
inline说明符在教科书或者常见的一些书比如《C++ Primer》里面都是一句简单的建议编译器内联函数带过。以前是这样的,inline说明符就是建议编译器内联函数,至于有没有内联,这个不一定。
但是从C++17开始,inline被赋予了一个新的意义,它可以用来解决一个遗留问题。
众所周知,静态变量是需要初始化的。C17之前,类内定义的非const静态成员只能在类外进行初始化。如果初始化的代码放在头文件而且头文件被重复引用,就会造成重定义问题。C17之后,只要在静态成员变量前加一个inline,便可以解决这个问题。
下面一起分析一下这个问题。
前面提到引入头文件其实就是简单的宏替换,所以头文件被重复引用时,每个文件都定义了一个同名且非const的static变量。在一个相同作用域下定义重名的变量自然就造成了冲突。
C++17加入的inline就是向编译器说明,同一作用域下这些定义都是指向同一个变量。这样就解决了冲突。
const
很常见的常量表达式,用途也是很广泛
constexpr
这个关键字可以拆分成const + expr(ess),翻译过来就是常量表达式。这个关键字可以用来代替宏定义的没有类型的值,它会在编译的时候确定值。
除此之外,constexpr也可以用在函数前面,这样做的目的是在编译时把函数替换成值,看下面的例子
1 | constexpr int add(int a, int b) |
这个函数就可以加上constexpr关键字,这个函数也会在调用的时候直接用值替换。