1. 程式人生 > >C語言中有關外部函式呼叫的問題

C語言中有關外部函式呼叫的問題

首先指出一點,我們通常所說的編譯器並非僅指編譯器,確切來說是編譯工具鏈,裡面包括了預編譯器、編譯器、彙編器和聯結器。

對於外部函式實體(處於呼叫函式所在原始檔之外的其他原始檔中的函式),是在連結過程中,才會被尋找和新增程序序,一旦沒有找到函式實體,就會報錯,無法成功連結。

而外部函式的宣告(一般宣告在標頭檔案中)只是令程式順利通過編譯而已,此時並不需要搜尋到外部函式的實體。

當然,外部函式實體所在原始檔也需要被編譯為目標檔案,至於連結時 如何找到該函式實體,這由連結器完成。

另外,標頭檔案和對應原始檔的命名是沒有任何必然聯絡的。

下面在網上找了一篇博文:

簡單的說其實要理解C檔案與標頭檔案(即.h)有什麼不同之處,首先需要弄明白編譯器的工作過程,一般說來編譯器會做以下幾個過程:


1.預處理階段 
2.詞法與語法分析階段 
3.編譯階段,首先編譯成純彙編語句,再將之彙編成跟CPU相關的二進位制碼,生成各個目標檔案 (.obj檔案)
4.連線階段,將各個目標檔案中的各段程式碼進行絕對地址定位,生成跟特定平臺相關的可執行檔案,當然,最後還可以用objcopy生成純二進位制碼,也就是去掉了檔案格式資訊。(生成.exe檔案)

編譯器在編譯時是以C檔案為單位進行的,也就是說如果你的專案中一個C檔案都沒有,那麼你的專案將無法編譯,聯結器是以目標檔案為單位,它將一個或多個目標檔案進行函式與變數的重定位,生成最終的可執行檔案,在PC上的程式開發,一般都有一個main函式,這是各個編譯器的約定,當然,你如果自己寫聯結器指令碼的話,可以不用main函式作為程式入口!!!!

(main .c檔案 目標檔案 可執行檔案 )

有了這些基礎知識,再言歸正傳,為了生成一個最終的可執行檔案,就需要一些目標檔案,也就是需要C檔案,而這些C檔案中又需要一個main函式作為可執行程式的入口,那麼我們就從一個C檔案入手,假定這個C檔案內容如下: 
#include 
#include "mytest.h"

int main(int argc,char **argv) 

test = 25; 
printf("test.................%d/n",test); 
}

標頭檔案內容如下: 
int test;

現在以這個例子來講解編譯器的工作: 
1.預處理階段:編譯器以C檔案作為一 個單元,首先讀這個C檔案,發現第一句與第二句是包含一個頭檔案,就會在所有搜尋路徑中尋找這兩個檔案,找到之後,就會將相應標頭檔案中再去處理巨集,變數, 函式宣告,巢狀的標頭檔案包含等,檢測依賴關係,進行巨集替換,看是否有重複定義與宣告的情況發生,最後將那些檔案中所有的東東全部掃描進這個當前的C檔案 中,形成一箇中間“C檔案”


2.編譯階段,在上一步中相當於將那個標頭檔案中的test變數掃描進了一箇中 間C檔案,那麼test變數就變成了這個檔案中的一個全域性變數,此時就將所有這個中間C檔案的所有變數,函式分配空間,將各個函式編譯成二進位制碼,按照特 定目標檔案格式生成目標檔案,在這種格式的目標檔案中進行各個全域性變數,函式的符號描述,將這些二進位制碼按照一定的標準組織成一個目標檔案

3.連線階段,將上一步成生的各個目標檔案,根據一些引數,連線生成最終的可 執行檔案,主要的工作就是重定位各個目標檔案的函式,變數等,相當於將個目標檔案中的二進位制碼按一定的規範合到一個檔案中再回到C檔案與標頭檔案各寫什麼內 容的話題上:理論上來說C檔案與標頭檔案裡的內容,只要是C語言所支援的,無論寫什麼都可以的,比如你在標頭檔案中寫函式體,只要在任何一個C檔案包含此頭文 件就可以將這個函式編譯成目標檔案的一部分(編譯是以C檔案為單位的,如果不在任何C檔案中包含此標頭檔案的話,這段程式碼就形同虛設),你可以在C檔案中進 行函式宣告,變數宣告,結構體宣告,這也不成問題!!!那為何一定要分成標頭檔案與C檔案呢?又為何一般都在頭件中進行函式,變數宣告,巨集宣告,結構體宣告 呢?而在C檔案中去進行變數定義,函式實現呢??原因如下: 
1.如果在標頭檔案中實現一個函式體,那麼如果在多個C檔案中引用它,而且又同時編 譯多個C檔案,將其生成的目標檔案連線成一個可執行檔案,在每個引用此標頭檔案的C檔案所生成的目標檔案中,都有一份這個函式的程式碼,如果這段函式又沒有定 義成區域性函式,那麼在連線時,就會發現多個相同的函式,就會報錯 
2.如果在標頭檔案中定義全域性變數,並且將此全域性變數賦初值,那麼在多個引用此 標頭檔案的C檔案中同樣存在相同變數名的拷貝,關鍵是此變數被賦了初值,所以編譯器就會將此變數放入DATA段,最終在連線階段,會在DATA段中存在多個 相同的變數,它無法將這些變數統一成一個變數,也就是僅為此變數分配一個空間,而不是多份空間,假定這個變數在標頭檔案沒有賦初值,編譯器就會將之放入 BSS段,聯結器會對BSS段的多個同名變數僅分配一個儲存空間 
3.如果在C檔案中宣告巨集,結構體,函式等,那麼我要在另一個C檔案中引用相 應的巨集,結構體,就必須再做一次重複的工作,如果我改了一個C檔案中的一個宣告,那麼又忘了改其它C檔案中的宣告,這不就出了大問題了,程式的邏輯就變成 了你不可想象的了,如果把這些公共的東東放在一個頭檔案中,想用它的C檔案就只需要引用一個就OK了!!!這樣豈不方便,要改某個宣告的時候,只需要動一 下標頭檔案就行了 
4.在標頭檔案中宣告結構體,函式等,當你需要將你的程式碼封裝成一個庫,讓別人來用你的程式碼,你又不想公佈原始碼,那麼人家如何利 用你的庫呢?也就是如何利用你的庫中的各個函式呢??一種方法是公佈原始碼,別人想怎麼用就怎麼用,另一種是提供標頭檔案,別人從標頭檔案中看你的函式原型,這 樣人家才知道如何呼叫你寫的函式,就如同你呼叫printf函式一樣,裡面的引數是怎樣的??你是怎麼知道的??還不是看人家的標頭檔案中的相關宣告 啊!!!當然這些東東都成了C標準,就算不看人家的標頭檔案,你一樣可以知道怎麼使用

c語言中.c和.h檔案的困惑
 

本質上沒有任何區別。   只不過一般: 
.h檔案是標頭檔案,內含函式宣告、巨集定義、結構體定義等內容.c檔案是程式檔案,內含函式實現,變數定義等內容。而且是什麼字尾也沒有關係,只不過編譯器會預設對某些字尾的檔案採取某些動作。你可以強制編譯器把任何字尾的檔案都當作c檔案來編。

這樣分開寫成兩個檔案是一個良好的程式設計風格。


而且,比方說 我在aaa.h裡定義了一個函式的宣告,然後我在aaa.h的同一個目錄下建立aaa.c , aaa.c裡定義了這個函式的實現,然後是在main函式所在.c檔案裡#include這個aaa.h  然後我就可以使用這個函數了。 main在執行時就會找到這個定義了這個函式的aaa.c檔案。這是因為:main函式為標準C/C++的程式入口,編譯器會先找到該函式所在的檔案。假定編譯程式編譯myproj.c(其中含main())時,發現它include了mylib.h(其中聲明瞭函式void test()),那麼此時編譯器將按照事先設定的路徑(Include路徑列表及程式碼檔案所在的路徑)查詢與之同名的實現檔案(副檔名為.cpp或.c,此例中為mylib.c),如果找到該檔案,並在其中找到該函式(此例中為void test())的實現程式碼,則繼續編譯;如果在指定目錄找不到實現檔案,或者在該檔案及後續的各include檔案中未找到實現程式碼,則返回一個編譯錯誤.其實include的過程完全可以“看成”是一個檔案拼接的過程,將宣告和實現分別寫在標頭檔案及C檔案中,或者將二者同時寫在標頭檔案中,理論上沒有本質的區別。以上是所謂動態方式。對於靜態方式,基本所有的C/C++編譯器都支援一種連結方式被稱為Static Link,即所謂靜態連結。在這種方式下,我們所要做的,就是寫出包含函式,類等等宣告的標頭檔案(a.h,b.h,...),以及他們對應的實現檔案(a.cpp,b.cpp,...),編譯程式會將其編譯為靜態的庫檔案(a.lib,b.lib,...)。在隨後的程式碼重用過程中,我們只需要提供相應的標頭檔案(.h)和相應的庫檔案(.lib),就可以使用過去的程式碼了。相對動態方式而言,靜態方式的好處是實現程式碼的隱蔽性,即C++中提倡的“介面對外,實現程式碼不可見”。有利於庫檔案的轉發.c檔案和.h檔案的概念與聯絡
      如果說難題最難的部分是基本概念,可能很多人都會持反對意見,但實際上也確實如此。我高中的時候學物理,老師抓的重點就是概念——概念一定要搞清,於是難題也成了容易題。如果你能分析清楚一道物理難題存在著幾個物理過程,每一個過程都遵守那一條物理定律(比如動量守恆、牛II定律、能量守恆),那麼就很輕鬆的根據定律列出這個過程的方程,N個過程必定是N個N元方程,難題也就迎刃而解。即便是高中的物理競賽難題,最難之處也不過在於:

(1)、混淆你的概念,讓你無法分析出幾個物理過程,或某個物理過程遵循的那條物理定律;
(2)、存在高次方程,列出方程也解不出。而後者已經是數學的範疇了,所以說,最難之處還在於掌握清晰的概念;

  程式設計也是如此,如果概念很清晰,那基本上沒什麼難題(會難在數學上,比如演算法的選擇、時間空間與效率的取捨、穩定與資源的平衡上)。但是,要掌握清晰的概念也沒那麼容易。比如下面這個例子,看看你有沒有很清晰透徹的認識。

//a.h
void foo();

//a.c
#include "a.h"   //我的問題出來了:這句話是要,還是不要?
void foo()
{
     return;
}

//main.c
#include "a.h"
int main(int argc, char *argv[])
{
    foo(); 
  return 0;
}                
針對上面的程式碼,請回答三個問題:

a.c 中的 #include "a.h" 這句話是不是多餘的? 
為什麼經常見 xx.c 裡面 include 對應的 xx.h? 
如果 a.c 中不寫,那麼編譯器是不是會自動把 .h 檔案裡面的東西跟同名的 .c 檔案繫結在一起? 
(請針對上面3道題仔細考慮10分鐘,莫要著急看下面的解釋。:) 考慮的越多,下面理解的就越深。)

  好了,時間到!請忘掉上面的3道題,以及對這三道題引發出的你的想法,然後再聽我慢慢道來。正確的概念是:從C編譯器角度看,.h和.c皆是浮雲,就是改名為.txt、.doc也沒有大的分別。換句話說,就是.h和.c沒啥必然聯絡。.h中一般放的是同名.c檔案中定義的變數、陣列、函式的宣告,需要讓.c外部使用的宣告。這個宣告有啥用?只是讓需要用這些宣告的地方方便引用。因為 #include "xx.h" 這個巨集其實際意思就是把當前這一行刪掉,把 xx.h 中的內容原封不動的插入在當前行的位置。由於想寫這些函式宣告的地方非常多(每一個呼叫 xx.c 中函式的地方,都要在使用前宣告一下子),所以用 #include "xx.h" 這個巨集就簡化了許多行程式碼——讓前處理器自己替換好了。也就是說,xx.h 其實只是讓需要寫 xx.c 中函式宣告的地方呼叫(可以少寫幾行字),至於 include 這個 .h 檔案是誰,是 .h 還是 .c,還是與這個 .h 同名的 .c,都沒有任何必然關係。
  這樣你可能會說:啊?那我平時只想呼叫 xx.c 中的某個函式,卻 include了 xx.h 檔案,豈不是巨集替換後出現了很多無用的宣告?沒錯,確實引入了很多垃圾,但是它卻省了你不少筆墨,並且整個版面也看起來清爽的多。魚與熊掌不可得兼,就是這個道理。反正多些宣告(.h一般只用來放宣告,而放不定義,參見拙著“過馬路,左右看”)也無害處,又不會影響編譯,何樂而不為呢?
翻回頭再看上面的3個問題,很好解答了吧?

答:不一定。這個例子中顯然是多餘的。但是如果.c中的函式也需要呼叫同個.c中的其它函式,那麼這個.c往往會include同名的.h,這樣就不需要為宣告和呼叫順序而發愁了(C語言要求使用之前必須宣告,而include同名.h一般會放在.c的開頭)。有很多工程甚至把這種寫法約定為程式碼規範,以規範出清晰的程式碼來。 
答:1中已經回答過了。 
答:不會。問這個問題的人絕對是概念不清,要不就是想混水摸魚。非常討厭的是中國的很多考試出的都是這種爛題,生怕別人有個清楚的概念了,絕對要把考生搞暈。 
搞清楚語法和概念說易也易,說難也難。竅門有三點:

不要暈著頭工作,要抽空多思考思考,多看看書; 
看書要看好書,問人要問強人。爛書和爛人都會給你一個錯誤的概念,誤導你; 
勤能補拙是良訓,一分辛苦一分才; 
 

(1)通過標頭檔案來呼叫庫功能。在很多場合,原始碼不便(或不準)向用戶公佈,只要向用戶提供標頭檔案和二進位制的庫即可。使用者只需要按照標頭檔案中的介面宣告來呼叫庫功能,而不必關心介面怎麼實現的。編譯器會從庫中提取相應的程式碼。
(2)標頭檔案能加強型別安全檢查。如果某個介面被實現或被使用時,其方式與標頭檔案中的宣告不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式設計師除錯、改錯的負擔。
標頭檔案用來存放函式原型。

標頭檔案如何來關聯原始檔?

 這個問題實際上是說,已知標頭檔案“a.h”聲明瞭一系列函式(僅有函式原型,沒有函式實現),“b.cpp”中實現了這些函式,那麼如果我想在“c.cpp”中使用“a.h”中宣告的這些在“b.cpp”中實現的函式,通常都是在“c.cpp”中使用#include “a.h”,那麼c.cpp是怎樣找到b.cpp中的實現呢? 
其實.cpp和.h檔名稱沒有任何直接關係,很多編譯器都可以接受其他副檔名。 
譚浩強老師的《C程式設計》一書中提到,編譯器預處理時,要對#include命令進行“檔案包含處理”:將headfile.h的全部內容複製到#include “headfile.h”處。這也正說明了,為什麼很多編譯器並不care到底這個檔案的字尾名是什麼----因為#include預處理就是完成了一個“複製並插入程式碼”的工作。 
程式編譯的時候,並不會去找b.cpp檔案中的函式實現,只有在link的時候才進行這個工作。我們在b.cpp或c.cpp中用#include “a.h”實際上是引入相關宣告,使得編譯可以通過,程式並不關心實現是在哪裡,是怎麼實現的。原始檔編譯後成生了目標檔案(.o或.obj檔案),目標檔案中,這些函式和變數就視作一個個符號。在link的時候,需要在makefile裡面說明需要連線哪個.o或.obj檔案(在這裡是b.cpp生成的.o或.obj檔案),此時,聯結器會去這個.o或.obj檔案中找在b.cpp中實現的函式,再把他們build到makefile中指定的那個可以執行檔案中。 
在VC中,一幫情況下不需要自己寫makefile,只需要將需要的檔案都包括在project中,VC會自動幫你把makefile寫好。 
通常,編譯器會在每個.o或.obj檔案中都去找一下所需要的符號,而不是隻在某個檔案中找或者說找到一個就不找了。因此,如果在幾個不同檔案中實現了同一個函式,或者定義了同一個全域性變數,連結的時候就會提示“redefined”.


相關推薦

C語言有關外部函式呼叫的問題

首先指出一點,我們通常所說的編譯器並非僅指編譯器,確切來說是編譯工具鏈,裡面包括了預編譯器、編譯器、彙編器和聯結器。 對於外部函式實體(處於呼叫函式所在原始檔之外的其他原始檔中的函式),是在連結過程中,才會被尋找和新增程序序,一旦沒有找到函式實體,就會報錯,無法成功連結。

C語言的內部函式外部函式

內部函式:   如果一個函式只能被本檔案中其它函式所呼叫,它稱為內部函式。在定義內部函式時,在函式名和函式型別的前面加static。即   static 型別識別符號 函式名 (形參表) 如: static int fun (int a, int b) 內部函式又稱靜態函

c++可以把任意基本型別轉換為string, 類似於c 語言的 sprintf函式

//c++中按照格式輸入輸出 類似於c 語言中的 sprintf函式 #include<iostream> #include<sstream>//std::stringstream 標頭檔案 int main() { std::string str = "高海文"

c語言有關FatFs的操作程式碼

文章完全是從http://blog.csdn.net/qsycn/article/details/9226403貼上過來的,怕原版找不到,所以自己貼上過來一份。 FatFs FatFS是一個為小型嵌入式系統設計的通用FAT(File Allocation Table)檔案系統模組。F

C語言變數和函式的宣告與定義

一、變數在將變數前,先解釋一下宣告和定義這兩個概念。宣告一個變數意味著向編譯器描述變數的型別,但並不為變數分配儲存空間。定義一個變數意味著在宣告變數的同時還要為變數分配儲存空間。在定義一個變數的同時還可以對變數進行初始化。 區域性變數通常只定義不宣告,而全域性變數多在原始檔中定義,在標頭檔案中宣告。 區域性變

C++語言外部變數引用和char[],char*的若干問題探究

今天覆習了一下C++的知識點,發現了一些有疑問的地方,查看了一些資料,得到一些結論,記錄下來。1,如果在一個工程目錄下,存在多個頭檔案,這些標頭檔案裡面是否可以包含相同的變數名呢?思考:標頭檔案給我們的資訊其實就是介面資訊,我們呼叫一個頭檔案其實就是呼叫跟它相關的原始檔,原始

C++類一個建構函式呼叫另一個建構函式

class A { int a; int b; int c; public: A(int aa, int bb) : a(aa), b(bb),c(0) { cout << "aa bb" << endl; } A(int aa, in

C語言返回字串函式的四種實現方法

其實就是要返回一個有效的指標,尾部變數退出後就無效了。 使用分配的記憶體,地址是有效 char   *fun() {         char*   s   =   (char*)calloc(100,   sizeof(char*)   );         if   (s)                

C語言有關字串的程式設計題

(1)字串拷貝strcpy 特點:只能拷貝字串,遇到\0停止拷貝 char* MyStrcpy(char* dst,const char* src) { assert(dst != NU

關於C語言一些常用函式的說明

1.I/O函式 (1)scanf函式: int scanf(const char *format…..); 從標準輸入流stdin中按格式format將資料寫到引數表中;若操作成功,返回寫到引數表中的引數個數,否則返回EOF; 注意以下幾點: ①scanf函式沒

C語言使用靜態函式的好處

          靜態函式會被自動分配在一個一直使用的儲存區,直到退出應用程式例項,避免了呼叫函式時壓棧出棧,速度快很多。 關鍵字“static”,譯成中文就是“靜態的”,所以內部函式又稱靜態函式。但此處“static”的含義不是指儲存方式,而是指對函式的作用域僅侷限於本

C語言常用的函式

字串函式     複製函式strcpy()  格式 strcpy(字串陣列名,"字串");     連線函式strcat(字串陣列1,字串陣列2)   格式  strcat(字串1,字串2);     比較函式strcmp()  格式 strcmp(字串1,字串2);  

C語言中static的作用及C語言使用靜態函式有何好處

在C語言中,static的字面意思很容易把我們匯入歧途,其實它的作用有三條,分別是: 一是隱藏功能,對於static修飾的函式和全域性變數而言 二是保持永續性功能,對於static修飾的區域性變數而言。 三是因為存放在靜態區,全域性和區域性的static修飾的變數,都預設初始化為0 下面我逐一給

C語言檔案操作函式彙總

#include <stdio.h> #include <stdlib.h> int main() { FILE* fd = fopen("test.txt","r"); if(NULL == fd)//檔案開啟失敗 { perror("fope

C語言有關兩個數,值的交換

一、開講    最近在系統的學習C語言,雖說以前在大學裡學習過,但是人嘛,時間一久就會忘了。由於行業的原因,我學習了iOS,但是想學習iOS,C就是萬萬不能忘的。於是經過幾天的時間,我終於有了一點小成就,就是有關在C裡面的兩個值的交換,這裡或許只是一部分,希望還有知道方法

C語言的子函式和主函式有什麼聯絡啊?它們是怎麼編寫的?

函式定義的一般形式1.無參函式的一般形式 型別說明符 函式名() { 型別說明 語句 }  其中型別說明符和函式名稱為函式頭。 型別說明符指明瞭本函式的型別,函式的型別實際上是函式返回值的型別。 該型別說明符與第二章介紹的各種說明符相同。 函式名是由使用者定義的識別符號,函式名後有一個空括號,其中無引數,但括

C語言沒有main函式生成可執行程式的幾種方法

轉自:http://www.linuxidc.com/Linux/2013-09/90061.htm 1、define預處理指令 這種方式很簡單,只是簡單地將main字串用巨集來代替,或者使用##拼接字串。示例程式如下: #include <stdio.h>

C語言的靜態函式的作用

轉載 在C語言中為什麼要用靜態函式(static function)? 如果不用這個static關鍵字,好象沒有關係。那麼,用了static以後,有什麼作用呢? 我們知道,用了static的變數,叫做靜態變數,其意義是,該變數的值在下次呼叫時,還繼續保留前次呼叫時的值。

C語言通過sprintf()函式構造sql語句

一、C語言如何構造sql 做專案時,由嵌入式開發慢慢涉及到後臺開發,接觸資料庫慢慢就多了,一般情況下,sql定義成一個char *,或者一個字元陣列,裡面就寫sql語句就行了。例如: char *sql; sql = "create ta

C語言函式呼叫怎麼返回兩個值

在C語言中,函式只能返回一個值,要返回兩個值,可以改換思路,通過其它方式做到。 1 建立陣列,返回指標。 在要返回的兩個值型別相同時,可以用建立陣列的方式,將要返回的值存在陣列中,並返回陣列首地址,這樣就可以實現返回兩個值的效果。 需要注意的是,要返回陣列首地址,那麼返回