C/C++ extern关键字用法示例全面解析

目录
  • 前言
  • 一般用法
    • 在本模块中使用:
    • 跨模块中
  • extern 使用过程中的一些注意事项
    • 数组与指针的区别
    • extern 声明全局变量的内部实现
  • extern "C"
    • C和C++互相调用
    • C++的编译和链接
    • C的编译和连接
    • C++中调用C的代码
    • C中调用C++的代码
  • 总结

前言

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 &lt;stdio.h&gt;

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 &ldquo;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++互相调用以及内部实现机制。

参考

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

以上就是C/C++ extern关键字用法示例全面解析的详细内容,更多关于C/C++ extern关键字用法的资料请关注我们其它相关文章!

(0)

相关推荐

  • c++中的volatile和variant关键字详解

    目录 一.两个长得有点像的变量 二.二者的功能 三.应用实例 四.总结 一.两个长得有点像的变量 对volatile关键字,其实很多人只是能用,知道用到啥处,但其实应用的原理并不知道.在一些多线程的通信中,往往是这个关键字应用到的场所,很多人也是如此想的.但其实这个想法是不准确的.volatile这个关键字的目的最初是针对硬件IO操作的,防止访问IO操作中的缓存影响到真实的数据.但这个关键字的溢出效应是,多线程也可以应用这个原理(注意在多核和多CPU编程中有危险,但是在其它语言如Java中,得按

  • 聊聊C++的mutable和volatile

    C++中修饰数据可变的关键字有三个:const.volatile和mutable.const比较好理解,表示其修饰的内容不可改变(至少编译期不可改变),而volatile和mutable恰好相反,指示数据总是可变的.mutable和volatile均可以和const搭配使用,但两者在使用上有比较大差别. mutable mutable只能作用在类成员上,指示其数据总是可变的.不能和const 同时修饰一个成员,但能配合使用:const修饰的方法中,mutable修饰的成员数据可以发生改变,除此之

  • C语言基础函数用法示例详细解析

    目录 函数 函数定义 函数一般格式 C语言函数分类 库函数 库函数的分类 库函数的学习 自定义函数 函数的参数 实际参数 形式参数 函数的调用 传值调用 传址调用 无参函数调用 函数的声明和定义 函数的声明 函数的定义 函数声明与定义的区别 exit与return介绍 函数 函数定义 百度百科对函数的定义:子程序 在计算机科学中,子程序是一个大型程序中的某部分代码,有一个或者多个语句块组成,它负责完成某项特定的任务,而且相比于其他的代码,具备相对的独立性. 一般来说会有输入参数和返回值,提供对过

  • C语言中extern详细用法解析

    在C语言中,修饰符extern用在变量或者函数的声明前,用来说明"此变量/函数是在别处定义的,要在此处引用".  1. extern修饰变量的声明.  举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v.能够被其他模块以extern修饰符引用到的变量通常是全局变量.还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就

  • asp.net中的check与uncheck关键字用法解析

    本文实例讲述了asp.net中的check与uncheck关键字用法.分享给大家供大家参考.具体分析如下: checked和unchecked是两个不常用的关键字,但是确是非常有用的关键字,对此,建议测试时开启全局checked编译器选项. 1. 一段编译没经由过程的代码 复制代码 代码如下: int a = int.MaxValue * 2; 以上代码段编译没有经由过程,在VS2010中会有一条红色的波浪线指出这段代码有题目:"The operation overflows at compil

  • c语言中static和extern的用法详细解析

    一,static和extern:大工程下我们会碰到很多源文档. 文档a.c 复制代码 代码如下: static int i; //只在a文档中用int j;    //在工程里用static void init()         //只在a文档中用{}void callme()          //在工程中用{   static int sum;} 上面的全局i变量和init()函数只能用在a.c文档中,全局变量sum的作用域只在callme里.变量j和函数callme()的全局限扩充到整个

  • java之static关键字用法实例解析

    本文实例讲述了java中static关键字用法,分享给大家供大家参考.具体分析如下: 一.介绍: 1.在类中,用static声明的成员变量为静态成员变量,它为该类的公用变量,在第一次使用时被初始化,对于该类的所有对象来说,static成员变量只有一份. 2.用static声明的方法为静态方法,在调用该方法时,不会将对象的引用传递给它,所以在static方法中不可访问非static成员.(静态方法不再是针对于某个对象调用,所以不能访问非静态成员) 3.可以通过对象引用或类名(不需要实例化)访问静态

  • C语言学习之关键字的示例详解

    目录 1. 前言 2. 什么是关键字 3. extern-声明外部符号 4. auto-自动 5. typedef-类型重定义(类型重命名) 6. register-寄存器 6.1 存储器 6.2 register关键字的作用 7. static-静态 7.1 static修饰局部变量 7.2 static修饰全局变量 7.3 static修饰函数 1. 前言 大家好,我是努力学习游泳的鱼.关键字,这名字一听,就很关键.而有些关键字,你可能不是很了解,更别谈使用.所以,这篇文章将带你见识常见的关

  • 浅析java 的 static 关键字用法

    本篇浅析java中static的用法,主要五个方面:静态成员变量,静态方法,静态块,静态内部类,静态导包. 首先还是一张表格说一下静态对象和非静态对象的区别: 静态对象 非静态对象 归属 类共同具有 类的各个实例独立拥有 内存分配 内存空间上固定的 附属类分配 分配空间顺序 优先分配静态对象空间 优先分配静态对象空间,初始化也一样 1 静态变量,静态方法,静态块 静态对象,静态方法都是在原对象和方法上加上static关键字修饰,表示类可以直接调用这些,而不需要实例化后再调用.具有的好处是: 1-

  • Android编程录音工具类RecorderUtil定义与用法示例

    本文实例讲述了Android编程录音工具类RecorderUtil定义与用法.分享给大家供大家参考,具体如下: 以下工具类都是经过实战开发验证都是可以直接复制使用的. 录音工具类介绍: 录音工具类主要平时用来开发语音聊天的,在微信和QQ上该工具类都是常用的,因为语音聊天. 使用硬件一般都要开权限,别忘了.这里还需要搭配 Android FileUtil 类使用,为了方便才这么封装的 import android.media.MediaRecorder; import android.util.L

  • JavaScript常用截取字符串的三种方式用法区别实例解析

    stringObject.substring(start,stop) 用于提取字符串中介于两个指定下标之间的字符. start必需.一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置. stop可选.一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1.如果省略该参数,那么返回的子串会一直到字符串的结尾. start从0开始 到stop(不包含stop)结束 不接受负的参数. stringObject.substr(start,

  • 详解Java 10 var关键字和示例教程

    关键要点 Java 10引入了一个闪亮的新功能:局部变量类型推断.对于局部变量,现在可以使用特殊的保留类型名称"var"代替实际类型. 提供这个特性是为了增强Java语言,并将类型推断扩展到局部变量的声明上.这样可以减少板代码,同时仍然保留Java的编译时类型检查. 由于编译器需要通过检查赋值等式右侧(RHS)来推断var的实际类型,因此在某些情况下,这个特性具有局限性,例如在初始化Array和Stream的时候. 如何使用新的"var"来减少样板代码. 在本文中,

随机推荐