精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

蝦皮C++后端一面:解析static析構順序問題 ?

開發 前端
在深入探討 static 析構順序之前,我們先來全面認識一下 static 關鍵字在 C++ 中的強大功能。它就像一個多面手,在不同的場景下有著不同的神奇效果。

在C++編程的廣闊世界里,static關鍵字猶如一位低調卻又至關重要的幕后英雄,廣泛應用于變量、函數以及類的成員之中 ,發揮著不可或缺的作用。從修飾局部變量以保留其狀態,到限制函數的作用域,再到實現類成員的共享,static的身影無處不在。

然而,在關注static帶來的各種便利時,我們往往容易忽視一個重要的方面 ——static對象的析構問題。析構函數作為對象生命周期結束時的清理機制,對于static對象而言,有著獨特的規則和容易被誤解的地方。這些細節問題,雖然平時可能不常被提及,但一旦在復雜的項目中出現,就可能引發難以排查的錯誤,導致程序運行異常。接下來,就讓我們深入探究static析構的奧秘,揭開它神秘的面紗。

一、static 關鍵字的多面剖析

在深入探討 static 析構順序之前,我們先來全面認識一下 static 關鍵字在 C++ 中的強大功能。它就像一個多面手,在不同的場景下有著不同的神奇效果。

1.1修飾變量

(1)修飾局部變量:當 static 修飾局部變量時,這個局部變量就擁有了 “超能力”—— 持久性。普通的局部變量在函數結束時就會被銷毀,如同曇花一現。但靜態局部變量卻不一樣,它在程序運行期間一直存在,就像一個忠誠的衛士,堅守崗位。它的生命周期從第一次被初始化開始,一直延續到程序結束 。比如,我們在一個函數中定義一個靜態局部變量來統計函數被調用的次數:

void countFunctionCalls() {
    static int callCount = 0;
    callCount++;
    std::cout << "函數被調用了 " << callCount << " 次" << std::endl;
}

每次調用countFunctionCalls函數,callCount的值都會保留上一次調用結束時的值,并在此基礎上增加,而不會像普通局部變量那樣每次都重新初始化為 0。

(2)修飾全局變量:static 修飾全局變量時,就像是給這個全局變量戴上了一個 “神秘面紗”,將其作用域限制在了當前文件中。全局變量默認是具有外部鏈接性的,也就是說,在其他文件中可以通過extern關鍵字來訪問它。但被 static 修飾后,它就只能在當前文件中被訪問,其他文件無法窺探到它的存在。

這樣做的好處是可以避免不同文件中同名全局變量的沖突,提高代碼的模塊化和安全性 。例如,在一個大型項目中,可能有多個文件都需要使用一個名為config的配置變量,如果不使用 static 修飾,就需要非常小心地管理這個變量,防止在不同文件中被意外修改。而使用 static 修飾后,每個文件都可以有自己獨立的config變量,互不干擾。

(3)修飾類成員變量:在類中,static 修飾的成員變量就像是一個公共的寶藏,被類的所有對象共享。每個對象都可以訪問和修改這個靜態成員變量,而且對它的修改會影響到所有對象。靜態成員變量不屬于任何一個具體的對象,而是屬于整個類。它的初始化需要在類外進行,例如:

class MyClass {
public:
    static int sharedValue;
};
int MyClass::sharedValue = 0;

在上面的代碼中,sharedValue是MyClass類的靜態成員變量,在類外進行了初始化。所有MyClass類的對象都可以訪問和修改sharedValue,比如:

MyClass obj1, obj2;
obj1.sharedValue = 10;
std::cout << "obj2的sharedValue: " << obj2.sharedValue << std::endl; // 輸出10

1.2修飾函數

當 static 修飾函數時,這個函數就變成了一個 “隱士”,它的作用域被限制在了當前文件中。其他文件無法調用這個靜態函數,只有當前文件中的其他函數可以訪問它。這在模塊化編程中非常有用,可以將一些只在當前文件中使用的輔助函數聲明為靜態函數,隱藏實現細節,提高代碼的封裝性 。比如,我們在一個文件中實現一個數學計算模塊,其中有一些內部使用的輔助函數,我們可以將它們聲明為靜態函數:

// mathUtils.cpp
static int add(int a, int b) {
    return a + b;
}
int calculateSum() {
    int result = add(3, 5);
    return result;
}

在上面的代碼中,add函數被聲明為靜態函數,只能在mathUtils.cpp文件中被調用,其他文件無法訪問它。這樣可以避免命名沖突,同時也保護了函數的實現細節不被外部隨意修改。

1.3static 成員

(1)static 成員變量

static成員變量是屬于類的,而不是屬于類的某個對象。這意味著無論創建多少個類的對象,static成員變量在內存中都只有一份拷貝,所有對象共享這一份數據 。例如,我們有一個Student類,其中包含一個static成員變量totalStudentCount用于統計學生總數,代碼如下:

class Student {
public:
    Student() {
        totalStudentCount++;
    }
    ~Student() {
        totalStudentCount--;
    }
    static int getTotalStudentCount() {
        return totalStudentCount;
    }
private:
    static int totalStudentCount;
};

int Student::totalStudentCount = 0;

int main() {
    Student s1, s2;
    std::cout << "Total students: " << Student::getTotalStudentCount() << std::endl; 
    return 0;
}

在上述代碼中,totalStudentCount 是static 成員變量,在類外進行初始化。每個Student對象創建時,totalStudentCount 會增加;對象銷毀時,totalStudentCount 會減少。通過 Student::getTotalStudentCount() 函數可以獲取當前學生總數 。

static成員變量的內存分配在靜態存儲區,它的生命周期從程序開始到程序結束。與普通成員變量不同,普通成員變量是在對象創建時分配內存,存儲在對象的內存空間中,每個對象都有自己獨立的一份;而 static 成員變量不依賴于對象的創建,即使沒有創建任何對象,也可以訪問static成員變量 。

(2)static 成員函數

static成員函數同樣屬于類,而不是類的對象。它的一個重要特點是沒有this指針,因為它不與任何特定的對象相關聯 。這就導致static成員函數只能訪問靜態成員變量和靜態成員函數,不能訪問非靜態成員。例如:

class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }
private:
    static int result; 
};

int MathUtils::result = 0;

int main() {
    int sum = MathUtils::add(3, 5); 
    std::cout << "Sum: " << sum << std::endl; 
    return 0;
}

在這個 MathUtils 類中,add 函數是 static 成員函數,它可以直接通過類名調用,無需創建 MathUtils 對象。由于沒有 this 指針,它不能訪問非靜態成員 。相比之下,普通成員函數有 this 指針,它指向調用該函數的對象,因此可以訪問對象的所有成員,包括靜態和非靜態成員 。static 成員函數常用于實現一些與類相關但不依賴于具體對象狀態的操作,比如工具類中的方法、工廠模式中的創建對象方法等 。

二、static析構的原理與規則

析構函數,簡單來說,它的作用與構造函數恰好相反。構造函數是在對象創建時被調用,用于初始化對象的成員變量和資源;而析構函數則是在對象銷毀時被調用,負責清理對象所占用的資源,比如釋放動態分配的內存、關閉打開的文件、斷開網絡連接等 。

析構函數有著獨特的特點。它沒有返回值,連void類型都不能指定,因為它的使命就是默默地完成清理工作,不需要向外界返回任何結果。它的函數名是在類名前面加上波浪號~,以此來表明它是析構函數,與構造函數區分開來 。比如,對于一個名為Student的類,它的析構函數就是~Student()。而且,析構函數不能帶有參數,這就意味著它不能被重載,一個類只能有一個析構函數 。這是 C++ 語言的設計規則,保證了析構函數的唯一性和確定性,讓編譯器能夠準確地在合適的時機調用它。

2.1調用時機

(1)局部對象:當局部對象離開其作用域時,析構函數就會被自動調用。例如,在一個函數內部定義的對象,當函數執行結束,程序的控制權離開這個函數時,這個局部對象的析構函數就會被調用。就像下面這段代碼:

void testFunction() {
    class LocalClass {
    public:
        ~LocalClass() {
            std::cout << "LocalClass的析構函數被調用" << std::endl;
        }
    };
    LocalClass obj;
}// 當程序執行到這里,obj離開作用域,其析構函數被調用

在testFunction函數中,obj是一個局部對象,當函數結束時,obj的析構函數會自動執行,輸出 “LocalClass 的析構函數被調用”。

(2)靜態對象:靜態對象包括靜態局部對象和靜態全局對象,它們的析構函數會在程序結束時被調用。靜態局部對象在函數第一次執行到它的定義處時被初始化,之后在函數多次調用過程中,它不會被重新初始化。而當整個程序結束時,靜態局部對象和靜態全局對象的析構函數才會被調用 。比如:

class StaticClass {
public:
    ~StaticClass() {
        std::cout << "StaticClass的析構函數被調用" << std::endl;
    }
};
static StaticClass globalStaticObj;
void anotherFunction() {
    static StaticClass localStaticObj;
}
int main() {
    anotherFunction();
    return 0;
}// 程序結束時,localStaticObj和globalStaticObj的析構函數被調用

在這個例子中,globalStaticObj是靜態全局對象,localStaticObj是靜態局部對象,它們的析構函數都會在main函數結束,程序即將退出時被調用。

(3)全局對象:全局對象的析構函數調用時機和靜態對象類似,也是在程序結束時被調用。全局對象在程序啟動時就被創建并初始化,在整個程序運行期間都存在,直到程序結束才會被銷毀,此時其析構函數被調用 。比如:

class GlobalClass {
public:
    ~GlobalClass() {
        std::cout << "GlobalClass的析構函數被調用" << std::endl;
    }
};
GlobalClass globalObj;
int main() {
    return 0;
}// 程序結束時,globalObj的析構函數被調用

在這個代碼中,globalObj是全局對象,當main函數返回,程序即將結束時,globalObj的析構函數會被調用,輸出 “GlobalClass 的析構函數被調用”。

(4)動態創建對象:通過new關鍵字動態創建的對象,需要使用delete關鍵字來手動釋放內存,并且在釋放內存時會調用析構函數。如果不使用delete釋放動態分配的對象,就會導致內存泄漏,這是 C++ 編程中需要特別注意的問題 。例如:

class DynamicClass {
public:
    ~DynamicClass() {
        std::cout << "DynamicClass的析構函數被調用" << std::endl;
    }
};
int main() {
    DynamicClass* ptr = new DynamicClass();
    delete ptr; // 調用DynamicClass的析構函數
    return 0;
}

在main函數中,我們使用new創建了一個DynamicClass對象,并將其指針賦值給ptr。當我們使用delete ptr時,DynamicClass對象的析構函數會被調用,輸出 “DynamicClass 的析構函數被調用”,然后釋放該對象所占用的內存。如果遺漏了delete ptr這一步,那么這個DynamicClass對象所占用的內存就永遠不會被釋放,造成內存泄漏。

2.2析構函數的特點

析構函數是 C++ 中一種特殊的成員函數,它的主要作用是在對象銷毀時進行一些必要的清理工作,比如釋放對象在生命周期內動態分配的資源,像內存、文件句柄、網絡連接等 。它的定義方式很獨特,函數名是在類名前加上字符~ ,例如,對于類MyClass,其析構函數為~MyClass()。

析構函數有幾個顯著的特性:它沒有參數,也沒有返回值類型,這是因為它的目的純粹是為了清理對象相關的資源,不需要接收額外的信息,也無需返回任何數據給調用者 。而且,一個類只能有一個析構函數,如果用戶沒有顯式定義析構函數,系統會自動生成默認的析構函數 。

不過,默認析構函數通常不會執行實際的清理操作,只是一個空函數體。只有當類中包含需要手動釋放的資源時,才需要用戶自定義析構函數 。當對象的生命周期結束時,比如局部對象在其作用域結束時,動態分配的對象在使用delete操作符時,以及全局或靜態對象在程序終止時,C++ 編譯系統會自動調用析構函數 。例如:

class Resource {
public:
    Resource() {
        data = new int[10]; 
        std::cout << "Resource constructed" << std::endl;
    }
    ~Resource() {
        delete[] data; 
        std::cout << "Resource destructed" << std::endl;
    }
private:
    int* data;
};

int main() {
    {
        Resource res; 
    } 
    std::cout << "End of main" << std::endl;
    return 0;
}

在上述代碼中,Resource類的析構函數負責釋放構造函數中動態分配的整數數組。在main函數中,當res對象離開其作用域時,析構函數會自動被調用,輸出 “Resource destructed”,然后才輸出 “End of main” 。這清晰地展示了析構函數在對象銷毀時自動執行清理工作的特性。

2.3static 對象的析構順序

static對象包括全局static對象和局部static對象,它們的析構順序遵循特定的規則 。根據 C++ 標準,全局static對象的析構順序與它們的構造順序相反 。例如:

class GlobalStaticA {
public:
    GlobalStaticA() {
        std::cout << "GlobalStaticA constructed" << std::endl;
    }
    ~GlobalStaticA() {
        std::cout << "GlobalStaticA destructed" << std::endl;
    }
};

class GlobalStaticB {
public:
    GlobalStaticB() {
        std::cout << "GlobalStaticB constructed" << std::endl;
    }
    ~GlobalStaticB() {
        std::cout << "GlobalStaticB destructed" << std::endl;
    }
};

GlobalStaticA a;
GlobalStaticB b;

int main() {
    std::cout << "Inside main" << std::endl;
    return 0;
}

在這個例子中,a和b是全局static對象。程序運行時,首先會構造a,輸出 “GlobalStaticA constructed”,然后構造b,輸出 “GlobalStaticB constructed” 。當程序結束時,會先析構b,輸出 “GlobalStaticB destructed”,再析構a,輸出 “GlobalStaticA destructed” 。

對于局部static對象,它們在程序執行流第一次到達其定義處時被構造,析構則發生在程序結束時,并且析構順序也與構造順序相反 。來看下面這個例子:

class LocalStatic {
public:
    LocalStatic() {
        std::cout << "LocalStatic constructed" << std::endl;
    }
    ~LocalStatic() {
        std::cout << "LocalStatic destructed" << std::endl;
    }
};

void func() {
    static LocalStatic s; 
}

int main() {
    func(); 
    std::cout << "Inside main" << std::endl;
    return 0;
}

在func函數中,s是局部static對象。當第一次調用func時,s被構造,輸出 “LocalStatic constructed” 。當程序結束時,s被析構,輸出 “LocalStatic destructed” 。如果在main函數中有多個局部static對象,它們的構造和析構順序也遵循上述規則 。理解static對象的析構順序對于正確管理資源、避免內存泄漏以及確保程序的穩定性至關重要,在復雜的程序中,尤其要注意不同static對象之間可能存在的依賴關系對析構順序的影響 。

三、static 對象析構順序深度解析

3.1單文件內的規則

在單文件的簡單場景下,static 對象的析構順序遵循一個清晰且固定的規則:按照構造順序的相反順序進行析構 。這就好比我們搭建一座積木塔,搭建的過程是從下往上一層一層堆積(構造順序),而拆除的時候則是從上往下一層一層取下(析構順序)。

我們通過具體的代碼示例來直觀感受一下:

#include <iostream>
class StaticObject {
public:
    StaticObject(const std::string& name) : m_name(name) {
        std::cout << m_name << " 的構造函數被調用" << std::endl;
    }
    ~StaticObject() {
        std::cout << m_name << " 的析構函數被調用" << std::endl;
    }
private:
    std::string m_name;
};
StaticObject globalObj("全局靜態對象");
void testFunction() {
    static StaticObject localStaticObj("局部靜態對象");
}
int main() {
    testFunction();
    return 0;
}

在上述代碼中,首先定義了一個StaticObject類,它有一個構造函數和一個析構函數,用于輸出對象的構造和析構信息 。然后,在文件中定義了一個全局靜態對象globalObj,在testFunction函數中定義了一個局部靜態對象localStaticObj 。

當程序運行時,首先會調用全局靜態對象globalObj的構造函數,輸出 “全局靜態對象 的構造函數被調用” 。接著,當testFunction函數被調用時,會調用局部靜態對象localStaticObj的構造函數,輸出 “局部靜態對象 的構造函數被調用” 。

而在程序結束時,析構的順序則與構造順序相反。首先調用局部靜態對象localStaticObj的析構函數,輸出 “局部靜態對象 的析構函數被調用” ,然后調用全局靜態對象globalObj的析構函數,輸出 “全局靜態對象 的析構函數被調用” 。

通過這個簡單的示例,我們可以清晰地看到在單文件內,static 對象嚴格按照構造順序的相反順序進行析構,這是 C++ 語言為了保證資源的正確釋放和對象生命周期的合理管理而制定的規則 。這種規則使得我們在編寫代碼時,能夠更好地預測和控制程序的行為,減少因資源管理不當而引發的錯誤 。

3.2多文件場景的復雜性

當我們的項目涉及多個文件時,static 對象析構順序的問題就變得復雜起來 。在多文件場景下,不同文件中的 static 對象析構順序是未定義的 。這意味著,我們無法確切地知道哪個文件中的 static 對象會先被析構,哪個會后被析構 。這種不確定性源于 C++ 標準并沒有對多文件中 static 對象的析構順序做出明確規定,其順序主要取決于編譯器的實現和鏈接器的處理方式 。

這種未定義的析構順序可能會導致一些潛在的問題,其中最常見的就是資源依賴問題 。例如,假設在file1.cpp中定義了一個靜態對象obj1,它在析構時需要訪問file2.cpp中定義的靜態對象obj2 。如果obj2先于obj1被析構,那么當obj1析構時訪問obj2,就會訪問到一個已經被銷毀的對象,從而引發未定義行為,可能導致程序崩潰或出現其他難以調試的錯誤 。

下面是一個簡單的多文件示例來展示這種問題:

// file1.cpp
#include <iostream>
class Object1 {
public:
    ~Object1() {
        std::cout << "Object1 析構,嘗試訪問Object2" << std::endl;
        // 這里假設需要訪問Object2的某個成員或方法
        // 由于析構順序不確定,可能此時Object2已被析構
    }
};
static Object1 obj1;
// file2.cpp
#include <iostream>
class Object2 {
public:
    ~Object2() {
        std::cout << "Object2 析構" << std::endl;
    }
};
static Object2 obj2;

在這個示例中,Object1 的析構函數中假設需要訪問 Object2 。由于多文件中 static 對象析構順序未定義,Object2 有可能先于 Object1 被析構,那么當Object1 析構時訪問 Object2,就會出現問題 。這種問題在實際項目中往往很難調試,因為它可能不是每次都會復現,而且錯誤發生的位置可能與實際問題的根源相距甚遠 。

多文件場景下 static 對象析構順序的未定義性給我們的編程帶來了一定的挑戰,需要我們在設計和實現代碼時格外小心,避免出現因析構順序不當而導致的資源依賴問題 。

四、static 析構常見問題

4.1析構順序導致的問題

在 C++ 編程中,static對象的析構順序問題常常容易被忽視,但卻可能引發嚴重的程序錯誤。以 OceanBase 源碼中的一個實際案例來說,在其開源代碼里,有這樣一段代碼:

oceanbase::sql::ObSQLSessionInfo &session() {
    static oceanbase::sql::ObSQLSessionInfo SESSION;
    return SESSION;
}

ObArenaAllocator &session_alloc() {
    static ObArenaAllocator SESSION_ALLOC;
    return SESSION_ALLOC;
}

int ObTableApiProcessorBase::init_session() {
    int ret = OB_SUCCESS;
    static const uint32_t sess_version = 0;
    static const uint32_t sess_id = 1;
    static const uint64_t proxy_sess_id = 1;
    if (OB_FAIL(session().test_init(sess_version, sess_id, proxy_sess_id, &session_alloc()))) {
        LOG_WARN("init session failed", K(ret));
    }
    // more...
    return ret;
}

在系統退出時,這段代碼可能會導致 coredump 問題 。利用 ASAN 診斷發現,靜態對象SESSION析構時會引用SESSION_ALLOC,而SESSION_ALLOC同樣是一個靜態對象。由于 C++ 中static變量的析構規則是先構造者后析構 ,當SESSION_ALLOC先于SESSION析構時,SESSION析構時就會訪問到非法內存,因為此時SESSION_ALLOC已經析構,其所管理的內存資源已被釋放,SESSION再去訪問相關資源就會出錯,從而引發 coredump 。

為了更直觀地理解,我們可以用一個簡單的示例程序來驗證這種析構順序:

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "construct A" << endl; }
    ~A() { cout << "deconstruct A" << endl; }
    void init() {}
};

class B {
public:
    B() { cout << "construct B" << endl; }
    ~B() { cout << "deconstruct B" << endl; }
    void init(A &a) { a.init(); }
};

A &getA() {
    static A a;
    return a;
}

B &getB() {
    static B b;
    return b;
}

void func() {
    getB().init(getA());
}

int main(int argc, const char *argv[]) {
    func();
    return 0;
}

運行上述程序,輸出結果為:

construct A
construct B
deconstruct B
deconstruct A

從輸出可以清晰地看到,A先構造,B后構造,而析構時,B先析構,A后析構,這完全符合先構造者后析構的規則 。在實際項目中,像 OceanBase 遇到的這種由于static對象析構順序不當導致的問題,往往需要對代碼中各個static對象之間的依賴關系有清晰的認識,并進行合理的設計和處理 。

4.2內存泄漏問題

在 C++ 中,內存泄漏是一個常見且棘手的問題,而static成員指針如果使用不當,就很容易引發內存泄漏 。當static成員指針指向動態分配的內存,但在析構時沒有正確釋放該內存,就會導致這部分內存無法被回收,從而造成內存泄漏 。例如:

class Resource {
public:
    Resource() {
        data = new int[100]; 
    }
    ~Resource() {
        // 這里沒有釋放data內存,會導致內存泄漏
    }
private:
    static int* data; 
};

int* Resource::data = nullptr;

int main() {
    Resource res; 
    return 0;
}

在上述代碼中,Resource類的data是一個static成員指針,在構造函數中分配了內存,但析構函數中沒有釋放,當程序結束時,data所指向的內存就會泄漏 。內存泄漏的危害是隨著程序運行時間的增長,可用內存會逐漸減少,這可能導致系統性能下降,程序響應速度變慢。最終,當可用內存被耗盡時,程序可能會崩潰,甚至導致整個系統出現故障 。

為了解決這個問題,我們需要在析構函數中正確釋放static成員指針所指向的內存 。修改后的代碼如下:

class Resource {
public:
    Resource() {
        data = new int[100]; 
    }
    ~Resource() {
        delete[] data; 
        data = nullptr; 
    }
private:
    static int* data; 
};

int* Resource::data = nullptr;

int main() {
    Resource res; 
    return 0;
}

這樣,在Resource對象析構時,data所指向的內存會被正確釋放,從而避免了內存泄漏問題 。此外,我們還可以利用智能指針來管理static成員指針,進一步提高代碼的安全性和可靠性 。例如:

#include <memory>

class Resource {
public:
    Resource() {
        data = std::make_unique<int[]>(100); 
    }
    ~Resource() {
        // 智能指針會自動釋放內存,無需手動delete
    }
private:
    static std::unique_ptr<int[]> data; 
};

std::unique_ptr<int[]> Resource::data = nullptr;

int main() {
    Resource res; 
    return 0;
}

使用std::unique_ptr后,data的內存管理變得更加安全和便捷,無需手動釋放內存,智能指針會在其生命周期結束時自動釋放所指向的內存 。

4.3多線程環境下的析構問題

在多線程環境中,static變量的析構會面臨線程安全的挑戰 。以 MNN 推理引擎在 IOS 平臺出現的崩潰案例為例,實際代碼中存在子線程訪問靜態變量的情況,當子線程崩潰時,主線程在調用_exit函數 。異常信息顯示為Exception Type: EXC_BAD_ACCESS,Exception Codes: KERN_INVALID_ADDRESS at 0x000095f59f17ce20,Exception Subtype: SIGSEGV,Triggered by Thread: 28 。

從理論上來說,C++11 標準對靜態變量在多線程環境下的使用有相關規范 。在構造階段,如果多個線程同時嘗試初始化同一個局部靜態變量,C++11 規定后續線程需要等待正在初始化的線程完成初始化 。在析構階段,線程生命周期的對象全部在靜態變量之前析構,靜態變量按照后構造的先析構的棧式順序釋放 。

不同的編譯器對這些特性的實現有所不同 。例如,GCC 從 4.3 版本開始支持靜態變量構造和析構函數的多線程安全 。在構造階段,對于局部靜態變量,多線程調用時,首先構造靜態變量的線程先加鎖,其他線程等待前者執行完鎖操作 。對于全局靜態變量,按照聲明順序在主線程構造,早于子線程啟動 。在析構階段,全局和局部靜態變量的析構函數在所有線程結束后才開始調用,保證析構時線程安全 。而對于 Apple clang 編譯器,其對多線程析構構造的支持情況不太明確,從實際案例來看,屬于部分支持 。

在多線程環境下,除了構造和析構階段的線程安全問題,如果在這兩個階段之間,多個線程訪問靜態變量的含有寫操作的成員函數,或某種異步操作的函數,仍然可能出現數據競爭和不一致的問題 。例如:

class SharedData {
public:
    static int value; 
    static void increment() {
        ++value; 
    }
};

int SharedData::value = 0;

void threadFunction() {
    for (int i = 0; i < 1000; ++i) {
        SharedData::increment(); 
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    std::cout << "Final value: " << SharedData::value << std::endl; 
    return 0;
}

在上述代碼中,SharedData類的value是一個static成員變量,increment函數對其進行寫操作 。當多個線程同時調用increment函數時,由于沒有同步機制,會導致數據競爭,最終的value值可能不是預期的 2000 。為了解決這個問題,我們可以使用互斥鎖等同步機制來保證線程安全:

#include <mutex>

class SharedData {
public:
    static int value; 
    static std::mutex mtx; 
    static void increment() {
        std::lock_guard<std::mutex> lock(mtx); 
        ++value; 
    }
};

int SharedData::value = 0;
std::mutex SharedData::mtx;

void threadFunction() {
    for (int i = 0; i < 1000; ++i) {
        SharedData::increment(); 
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    std::cout << "Final value: " << SharedData::value << std::endl; 
    return 0;
}

通過使用std::lock_guard和std::mutex,確保了在對value進行寫操作時的線程安全 。

五、解決static析構問題的方法

5.1確保正確的析構順序

在實際編程中,確保static對象的正確析構順序是避免因析構順序不當而引發問題的關鍵 。以修復 OceanBase 代碼中由于static變量析構順序導致的 coredump 問題為例,我們可以通過調整代碼邏輯來保證析構順序 。

在原代碼中,ObSQLSessionInfo類型的SESSION靜態對象析構時會引用ObArenaAllocator類型的SESSION_ALLOC靜態對象,當SESSION_ALLOC先于SESSION析構時,就會導致SESSION析構時訪問非法內存 。為了解決這個問題,我們可以在SESSION構造之前,主動調用一次session_alloc 。具體修改后的代碼如下:

oceanbase::sql::ObSQLSessionInfo &session() {
    static oceanbase::sql::ObSQLSessionInfo SESSION;
    return SESSION;
}

ObArenaAllocator &session_alloc() {
    static ObArenaAllocator SESSION_ALLOC;
    return SESSION_ALLOC;
}

int ObTableApiProcessorBase::init_session() {
    int ret = OB_SUCCESS;
    static const uint32_t sess_version = 0;
    static const uint32_t sess_id = 1;
    static const uint64_t proxy_sess_id = 1;
    // ensure allocator is constructed before session to
    // avoid coredump at observer exit
    ObArenaAllocator &dummy_allocator = session_alloc();
    UNUSED(dummy_allocator);
    if (OB_FAIL(session().test_init(sess_version, sess_id, proxy_sess_id, &session_alloc()))) {
        LOG_WARN("init session failed", K(ret));
    }
    // more...
    return ret;
}

通過這種方式,使得SESSION_ALLOC先于SESSION構造,根據static對象先構造者后析構的規則,就能保證在析構時,SESSION先于SESSION_ALLOC析構,從而避免了訪問非法內存的問題 。在設計和編寫代碼時,我們應該充分考慮static對象之間的依賴關系,盡量將相互依賴的static對象的構造和析構順序進行合理安排,以確保程序的穩定性和正確性 。

5.2資源管理策略

為了有效避免static析構時可能出現的內存泄漏和懸空指針問題,引入智能指針是一種非常有效的資源管理策略 。智能指針是 C++ 中用于自動管理動態分配資源的類模板,它利用 RAII(資源獲取即初始化)技術,在對象創建時獲取資源,在對象銷毀時自動釋放資源,從而避免了手動new/delete操作可能帶來的內存泄漏和懸空指針風險 。

例如,當我們有一個static成員指針指向動態分配的內存時,使用智能指針可以大大簡化資源管理 。假設我們有一個DataManager類,其中包含一個static成員指針data指向動態分配的整數數組:

#include <memory>

class DataManager {
public:
    static void init() {
        data = std::make_unique<int[]>(100); 
    }
    static void deinit() {
        // 智能指針會自動釋放內存,無需手動delete
    }
private:
    static std::unique_ptr<int[]> data; 
};

std::unique_ptr<int[]> DataManager::data = nullptr;

在上述代碼中,我們使用std::unique_ptr來管理data指針 。在init函數中,通過std::make_unique創建一個包含 100 個整數的數組,并將其賦值給data 。當程序結束,DataManager類的static成員析構時,std::unique_ptr會自動調用delete[]來釋放數組所占用的內存,無需我們手動編寫釋放代碼,從而避免了內存泄漏的風險 。

除了std::unique_ptr,C++ 還提供了std::shared_ptr用于實現多個指針共享同一資源的場景,它通過引用計數來管理資源的生命周期,當引用計數為 0 時,自動釋放資源 。還有std::weak_ptr,它是一種弱引用智能指針,用于解決std::shared_ptr的循環引用問題 。根據不同的場景需求,合理選擇和使用智能指針,可以顯著提高代碼的安全性和可靠性 。

5.3多線程環境下的處理

在多線程環境中,static變量的析構面臨著線程安全的挑戰,需要遵循相關標準規范并采取合適的處理方式 。C++11 標準為static變量在多線程環境下的使用提供了一些規范 。在構造階段,對于局部靜態變量,如果多個線程同時嘗試初始化同一個局部靜態變量,C++11 規定后續線程需要等待正在初始化的線程完成初始化 。在析構階段,線程生命周期的對象全部在靜態變量之前析構,靜態變量按照后構造的先析構的棧式順序釋放 。

為了確保多線程環境下static變量析構的線程安全,我們可以利用鎖機制來同步對static變量的訪問 。例如,當多個線程可能同時訪問和修改一個static成員變量時,我們可以使用互斥鎖來保護對它的操作 。假設有一個Counter類,其中包含一個static成員變量count用于計數:

#include <mutex>

class Counter {
public:
    static void increment() {
        std::lock_guard<std::mutex> lock(mtx); 
        ++count; 
    }
    static int getCount() {
        std::lock_guard<std::mutex> lock(mtx); 
        return count;
    }
private:
    static int count; 
    static std::mutex mtx; 
};

int Counter::count = 0;
std::mutex Counter::mtx;

在上述代碼中,std::mutex類型的mtx用于保護對count的操作 。在increment和getCount函數中,使用std::lock_guard來自動管理鎖的生命周期,在函數進入時自動加鎖,離開時自動解鎖 。這樣,即使在多線程環境下,也能保證對count的操作是線程安全的 。

另外,線程本地存儲(TLS)也是一種在多線程環境下處理數據的有效方式 。通過使用thread_local關鍵字聲明變量,可以使每個線程擁有自己獨立的變量副本,避免了線程間的數據競爭 。例如:

#include <iostream>
#include <thread>

thread_local int threadLocalValue = 0;

void threadFunction() {
    ++threadLocalValue; 
    std::cout << "Thread " << std::this_thread::get_id() << ": threadLocalValue = " << threadLocalValue << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    return 0;
}

在這個例子中,threadLocalValue是一個線程本地存儲變量,每個線程都有自己獨立的副本 。在threadFunction中對threadLocalValue的修改只影響當前線程的副本,不會影響其他線程,從而避免了多線程環境下的競爭問題 。在多線程環境下處理static析構問題時,我們要充分理解 C++11 標準的相關規范,合理運用鎖機制和線程本地存儲等技術,確保程序的線程安全性和正確性 。

六、高頻面試題講解

面試題 1:全局靜態變量和局部靜態變量析構順序誰先誰后?

答案:通常局部靜態變量先析構,全局靜態變量后析構。它們均在 main 結束或調用 exit 時析構,析構順序與構造順序相反,全局靜態變量于 main 前構造,局部靜態變量在函數首次執行到聲明處構造,所以一般全局靜態構造更早。

題目 2:同一函數里多個局部靜態對象,其析構順序如何確定?

答案:與構造順序相反。即后構造的先析構,遵循 “后進先出” 原則。

題目 3:程序包含多個文件,各文件有全局靜態變量,析構順序是怎樣的?

答案:析構順序不確定。不同文件全局靜態變量的構造順序編譯器不定,依 “先構造后析構” 原則,其析構順序同樣不確定。

題目 4:類的靜態成員變量何時析構?

答案:和程序生命周期一致,于 main 函數結束或調用 exit 后析構。其需類外定義,同一源文件內,按和定義相反順序析構;不同源文件時,析構順序編譯器未規定。

題目5:看以下代碼,說出析構函數調用順序:

class A {};  
class B {};  
A a;  
int main() {  
    B b;  
    static A a2;  
    return 0;  
}

答案:先 b 析構(局部對象在函數結束時析構),接著 a2 析構(局部靜態于 main 結束后析構),最后 a 析構(全局對象于程序結束最后析構)。

題目 6:若局部靜態對象所在函數未被調用,其析構函數會執行嗎?

答案:不會。局部靜態對象在函數首次調用至聲明處構造,沒構造便不會觸發析構。

題目 7:派生類對象含靜態成員變量,對象析構時,靜態成員變量析構會受影響嗎?

答案:不受影響。類靜態成員變量析構僅取決于程序結束與否,和類對象創建或析構無直接聯系。

題目 8:以下程序析構順序是什么?

class A {};  
class B {};  
class C {};  
class D {};  
C c;  
int main() {  
    A a;  
    B b;  
    static D d;  
    return 0;  
}

答案:先 b 再 a(局部對象按定義相反序析構),然后 d 析構(靜態對象于程序結束析構),最后 c 析構(全局對象程序結束析構,c 比 d 構造更早)。

題目9:使用 atexit 注冊清理函數,其和靜態變量析構,執行先后順序如何?

答案:通常靜態變量析構優先。atexit 注冊的函數在所有靜態存儲持續期對象析構后,由 exit 觸發執行。

題目10:靜態對象析構期間拋未捕獲異常,會有什么狀況?

答案:C++ 里會調用 std::terminate 結束程序。靜態對象析構屬程序結束關鍵階段,需避免拋未捕獲異常。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2011-06-15 09:47:14

C++

2025-07-04 01:00:00

2025-02-18 00:08:00

代碼C++RAII

2023-11-28 11:25:36

數據雙寫一致數據庫

2025-08-13 01:00:00

2010-01-18 15:53:27

C++析構函數

2010-02-04 16:39:26

C++析構函數

2011-07-15 01:29:39

C++析構函數

2025-05-29 10:30:00

C++編程recv

2025-08-18 02:11:00

2025-08-11 05:00:00

2025-05-27 10:15:00

void*函數開發

2010-02-05 13:35:19

C++虛析構函數

2024-12-19 14:42:15

C++內存泄漏內存管理

2025-08-26 02:15:00

C++函數Student

2025-08-21 13:40:58

頭文件循環項目

2025-08-18 02:12:00

2024-12-11 16:00:00

C++函數編譯器

2025-05-15 09:45:54

2010-01-25 10:10:42

C++函數參數
點贊
收藏

51CTO技術棧公眾號

国产精品自产自拍| 视频福利一区| 国产日产亚洲精品系列| 国产精品96久久久久久| 亚洲第一视频区| 成人免费短视频| 欧美国产精品v| 91九色蝌蚪国产| 国产午夜久久久| 免费成人av| 欧美高清激情brazzers| 福利在线一区二区| 免费人成黄页在线观看忧物| 日韩精品一区第一页| 久久成人精品电影| 制服丝袜第二页| 亚洲男女网站| 午夜精品福利久久久| 日韩女优中文字幕| 亚洲特级黄色片| 伊人精品在线| 伊人久久久久久久久久久| 在线观看视频在线观看| 在线视频超级| 亚洲免费资源在线播放| 久久久久久高清| 国产裸体无遮挡| 亚洲尤物影院| 欧美成人性生活| 婷婷色一区二区三区| av综合网页| 欧美久久一二区| 久久精品国产sm调教网站演员| 2021av在线| av电影在线观看不卡| 成人伊人精品色xxxx视频| 五月婷婷亚洲综合| 欧美三区在线| 日韩性xxxx爱| 国产全是老熟女太爽了| 日韩欧美激情电影| 欧美日韩aaaaaa| 国产曰肥老太婆无遮挡| 麻豆tv入口在线看| 中文一区二区在线观看| 欧美一二三区| 亚洲欧美自偷自拍| 成人免费高清在线观看| 国产成人精品综合| 日韩少妇高潮抽搐| 亚洲午夜电影| 欧美丰满老妇厨房牲生活 | 免费不卡在线视频| 77777少妇光屁股久久一区| 欧美日韩激情在线观看| 久操国产精品| 亚洲精品国产美女| 中文字幕人妻一区| 国产精品一区二区精品| 欧美日韩精品综合在线| 我看黄色一级片| 外国成人直播| 色香蕉久久蜜桃| 国产精品沙发午睡系列| 国产第一页在线| 亚洲最色的网站| 在线观看污视频| 国产原厂视频在线观看| 国产精品久久久久久久蜜臀| 三区精品视频观看| 欧美套图亚洲一区| 91原创在线视频| 蜜桃成人在线| 国产一区精品| 中文欧美字幕免费| 欧美理论一区二区| 成人高清免费观看mv| 日本一区二区三区dvd视频在线 | 日韩高清免费av| 99成人在线| 68精品国产免费久久久久久婷婷| 亚洲精品午夜久久久久久久| 亚洲视频二区| 日本欧美中文字幕| 中文字幕无码乱码人妻日韩精品| 日韩国产精品久久久久久亚洲| 国产99久久精品一区二区 夜夜躁日日躁| 在线观看亚洲欧美| 久久视频一区| 成人福利网站在线观看| 国产高清免费av| 处破女av一区二区| 免费观看成人高| 成人高清在线| 一区二区高清免费观看影视大全| 久久国产精品网| 中文字幕在线免费观看视频| 日本精品视频一区二区| 免费网站在线观看黄| jizzjizzjizz欧美| 国产亚洲欧美日韩精品| 日日碰狠狠添天天爽| 国产精品av久久久久久麻豆网| 国内精品久久影院| 日韩国产成人在线| 韩国一区二区在线观看| 国产精品久久久久久久久久久久冷| 神马午夜在线观看| 亚洲国产精品传媒在线观看| 国产树林野战在线播放| 擼擼色在线看观看免费| 欧美日韩国产乱码电影| 国产精品日日摸夜夜爽| 成人福利一区| 中文字幕一区电影| 日韩 欧美 亚洲| 美女视频免费一区| 国产日韩二区| 欧美一级二级三级区| 亚洲va国产天堂va久久en| 国产精品沙发午睡系列| 欧美大片91| 国产一区二区三区网站| 久草视频在线资源| 日本午夜一本久久久综合| 操人视频欧美| 1pondo在线播放免费| 亚洲成国产人片在线观看| 尤蜜粉嫩av国产一区二区三区| 亚洲欧洲日韩精品在线| 亚洲欧美另类自拍| 免费无遮挡无码永久在线观看视频| 欧美激情麻豆| 国产精品专区h在线观看| 亚洲男女视频在线观看| 亚洲伦理在线精品| 亚洲一级片免费| 蜜桃一区二区三区| 萌白酱国产一区二区| 中文字幕制服诱惑| 久久嫩草精品久久久久| 樱花www成人免费视频| 自拍偷自拍亚洲精品被多人伦好爽| 精品蜜桃在线看| 一区二区三区四区五区| 奇米影视7777精品一区二区| 国内一区二区在线视频观看| 18视频在线观看网站| 欧美日本精品一区二区三区| 久操视频在线观看免费| 国产视频一区欧美| 九色综合日本| 国产精品一二三产区| 日韩精品一区二区三区老鸭窝| 中文字幕 自拍| 首页国产欧美久久| 欧美日韩中文国产一区发布| 国产伦精品一区二区三区视频金莲| 亚洲国产精品电影| 日本熟妇乱子伦xxxx| 成人视屏免费看| 国产91沈先生在线播放| 美女精品视频在线| 久久国产视频网站| www.精品久久| 亚洲手机成人高清视频| 四虎国产精品永久免费观看视频| 成人情趣视频网站| 国产精品视频在线观看| 日韩专区在线| 欧美一级黄色录像| 激情综合网五月天| 99在线热播精品免费| 国产最新免费视频| 韩日一区二区三区| 国产精品视频专区| 1024在线播放| 欧美一区中文字幕| 久久精品www人人爽人人| 成人激情校园春色| youjizz.com在线观看| 美女一区2区| 欧洲成人免费视频| 国产福利在线| 欧美精品丝袜中出| 欧美精品videos极品| gogogo免费视频观看亚洲一| 日本免费一级视频| 久久社区一区| 国产精品国产三级国产专区53 | 国内精品偷拍| 欧美在线一区二区三区四| 三级在线视频| 欧美日韩中文一区| 国产又黄又爽又无遮挡| av不卡免费电影| 天天色综合社区| 久久久久久久久久久9不雅视频| 亚洲一区二区中文| 少妇在线看www| 久久国产精品久久国产精品| 精品电影在线| 亚洲国产精品va在线| 91精品中文字幕| 色成人在线视频| 国产无遮挡又黄又爽在线观看| 中文字幕欧美国产| www国产视频| 国产一区二区精品久久| 日本在线观看a| 国语自产精品视频在线看8查询8| 日韩免费三级| 亚洲人成精品久久久| 不卡一卡2卡3卡4卡精品在| 欧美日韩国产网站| 91精品国产91久久久久久| 一色桃子av在线| 日韩中文字幕精品| 国产视频网址在线| 精品一区二区三区电影| 亚洲免费成人在线| 欧美一区二区三区免费大片| 亚洲天堂手机版| 色婷婷av一区| 亚洲欧美综合另类| 午夜在线电影亚洲一区| 久艹视频在线观看| 一区二区三区美女视频| 日韩视频中文字幕在线观看| 国产精品免费丝袜| 粉嫩精品久久99综合一区| 91天堂素人约啪| 久久久久国产精品无码免费看| 经典一区二区三区| 亚欧激情乱码久久久久久久久| 日日嗨av一区二区三区四区| aaa毛片在线观看| 亚久久调教视频| 国产美女三级视频| 久久都是精品| 国产精品亚洲二区在线观看| 性高湖久久久久久久久| 久草资源站在线观看| 亚洲欧洲视频| 毛片在线视频播放| 国产亚洲欧洲| 欧美综合在线观看视频| 日本女人一区二区三区| 亚洲第一中文av| www日本高清| 亚洲欧美日韩在线| 91在线播放观看| 亚洲欧洲中文日韩久久av乱码| 日本美女黄色一级片| 中文字幕亚洲欧美在线不卡| 成人在线观看小视频| 亚洲综合色丁香婷婷六月图片| 黄色一级视频免费| 亚洲成人动漫av| 精品人妻无码一区二区性色| 色就色 综合激情| 中文字幕在线观看高清| 日韩一区国产二区欧美三区| 亚洲乱码在线观看| 日韩av中文字幕在线播放| 国产精品一区二区婷婷| 久久精品视频在线| 欧美xxxx做受欧美88bbw| 97免费中文视频在线观看| 韩日成人影院| 91精品久久久久久| 成人爽a毛片免费啪啪红桃视频| 国产精品一 二 三| 九九久久精品| 欧美 日韩 国产 在线观看| 欧美永久精品| 免费在线激情视频| 秋霞午夜av一区二区三区| 男女视频在线观看网站| 成人h精品动漫一区二区三区| 受虐m奴xxx在线观看| 一区二区中文视频| 日韩成人一区二区三区| 欧美丝袜丝交足nylons| www.麻豆av| 在线观看精品国产视频| 午夜伦理在线视频| 日本亚洲欧洲色α| 欧美午夜网站| 欧美中日韩免费视频| 欧美日韩国产色综合一二三四| 精品免费国产一区二区| 丰满白嫩尤物一区二区| 最新中文字幕av| 亚洲国产日韩a在线播放性色| 无码视频在线观看| 日韩精品一区二区三区蜜臀| av在线女优影院| 欧美激情精品久久久久久久变态| 国产亚洲一区二区手机在线观看 | 欧美成人激情视频| 最近在线中文字幕| 成人免费淫片aa视频免费| 极品国产人妖chinesets亚洲人妖| 日韩国产美国| 亚洲国产日本| 深爱五月综合网| 国产欧美精品国产国产专区| 国产女同在线观看| 日韩视频在线永久播放| 成人高清免费在线播放| 欧美在线播放视频| 久久久精品国产**网站| 国产一区一区三区| 毛片av中文字幕一区二区| 99久久国产精| 亚洲va欧美va天堂v国产综合| 91一区二区视频| 中文字幕亚洲一区在线观看| 成人美女大片| 免费av一区二区三区| 亚洲看片免费| 亚洲精品成人无码毛片| 亚洲三级免费电影| 亚洲天堂网视频| 中文字幕久热精品视频在线| 三上悠亚国产精品一区二区三区| 国产精品国产亚洲精品看不卡15| 亚洲先锋影音| 天堂av2020| 国产精品美女一区二区| 国产99免费视频| 亚洲欧美中文另类| 性xxxxfreexxxxx欧美丶| 国产一区二区三区高清| 激情自拍一区| 中文字幕人妻一区二区三区| 亚洲一区二区三区视频在线播放| 国产美女免费视频| 久久这里有精品视频| 伊人久久大香伊蕉在人线观看热v 伊人久久大香线蕉综合影院首页 伊人久久大香 | 国产欧美一区二区三区另类精品| 亚洲天堂久久| 国产精品成人99一区无码 | 中文字幕欧美精品日韩中文字幕| 欧美艳星kaydenkross| 免费看成人片| 石原莉奈在线亚洲二区| 亚洲欧洲久久久| 在线一区二区三区四区五区| 国产黄在线观看| 国产精品第一区| 成人激情诱惑| 日本在线播放一区二区| 中文字幕日韩欧美一区二区三区| 一级黄色片免费| 久久综合网hezyo| 一区二区三区高清在线观看| 成人免费播放器| 91丨国产丨九色丨pron| 无码人妻一区二区三区免费| 色偷偷888欧美精品久久久 | 日韩免费成人网| ****av在线网毛片| 久久久人人爽| 日本少妇一区二区| 午夜激情福利网| 亚洲变态欧美另类捆绑| jizz内谢中国亚洲jizz| 亚洲乱码一区二区三区| 极品少妇xxxx精品少妇| 久久久久香蕉视频| 精品无码久久久久久国产| 小明成人免费视频一区| 欧美日韩一级在线| 不卡一区二区在线| 波多野结衣电车| 久久综合五月天| 性欧美lx╳lx╳| 亚洲 国产 图片| 黄色成人在线免费| а√天堂中文在线资源bt在线| 亚洲自拍欧美色图| 亚洲欧美成人| 91香蕉视频在线播放| 日韩成人激情在线| 亚州欧美在线| 欧美三级一级片| 亚洲视频免费在线| 水莓100在线视频| 91在线高清视频| 男女精品网站| 少妇久久久久久被弄高潮| 亚洲免费精彩视频| 视频精品二区| 黄色免费网址大全| 午夜精品一区二区三区三上悠亚| 都市激情在线视频| 国产在线一区二| 精品无码三级在线观看视频|