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聲明,這樣做到了一次聲明,多處使用。具體編譯過程的展開以及鏈接都由程序幫我們進行。

轉自: https://blog.csdn.net/wutong_xingkong/article/details/50550430

發佈了11 篇原創文章 · 獲贊 38 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章