目录
1. 变量声明和定义的关系
2. 默认状态下,const对象仅在文件内有效
3. 链接指示:extern "C"
3.1 声明一个非C++的函数
3.2 链接指示与头文件
3.3 指向extern "C"函数的指针
3.4 链接指示对整个声明都有效
3.5 导出C++函数到其他语言
3.6 重载函数与链接指示
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
如果将程序分为多个文件,则需要有在文件间共享代码的方法。例如,一个文件的代码可能需要使用另一个文件中定义的变量。一个实际的例子是std::cout和std::cin,它们定义于标准库,却能被我们写的程序使用。
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i; // 声明i而非定义i
int j; // 声明并定义j
任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi = 3.1416; // 定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
// 源文件a.cpp中定义了全局变量a
int a = 10;
// 源文件b.c声明a
extern int a;
当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:
const int bufSize = 512; // 输入缓冲区大小
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufsize的地方,然后用512替换。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。
解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
// file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h头文件
extern const int bufsize; // 与file_1.cc中定义的bufSize是同一个
如上述程序所示,file_1.cc定义并初始化了bufsize。因为这条语句包含了初始值,所以它(显然)是一次定义。然而,因为bufsize是一个常量,必须用extern加以限定使其被其他文件使用。
file_1.h头文件中的声明也由extern做了限定,其作用是指明bufsize并非本文件所独有,它的定义将在别处出现。
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
C++程序有时需要调用其他语言编写的函数,最常见的是调用C语言编写的函数。像所有其他名字一样,其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参列表。对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。C++使用链接指示(linkage directive)指出任意非C++函数所用的语言。
要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。
链接指示可以有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现。
举个例子,接下来的声明显示了cstring头文件的某些C函数是如何声明的:
// 可能出现在C++头文件中的链接指示
// 单语句链接指示
extern "C" size_t strlen(const char*);
// 复合语句链接指示
extern "C"
{
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面值常量以及一个“普通的”函数声明。
其中的字符串字面值常量指出了编写函数所用的语言。编译器应该支持对C语言的链接指示。此外,编译器也可能会支持其他语言的链接指示,如extern "Ada"、extern "FORTRAN"等。
我们可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起,否则花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样。
多重声明的形式可以应用于整个头文件。例如,C++的cstring头文件可能形如:
// 复合语句链接指示
extern "C"
{
#include // 操作C风格字符串的C函数
}
当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响。
C++从C语言继承的标准库函数可以定义成C函数,但并非必须:决定使用C还是C++实现C标准库,是每个C++实现的事情。
编写函数所用的语言是函数类型的一部分。因此,对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示。而且,指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示:
// pf指向一个C函数,该函数接受一个int返回void
extern "C" void (*pf) (int);
当我们使用pf调用函数时,编译器认定当前调用的是一个C函数。
指向C函数的指针与指向C++函数的指针是不一样的类型。一个指向C函数的指针不能用在执行初始化或赋值操作后指向C++函数,反之亦然。就像其他类型不匹配的问题一样,如果我们试图在两个链接指示不同的指针之间进行赋值操作,则程序将发生错误:
void (*pf1) (int); // 指向一个C++函数
extern "C" void (*pf2) (int); // 指向一个C函数
pf1 = pf2; // 错误:pf1和pf2的类型不同
有的C++编译器会接受之前的这种赋值操作并将其作为对语言的扩展,尽管从严格意义上来看它是非法的。
当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效:
// f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" void f1(void (*) (int));
这条声明语句指出f1是一个不返回任何值的C函数。它有一个类型是函数指针的形参,其中的函数接受一个int形参返回为空。这个链接指示不仅对f1有效,对函数指针同样有效。当我们调用f1时,必须传给它一个C函数的名字或者指向C函数的指针。
因为链接指示同时作用于声明语句中的所有函数,所以如果我们希望给C++函数传入一个指向C函数的指针,则必须使用类型别名:
// FC是一个指向C函数的指针
extern "C" typedef void FC(int);
// f2是一个C++函数,该函数的形参是指向C函数的指针
void f2(FC*);
通过使用链接指示对函数进行定义,我们可以令一个C++函数在其他语言编写的程序中可用:
// calc函数可以被C程序调用
extern "C" double calc(double dparm) { /* ...*/ }
编译器将为该函数生成适合于指定语言的代码。
值得注意的是,可被多种语言共享的函数的返回类型或形参类型受到很多限制。例如,我们不太可能把一个C++类的对象传给C程序,因为C程序根本无法理解构造函数、析构函数以及其他类特有的操作。
对链接到C的预处理器的支持:
有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C+版本的程序时预处理器定义_ _cplusplus(两个下画线)。利用这个变量,我们可以在编译C++程序的时候有条件地包含进来一些代码:
#ifdef__cplusplus
// 正确:我们正在编译C++程序
extern "C"
#endif
int strcmp(const char*, const char*);
链接指示与重载函数的相互作用依赖于目标语言。如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些C++的函数。
C语言不支持函数重载,因此也就不难理解为什么一个C链接指示只能用于说明一组重载函数中的某一个了:
// 错误:两个extern "C"函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);
如果在一组重载函数中有一个是C函数,则其余的必定都是C++函数:
class SmallInt { /* ...*/ };
class BigNum { /* ...*/ };
// C函数可以在C或C++程序中调用
// C++函数重载了该函数,可以在C++程序中调用
extern "C" double calc(double);
extern sma1lInt calc(const smallInt&);
extern BigNum calc(const BigNum&);
C版本的calc函数可以在C或C++程序中调用,而使用了类类型形参的C++函数只能在C++程序中调用。上述性质与声明的顺序无关。