与非C++代码的连接

  在许多情况下,一个C++程序中也可能包含着一些采用其他语言写出的部分。类似地,C++代码片段也经常被用于主要由其他语言写出的程序。用不同语言写出的程序片段之间的协作比较困难;甚至采用同一种语言写出,但通过不同编译器编译的片段也是这样。例如,不同语言,或者同一语言的不同实现可能在它们使用寄存器保存参数的方式上,在将参数放入堆栈的顺序上,在整数或字符串等内部类型的布局上,在编译器传递给连接器的名字方面,在对连接器所要求的类型检查的量等方面存在着差异。为了能有所帮助,可以在一个extern声明中给出有关的连接约定。例如,下面声明了C和C++标准库函数strcpy(),并特别说明它应该按照C连接约定进行连接:

extern "C" char* strcpy(char*, const char*);

这个声明的作用与

extern char* strcpy(char*, const char*);

“简单”声明的差异仅在于调用strcpy()的连接约定不同。

  由于C和C++语言之间的紧密关系,extern "C"指令就特别有用。请注意,在extern "C"中的C表示一个连接约定,而不是一种语言。人们也经常将extern "C"用于连接Fortran和汇编例程,因为它们的要求也正好符合C实现的约定。

  extern "C"指令描述的(只)是一种连接约定,它并不影响调用函数的语义。特别地,声明为extern "C"的函数仍然要遵守C++的类型检查和参数转换规则,而不是C的较弱的规则。例如,

    extern "C" int f();
    int g()
    {
        return f(1);            // 错误❌:未期望有参数
    }

为许多声明添加extern "C"也会成为令人讨厌的事情。因此,这里还提供了一种为一组声明描述连接约定的机制。例如,

    extern "C" {
        char* strcpy (char*, const char*);
        int strcmp (const char*, const char*);
        int strlen (cosnt char*);
        // ...
    }

这种结构经常被称为连接块,它可以用于包裹起整个的C头文件,使整个文件能适合C++的使用。例如,

    extern "C" {
        #include <string.h>
    }

这种技术经常被用于由C头文件产生出的C++头文件。换一种方式,也可以用条件编译(7.8.1节)建立起公共的C和C++头文件:

    #ifdef __cplusplus
    extern "C" {
    #endif

        char* strcpy(char*, const char*);
        int strcmp(const char*, const char*);
        int strlen(const char*);
        // ...

    #ifdef __cplusplus 
    }
    #endif

__cplusplus是一个预定义的宏名字,它被用于保证当这个文件被用做C头文件时,其中的C++结构将被去掉。

  任何声明都可以出现在连接块里:

    extern "C" {            // 这里可以有任何声明,如:
        int g1;             // 定义
        extern int g2;      // 声明,不是定义
    }

应特别指出,变量的作用域及存储类都不会受到影响。所以,g1仍然是一个全局变量,还是在这里被定义而不是被声明。要想声明而不是定义一个变量,你必须将关键字extern直接应用在声明里。例如,

extern "C" int g3;       // 声明,不是定义

这一写法初看起来有点古怪。然而,它不过是给一个外部声明加上“C”后应该保持意义不变,将一个文件用连接块包裹后意义也不变的一个简单推论。

  有着C连接的名字也可以在名字空间里定义。名字空间只影响到C++程序里对于该名字的访问方式,但却不会影响连接器看到它的方式。取自std的printf()是一个典型的例子:

    #include <cstdio>

    void f()
    {
        std::printf("Hello, ");        // ok
        printf("world!\n");            // 错误❌:没有全局的printf()
    }

即使调用的是std::printf(),它也还是那个老的C的printf()(21.8节)。

  注意,这些将使我们可以把具有C连接的库包含到选定的名字空间里,而不去污染全局名字空间。不幸的是,对于将具有C++连接的函数定义在全局名字空间里的头文件,我们就没有同样的灵活手段了。出现此问题,是因为C++实体的连接必须将名字空间考虑在内,因此所生成的目标文件将反应它们在或者不在名字空间里的情况。

🔚