[TOC]
# 簡介
在 C 語言中,對于存放錯誤碼的全局變量 errno,相信大家都不陌生。為防止和正常的返回值混淆,系統調用一般并不直接返回錯誤碼,而是將錯誤碼(是一個整數值,不同的值代表不同的含義)存入一個名為 errno 的全局變量中,errno 不同數值所代表的錯誤消息定義在 文件中。如果一個系統調用或庫函數調用失敗,可以通過讀出 errno 的值來確定問題所在,推測程序出錯的原因,這也是調試程序的一個重要方法。
配合 perror 和 strerror 函數,還可以很方便地查看出錯的詳細信息。其中:
* perror 在 中定義,用于打印錯誤碼及其消息描述;
* strerror 在 中定義,用于獲取錯誤碼對應的消息描述;
需要特別強調的是,并不是所有的庫函數都適合使用 errno 全局變量。就 errno 而言,庫函數一般分為如下 4 種類型。
1. 設置errno并返回一個帶內“In-Band”錯誤指示符的庫函數

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

如表 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 所示。

從上面的示例可以看出,使用 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語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介