【C++】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" 通常用于以下几种情况:

  1. 函数声明
    当你需要在C++代码中声明一个用C语言编写的函数时,你可以在函数声明前加上 extern "C"。这样做会告诉编译器,这个函数应该按照C语言的命名规则(即不进行名称修饰)来链接。

    extern "C" {
        int foo(const char *p);
    }
    
  2. 全局变量声明
    同样,如果你需要引用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)修饰符用于指定变量或函数的存储期和链接属性。存储类修饰符主要包括 staticexternauto(在 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"影响的是本文件符号导出这句话有两个实际用途理解

  1. 上述提到的C++调用C语言代码
  2. 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在外部定义。此处注意这个小坑

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值