tft每日頭條

 > 圖文

 > c127使用技巧

c127使用技巧

圖文 更新时间:2025-04-27 05:46:14

C 20 最大的特性是什麼?

最大的特性是迄今為止沒有哪一款編譯器完全實現了所有特性。

c127使用技巧(C20四大特性之一)1

文章來源:網易雲信

有人認為 C 20 是 C 11 以來最大的一次改動,甚至比 C 11 還要大。本文僅介紹 C 20 四大特性當中的 Module 部分,分為三部分:

探究 C 編譯鍊接模型的由來以及利弊

介紹 C 20 Module 機制的使用姿勢

總結 Module 背後的機制、利弊、以及各大編譯器的支持情況

C 是兼容 C 的,不但兼容了 C 的語法,也兼容了 C 的編譯鍊接模型。1973年初,C 語言基本定型:有了預處理、支持結構體;編譯模型也基本定型為:預處理、編譯、彙編、鍊接四個步驟并沿用至今;1973年,K&R 二人使用 C 語言重寫了 Unix 内核。

為何要有預處理?為何要有頭文件?在 C 誕生的年代,用來跑 C 編譯器的計算機 PDP-11 的硬件配置是這樣的:内存:64 KiB 硬盤:512 KiB。編譯器無法把較大的源碼文件放入狹小的内存,故當時 C 編譯器的設計目标是能夠支持模塊化編譯,即将源碼分成多個源碼文件、挨個編譯,以生成多個目标文件,最後整合(鍊接)成一個可執行文件。

C 編譯器分别編譯多個源碼文件的過程,實際上是一個 One pass compile 的過程,即:從頭到尾掃描一遍源碼、邊掃描邊生成目标文件、過眼即忘(以源碼文件為單位)、後面的代碼不會影響編譯器前面的決策,該特性導緻了 C 語言的以下特征:

結構體必須先定義再使用,否則無法知道成員的類型以及偏移,就無法生成目标代碼。

局部變量先定義再使用,否則無法知道變量的類型以及在棧中的位置,且為了方便編譯器管理棧空間,局部變量必須定義在語句塊的開始處。

外部變量隻需要知道類型、名字(二者合起來便是聲明)即可使用(生成目标代碼),外部變量的實際地址由連接器填寫。

外部函數隻需知道函數名、返回值、參數類型列表(函數聲明)即可生成調用函數的目标代碼,函數的實際地址由連接器填寫。

頭文件和預處理恰好滿足了上述要求,頭文件隻需用少量的代碼,聲明好函數原型、結構體等信息,編譯時将頭文件展開到實現文件中,編譯器即可完美執行 One pass comlile 過程了。

至此,我們看到的都是頭文件的必要性和益處,當然,頭文件也有很多負面影響:

低效:頭文件的本職工作是提供前置聲明,而提供前置聲明的方式采用了文本拷貝,文本拷貝過程不帶有語法分析,會一股腦将需要的、不需要的聲明全部拷貝到源文件中。

傳遞性:最底層的頭文件中宏、變量等實體的可見性,可以通過中間頭文件“透傳”給最上層的頭文件,這種透傳會帶來很多麻煩。

降低編譯速度:加入 a.h 被三個模塊包含,則 a 會被展開三次、編譯三次。

順序相關:程序的行為受頭文件的包含順影響,也受是否包含某一個頭文件影響,在 C 中尤為嚴重(重載)。

不确定性:同一個頭文件在不同的源文件中可能表現出不同的行為,導緻這些不同的原因,可能源自源文件(比如該源文件包含的其他頭文件、該源文件中定義的宏等),也可能源自編譯選項。

C 20 中加入了 Module,我們先看 Module 的基本使用姿勢,最後再總結 Module 比 頭文件的優勢。

Module(即模塊)避免了傳統頭文件機制的諸多缺點,一個 Module 是一個獨立的翻譯單元,包含一個到多個 module interface file(即模塊接口文件),包含 0 個到多個 module implementation file(即模塊實現文件),使用 Import 關鍵字即可導入一個模塊、使用這個模塊暴露的方法。

實現一個最簡單的 Module

module_hello.cppm:定義一個完整的hello模塊,并導出一個 say_hello_to 方法給外部使用。當前各編譯器并未規定模塊接口文件的後綴,本文統一使用 ".cppm" 後綴名。".cppm" 文件有一個專用名稱"模塊接口文件",值得注意的是,該文件不光可以聲明實體,也可定義實體。

c127使用技巧(C20四大特性之一)2

main 函數中可以直接使用 hello 模塊:

c127使用技巧(C20四大特性之一)3

編譯腳本如下,需要先編譯 module_hello.cppm 生成一個 pcm 文件(Module 緩存文件),該文件包含了 hello 模塊導出的符号。

c127使用技巧(C20四大特性之一)4

以上代碼有以下細節需要注意:

module hello:聲明了一個模塊,前面加一個 export,則意味着當前文件是一個模塊接口文件(module interface file),隻有在模塊接口文件中可以導出實體(變量、函數、類、namespace等)。一個模塊至少有一個模塊接口文件、模塊接口文件可以隻放實體聲明,也可以放實體定義。

Import hello:不需加尖括号,且不同于 include,import 後跟的不是文件名,而是模塊名(文件名為 module_hello.cpp),編譯器并未強制模塊名必須與文件名一緻。

想要導出一個函數,在函數定義/聲明前加一個 export 關鍵字即可。

Import 的模塊不具有傳遞性。hello 模塊包含了 string_view,但是 main 函數在使用 hello 模塊前,依然需要再 import ; 。

模塊中的 Import 聲明需要放在模塊聲明之後、模塊内部其他實體聲明之前,即:import <iostream>; 必須放在 export module hello; 之後,void internal_helper() 之前。

編譯時需要先編譯基礎的模塊,再編譯上層模塊,buildfile.sh 中先将 module_hello 編譯生成 pcm,再編譯 main。

接口與實現分離

上個示例中,接口的聲明與實現都在同一個文件中(.cppm中,準确地說,該文件中隻有函數的實現,聲明是由編譯器自動生成、放到緩存文件pcm中),當模塊的規模變大、接口變多之後,将所有的實體定義都放在模塊接口文件中會非常不利于代碼的維護,C 20 的模塊機制還支持接口與實現分離。下面我們将接口的聲明與實現分别放到 .cppm 和 .cpp 文件中。

module_hello.cppm:我們假設 say_hello_to、func_a、func_b 等接口十分複雜,.cppm 文件中隻包含接口的聲明(square 方法是個例外,它是函數模闆,隻能定義在 .cppm 中,不能分離式編譯)。

c127使用技巧(C20四大特性之一)5

module_hello.cpp:給出 hello 模塊的各個接口聲明對應的實現。

c127使用技巧(C20四大特性之一)6

代碼有幾個細節需要注意:

整個 hello 模塊分成了 module_hello.cppm 和 module_hello.cpp 兩個文件,前者是模塊接口文件(module 聲明前有 export 關鍵字),後者是模塊實現文件(module implementation file)。當前各大編譯器并未規定模塊接口文件的後綴必須是 cppm。

模塊實現文件中不能 export 任何實體。

函數模闆,比如代碼中的 square 函數,定義必須放在模塊接口文件中,使用 auto 返回值的函數,定義也必須放在模塊接口文件。

可見性控制

在模塊最開始的例子中,我們就提到了模塊的 Import 不具有傳遞性:main 函數使用 hello 模塊的時候必須 import <string_view>,如果想讓 hello 模塊中的 string_view 模塊暴露給使用者,需使用 export import 顯式聲明:

c127使用技巧(C20四大特性之一)7

hello 模塊顯式導出 string_view 後,main 文件中便無需再包含 string_view 了。

c127使用技巧(C20四大特性之一)8

子模塊(Submodule)

當模塊變得再大一些,僅僅是将模塊的接口與實現拆分到兩個文件也有點力不從心,模塊實現文件會變得非常大,不便于代碼的維護。C 20 的模塊機制支持子模塊。

這次 module_hello.cppm 文件不再定義、聲明任何函數,而是僅僅顯式導出 hello.sub_a、hello.sub_b 兩個子模塊,外部需要的方法都由上述兩個子模塊定義,module_hello.cppm 充當一個“彙總”的角色。

c127使用技巧(C20四大特性之一)9

子模塊 module hello.sub_a 采用了接口與實現分離的定義方式:“.cppm” 中給出定義,“.cpp” 中給出實現。

c127使用技巧(C20四大特性之一)10

c127使用技巧(C20四大特性之一)11

module hello.sub_b 同上,不再贅述。

c127使用技巧(C20四大特性之一)12

c127使用技巧(C20四大特性之一)13

這樣,hello 模塊的接口和實現文件被拆分到了兩個子模塊中,每個子模塊又有自己的接口文件、實現文件。

值得注意的是,C 20 的子模塊是一種“模拟機制”,模塊 hello.sub_b 是一個完整的模塊,中間的點并不代表語法上的從屬關系,不同于函數名、變量名等标識符的命名規則,模塊的命名規則中允許點存在于模塊名字當中,點隻是從邏輯語義上幫助程序員理解模塊間的邏輯關系。

Module Partition

除了子模塊之外,處理複雜模塊的機制還有 Module Partition。Module Partition 一直沒想到一個貼切的中文翻譯,或者可以翻譯為模塊分區,下文直接使用 Module Partition。Module Partition 分為兩種:

module implementation partition

module interface partition

module implementation partition 可以通俗的理解為:将模塊的實現文件拆分成多個。module_hello.cppm 文件:給出模塊的聲明、導出函數的聲明。

c127使用技巧(C20四大特性之一)14

模塊的一部分實現代碼拆分到 module_hello_partition_internal.cpp 文件,該文件實現了一個内部方法 internal_helper。

c127使用技巧(C20四大特性之一)15

模塊的另一部分實現拆分到 module_hello.cpp 文件,該文件實現了 func_a、func_b,同時引用了内部方法 internal_helper(func_a、func_b 當然也可以拆分到兩個 cpp 文件中)。

c127使用技巧(C20四大特性之一)16

值得注意的是, 模塊内部 Import 一個 module partition 時,不能 import hello:internal;而是直接import :internal; 。

module interface partition 可以理解為模塊聲明拆分到多個文件中。module implementation partition 的例子中,函數聲明隻集中在一個文件中,module interface partition 可以将這些聲明拆分到多個接口文件。

首先定義一個内部 helper:internal_helper:

c127使用技巧(C20四大特性之一)17

hello 模塊的 a 部分采用聲明 定義合一的方式,定義在 module_hello_partition_a.cppm 中:

c127使用技巧(C20四大特性之一)18

hello 模塊的 b 部分采用聲明 定義分離的方式,module_hello_partition_b.cppm 隻做聲明:

c127使用技巧(C20四大特性之一)19

module_hello_partition_b.cpp 給出 hello 模塊的 b 部分對應的實現:

c127使用技巧(C20四大特性之一)20

module_hello.cppm 再次充當了”彙總“的角色,将模塊的 a 部分 b 部分導出給外部使用:

c127使用技巧(C20四大特性之一)21

module implementation partition 的使用方式較為直觀,相當于我們平時編程中“一個頭文件聲明多個 cpp 實現”這種情況。module interface partition 有點類似于 submodule 機制,但語法上有較多差異:

module_hello_partition_b.cpp 第一行不能使用 import hello:partition_b;雖然這樣看上去更符合直覺,但是不允許。

每個 module partition interface 最終必須被 primary module interface file 導出,不能遺漏。

primary module interface file 不能導出 module implementation file,隻能導出 module interface file,故在 module_hello.cppm 中 export :internal; 是錯誤的。

同樣作為處理大模塊的機制,Module Partition 與子模塊最本質的區别在于:子模塊可以獨立的被外部使用者 Import,而 Module Partition 隻在模塊内部可見。

全局模塊片段

(Global module fragments)

C 20 之前有大量的不支持模塊的代碼、頭文件,這些代碼實際被隐式的當作全局模塊片段處理,模塊代碼與這些片段交互方式如下:

c127使用技巧(C20四大特性之一)22

事實上,由于标準庫的大多數頭文件尚未模塊化(VS 模塊化了部分頭文件),整個第二章的代碼在當前編譯器環境下(Clang12)是不能直接編譯通過的——當前尚不能直接 import < iostream > 等模塊,通全局模塊段則可以進行方便的過渡(在全局模塊片段直接 #include <iostream>),另一個過渡方案便是下一節所介紹的 Module Map——該機制可以使我們能夠将舊的 iostream編譯成一個 Module。

Module Map

Module Map 機制可以将普通的頭文件映射成 Module,進而可以使舊的代碼吃到 Module 機制的紅利。下面便以 Clang13 中的 Module Map 機制為例:

假設有一個 a.h 頭文件,該頭文件曆史較久,不支持 Module:

c127使用技巧(C20四大特性之一)23

通過給 Clang 編譯器定義一個 module.modulemap 文件,在該文件中可以将頭文件映射成模塊:

c127使用技巧(C20四大特性之一)24

c127使用技巧(C20四大特性之一)25

編譯腳本需要依次編譯 A、ctype、iostream 三個模塊,然後再編譯 main 文件:

c127使用技巧(C20四大特性之一)26

首先使用 -fmodule-map-file 參數,指定一個 module map file,然後通過 -fmodule 指定 map file 中定義的 module,就可以将頭文件編譯成 pcm。main 文件使用 A、iostream 等模塊時,同樣需要使用 fmodule-map-file 參數指定 mdule map 文件,同時使用 -fmodule 指定依賴的模塊名稱。

注:關于 Module Map 機制能夠查到的資料較少,有些細節筆者也未能一一查明,例如:

通過 Module Map 将一個頭文件模塊化之後,頭文件中暴露的宏會如何處理?

假如頭文件聲明的實體的實現分散在多個 cpp 中,該如何組織編譯?

Module 與 Namespace

Module 與 Namespace 是兩個維度的概念,在 Module 中同樣可以導出 Namespace:

c127使用技巧(C20四大特性之一)27

c127使用技巧(C20四大特性之一)28

總結

最後,對比最開始提到的頭文件的缺點,模塊機制有以下幾點優勢:

無需重複編譯:一個模塊的所有接口文件、實現文件,作為一個翻譯單元,一次編譯後生成 pcm,之後遇到 Import 該模塊的代碼,編譯器會從 pcm 中尋找函數聲明等信息,該特性會極大加快 C 代碼的編譯速度。

隔離性更好:模塊内 Import 的内容,不會洩漏到模塊外部,除非顯式使用 export Import 聲明。

順序無關:Import 多個模塊,無需關心這些模塊間的順序。

減少冗餘與不一緻:小的模塊可以直接在單個 cppm 文件中完成實體的導出、定義,但大的模塊依然會把聲明、實現拆分到不同文件。

子模塊、Module Partition 等機制讓大模塊、超大模塊的組織方式更加靈活。

全局模塊段、Module Map 制使得 Module 與老舊的頭文件交互成為可能。

缺點也有:

編譯器支持不穩定:尚未有編譯器完全支持 Module 的所有特性、Clang13 支持的 Module Map 特性不一定保留到主幹版本。

編譯時需要分析依賴關系、先編譯最基礎的模塊。

現有的 C 工程需要重新組織 pipline,且尚未出現自動化的構建系統,需要人工根據依賴關系組構建腳本,實施難度巨大。

Module 不能做什麼?

Module 不能實現代碼的二進制分發,依然需要通過源碼分發 Module。

pcm 文件不能通用,不同編譯器的 pcm 文件不能通用,同一編譯器不同參數的 pcm 不能通用。

無法自動構建,現階段需要人工組織構建腳本。

編譯器如何實現對外隐藏 Module 内部符号的?

在 Module 機制出現之前,符号的鍊接性分為外部連接性(external linkage,符号可在文件之間共享)、内部鍊接性(internal linkage,符号隻能在文件内部使用),可以通過 extern、static 等關鍵字控制一個符号的鍊接性。

Module 機制引入了模塊鍊接性(module linkage),符号可在整個模塊内部共享(一個模塊可能存在多個 partition 文件)。

對于模塊 export 的符号,編譯器根據現有規則(外部連接性)對符号進行名稱修飾(name mangling)。

對于 Module 内部的符号,統一在符号名稱前面添加 “_Zw” 名稱修飾,這樣鍊接器鍊接時便不會鍊接到内部符号。

截至2020.7,三大編譯器對 Module 機制的支持情況:

c127使用技巧(C20四大特性之一)29

以上就是本文的全部内容,關于 C 20 的四大特性我們介紹了其一

寫在最後:其實每個人都有自己的選擇,學編程,每一種編程語言的存在都有其應用的方向,選擇你想從事的方向,去進行合适的選擇就對了!對于準備學習編程的小夥伴,如果你想更好的提升你的編程核心能力(内功)不妨從現在開始!

編程學習書籍分享:

c127使用技巧(C20四大特性之一)30

編程學習視頻分享:

c127使用技巧(C20四大特性之一)31

整理分享(多年學習的源碼、項目實戰視頻、項目筆記,基礎入門教程)

歡迎轉行和學習編程的夥伴,利用更多的資料學習成長比自己琢磨更快哦!

對于C/C 感興趣可以關注小編在後台私信我:【編程交流】一起來學習哦!可以領取一些C/C 的項目學習視頻資料哦!已經設置好了關鍵詞自動回複,自動領取就好了!

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2025 - www.tftnews.com All Rights Reserved