C++ 之頭文件聲明定義

himg

最近在學習 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 也能夠). 可是若是在一個 cc++ 混合使用的大型項目中, 你很難馬上分辨出這是一個 cppheader file 或者是一個 cheader file; 此外, 在 vim 或者 vscode 的語法提示插件看來, .h 就是 c 語言的, 那麼當你在 c 文件寫了 cpp 的某些語法固然會提示不正確 (固然確定仍是能夠編譯經過的)數組

所以, 我認爲最好的處理結果就是若是 header file 中涉及到了任何 c++ 的語法, 那麼這個頭文件就應該以 .hpp 爲後綴, 不然都已 .h 爲後綴markdown

implementation fileheader file 寫什麼內容

理論上來講 implementation fileheader file 裏的內容, 只要是 c++ 語言所支持的, 不管寫什麼均可以的, 好比你在 header file 中寫函數體, 只要在任何一個 implementation file 包含此 header file 就能夠將這個函數編譯成 object 文件的一部分 (編譯是以 implementation file 爲單位的, 若是不在任何 implementation file 中包含此 header file 的話, 這段代碼就形同虛設), 你能夠在 implementation file 中進行函數聲明, 變量聲明, 結構體聲明, 這也不成問題!!!函數

那爲什麼必定要分紅 header fileimplementation file 呢? 爲什麼通常都在 header file 中進行函數, 變量聲明, 宏聲明, 結構體聲明呢? 而在 implementation file 中去進行變量定義, 函數實現呢?oop

緣由以下:  學習

  1. 若是在 header file 中實現一個函數體, 那麼若是在多個 implementation file 中引用它, 並且又同時編譯多個 implementation file, 將其生成的 object file 鏈接成一個可執行文件, 在每一個引用此 header fileimplementation file 所生成的 object file 中, 都有一份這個函數的代碼, 若是這段函數又沒有定義成局部函數, 那麼在鏈接時, 就會發現多個相同的函數, 就會報錯.ui

  2. 若是在 header file 中定義全局變量, 而且將此全局變量賦初值, 那麼在多個引用此 header fileimplementation file 中一樣存在相同變量名的拷貝, 關鍵是此變量被賦了初值, 因此編譯器就會將此變量放入 DATA 段, 最終在鏈接階段, 會在 DATA 段 中存在多個相同的變量, 它沒法將這些變量統一成一個變量, 也就是僅爲此變量分配一個空間, 而不是多份空間, 假定這個變量在 header file 中沒有賦初值, 編譯器就會將之放入 BSS 段, 鏈接器會對 BSS 段 的多個同名變量僅分配一個存儲空間.

  3. 若是在 implementation file 中聲明宏, 結構體, 函數等, 那麼若是要在另外一個 implementation file 中引用相應的宏, 結構體, 就必須再作一次重複的工做, 若是我改了一個 implementation file 中的一個聲明, 那麼又忘了改其它 implementation file 中的聲明, 這不就出了大問題了, 若是把這些公共的東西放在一個頭文件中, 想用它的 implementation file 就只須要引用一個就 OK 了!

  4. 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.cppc.cpp 中用 #include "a.h" 其實是引入相關聲明, 使得編譯能夠經過, 程序並不關心實現是在哪裏, 是怎麼實現的. 源文件編譯後成生成 obj file, 在此文件中, 這些函數和變量就視做一個個符號. 在 link 的時候, 須要在 makefile 裏面說明須要鏈接哪一個 obj 文件 (在這裏是 b.cpp 生成的 .obj 文件), 此時, 鏈接器會去 .obj 文件中找在 b.cpp 中實現的函數, 再把他們 buildmakefile 中指定的那個能夠執行文件中.  

Clion 中, 通常狀況下不須要本身寫 makefile, 只須要將須要的文件都包括在 project 中, Clion 會自動幫你把 makefile 寫好.  

一般, 編譯器會在每一個 .o.obj 文件中都去找一下所須要的符號, 而不是隻在某個文件中找或者說找到一個就不找了. 所以, 若是在幾個不一樣文件中實現了同一個函數, 或者定義了同一個全局變量, 連接的時候就會提示redefined.

什麼是聲明? 什麼是定義?

  • 根據 cpp 標準的規定, 一個變量聲明必須知足兩個條件, 不然就是定義:
    1. 必須使用 extern;
    2. 不能爲變量賦予初始值;
  • 一個變量 / 函數能夠被多處聲明, 可是隻能定義在一處;

是定義仍是聲明與其位於 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;

正確方式

  1. test1.hpp 中聲明 extern int a;
  2. test1.cpp 中定義 int a = 10; (或者使用 int a; 定義, 這樣的話值是默認值 0)
  3. test2.cpp#include "test1.hpp", 這樣即可以在 test2.cpp 中直接使用 a 變量了.

錯誤方式 1

在頭文件 test1.hpp 中直接 extern int a = 10;

這樣屬於在頭文件中直接定義, 咱們已經說了 一個變量能夠被多處聲明, 但只能定義在一處, 在這種狀況下若是有多個 implementation file#include "test1.hpp", 那麼會形成在 obj 文件的 連接 階段發現多處存在同一個變量的定義, 這時會報錯 ld: 1 duplicate symbol for architecture x86_64

同時, 在頭文件中定義一個變量屬於很是業餘的作法, 請不要爭相模仿

錯誤方式 2

在頭文件 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 修飾的變量認爲是同一分內存, 他們其實是兩分內存, 修改其中一個不會影響另一個

static 針對的做用域是編譯模塊, 如何理解?

  • 若是一個 implementation file 及其全部的 #include ... 文件內所組成的一個編譯模塊中有多個 static int a = 0, 那麼會報錯 error: redefinition of 'a'
  • 若是test.hppstatic int a = 0, test1.cpptest2.cpp 分別都有 #include "test.hpp", 那麼這就是兩個編譯模塊各有一個 static int a, 這時是 cpp 容許的, 能夠順利經過編譯並運行的

const

const 單獨使用時它就與 static 相同, 而當與 extern 一塊兒合做的時候, 它的特性就跟 extern 的同樣了

ifndef 的使用與意義

#ifndef 能保證你的頭文件在本編譯模塊只被編譯一次(可是多個模塊都編譯此段代碼的話則仍是會有重複代碼)

總結一些 頭文件 & 聲明 & 定義 的規則

  1. header file 中是對於該模塊接口的聲明, 接口包括該模塊提供給其它模塊調用的外部函數及外部全局變量, 對這些變量和函數都需在 header file 中冠以 extern 關鍵字聲明
  2. 模塊內的函數和全局變量需在 implementation file 開頭冠以 static 關鍵字聲明
  3. 永遠不要在 header file 中定義變量
  4. 若是要用其它模塊定義的變量和函數, 直接 #includeheader file 便可.

若是工程很大, 頭文件不少, 而有幾個頭文件又是常常要用的, 那麼

  1. 把這些頭文件所有寫到一個 header file 裏面去, 好比寫到 preh.h
  2. 寫一個 preh.cpp, 裏面只一句話: #include "preh.h"
  3. 對於 preh.c, 在 project setting 裏面設置 create precompiled headers, 對於其餘 c++ 文件, 設置 use precompiled header file

參考

相關文章
相關標籤/搜索