深入淺出 C++:與程式終止相關的函式 PART 1
C/C++ 程式,一般是藉由 main() 的返回值呼叫 exit() 函式以正常結束程式。除了程式崩潰、或使用者強制結束程式外,C++ 亦提供數個函式,允許呼叫以立即終止程式,本文將一一介紹這些函式。
不過,在進入主題前,需提醒讀者:撰寫程式時,儘可能使程式執行到 main() 結束為宜。C++ 在程式正常結束時,會負責清理所有產生的變數 (variable)、物件 (object),若以本文所介紹的函式強制終止程式,某些物件可能未完全摧毀而導致該執行的程式碼沒有跑到。舉例,程式中可能定義了某一物件處理臨時檔案,正常流程中,當物件的 destructor 執行時,臨時檔案會被刪除,但若程式中以本文所列的函式終止程式,destructor 很可能未執行而留下臨時檔案而佔用空間。
exit()
[[noreturn]] void exit(int exit_code);
函式宣告一開始的 [[noreturn]] 是 C++11 引進的 attribute,當函式的宣告/定義加上這個 attribute,代表不可能 return。若有一函式 A 呼叫了 exit(),則 exit() 並不會在結束時將控制權返還給 A。
請注意,“不可能 return” 與 “返回值型別為 void” 是兩個截然不同的概念,”返回值型別為 void” 是能在函式結束時將控制權返還給呼叫端。
[[noreturn]] 能讓 compiler 進行優惠,例如既然知道這個函式不會返回,記錄返回地址的步奏就能免了;若發現呼叫 [[noreturn]] 函式的之後還有程式碼,也能直接消去這些不可能執行的部分以節省 compile 後的空間。
回到 exit()。它需要傳入一個整數代表結束的狀態,若傳入 EXIT_SUCCESS (對應的整數為 0) 代表正常結束。
程式呼叫 exit() 函式後,仍是有部分程式碼會執行,為理解這個行為,下面範例中,MyClass 類的 constructor 與 destructor 執行時都輸出文字,方便理解物件究竟是何時建立、何時呼叫 destructor 適當摧毀:
#include <iostream>
#include <string>
#include <cstdlib>
#include <thread>
#include <vector>
void Print(const std::string& s, int m)
{
std::cout << s << ", m = " << m
<< ", thread id=" << std::this_thread::get_id() << std::endl;
}
class MyClass
{
public:
MyClass(int a) : m_(a) { Print("Constructor", m_); }
~MyClass() { Print("Destructor", m_); }
void Show() { Print("Print", m_); }
private:
int m_;
};
MyClass c1(1);
static MyClass c2(2);
thread_local MyClass c3(3);
thread_local MyClass c4(4);
int main()
{
std::cout << "Begin of main()" << std::endl;
static MyClass c5(5);
static MyClass c6(6);
MyClass c7(7);
MyClass c8(8);
MyClass* c9 = new MyClass(9);
MyClass* c10 = new MyClass(10);
MyClass* c11 = new MyClass(11);
std::vector<std::thread> threads;
for (int i = 0 ; i < 2 ; ++i) {
threads.push_back(
std::thread([=]() {
c3.Show();
c4.Show();
})
);
}
for (auto& thread : threads)
thread.join();
c3.Show();
c4.Show();
std::cout << "Call exit" << std::endl;
std::exit(EXIT_SUCCESS);
static MyClass c12(12);
std::cout << "End of main()" << std::endl;
}
[email protected]:~/cpp/c2$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -pthread -o exit exit.cpp [email protected]:~/cpp/c2$ ./exit Constructor, m = 1, thread id=140444569024192 Constructor, m = 2, thread id=140444569024192 Begin of main() Constructor, m = 5, thread id=140444569024192 Constructor, m = 6, thread id=140444569024192 Constructor, m = 7, thread id=140444569024192 Constructor, m = 8, thread id=140444569024192 Constructor, m = 9, thread id=140444569024192 Constructor, m = 10, thread id=140444569024192 Constructor, m = 11, thread id=140444569024192 Constructor, m = 3, thread id=140444540937984 Constructor, m = 4, thread id=140444540937984 Print, m = 3, thread id=140444540937984 Print, m = 4, thread id=140444540937984 Destructor, m = 4, thread id=140444540937984 Destructor, m = 3, thread id=140444540937984 Constructor, m = 3, thread id=140444549330688 Constructor, m = 4, thread id=140444549330688 Print, m = 3, thread id=140444549330688 Print, m = 4, thread id=140444549330688 Destructor, m = 4, thread id=140444549330688 Destructor, m = 3, thread id=140444549330688 Constructor, m = 3, thread id=140444569024192 Constructor, m = 4, thread id=140444569024192 Print, m = 3, thread id=140444569024192 Print, m = 4, thread id=140444569024192 Call exit Destructor, m = 4, thread id=140444569024192 Destructor, m = 3, thread id=140444569024192 Destructor, m = 6, thread id=140444569024192 Destructor, m = 5, thread id=140444569024192 Destructor, m = 2, thread id=140444569024192 Destructor, m = 1, thread id=140444569024192
觀察輸出可印證下面行為:
. C++ 保證,全域性變數/物件會在 main() 進入前進行初始化,在 main() 結束後,以相反順序執行他們的 destructor。若程式呼叫了 exit(),destructor 仍會執行。
. 函式內定義的 static 變數/物件,在第一次執行到定義的程式碼時產生。本例中,c12 從未產生。此外,在程式結束時,也是以相反順序執行他們的 destructor。若程式呼叫了 exit(),destructor 仍會執行。
. 一般的區域變數/物件,只有函式正常結束,才會執行 destructor。呼叫 exit() 時,並不會執行他們的 destructor。故本例中,c7、c8 皆未執行 destructor。
. 以 new 分配的物件,只有在程式中呼叫 delete,才會執行到他們的 destructor。若 exit() 先呼叫了,則 destructor 不會執行到。本例 c9、c10、c11 並未執行 destructor。
. 由 thread_local 關鍵字定義的變數/物件,稱為 thread-local storage (TLS) 變數/物件,會在各 thread 第一次使用它時初始化,且每個 thread 雖然使用相同的名稱存取,事實上是不同的實體,當 thread 結束時,會以相反順序摧毀產生的 TLS 變數/物件。此外,C++ 還保證當 main() 結束時,TLS 變數/物件的摧毀,會早於 static 變數/物件。當 exit() 呼叫後,會先執行當前 thread 產生的 TLS 物件的 destructor,再執行函式內 static 物件的 destructor,最後則是全域性物件的 destructor。值得注意的是,若有 TLS 物件在其他 thread 產生,其 destructor 是不會被呼叫的。
atexit()
若有其他程式碼,需要在 exit() 呼叫時一併執行,可使用 atexit() 函式註冊:
extern "C" int atexit(void (*func)()) noexcept;
extern "C++" int atexit(void (*func)()) noexcept;
函式宣告結尾的 noexcept specifier 代表這個函式的實現不會丟出 exception。atexit() 可傳入一個函式指標,註冊該函式要在程式結束前自動呼叫。atexit() 可以呼叫多次以註冊多個函式,同一個函式若註冊多次,則會執行多次。C++ 標準規定實現平臺至少要支援註冊 32 個。當 exit() 被呼叫時,C++ 會呼叫 atexit() 註冊的函式來執行。atexit() 返回 0 代表註冊成功。下面是使用範例:
#include <iostream>
#include <cstdlib>
void ExitFunction1() { std::cout << "ExitFunction1()" << std::endl; }
void ExitFunction2() { std::cout << "ExitFunction2()" << std::endl; }
int main()
{
std::cout << "Begin of main()" << std::endl;
if (int result = std::atexit(ExitFunction1); !result)
std::cout << "Register ExitFunction1()" << std::endl;
if (int result = std::atexit(ExitFunction2); !result)
std::cout << "Register ExitFunction2()" << std::endl;
if (int result = std::atexit(ExitFunction2); !result)
std::cout << "Register ExitFunction2()" << std::endl;
std::cout << "End of main()" << std::endl;
}
[email protected]:~/cpp/c2$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -o atexit atexit.cpp [email protected]:~/cpp/c2$ ./atexit Begin of main() Register ExitFunction1() Register ExitFunction2() Register ExitFunction2() End of main() ExitFunction2() ExitFunction2() ExitFunction1()
由輸出可知,atext() 所註冊的函式,執行順序與註冊順序相反。
exit() 的缺陷
exit() 的規則看似清楚,實則在 multithreading 的環境中潛藏各種問題。以下是個惡搞的程式,用以突出其薄弱之處:
#include <iostream>
#include <cstdlib>
#include <thread>
#include <condition_variable>
#include <string>
void Print(const std::string&s, int m)
{
std::cout << s << ", m_ = " << m
<< ", thread_id = " << std::this_thread::get_id() << std::endl;
}
class MyClass
{
public:
MyClass(int a) : m_(a) { Print("Constructor", m_); }
~MyClass() { Print("Destructor", m_); }
void Show() { Print("Show", m_); }
private:
int m_;
};
MyClass c1(1);
thread_local MyClass c2(2);
std::mutex gMutex;
std::condition_variable gConditionVariable;
void ThreadFunction()
{
static MyClass c3(3);
c2.Show();
gConditionVariable.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "End of Thread Function" << std::endl;
}
int main()
{
std::cout << "Begin of main()" << std::endl;
static MyClass c4(4);
std::thread t(ThreadFunction);
std::unique_lock<std::mutex> lock(gMutex);
gConditionVariable.wait(lock);
std::cout << "Call exit()" << std::endl;
std::exit(1);
t.join();
std::cout << "End of main()" << std::endl;
}
雖然 ThreadFunction() 需等待 10 秒才結束,但由於 main() 已經呼叫了 exit(),會使程式立即結束:
[email protected]alBox:~/cpp/c2$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -pthread -o exit_mt exit_mt.cpp [email protected]:~/cpp/c2$ ./exit_mt Constructor, m_ = 1, thread_id = 139939300996800 Begin of main() Constructor, m_ = 4, thread_id = 139939300996800 Constructor, m_ = 3, thread_id = 139939281303296 Constructor, m_ = 2, thread_id = 139939281303296 Show, m_ = 2, thread_id = 139939281303296 Call exit() Destructor, m_ = 3, thread_id = 139939300996800 Destructor, m_ = 4, thread_id = 139939300996800 Destructor, m_ = 1, thread_id = 139939300996800
本例中,有問題的地方,在於:
. c3 的 destructor,是在呼叫 exit() 的同一個 thread 裡呼叫的,而非原先產生 c3 的 thread。此處產生了 race condition 的隱患。
. 由於 c3 的 destructor 立即被呼叫,若本例中,sleep 10 秒替換為 c3 在處理工作,則工作無法做完、且強制終止,容易發生問題。
總之,exit() 只能保證在 single threaded 環境中,destructor 執行的順序與正確性;在 multithreaded 環境下,由於 thread 之間可能有執行時序的要求,exit() 的行為常無法滿足要求而造成與程式設計師認知相異的結果。是故,撰寫 multithreaded 程式時,最後還是以控制各 thread 邏輯、確保其他 thread 都結束後才正常結束 main() 為佳,別依賴 exit()。
相關推薦
深入淺出 C++:與程式終止相關的函式 PART 3
Markdown 編輯器真是不好用,這個文章裡,好幾個程式輸出的地方,# 開頭的都被識別成標題了。如果在 # 前面加上 \,看起來似乎能解決,但好幾行一改,又變成能在文章內看到 \ # 開頭了。哎,試了半個小時,懶得再試了,客官們擔待些,反正對理解正文沒影響便是
深入淺出 C++:與程式終止相關的函式 PART 1
C/C++ 程式,一般是藉由 main() 的返回值呼叫 exit() 函式以正常結束程式。除了程式崩潰、或使用者強制結束程式外,C++ 亦提供數個函式,允許呼叫以立即終止程式,本文將一一介紹這些函式。 不過,在進入主題前,需提醒讀者:撰寫程式時,儘可能使程式
深入淺出 C++:與程式終止相關的函式 PART 2
quick_exit() 與 at_quick_exit() (C++11新增) [[noreturn]] void quick_exit(int status) noexcept; quick_exit() 為 C++11 引入的函式,如果程式有特殊理
C#:四捨五入程式
問題 C#中的Math.Round()不是"四捨五入"法; 其實在VB、VBScript、C#、J#、T-SQL中Round函式採用的都是 Banker's rounding(銀行家演算法),即:四捨六入五取偶。 這是IEEE的規範; .NET 2.0 開始,Math.
C#:修改程式集資訊後DragDrop註冊失敗
因為某些原因,要修改原來的程式集名稱、namespace名稱,修改完後再執行時,在 Application.Run(new fMain()); 執行之後出現了“DragDrop註冊失敗”的錯誤。 百度一下,發現造成這個錯誤的原因各種各樣,只能自己試試了。 關閉程式集,將Re
C語言與組合語言之間的函式呼叫
教材:嵌入式系統及應用,羅蕾、李允、陳麗蓉等,電子工業出版社 ARM 程式設計 C與彙編之間的函式呼叫 ATPCS簡介 ARM-Thumb 過程呼叫標準 ATPC
Linux C的檔案操作及相關函式
一、Linux檔案的屬性及檔案的分類 二、檔案描述符的概念及作用 三、系統呼叫的概念 三、不帶快取的檔案I/O操作的相關函式 四、帶快取的檔案I/O操作的相關函式 一、Linux檔案的屬性 檔案的屬性: 我們在Gcc編譯器輸入“ ls -al"指令時,除了有不同
Linux之父炮轟C++:糟糕程式設計師的垃圾語言
眾所期待的程式設計聖經 【寫在前面】此文貼出後,引起了大家的較多關注,是意料之中的事情。畢竟,C、C++、Linux之父,都是大家最最熟悉的東西。但是許多同學把精力放在純粹語言優劣的爭論上,就沒有太大意思了。這場爭論的主角之一,微軟的Dmitry Kakurin有一句話
C語言線性單鏈表相關函式和演算法的基本實現
備考期間嘗試寫了一些基本資料結構的C語言實現,現做以下記錄(基本資料元以int型為例):全域性定義及依賴:#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #d
深入淺出 C++:#include Directive PART 1
除了基本語法外,使用 C++ 提供的標準庫、型別定義等,都需要使用 #include 引入 header file,寫法如下: #include <iostream> #include <vector> #include <s
深入淺出 C++:main()
main() 是 C/C++ 程式執行的進入點,作業系統執行程式時,首先會執行 Runtime Library 內的函式進行必要的初始化,接著才呼叫 main() 轉移控制權,當 main() 返回時,再根據 main() 的返回值呼叫 exit() 結束程式。
【theano-windows】學習筆記十一——theano中與神經網路相關函式
前言 經過softmax和MLP的學習, 我們發現thenao.tensor中除了之前的部落格【theano-windows】學習筆記五——theano中張量部分函式提到的張量的定義和基本運算外, 還有一個方法稱為nnet, 如果自己實現過前面兩篇部落格中的程
Python中列表、元組、字典、集合與字串,相關函式,持續更新中……
> 本篇部落格為博主第一次學 Python 所做的筆記(希望讀者能夠少點浮躁,認真閱讀,平心靜氣學習!) **補充:** - 列表、元組和字串共同屬性: - 屬於有序序列,其中的元素有嚴格的先後順序 - 都支援雙向索引,索引範圍 [ -L, L-1 ] ,L -- 表示列表、元組和字串的長度(分正向索引
持續集成與持續部署寶典Part 1:將構建環境容器化
成熟 curl命令 設置 doc 包括 探討 完成 2.7 mage 介 紹隨著Docker項目及其相關生態系統逐漸成熟,容器已經開始被更多企業用在了更大規模的項目中。因此,我們需要一套連貫的工作流程和流水線來簡化大規模項目的部署。在本指南中,我們將從代碼開發、持續集成
【讀書1】【2017】MATLAB與深度學習——代價函式比較(1)
該程式的撰寫方式幾乎與第2章“SGD與批處理比較”中的SGDvsBatch.m檔案的撰寫方式相同。 The architecture of this file is almostidentical to that of the SGDvsBatch.m file
C++ and OO Num. Comp. Sci. Eng. - Part 1.
nim num 內容 general -o 編譯時間 增加 radi gpo 本文參考自 《C++ and Object-Oriented Numeric Computing for Scientists and Engineers》。 序言 書中主要討論的問題是面向對象的
關於記憶體的相關概念part-1
關於記憶體的相關概念part-1 需要香港資料灣,香港高防伺服器,站群伺服器,越南伺服器請加Q:723645709,售後有保障,頻寬快,IP足,各種BGP任你選! 如果說把一個計算機當成一個汽車的話,CPU相當於其發動機,主要控制其計算機執行快慢。那記憶體就好比是計算機要走的道路,記憶體小,道路狹窄自然再
【MOOC】Python網路爬蟲與資訊提取-北京理工大學-part 1
【第〇周】網路爬蟲之前奏 網路爬蟲”課程內容導學 【第一週】網路爬蟲之規則 1.Requests庫入門 注意:中文文件的內容要稍微比英文文件的更新得慢一些,參考時需要關注兩種文件對應的Requests庫版本。(對於比較簡單的使
the c programming language second edition 第四章函式與程式結構筆記及練習題中
the c programming language second edition 第四章函式與程式結構筆記 4.3外部變數 C語言程式可以看成由一系列的外部物件構成,這些外部物件可能是變數或函式 外部變數和函式具有以下性質:通過同一個名字對外部變數的所有引
the c programming language second edition 第四章函式與程式結構筆記及練習題上
the c programming language second edition 第四章函式與程式結構筆記 4.1函式的基本認識 編寫一個程式它將輸入中包含特定模式或字串的各行打印出來。 該任務可以明確地劃分成下列3部分: while(未處理的行) if