C++ 基礎系列——多文件編程


一、多文件編程是什么

為了方便后期的維護,分散代碼應遵循一個基本原則:實現相同功能的代碼應存儲在一個文件中。

C++ 代碼文件根據后綴名的不同,大致可以分為如下幾類:

.h:頭文件
.hpp:頭文件,header plus plus 的縮寫,混雜着 .h 的聲明 .cpp 的定義,OpenCV 采用
.cpp:源文件,windows
.cc:源文件,Unix/Linux

對於一些系統提供的庫,出於版權和保密考慮,大多是已經編譯好的二進制文件,可能僅包含 .h 文件。

// student.h
class Student{
    // ...
};
// student.cc
#include "sudent.h"
// Student 定義
// main.cc
#include "student.h"

int main(){
    // ...
}

二、如何防治頭文件被重復引入

1. 使用宏定義避免

#ifndef _NAME_H
#define _NAME_H

//頭文件內容

#endif

_NAME_H 是宏的名稱。需要注意的是,這里設置的宏名必須是獨一無二的,不要和項目中其他宏的名稱相同。

// student.h
#ifndef _STUDENT_H
#define _STUDENT_H
class Student{
    // ...
};
#endif

2. 使用 #pragma once 避免

使用 #pragma once 指令,將其附加到指定文件的最開頭位置,則該文件就只會被 #include 一次。

#ifndef 是通過定義獨一無二的宏來避免重復引入的,這意味着每次引入頭文件都要進行識別,所以 效率不高。但考慮到 C 和 C++ 都支持宏定義,所以項目中使用 #ifndef 規避可能出現的“頭文件重復引入”問題,不會影響項目的可移植性。

#pragma once 不涉及宏定義,當編譯器遇到它時會立刻知道當前文件只引入一次,所以效率很高。但值得一提的是,並不是每個版本的編譯器都能識別 #pragma once 指令,一些較老版本的編譯器就不支持該指令(執行時會發出警告,但編譯會繼續進行),即 #pragma once 指令的兼容性不是很好

#pragma once 只能作用於某個具體的文件,而無法向 #ifndef 那樣僅作用於指定的一段代碼。

#pragma once
class Student{
    // ...
};

3. 使用 _Pragma 操作符

_Pragma 操作符可以看做是 #pragma 的增強版,不僅可以實現 #pragma 所有的功能,還能和宏搭配使用。

這里僅介紹用 _Pragma 操作符避免頭文件重復引入。

當處理頭文件重復引入問題時,可以將如下語句添加到相應文件的開頭:

_Pragma("once")

_Pragma("once");
class Student{
    // ...
};

在某些場景中,考慮到編譯效率和可移植性,#pragma once 和 #ifndef 經常被結合使用來避免頭文件被 重復引入。比如說:

#pragma once
#ifndef _STUDENT_H
#define _STUDENT_H
class Student{
    // ...
};
#endif

當編譯器可以識別 #pragma once 時,則整個文件僅被編譯一次;反之,即便編譯器不識別 #pragma once 指令,此時仍有 #ifndef 在發揮作用。

三、命名空間如何應用在多文件編程中

當進行多文件編程時,命名空間常位於 .h 頭文件中。

// student_li.h
#ifndef _STUDENT_LI_H
#define _STUDENT_LI_H
namespace Li{
    class Student{
        // ...
    };
}
#endif
// student_li.cc
#include "student_li.h"
#include <iostream>
void Li::Student::display(){

}
// student_han.h
#ifndef _STUDENT_HAN_H
#define _STUDENT_HAN_H
namespace Han{
    class Student{
        // ...
    };
}
#endif
// student_han.cpp
#include "student_han.h"
#include <iostream>
void Han::Student::display(){}

注意,當類的聲明位於指定的命名空間中時,如果要在類的外部實現其成員方法,需同時注明所在命名空間名 和類名(例如本項目中的 Li::Student::display() )。

四、const常量如何在多文件編程中使用

用 const 修飾的變量必須在定義的同時進行初始化操作(除非用 extern 修飾)

C++ 中 const 關鍵字的功能有 2 個,除了表明其修飾的變量為常量外,還將所修飾變量的可見范圍 限制為當前文件。這意味着,除非 const 常量的定義和 main 主函數位於同一個 .cpp 文件,否則該 const 常量只能在其所在的 .cpp 文件中使用。

那么,如何定義 const 常量,才能在其他文件中使用呢?

1. 將 const 常量定義在 .h 頭文件中

// demo.h
#ifndef _DEMO_H
#define _DEMO_H
const int num = 10;
#endif
// main.cc
#include <iostream>
#include "demo.h"
int main(){
    std::cout << num << std::endl;
    return 0;
}

2. 借助 extern 先聲明再定義 const 常量

借助 extern 關鍵字,const 常量的定義也可以遵循“聲明在 .h 文件,定義在 .cpp 文件”。

// demo.h
#ifndef _DEMO_H
#define _DEMO_H
extern const int num;   // 聲明 const 常量
#endif
// demo.cc
#include "demo.h"
const int num = 10;
// main.cpp
#include <iostream>
#include "demo.h"
int main(){
    std::cout << num << std::endl;
    return 0;
}

3. 借助 extern 直接定義 const 常量

C++ 編譯器在運行項目時,會在預處理階段直接將 #include 引入的頭文件替換成該頭文件中的內容(復制粘貼),因此可以對上節代碼做修改:

// demo.cpp
extern const int num = 10;
// main.cpp
#include <iostream>
extern const int num;
int main(){
    std::cout << num << std::endl;
    return 0;
}

五、多文件項目如何用 g++ 命令執行

在 Linux 平台上,雖然也有很多可用的 C++ IDE,但執行 C++ 程序更常采用的方式是使用 g++ 命令。

除此之外,Linux 平台還經常編寫 makefile 來運行規模較大的 C++ 項目。

C++ 程序的執行過程分為 4 步,依次是預處理、編譯、 匯編和鏈接。在執行 C++ 項目時,頭文件是不需要經歷以上這 4 個階段的,只有項目中的所有源文件才必須經歷這 4 個階段。

假設有這個一個 C++ 項目

// studetn.h
class Student{
    // ...
};
// student.cc
#include <iostream>
#include "student.h"
void Student::say(){
    std::cout << name << "的年齡是" << age << ",成績是" << score << std::endl;
}
// main.cc
#include "student.h"
int main(){
    Student *pStu = new Student;
    // ...
    delete pStu;
    return 0;
}

預處理階段,執行如下命令:

[root@bogon ~]# g++ -E main.cc -o main.i
[root@bogon ~]# g++ -E student.cc -o student.i

  • -E 選項用於限定 g++ 編譯器只進行預處理而不進行后續的 3 個階段;
  • -o 選項用於指定生成文件的名稱。

編譯階段,進一步生成響應的匯編代碼文件:

[root@bogon ~]# g++ -S main.i -o main.s
[root@bogon ~]# g++ -S student.i -o student.s

  • -S 選項用於限定 g++ 編譯器對指定文件進行編譯,得到的匯編代碼文件通常以“.s”作為后綴名。

將匯編文件轉換成可執行的機器命令:

[root@bogon ~]# g++ -c main.s -o main.o
[root@bogon ~]# g++ -c student.s -o student.o

  • 如果不用 -o 指定可執行文件的名稱,默認情況下會生成 a.out 可執行文件。Linux 系統並不以文件的擴 展名開分區文件類型,所以 a.out 和 student.exe 都是可執行文件,只是文件名稱有區別罷了。

最終執行:

[root@bogon ~]# ./student.exe

從頭開始直接生成可執行文件:

[root@bogon ~]# g++ main.cpp student.cpp -o student.exe

六、多文件編程的底層原理

實際上,在編譯階段,編輯器會對源文件生成一個符號表,源文件中看不到的定義的符號就會存在這個表中。在鏈接階段,編譯器會在別的目標文件中尋找這個符號的定義,如何沒有找到,會出現鏈接錯誤。

定義,指的是就是將某個符號完整的描述清楚,它是變量還是函數,變量類型以及變量值是多少,函數的參數有哪些以及返回值是什么等等;而“聲明”的作用僅是告訴編譯器該符號的存在,至於該符號的具體的含義,只有等鏈接的時候才能知道。

C++ 中一個符號允許被聲明多次,但只能被定義一次。

基於聲明和定義的不同,才有了多文件編程的出現。

所謂的頭文件,其實它的內容跟 .cpp 文件中的內容是一樣的,都是 C++ 的源代碼,唯一的區別在於頭文件不 用被編譯。我們把所有的函數聲明全部放進一個頭文件中,當某一個 .cpp 源文件需要時,可以通過 #include 宏命令直接將頭文件中的所有內容引入到 .cpp 文件中。這樣,當 .cpp 文件被編譯之前(也就是預處理階段),使用 #include 引入的 .h 文件就會替換成該文件中的所有聲明。

通常一個頭文件的內容會被引入到多個不同的源文件中,並且都會被編譯,所以頭文件中一般只存放變量或者函數的聲明,不要放定義。但存在3種情況屬於定義范疇,但應該放在 .h 文件種:

  1. 頭文件中定義 const 對象、static 對象
  2. 頭文件中定義內聯函數,編譯器必須在編譯時就找到內聯函數的完成定義
  3. 頭文件中可以定義類。類的內部通常包含成員變量和成員函數,成員變量是要等到具體的對象被創建時才會被定義(分配空間),但成員函數卻是需要在一開始就被定義的,這也就是類的實現。通常的做法是將類的定義放在頭文件中,而把成員函數的實現代碼放在一個 .cpp 文件中。也可以直接實現在類內,作為內聯函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM