C語言編程筆記丨失落的C語言結構體封裝藝術

来源:https://www.cnblogs.com/L928/archive/2019/03/29/10624620.html
-Advertisement-
Play Games

1. 誰該閱讀這篇文章 本文是關於削減C語言程式記憶體占用空間的一項技術——為了減小記憶體大小而手工重新封裝C結構體聲明。你需要C語言的基本知識來讀懂本文。 如果你要為記憶體有限制的嵌入式系統、或者操作系統內核寫代碼,那麼你需要懂這項技術。如果你在處理極大的應用程式數據集,以至於你的程式常常達到記憶體的界限 ...


1. 誰該閱讀這篇文章

本文是關於削減C語言程式記憶體占用空間的一項技術——為了減小記憶體大小而手工重新封裝C結構體聲明。你需要C語言的基本知識來讀懂本文。

如果你要為記憶體有限制的嵌入式系統、或者操作系統內核寫代碼,那麼你需要懂這項技術。如果你在處理極大的應用程式數據集,以至於你的程式常常達到記憶體的界限時,這項技術是有幫助的。在任何你真的真的需要關註將高速緩存行未命中降到最低的應用程式里,懂得這項技術是很好的。

最後,理解該技術是一個通往其他深奧的C語言話題的入口。直到你掌握了它,你才成為一個高端的C程式員。直到你可以自己寫出這篇文檔並且可以理智地評論它,你才成為一位C語言大師。

 

2. 我為什麼寫這篇文章

本文之所以存在,是因為在2013年底,我發現我自己在大量使用一項C語言的優化技術,我早在二十多年前就已經學會了該技術,不過在那之後並沒怎麼使用過。

我需要減小一個程式的記憶體占用空間,它用了幾千——有時是幾十萬個——C結構體的實例。這個程式是cvs-fast-export,而問題在於處理巨大的代碼庫時,它曾因記憶體耗盡的錯誤而瀕臨崩潰。

在這類情況下,有好些辦法能極大地減少記憶體使用的,比如小心地重新安排結構體成員的順序之類的。這可以獲得巨大的收益——在我的事例中,我能夠減掉大約40%的工作區大小,使得程式能夠在不崩潰的情況下處理大得多的代碼庫。

當我解決這個問題,並且回想我所做的工作時,我開始發現,我在用的這個技術現今應被忘了大半了。一個網路調查確認,C程式員好像已經不再談論該技術了,至少在搜索引擎可以看到的地方不談論了。有幾個維基百科條目觸及了這個話題,但是我發現沒人能全面涵蓋。

實際上這個現象也是有合理的理由的。電腦科學課程(應當)引導人們避開細節的優化而去尋找更好的演算法。機器資源價格的暴跌已經使得壓榨記憶體用量變得不那麼必要了。而且,想當年,駭客們曾經學習如何使用該技術,使得他們在陌生的硬體架構上撞牆了——現在已經不太常見的經歷。

但是這項技術仍然在重要的場合有價值, 並且只要記憶體有限,就能永存。本文目的就是讓C程式員免於重新找尋這項技術,而讓他們可以集中精力在更重要的事情上。

 

3. 對齊要求(Alignment Requirement)

要明白的第一件事是,在現代處理器上,你的C編譯器在記憶體里對基本的C數據類型的存放方式是受約束的,為的是記憶體訪問更快。

在x86或者ARM處理器上,基本的C數據類型的儲存一般並不是起始於記憶體中的任意位元組地址。而是,每種類型,除了字元型以外,都有對齊要求;字元可以起始於任何位元組地址,但是2位元組的短整型必須起始於一個偶數地址,4位元組整型或者浮點型必須起始於被4整除的地址,以及8位元組長整型或者雙精度浮點型必須起始於被8整除的地址。帶符號與不帶符號之間沒有差別。

這個的行話叫:在x86和ARM上,基本的C語言類型是自對齊(self-aligned)的。指針,無論是32位(4位元組)亦或是64位(8位元組)也都是自對齊的。

自對齊使得訪問更快,因為它使得一條指令就完成對類型化數據的取和存操作。沒有對齊的約束,反過來,代碼最終可能會不得不跨越機器字的邊界做兩次或更多次訪問。字元是特殊的情況;無論在一個單機器字中的何處,存取的花費都是一樣的。那就是為什麼字元型沒有被建議對齊。

我說“在現代的處理器上”是因為,在一些舊的處理器上,強制讓你的C程式違反對齊約束(比方說,將一個奇數的地址轉換成一個整型指針,並試圖使用它)不僅會使你的代碼慢下來,還會造成非法指令的錯誤。比如在Sun的SPARC晶元上就曾經這麼乾。實際上,只要夠決心併在處理器上設定正確(e18)的硬體標誌位,你仍然可以在x86上觸發此錯誤。

此外,自對齊不是唯一的可能的規則。歷史上,一些處理器(特別是那些缺少移位暫存器的)有更強的限制性規則。如果你做嵌入式系統,你也許會在跌倒在這些叢林陷阱中。註意,這是有可能的。

有時你可以通過編譯指示,強制讓你的編譯器不使用處理器正常的對齊規則,通常是#pragma pack。不要隨意使用,因為它會導致產生開銷更大、更慢的代碼。使用我在這裡描述的技術,通常你可以節省同樣或者幾乎同樣多的記憶體。

#pragma pack的唯一好處是,如果你不得不將你的C語言數據分佈精確匹配到某些位級別的硬體或協議的需求,比如一個記憶體映射的硬體埠,要求違反正常的對齊才能奏效。如果你遇到那種情況,並且你還未理解我在這裡寫的這一切,你會有大麻煩的,我只能祝你好運了。

 

 

4. 填充(Padding)

現在我們來看一個簡單變數在記憶體里的分佈的例子。考慮在C模塊的最頂上的以下一系列的變數聲明:

char *p;

char c;

int x;

如果你不知道任何關於數據對齊的事情,你可能會假設這3個變數在記憶體里會占據一個連續位元組空間。那也就是說,在一個32位機器上,指針的4位元組,之後緊接著1位元組的字元型,且之後緊接著4位元組的整型。在64位機器只在指針是8位元組上會有所不同。

這裡是實際發生的(在x86或ARM或其他任何有自對齊的處理器類型)。p的存儲地址始於一個自對齊的4位元組或者8位元組邊界,取決於機器的字長。這是指針對齊——可能是最嚴格的情況。

緊跟著的是c的存儲地址。但是x的4位元組對齊要求,在記憶體分佈上造成了一個間隙;變成了恰似第四個變數插在其中,像這樣:

char *p;      /* 4 or 8 bytes */

char c;       /* 1 byte */

char pad[3];  /* 3 bytes */

int x;        /* 4 bytes */

pad[3]字元數組表示了一個事實,結構體中有3位元組的無用的空間。 老派的術語稱之為“slop(水坑)”。

比較如果x是2位元組的短整型會發生什麼:

char *p;

char c;

short x;

在那個情況下,實際的記憶體分佈會變成這樣:

char *p;      /* 4 or 8 bytes */

char c;       /* 1 byte */

char pad[1];  /* 1 byte */

short x;      /* 2 bytes */

另一方面,如果x是一個在64位機上的長整型

char *p;

char c;

long x;

最終我們會得到:

char *p;     /* 8 bytes */

char c;      /* 1 byte

char pad[7]; /* 7 bytes */

long x;      /* 8 bytes */

如果你已仔細看到這裡,現在你可能會想到越短的變數聲明先聲明的情況:

char c;

char *p;

int x;

如果實際的記憶體分佈寫成這樣:

char c;

char pad1[M];

char *p;

char pad2[N];

int x;

我們可以說出M和N的值嗎?

首先,在這個例子中,N是零。x的地址,緊接在p之後,是保證指針對齊的,肯定比整型對齊更嚴格的。

M的值不太能預測。如果編譯器恰巧把c映射到機器字的最後一個位元組,下一個位元組(p的第一部分)會成為下一個機器字的第一個位元組,並且正常地指針對齊。M為零。

c更可能會被映射到機器字的第一個位元組。在那個情況下,M會是以保證p指針對齊而填補的數——在32位機器上是3,64位機器上是7。

如果你想讓那些變數占用更少的空間,你可以通過交換原序列中的x和c來達到效果。

char *p;     /* 8 bytes */

long x;      /* 8 bytes */

char c;      /* 1 byte

通常,對於C程式里少數的簡單變數,你可以通過調整聲明順序來壓縮掉極少幾個位元組數,不會有顯著的節約。但當用於非標量變數(nonscalar variables),尤其是結構體時,這項技術會變得更有趣。

在我們講到非標量變數之前,讓我們講一下標量數組。在一個有自對齊類型的平臺上,字元、短整型、整型、長整型、指針數組沒有內部填充。每個成員會自動自對齊到上一個之後(譯者註:原文 self-aligned at the end of the next one 似有誤)。

在下一章,我們會看到對於結構體數組,一樣的規則並不一定正確。

 

5. 結構體的對齊和填充

總的來說,一個結構體實例會按照它最寬的標量成員對齊。編譯器這樣做,把它作為最簡單的方式來保證所有成員是自對齊,為了快速訪問的目的。

而且,在C語言里,結構體的地址與它第一個成員的地址是相同的——沒有前置填充。註意:在C++里,看上去像結構體的類可能不遵守這個規則!(遵不遵守依賴於基類和虛擬記憶體函數如何實現,而且因編譯器而不同。)

(當你不能確定此類事情時,ANSI C提供了一個offsetof()巨集,能夠用來表示出結構體成員的偏移量。)

考慮這個結構體:

struct foo1 {

    char *p;

    char c;

    long x;

};

假設一臺64位的機器,任何struct foo1的實例會按8位元組對齊。其中的任何一個的記憶體分佈看上去無疑應該像這樣:

struct foo1 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte

    char pad[7]; /* 7 bytes */

    long x;      /* 8 bytes */

};

它的分佈就恰好就像這些類型的變數是單獨聲明的。但是如果我們把c放在第一個,這就不是了。

struct foo2 {

    char c;      /* 1 byte */

    char pad[7]; /* 7 bytes */

    char *p;     /* 8 bytes */

    long x;      /* 8 bytes */

};

如果成員是單獨的變數,c可以起始於任何位元組邊界,並且pad的大小會不同。但因為struct foo2有按其最寬成員進行的指針對齊,那就不可能了。現在c必須於指針對齊,之後7個位元組的填充就被鎖定了。

現在讓我們來說說關於在結構體成員的尾隨填充(trailing padding)。要解釋這個,我需要介紹一個基本概念,我稱之為結構體的跨步地址(stride address)。它是跟隨結構體數據後的第一個地址,與結構體擁有同樣對齊方式。

結構體尾隨填充的通常規則是這樣的:編譯器的行為就如把結構體尾隨填充到它的跨步地址。這條規則決定了sizeof()的返回值。

考慮在64位的x86或ARM上的這個例子:

struct foo3 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte */

};

 

struct foo3 singleton;

struct foo3 quad[4];

你可能會認為,sizeof(struct foo3)應該是9,但實際上是16。跨步地址是(&p)[2]的地址。如此,在quad數組中,每個成員有尾隨填充的7位元組,因為每個跟隨的結構體的第一個成員都要自對齊到8位元組的邊界上。記憶體分佈就如結構體像這樣聲明:

struct foo3 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte */

    char pad[7];

};

作為對照,考慮下麵的例子:

struct foo4 {

    short s;     /* 2 bytes */

    char c;      /* 1 byte */

};

因為s只需對齊到2位元組, 跨步地址就只有c後面的一個位元組,struct foo4作為一個整體,只需要一個位元組的尾隨填充。它會像這樣分佈

struct foo4 {

    short s;     /* 2 bytes */

    char c;      /* 1 byte */

    char pad[1];

};

並且sizeof(struct foo4)會返回4。

現在讓我們考慮位域(bitfield)。它們是你能夠聲明比字元寬度還小的結構體域,小到1位,像這樣:

struct foo5 {

    short s;

    char c;

    int flip:1;

    int nybble:4;

    int septet:7;

};

關於位域需要知道的事情是,它們以字或位元組級別的掩碼和移位指令來實現。從編譯器的觀點來看,struct foo5的位域看上去像2位元組,16位的字元數組裡只有12位被使用。接著是填充,使得這個結構體的位元組長度成為sizeof(short)的倍數即最長成員的大小。

struct foo5 {

    short s;       /* 2 bytes */

    char c;        /* 1 byte */

    int flip:1;    /* total 1 bit */

    int nybble:4;  /* total 5 bits */

    int septet:7;  /* total 12 bits */

    int pad1:4;    /* total 16 bits = 2 bytes */

    char pad2;     /* 1 byte */

};

這裡是最後一個重要的細節:如果你的結構體含有結構體的成員,裡面的結構體也需要按最長的標量對齊。假設如果你寫成這樣:

struct foo6 {

    char c;

    struct foo5 {

        char *p;

        short x;

    } inner;

};

內部結構體的char *p成員使得外部的結構體與內部的一樣成為指針對齊。在64位機器上,實際的分佈是像這樣的:

struct foo6 {

    char c;           /* 1 byte*/

    char pad1[7];     /* 7 bytes */

    struct foo6_inner {

        char *p;      /* 8 bytes */

        short x;      /* 2 bytes */

        char pad2[6]; /* 6 bytes */

    } inner;

};

這個結構體給了我們一個啟示,重新封裝結構體可能節省空間。24個位元組中,有13個位元組是用作填充的。超過50%的無用空間!

 

6. 結構體重排序(reordering)

現在你知道如何以及為何編譯器要插入填充,在你的結構體之中或者之後,我們要考察你可以做些什麼來擠掉這些“水坑”。這就是結構體封裝的藝術。

第一件需要註意的事情是,“水坑”僅發生於兩個地方。一個是大數據類型(有更嚴格的對齊要求)的存儲區域緊跟在一個較小的數據類型的存儲區域之後。另一個是結構體自然結束於它的跨步地址之前,需要填充,以使下一個實例可以正確對齊。

消除“水坑”的最簡單的方法是按對齊的降序來對結構體成員重排序。就是說:所有指針對齊的子域在前面,因為在64位的機器上,它們會有8位元組。接下來是4位元組的整型;然後是2位元組的短整型;然後是字元域。

因此,舉個例子,考慮這個簡單的鏈表結構體:

struct foo7 {

    char c;

    struct foo7 *p;

    short x;

};

顯現出隱含的“水坑”,這樣:

struct foo7 {

    char c;         /* 1 byte */

    char pad1[7];   /* 7 bytes */

    struct foo7 *p; /* 8 bytes */

    short x;        /* 2 bytes */

    char pad2[6];   /* 6 bytes */

};

24個位元組。如果我們按大小重新排序,我們得到:

struct foo8 {

    struct foo8 *p;

    short x;

    char c;

};

考慮到自對齊,我們看到沒有數據域需要填充。這是因為一個較長的、有較嚴格對齊的域的跨步地址,對於較短的、較不嚴格對齊的域來說,總是合法對齊的起始地址。所有重封裝的結構體實際上需要的只是尾隨填充:

struct foo8 {

    struct foo8 *p; /* 8 bytes */

    short x;        /* 2 bytes */

    char c;         /* 1 byte */

    char pad[5];    /* 5 bytes */

};

我們重封裝的轉變把大小降到了16位元組。這可能看上去沒什麼,但是假設你有一個200k的這樣的鏈表呢?節省的空間累積起來就不小了。

註意重排序並不能保證節省空間。把這個技巧運用到早先的例子,struct foo6,我們得到:

struct foo9 {

    struct foo9_inner {

        char *p;      /* 8 bytes */

        int x;        /* 4 bytes */

    } inner;

    char c;           /* 1 byte*/

};

把填充寫出來,就是這樣

struct foo9 {

    struct foo9_inner {

        char *p;      /* 8 bytes */

        int x;        /* 4 bytes */

        char pad[4];  /* 4 bytes */

    } inner;

    char c;           /* 1 byte*/

    char pad[7];      /* 7 bytes */

};

它仍然是24位元組,因為c不能轉換到內部結構體成員的尾隨填充。為了獲得節省空間的好處,你需要重新設計你的數據結構。

自從發佈了這篇指南的第一版,我就被問到了,如果通過重排序來得到最少的“水坑”是如此簡單,為什麼C編譯器不自動完成呢?答案是:C語言最初是被設計用來寫操作系統和其他接近硬體的語言。自動重排序會妨礙到系統程式員規劃結構體,精確匹配位元組和記憶體映射設備控制塊的位級分佈的能力。

 

7. 難以處理的標量的情況

使用枚舉類型而不是#defines是個好主意,因為符號調試器可以用那些符號並且可以顯示它們,而不是未處理的整數。但是,儘管枚舉要保證相容整型類型,C標準沒有明確規定哪些潛在的整型類型會被使用。

註意,當重新封裝你的結構體時,雖然枚舉類型變數通常是整型,但它依賴於編譯器;它們可能是短整型、長整型、甚至是預設的字元型。你的編譯器可能有一個編譯指示或者命令行選項來強制規定大小。

long double類型也是個相似的麻煩點。有的C平臺以80位實現,有的是128, 還有的80位的平臺填充到96或128位。

在這兩種情況下,最好用sizeof()來檢查存儲大小。

最後,在x86下,Linux的雙精度類型有時是一個自對齊規則的特例;一個8位元組的雙精度數據在一個結構體內可以只要求4位元組對齊,雖然單獨的雙精度變數要求8位元組的自對齊。這依賴於編譯器及其選項。

 

8. 可讀性和緩存局部性

儘管按大小重排序是消除“水坑”的最簡單的方式,但它不是必定正確的。還有兩個問題:可讀性和緩存局部性。

程式不只是與電腦的交流,還是與其他人的交流。代碼可讀性是重要的,即便(或者尤其是!)交流的另一方不只是未來的你。

笨拙的、機械的結構體重排序會損害可讀性。可能的話,最好重排域,使得語義相關的數據段緊緊相連,能形成連貫的組群。理想情況下,你的結構體設計應該傳達到你的程式。

當你的程式經常訪問一個結構體,或者結構體的一部分,如果訪問常命中緩存行(當被告知去讀取任何一個塊里單個地址時,你的處理器讀取的整一塊記憶體)有助於提高性能。在64位x86機上一條緩存行為64位元組,始於一個自對齊的地址;在其他平臺上經常是32位元組。

你應該做的事情是保持可讀性——把相關的和同時訪問的數據組合到毗鄰的區域——這也會提高緩存行的局部性。這都是用代碼的數據訪問模式的意識,聰明地重排序的原因。

如果你的代碼有多線程併發訪問一個結構體,就會有第三個問題:緩存行反彈(cache line bouncing)。為了減少代價高昂的匯流排通信,你應該組織你的數據,使得在緊湊的迴圈中,從一條緩存行中讀取,而在另一條緩存行中寫。

是的,這與之前關於把相關數據組成同樣大小的緩存行塊的指南有些矛盾。多線程是困難的。緩存行反彈以及其它的多線程優化問題是十分高級的話題,需要整篇關於它們的教程。這裡我能做的最好的就就是讓你意識到這些問題的存在。

 

9. 其它封裝技術

當重排序與其他技術結合讓你的結構體瘦身時效果最好。如果你在一個結構體里有若幹布爾型標誌,舉個例子,可以考慮將它們減小到1位的位域,並且將它們封裝到結構體里的一個本會成為“水坑”的地方。

為此,你會碰到些許訪問時間上的不利——但是如果它把工作區擠壓得足夠小,這些不利會被避免緩存不命中的得益所掩蓋。

更普遍的,尋找縮小數據域大小的方式。比如在cvs-fast-export里,我用的一項壓縮技術里用到了在1982年之前RCS和CVS代碼庫還不存在的知識。我把64位的Unix time_t(1970年作為起始0日期)減少到32位的、從1982-01-01T00:00:00開始的時間偏移量;這會覆蓋2118年前的日期。(註意:如果你要玩這樣的花招,每當你要設定欄位,你都要做邊界檢查以防討厭的錯誤!)

每一個這樣被縮小的域不僅減少了你結構體顯在的大小,還會消除“水坑”,且/或創建額外的機會來得到域重排序的好處。這些效果的良性疊加不難得到。

最有風險的封裝形式是使用聯合體。如果你知道你結構體中特定的域永遠不會被用於與其他特定域的組合,考慮使用聯合體使得它們共用存儲空間。但你要額外小心,並且用回歸測試來驗證你的工作,因為如果你的生命周期分析即使有輕微差錯,你會得到各種程式漏洞,從程式崩潰到(更糟糕的)不易發覺的數據損壞。

 

10. 工具

C語言編譯器有個-Wpadded選項,能使它產生關於對齊空洞和填充的消息。

雖然我自己還沒用過,但是一些反饋者稱贊了一個叫pahole的程式。這個工具與編譯器合作,產生關於你的結構體的報告,記述了填充、對齊及緩存行邊界。

 

11. 證明及例外

你可以下載一個小程式的代碼,此代碼用來展示了上述標量和結構體大小的論斷。就是packtest.c

如果你瀏覽足夠多的編譯器、選項和不常見的硬體的奇怪組合,你會發現針對我講述的一些規則的特例。如果你回到越舊的處理器設計,就會越常見。

比知道這些規則更進一步,是知道如何以及何時這些規則會被打破。在我學習它們的那些年(1980年代早期),我們把不懂這些的人稱為“世界都是VAX綜合徵”的受害者。記住世界上不只有PC。

 

12. 版本履歷

1.5 @ 2014-01-03

解釋了為什麼不自動做結構體成員的重排序。

1.4 @ 2014-01-06

關於x86 Linux下雙精度的註意。

1.3 @ 2014-01-03

關於難以處理的標量實例、可讀性和緩存局部性及工具的段落。

1.2 @ 2014-01-02

修正了一個錯誤的地址計算。

1.1 @ 2014-01-01

解釋為什麼對齊的訪問會更快。提及offsetof。各種小修複,包括packtest.c的下載鏈接。

1.0 @ 2014-01-01

另外博主是一個有著7年工作經驗的架構師,對於c++,自己有做資料的整合,一個完整學習C語言c++的路線,學習資料和工具。可以進我的Q群7418,18652領取,免費送給大家。希望你也能憑自己的努力,成為下一個優秀的程式員!另外博主的微信公眾號是:C語言編程學習基地,歡迎關註!

 


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

-Advertisement-
Play Games
更多相關文章
  • 什麼是動態代理呢?就是在java的運行過程中,動態的生成的代理類。(為了更熟悉的瞭解動態代理,你必須先熟悉代理模式,可點擊設計模式之代理模式 閱讀)我們知道java屬於解釋型語言,是在運行過程中,尋找位元組碼文件從而實現類載入的。但是位元組碼文件並不需要一定是硬碟中的class文件,也可以是來自網路、數 ...
  • @Data註解主要是幫助解決Setter 和 Getter以及 toString這種重覆的無腦工作 加入@Data註解可以直接幫助我們添加實體類相應的Setter 和 Getter以及 toString 需要自己添加jar包或pom依賴 然後我們可以直接調用相應的Setter 和 Getter以及 ...
  • 自定義 ThreadPoolExecutor 處理線程運行時異常 最近看完了 "ElasticSearch線程池模塊" 的源碼,感觸頗深,然後也自不量力地借鑒ES的 EsThreadPoolExecutor 重新造了一把輪子,對線程池的理解又加深了一些。在繼承 ThreadPoolExecutor實 ...
  • centos7下安裝python3總步驟分三步: 一、依賴解決: 1.安裝依賴包: yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make ...
  • 1.實現Runnable介面,重載run(),無返回值 2.繼承Thread類,覆寫run() 使用時通過調用Thread的start()(該方法是native),再調用創建線程的run(),不同線程的run方法裡面的代碼交替執行。 不足:由於java為單繼承,若使用線程類已經有個父類,則不能使用該 ...
  • 使用Visual Studio新建一個空項目,項目命名為HelloMFC,完成後,打開項目屬性頁面,將配置屬性選項卡中的常規項打開,將其中的MFC的使用屬性欄改為:在靜態庫中使用MFC或者在共用DLL中使用MFC,同時將字元集屬性欄改為使用Unicode字元集。 圖1-1 新建一個空項目 圖1-2 ...
  • 本文以JDK 1.8為例,包路徑rt.jar java.util.lang 1 HashMap原理 在bucket中存儲鍵對象和值對象,作為Map.Entry。使用put(key,value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put方法傳遞鍵值的時候 ...
  • 如果你用過BottomNavigationBar、TabBar、還有Drawer,你就會發現,在切換頁面之後,原來的頁面狀態就會丟失。 要是上一頁有一個數據列表,很多數據,你滾動到了下頭,切換頁面後,想再看一下下頭的數據,但是Flutter給你重回頁面了。。。 這誰能頂得住啊。 看了一下解釋,原來F ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...