為什麼不推薦在頭文件中直接定義函數?

来源:https://www.cnblogs.com/sarexpine/archive/2023/07/16/17558440.html
-Advertisement-
Play Games

這篇技術博客探討了為什麼不推薦在頭文件中直接定義函數。它解釋了在將函數定義放在頭文件中的潛在問題,並提供了更好的替代方案。通過避免在頭文件中定義函數,讀者可以更好地管理代碼的複雜性,並提高代碼的可讀性和可維護性 ...


為什麼不推薦在頭文件中直接定義函數?

1. 函數的分文件編寫

在C++中,函數的分文件編寫是一種讓代碼結構更加清晰的方法,通常可以分為以下幾個步驟:

  • 創建尾碼名為 .h 的頭文件,在頭文件中寫函數的聲明,以及可能用到的其他頭文件或命名空間
  • 創建尾碼名為 .cpp 的源文件,在源文件中寫函數的定義,同時引入自定義頭文件,將頭文件與源文件綁定
  • 在需要使用函數的地方,引入自定義頭文件,然後直接調用函數,無需再寫函數的實現

例如,如果要編寫一個求兩個數最大值的函數,可以這樣做:

  • 創建一個 max.h 頭文件,在其中寫入以下內容:
#pragma once // 防止頭文件重覆包含
#include <iostream> // 引入輸入輸出流頭文件
using namespace std; // 使用標準命名空間
// 函數聲明
int max(int a, int b);
  • 創建一個 max.cpp 源文件,在其中寫入以下內容:
#include "max.h" // 引入自定義頭文件
// 函數定義
int max(int a, int b) {
    return a > b ? a : b; // 三目運算符,返回最大值
}
  • 在需要使用函數的地方,例如 main.cpp 文件中,引入自定義頭文件,並調用函數:
#include "max.h" // 引入自定義頭文件
int main() {
    int a = 10;
    int b = 20;
    cout << "The max of " << a << " and " << b << " is " << max(a, b) << endl; // 調用函數並輸出結果
    system("pause"); // 暫停程式
    return 0;
}

文件結構如圖所示:

img

2. 頭文件中不推薦直接定義函數

在頭文件中直接寫函數的定義是不推薦的,有以下幾個原因:

  • 在頭文件中寫函數的定義會導致重覆定義的錯誤,如果這個頭文件被多個源文件包含。因為每個源文件都會把頭文件的內容複製過來,相當於在多個地方定義了同一個函數,這違反了單定義原則
  • 在頭文件中寫函數的定義會增加編譯的時間,如果這個頭文件被頻繁修改。因為每次修改頭文件後,所有包含這個頭文件的源文件都需要重新編譯,這對於大型項目來說非常耗時
  • 在頭文件中寫函數的定義會降低代碼的可讀性和可維護性,如果這個頭文件包含了很多函數的定義。因為頭文件的主要作用是提供函數的聲明和介面,而不是實現細節。把函數的定義放在源文件中,可以讓代碼結構更清晰,也便於隱藏實現細節和保護數據

2.1 單定義原則

在頭文件中寫函數的定義會導致重覆定義的錯誤,如果這個頭文件被多個源文件包含。比如,假設有一個頭文件 max.h,其中定義了一個求兩個數最大值的函數:

// max.h
#pragma once
#include <iostream>
using namespace std;

int max(int a, int b) {
    return a > b ? a : b;
}

然後,有兩個源文件 main1.cppmain2.cpp,都包含了這個頭文件,並且都調用了這個函數:

// main1.cpp
#include "max.h"

int foo() {
    cout << "The max of 10 and 20 is " << max(10, 20) << endl;
    return 0;
}
// main2.cpp
#include "max.h"

int main() {
    cout << "The max of 30 and 40 is " << max(30, 40) << endl;
    return 0;
}

img

看到這裡可能會有個疑問,編譯的時候 main1.cpp 調用 max.h 中的函數,但是 main2.cpp 中的主函數中沒有調用 main1.cpp 中的函數,為什麼還是會編譯不通過呢?兩個不同的文件定義同一個函數也會衝突嗎?即使其中一個文件和另一個文件沒有任何關係?

編譯時,每個源文件會生成一個目標文件,然後鏈接生成可執行文件。即使 main2.cpp 沒有調用 main1.cpp 的函數,但 main1.cpp 中包含了 max.h,相當於在 main1.cpp 中定義了max函數,與 main2.cpp 中的max函數衝突。當鏈接時,如果出現同名的函數,就會出現重覆定義的錯誤。因此,每個函數應該只在一個源文件中定義,或者使用命名空間或靜態修飾符來避免衝突


為瞭解決這個問題,我們應該把函數的定義放在另一個源文件 max.cpp 中,然後在頭文件中只聲明函數:

// max.h
#pragma once
#include <iostream>
using namespace std;

int max(int a, int b); // 函數聲明
// max.cpp
#include "max.h"
int max(int a, int b) { // 函數定義
    return a > b ? a : b;
}

img

這樣就可以避免重覆定義的錯誤了


2.2 減少編譯時間

在頭文件中寫函數的定義會增加編譯的時間,如果這個頭文件被頻繁修改。比如,假設有一個頭文件 math.h,其中定義了一些數學相關的函數:

// math.h
double sin(double x) {
    // some code to calculate sin(x)
}

double cos(double x) {
    // some code to calculate cos(x)
}

double tan(double x) {
    // some code to calculate tan(x)
}

然後,有很多源文件都包含了這個頭文件,並且都調用了這些函數。如果我們想要修改或添加某個函數的實現細節,比如改進 sin 函數的演算法,那麼我們就需要修改頭文件 math.h。但是這樣一來,所有包含了這個頭文件的源文件都需要重新編譯,因為它們都依賴於頭文件的內容。這對於大型項目來說非常耗時。為瞭解決這個問題,我們應該把函數的定義放在另一個源文件 math.cpp 中,然後在頭文件中只聲明函數:

// math.h
double sin(double x); // 函數聲明
double cos(double x); // 函數聲明
double tan(double x); // 函數聲明
// math.cpp
#include "math.h"
double sin(double x) { // 函數定義
    // some code to calculate sin(x)
}

double cos(double x) { // 函數定義
    // some code to calculate cos(x)
}

double tan(double x) { // 函數定義
    // some code to calculate tan(x)
}

這樣就可以減少編譯的時間了,因為只有修改或添加了函數的源文件才需要重新編譯

簡單來說,分為兩種情況

  • 第一種:在頭文件中定義函數。如果有很多源文件都引用了這個頭文件,那麼當頭文件修改後,所有引用頭文件的源文件都要重新編譯,對於大型項目非常耗時

  • 第二種:把函數的定義和聲明放在不同的文件中。這樣做可以使得當源文件中定義的函數發生修改時,只需要重新編譯被修改的源文件就可以了,不需要所有引用這個頭文件的源文件重新編譯,節省了非常多的時間


為什麼在頭文件中定義的函數發生改變時,所有包含該頭文件的源文件需要重新編譯?

還是借用以上的例子,我的猜想是這樣的

假如在 main.cpp 源文件中引用 math.h 頭文件,相當於把頭文件中的內容複製到了源文件里

那麼如果 math.h 頭文件中定義函數,並且 main.cpp 源文件中引用了 math.h 頭文件,則相當於把 math.h 中的定義的函數複製到 main.cpp 源文件里,一旦頭文件中的函數發生改變,那麼就相當於源文件發生了改變

因此所有包含 math.h 頭文件的源文件都需要重新編譯

此外,多個源文件包含同一個定義函數的頭文件,會導致重定義的錯誤。這裡只是舉個例子假設編譯器允許這樣的操作,實際上編譯不會通過

img

調用函數時的索引順序:

在源文件中調用函數的時候,是先到頭文件里找聲明的函數,然後再通過鏈接的過程找到對應的源文件里的函數

如下圖所示,main.cpp 調用函數時,先到 math.h 中找到聲明的函數,然後再通過鏈接的過程找到對應的源文件 math.cpp 里的函數

img

這個過程可以看作是查字典,頭文件相當於目錄,對應著每個函數所在的位置


2.3 可讀性與安全性

在頭文件中寫函數的定義會降低代碼的可讀性和可維護性,如果這個頭文件包含了很多函數的定義。比如,假設有一個頭文件 utils.h,其中定義了一些工具類的函數:

// utils.h
#include <string>
#include <vector>
using namespace std;

string trim(string s) {
    // some code to trim the whitespace of s
}

vector<string> split(string s, char delim) {
    // some code to split s by delim
}

string join(vector<string> v, char delim) {
    // some code to join v by delim
}

bool is_number(string s) {
    // some code to check if s is a number
}

int to_int(string s) {
    // some code to convert s to int
}

string to_string(int x) {
    // some code to convert x to string
}

這個頭文件包含了很多函數的定義,這會讓代碼看起來很冗長,也不容易找到想要的函數。而且,如果我們想要修改或添加某個函數的實現細節,比如改進 trim 函數的效率,那麼我們就需要修改頭文件 utils.h。但是這樣會影響到所有包含了這個頭文件的源文件,也會增加代碼的複雜度和出錯的風險。為瞭解決這個問題,我們應該把函數的定義放在另一個源文件 utils.cpp 中,然後在頭文件中只聲明函數:

// utils.h
#include <string>
#include <vector>
using namespace std;

string trim(string s); // 函數聲明
vector<string> split(string s, char delim); // 函數聲明
string join(vector<string> v, char delim); // 函數聲明
bool is_number(string s); // 函數聲明
int to_int(string s); // 函數聲明
string to_string(int x); // 函數聲明
// utils.cpp
#include "utils.h"

string trim(string s) {
    // some code to trim the whitespace of s
}

vector<string> split(string s, char delim) {
    // some code to split s by delim
}

string join(vector<string> v, char delim) {
    // some code to join v by delim
}

bool is_number(string s) {
    // some code to check if s is a number
}

int to_int(string s) {
    // some code to convert s to int
}

string to_string(int x) {
    // some code to convert x to string
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • 今天接到粉絲私信,詢問是否可以通過Canvas繪製一些圖形,然後根據粉絲提供的模板圖,通過Canvas進行模擬繪製,通過分析發現,圖形雖然相對簡單,但是如果不藉助相應的軟體,純代碼繪製還是稍微費些時間。今天將繪製圖形源碼分享出來,僅供學習分享之用,如有不足之處,還請指正。 ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202307/3076680-20230713141300146-1450511408.png) # 1. 水平擴展 ## 1.1. 有助於提高系統的整體容量和韌性 ## 1.2. 現階段構建的幾乎所有系統,都 ...
  • 電腦編程發展至今,一共只有三個編程範式: - 結構化編程 - 面向對象編程 - 函數式編程 ### 編程範式和軟體架構的關係 - 結構化編程是各個模塊的演算法實現基礎 - 多態(面向對象編程)是跨越架構邊界的手段 - 函數式編程是規範和限制數據存放位置與訪問許可權的手段 **軟體架構的三大關註重點** ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 作為《Java擴展Nginx》系列的第七 ...
  • ## 介紹 在數據科學和分析的領域,數據能力的釋放不僅是通過提取見解的方式, 同時也要能通過有效的方式來傳達見解.這就是數據可視化發揮見解的地方. ![image](https://img2023.cnblogs.com/blog/682547/202307/682547-2023070809272 ...
  • 本文主要介紹如何通過 dockerfile-maven-plugin 插件把 Java 服務構建成 docker 鏡像;文中所使用到的軟體版本:Docker 20.10.17、Java 1.8.0_341、SpringBoot 2.7.12、Maven 3.8.4、dockerfile-maven- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...