Hi,我是小余。 本文已收录到 GitHub Androider-Planet 中。这儿有 Android 进阶成长知识体系,重视公众号 [小余的自习室] ,在成功的路上不走失!

前语

extern 是C/C++言语中标明全局变量或许函数效果范围(可见性)的关键字,编译器收到extern通知,则其声明的变量或许函数能够在本模块或许其他模块运用。

关于函数而言,由于函数的声明如“extern int method();”与函数界说“int method(){}”能够很明晰的区分开来,为了简洁起见,能够把extern关键字省掉,所以有了咱们常见的函数声明办法“int method();”,但是关于变量并非如此,变量的界说格式如“int i;”,声明格式为“extern int i;”,假如省掉extern关键字,就会造成混乱,故不允许省掉。

一般用法

在本模块中运用:

extern int a;
extern int b;
int maxbb(int l,int r) {
    return l > r ? l : r;
}
int main() {
    cout << maxbb(a, b) << endl;
}
​
int a = 10;
int b = 20;

当时模块的main函数在a和b之前界说,所以main函数对a和b是没有拜访权限的,能够在main之前界说

extern int a;
extern int b;

这样就能够正常拜访到a和b了。

跨模块中

在模块1 _extern.cpp中界说:

extern int _a = 10;
extern int _b = 20;
​
int maxAB(int a,int b) {
    return a > b ? a : b;
}

在模块2 main.cpp中界说:

extern int _a;
extern int _b;
int maxAB(int a,int b);
int main() {
    cout << "a:" << _a << " b:" << _b << endl;
    cout << maxAB(100,200) << endl;
}
​
成果:
a:10 b:20
200

看到运用extern关键字运用到了外部模块_extern.cpp中的全局变量以及函数。

标准界说运用extern关键字的进程为

  • 1.界说一个.h文件用来声明需求供给外部拜访的变量或许函数。
module1.h
extern int _a;
extern int _b;
int maxAB(int a, int b);
  • 2.界说一个.cpp文件来初始化全局变量或许函数的完结

    module1.cpp
    ​
    #include "module1.h"int _a = 100;
    int _b = 200;
    ​
    int maxAB(int x, int y) {
        return x > y ? x : y;
    }
    
  • 3.在需求运用到的地方运用extern关键字润饰。

    //main.cpp
    extern int _a;
    extern int _b;
    int maxAB(int a,int b);
    int main() {
    cout << "a:" << _a << " b:" << _b << endl;
    cout << maxAB(100,200) << endl;
    }
    ​
    运行成果:
    a:100 b:200
    200
    

假如咱们把module1.h中如下界说:

extern int _a = 100;
extern int _b = 200;

这样肯定会报错的,由于extern int _a是变量声明,而extern int _a = 100则是变量声明和界说。 由于module1.cpp中是将”module1.h” include到cpp中的,假如在.h中声明和界说即运用extern int _a = 100办法,则会引起重复界说,而extern int _a是变量声明,并非界说,一切不会重复

extern 运用进程中的一些注意事项

这儿引证掘友出的题:

数组经过外部声明为指针时,数组和指针是不能交换运用的;那么请思考一下,在 A 文件中界说数组 char a[100];在 B 文件中声明为指针:extern char *a;此刻拜访 a[i],会发生什么;

先说成果,会引起 segmentation fault 报错;

这儿触及到了数组与指针的差异

数组与指针的差异

数组变量和枚举常量相同都归于符号常量。注意,不是数组变量这个符号的值是那块内存的首地址, 而是数组变量这个符号自身代表了首地址,它便是这个地址值。这便是数组变量归于符号常量的含义地点。

由于数组变量是一个符号常量,所以其能够看做是右值,而指针作为变量,只能看作为左值。 右值永远不等于左值,所以将指针赋予数组常量是不合法的。

例如:char a[] 中的 a 是常量,是一个地址,char *a 中 a 是一个变量,一个能够存放地址的变量。

extern 声明全局变量的内部完结

被extern润饰的全局变量,在编译期不会分配空间,而是在链接的时分经过索引去其他文件中查找索引对应的地址。假定文件中声明晰一个:

extern char a[];

这是一个外部变量的声明,声明晰一个外部的字符数组,编译器看到这玩意时不会立即给a分配空间,而是等链接器进行寻址,编译器会将一切关于a的引证化为一个不包括类型的标号,编译完结后会得到一个方针中心产品a.o,但是此刻a.o中关于a还是一个无类型标号,链接器衔接的时分发现这个标号,会去其他 中心产品中查找和这个标号对应的地址,找到之后替换这个标号。最终链接为一个可履行的文件。

extern char * a;

这也是一个外部变量的声明,它声明晰一个字符指针。编译以及链接进程和前面字符数组进程类似,只是此刻链接器在寻找符号地址的时分,找到的是前面声明的 extern char a[] 字符数组,这儿就有问题了 :由于在这个文件中声明的 a 是一个指针变量而不是数组,链接器的行为实践上是把指针 a 自身的地址定位到了另一个 .cpp 文件中界说的数组首地址上, 而不是咱们所希望的把数组的首地址赋予指针 a。(这很容易了解:指针变量也需求占用空间,假如说把数组的首地址赋给了指针 a,那么指针 a 自身在哪里存放呢?) 这便是症结地点了。所以此例中指针 a 的内容实践上变成了数组 a 首地址开始的 4 字节标明的地址

上述加粗部分的能够了解为,链接器认为 a 变量自身的内存方位是数组的首地址,但其实 a 的方位是其他方位,其内容才是数组首地址。

这儿着重要了解的是:指针的地址以及指针的内容的差异,指针自身也存在地址,链接器将数组的首地址赋予了指针自身,这样肯定是不行的

举个比如,界说 char a[] = “abcd”,则外部变量 extern char a[] 的地址是 0x12345678 (数组的起始地址), 而 extern char *a 是重新界说了一个指针变量 a,其地址可能是 0x87654321,因此直接运用 extern char *a 是过错的。

经过上述剖析,咱们得到的最重要的结论是:运用 extern 润饰的变量在链接的时分只找寻同名的标号,不查看类型,所以才会导致编译经过,运行时出错。

extern “C”

extern “C”的真实意图是完结类C和C++的混合编程。在C++源文件中的句子前面加上extern “C”,标明它按照类C的编译和衔接规约来编译和衔接,而不是C++的编译的衔接规约。这样在类C的代码中就能够调用C++的函数or变量等。(注:我在这儿所说的类C,代表的是跟C言语的编译和衔接办法共同的一切言语)

C和C++彼此调用

前面咱们说了extern “C”是为了完结C和C++混编,接下来就来解说下C与C++怎么彼此调用,在解说彼此调用之前,咱们先来了解C和C++编译和链接进程的差异

C++的编译和链接

咱们都知道C++是一个面向对象的编程办法,而面向对象最核心的特性便是重载,函数重载给咱们带来了很大便利性。假定界说如下函数重载办法:

void log(int i);
void log(char c);
void log(float f);
void log(char* c);

则在编译后:

_log_int
_log_char
_log_float
_log_string

编译后的函数名经过带上参数的类型信息,这样衔接时依据参数就能够找到正确的重载办法。

C++中给的变量编译也是这样一个进程,如全局变量会编译为g_xx,类变量编译为c_xx.衔接时也是按照这种机制去查找对应的变量的。

C的编译和衔接

C言语中并没有重载和类这些特性,故不会像C++相同将log(int i)编译为log_int,而是直接编译为log函数,当C++去调用C中的log(int i)办法时,会找不到_log_int办法,此刻extern “C”的效果就体现出来了。

下面来看下C和C++是怎么彼此调用的。

C++中调用C的代码

假定一个C的头文件cHeader.h中声明晰一个函数_log(int i),假如C++要调用它,则必须添加上extern关键字。代码如下:

//cHeader.h
#ifndef C_HEADER
#define C_HEADER
extern void _log(int i);
#endif // !C_HEADER

在对应的cHeader.c文件中完结_log办法:

//cHeader.c
#include "cHeader.h"
#include <stdio.h>void _log(int i) {
    printf("cHeader %d\n", i);
}

在C++中引证cHeader中的_log办法:

//main.cpp
extern "C" {
    //void _log(int i);
    #include "cHeader.h"
}
int main() {
    _log(100);
}

linux履行上述文件的指令为:

  • 1.首要履行gcc -c cHeader.c,会产生cHeader.o;
  • 2.然后履行g++ -o C++ main.cpp cHeader.o
  • 3.履行程序输出:Header 100

注意: 在main.cpp文件中能够不必包括函数声明的文件,即“extern “C”{#include”cHeader.h”}”,而直接改用extern “C” void log(int i)的方法。那main.cpp是怎么找到C中的log函数,并调用的呢?

那是由于首要经过gcc -c cHeader.c生成一个方针文件cHeader.o,然后咱们经过履行g++ -o C++ main.cpp cHeader.o这个指令指明晰需求链接的方针文件cHeader.o。 main.cpp中只需求声明哪些函数需求以C的方法调用,然后去方针文件中查找即可。“.o”为方针文件。类似Windows中的obj文件。

C中调用C++的代码

C中调用C++中的代码和前面的有所不同,首要在cppHeader.h中声明一个_log_i办法。

#pragma once
extern "C" {
    void _log_i(int i);
}

在对应的cppHeader.cpp中完结该办法:

#include "cppHeader.h"
#include <stdio.h>void _log_i(int i) {
    printf("cppHeader:%d\n", i);
}

界说一个cMain.c文件调用_log_i办法:

extern void _log_i(int i);
int main() {
    _log_i(120);
}

注意点

  • 1.假如直接在.c文件中include “cppHeader.h”是会报错的,由于cppHeader.h中包括了extern “C”,而将cppHeader.h包括进来,会直接打开cppHeader.h内容,而extern “C”在C言语中是不支持的,所以会报错。
  • 2.在.c文件中不加extern void _log_i(int i)也会报错

linux履行上述文件的指令为

  • (1)首要履行指令:g++ cppHeader.cpp -fpic -shared -g -o cppHeader.so 该指令是将cppHeader.cpp编译成动态衔接库,其中编译参数的解说如下:

    • -shared 该选项指定生成动态衔接库(让衔接器生成T类型的导出符号表,有时分也生成弱衔接W类型的导出符号),不必该标志外部程序无法衔接。相当于一个可履行文件
    • -fPIC:标明编译为方位独立的代码,不必此选项的话编译后的代码是方位相关的所以动态载入时是经过代码复制的办法来满意不同进程的需求,而不能到达真正代码段同享的意图。
    • -g:为调试
  • (2)然后再履行指令:gcc cMain.c cppHeader.so -o cmain 该指令是编译cMain.c文件,同时链接cppHeader.so文件,然后产生cmain的可履行文件。

  • (3)最终履行指令: ./cmain 来履行该可履行程序

    成果:cppHeader:120
    

总结

本文首要解说了关于extern的三个知识点:

  • 1.extern的基础用法:本模块以及跨模块的运用
  • 2.extern的在运用进程中的一些注意点,首要经过数组和指针的差异来解说。
  • 3.extern “C”在C++中的用法以及原理:解说了关于C和C++彼此调用以及内部完结机制。

好了,关于C++/C中的extern解说就到这儿,我是小余,欢迎点赞加重视,咱们下期见。

参阅

extern “C“ 用法详细说明
extern关键字用法详解
【C/C++】extern 的一些注意事项