C++中extern关键字

1. 序言

extern是一个关键字,它告诉编译器程序中存在着一个变量或者一个函数,如果在当前编译语句的前面中没有找到相应的变量或者函数,也会在当前文件的后面或者其它文件中定义。

因此,extern的功能主要为声明外部有一个可用的函数或者变量(通常,这些变量时在cpp或者c文件中定义的),而且这些变量和函数是全局可见的。

2. 现代程序编译流程简要介绍

在具体讲述extern之前,觉得十分有必要介绍下现代程序编译的流程。

现代程序在可以执行之前,一般主要进行以下几个流程:
1)编写源码;
2)编译;
3)链接;
其中,十分重要的是编译阶段和链接阶段。

2.1 编译阶段

现代程序的编译主要是按照文件单独进行编译,生成目标代码文件。编译过程的输出是一系列的目标文件。因此,在编译其中一个文件中,编译器并不知道其他文件中的内容。

这样,就涉及到了外部函数调用和外部变量的调用的情况。

对于这两种情况,编译器仅仅在调用部分标记一个“引用”,表明当前的变量或者函数在其他地方,等待链接时进行解析引用。此时,编译器并不能在调用和真实的代码直接建立链接。因此,编译阶段仅仅只是对单个源码文件的语法语义分析,并对外部函数调用和外部变量进行引用标记。

2.2 链接阶段

链接阶段的作用便是对编译阶段产生的目标文件进行链接或者拼接为一个2进制代码文件。

由于编译阶段遗留的外部引用问题,链接阶段的作用除了自己独有的作用,还需要进行以下工作:
1)解析引用:链接器需要为在编译阶段的代码引用标记进行解析引用,确定引用的真实的函数地址或者变量地址;
2)重定位:在编译阶段,目标文件的代码的地址是按照逻辑地址进行生成的,从0开始进行偏移;因此,在链接阶段,不同文件均是从0开始的,需要链接器对不同文件的逻辑地址进行重定位以便能够生成一个独立完整的2进制文件。

3. extern的作用

由于现代编译技术的规格,需要一个关键字来告诉编译器,在这个地方需要链接外部引用或者函数,在编译中,需要建立链接引用,在链接阶段需要解析引用。该关键字便是extern。

3.1 声明外部变量

以下三个示例意思为:在file2.cpp文件中使用file1.cpp的变量i并赋值为100。

示例1(错误示例):

// file1.cpp
#include <iostream>

int i;

// file2.cpp
#include <iostream>

int main()
{
    i = 100;
    return 0;
};

编译file1.cpp时,程序编译 成功;编译file2.cpp中时,程序报错

error C2065: “i”: 未声明的标识符

原因: 编译文件时,编译器是单独编译,其并不知i已经在file1中定义了,因此i相当于未定义。所以报出了编译错误。

示例2(错误示例):

// file1.cpp
#include <iostream>

int i;

// file2.cpp
#include <iostream>
int i;
int main()
{
    i = 100;
    return 0;
};

编译file1和file2时,均能编译成功,这说明没有编译错误。
但是,在链接阶段,报出以下错误:

error LNK2005: "int i" (?i@@3HA) 已经在 file1.obj 中定义

原因在于:两个文件中的i均为全局变量,在编译阶段,一个文件对其他文件未知,可以编译通过;但是链接阶段,在重定位时,链接器发现i有两个定义,违背了全局变量全局唯一的原则,因此报错。

示例3(正确示例):

// file1.cpp
#include <iostream>

int i;

// file2.cpp
#include <iostream>
extern int i;
int main()
{
    i = 100;
    return 0;
};

示例2的错误在于变量重定义,因此,在file2引入了extern来告诉编译器,变量i在其他地方有定义,编译时做一个声明,等链接时进行解析引用并重定义。

3.2 声明外部函数

extern声明外部函数和声明外部变量的道理是一致的。用extern来标记外部函数。

示例4(正确示例):

// file1.cpp
#include <iostream>

#include <iostream>

void fn()
{
    int i = 100;
}

// file2.cpp
#include <iostream>
extern void fn();
int main()
{
    fn();
    return 0;
};

补充:
extern声明的变量一般声明的在cpp或者c文件中。如果在头文件中定义的话,那么其调用的时候完全可以采用包含头文件的方法来使用,无需extern外部变量。

4. 现代编译带来的extern C 问题

由于C++语言的出现,或者C++重载机制的出现,同一函数的不同重载版本的区分显得十分重要。因此,在编译过程中,编译器会在函数名称中添加前缀或者后缀进行区分,通常这些前缀后缀是函数的参数或者返回值。

因此,如果在C++工程中调用C代码实现的接口函数,或者调用dll库中的导出接口函数,需要使用extern C 来进行标记。
以下是一个动态导出库的示例:

示例5(错误示例):
1)导出库部分
dlllibrary.h:

// dll library
// dlllibrary.h:
#ifdef DLLLIBRARY_EXPORTS
#define DLLLIBRARY_API __declspec(dllexport)
#else
#define DLLLIBRARY_API __declspec(dllimport)
#endif

// 此类是从 DllLibrary.dll 导出的
class DLLLIBRARY_API CDllLibrary {
public:
    CDllLibrary(void){}
    // TODO: 在此添加您的方法。
};

extern DLLLIBRARY_API int nDllLibrary;

DLLLIBRARY_API int DllLibrary(int nums);  // 未添加extern C声明

dlllibrary.cpp:

// dll library
// dlllibrary.cpp
#include "stdafx.h"
#include "DllLibrary.h"
#include <stdio.h>

#ifdef _MANAGED
#pragma managed(push, off)
#endif

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

#ifdef _MANAGED
#pragma managed(pop)
#endif

// 这是导出变量的一个示例
DLLLIBRARY_API int nDllLibrary=0;

// 这是导出函数的一个示例。
DLLLIBRARY_API int DllLibrary(int nums)
{
    printf("Hello World\n");
    return 42;
}

2)应用程序部分

// DLLUseApp.cpp
#include "stdafx.h"
#include <Windows.h>
typedef int (*FUNPTR)(int nums);
int _tmain(int argc, _TCHAR* argv[])
{
    HMODULE hModule = LoadLibrary("DllLibrary");
    if (hModule == NULL)
    {
        printf("eeeeeeeee\n");
    }
    FUNPTR pFun;
    pFun = (FUNPTR) GetProcAddress(hModule, "DllLibrary");
    printf("%d\n", GetLastError());
    (*pFun)(5);
    printf("%d\n", GetLastError());
    return 0;
}

程序编译链接都成功,但是,程序在执行中会报错:

DLLUseApp.exe 中的 0x00000000 处未处理的异常: 0xC0000005: 读取位置 0x00000000 时发生访问冲突

而且控制台使用GetLastError()输出错误码为127。
该错误码说明:程序使用GetProcAddress()找不到函数DllLibrary()的入口地址。
原因在于,动态库是按照C++编译进行的,动态库中的DllLibrary()在编译完成后名称已经发生变化,不能调用。

修改:

// dlllibrary.h
#ifdef DLLLIBRARY_EXPORTS
#define DLLLIBRARY_API __declspec(dllexport)
#else
#define DLLLIBRARY_API __declspec(dllimport)
#endif

// 此类是从 DllLibrary.dll 导出的
class DLLLIBRARY_API CDllLibrary {
public:
    CDllLibrary(void){}
    // TODO: 在此添加您的方法。
};

extern DLLLIBRARY_API int nDllLibrary;

#ifdef __cplusplus
extern "C" {

    DLLLIBRARY_API int DllLibrary(int nums);

#endif 
#ifdef __cplusplus
};
#endif 

在导出函数部分添加extern "C"语句,程序问题得到解决。

5. extern外部调用的解决办法

通常,在程序中使用extern声明的办法是在调用代码处先声明外部变量,但是这一方法在大的工程中,容易造成代码混乱,增加阅读难度,因此,笔者使用的方法是,将声明外部变量的extern语句放置在一个头文件中,这样在使用外部变量时,在源代码中,直接包含头文件,即可取得程序外部变量的使用权限。

示例6:
1)外部变量或者外部函数的声明文件:

// GlobalDomainInfo.h
// 定义外部变量的声明
#ifndef _GLOBAL_DOMAIN_INFO_H_
#define _GLOBAL_DOMAIN_INFO_H_
#include "stdafx.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <string>

// 声明外部变量
extern char             g_szSql[1024];
extern char             g_szPrimaryKeyQuery[256];
extern int              g_nSqlWay;
extern HANDLE g_hResponseEvent;

// 声明外部函数
extern void GetConfigFile(char *lpszFileName);

// 在此处也可以添加一些非外部函数或者外部变量的头文件,使用时直接包含头文件即可,实现文件放置在其他地方
void GetCurrentExecutePath(char *lpszFileName);

#endif

2)外部变量和外部函数的定义
外部变量以及外部函数的定义和实现可以放置在其他的cpp文件中。

// sql.cpp
char             g_szSql[1024] = {'\0'};

// primarykey.cpp
char             g_szPrimaryKeyQuery[256] = {'\0'};

// event.cpp
HANDLE g_hResponseEvent = NULL;

3)外部变量和外部函数的使用

// application.cpp
#include "stdafx.h"
#include "GlobalDomainInfo.h"   // 直接包含extern声明所在的头文件,无需填写extern
#include <conio.h>

int main()
{
    strcpy(g_szSql, "select * from table");
    return 0;
}

总结:
示例6将工程中所有的extern声明放置在一个头文件中,这样在使用的地方直接包含头文件即可,无需再次进行extern声明,这样做到了一次声明,多处使用。具体编译过程的展开以及链接都由程序帮我们进行。

  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值