題:
C-將全局變量包裝在結構中?
Dirk Bruere
2019-11-19 20:17:29 UTC
view on stackexchange narkive permalink

有關C的固件樣式問題。

我正在整理一些舊代碼。討厭的功能之一是全局變量分散在源文件中。我可以讓他們中的一些當地人,這很好。但是,其餘如何處理。我更喜歡創建一個結構並將其放入其中。

我知道從理論上講,訪問它們時還存在另一個間接級別,但是從POV樣式來看,這比將它們放入文件globals.c和/或globals.h更好或更糟?

欺騙?https://stackoverflow.com/questions/2868651/includes-c-header-file-with-lots-of-global-variables“僅.h文件中的聲明和.c文件中的定義。”
@TonyStewartSunnyskyguyEE75之類的。那裡接受的答案是使用結構。儘管如此,代碼中還有更多邪惡的東西,包括#define中的代碼和隱式運算符優先級被使用,例如x = y / 1/2/3/4;
不是答案而是一種意見:我更喜歡以某種方式對“全局變量”進行分組,例如`typedef struct {static const int baudrate = 115200;靜態常量int TX_Pin = 11;靜態常量int RX_Pin = 12;} UART;` 這樣我就可以調用UART :: TX_Pin或類似的東西(偽代碼) 這也允許將不同的組放置在最合適的位置(不再需要`globals.h`)。
@FMashiro:那是C ++,不是C。
@LaurentLARIZZA是受c啟發的偽代碼,該思想可以用C以及幾乎任何其他編程語言來實現。
全局變量用於什麼?我敢肯定,只要有足夠的時間/意願,就有可能完全擺脫它們。
@JohnGo-Soco不幸的是,我只有兩週的時間來記錄和整理約6000行未記錄的代碼,其中填充了諸如與本地人同名的全局變量,包含代碼和魔術數字的#defines
@DirkBruere認為。真正向前邁進的最好方法是盡力而為,但盡可能多地記錄文檔。
德克,你忘了接受答案嗎?
七 答案:
Lundin
2019-11-19 20:49:17 UTC
view on stackexchange narkive permalink

眾所周知的 不良做法 擁有一個諸如“ globals.h”或“ includes.h”之類的“超級標頭”文件,因為除了全局變量在首先,這還會在項目中的每個不相關文件之間創建緊密耦合的依賴關係。

假設您有一個PWM驅動器和一個RS-232調試打印模塊。您想獲取RS-232調試打印模塊,然後在另一個項目中重新使用它。突然,您發現自己需要一些PWM.h,而您對它的含義或需求來自何處卻一無所知。您會發現自己在問為什麼在地球上需要PWM驅動器才能運行RS-232。

然後,我們甚至都沒有考慮過重新加入這個凌亂的全局結構。甚至不這樣做。


解開全局意大利麵條的正確方法應該是這樣的:

  • 首先使用變量嗎?如果沒有,請卸下。 (這很常見)
  • 能否將變量移到函數內部的局部範圍內?
  • 是否可以通過使變量成為 static 來將其移動到本地 .c 文件範圍?您是否可以通過實現setter / getter函數來減少 .c 文件外部的訪問?

如果上述所有操作均失敗,則您發現自己所查看的變量應該是寄存器映射的內存映射硬件寄存器部分,或者它是在維護期間添加的一些實時關鍵性臟修復程序。

但是,將**相關變量放在一個結構中怎麼了?假設您想獲取PWM驅動器模塊,然後在另一個需要實例化它們的項目中重新使用它。
@Bergi如果它們在.c文件中,則完全沒有問題。如果它們在.h文件中是全局的,則將它們放置在結構中並沒有明顯的改善。如果它們屬於一起,則它們可以位於一個結構中,而不管該結構的實例在何處聲明。
我喜歡第三個選項,其中變量是在.c文件中定義的,而get / set函數僅位於標頭中。
@danmcb:我想通過鏈接時優化可以希望它不會損失任何性能,並且可以阻止您使用全局地址。如果某些全局函數調用比變量名更具可讀性,則IDK取決於選擇的名稱,但這確實使它看起來與其他變量不同。使用getter / setter使其成為“靜態”文件範圍,如果仍然可以在整個意大利面樣式中訪問它,則並不能真正使事情變得更好。這個答案表明,如果*大部分*用途可以包含在一個文件中,則很有用,但您仍然需要一些外部訪問權限。
值得指出的是,“本地範圍”仍然可以具有靜態存儲類。只是要注意,非常量初始化器不再像C中的全局作用域那樣,不再是編譯時錯誤,而是會花費運行時性能檢查已初始化的標誌。(並且在第一次調用時進行線程安全的初始化。)
-1
@PeterCordes對。正如Lundin所說,如果以後需要確保“無論何時更改X的值,我都還應該檢查...”,那麼如果使用get / set,則很容易做到。隨著這些類型的系統的發展,這確實發生了。我從來沒有看到過調用的開銷是一個問題,如果我想您可以將其聲明為內聯或其他內容。無論如何,都無需過早優化。
Anders Petersson
2019-11-19 20:31:55 UTC
view on stackexchange narkive permalink

將實例化為單個全局變量的結構定義為有效。以'the_global.the_var'形式進行的訪問不會增加運行時的開銷,並且可以說明它確實是全局的。 正如 https://stackoverflow.com/questions/2868651/includes-c-header-file-with-lots-of-global-variables所述,它使您免於單獨的聲明和定義。>

就我個人而言,我不會費心去做一個結構,而是喜歡將全局變量排序到頭文件中,在這些文件中我覺得它們在邏輯上屬於它們,並為每個頭文件使用通用前綴。示例:文件calculate.h聲明為“ extern int calc_result;”並定義了“ int calc_result;”

其他變量不再是文件本地文件,即“靜態int結果;”在.c文件中。

由於您擁有舊版代碼,我認為除了整理代碼之外,您將無法進行很多工作,因此我想,使結構清晰的最快解決方案是最好的。

+1不要對遺留代碼進行過度處理。當然,您可以通過重寫等來做得更好(甚至是原始作者也可以),但是讓我向您保證,您的職業生涯中還有很多代碼需要編寫。專注於使其盡快進入更可維護的階段並繼續前進。
Geoxion
2019-11-19 20:29:04 UTC
view on stackexchange narkive permalink

如果您不能擺脫全局變量,我想說的是,如果它們確實相關,則只應將它們打包在一起。如果沒有,那麼我將它們分開或放在較小的結構中。

此外,我也不想要globals.h文件。將它們放在它們最所屬的源文件的頂部。這樣,在瀏覽代碼時,您可能會留在原本的地方或去可能要去的地方。

另外:將它們不必要地放入結構中可能會誤導未來的程序員(包括您自己)分配此類結構的多個實例;然後變得非常混亂
@Curd另一方面,如果代碼被足夠模塊化地編寫,則它*可以*輕鬆地多次實例化此類結構,而不會造成麻煩。
但是從上下文來看,我認為,如果這些結構存在多次,那麼這不僅是不必要的,而且甚至是有害的。
這聽起來對我來說是最實用的方法。OP顯然不希望沒有任何全局變量,因此如果可以在不進行大量重構的情況下將其刪除,則不會提出此問題。
通常,將某些全局變量放到一個結構中沒有錯-如果它們彼此相關(例如,屬於同一接口)。但是,將**任何**全局變量放到一個結構中並不是一個好主意,除了成為全局變量**之外沒有其他原因。
Michel Keijzers
2019-11-19 20:29:21 UTC
view on stackexchange narkive permalink

實際上,這並不好,唯一的好處是您“知道”哪些是全球性的。因此,所有全局變量都集中在一個位置,但是它們仍然分散在各處。

C

對於每個全局變量:

  • 在創建或使用它的初始文件中查找。
  • 在該文件中將其設為靜態
  • 如果在許多地方使用了global,請在其他文件的外部使用它(使用 extern 關鍵字)。
  • 但是最好的方法是將變量傳遞到需要的地方。

C ++

對於每個全局變量:

  • 找到創建或使用它的初始位置。
  • 找到最適合的課程。
  • 將其移動到該類(作為字段,連接到對象)。
  • 現在創建一個Get / Set方法(函數)。 Set方法最好是受保護的或私有的。
  • 代替Get和Set方法,您還可以通過方法參數傳遞它們。
  • 使用其他類的Get / Set方法。
  • 在某些情況下,您會看到它不能屬於類的字段,例如,當只能有一個“ one”時。在這種情況下,請使用Singleton設計模式。 (請注意;它仍然是全球性的,因此請確保將來不再使用它們;否則,這是一個變相的全球性空間。)
好吧,C ++會好很多,但是我會堅持使用C,除非我進行完全重寫(我想這樣做,但是沒有時間等)
@DirkBruere的“ static”文件作用域是在單核,單進程MCU系統中進行私有封裝的完美方法。大多數情況下,您不需要OO私有封裝,但是如果需要,可以在SO上查看_opaque type_的概念。
@DirkBruere我真的以為我看到了C ++,但是我更新了答案。保持C ++只是為了完整性。
*“ C ...在該類中使其靜態” *-應該是*“ C ...在該類中使其靜態” *嗎?
諸如類之類的@DigitalTrauma OO概念與語言無關。C沒有`class`關鍵字,但是您可以使用任何程序語言編寫類。這是一個程序設計術語。
@DigitalTrauma謝謝,在這種情況下文件會更好,但是正如Lundin解釋的那樣,文件可以用作類(至少作為基礎)。
@DirkBruere,我不知道您的環境的細節,但是從C到C ++通常不必完全重寫。通常,將代碼從C轉換為C和C ++的公共子集並不難,然後可以切換編譯器並開始在新代碼中使用C ++功能。
我覺得stackoverflow通常會誤導人們應該寫“普通C”或“現代C ++”,而兩者之間的中間點在某種程度上是無效的。
文件“可以用作類”,但通常不應該-除非您使用Java之類的語言進行編程,這種語言將類的程序設計概念與文件名必須的實現細節混合在一起。
@PeterGreen同意…我看到在工作中人們認為他們用現代C ++編寫,但實際上他們幾乎不使用OO原理來編寫C。
@alephzero不知道您的意思是什麼……在C和C ++中,您都有.h和.cpp文件。如果C程序員很好地構造其代碼,則一個類或多或少是一個文件(或可以將其轉換為另一個文件)。
我必須承認,我更喜歡對中斷處理程序中僅更新的變量使用靜態volatile,並在對其進行測試/更新的地方聲明它們為extern(如果它們自然地組合在一起,則我為那些extern語句有一個頭文件)。
grahamj42
2019-11-21 03:03:47 UTC
view on stackexchange narkive permalink

將全局變量放入結構的最大優點是:

  1. 當您使用全局變量還是局部變量時,很明顯。
  2. 不可能意外地屏蔽具有相同名稱的局部變量的全局變量。
  3. ol>

    更小的優勢是:

    1. 調試全局數組或內存緩衝區中的溢出更容易,因為可以從結構推斷出內存佈局。
    2. 可以非常簡單地將全局結構保存到非易失性存儲中或從中存儲。
    3. ol>

      合理使用全局變量可以降低堆棧要求,但是如果沒有充分說明它們的用法並遵守規則,則會增加出錯的風險。

Laurent LA RIZZA
2019-11-21 18:28:24 UTC
view on stackexchange narkive permalink

為什麼在 struct 中應該有另一種間接訪問變量的級別?如果您這樣聲明結構:

  typedef struct tagUART {
    int field1;
    int field2;
} UART;
 

然後聲明一個全局變量:

  UART uart;
 

而不是聲明這些全局變量:

  int uartField1;
int uartField2;
 

與通過訪問 uartField1 。編譯器知道結構的起始地址,而struct聲明定義結構中的字段偏移量。編譯器知道參考點的一切。

現在,如果您開始將指向 uart 的指針作為函數參數傳遞給指針,並且函數調用不會被內聯,則將出現額外的間接級別。在那裡,編譯器失去了“嘿,這些字段都有一個眾所周知的地址”的事實。

Peter Green
2019-12-20 01:49:47 UTC
view on stackexchange narkive permalink

我知道從理論上講,訪問它們時還存在另一種間接方式,

這取決於一個真正愚蠢的編譯器可能會為結構引入額外的間接級別,但是如果涉及到一個結構,智能編譯器實際上可以產生更好的代碼。

與其他答案相反,編譯器不知道全局變量的地址。對於同一編譯單元中的全局變量,它知道它們相對於彼此的位置,但不知道它們的絕對位置。對於其他編譯單元中的變量,無論是絕對值還是相對值,都不知道它們的位置。

相反,由編譯器生成的代碼將使用佔位符表示全局變量的位置,然後在確定最終地址時(通常是在鏈接程序時,但有時直到運行)將其替換為實際位置。 )。

此外,儘管有些CPU(如6502)可以在沒有任何預先設置的情況下直接訪問全局內存位置,但有許多不能。例如,在arm上,要訪問全局變量,編譯器通常必須首先使用文字池或在mov的最新版本中使用movw / movt將變量的地址加載到寄存器中。然後使用相對寄存器加載指令訪問全局變量。

這意味著對於編譯單元外部的變量,訪問同一全局結構的多個元素可能比訪問單個全局變量更有效。

要對此進行測試,我將以下代碼通過ARM gcc 8.2和-O3優化輸入到了godbolt中。

  extern int a;
extern int b;

struct Foo {
    詮釋
    int b;
};

extern struct Foo foo;

無效f1(void){
    a = 1;
    b = 2;
}

無效f2(void){
    foo.a = 1;
    foo.b = 2;
}
 

這導致了

  f1:
        mov r0,#1
        mov r2#2
        ldr r1,.L3
        ldr r3,.L3 + 4
str r0,[r1]
        str r2,[r3]
        bx lr
.L3:
        .word a
        .word b
f2:
        mov r1#1
        mov r2#2
        ldr r3,.L6
        stm r3,{r1,r2}
        bx lr
.L6:
        .word foo
 

在f1的情況下,我們看到編譯器從文字池中單獨加載a和b的地址(文字池中的地址稍後將由鏈接器填充)。

但是對於f2來說,編譯器只需從文字池中加載一次結構的地址,更好的是,因為它知道兩個變量在內存中彼此相鄰,因此可以使用單個“存儲”將它們寫入多個”指令,而不是兩個單獨的存儲指令。

但是從樣式POV來看,這比將它們放入文件globals.c和/或globals.h更好或更糟?

取決於變量是否實際相關的IMO。



該問答將自動從英語翻譯而來。原始內容可在stackexchange上找到,我們感謝它分發的cc by-sa 4.0許可。
Loading...