C++如何在main函式開始之前(或結束之後)執行一段邏輯?

陌尘(MoChen)發表於2024-07-22
  • 1. 問題
  • 2. 考察的要點
  • 3. 解決策略
    • 3.1. 方案一:使用GCC的擴充功能
    • 3.2. 方案二:使用全域性變數
    • 3.3. 方案三:atexit
  • 4. Demo測試
    • 4.1. 測試程式碼
    • 4.2. 執行結果
  • 5. 程式異常退出場景
    • 5.1. 存在的問題
    • 5.2. 解決方案
      • 5.2.1. 原理
      • 5.2.2. 示例程式碼
      • 5.2.3. 執行結果
      • 5.2.4. 特殊說明
  • 6. 參考文件

1. 問題

我們知道C/C++程式的執行邏輯是從main函式開始,到main函式結束。但是,有時我們需要在main函式開始之前或結束之後執行一段邏輯,比如:

  1. 如何在main函式開始之前執行一段邏輯?
  2. 如何在main函式結束之後執行一段邏輯?

有辦法實現嗎?在往下閱讀之前,請先思考一下。

2. 考察的要點

C++程式的程式碼執行邏輯。
全域性變數|靜態變數的理解。

3. 解決策略

3.1. 方案一:使用GCC的擴充功能

GCC編譯器的擴充功能,透過 __attribute__ 關鍵字註冊“在main函式開始之前或結束之後”執行的回撥函式。

__attribute((constructor)) void before_main() {
    std::cout << "before main" << std::endl;
}

__attribute((destructor)) void after_main() {
    std::cout << "after main" << std::endl;
}

3.2. 方案二:使用全域性變數

全域性變數會在程序剛啟動的時候就初始化,在程序結束的時候被銷燬。所以:全域性物件的初始化會在main函式執行之前被執行;全域性物件的銷燬會在main函式執行之後被執行。

結合C++類的建構函式和虛構函式的特點,可以專門定義一個類來處理main函式開始之前和結束之後的邏輯(為了保證這個類只有一個全域性物件,建議將這個類設計成單例模式),然後在main之前宣告這個類的一個全域性變數。

class BeforeAndAfterMain
{
public:
    static BeforeAndAfterMain& GetInstance()
    {
        static BeforeAndAfterMain instance;
        return instance;
    }

    ~BeforeAndAfterMain()
    {
        std::cout << "Global object destory after main" << std::endl;
    }

private:
    BeforeAndAfterMain()
    {
        std::cout << "Global object construct before main" << std::endl; 
    }
    BeforeAndAfterMain(const BeforeAndAfterMain&) = delete;
    BeforeAndAfterMain& operator=(const BeforeAndAfterMain&) = delete;
};

auto& g_before_and_after_main = BeforeAndAfterMain::GetInstance();

3.3. 方案三:atexit

針對main函式結束之後的邏輯,可以使用atexit函式註冊一個回撥函式,在main函式執行之後被執行。

#include <cstdlib>

void at_main_exit(){
    std::cout << "at_main_exit" << std::endl;
}

4. Demo測試

4.1. 測試程式碼

完整測試程式碼如下:

#include <iostream>
#include <cstdlib>

__attribute((constructor)) void before_main() {
    std::cout << "before main" << std::endl;
}

__attribute((destructor)) void after_main() {
    std::cout << "after main" << std::endl;
}

class BeforeAndAfterMain
{
public:
    static BeforeAndAfterMain& GetInstance()
    {
        static BeforeAndAfterMain instance;
        return instance;
    }

    ~BeforeAndAfterMain()
    {
        std::cout << "Global object destory after main" << std::endl;
    }

private:
    BeforeAndAfterMain()
    {
        std::cout << "Global object construct before main" << std::endl; 
    }
    BeforeAndAfterMain(const BeforeAndAfterMain&) = delete;
    BeforeAndAfterMain& operator=(const BeforeAndAfterMain&) = delete;
};

auto& g_before_and_after_main = BeforeAndAfterMain::GetInstance();

void at_main_exit(){
    std::cout << "at_main_exit" << std::endl;
}

int main() {
    // https://en.cppreference.com/w/cpp/header/cstdlib
    atexit(at_main_exit);

    std::cout << "main begin" << std::endl;
    int a = 10;
    int b = 5;
    // crash to exit
    // int b = 0;
    int c = a / b;
    std::cout << "a /b = " << c << std::endl;
    std::cout << "main end" << std::endl;
    return 0;
}

4.2. 執行結果

before main
Global object construct before main
main begin
a /b = 2
main end
at_main_exit
Global object destory after main
after main

5. 程式異常退出場景

5.1. 存在的問題

上面的Demo,把

    int b = 5;

替換成

    // crash to exit
    int b = 0;

會導致程式異常(除數不能為0)退出,輸出如下:

before main
Global object construct before main
main begin
Floating point exception

三種main函式結束後的邏輯均未被執行。說明:程式異常退出時(如:crash),“main函式結束後的邏輯均”不被執行,不能cover住這種場景。

5.2. 解決方案

5.2.1. 原理

當程式崩潰時,作業系統會傳送一個訊號給程式,通知它發生了異常。在 C++中,可以透過 signal 函式來註冊一個訊號處理程式,使程式能夠在接收到該訊號時執行自定義的程式碼。

程式的執行流程:

  1. 執行程式,按正常邏輯執行。
  2. 程式崩潰,異常退出,根據不同的崩潰原因,作業系統能識別出不同的崩潰訊號(signal)。
  3. 作業系統傳送對應的崩潰訊號(signal)給執行程式。
  4. 執行程式根據提前已註冊好的訊號處理函式,執行對應的訊號處理邏輯。
  5. 訊號處理函式執行完畢,透過exit函式退出程式。

這樣保證了:雖然程式的主流程崩潰了,但是程式還是能正常結束。這樣即使程式崩潰了,還是能夠自己完成如:“資源釋放”、“狀態儲存或重置”等一些重要的邏輯。

5.2.2. 示例程式碼

void signal_handler(int sig) {
    // 這裡編寫你的異常訊號處理邏輯,比如列印日誌,儲存狀態,捕獲堆疊資訊等。
    std::cerr << "signal_handler" << std::endl;
    // 注意:訊號處理程式執行完成,一定要呼叫exit退出,否則訊號處理函式可能會被迴圈執行。
    exit(1);
}

int main() {
    // 註冊訊號處理函式
    // signal(SIGSEGV, signal_handler);
    signal(SIGFPE, signal_handler);
    

    // https://en.cppreference.com/w/cpp/header/cstdlib
    atexit(at_main_exit);

    std::cout << "main begin" << std::endl;
    int a = 10;
    // int b = 5;
    // crash to exit
    int b = 0;
    int c = a / b;
    std::cout << "a /b = " << c << std::endl;
    std::cout << "main end" << std::endl;
    return 0;
}

5.2.3. 執行結果

before main
Global object construct before main
main begin
signal_handler
at_main_exit
Global object destory after main
after main

5.2.4. 特殊說明

  1. 當程式崩潰時,可能已經無法正常執行程式碼,因此需要謹慎地編寫訊號處理程式,以避免進一步的崩潰或資料損壞。
  2. 訊號處理程式執行完成,一定要呼叫exit退出,否則訊號處理函式可能會被迴圈執行。
  3. 考慮各種可能出現的異常訊號,比如:SIGSEGV、SIGFPE、SIGILL、SIGABRT等。這些可能出現的異常,都需要註冊對應的訊號處理程式。以免出現異常漏捕獲的情況。

6. 參考文件

https://blog.csdn.net/zhizhengguan/article/details/122623008
https://blog.csdn.net/MldXTieJiang/article/details/129620160

相關文章