引言 - 為尋一顆明星 { 風 : http://music.163.com/#/song?id=5276735 } 前言 - 有點扯 C基本是程式生涯的入門語言. 雖說簡單, 但已經斷層了. 估計是不合時宜吧. 工作中也就在網路層框架會看見部分C的影子. 自己用C開發久了, 發現C一個弊端是 當一 ...
引言 - 為尋一顆明星
為要尋一顆明星
徐志摩 1924年12月1日《晨報六周年紀念增刊》 我騎著一匹拐腿的瞎馬, 向著黑夜裡加鞭;—— 向著黑夜裡加鞭, 我跨著一匹拐腿的瞎馬。// 我沖入這黑綿綿的昏夜, 為要尋一顆明星;—— 為要尋一顆明星, 我沖入這黑茫茫的荒野。// 累壞了,累壞了我胯下的牲口, 那明星還不出現;—— 那明星還不出現, 累壞了,累壞了馬鞍上的身手。// 這回天上透出了水晶似的光明, 荒野里倒著一隻牲口, 黑夜裡躺著一具屍首。—— 這回天上透出了水晶似的光明!//
{ 風 : http://music.163.com/#/song?id=5276735 }
前言 - 有點扯
C基本是程式生涯的入門語言. 雖說簡單, 但已經斷層了. 估計是不合時宜吧.
工作中也就在網路層框架會看見部分C的影子. 自己用C開發久了, 發現C一個弊端是 當一個項目超過 2千行 x 10 時候用C協作
非常難受. C風格是個自由的英雄主義表現.
但是 真實的生活如dota, 我們不是 hero 而只是 那個小兵, 時來運轉會成為超級兵. 哈哈.
但這不重要, 喜歡就好.
生活不止眼前的苟且 ... ...
好那我們開始,看看那些關於C基礎的活化石. 真想問 <<C程式設計>> 這門課你真的學好了嗎?
正文 - 有點難
1. int i = 0; ++i 一直繼續會怎樣?
我們先看這樣的測試代碼
#include <stdio.h> #include <stdlib.h> /* * 測試 int 的最大值 */ int main(void) { int id = 0x7fffffff; printf("-1 = %x\n", -1); printf("id = %d\n", id); ++id; printf("id = %d\n", id); id += 0x7fffffff; printf("id = %d\n", id); id += 0x7fffffff; printf("id = %d\n", id); system("pause"); return 0; }
你能算明白測試結果嗎, 如果可以說明你電腦組成原理學的很好. 運行截圖如下
因而 我們得到 int i = 0; ++i 一直繼續的 會是 0->INT_MAX->INT_MIN->0 這樣迴圈的. 例如 skynet 存在這個使用錯誤
int id = __sync_add_and_fetch(&(ss->alloc_id), 1); if (id < 0) { id = __sync_and_and_fetch(&(ss->alloc_id), 0x7fffffff); }
原作者希望 再從 0開始 , 但卻忘了
#define INT_MIN (-2147483647 - 1) // minimum (signed) int value #define INT_MAX 2147483647 // maximum (signed) int value
對於 signed MAX + MIN = -1 , 因為電腦中 正數從0開始, 負數從-1開始.
2. 添加雙引號 的巨集用法
看下麵代碼
// 添加雙引號的巨集 #ifdef _API_MEM # define STRINIFY_(S) #S # define STRINIFY(S) STRINIFY_(S) # include STRINIFY(_API_MEM) # undef STRINIFY # undef STRINIFY_ #endif
有些工程中使用上面代碼, 來動態的導入頭文件. 核心在於 STRINIFY_ 和 STRINIFY 兩個巨集使用. 我們測試一下
#include <stdio.h> #include <stdlib.h> # define STRINIFY_(S) #S # define STRINIFY(S) STRINIFY_(S) #define _API_MEM api.h // 測試添加雙引號巨集 int main(void) { puts(STRINIFY_(_API_MEM)); puts(STRINIFY(_API_MEM)); system("pause"); return 0; }
運行結果是
通過這個發現, 如果直接用 STRINIFY_ 不會將參數展開了. 這也是一個C行業淫蕩的技巧了. 但是覺得大巧若拙
個人覺得 最好做法是
#define _API_MEM "api.h"
#ifdef _API_MEM # include _API_MEM #endif
3. 除了sizeof, 其實還有 offsetof
直接看例子
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <stddef.h> #include <stdbool.h> #define UDP_ADDRESS_SIZE 19 // ipv6 128bit + port 16 bit + 1 byte type struct write_buffer { struct write_buffer* next; void* buffer; char* ptr; int sz; bool userobject; uint8_t udp_address[UDP_ADDRESS_SIZE]; }; /* * 測試 巨集 offsetof */ int main(int argc, char* argv[]) { printf("offsetof(struct write_buffer, udp_address[0]) = %d\n", offsetof(struct write_buffer, udp_address[0])); printf("offsetof(struct write_buffer, udp_address) = %d\n", offsetof(struct write_buffer, udp_address)); system("pause"); return 0; }
運行的結果如下
通過上面 可以知道 offsetof 其實計算的是結構體中欄位的偏移量. 關於結構體的記憶體計算基礎能力, 必須要掌握的. 洞悉記憶體結構很重要.
其實 offsetof 是 stddef.h 中定義的一個 巨集 如下
#define offsetof(s,m) ((size_t)&(((s*)0)->m))
是不是很清爽. 就是這樣, 沒事簡單的.
其實上面代碼還隱含一個 關於 數組的 細節 . int a[10]; &a[0] == a == &a 地址是相同的.
4. 如何構造一個只能在堆上分配結構體?
//堆上 聲明結構體 struct request_open { int id; int port; uintptr_t opaque; char host[]; };
就是上面那樣, 加了[], 表示不完全類型. 只能在堆上分配記憶體. 使用方法.
struct request_open *open = malloc(sizeof(struct request_open) + sizeof(char) * 19);
這種結構一般在底層庫會看見. 一些老的程式員喜歡這麼寫
//堆上 聲明結構體 struct request_open { int id; int port; uintptr_t opaque; char host[0]; };
或
//堆上 聲明結構體 struct request_open { int id; int port; uintptr_t opaque; char host[1]; };
因為老的編譯器不支持 char host[]; 後面標準加了. 後來沒改過習慣.
5. 如何構造一個在棧上初始化的指針變數
說的不好明白, 或者這麼問, 下麵定義的類型怎麼解.
struct cstring_data { char* cstr; //保存字元串的內容 uint32_t hash; //字元串hash,如果是棧上的保存大小 uint16_t type; //主要看 _INT_STRING_* 巨集,預設0表示臨時串 uint16_t ref; //引用的個數, 在 type == 0時候才有用 }; typedef struct _cstring_buffer { struct cstring_data* str; } cstring_buffer[1]; //這個cstring_buffer是一個在棧上分配的的指針類型
上面也是底層庫中會遇到一個技巧.
當聲明cstring_buffer cb; 後.可以直接cb->str調用它,
當 cb 傳入到 函數中. 仍然可以 cb->str. 可以理解為這個值是棧上的但是可以當指針變數用法去使用. 看下麵也許好理解
typedef struct _jmp_buf { int _jb[_JBLEN + 1]; } jmp_buf[1];
這個是 setjmp.h 里的一行定義,把一個 struct 定義成一個數組。
這樣,在聲明 jmp_buf 的時候,可以把數據分配到堆棧上。但是作為參數傳遞的時候則作為一個指針.
擴展一下閱讀理解可以看下麵. 應該可以知道為什麼這麼搞.
//特殊的數組 聲明結構體 #define _INT_STRING_ONSTACK (4) //標識 字元串分配在棧上 //0 潛在 標識,這個字元串可以被回收,游離態 #define _INT_ONSTACK (128) //棧上記憶體大小 struct cstring_data { char* cstr; //保存字元串的內容 uint32_t hash; //字元串hash,如果是棧上的保存大小 uint16_t type; //主要看 _INT_STRING_* 巨集,預設0表示臨時串 uint16_t ref; //引用的個數, 在 type == 0時候才有用 }; typedef struct _cstring_buffer { struct cstring_data* str; } cstring_buffer[1]; //這個cstring_buffer是一個在棧上分配的的指針類型 /* * v : 是一個變數名 * * 構建一個 分配在棧上的字元串. * 對於 cstring_buffer 臨時串,都需要用這個 巨集聲明創建聲明, * 之後可以用 CSTRING_CLOSE 關閉和銷毀這個變數,防止這個變數變成臨時串 */ #define CSTRING_BUFFER(v) \ char v##_cstring[_INT_ONSTACK] = { '\0' }; \ struct cstring_data v##_cstring_data = { v##_cstring, 0, _INT_STRING_ONSTACK, 0 }; \ cstring_buffer v; \ v->str = &v##_cstring_data;
6. 那些年總有個align欄位進行記憶體對齊
/*位元組對齊的類型Align,為了優化CPU讀取*/ typedef union { long l_dummy; double d_dummy; void *p_dummy; } Align; /*標誌大小,預設是4位元組*/ #define MARK_SIZE (4) /*記憶體塊頭結點,雙向鏈表結點size,filename,line都是為了調試添加的調試信息.prev和next是雙向鏈表的核心*/ typedef struct { int size; char *filename; int line; Header *prev; Header *next; unsigned char mark[MARK_SIZE]; } HeaderStruct; /*Align類型的位元組大小*/ #define ALIGN_SIZE (sizeof(Align)) /*這是個不錯的技巧,求最小的n使得n*ALIGN_SIZE>=val成立,n,val,ALIGN_SIZE都屬於自然數*/ #define revalue_up_align(val) ((val) ? (((val) - 1) / ALIGN_SIZE + 1) : 0) /*將HeaderStruct按照Align劃分,找到最小的n,使得n*ALIGN_SIZE>=sizeof(HeaderStruct),在自然數集中*/ #define HEADER_ALIGN_SIZE (revalue_up_align(sizeof(HeaderStruct))) /*實現了memory.h介面中Header不完全類型,Align是對齊用的,記憶體結構的頭結點.鏈錶鏈接的主要結點*/ union Header_tag { HeaderStruct s; Align u[HEADER_ALIGN_SIZE]; };
主要看 union Headr_tag 中 Align結構. 保證不同機器上記憶體是對齊的. 比較古老了. 特別底層的庫會見到.
7. 可變參數巨集, 那些事
同樣直接看下麵工程中用的示例
//4.0 控制台列印錯誤信息, fmt必須是雙引號括起來的巨集 #ifndef CERR #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) #endif/* !CERR */ //4.1 控制台列印錯誤信息並退出, t同樣fmt必須是 ""括起來的字元串常量 #ifndef CERR_EXIT #define CERR_EXIT(fmt,...) \ CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE) #endif/* !ERR */ #ifndef IF_CERR /* *4.2 if 的 代碼檢測 * * 舉例: * IF_CERR(fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), "socket create error!"); * 遇到問題列印日誌直接退出,可以認為是一種簡單模板 * code : 要檢測的代碼 * fmt : 必須是""括起來的字元串巨集 * ... : 後面的參數,參照printf */ #define IF_CERR(code, fmt, ...) \ if((code) < 0) \ CERR_EXIT(fmt, ##__VA_ARGS__) #endif //!IF_CERR #ifndef IF_CHECK /* * 是上面IF_CERR 的簡化版很好用 */ #define IF_CHECK(code) \ if((code) < 0) \ CERR_EXIT(#code) #endif // !IF_CHECK
那 傳說中的 3顆痣, 就是可變參數巨集的一切o(∩_∩)o
8. 簡單的謝幕. 還是巨集
一個數如何和0比較,真的是 == 嗎. 其實好的思路是定義閥值.
//3.0 浮點數據判斷巨集幫助, __開頭表示不希望你使用的巨集 #define __DIFF(x, y) ((x)-(y)) //兩個表達式做差巨集 #define __IF_X(x, z) ((x)<z&&(x)>-z) //判斷巨集,z必須是巨集常量 #define EQ(x, y, c) EQ_ZERO(__DIFF(x,y), c) //判斷x和y是否在誤差範圍內相等 //3.1 float判斷定義的巨集 #define _FLOAT_ZERO (0.000001f) //float 0的誤差判斷值 #define EQ_FLOAT_ZERO(x) __IF_X(x,_FLOAT_ZERO) //float 判斷x是否為零是返回true #define EQ_FLOAT(x, y) EQ(x, y, _FLOAT_ZERO) //判斷表達式x與y是否相等
謝幕吧 : [
老師佈置一個作業, 問學生, 看見那個晾衣桿嗎. 誰能幫我測試出高度來.
一個同學自告奮勇的把晾衣桿放倒了. 測試出長度 為 1.5m.
老師把他罵了一頓, 我要的是高度, 不是長度.
]
//5.0 獲取數組長度,只能是數組類型或""字元串常量,後者包含'\0' #ifndef LEN #define LEN(arr) \ (sizeof(arr)/sizeof(*(arr))) #endif/* !ARRLEN */
後記 - 認真做容易的
錯誤是難免的, 歡迎吐槽交流. ... 還有下半輩子的苟且. 哈哈, 但是在變化, 那會變得有意思, O(∩_∩)O哈哈~