【嵌入式】C語言多文件編程與內聯函數

文章目錄

    • 0 前言
    • 1 從C語言編譯說起
    • 2 重復定義錯誤(ODR violation)和條件編譯
    • 3 內聯函數inline和static inline
    • 4 總結

0 前言

??最近在研究ARM內核代碼時,看到core_cm3.h中有大量的內聯函數,為此查閱了很多資料,也和朋友討論了很久,最后對C語言多文件編程有了一點不一樣的體會,對此前很多習以為常的東西也知道了這么做的原因。特寫此文以作總結。

1 從C語言編譯說起

??在使用gcc或者g++編譯時,直接傳入c文件即可得到執行程序,看似非常簡單,但實際有很多步驟,包括:預處理(Preprocess),編譯(Compile),匯編(Assemble),鏈接(Link)四個步驟。其中,所謂預處理,即對帶有#的語句進行處理,如#include, #define以及條件編譯語句#if, #ifdef, #ifndef等(當然,這一步也會進行刪除注釋等操作);而編譯,即是將c語言編譯成匯編語言;匯編,是基于匯編語言生成機器碼;鏈接,則是鏈接具有函數引用關系的不同c文件。

參考鏈接

??以上這些過程中有一些注意點:

  • #include實際上就是將這個文件的內容復制過來,所以預處理之后,得到的仍然是c格式的文本文件,但體積會比原來的文件大很多。
  • 既然如此,豈不是理論上可以包含任何文件?是的,include某個c文件其實也是允許的,只要復制過來不會報錯就行。
  • 那為什么還要建立同名的c和h文件呢?直接一個c文件不行嗎?這其實是考慮到C語言不能重復定義函數和變量的特性,以及庫文件加調用接口的這種應用場景。一般都是h文件放聲明,c文件放定義。至于重復定義錯誤的相關介紹,參考后面章節。
  • 為什么需要鏈接?簡單來說,只要寫的代碼中引用了其他文件中定義的函數,就需要鏈接。這里需要理解一個專業名詞:編譯實體,即每一個c文件都是一個編譯實體,是最小的編譯單位。各個編譯實體在上述前三個階段都是獨立的,互不影響,而最后的鏈接階段就是將不同編譯實體給“拼接”到一塊,組成一個完整的執行程序。

2 重復定義錯誤(ODR violation)和條件編譯

??相信使用過ADC模塊的都遇到過重復定義的問題,即在h文件定義一個轉換值的變量,int ADC_value = 0; 然后在main.c中包含這個h文件之后直接使用變量ADC_value,這樣就會報重復定義的錯誤。
??所謂條件編譯,即在h文件中用這么一段代碼括起來:

#ifndef __FILE_H_
#define __FILE_H_// 中間是頭文件的內容#endif

對于這個東西的作用,網上絕大多數的描述都是防止重復包含。確實,從條件編譯的邏輯來看可以實現這個功能,但很多人可能會將這個當作上述描述的重復定義錯誤的解決辦法,這顯然是不對的。

??首先需要明確,函數或變量的聲明,是可以重復include的,如果h文件中只有聲明,那完全可以多次include的,那為什么現在的庫文件中h文件中都會有上述的條件編譯代碼呢?確實是防止重復包含,但最終目的不是避免報錯,而是加快編譯速度

參考鏈接

??舉個例子,有一個庫(func.c, func.h)包含了stdio.h,然后main.c包含了func.h,但是出于編寫習慣,main.c中也會包含stdio.h,也就是說,最后在編譯main.c時就包含了兩次stdio.h文件,如果stdio.h文件中沒有條件編譯,那么它就會被包含兩次,雖然不會報錯,但會影響編譯的速度,而且這種庫數量越多,影響越大。

??那重復定義到底是怎么回事呢?如果在h文件中定義全局變量,那么包含該h文件的c文件也就定義了一個全局變量(因為include是完全復制),編譯器在編譯該c文件時,這個變量就會被存放在全局/靜態區。同理,假如該h文件也被其他c文件包含,那么其他包含該h文件的c文件也會這么干,因為不同c文件在預處理,編譯和匯編這三個階段(生成目標文件階段)是獨立的。到這各個c文件都可以被正常編譯,不會報錯,但是在最后鏈接階段時,編譯器就會發現全局/靜態區存在相同的變量定義,由此報錯。

??總結來說,防止重復包含是在前三個階段,是同一個編譯單元編譯時的考慮;而重復定義,是不同編譯實體之間在第四個階段鏈接過程中的問題。因此,防止重復包含并不能解決重復定義的問題。

??所以,對于全局變量,建議采用的方式就是頭文件中只聲明(extern int a;),定義放在同名的c文件中,這樣即使有不同的編譯實體包含了該頭文件,也只是包含了聲明,沒有變量定義,這樣在鏈接階段就不會出現重復定義的問題。

3 內聯函數inline和static inline

??inline這個關鍵詞比較復雜,它在不同C語言版本,不同編譯器,c和c++中的含義都不盡相同,所以在使用前一定要了解編譯環境。

??所謂內聯函數,指調用時沒有普通函數調用時的堆棧壓入和彈出的步驟,而是將函數展開,直接執行內部的代碼。內聯函數的好處在于減少了函數出入棧的操作,代碼執行效率更高,但同樣也有缺點,那就是每調用一次,都需要復制一遍函數的代碼,空間成本更高,所以內聯函數一般只適用于比較簡短的代碼。

??另外,inline關鍵詞只能建議該函數內聯調用,但最終是否調用仍然取決于編譯器,所以就有可能會內聯失敗。對于這個問題,在c++中,一般編譯器會將該函數自動轉換成普通函數,且只保留一份定義,然后正常調用,從而保證不會出錯。比如,在func.h文件中定義一個內聯函數,但由于函數內容太長或者其他原因,內聯失敗了,那么編譯器可能會自動創建一個func.c文件(原來沒有,不一定是這個名字),然后在這個文件中生成該內聯函數的定義,原func.h文件中的定義就只有聲明的作用,從而轉換成普通函數。

參考鏈接

??但是,以上是c++的處理方式,可以保證內聯的函數有且僅有一份定義,但這并不適用于C語言。先來看一個vscode中的例子:

test.c

#include "stdio.h"inline void func()
{printf("Hello World!\n");
}int main(void)
{func();return 0;
}

點擊編譯運行,發現會報錯:undefined reference to 'func' ... error: ld returned 1 exit status 但是如果將文件改成cpp,同樣的代碼就不會報錯。這看起來好像是C語言編譯器的問題?在這篇博客中,介紹了一種辦法,就是在函數前再加上static關鍵詞進行修飾,這也就是后面要提到的static inline聯合使用的問題,暫且按下不表。

??為解決這個問題,嘗試在編譯時開啟優化,執行gcc -O1 test.c -o run_c; ./run_c,結果發現竟然正常輸出了Hello World!換成O2,O3,也都正常。(默認為O0,不開啟優化)

在這里插入圖片描述

由此可知,如果有inline函數,必須要考慮編譯優化等級的問題。

static inline

??再來看看這個static inline,在介紹之前,首先介紹一下static關鍵詞。

??對于static關鍵詞,在我之前的一篇博客中有詳細介紹。簡單來說,修飾變量時,表示該變量為靜態變量,存放在靜態區,比如函數中如果使用了靜態變量,那么它在內存中的地址就是固定的,全局變量加不加static修飾其實差別不大;修飾函數時,表示該函數只能被當前文件訪問,不能被其他文件訪問,常用于庫的內部函數,不開放對外接口。

??對于修飾函數的情況,static可以起到隔絕作用域的功能。比如,在兩個c文件中定義同名函數,且都用static修飾,如下所示:

在這里插入圖片描述

這樣是可以正常編譯運行的。兩個test函數雖然一樣,但由于被限制在各自的文件中,所以不會造成沖突。

??那為什么inline還要加上static呢?如前所述,inline關鍵詞只能建議編譯器將該函數內聯展開,如果成功,那么即使內聯函數定義所在的文件被多次包含,也可以正常編譯運行,舉個例子:

在這里插入圖片描述

這個例子中,io.h中定義了一個內聯函數,另外有兩個c文件包含了該h文件,且調用了該內聯函數,可以正常編譯運行。

??但考慮到inline可能會失敗,而且C語言的編譯器在這方面又沒有c++那么智能,可以自動實現只保留一份定義,避免ODR Violation。那么就需要加上static,這樣每次調用都相當于是內部函數,只在該編譯實體下可調用,且允許不同編譯實體中存在重復定義,這樣就能正常編譯運行了。總結來說,static起到的是一個安全保障功能。

參考鏈接

4 總結

??本文從研究內聯函數出發,分析了C語言多文件編程的具體流程,并基于次對內聯函數的含義和性質、inline和static兩個關鍵詞及其組合等內容進行了詳細的介紹,對于閱讀ARM內核代碼有一定的幫助。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/894684.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/894684.shtml
英文地址,請注明出處:http://en.pswp.cn/news/894684.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

10分鐘本地部署Deepseek-R1

10分鐘本地部署DeepSeek-R1 什么是DeepSeek-R1快速本地部署DeepSeek-R1Ollama下載Ollama安裝檢查是否安裝成功 安裝DeepSeek-R1模型模型使用測試 什么是DeepSeek-R1 DeepSeek-R1是中國的深度求索(DeepSeek)公司開發的智能助手。其具有極佳的語義理解和生…

Office / WPS 公式、Mathtype 公式輸入花體字、空心字

注:引文主要看注意事項。 1、Office / WPS 公式中字體轉換 花體字 字體選擇 “Eulid Math One” 空心字 字體選擇 “Eulid Math Two” 2、Mathtype 公式輸入花體字、空心字 2.1 直接輸入 花體字 在 mathtype 中直接輸入 \mathcal{L} L \Large \mathcal{L} L…

Python小游戲29乒乓球

import pygame import sys # 初始化pygame pygame.init() # 屏幕大小 screen_width 800 screen_height 600 screen pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("打乒乓球") # 顏色定義 WHITE (255, 255, 255) BLACK (…

【C++】STL——vector底層實現

目錄 💕 1.vector三個核心 💕2.begin函數,end函數的實現(簡單略講) 💕3.size函數,capacity函數的實現 (簡單略講) 💕4.reserve函數實現 (細節…

7、怎么定義一個簡單的自動化測試框架?

定義一個簡單的自動化測試框架可以從需求理解、框架設計、核心模塊實現、測試用例編寫和集成執行等方面入手,以下為你詳細介紹: 1. 明確框架需求和范圍 確定測試類型:明確框架要支持的測試類型,如單元測試、接口測試、UI 測試等…

安卓(android)讀取手機通訊錄【Android移動開發基礎案例教程(第2版)黑馬程序員】

一、實驗目的(如果代碼有錯漏,可在代碼地址查看) 1.熟悉內容提供者(Content Provider)的概念和作用。 2.掌握內容提供者的創建和使用方法。 4.掌握內容URI的結構和用途。 二、實驗條件 1.熟悉內容提供者的工作原理。 2.掌握內容提供者訪問其…

AI取代人類?

每周跟蹤AI熱點新聞動向和震撼發展 想要探索生成式人工智能的前沿進展嗎?訂閱我們的簡報,深入解析最新的技術突破、實際應用案例和未來的趨勢。與全球數同行一同,從行業內部的深度分析和實用指南中受益。不要錯過這個機會,成為AI領…

C語言-----數據結構從門到精通

1.數據結構基本概念 數據結構是計算機中存儲、組織數據的方式,旨在提高數據的訪問和操作效率。它是實現高效算法和程序設計的基石。 目標:通過思維導圖了解數據結構的知識點,并掌握。 1.1邏輯結構 邏輯結構主要四種類型: 集合:結構中的數據元素之…

華為小米vivo向上,蘋果榮耀OPPO向下

日前,Counterpoint發布的手機銷量月度報告顯示,中國智能手機銷量在2024年第四季度同比下降3.2%,成為2024年唯一出現同比下滑的季度。而對于各大智能手機品牌來說,他們的市場份額和格局也在悄然發生變化。 華為逆勢向上 在2024年第…

每日一博 - 三高系統架構設計:高性能、高并發、高可用性解析

文章目錄 引言一、高性能篇1.1 高性能的核心意義1.2 影響系統性能的因素1.3 高性能優化方法論1.3.1 讀優化:緩存與數據庫的結合1.3.2 寫優化:異步化處理 1.4 高性能優化實踐1.4.1 本地緩存 vs 分布式緩存1.4.2 數據庫優化 二、高并發篇2.1 高并發的核心意…

吳恩達深度學習——有效運作神經網絡

內容來自https://www.bilibili.com/video/BV1FT4y1E74V,僅為本人學習所用。 文章目錄 訓練集、驗證集、測試集偏差、方差正則化正則化參數為什么正則化可以減少過擬合Dropout正則化Inverted Dropout其他的正則化方法數據增廣Early stopping 歸一化梯度消失與梯度爆…

20【變量的深度理解】

一說起變量,懂點編程的都知道,但是在理解上可能還不夠深 變量就是存儲空間,電腦上的存儲空間有永久(硬盤)和臨時(內存條)兩種,永久數據重啟電腦后依舊存在,臨時數據只…

RESTful API的設計原則與這些原則在Java中的應用

RESTful API 是基于 REST(Representational State Transfer) 架構風格設計的 API,其核心目標是提高系統的可伸縮性、簡潔性和可維護性。以下是 RESTful API 的設計原則及在 Java 中的實現方法: 一、RESTful API 的核心設計原則 客…

【apt源】RK3588 平臺ubuntu20.04更換apt源

RK3588芯片使用的是aarch64架構,因此在Ubuntu 20.04上更換apt源時需要使用針對aarch64架構的源地址。以下是針對RK3588芯片在Ubuntu 20.04上更換apt源到清華源的正確步驟: 步驟一:打開終端 在Ubuntu 20.04中,按下Ctrl Alt T打…

k8s二進制集群之Kube ApiServer部署

創建kube工作目錄(僅在主節點上創建即可)同樣在我們的部署主機上創建apiserver證書請求文件根據證書文件生成apiserver證書僅接著創建TLS所需要的TOKEN創建apiserver服務的配置文件(僅在主節點上創建即可)創建apiserver服務管理配置文件對所有master節點分發證書 & TOK…

基于RK3588/RK3576+MCU STM32+AI的儲能電站電池簇管理系統設計與實現

伴隨近年來新型儲能技術的高質量規模化發展,儲能電站作為新能源領域的重要載體, 旨在配合逐步邁進智能電網時代,滿足電力系統能源結構與分布的創新升級,給予相應規模 電池管理系統的設計與實現以新的挑戰。同時,電子系…

K8s 分布式存儲后端(K8s Distributed Storage Backend)

K8s 分布式存儲后端 在 K8s 中實現分布式存儲后端對于管理跨集群的持久數據、確保高可用性、可擴展性和可靠性至關重要。在 K8s 環境中,應用程序通常被容器化并跨多個節點部署。雖然 K8s 可以有效處理無狀態應用程序,但有狀態應用程序需要持久存儲來維護…

FFmpeg:多媒體處理的瑞士軍刀

FFmpeg:多媒體處理的瑞士軍刀 前言 FFmpeg 是一個功能強大且跨平臺的開源多媒體框架,廣泛應用于音視頻處理領域。 它由多個庫和工具組成,能夠處理各種音視頻格式,涵蓋編碼、解碼、轉碼、流處理等多種操作。 無論是專業視頻編輯…

unordered_map/set的哈希封裝

【C筆記】unordered_map/set的哈希封裝 🔥個人主頁:大白的編程日記 🔥專欄:C筆記 文章目錄 【C筆記】unordered_map/set的哈希封裝前言一. 源碼及框架分析二.迭代器三.operator[]四.使用哈希表封裝unordered_map/set后言 前言 哈…

編程AI深度實戰:大模型哪個好? Mistral vs Qwen vs Deepseek vs Llama

?? 系列文章: 編程AI深度實戰:私有模型deep seek r1,必會ollama-CSDN博客 編程AI深度實戰:自己的AI,必會LangChain-CSDN博客 編程AI深度實戰:給vim裝上AI-CSDN博客 編程AI深度實戰:火的編…