C/C++ 的編譯和鏈接

来源:https://www.cnblogs.com/macomfan/archive/2019/11/11/11833401.html
-Advertisement-
Play Games

C/C++文件 C/C++程式文件包括 .h .c .hpp .cpp,其中源文件(.c .cpp)是基本的編譯單元,頭文件(.h .hpp)不會被編譯器編譯。 C/C++項目構建(build)過程,分為以下幾個步驟 預處理 → 編譯 → 鏈接。 預編譯 預編譯的過程可以理解為編譯器(實際上是預處理 ...


C/C++文件

C/C++程式文件包括 .h .c .hpp .cpp,其中源文件(.c .cpp)是基本的編譯單元,頭文件(.h .hpp)不會被編譯器編譯。

C/C++項目構建(build)過程,分為以下幾個步驟 預處理 → 編譯 → 鏈接。

預編譯

預編譯的過程可以理解為編譯器(實際上是預處理器,這裡統稱為編譯器就可以了)在正式編譯之前處理C/C++文件中的預處理命令,即#開頭的代碼。

常用的幾個預處理命令如下:

#include ......

#ifdef ...... #else......#endif

#define ......

#pragma ......

舉個例子,下麵是個很簡單的類定義:

MyClass.h

#define DEFAULT_VALUE 0

class MyClass {
public:
    void Fun();
public:
    int value = DEFAULT_VALUE;
};

MyClass.cpp

#include "MyClass.h"

void MyClass::Fun() {
    // Do someting
    return;
}

預編譯完成後的樣子:

class MyClass {
public:
    void Fun();
public:
    int value = 0;
};

void MyClass::Fun() {
    // Do someting
    return;
}

可以看到編譯器把.h文件替換到了.cpp文件中的#include 位置上,把DEFAULT_VALUE定義的值也替換到了相應的位置。

 

編譯

預編譯之後,編譯器會編譯每個源文件(.c .cpp),如果編譯成功,會生成對應的目標文件,Linux為.o文件,Windows平臺下為.obj文件。

以Linux平臺為例,上面的MyClass.cpp編譯完成後會生成MyClass.o文件

使用objdump可以看到目標文件MyClass.o的內容

$$ objdump -x MyClass.o

......
0000000000000000 g     F .text  0000000000000015 _ZN7MyClass3FunEv
......

編譯器會把MyClass::Fun()的名字改成_ZN7MyClass3FunEv,這個過程叫Mangle,由於C++支持重載,覆蓋等特性,所以編譯器必須把函數用一個唯一的標識表示。這個字元串就是編譯器生成的唯一標識。

這裡還要單獨說一下頭文件,頭文件的既然不是編譯單元,那麼它的作用是什麼?

頭文件就是負責”聲明“,編譯器在編譯MyClass.cpp的時候,對於MyClass這個類以及Fun()這個成員函數,編譯器必須找到它的聲明,這個函數才能被正確編譯。

如果有其他cpp需要使用MyClass這個類的時候,也需要它的的聲明。例如

main.cpp

#include "MyClass.h"

int main(int argc, char** argv) {
    MyClass tmp;
    tmp.Fun();
    return 0;
}

加上#include "MyClass.h" 編譯器在編譯main.cpp的時候才知道怎麼編譯MyClass這個類。MyClass.h里聲明是不會真正被編譯到main.o中,.h文件中的內容在目標文件中只是以列表的形式存在,這個表在後面鏈接時會用到。

當然,頭文件不僅可以用來聲明,還可以定義(定義全局變數,全局函數等),在頭文件中的定義要小心,可能會引起鏈接錯誤。

 

鏈接

鏈接就是將一堆目標文件加靜態庫文件裝配成可執行文件的過程。(或者是裝配成靜態/動態庫的過程)

上面兩個cpp分別被編譯成了MyClass.o, 和main.o,我們要生成可執行程式的話,就必須經過鏈接的過程,把兩個目標文件合成一個可執行文件。

main.o中,main函數會構造MyClass, 並且調用Fun()函數,那麼main就根據MyClass.h生成的表,找到MyClass.o中的函數,這個就是鏈接器要做的工作。

 

常見錯誤

構建c/c++工程的時候,最常見的就是兩種錯誤:

-- 編譯錯誤,在編譯過程中產生的錯誤,通常是語法錯誤,沒有聲明,重覆聲明導致編譯目標文件錯誤

其中沒有聲明通常是由於沒有#include相應的頭文件,或者頭文件缺少相應的聲明。

而重覆聲明通常是#include了相同的頭文件,比如 B.h 和 C.h 都包含 A.h,然後 main.h 包含了 B.h 和 C.h,這就導致A.h 在main中被包含了兩次。

解決這個問題的方法是可以在所有.h文件的第一行加上

#pragma once

或者,使用#ifndef ... #define ... #endif 語句塊

#ifndef NEWCLASS_H
#define NEWCLASS_H

......

#endif /* NEWCLASS_H */

 

-- 鏈接錯誤,常見的錯誤也是兩種,沒有定義和重覆定義,和上面的沒有聲明,重覆聲明類似。(這裡定義指的就是函數實現)

  • 先討論沒有定義(undefined reference to xxx)

通常是因為函數有聲明,而且被使用了,但是沒有被定義。比如上面MyClass.cpp中,如果Fun()沒有被實現的話,MyClass.cpp和main.cpp編譯時都不會報錯,但是鏈接時會報告找不到Fun()。

當然,如果Fun()沒被main.cpp調用的話,即使不實現它,整個構建過程也不會出錯,因為鏈接器根本不會去找這個函數的定義。

  • 然後是重覆定義(multiple definition)

指的一份相同的定義在兩個目標文件中都存在,鏈接的時候鏈接器不知道時用哪個了。這種問題通常由於全局函數,和全局變數定義在了頭文件中。導致多個目標文件包含相同的全局函數和全局變數的定義。

解決方法就是在頭文件中聲明,定義放到cpp文件中,或者為定義加上const 或 static這樣的修飾符,鏈接時會對這些帶有const和修飾符的變數特殊處理的。

const只適用於定義常量變數,static定義的是靜態全局變數,只在當前cpp有效,所以鏈接它也不會被別的目標文件鏈接,就不會有重覆定義的問題了。

總之在頭文件中定義變數和函數要特別主意,可能會導致鏈接錯誤。

當然也不是所有定義都不能放到頭文件中,比如剛纔說的const常量,static全局變數就是例外,還有內聯函數,可以定義在.h文件中,因為內聯函數會被拷貝到每個目標文件中,也不會參與鏈接的過程。

還有模板類必須放在頭文件中定義,這個下麵會討論這個。

 

關於模板,靜態成員變數

模板類模板函數必須聲明和定義在頭文件中,原因是什麼,舉個例子,假設MyClass是模板類

MyClass.h

template <typename T>
class MyClass {
public:
    void Fun();
public:
    T value;
};

MyClass.cpp

#include "MyClass.h"

template <typename T>
void MyClass<T>::Fun() {
    // Do someting
    return;
}

main.cpp

int main(int argc, char** argv) {
    MyClass<int> tmp;
    tmp.Fun();
    return 0;
}

編譯的時候沒有問題,但是鏈接時會報錯,main.cpp找不到MyClass<int>::Fun(),如下圖

MyClass雖然定義了Fun函數,但是MyClass.o中存在MyClass<T>::Fun(),而根據MyClass.h文件,main.o中需要找到MyClass<int>::Fun()的定義

結果鏈接器哪都找不到,只好報錯了。(實際上通過objdump查看MyClass.o,編譯器都沒有生成MyClass<T>::Fun(),因為編譯器認為這個函數沒人使用,就直接優化掉了)

如果非得在cpp中定義模板類的成員函數呢,有一種方法就是要顯示的在cpp文件中聲明,比如

MyClass.cpp

#include "MyClass.h"

template <typename T>
void MyClass<T>::Fun() {
    // Do someting
    return;
}

template void MyClass<int>::Fun();

加上下麵這行就不會有問題了,但是缺點就是開發MyClass的程式員無從知道其他類是怎麼使用這個模板的,不可能把所有可能的模板參數全都一一的列在這裡。

所以模板類的定義還是要寫在.h文件中,

那麼如果main.cpp使用到了MyClass<int>, 另外一個cpp也使用到了MyClass<int>,會不會產生重覆代碼導致重覆定義呢,不會,編譯器會處理好模板類的。

 

下麵是靜態成員變數,為什麼靜態成員變數的定義要放在cpp里,(模板類的靜態成員變數除外)

靜態成員變數和靜態全局成員變數不同。

靜態成員變數的作用域可以是整個工程,而靜態全局變數的作用域只是當前的cpp。所以靜態成員變數定義在.h中就會發生重定義錯誤。

 


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

-Advertisement-
Play Games
更多相關文章
  • [TOC] 題目 "Largest Rectangle in a Histogram" 思路 單調棧。 不知道怎麼描述所以用樣例講一下。 我們可以用單調棧去維護每一個高度左右第一個比他矮的位置即可 $Code$ ...
  • 入門python一切都感覺到那麼簡單,從來沒有想過人生還可以有這麼美好的待遇,這一切都是因為接觸了python才讓我感到生活原來一切又充滿了希望 ...
  • jdk: 解壓: tar zxvf jdk-8u144-linux-x64.tar.gz 執行:vi /etc/profile export JAVA_HOME=/usr/local/jdk1.8.0_201 export CLASSPATH=$JAVA_HOME/lib export PATH=$ ...
  • Day1 考的不是很好,T1T2沒區分度,T3想的太少,考試後期幾乎都是在摸魚,bitset亂搞也不敢打,只拿到了35分,跟前面的差距很大 A. 最大或 標簽: 二進位+貪心 題解: 首先x,y中一定有一個是R,考慮L的取值:對於每一位分為x中有沒有討論: 1>有 如果這一位不加以後全加可以>=L則 ...
  • Scrapy.http.Request 自動去重,根據url的哈希值,進行去重 屬性 meta(dict) 在不同的請求之間傳遞數據,dict priority(int) 此請求的優先順序(預設為0) dont_filter(boolean) 關閉自動去重 errback(callable) 在處理請 ...
  • 一、環境準備 1. jdk1.8.1 做java開發的這個應該能自己找到 2.gradle-4.9 https://services.gradle.org/distributions/ 沒用過gradle的同學可以將其理解為類似於maven的包管理工具,這裡下載gradle-4.9-bin.zip, ...
  • 本文源碼: "GitHub·點這裡" || "GitEE·點這裡" 一、生活場景 1、場景描述 在公司的日常安排中,通常劃分多個部門,每個部門又會分為不同的小組,部門經理的一項核心工作就是協調部門小組之間的工作,例如開發小組,產品小組,小組的需求統一彙總到經理,經理統一安排和協調。 2、場景圖解 3 ...
  • 什麼是ThreadLocal ThreadLocal有點類似於Map類型的數據變數。ThreadLocal類型的變數每個線程都有自己的一個副本,某個線程對這個變數的修改不會影響其他線程副本的值。需要註意的是一個ThreadLocal變數,其中只能set一個值。 線上程1中初始化了一個ThreadLo ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...