是否應該使用goto語句
goto語句也被稱為無條件轉移語句,它通常與條件語句配合使用來改變程序流向,使得程序轉去執行語句標號所標識的語句。
關於是否應該使用goto語句,歷史上也爭論不休。恐怕國內大部分教授高級編程語言的課堂上,都會主張在結構化程序設計中不使用goto語句, 以免造成程序流程的混亂,使得理解和調試程序都產生困難。歷史上支持goto語句有害的人的主要理由是:goto語句會使程序的靜態結構和動態結構不一致,從而使程序難以理解且難以查錯。並且G·加科皮尼和C·波姆從理論上證明了:任何程序都可以用順序、分支和重復結構表示出來。這個結論表明,從高級程序語言中去掉goto語句並不影響高級程序語言的編程能力,而且編寫的程序的結構更加清晰。
然而偉大的哲學家黑格爾說過:存在即合理。當筆者剛從校園中走出的時候,對於goto語句有害論也深以為然,然后多年之后在自己編寫的代碼中隨處可見goto的身影。如今很多高級編程語言中,似乎是難以看見goto的身影:Java中不提供goto語句,雖然仍然保留goto為關鍵字,但不支持它的使用;C#中依然支持goto語句,但是一般不建議使用。其實可以很容易發現一點,這些不提倡使用goto語句的語言,大多是有自帶的垃圾回收機制,也就是說不需要過多關心資源的釋放的問題,因而在程序流程中沒有“為資源釋放設置統一出口”的需求。然而對於C++語言來說,程序員需要自己管理資源的分配和釋放。倘若沒有goto語句,那么我們在某個函數資源分配后的每個出錯點需要釋放資源並返回結果。雖然我們依然可以不使用goto語句完整地寫完流程,但是代碼將變得又臭又長。譬如我們需要寫一個全局函數g_CreateListenSocket用來創建監聽套接字,那么如果不使用goto語句,我們的代碼將會是這個樣子:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #define MAX_ACCEPT_BACK_LOG 5 void g_CloseSocket(int &nSockfd) { if ( -1 == nSockfd ) { return; } struct linger li = { 1, 0 }; ::setsockopt(nSockfd, SOL_SOCKET, SO_LINGER, (const char *)&li, sizeof(li)); ::close(nSockfd); nSockfd = -1; } in_addr_t g_InetAddr(const char *cszIp) { in_addr_t uAddress = INADDR_ANY; if ( 0 != cszIp && '\0' != cszIp[0] ) { if ( INADDR_NONE == (uAddress = ::inet_addr(cszIp)) ) { uAddress = INADDR_ANY; } } return uAddress; } int g_CreateListenSocket(const char *cszIp, unsigned uPort) { int nOptVal = 1; int nRetCode = 0; int nSocketfd = -1; sockaddr_in saBindAddr; // create a tcp socket nSocketfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if ( -1 == nSocketfd ) { return nSocketfd; } // set address can be reused nRetCode = ::setsockopt(nSocketfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&nOptVal, sizeof(nOptVal)); if ( 0 != nRetCode ) { g_CloseSocket(nSocketfd); return nSocketfd; } // bind address saBindAddr.sin_family = AF_INET; saBindAddr.sin_addr.s_addr = g_InetAddr(cszIp); saBindAddr.sin_port = ::htons(uPort); nRetCode = ::bind(nSocketfd, (struct sockaddr *)&saBindAddr, sizeof(saBindAddr)); if ( 0 != nRetCode ) { g_CloseSocket(nSocketfd); return nSocketfd; } // create a listen socket nRetCode = ::listen(nSocketfd, MAX_ACCEPT_BACK_LOG); if ( 0 != nRetCode ) { g_CloseSocket(nSocketfd); return nSocketfd; } return nSocketfd; }
上面藍色標記的代碼中就包含了出錯時候對資源(這里是套接字描述符)進行清理的操作,這里只有單一的資源,所以流程看起來也比較干凈。倘若流程中還夾雜着內存分配、打開文件的操作,那么對資源釋放操作將變得復雜,不僅代碼變得臃腫難看,還不利於對流程的理解。而如果使用了goto語句,那么我們統一為資源釋放設定單一出口,那么代碼將會是下面這個樣子:
int g_CreateListenSocket(const char *cszIp, unsigned uPort) { int nOptVal = 1; int nRetCode = 0; int nSocketfd = -1; sockaddr_in saBindAddr; // create a tcp socket nSocketfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if ( -1 == nSocketfd ) { goto Exit0; } // set address can be reused nRetCode = ::setsockopt(nSocketfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&nOptVal, sizeof(nOptVal)); if ( 0 != nRetCode ) { goto Exit0; } // bind address saBindAddr.sin_family = AF_INET; saBindAddr.sin_addr.s_addr = g_InetAddr(cszIp); saBindAddr.sin_port = ::htons(uPort); nRetCode = ::bind(nSocketfd, (struct sockaddr *)&saBindAddr, sizeof(saBindAddr)); if ( 0 != nRetCode ) { goto Exit0; } // create a listen socket nRetCode = ::listen(nSocketfd, MAX_ACCEPT_BACK_LOG); if ( 0 != nRetCode ) { goto Exit0; } // success here return nSocketfd; Exit0: // fail and clean up resources here if (-1 != nSocketfd) { g_CloseSocket(nSocketfd); } return nSocketfd; }
其實可以發現,加入goto語句之后,流程反而變得清晰了。一個函數將擁有兩個出口:執行成功返回和執行失敗返回。每次在流程某處出錯后都跳轉到固定標號處執行資源釋放操作,這樣在主體流程中將不再出現與資源釋放相關的代碼,那么主體流程只需專注於邏輯功能,代碼將變得更易於理解和維護。另外一個好處就是不容易忘記釋放資源,只需要養成分配完一個資源后立即在資源統一釋放處編寫資源釋放代碼的好習慣即可,對於程序員復查自己的代碼也帶來好處。
使用宏來簡化代碼量
仔細觀察上面的代碼,再結合前面所言的goto語句通常與條件語句配合使用來改變程序流向,可以總結規律:我們總是檢查某個條件是否成立,如果條件不成立立即goto到指定的函數執行失敗入口處,那么我們可以設計宏如下:
#undef DISABLE_WARNING #ifdef _MSC_VER // MS VC++ #define DISABLE_WARNING(code, expression) \ __pragma(warning(push)) \ __pragma(warning(disable:code)) expression \ __pragma(warning(pop)) #else // GCC #define DISABLE_WARNING(code, expression) \ expression #endif // _MSC_VER #undef WHILE_FALSE_NO_WARNING #define WHILE_FALSE_NO_WARNING DISABLE_WARNING(4127, while(false)) #undef PROCESS_ERROR_Q #define PROCESS_ERROR_Q(condition) \ do \ { \ if (!(condition)) \ { \ goto Exit0; \ } \ } WHILE_FALSE_NO_WARNING #undef PROCESS_ERROR #define PROCESS_ERROR(condition) \ do \ { \ if (!(condition)) \ { \ assert(false); \ goto Exit0; \ } \ } WHILE_FALSE_NO_WARNING
那么我們的g_CreateListenSocket函數將最終簡化為如下代碼:
int g_CreateListenSocket(const char *cszIp, unsigned uPort) { int nOptVal = 1; int nRetCode = 0; int nSocketfd = -1; sockaddr_in saBindAddr; // create a tcp socket nSocketfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_IP); PROCESS_ERROR(-1 != nSocketfd); // set address can be reused nRetCode = ::setsockopt(nSocketfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&nOptVal, sizeof(nOptVal)); PROCESS_ERROR(0 == nRetCode); // bind address saBindAddr.sin_family = AF_INET; saBindAddr.sin_addr.s_addr = g_InetAddr(cszIp); saBindAddr.sin_port = ::htons(uPort); nRetCode = ::bind(nSocketfd, (struct sockaddr *)&saBindAddr, sizeof(saBindAddr)); PROCESS_ERROR(0 == nRetCode); // create a listen socket nRetCode = ::listen(nSocketfd, MAX_ACCEPT_BACK_LOG); PROCESS_ERROR(0 == nRetCode); // success here return nSocketfd; Exit0: // fail and clean up resources here if (-1 != nSocketfd) { g_CloseSocket(nSocketfd); } return nSocketfd; }
統一函數出口
如果想統一函數出口,其實方法很簡單:只需要加入一個int nResult字段,初始化為false,在函數流程完全走完時標記為true,然后在釋放資源處判斷該字段是否為false即可。可以參考下面代碼:
int g_CreateListenSocket(const char *cszIp, unsigned uPort) { int nResult = false; int nRetCode = false; int nOptVal = 1; int nSocketfd = -1; sockaddr_in saBindAddr; // create a tcp socket nSocketfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_IP); PROCESS_ERROR(-1 != nSocketfd); // set address can be reused nRetCode = ::setsockopt(nSocketfd, SOL_SOCKET, SO_REUSEADDR, (const char *)&nOptVal, sizeof(nOptVal)); PROCESS_ERROR(0 == nRetCode); // bind address saBindAddr.sin_family = AF_INET; saBindAddr.sin_addr.s_addr = g_InetAddr(cszIp); saBindAddr.sin_port = ::htons(uPort); nRetCode = ::bind(nSocketfd, (struct sockaddr *)&saBindAddr, sizeof(saBindAddr)); PROCESS_ERROR(0 == nRetCode); // create a listen socket nRetCode = ::listen(nSocketfd, MAX_ACCEPT_BACK_LOG); PROCESS_ERROR(0 == nRetCode); // success here nResult = true; Exit0: // fail and clean up resources here if (!nResult) { if (-1 != nSocketfd) { g_CloseSocket(nSocketfd); } } return nSocketfd; }
測試代碼
最后附上上述代碼的測試代碼:
int main(int argc, char ** argv) { socklen_t nAddrLen = sizeof(struct sockaddr_in); int nListenSocketfd = -1; struct sockaddr_in saRemoteAddr; nListenSocketfd = g_CreateListenSocket("", 9999); if ( -1 == nListenSocketfd ) { return 0; } while (true) { ::memset(&saRemoteAddr, 0, sizeof(saRemoteAddr)); int nSocketfd = ::accept(nListenSocketfd, (struct sockaddr *)&saRemoteAddr, &nAddrLen); ::printf("Accept a new connection from [ip - %s, port - %d]\n", ::inet_ntoa(saRemoteAddr.sin_addr), ::ntohs(saRemoteAddr.sin_port) ); g_CloseSocket(nSocketfd); } return 1; }