日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C/C++開發中extern的一些使用注意事項_C 語言

作者:???????????? ? 更新時間: 2023-02-10 編程語言

前言

前些日子,有友友問了我這樣的一道問題:

數組通過外部聲明為指針時,數組和指針是不能互換使用的;那么請思考一下,在 A 文件中定義數組 char a[100];在 B 文件中聲明為指針:extern char *a;此時訪問 a[i],會發生什么;

先說結果,會引起 segmentation fault 報錯;

那接下來由博主來分析一番;

數組與指針的區別

在介紹 extern 之前,我們需要了解一下數組與指針有什么區別?

數組變量代表了存放該數組的那塊內存,它是這塊內存的首地址。這就說明了數組變量是一個地址,而且,還是一個不可修改的常量,具體來說,就是一個地址常量。

數組變量跟枚舉常量一樣,都屬于符號常量。數組變量這個符號,就代表了那塊內存的首地址。注意,不是數組變量這個符號的值是那塊內存的首地址,而是數組變量這個符號本身代表了首地址,它就是這個地址值。這就是數組變量屬于符號常量的意義所在。

由于數組變量是一種符號常量,它是一個右值,而指針,作為變量,卻是一個左值,一個右值永遠都不是左值,那么,數組名永遠都不會是指針!

舉個例子,char a[]?中的?a?是常量,是一個地址,char *a?中?a?是一個變量,一個可以存放地址的變量。

具體分析

了解了數組與指針的區別之后,讓我們來看看 extern?聲明全局變量的內部實現;

extern 是 C/C++ 語言中表明函數和全局變量作用范圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。

TIP :被 extern 修飾的全局變量不被分配空間,而是在鏈接的時候到別的文件中通過查找索引定位該全局變量的地址。

extern char a[];

這是一個外部變量的聲明,它聲明了一個名為?a?的字符數組,編譯器看到這個聲明就知道不必為這個變量分配空間,這個 .cpp 文件中所有對數組?a?的引用都化為一個不包含類型的標號,具體地址的定位留給鏈接器完成。編譯完成之后也得到一個中間文件,鏈接器遍歷這個文件,發現有未經定位的標號,于是它搜索其他中間文件,試圖尋找到一個匹配的空間地址,在此例中無疑鏈接器將成功地尋找到這個地址并將此中間文件中所有的這個標號替換為鏈接器所尋找到的地址,最終生成的可執行文件中,所有曾經的標號都應當已經被替換為地址。這是一個正常工作過程,鏈接出來的可執行文件至少在對于該數組的引用部分將工作得很好。

extern char * a; 

這是一個外部變量的聲明,它聲明了一個名為?a?的字符指針,中間過程與上同,經過一番搜索,找到了一個分配過空間的名為 a 的地方(也就是我們先定義的那個字符數組),鏈接器并不知道它們的類型,僅僅是發現它們的名字一樣,就認為應該把 extern 聲明的標號鏈接到數組 a 的首地址上,因此鏈接器把指針?a?對應的標號替換為數組?a?的首地址。這里問題就出現了:由于在這個文件中聲明的?a?是一個指針變量而不是數組,鏈接器的行為實際上是把指針?a?自身的地址定位到了另一個 .c 文件中定義的數組首地址上,而不是我們所希望的把數組的首地址賦予指針?a(這很容易理解:指針變量也需要占用空間,如果說把數組的首地址賦給了指針?a,那么指針?a?本身在哪里存放呢?)。這就是癥結所在了。所以此例中指針?a?的內容實際上變成了數組?a?首地址開始的 4 字節表示的地址(如果在 16 位機上,就是 2 字節)。

上述加粗部分的可以理解為,鏈接器認為 a 變量本身的內存位置是數組的首地址,但其實 a 的位置是其他位置,其內容才是數組首地址。

舉個例子,定義 char a[] = "abcd",則外部變量 extern char a[] 的地址是 0x12345678 (數組的起始地址),而 extern char *a 是重新定義了一個指針變量 a,其地址可能是 0x87654321,因此直接使用 extern char *a 是錯誤的。

通過上述分析,我們得到的最重要的結論是:使用 extern 修飾的變量在鏈接的時候只找尋同名的標號,不檢查類型,所以才會導致編譯通過,運行時出錯。

extern "C"

extern "C" 包含雙重含義,從字面上即可得到:

  • 首先,被它修飾的目標是 extern 的;
  • 其次,被它修飾的目標是 C 的。

1、 被 extern "C" 限定的函數或變量是 extern 類型的;

extern int a;

僅僅是一個變量的聲明,其并不是在定義變量 a,并未為 a 分配內存空間。變量 a 在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。

通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字 extern 聲明。例如,如果模塊 B 欲引用該模塊 A 中定義的全局變量和函數時只需包含模塊 A 的頭文件即可。這樣,模塊 B 中調用模塊 A 中的函數時,在編譯階段,模塊 B 雖然找不到該函數,但是并不會報錯,它會在連接階段中從模塊 A 編譯生成的目標代碼中找到此函數。

extern 對應的關鍵字是 static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被 extern "C" 修飾。

2、被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連接的;

未加 extern "C" 聲明時的編譯方式

作為一種面向對象的語言,C++ 支持函數重載,而過程式語言 C 則不支持。函數被 C++ 編譯后在符號庫中的名字與 C 語言的不同。例如,假設某個函數的原型為:

void foo( int x, int y );

該函數被 C 編譯器編譯后在符號庫中的名字為 _foo,而 C++ 編譯器則會產生像 _foo_int_int 之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為 “mangled name”)。

_foo_int_int 這樣的名字包含了函數名、函數參數數量及類型信息,C++ 就是靠這種機制來實現函數重載的。例如,在 C++ 中,函數 void foo(int x, int y)void foo(int x, float y) 編譯生成的符號是不相同的,后者為 _foo_int_float

同樣地,C++ 中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以 . 來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也為類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。

未加 extern "C" 聲明時的連接方式

假設在 C++ 中,模塊 A 的頭文件如下:

// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo(int x, int y);
#endif

在模塊 B 中引用該函數:

// 模塊B實現文件 moduleB.cpp
#include "moduleA.h"
foo(2, 3);

實際上,在連接階段,連接器會從模塊 A 生成的目標文件 moduleA.obj 中尋找 _foo_int_int 這樣的符號;

extern "C" 聲明后的編譯和連接方式

extern "C" 聲明后,模塊 A 的頭文件變為:

// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo(int x, int y);
#endif

在模塊 B 的實現文件中仍然調用 foo(2, 3),其結果是:

  • 模塊 A 編譯生成 foo 的目標代碼時,沒有對其名字進行特殊處理,采用了 C 語言的方式;
  • 連接器在為模塊 B 的目標代碼尋找 foo(2, 3) 調用時,尋找的是未經修改的符號名 _foo

如果在模塊 A 中函數聲明了 fooextern "C" 類型,而模塊 B 中包含的是 extern int foo(int x, int y),則模塊 B 找不到模塊 A 中的函數;反之亦然。

所以,可以用一句話概括 extern "C" 這個聲明的真實目的:實現 C++ 與 C 及其它語言的混合編程。

原文鏈接:https://juejin.cn/post/7176210457022660664

欄目分類
最近更新