C語言中文網 目錄

errno全局變量及使用細則,C語言errno全局變量完全攻略

在 C 語言中,對于存放錯誤碼的全局變量 errno,相信大家都不陌生。為防止和正常的返回值混淆,系統調用一般并不直接返回錯誤碼,而是將錯誤碼(是一個整數值,不同的值代表不同的含義)存入一個名為 errno 的全局變量中,errno 不同數值所代表的錯誤消息定義在 <errno.h> 文件中。如果一個系統調用或庫函數調用失敗,可以通過讀出 errno 的值來確定問題所在,推測程序出錯的原因,這也是調試程序的一個重要方法。

配合 perror 和 strerror 函數,還可以很方便地查看出錯的詳細信息。其中:
  • perror 在 <stdio.h> 中定義,用于打印錯誤碼及其消息描述;
  • strerror 在 <string.h> 中定義,用于獲取錯誤碼對應的消息描述;

需要特別強調的是,并不是所有的庫函數都適合使用 errno 全局變量。就 errno 而言,庫函數一般分為如下 4 種類型。

1) 設置errno并返回一個帶內“In-Band”錯誤指示符的庫函數

表1 設置 errno 并返回一個帶內“In-Band”錯誤指示符的庫函數
函 數 返回值 errno 值
fgetwc、fputwc WEOF EILSEQ
strtol、wcstol LONG_MIN 或 LONG_MAX ERANGE
stitoll、wcstoll LLONG_MIN 或 LLONG_MAX ERANGE
stitoul、wcstoul ULONG_MAX ERANGE
stitoull、wcstoull ULLONG_MAX ERANGE
stitoumax、wcstoumax UINTMAX_MAX ERANGE
strtod、wcstod 0 或 ±HUGE_VAL ERANGE
strtof、wcstof 0 或 ±HUGE_VALF ERANGE
strtold、wcstold 0 或 ±HUGE_VALL ERANGE
stitoimax、wcstoimax INTMAX_MIN、INTMAX_MAX ERANGE

如表 1 所示,這些函數將設置 errno,并返回一個帶內“In-Band”錯誤指示符。例如,函數 strtoul 發生錯誤時將返回 ULONG_MAX,并將 errno 的值設置為 ERANGE。這里就需要注意了,由于 ULONG_MAX 也是一個合法的返回值,因此必須使用 errno 來檢查是否發生錯誤。與此同時,對于這類函數,必須在調用這些庫函數之前將 errno 的值設置為 0,然后在調用庫函數之后檢查 errno 的值。

2) 設置 errno 并返回一個帶外“Out-of-Band”錯誤指示符的庫函數

表 2 設置 errno 并返回一個帶外“Out-of-Band”錯誤指示符的庫函數
函 數 返回值 errno 值
ftell() -1L positive
fgetpos()、fsetpos() nonzero positive
mbitowc()、mbsrtowcs() (size_t)(-1) EILSEQ
signal() SIG_ERR positive
wcrtomb()、wcsitombs() (size_t)(-1) EILSEQ
mbrtocl6()、mbitoc32() (size_t)(-1) EILSEQ
c16rtomb()、cr32rtomb() (size_t)(-1) EILSEQ

如表 2 所示,對于這類函數,應該先檢查它的返回值,之后如果確實需要再繼續檢查 errno 的值。

3) 不保證設置errno的庫函數

例如,setlocale 函數在發送錯誤時將返回 NULL,但卻不能保證一定會設置 errno 的值。因此,在調用這類函數時,不應完全依賴于 errno 的值來確定是否發生了錯誤。與此同時,該函數可能會設置 errno 的值,就算是這樣也不能夠保證 errno 會正確地提示錯誤的信息。

4) 具有不同標準文檔的庫函數

有些函數在不同的標準中對 errno 有不同的定義。例如,fopen 函數就是一個典型的例子。在 C99 中,并沒有在描述 fopen 時提到 errno,但是,POSIX.1 卻聲明了當 fopen 遇到一個錯誤時將返回 NULL,并且為 errno 設置一個值以提示這個錯誤。

調用errno之前必須先將其清零

在 C 語言中,如果系統調用或庫函數被正確地執行,那么 errno 的值不會被清零。換句話說,errno 的值只有在一個庫函數調用發生錯誤時才會被設置,當庫函數調用成功運行時,errno 的值不會被修改,當然也不會主動被置為 0。也正因為如此,在實際編程中進行錯誤診斷會有不少問題。

例如,在一段示例代碼中,首先執行函數 A 的調用,如果函數 A 在執行中發生了錯誤,那么 errno 的值將被修改。接下來,在不對 errno 的值做任何處理的情況下,繼續直接執行函數 B 的調用,如果函數 B 被正確地執行,那么 errno 將還保留著函數 A 發生錯誤時被設置的值。也正是這個原因,我們不能通過測試 errno 的值來判斷是否存在錯誤。

由此可見,在調用 errno 之前,應該首先對函數的返回值進行判斷,通過對返回值的判斷來檢查函數的執行是否發生了錯誤。如果通過檢查返回值確認函數調用發生了錯誤,那么再繼續利用 errno 的值來確認究竟是什么原因導致了錯誤。

但是,如果一個函數調用無法從其返回值上判斷是否發生了錯誤時,那么將只能通過 errno 的值來判斷是否出錯以及出錯的原因。對于這種情況,必須在調用函數之前先將 errno 的值手動清零,否則,errno 的值將有可能夠發生上面示例所展示的情況。

例如,當調用 fopen 函數發生錯誤時,它將會去修改 errno 的值,這樣外部的代碼就可以通過判斷 errno 的值來區分 fopen 內部執行時是否發生錯誤,并且根據 errno 值的不同來確定具體的錯誤類型。如下面的示例代碼所示:
int main(void)
{
    /*調用errno之前必須先將其清零*/
    errno=0;
    FILE *fp = fopen("test.txt","r");
    if(errno!=0)
    {
        printf("errno值: %d\n",errno);
        printf("錯誤信息: %s\n",strerror(errno));
    }
}
在這里,假設“test.txt”是一個根本不存在的文件。因此,在調用 fopen 函數嘗試打開一個并不存在的文件時將發生錯誤,同時修改 errno 的值。這時,fopen 函數會將 errno 指向的值修改為 2。我們通過 stderror 函數可以看到錯誤代碼“2”的意思是“No such file or directory”,如圖 3 所示。


圖 3 示例代碼的運行結果

從上面的示例可以看出,使用 errno 來報告錯誤看起來似乎非常簡單完美,但其實情況并非如此。前面也闡述過,在 C99 中,并沒有在描述 fopen 時提到 errno。但是,POSIX.1 卻聲明了當 fopen 遇到一個錯誤時,它將返回 NULL,并且為 errno 設置一個值以提示這個錯誤,這就暗示一個遵循了 C99 但不遵循 POSIX 的程序不應該在調用 fopen 之后再繼續檢查 errno 的值。因此,下面的寫法完全合乎要求:
int main(void)
{
    FILE *fp = fopen("test.txt","r");
    if(fp==NULL)
    {
        /*...*/
    }
}
但是,上面也說過,在 POSIX 標準中,當 fopen 遇到一個錯誤的時候將返回 NULL,并且為 errno 設置一個值以提示這個錯誤。因此,在遵循 POSIX 標準中,應該首先檢查 fopen 是否返回 NULL 值,如果返回,再繼續檢查 errno 的值以確認產生錯誤的具體信息,如下面的代碼所示:
int main(void)
{
    /*調用errno之前必須先將其清零*/
    errno=0;
    FILE *fp = fopen("test.txt","r");
    if(fp==NULL)
    {
        if(errno!=0)
        {
            printf("errno值: %d\n",errno);
            printf("錯誤信息:%s\n",strerror(errno));
        }
    }
}
其實,即使系統調用或者庫函數正確執行,也不能夠保證 errno 的值不會被改變。因此,在沒有發生錯誤的情況下,fopen 也有可能修改的 errno 值。先檢查 fopen 的返回值,再檢查 errno 的值才是正確的做法。

除此之外,建議在使用 errno 的值之前,必須先將其值賦給另外一個變量保存起來,因為很多函數(如 fprintf)自身就可能會改變 errno 的值。

避免重定義errno

對于 errno,它是一個由 ISO C 與 POSIX 標準定義的符號。早些時候,POSIX 標準曾經將 errno 定義成“extern int errno”這種形式,但現在這種定義方式比較少見了,那是因為這種形式定義的 errno 對多線程來說是致命的。

在多線程環境下,errno 變量是被多個線程共享的,這樣就可能引發如下情況:線程 A 發生某些錯誤而改變了 errno 的值,那么線程 B 雖然沒有發生任何錯誤,但是當它檢測 errno 的值時,線程 B 同樣會以為自己發生了錯誤。

我們知道,在多線程環境中,多個線程共享進程地址空間,因此就要求每個線程都必須有屬于自己的局部 errno,以避免一個線程干擾另一個線程。其實,現在的大多部分編譯器都是通過將 errno 設置為線程局部變量的實現形式來保證線程之間的錯誤原因不會互相串改。

例如,在 Linux 下的 GCC 編譯器中,標準的 errno 在“/usr/include/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.  */
#ifndef   errno
extern int errno;
#endif

其中,errno在“/usr/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__ */
# endif /* _ERRNO_H */

這樣,通過“extern int*__errno_location(void)__THROW__attribute__((__const__));”與“#define errno(*__errno_location())”定義,使每個線程都有自己的 errno,不管哪個線程修改 errno 都是修改自己的局部變量,從而達到線程安全的目的。

除此之外,如果要在多線程環境下正確使用 errno,首先需要確保 __ASSEMBLER__ 沒有被定義,同時 _LIBC 沒被定義或定義了 _LIBC_REENTRANT??梢酝ㄟ^下面的程序來在自己的開發環境中測試這幾個宏的設置:
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 時,只需要在程序中簡單地包含它的頭文件“errno.h”即可,千萬不要多此一舉,在程序中重新定義它。如果在程序中定義了一個名為 errno 的標識符,其行為是未定義的。

避免使用errno檢查文件流錯誤

上面已經闡述過,在 POSIX 標準中,可以通過 errno 值來檢查 fopen 函數調用是否發生了錯誤。但是,對特定文件流操作是否出錯的檢查則必須使用 ferror 函數,而不能夠使用 errno 進行文件流錯誤檢查。如下面的示例代碼所示:
int main(void)
{
    FILE* fp=NULL;
    /*調用errno之前必須先將其清零*/
    errno=0;
    fp = fopen("Test.txt","w");
    if(fp == NULL)
    {
        if(errno!=0)
        {
            /*處理錯誤*/
        }
    }
    else
    {
        /*錯誤地從fp所指定的文件中讀取一個字符*/
        fgetc(fp);
        /*判斷是否讀取出錯*/
        if(ferror(fp))
        {
            /*處理錯誤*/
            clearerr(fp);
        }
        fclose(fp);
        return 0;
    }
}

精美而實用的網站,提供C語言、C++、STL、Linux、Shell、Java、Go語言等教程,以及socket、GCC、vi、Swing、設計模式、JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo
极速pk10开户