extern “C“ 和 extern 深度探索
一、基础语法
1.1 extern
extern
是C和C++语言中的一个关键字,用于声明全局变量或函数,以便在其他文件中使用。extern
关键字的作用是告诉编译器,所声明的变量或函数的定义存在于别的地方(通常是在另一个源文件中),而不是在当前文件中。
作用:
1. 跨文件共享全局变量:
当你在一个文件中定义了一个全局变量,并且希望在另一个文件中使用这个变量时,你可以在第二个文件中使用 extern 来声明这个变量。这样,编译器会知道这个变量的定义在别的地方,而不是在当前文件中。
2. 跨文件共享函数:
类似地,如果你在一个文件中定义了一个函数,并且希望在另一个文件中调用这个函数,你可以在调用函数的文件中使用 extern 来声明这个函数。这允许你在不同的文件中分开组织代码,同时保持代码的模块化。
1.2 extern “C”
extern "C"
是C++中的一个关键字,用于指示编译器使用C语言的链接方式来处理特定代码。在C++中,为了保持与C语言的兼容性,extern "C"
允许编写C++代码时,同时确保这些代码在链接时能够与用C语言编写的代码兼容。
基本语法:
在C++中,extern "C"
通常用于以下几种情况:
-
函数声明:
当你需要在C++代码中声明一个用C语言编写的函数时,你可以在函数声明前加上extern "C"
。这样做会告诉编译器,这个函数应该按照C语言的命名规则(即不进行名称修饰)来链接。extern "C" { int foo(const char *p); }
-
全局变量声明:
同样,如果你需要引用C语言中的全局变量,也可以使用extern "C"
。extern "C" int globalVariable;
作用:
extern "C"
的主要作用是解决C++与C语言之间的链接兼容性问题。C++编译器默认会对函数和变量名进行名称修饰(Name Mangling),用于支持重载等特性【函数重载原理可见此文】。然而,C语言并不支持这种名称修饰,因此在C++中编写的库如果直接被C语言代码链接,可能会出现链接错误,为了解决这个问题,通过使用 extern "C"
,可以告诉编译器忽略名称修饰,按照C语言的规则来处理这些声明,从而实现跨语言调用。
二、深入探究
2.1 extern
extern能修饰什么?
从上一章节可以看到extern
是修饰变量或者函数的。这是从语法层面上进行分析,但是我们从系统层面进行探究,extern
是在链接环节发挥关键作用的,而变量和函数都是链接过程中需要去定位的一个个实体,如函数会导出一个label函数签名用于链接。
基于此,进一步延伸拓展,extern
是不能修饰类型的,如我在other.cpp
定义了一个结构体,在 main.cpp
无法通过extern
来进行导入。从语法上解释:extern
是修饰变量或者函数的,不能修饰类型;从系统层面上解释:extern
是作用在链接阶段的,而类型定义在链接时并不需要去“找”,只有变量,函数这种实体才需要去“找”,链接。
那么,类型信息无法通过extern
导入,如果我想使用other.cpp
定义的结构体应该怎么办?答案是把other.cpp
进行.h
和.cpp
分离,通过include "other.h"
来导入结构体类型信息
此处我们可以总结头文件和extern
的关系
- 类型信息通过包含头文件引入
- 实体信息(变量/函数)通过
extern
引入
在 C 和 C++ 语言中,存储类(storage class)修饰符用于指定变量或函数的存储期和链接属性。存储类修饰符主要包括 static
、extern
、auto
(在 C++ 中已不常用)、register
(在现代 C++ 中已废弃)和 thread_local
(C++11 引入)。
存储类修饰符通常用于全局变量和函数,但也可以用于局部变量
资料参考:C99
extern细节探索
a. extern和const共用
假设有下面的代码
// main.cpp
#include<iostream>
using namespace std;
extern const int maxNum;
int main() {
cout << "maxNum = " << maxNum << endl;
return 0;
}
输入:g++ -c main.cpp
后输入nm main.o | grep maxNum
结果:可以看到和main.o
导出的maxNum
的符号如下
此时我们有另一份代码:
// other.cpp
// const int maxNum = 666; // 这样不行
int maxNum = 666; // 这样可以
输入:g++ -c other.cpp
后输入nm other.o | grep maxNum
结果:可以看到maxNum
的符号和main.o
导出的一致,因此后续使用g++ main.o other.o
可以正常链接
但是当我们将代码稍作改变:
// other.cpp
const int maxNum = 666; // 这样不行
// int maxNum = 666; // 这样可以
输入:g++ -c other.cpp
后输入nm other.o | grep maxNum
结果:可以看到maxNum
的符号和main.o
导出的不一致,因此后续使用g++ main.o other.o
无法链接
总结:当想要引用外部的变量时,需要注意const修饰是否会影响导出的符号,以免出现链接错误
b. extern和constexper共用
结论:不能共用,上述章节已经提到extern影响的是链接行为,而使用 constexpr 修饰的变量必须在编译时期就能确定其值,此时extern修饰的变量无法在编译时期确定,因此报错
例子:
// main.cpp
#include<iostream>
using namespace std;
extern constexpr int maxNum;
int main() {
cout << "maxNum = " << maxNum << endl;
return 0;
}
当执行:g++ -c main.cpp
出现报错
2.2 extern “C”
extern "C"的定位
在第一章节基础语法中介绍了extern "C"的语法,此处再明确其定位。总结而言:extern "C"影响的是本文件符号导出,即,当我的C++代码要使用某个C语言编译出来的库的add函数时,我需要用extern "C"修饰我想使用的函数,使得其按照C语言的风格编译,此处本文件所有add函数的符号都会按照C风格导出。
对于extern "C"影响的是本文件符号导出这句话有两个实际用途理解
- 上述提到的C++调用C语言代码
- linux和windows跨平台库调用,假设你有一套C++代码,其在两个平台编译出来的符号是不一致的,可见【函数重载原理】具体有什么差异。因此,为了这套C++代码无论在哪个平台编译出来的符号都保持一致,可以用extern "C"修饰统一为C风格符号修饰,则符号导出会保持原样,不存在不同平台编译出来的符号不一致的问题
当我们的C语言编译出来的库想让C语言,C++都能调用时,通常头文件的写法如下:
#ifdef __cplusplus
extern "C" {
#endif
int add(int, int); // 你的函数
#ifdef __cplusplus
}
#endif
而对于C++编译出来的库想让C语言,C++都能调用时,则需要对库向外暴露的接口再做一层封装,这层封装分为.h和.cpp,其中.h按照上述模板编写,.cpp处则调用对库向外暴露的接口。具体见C调用C++做法
extern "C"细节探索
a. extern "C"影响范围
结论:
对于普通类型,用不用extern “C”修饰都没关系,导出来的符号label是原样。
对于C++类型:string,函数,有无extern “C”修饰,导出来的label不同。
示例代码:
// main.cpp
#include <string>
extern "C" {
std::string inStr;
std::string inStrArr[5] = {};
}
// 上面和下面进行对比,上面被extern "C"包住,下面没有
std::string outStr;
std::string outStrArr[5] = {};
上面代码定义了一组对比的代码,分别为被extern "C"包住的string变量和string数组与没被extern "C"包住的string变量和string数组,执行结果如下:
可以看到:C++类型是否被extern "C"修饰导出符号有差异
接下来对代码小修改:
// main.cpp
#include <string>
extern "C" {
char* inStr;
char* inStrArr[5] = {};
}
// 上面和下面进行对比,上面被extern "C"包住,下面没有
char* outStr;
char* outStrArr[5] = {};
执行结果如下:
结论:
对于普通类型,用不用extern “C”修饰都没关系,导出来的符号label是原样。
对于C++类型:string,函数,有无extern “C”修饰,导出来的label不同。
b. 写法不同
看下面的代码
extern "C" {
int val;
}
extern "C" int val;
上面的写法和下面的不等价。上面写法为在本文件定义一个val变量,但是用C风格修饰。下面写法为用C风格修饰同时extern它,也就是说表达val在外部定义。此处注意这个小坑