最近在學習 c++
, 在編譯與連接過程當中遇到了一些定義與聲明的問題, 通過多處查閱資料, 基本解惑. 現記錄與此, 但願讓後面人少走些彎路.html
C++
的頭文件應該用什麼擴展名?目前業界的經常使用格式以下:c++
implementation file
*.cpp
*.cc
*.cc
*.c
header file
*.hpp
*.h++
*.hh
*.hxx
*.h
一句話: 建議 源文件使用 .cpp
, 頭文件使用 .hpp
程序員
關於 implementation file
並無什麼說的, 使用 .cpp
/.cc
都是能夠的. 可是 header file
須要注意.vim
c 的頭文件格式是 .h
, 認爲 h
表明 header
, 因而有不少人也喜歡在 c++ 用 .h
做爲頭文件擴展名. 其實擴展名並不影響編譯結果, 對於編譯器來講擴展名是不重要的 (甚至使用 .txt
也能夠). 可是若是在一個 c
與 c++
混合使用的大型項目中, 你很難馬上分辨出這是一個 cpp
的 header file
或者是一個 c
的header file
; 此外, 在 vim
或者 vscode
的語法提示插件看來, .h
就是 c 語言的, 那麼當你在 c 文件寫了 cpp 的某些語法固然會提示不正確 (固然確定仍是能夠編譯經過的)數組
所以, 我認爲最好的處理結果就是若是 header file
中涉及到了任何 c++
的語法, 那麼這個頭文件就應該以 .hpp
爲後綴, 不然都已 .h
爲後綴markdown
implementation file
與 header file
寫什麼內容理論上來講 implementation file
與 header file
裏的內容, 只要是 c++ 語言所支持的, 不管寫什麼均可以的, 好比你在 header file
中寫函數體, 只要在任何一個 implementation file
包含此 header file
就能夠將這個函數編譯成 object
文件的一部分 (編譯是以 implementation file
爲單位的, 若是不在任何 implementation file
中包含此 header file
的話, 這段代碼就形同虛設), 你能夠在 implementation file
中進行函數聲明, 變量聲明, 結構體聲明, 這也不成問題!!!函數
那爲什麼必定要分紅 header file
與 implementation file
呢? 爲什麼通常都在 header file
中進行函數, 變量聲明, 宏聲明, 結構體聲明呢? 而在 implementation file
中去進行變量定義, 函數實現呢?oop
緣由以下: 學習
若是在 header file
中實現一個函數體, 那麼若是在多個 implementation file
中引用它, 並且又同時編譯多個 implementation file
, 將其生成的 object file
鏈接成一個可執行文件, 在每一個引用此 header file
的 implementation file
所生成的 object file
中, 都有一份這個函數的代碼, 若是這段函數又沒有定義成局部函數, 那麼在鏈接時, 就會發現多個相同的函數, 就會報錯.ui
若是在 header file
中定義全局變量, 而且將此全局變量賦初值, 那麼在多個引用此 header file
的 implementation file
中一樣存在相同變量名的拷貝, 關鍵是此變量被賦了初值, 因此編譯器就會將此變量放入 DATA 段
, 最終在鏈接階段, 會在 DATA 段
中存在多個相同的變量, 它沒法將這些變量統一成一個變量, 也就是僅爲此變量分配一個空間, 而不是多份空間, 假定這個變量在 header file
中沒有賦初值, 編譯器就會將之放入 BSS 段
, 鏈接器會對 BSS 段
的多個同名變量僅分配一個存儲空間.
若是在 implementation file
中聲明宏, 結構體, 函數等, 那麼若是要在另外一個 implementation file
中引用相應的宏, 結構體, 就必須再作一次重複的工做, 若是我改了一個 implementation file
中的一個聲明, 那麼又忘了改其它 implementation file
中的聲明, 這不就出了大問題了, 若是把這些公共的東西放在一個頭文件中, 想用它的 implementation file
就只須要引用一個就 OK 了!
在 header file
中聲明結構體, 函數等, 當你須要將你的代碼封裝成一個庫, 讓別人來用你的代碼, 你又不想公佈源碼, 那麼人家如何利用你的庫中的各個函數呢? ? 一種方法是公佈源碼, 別人想怎麼用就怎麼用, 另外一種是提供 header file
, 別人從 header file
中看你的函數原型, 這樣人家才知道如何調用你寫的函數, 就如同你調用 printf
函數同樣, 裏面的參數是怎樣的? 你是怎麼知道的? 還不是看人家的頭文件中的相關聲明!
已知 header file
a.h
聲明瞭一系列函數 (僅有函數原型, 沒有函數實現), b.cpp
中實現了這些函數, 那麼若是我想在 c.cpp
中使用 a.h
中聲明的這些在 b.cpp
中實現的函數, 一般都是在 c.cpp
中使用 #include "a.h"
, 那麼 c.cpp
是怎樣找到 b.cpp
中的實現呢?
編譯器預處理時, 要對 #include
命令進行 文件包含處理: 將 a.h
的所有內容複製到#include "a.h"
處. 這也正說明了, 爲何不少編譯器並不 care 到底這個文件的後綴名是什麼 - 由於 #include
預處理就是完成了一個 複製並插入代碼 的工做.
程序編譯的時候, 並不會去找 b.cpp
文件中的函數實現, 只有在 link
的時候才進行這個工做. 咱們在 b.cpp
或 c.cpp
中用 #include "a.h"
其實是引入相關聲明, 使得編譯能夠經過, 程序並不關心實現是在哪裏, 是怎麼實現的. 源文件編譯後成生成 obj file
, 在此文件中, 這些函數和變量就視做一個個符號. 在 link
的時候, 須要在 makefile
裏面說明須要鏈接哪一個 obj
文件 (在這裏是 b.cpp
生成的 .obj
文件), 此時, 鏈接器會去 .obj
文件中找在 b.cpp
中實現的函數, 再把他們 build
到 makefile
中指定的那個能夠執行文件中.
在 Clion
中, 通常狀況下不須要本身寫 makefile
, 只須要將須要的文件都包括在 project 中, Clion
會自動幫你把 makefile 寫好.
一般, 編譯器會在每一個 .o
或 .obj
文件中都去找一下所須要的符號, 而不是隻在某個文件中找或者說找到一個就不找了. 所以, 若是在幾個不一樣文件中實現了同一個函數, 或者定義了同一個全局變量, 連接的時候就會提示redefined
.
是定義仍是聲明與其位於
header file
仍是implementation file
無關.
根據以上規定, 咱們能夠有以下的結論:
extern int a; // 聲明
int a; // 定義
int a = 0; // 定義
extern int a = 0; // 定義
複製代碼
許多程序員對定義變量和聲明變量混淆不清, 定義變量和聲明變量的區別在於定義會產生內存分配的操做, 是彙編階段的概念; 而聲明則只是告訴包含該聲明的模塊在鏈接階段從其它模塊尋找外部函數和變量.
咱們在編譯模塊中的任意一個文件中書寫的變量/函數在此模塊中其餘文件中均可以被訪問到, 可是其餘編譯模塊的文件是沒有訪問此變量的權限的. 那麼如何跨模塊共享變量 / 函數呢?
答案就是使用 extern
. 在這裏請在作的各位緊緊記住它的定義: 標示所修飾的變量或函數的可能位於其餘模塊.
必定要緊緊記住上面的定義, 帶着定義咱們就能夠想明白如下問題
- 爲何在一個
implementation file
中使用一個外部變量要先extern
聲明該變量 (或者導入該變量所在的header file
)?- 爲何
header file
中要使用 extern 聲明一個變量?
這樣當咱們編譯某個單元時, 編譯器發現了使用 extern
修飾的變量, 若是正好本模塊中有其相關定義, 那麼就直接使用; 若是沒有相關定義, 那麼就掛起, 在編譯後續其餘模塊的時候進行查找, 若是到最後尚未找到, 那麼在連接階段就會報錯 ld: symbol(s) not found for architecture x86_64
;
test1.hpp
中聲明 extern int a;
test1.cpp
中定義 int a = 10;
(或者使用 int a;
定義, 這樣的話值是默認值 0)test2.cpp
中 #include "test1.hpp"
, 這樣即可以在 test2.cpp
中直接使用 a 變量了.在頭文件 test1.hpp
中直接 extern int a = 10;
這樣屬於在頭文件中直接定義, 咱們已經說了 一個變量能夠被多處聲明, 但只能定義在一處
, 在這種狀況下若是有多個 implementation file
都 #include "test1.hpp"
, 那麼會形成在 obj
文件的 連接
階段發現多處存在同一個變量的定義, 這時會報錯 ld: 1 duplicate symbol for architecture x86_64
同時, 在頭文件中定義一個變量屬於很是業餘的作法, 請不要爭相模仿
在頭文件 test1.hpp
中 直接 extern int a = 10;
, 在 test2.cpp
中直接使用 extern int a;
(沒有 #include test1.hpp
)
這樣作能夠避免多處重複定義的問題, 可是這樣的話 test1.hpp
定義的其餘變量與方法都不可使用了, 必須所有使用 extern ***
的形式進行聲明而後使用, 這樣會及其得不償失.
因此咱們能夠得出結論:
真理老是這麼簡單!
函數與變量相似, 也分爲定義與聲明. 可是與變量在聲明時必需要包含 extern
不一樣, 因爲函數的定義和聲明是有區別的, 定義函數要有函數體, 聲明函數沒有函數體, 因此函數定義和聲明時均可以將 extern
省略掉, 反正其餘文件也是知道這個函數是在其餘地方定義的, 因此不加 extern
也行.
因此在 cpp 中, 若是在一個函數前添加了 extern, 那麼僅表示此函數可能在別的模塊中定義; 或者也可讓咱們在只使用了某個頭文件的這個方法時不用
#include <***.hpp>
static
使用static
用於修飾類中的變量/函數是一個靜態成員變量/函數
static
用於修飾類以外的變量/函數是一個普通的全局靜態成員變量/函數
extern
聲明也不能夠), 不是真正意義的全局(普通的函數默認是 extern
的)static
, 好比函數中要返回一個數組, 不想讓這個數組函數結束時被釋放, 那麼可使用 static 修飾此局部變量static
使變量只在本編譯模塊內部可見, 這樣的話若是兩個編譯模塊各自都有一個 value
變量的話, 那麼千萬不要將兩個編譯模塊內 static 修飾的變量認爲是同一分內存, 他們其實是兩分內存, 修改其中一個不會影響另一個
implementation file
及其全部的 #include ...
文件內所組成的一個編譯模塊中有多個 static int a = 0
, 那麼會報錯 error: redefinition of 'a'
test.hpp
有 static int a = 0
, test1.cpp
與 test2.cpp
分別都有 #include "test.hpp"
, 那麼這就是兩個編譯模塊各有一個 static int a
, 這時是 cpp 容許的, 能夠順利經過編譯並運行的當 const
單獨使用時它就與 static
相同, 而當與 extern
一塊兒合做的時候, 它的特性就跟 extern
的同樣了
#ifndef
能保證你的頭文件在本編譯模塊只被編譯一次(可是多個模塊都編譯此段代碼的話則仍是會有重複代碼)
頭文件
& 聲明
& 定義
的規則header file
中是對於該模塊接口的聲明, 接口包括該模塊提供給其它模塊調用的外部函數及外部全局變量, 對這些變量和函數都需在 header file
中冠以 extern
關鍵字聲明implementation file
開頭冠以 static
關鍵字聲明header file
中定義變量#include
其 header file
便可.若是工程很大, 頭文件不少, 而有幾個頭文件又是常常要用的, 那麼
- 把這些頭文件所有寫到一個
header file
裏面去, 好比寫到 preh.h- 寫一個
preh.cpp
, 裏面只一句話:#include "preh.h"
- 對於
preh.c
, 在 project setting 裏面設置create precompiled headers
, 對於其餘 c++ 文件, 設置use precompiled header file