c++ 中.h 和.cpp

weixin_41874599發表於2019-02-20

標頭檔案(.h):

    寫類的宣告(包括類裡面的成員和方法的宣告)、函式原型、#define常數等,但一般來說不寫出具體的實現。

    在寫標頭檔案時需要注意,在開頭和結尾處必須按照如下樣式加上預編譯語句(如下):

 

#ifndef CIRCLE_H
#define CIRCLE_H

//你的程式碼寫在這裡

#endif

 

    這樣做是為了防止重複編譯,不這樣做就有可能出錯。

    至於CIRCLE_H這個名字實際上是無所謂的,你叫什麼都行,只要符合規範都行。原則上來說,非常建議把它寫成這種形式,因為比較容易和標頭檔案的名字對應。

   原始檔(.cpp):

    原始檔主要寫實現標頭檔案中已經宣告的那些函式的具體程式碼。需要注意的是,開頭必須#include一下實現的標頭檔案,以及要用到的標頭檔案。那麼當你需要用到自己寫的標頭檔案中的類時,只需要#include進來就行了。

    下面舉個最簡單的例子來描述一下,我們就求個圓面積。

     第1步,建立一個空工程(以在VS2003環境下為例)。

     第2步,在標頭檔案的資料夾裡新建一個名為Circle.h的標頭檔案,它的內容如下:

 

#ifndef CIRCLE_H
#define CIRCLE_H

class Circle
{
private:
    double r;//半徑
public:
    Circle();//建構函式
    Circle(double R);//建構函式
    double Area();//求面積函式
};

#endif

   注意到開頭結尾的預編譯語句。在標頭檔案裡,並不寫出函式的具體實現。

    第3步,要給出Circle類的具體實現,因此,在原始檔夾裡新建一個Circle.cpp的檔案,它的內容如下:

 

#include "Circle.h"

Circle::Circle()
{
    this->r=5.0;
}

Circle::Circle(double R)
{
    this->r=R;
}

double Circle:: Area()
{
    return 3.14*r*r;
}

    需要注意的是:開頭處包含了Circle.h,事實上,只要此cpp檔案用到的檔案,都要包含進來!這個檔案的名字其實不一定要叫Circle.cpp,但非常建議cpp檔案與標頭檔案相對應。

    最後,我們建一個main.cpp來測試我們寫的Circle類,它的內容如下:

 

#include <iostream>
#include "Circle.h"
using namespace std;

int main()
{
    Circle c(3);
    cout<<"Area="<<c.Area()<<endl;
    return 1;
}

    注意到開頭時有#include "Circle.h"的宣告,證明我們使用到了我們剛才寫的Circle類。

   至此,我們工程的結構為:

    執行一下,輸出結果為:

   說明我們寫的Circle類確實可以用了。

1..h叫做標頭檔案,它是不能被編譯的。“#include”叫做編譯預處理指令,可以簡單理解成,在1.cpp中的#include"1.h"指令把1.h中的程式碼在編譯前新增到了1.cpp的頭部。每個.cpp檔案會被編譯,生成一個.obj檔案,然後所有的.obj檔案連結起來你的可執行程式就算生成了。

發現了沒有,你要在.h檔案中嚴格區分宣告語句和定義語句。好的習慣是,標頭檔案中應只處理常量、變數、函式以及類等等等等的宣告,變數的定義和函式的實現等等等等都應該在原始檔.cpp中進行。

至於.h和.cpp具有同樣的主檔名的情況呢,對編譯器來講是沒有什麼意義的,編譯器不會去匹配二者的主檔名,相反它很傻,只認#include等語句。但是這樣寫是一種約定俗成的程式設計風格,一個類的名字作為其標頭檔案和原始檔的主檔名比如Class1.h和Class1.cpp,這個類的宣告在Class1.h中,實現在Class1.cpp中,我們人類看起來比較整齊,讀起來方便,也很有利於模組化和原始碼的重用。

為什麼這個風格會約定俗成?有一句著名的話,叫“程式是為程式設計師寫的”。

2.h檔案和cpp檔案也就是說,在h檔案中宣告Declare,而在cpp檔案中定義Define。 “宣告”向計算機介紹名字,它說,“這個名字是什麼意思”。而“定義”為這個名字分配儲存空間。無論涉及到變數時還是函式時含義都一樣。無論在哪種情況下,編譯器都在“定義”處分配儲存空間。對於變數,編譯器確定這個變數佔多少儲存單元,並在記憶體中產生存放它們的空間。對於函式,編譯器產生程式碼,併為之分配儲存空間。函式的儲存空間中有一個由使用不帶參數列或帶地址操作符的函式名產生的指標。定義也可以是宣告。如果該編譯器還沒有看到過名字A,程式設計師定義int A,則編譯器馬上為這個名字分配儲存地址。宣告常常使用於extern關鍵字。如果我們只是宣告變數而不是定義它,則要求使用extern。對於函式宣告, extern是可選的,不帶函式體的函式名連同參數列或返回值,自動地作為一個宣告。

另篇:

在C++程式設計過程中,隨著專案的越來越大,程式碼也會越來越多,並且難以管理和分析。於是,在C++中就要分出了頭(.h)檔案和實現(.cpp)檔案,並且也有了Package的概念。

對於以C起步,C#作為“母語”的我剛開始跟著導師學習C++對這方面還是感到很模糊。雖然我可以以C的知識面對C++的語法規範,用C#的思想領悟C++中類的使用。但是C#中定義和實現是都在一個檔案中(其實都是在類裡面),而使用C的時候也只是程式設計的剛剛起步,所寫的程式也只要一個檔案就夠了。因此對於C++的Package理解以及.h檔案和.cpp檔案的總是心存糾結。

幸好導師有詳細的PPT讓我瞭解,一次對於Package的認識就明白多了。簡單講,一個Package就是由同名的.h和.cpp檔案組成。當然可以少其中任意一個檔案:只有.h檔案的Package可以是介面或模板(template)的定義;只有.cpp檔案的Package可以是一個程式的入口。

當然更具體詳細的講解,歡迎下載導師的教學PPT-Package來了解更多。

不過我在這裡想講的還是關於.h檔案和.cpp檔案

知道Package只是相對比較巨集觀的理解:我們在專案中以Package為編輯物件來擴充套件和修正我們的程式。編寫程式碼時具體到應該把什麼放到.h檔案,又該什麼放在.cpp檔案中,我又迷惑了。

雖然Google給了我很多的連結,但是大部分的解釋都太籠統了:申明寫在.h檔案,定義實現寫在.cpp檔案。這個解釋沒有差錯,但是真正下手起來,又會發現不知道該把程式碼往哪裡打。

於是我又把這個問題拋給了導師,他很耐心地給我詳詳細細地表述瞭如何在C++中進行程式碼分離。很可惜,第一次我聽下了,但是沒有聽太懂,而且本來對C++就瞭解不深,所以也沒有深刻的印象。

經過幾個專案的試煉和體驗之後,我又拿出這個問題問導師,他又一次耐心地給我講解了一遍(我發誓他絕對不是忘記了我曾經問過同樣的問題),這次我把它記錄了下來。

為了不再忘記,我將它們總結在這裡。

概覽

  非模板型別(none-template) 模板型別(template)
標頭檔案(.h)
  • 全域性變數申明(帶extern限定符)
  • 全域性函式的申明
  • inline限定符的全域性函式的定義
  • 類的定義
  • 類函式成員和資料成員的申明(在類內部)
  • 類定義內的函式定義(相當於inline)
  • static const限定符的資料成員在類內部的初始化
  • inline限定符的類定義外的函式定義
  • 模板類的定義
  • 模板類成員的申明和定義(定義可以放在類內或者類外,類外不需要寫inline)
實現檔案(.cpp)
  • 全域性變數的定義(及初始化)
  • 全域性函式的定義
(無)
  • 類函式成員的定義
  • 類帶static限定符的資料成員的初始化

*申明:declaration
*定義:definition

標頭檔案

標頭檔案的所有內容,都必須包含在

#ifndef {Filename} 
#define {Filename} 

//{Content of head file} 

#endif

這樣才能保證標頭檔案被多個其他檔案引用(include)時,內部的資料不會被多次定義而造成錯誤

inline限定符

在標頭檔案中,可以對函式用inline限定符來告知編譯器,這段函式非常的簡單,可以直接嵌入到呼叫定義之處。

當然inline的函式並不一定會被編譯器作為inline來實現,如果函式過於複雜,編譯器也會拒絕inline。

因此簡單說來,程式碼最好短到只有3-5行的才作為inline。有迴圈,分支,遞迴的函式都不要用做inline。

對於在類定義內定義實現的函式,編譯器自動當做有inline請求(也是不一定inline的)。因此在下邊,我把帶有inline限定符的函式成員和寫在類定義體內的函式成員統稱為“要inline的函式成員”

非模板型別

全域性型別

就像前面籠統的話講的:申明寫在.h檔案。

對於函式來講,沒有實現體的函式,就相當於是申明;而對於資料型別(包括基本型別和自定義型別)來說,其申明就需要用extern來修飾。

然後在.cpp檔案裡定義、實現或初始化這些全域性函式和全域性變數。

不過導師一直反覆強調:不許使用全域性函式和全域性變數。用了之後造成的後果,目前就是交上去的作業專案會扣分。當然不能用自有不能用的理由以及解決方案,不過不在目前的討論範圍內。

 

自定義型別

對於自定義型別,包括類(class)和結構體(struct),它們的定義都是放在.h檔案中。其成員的申明和定義就比較複雜了,不過看上邊的表格,還是比較清晰的。

函式成員

函式成員無論是否帶有static限定符,其申明都放在.h檔案的類定義內部。

對於要inline的函式成員其定義放在.h檔案;其他函式的實現都放在.cpp檔案中。

資料成員

資料成員的申明與定義都是放在.h檔案的類定義內部。對於資料型別,關鍵問題是其初始化要放在什麼地方進行。

對於只含有static限定符的資料成員,它的初始化要放在.cpp檔案中。因為它是所有類物件共有的,因此必須對它做合適的初始化。

對於只含有const限定符的資料成員,它的初始化只能在建構函式的初始化列表中完成。因為它是一經初始化就不能重新賦值,因此它也必須進行合適的初始化。

對於既含有static限定符,又含有const限定符的資料成員,它的初始化和定義同時進行。它也是必須進行合適的初始化

對於既沒有static限定符,又沒有const限定符的資料成員,它的值只針對本物件可以隨意修改,因此我們並不在意它的初始化什麼時候進行。

 

模板型別

C++中,模板是一把開發利器,它與C#,Java的泛型很相似,卻又不盡相同。以前,我一直只覺得像泛型,模板這種東西我可能一輩子也不可能需要使用到。但是在導師的強制逼迫使用下,我才真正體會到模板的強大,也真正知道要如何去使用模板,更進一步是如何去設計模板。不過這不是三言兩語可以講完的,就不多說了。

對於模板,最重要的一點,就是在定義它的時候,編譯器並不會對它進行編譯,因為它沒有一個實體可用。

只有模板被具體化(specialization)之後(用在特定的型別上),編譯器才會根據具體的型別對模板進行編譯。

所以才定義模板的時候,會發現編譯器基本不會報錯(我當時還很開心的:我寫程式碼盡然會沒有錯誤,一氣呵成),也做不出智慧提示。但是當它被具體用在一個類上之後,錯誤就會大片大片的出現,卻往往無法準確定位。

因此設計模板就有設計模板的一套思路和方式,但是這跟本文的主題也有偏。

 

因為模板的這種特殊性,它並沒有自己的準確定義,因此我們不能把它放在.cpp檔案中,而要把他們全部放在.h檔案中進行書寫。這也是為了在模板具體化的時候,能夠讓編譯器可以找到模板的所有定義在哪裡,以便真正的定義方法。

至於模板類函式成員的定義放在哪裡,導師的意見是放在類定義之外,因為這樣當你看類的時候,一目瞭然地知道有那些方法和資料;我在用Visual Studio的時候檢視到其標準庫的實現,都是放在類內部的。

可能是我習慣了C#的風格,我比較喜歡把它們都寫在類內部,也因為在開發過程中,所使用的編輯器都有一個強大的功能:程式碼摺疊。

當然還有其他原因就是寫在類外部,對於每一個函式成員的實現都需要把模板型別作為限定符寫一遍,把類名限定符也要寫一遍。

相關文章