1. 程式人生 > >深入淺出 C++:與程式終止相關的函式 PART 1

深入淺出 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