</div>
整理下 C 語言中常用的技巧。
實際編程中,經常會使用變長數組,但是 C 語言並不支持變長的數組,可以使用結構體實現。
類似如下的結構體,其中 value 成員變量不佔用內存空間,也可以使用 char value[]
,但是不要使用 char *value
,該變量會佔用指針對應的空間。
常見的操作示例如下。
qsort()
會根據給出的比較函數進行快排,通過指針移動實現排序,時間複雜度為 n*log(n)
,排序之後的結果仍然放在原數組中,不保證排序穩定性,如下是其聲明。
通常可以對整數、字符串、結構體進行排序,如下是常用示例。
以及示例程序。
指針或許是 C 語言中最複雜的東西了。
前面是一個修飾詞,後面的是中心詞。
常量指針 首先是一個指針,指向的是常量,即指向常量的指針;可以通過如下的方式定義:
對於常量,我們不能對其內容進行修改;指針的內容本身是一個地址,通過常量指針指向一個常量,為的就是防止我們寫程序過程中對指針誤操作出現了修改常量這樣的錯誤,如果我們修改常量指針的所指向的空間的時候,編譯系統就會提示我們出錯信息。
在 C 語言中,通常定義的字符串會返回一個常量指針,因此字符串不能賦值給字符數組,只能賦值到指針。
總結一下,常量指針就是指向常量的指針,指針所指向的地址的內容是不可修改的,指針本身的內容是可以修改的 。
指針常量 首先是一個常量,再才是一個指針;可以通過如下的方式定義:
常量的性質是不能修改,指針的內容實際是一個地址,那麼指針常量就是內容不能修改的常量,即內容不能修改的指針,指針的內容是什麼呀?指針的內容是地址,所以,說到底,就是不能修改這個指針所指向的地址,一開始初始化,指向哪兒,它就只能指向哪兒了,不能指向其他的地方了,就像一個數組的數組名一樣,是一個固定的指針,不能對它移動操作。
它只是不能修改它指向的地方,但這個指向的地方里的內容是可以替換的,這和上面說的常量指針是完全不同的概念。
作一下總結,指針常量就是是指針的常量,它是不可改變地址的指針,但是可以對它所指向的內容進行修改 。
源碼可以參考 github const_pointer.c 。
假設有如下數組,
其中 Array 為指針常量,而 ptr 為指針變量,且 ptr = &Array[0]
,那麼如下的操作相同 ptr[i] <=> *(ptr+i)
以及 Array[i] <=> *(Array + i)
。
如下,簡單介紹下常見操作。
由於 *
和 ++
優先級相同,結合性為由右至左,即 *ptr++
等價於 *(ptr++)
,由於 ++
為後繼加,所以當得到 *ptr
後再處理 ++
;所以 *ptr++
等於 1,進行此項操作後 *ptr
等於 2。
執行的步驟為 1) ++
操作符產生 ptr 的一份拷貝;2) ++
操作符增加 ptr 的值;3) 在 ptr 上執行間接訪問操作。
利用優先級和結合性可得,++*ptr
等價於 ++(*ptr)
,此時 Array[0]
為 2,返回 2 。
利用優先級和結合性可得,*ptr++
等價於 *(ptr++)
,返回 1,ptr 值加 1 。
當數據類型大於一個字節時,其所佔用的字節在內存中的順序存在兩種模式:小端模式 (little endian) 和大端模式 (big endian),其中 MSB(Most Significant Bit) 最高有效位,LSB(Least Significant Bit) 最低有效位.
如下是一個測試程序。
如果採用大端模式,則在向某一個函數通過向下類型裝換來傳遞參數時可能會出錯。如一個變量為 int i=1;
經過函數 void foo(short *j);
的調用,即 foo((short*)&i);
,在 foo() 中將 i 修改為 3 則最後得到的 i 為 0x301 。
大端模式規定 MSB 在存儲時放在低地址,在傳輸時 MSB 放在流的開始;小段模式反之。
頭文件 stdarg.h
中對相關的宏進行了定義,其基本內容如下所示:
示例如下圖所示:
其中函數傳參是通過棧傳遞,保存時從右至左依次入棧,以函數 void func(int x, float y, char z)
為例,調用該函數時 z、y、x 依次入棧,理論上來說,只要知道任意一個變量地址,以及所有變量的類型,那麼就可以通過指針移位獲取到所有的輸入變量。
va_list
是一個字符指針,可以理解為指向當前參數的一個指針,取參必須通過這個指針進行。
在使用時,其步驟如下:
其中的使用關鍵是如何獲取變量的類型,通常有兩種方法:A) 提前約定好,如上面的示例;B) 通過入參判斷,如 printf() 。
另外,常見的用法還有獲取省略號指定的參數,例如:
假設,在調用上述的函數時,如果在 _vsnprintf()
中會再調用類似的函數,那麼可以通過 va_list args; va_copy(args, ap);
複製一份。
當調試時定義 DEBUG 輸出信息,通常有如下的幾種方式。
另外,也可以通過如下方式判斷支持可變參數的格式。
為了性能上的考慮,很多的平臺都會從某一個特定的地址開始讀取數據,比如偶地址。
數據結構中的數據變量都是按照定義的順序來定義,第一個變量的地址等同於數據結構的地址,結構體中的成員也要對齊,最後結構體也同樣需要對齊。對齊是指 起始地址對齊,其中對齊規則如下:
數據成員對齊規則
結構體(struct)或聯合(union)的數據成員,第一個數據成員放在offset為0的地方,以後每個數據成員的對齊按照#pragma pack
指定的數值n和這個數據成員自身長度中,比較小的那個進行。
結構體(或聯合)的整體對齊規則
在數據成員完成各自對齊之後,結構體(或聯合)本身也要進行對齊,對齊將按照#pragma pack
指定的數值n和結構體(或聯合)最大數據成員長度中,比較小的那個進行。
當#pragma pack
的n值等於或超過所有數據成員長度的時候,這個n值的大小將不生任何效果。
現舉例如下:
另一種方式是 __attribute((aligned(n)))
讓所作用的結構成員對齊在 n
字節自然邊界上,如果結構中有成員長度大於 n
,則按照最大的成員的長度對齊。
示例如下:
__attribute__((packed))
取消編譯過程中的優化對齊,按照實際佔用字節數進行對齊。
詳見參考程序 github align.c 。
getopt()
是採用緩衝機制,因此對於多線程編程是 不安全 的。
如 optstring="ab:c::d::"
,命令行為 getopt.exe -a -b host -ckeke -d haha
,在這個命令行參數中,-a
-b
和 -c
是選項元素,去掉 '-'
,a b c 就是選項。
host 是 b 的參數,keke 是 c 的參數,但 haha 並不是 d 的參數,因為它們中間有空格隔開。
注意:如果 optstring 中的字符串以 '+'
加號開頭或者環境變量 POSIXLY_CORRE
被設置,那麼一遇到不包含選項的命令行參數,getopt 就會停止,返回 -1;命令參數中的 "--"
用來強制終止掃描。
默認情況下 getopt 會重新排列命令行參數的順序,所以到最後所有不包含選項的命令行參數都排到最後,如 getopt -a ima -b host -ckeke -d haha
,都最後命令行參數的順序是 -a -b host -ckeke -d ima haha
。
如果檢測到設置的參數項,則返回參數項;如果檢測完成則返回 -1;如果有不能識別的參數則將該參數保存在 optopt 中,輸出錯誤信息到 stderr,如果 optstring 以 ':'
開頭則返回 ':'
否則返回 '?'
。
源碼可以參考 github getopt.c 。
源碼可以參考 github getopt_long.c 。
getopt_long_only()
和 getopt_long()
類似,但是 '-'
和 '--'
均被認為是長選項,只有當 '-'
沒有對應的選項時才會與相應的短選項匹配。
以 8-bits 的數據為例,unsigned 取值範圍為 0~255,signed 的取值範圍為 -128~127。在計算機中數據以補碼(正數原碼與補碼相同,原碼=除符號位的補碼求反+1)的形式存在,且規定 0x80 為-128 。
對於無符號整數,當超過 255 後將會溢出,常見的是 Linux 內核中的 jiffies 變量,jiffies 以及相關的宏保存在 linux/jiffies.h 中,如果 a 發生在 b 之後則返回真,即 a>b 返回真,無論是否有溢出。
Clang 是一個 C++ 編寫,基於 LLVM 的 C/C++、Objective-C 語言的輕量級編譯器,在 2013.04 開始,已經全面支持 C++11 標準。
#pragma
宏定義在本質上是聲明,常用的功能就是註釋,尤其是給 Code 分段註釋;另外,還支持處理編譯器警告。
該屬性用於自實現的字符串格式化參數添加類似 printf() 的格式化參數的校驗,判斷需要格式化的參數與入參是否相同。
如下是使用示例。
如下是一個簡單的使用示例。
編譯時添加 -Wall
就會打印 Warning 信息,如果去除,實際上不會顯示任何信息,通常可以提前發現常見的問題。
這是 GCC 的擴展機制,通過上述的屬性,可以使程序在開始執行或停止時調用指定的函數。
__attribute__((constructor))
在 main() 之前執行,__attribute__((destructor))
在 main() 執行結束之後執行。
如果有多個函數,可以指定優先級,其中 0~100 (含100)系統保留。在 main 之前順序為有小到大,退出時順序為由大到小。
在使用時也可以先聲明然再定義
程序調用某個函數 A,而 A 函數存在於兩個動態鏈接庫 liba.so 和 libb.so 中,並且程序執行需要鏈接這兩個庫,此時程序調用的 A 函數到底是來自於 a 還是 b 呢?
這取決於鏈接時的順序,首先鏈接的庫會更新符號表,比如先鏈接 liba.so,這時候通過 liba.so 的導出符號表就可以找到函數 A 的定義,並加入到符號表中,而不會再查找 libb.so 。
也就是說,這裡的調用嚴重的依賴於鏈接庫加載的順序,可能會導致混亂。
gcc 的擴展中有如下屬性 __attribute__ ((visibility("hidden")))
可以用於抑制將一個函數的名稱被導出,對連接該庫的程序文件來說,該函數是不可見的,使用的方法如下:
想要做的是,第一個函數符號可以被導出,第二個被隱藏。
先編譯成一個動態庫,使用到屬性 -fvisibility
。
可以看到,屬性確實有作用了。
現在試圖鏈接程序。
試圖編譯成一個可執行文件,鏈接到剛才生成的動態庫。
說明瞭 hidden 確實起到作用了。
該屬性表示,此可變參數函數需要一個 NULL
作為最後一個參數,這個 NULL
參數一般被叫做 “哨兵參數”。例如,有如下程序:
當通過 gcc main.c -Wall
進行編譯時,會發現沒有任何警告,不過很顯然,調用 foo()
時最後一個參數應該是個 NULL
以表明 “可變參數就這麼多”。
編譯完成後,如果嘗試運行則會打印一些亂碼,顯然是有問題的。
正常來說,應該通過如下方式調用 foo("Hello", "World", NULL);
,為此,就需要用到了上述的屬性,用於表示最後一個參數需要為 NULL
。
這樣再不寫哨兵參數,在編譯時編譯器就會發出警告了。
但是,對於同樣使用可變參數的 printf()
來說,為什麼就不需要哨兵屬性,實際上,通過第一個參數就可以確定需要多少個參數,如下。
很多時候我們需要在程序退出的時候做一些諸如釋放資源的操作,但程序退出的方式有很多種,比如 main() 函數運行結束、在程序的某個地方用 exit() 結束程序、用戶通過 Ctrl+C 或 Ctrl+break 操作來終止程序等等,因此需要有一種與程序退出方式無關的方法來進行程序退出時的必要處理。
方法就是用 atexit() 函數來註冊程序正常終止時要被調用的函數。
成功時返回零,失敗時返回非零。
在一個程序中至少可以用 atexit() 註冊 32 個處理函數,依賴於編譯器。這些處理函數的調用順序與其註冊的順序相反,也即最先註冊的最後調用,最後註冊的最先調用。
如果通過 define
定義一個含有多個語句的宏,通常我們使用 do{...} while(0);
進行定義,具體原因,如下詳細介紹。
如果想在宏中包含多個語句,可能會如下這樣寫。
通常,這樣就可以用 do_somethin()
來執行一系列操作,但這樣會有個問題:如果通過如下的方式用這個宏,將會出錯。
原代碼的目的是想在 if 為真的時候執行 do_a()
和 do_b()
, 但現在,實際上只有 do_a()
在條件語句中,而 do_b()
任何時候都會執行。
當然這時可以通過如下的方式將那個宏改進一下。
然而,即使是這樣,仍然有錯。假設有一個宏是這個樣子的,
在使用如下情況時,仍會出錯。
此時第二個 else 前邊會有一個分號,那麼編譯時就會出錯。
因此對於含有多條語句的宏我們使用 do{...} while(0);
,do-while 語句是需要分號來結束的,另外,現代編譯器的優化模塊能夠足夠聰明地注意到這個循環只會執行一次而將其優化掉。
綜上所述,do{...} while(0);
這個技術就是為了類似的宏可以在任何時候使用。
其作用是如果它的條件返回錯誤,則輸出錯誤信息 (包括文件名,函數名等信息),並終止程序執行,原型定義:
如下是一個簡單的示例。
當在 <assert.h>
之前定義 NDEBUG 時,assert 不會產生任何代碼,否則會顯示錯誤。
在 glibc 中,會定義如下的內容:
可以通過 nm 查看程序,判斷是否存在 __assert_fail@@GLIBC_2.2.5
,如果存在該函數則說明未關閉 assert()
。
對於 autotool 可以通過如下的一種方式關閉:
configure.ac
文件中添加 AC_HEADER_ASSERT
,然後如果關閉是添加 --disable-assert
參數,注意,一定要保證源碼包含了 config.h
頭文件;CPPFLAGS="CPPFLAGS=-DNDEBUG" ./configure
;Makefile.am
中設置 AM_CPPFLAGS += -DNDEBUG
參數。一般可以通過 gdb 的 bt 命令查看函數運行時堆棧,但是,有時為了分析程序的 BUG,可以在程序出錯時打印出函數的調用堆棧。
在 glibc 頭文件 execinfo.h
中聲明瞭三個函數用於獲取當前線程的函數調用堆棧。
注意,需要傳遞相應的符號給鏈接器以能支持函數名功能,比如,在使用 GNU ld 鏈接器的時需要傳遞 -rdynamic
參數,該參數用來通知鏈接器將所有符號添加到動態符號表中。
下面是 glibc 中的實例。
然後通過如下方式編譯,執行。
還可以利用 backtrace 來定位段錯誤位置。
正常情況下,類似庫 libxerces-c-3.0.so
應該是個符號鏈接,而不是實體文件,對於這種情況只需要修改其為符號鏈接即可。
對於 C 中結構體初始化可以通過如下設置。
通過結構體可以將多種不同類型的對象聚合到一個對象中,編譯器會按照成員列表順序分配內存,不過由於內存對齊機制不同,導致不同架構有所區別,所以各個成員之間可能會有間隙,所以不能簡單的通過成員類型所佔的字長來推斷其它成員或結構體對象的地址。
假設有如下的一個鏈表。
當已知一個變量的地址時,如何獲取到某個成員的偏移量,Linux 內核中的實現如下。
當知道了成員偏移量,那麼就可以通過結構體成員的地址,反向求結構體的地址,如下。
現在很多的動態語言是可以支持動態獲取變量類型的,其中 GCC 提供了 typeof
關鍵字,所不同的是這個只在預編譯時,最後實際轉化為數據類型被編譯器處理。基本用法是這樣的:
如上的宏定義中, ptr 代表已知成員的地址,type 代表結構體的類型,member 代表已知的成員。
一個比較容易犯錯的地方,願意是在 foobar()
函數內修改 main()
中的 v 指向的變量,其中後者實際上是修改的本地棧中保存的臨時版本。
Linux 每個數據類型的大小可以在 sys/types.h
中查看
簡單介紹下 C 中,如何獲取以及設置環境變量。
其中設置環境變量方法包括了 putenv()
以及 setenv()
兩種,前者必須是 Key=Value
這種格式,後者則以參數形式傳遞。
對於 putenv()
如果環境變量已經存在則替換,而 setenv()
則可以設置是否覆蓋 。
C 語言標準庫 float.h
中的 FLT_RADIX
常數用於定義指數的基數,也就是以這個數為底的多少次方。
例如:
意思是 float 型,最大指數是 128,它的底是 2,也就說最大指數是 2 的 128 方。
按常規來講,出現 implicit declaration of function 'xxxx'
是因為頭文件未包含導致的!
這裡是由於 nanosleep()
函數的報錯,而實際上 time.h
頭文件已經包含了,後來才發現原來是在 Makefile
中添加了 -std=c99
導致,可以通過 -std=gnu99
替換即可。
另外,不能定義 -D_POSIX_SOURCE
宏。
</div>