C/C++開發(fā)中extern的一些使用注意事項(xiàng)
前言
前些日子,有友友問了我這樣的一道問題:
數(shù)組通過外部聲明為指針時(shí),數(shù)組和指針是不能互換使用的;那么請(qǐng)思考一下,在 A 文件中定義數(shù)組 char a[100]
;在 B 文件中聲明為指針:extern char *a
;此時(shí)訪問 a[i]
,會(huì)發(fā)生什么;
先說結(jié)果,會(huì)引起 segmentation fault
報(bào)錯(cuò);
那接下來(lái)由博主來(lái)分析一番;
數(shù)組與指針的區(qū)別
在介紹 extern
之前,我們需要了解一下數(shù)組與指針有什么區(qū)別?
數(shù)組變量代表了存放該數(shù)組的那塊內(nèi)存,它是這塊內(nèi)存的首地址。這就說明了數(shù)組變量是一個(gè)地址,而且,還是一個(gè)不可修改的常量,具體來(lái)說,就是一個(gè)地址常量。
數(shù)組變量跟枚舉常量一樣,都屬于符號(hào)常量。數(shù)組變量這個(gè)符號(hào),就代表了那塊內(nèi)存的首地址。注意,不是數(shù)組變量這個(gè)符號(hào)的值是那塊內(nèi)存的首地址,而是數(shù)組變量這個(gè)符號(hào)本身代表了首地址,它就是這個(gè)地址值。這就是數(shù)組變量屬于符號(hào)常量的意義所在。
由于數(shù)組變量是一種符號(hào)常量,它是一個(gè)右值,而指針,作為變量,卻是一個(gè)左值,一個(gè)右值永遠(yuǎn)都不是左值,那么,數(shù)組名永遠(yuǎn)都不會(huì)是指針!
舉個(gè)例子,char a[]
中的 a
是常量,是一個(gè)地址,char *a
中 a
是一個(gè)變量,一個(gè)可以存放地址的變量。
具體分析
了解了數(shù)組與指針的區(qū)別之后,讓我們來(lái)看看 extern
聲明全局變量的內(nèi)部實(shí)現(xiàn);
extern
是 C/C++ 語(yǔ)言中表明函數(shù)和全局變量作用范圍(可見性)的關(guān)鍵字,該關(guān)鍵字告訴編譯器,其聲明的函數(shù)和變量可以在本模塊或其它模塊中使用。
TIP :被 extern
修飾的全局變量不被分配空間,而是在鏈接的時(shí)候到別的文件中通過查找索引定位該全局變量的地址。
extern char a[];
這是一個(gè)外部變量的聲明,它聲明了一個(gè)名為 a
的字符數(shù)組,編譯器看到這個(gè)聲明就知道不必為這個(gè)變量分配空間,這個(gè) .cpp 文件中所有對(duì)數(shù)組 a
的引用都化為一個(gè)不包含類型的標(biāo)號(hào),具體地址的定位留給鏈接器完成。編譯完成之后也得到一個(gè)中間文件,鏈接器遍歷這個(gè)文件,發(fā)現(xiàn)有未經(jīng)定位的標(biāo)號(hào),于是它搜索其他中間文件,試圖尋找到一個(gè)匹配的空間地址,在此例中無(wú)疑鏈接器將成功地尋找到這個(gè)地址并將此中間文件中所有的這個(gè)標(biāo)號(hào)替換為鏈接器所尋找到的地址,最終生成的可執(zhí)行文件中,所有曾經(jīng)的標(biāo)號(hào)都應(yīng)當(dāng)已經(jīng)被替換為地址。這是一個(gè)正常工作過程,鏈接出來(lái)的可執(zhí)行文件至少在對(duì)于該數(shù)組的引用部分將工作得很好。
extern char * a;
這是一個(gè)外部變量的聲明,它聲明了一個(gè)名為 a
的字符指針,中間過程與上同,經(jīng)過一番搜索,找到了一個(gè)分配過空間的名為 a
的地方(也就是我們先定義的那個(gè)字符數(shù)組),鏈接器并不知道它們的類型,僅僅是發(fā)現(xiàn)它們的名字一樣,就認(rèn)為應(yīng)該把 extern
聲明的標(biāo)號(hào)鏈接到數(shù)組 a
的首地址上,因此鏈接器把指針 a
對(duì)應(yīng)的標(biāo)號(hào)替換為數(shù)組 a
的首地址。這里問題就出現(xiàn)了:由于在這個(gè)文件中聲明的 a
是一個(gè)指針變量而不是數(shù)組,鏈接器的行為實(shí)際上是把指針 a
自身的地址定位到了另一個(gè) .c 文件中定義的數(shù)組首地址上,而不是我們所希望的把數(shù)組的首地址賦予指針 a
(這很容易理解:指針變量也需要占用空間,如果說把數(shù)組的首地址賦給了指針 a
,那么指針 a
本身在哪里存放呢?)。這就是癥結(jié)所在了。所以此例中指針 a
的內(nèi)容實(shí)際上變成了數(shù)組 a
首地址開始的 4 字節(jié)表示的地址(如果在 16 位機(jī)上,就是 2 字節(jié))。
上述加粗部分的可以理解為,鏈接器認(rèn)為 a
變量本身的內(nèi)存位置是數(shù)組的首地址,但其實(shí) a 的位置是其他位置,其內(nèi)容才是數(shù)組首地址。
舉個(gè)例子,定義 char a[] = "abcd"
,則外部變量 extern char a[]
的地址是 0x12345678 (數(shù)組的起始地址),而 extern char *a
是重新定義了一個(gè)指針變量 a
,其地址可能是 0x87654321,因此直接使用 extern char *a
是錯(cuò)誤的。
通過上述分析,我們得到的最重要的結(jié)論是:使用 extern
修飾的變量在鏈接的時(shí)候只找尋同名的標(biāo)號(hào),不檢查類型,所以才會(huì)導(dǎo)致編譯通過,運(yùn)行時(shí)出錯(cuò)。
extern "C"
extern "C"
包含雙重含義,從字面上即可得到:
- 首先,被它修飾的目標(biāo)是
extern
的; - 其次,被它修飾的目標(biāo)是
C
的。
1、 被 extern "C"
限定的函數(shù)或變量是 extern
類型的;
extern int a;
僅僅是一個(gè)變量的聲明,其并不是在定義變量 a
,并未為 a
分配內(nèi)存空間。變量 a
在所有模塊中作為一種全局變量只能被定義一次,否則會(huì)出現(xiàn)連接錯(cuò)誤。
通常,在模塊的頭文件中對(duì)本模塊提供給其它模塊引用的函數(shù)和全局變量以關(guān)鍵字 extern
聲明。例如,如果模塊 B 欲引用該模塊 A 中定義的全局變量和函數(shù)時(shí)只需包含模塊 A 的頭文件即可。這樣,模塊 B 中調(diào)用模塊 A 中的函數(shù)時(shí),在編譯階段,模塊 B 雖然找不到該函數(shù),但是并不會(huì)報(bào)錯(cuò),它會(huì)在連接階段中從模塊 A 編譯生成的目標(biāo)代碼中找到此函數(shù)。
與 extern
對(duì)應(yīng)的關(guān)鍵字是 static
,被它修飾的全局變量和函數(shù)只能在本模塊中使用。因此,一個(gè)函數(shù)或變量只可能被本模塊使用時(shí),其不可能被 extern "C"
修飾。
2、被 extern "C"
修飾的變量和函數(shù)是按照 C 語(yǔ)言方式編譯和連接的;
未加 extern "C"
聲明時(shí)的編譯方式
作為一種面向?qū)ο蟮恼Z(yǔ)言,C++ 支持函數(shù)重載,而過程式語(yǔ)言 C 則不支持。函數(shù)被 C++ 編譯后在符號(hào)庫(kù)中的名字與 C 語(yǔ)言的不同。例如,假設(shè)某個(gè)函數(shù)的原型為:
void foo( int x, int y );
該函數(shù)被 C 編譯器編譯后在符號(hào)庫(kù)中的名字為 _foo
,而 C++ 編譯器則會(huì)產(chǎn)生像 _foo_int_int
之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機(jī)制,生成的新名字稱為 “mangled name”)。
_foo_int_int
這樣的名字包含了函數(shù)名、函數(shù)參數(shù)數(shù)量及類型信息,C++ 就是靠這種機(jī)制來(lái)實(shí)現(xiàn)函數(shù)重載的。例如,在 C++ 中,函數(shù) void foo(int x, int y)
與 void foo(int x, float y)
編譯生成的符號(hào)是不相同的,后者為 _foo_int_float
。
同樣地,C++ 中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以 .
來(lái)區(qū)分。而本質(zhì)上,編譯器在進(jìn)行編譯時(shí),與函數(shù)的處理相似,也為類中的變量取了一個(gè)獨(dú)一無(wú)二的名字,這個(gè)名字與用戶程序中同名的全局變量名字不同。
未加 extern "C"
聲明時(shí)的連接方式
假設(shè)在 C++ 中,模塊 A 的頭文件如下:
// 模塊A頭文件 moduleA.h #ifndef MODULE_A_H #define MODULE_A_H int foo(int x, int y); #endif
在模塊 B 中引用該函數(shù):
// 模塊B實(shí)現(xiàn)文件 moduleB.cpp #include "moduleA.h" foo(2, 3);
實(shí)際上,在連接階段,連接器會(huì)從模塊 A 生成的目標(biāo)文件 moduleA.obj 中尋找 _foo_int_int
這樣的符號(hào);
加 extern "C"
聲明后的編譯和連接方式
加 extern "C"
聲明后,模塊 A 的頭文件變?yōu)椋?/p>
// 模塊A頭文件 moduleA.h #ifndef MODULE_A_H #define MODULE_A_H extern "C" int foo(int x, int y); #endif
在模塊 B 的實(shí)現(xiàn)文件中仍然調(diào)用 foo(2, 3)
,其結(jié)果是:
- 模塊 A 編譯生成
foo
的目標(biāo)代碼時(shí),沒有對(duì)其名字進(jìn)行特殊處理,采用了 C 語(yǔ)言的方式; - 連接器在為模塊 B 的目標(biāo)代碼尋找
foo(2, 3)
調(diào)用時(shí),尋找的是未經(jīng)修改的符號(hào)名_foo
;
如果在模塊 A 中函數(shù)聲明了 foo
為 extern "C"
類型,而模塊 B 中包含的是 extern int foo(int x, int y)
,則模塊 B 找不到模塊 A 中的函數(shù);反之亦然。
所以,可以用一句話概括 extern "C"
這個(gè)聲明的真實(shí)目的:實(shí)現(xiàn) C++ 與 C 及其它語(yǔ)言的混合編程。
以上就是C/C++開發(fā)中extern的一些使用注意事項(xiàng)的詳細(xì)內(nèi)容,更多關(guān)于C/C++開發(fā)extern使用事項(xiàng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(170.兩數(shù)之和之三 - 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì))
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(170.兩數(shù)之和之三 - 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08C語(yǔ)言基礎(chǔ)知識(shí)點(diǎn)指針的使用
這篇文章主要介紹了C語(yǔ)言基礎(chǔ)知識(shí)點(diǎn)指針的使用,下面文章將讓我們掌握指針的概念和用法、指針與數(shù)組之間的關(guān)系、指針指向的指針、如何使用指針變量做函數(shù)參數(shù)等更多相關(guān)內(nèi)容,需要的小伙伴可以參考一下2022-03-03C/C++ 開發(fā)神器CLion使用入門超詳細(xì)教程
這篇文章主要介紹了C/C++ 開發(fā)神器CLion使用入門超詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04C語(yǔ)言中關(guān)于庫(kù)函數(shù) qsort 的模擬實(shí)現(xiàn)過程
庫(kù)函數(shù)的模擬實(shí)現(xiàn)有利于我們?nèi)ド钊肓私膺@個(gè)函數(shù)內(nèi)部是怎樣實(shí)現(xiàn)的,以及學(xué)習(xí)它的算法,使我們更加了解這個(gè)函數(shù)該怎樣去使用,接下來(lái)我將詳細(xì)的介紹qsort的應(yīng)用及用法,并且用代碼模擬實(shí)現(xiàn)它們的功能2021-09-09C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)順序表中的增刪改(尾插尾刪)教程示例詳解
這篇文章主要為大家介紹了C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)順序表中的增刪改教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02基于Matlab實(shí)現(xiàn)有雪花飄落的圣誕樹的繪制
圣誕節(jié)快到了(雖然還有十天),一起來(lái)用MATLAB畫個(gè)簡(jiǎn)單圣誕樹叭~代碼幾乎取消了全部的循環(huán),因此至少需要17b之后的版本,僅存的循環(huán)用來(lái)讓樹旋轉(zhuǎn)起來(lái),讓雪花飄落起來(lái),讓樹頂上的星光搖曳起來(lái)~感興趣的可以試一試2022-12-12c++獲取sqlite3數(shù)據(jù)庫(kù)表中所有字段的方法小結(jié)
本文給大家分享c++獲取sqlite3數(shù)據(jù)庫(kù)表中所有字段的三種常用方法,本文針對(duì)每一種方法給大家詳細(xì)介紹,需要的的朋友通過本文一起學(xué)習(xí)吧2016-11-11