簡介:
標頭檔errno.h定義了一個全域的宏errno,它被展開為一個int類型的“左值”,這意味著宏errno不一定是個對象的標識符,也可以展開為一個由函數返回的可以修改的“左值”,比如int *errno(),這個後面會講,你可以暫且把它理解為一個全域的int型變數(雖然這樣理解是錯的,不過方便理解)。
簡單來說,errno.h只是為了提供了一種錯誤報表機制。比如,一個函數調用fopen()發生了錯誤,它可能就會去修改errno的值,這樣外部的代碼可以通過判斷errno的值來區分fopen()內部執行時是否發生錯誤,並根據errno值的不同來確定具體的錯誤類型。
先來看一段代碼,Demo1:
#include<stdio.h>#include<errno.h>int main ( int argc, char *argv[] ){//try to open file "whatever.txt". when you run this demo,make sure the file is NOT existed..。FILE *fp = fopen("whatever.txt","r");if(fp ==NULL){printf("Can not open file\n");printf("errno value: %d, it means: %s",errno, strerror(errno));}return 0;}
程式會輸出:
Can not open fileerrno value: 2, it means: No such file or directory
strerror是標準庫stdio.h定義的一個函數,它用來返回錯誤碼所代表的含義。如Demo1所示,我們用fopen(也在stdio.h中定義)開啟一個並不存在的檔案,因此返回的fp是一個null 指標。而在fopen嘗試開啟檔案失敗時會修改errno的值,Demo1裡fopen失敗原因是檔案不存在,因此fopen將會把errno指向的值修改為2,通過stderror可以看到錯誤碼2的意思是“No such file or directory”。
帥氣,看起來用errno來報告錯誤,既方便,也很簡單,但實際應用時遠沒Demo1那麼簡單,試下Demo2:
#include<stdio.h>#include<errno.h>#include<math.h>int main ( int argc, char *argv[] ){/*try to open file "whatever.txt". when you run this demo,make sure the file is NOT existed...*/FILE *fp = fopen("whatever.txt","r");if(fp ==NULL){printf("Can not open file\n");int root = sqrt(123568 -123668 );printf("errno value: %d, it means: %s",errno, strerror(errno));}return 0;}
程式會輸出:
Can not open fileerrno value: 33, it means: Numerical argument out of domain
這跟我們期望的錯誤資訊完全不一樣,因為sqrt函數在得到一個非法的參數時,它把errno的值修改成了33,覆蓋了fopen設定的錯誤碼。所以,使用errno一個比較安全的編碼方式是,在下個庫函數調用之前就查看它的值,千萬不要因為某個庫函數看似很簡單就假設它不會修改errno的值,那樣會死的很慘……如果必須在查看errno之前調用別的庫函數,一種安全的方式是先把errno的值儲存到一個臨時變數裡,然後調用那個“必須”調用的庫函數,處理完畢後再把errno恢複到之前的值。如Demo3:
#include<stdio.h>#include<errno.h>#include<math.h>double getSqrt(double value){//1、save last errno to a temp variableint tmpErrno = errno;//2、set errno to 0errno = 0;double root = sqrt(123568 -123668 );printf("I changed errno to '%d' sliently...but it's safe \n",errno);//3.restore errnoerrno = tmpErrno;return root;}int main ( int argc, char *argv[] ){FILE *fp = fopen("whatever.txt","r");if(fp ==NULL){printf("Can not open file\n");getSqrt(-1);printf("errno value: %d, it means: %s",errno, strerror(errno));}return 0;}
程式會輸出:
Can not open fileI changed errno to '33' sliently...but it's safeerrno value: 2, it means: No such file or directory
現在,就像期望的那樣輸出了。
但是……
這樣真的安全嗎?!想像一下,如果errno是個全域變數,那多線程環境下豈不完蛋了?!本來線程A把errno設定成2,還沒執行到查看錯誤的語句時,線程B就把errno設定成了33,然後線程A才開始查看errno並輸出錯誤資訊,而這時輸出的錯誤就很讓人抓狂了!神呀,這破東西多線程沒法兒用哇!
但是……
你多慮了……文章開始說過,宏errno可以被展開為一個“左值”,比如int* getYourErrno(),所以你可以在getYourErrno()裡返回一個線程內的局部變數,這樣不管哪個線程修改errno都修改的它自己的局部變數,所以我們擔心的問題是不存在的。看下errno.h的源碼就明白了
/* Get the error number constants from the system-specific file. This file will test __need_Emath and _ERRNO_H. */#include <bits/errno.h>#undef__need_Emath#ifdef_ERRNO_H/* Declare the `errno' variable, unless it's defined as a macro by bits/errno.h. This is the case in GNU, where it is a per-thread variable. This redeclaration using the macro still works, but it will be a function declaration without a prototype and may trigger a -Wstrict-prototypes warning. */#ifndeferrnoextern int errno;#endif
上面的注釋說了,如果errno沒有定義過就把errno定義為“extern int errno;”,如果這樣多線程時是會發生悲劇的,先不著急哭,我們去前面看看它是否被定義過,前面的代碼include了一個叫bits/errno.h的標頭檔,看名字就很“險惡”,進去看看:
# ifndef __ASSEMBLER__/* Function to get address of global `errno' variable. */extern int *__errno_location (void) __THROW __attribute__ ((__const__));# if !defined _LIBC || defined _LIBC_REENTRANT/* When using threads, errno is a per-thread value. */# define errno (*__errno_location ())# endif# endif /* !__ASSEMBLER__ */
果然來者不善……如果沒定義宏__ASSEMBLER__,就會執行中間的代碼,裡面的代碼又說了,如果你沒定義_LIBC或者定義了_LIBC_REENTRANT他們就會把errno定義為__errno_location,一看到宏_LIBC_REENTRANT裡面有“reentrant(重入)”就知道它不是個好東西……不是,是看到它的名字就知道它是跟多線程有關的,所以如果要在多線程環境下正確的使用errno,你需要確保__ASSEMBLER__沒有被定義,而且_LIBC沒被定義或者定義了_LIBC_REENTRANT。
可以寫個程式看下自己開發環境裡這幾個宏的設定:
#include <stdio.h>#include <errno.h>int main( void ){#ifndef __ASSEMBLER__ printf( "__ASSEMBLER__ is NOT defined!\n" );#else printf( "__ASSEMBLER__ is defined!\n" );#endif#ifndef __LIBC printf( "__LIBC is NOT defined\n" );#else printf( "__LIBC is defined!\n" );#endif#ifndef _LIBC_REENTRANT printf( "_LIBC_REENTRANT is NOT defined\n" );#else printf( "_LIBC_REENTRANT is defined!\n" );#endif return 0;}
我的輸出:
_ASSEMBLER__ is NOT defined!__LIBC is NOT defined!_LIBC_REENTRANT is NOT defined!
哈!看來我可以在多線程下安全的使用errno,如果你的預設環境不可以就在makefile裡定義上_LIBC_REENTRANT吧!
ok,有關errno.h的介紹到此就結束,休息,休息一下!