沒有學不會的C++:為什麼不要使用全域性變數

weixin_34162695發表於2019-01-16

在寫程式時,我們都知道一條規範:不要使用全域性變數。至於為什麼,有可能是因為它會汙染名稱空間,也有可能是因為它會造成程式的不確定性,本文主要使用一個例子,來說明全域性變數是如何讓程式變得不確定的。

我們先定義兩個類,一個 Cat,一個 Dog,如下是 cat.hcat.cc 檔案

// cat.h
#include <iostream>
using namespace std;

class Cat;
extern Cat c;

class Cat {
public:
    Cat(char* name);
    void meow();
private:
    char* _name;
};

// cat.cc
#include "cat.h"
#include "dog.h"

Cat c("mimi");
Cat::Cat(char* name) {
    cout << "construct cat" << endl;
    _name = name;
}

void Cat::meow() {
    cout << "cat " << _name << " meow" << endl;
}

Cat 類很簡單,只有一個成員 _name,它是一個指標變數,且通過 meow 方法把它列印到螢幕上,同時,我們還定義了一個全域性變數 Cat c("mimi");,同樣的,Dog 類的定義也很簡單,如下:

// dog.h
#include <iostream>
using namespace std;

class Dog;
extern Dog d;

class Dog {
public:
    Dog(char* name);
    void bark();
private:
    char* _name;
};

// dog.cc
#include "dog.h"
#include "cat.h"

Dog d("kobe");
Dog::Dog(char* name) {
    cout << "construct dog" << endl;
    _name = name;
}                                                                   
                                                                    
void Dog::bark() {                                                  
    cout << "dog " << _name << " bark" << endl;                     
}

我們給 Dog 也定義了一個全域性變數 d("kobe");,此時,我們修改一下 Cat 的建構函式,在裡面引用全域性變數 d,看看會發生什麼

Cat::Cat(char* name) {
    cout << "construct cat" << endl;
    _name = name;
    d.bark();
}

編譯執行前,別忘了我們的入口檔案 main.cc,如下

int main() {
    return 0;
}

現在,我們將其進行編譯執行

...
g++ -o main main.o cat.o dog.o -std=c++11  // 編譯時的輸出,說明了連結順序
$ ./main
construct cat
// [1]    50759 segmentation fault  ./main 

可以看到程式崩潰了,崩潰原因是我們剛才在 Cat 建構函式中增加的一行呼叫全域性變數的程式碼,因為在呼叫這行程式碼時,全域性變數 d (實際上是 d._name )還沒初始化。

而全域性變數的初始化順序是由編譯器決定的,所以如果我們的全域性變數間又有互相依賴的話,就很容易造成程式崩潰。

避免使用全域性變數的方法也有很多,其中最廣泛的應數 Singleton 模式了,針對上面的程式碼,我們可以定義一個 Singleton 類,其中包含 CatDog 的靜態指標,如下

// singleton.h
class Cat;
class Dog;
class Singleton {
    static Dog* pd;
    static Cat* pc;
public:
    ~Singleton();

    static Dog* getDog();
    static Cat* getCat();
};

與此同時,我們還宣告瞭兩個靜態方法,用來獲取 DogCat 的指標,並且,我們希望 DogCat 是以 lazy 的方式進行初始化的,即下面的 singleton.cc 檔案的實現

// singleton.cc
#include "singleton.h"
#include "dog.h"
#include "cat.h"

Dog* Singleton::pd = 0;
Cat* Singleton::pc = 0;
Singleton::~Singleton() {
    delete pd;
    delete pc;
    pd = 0;
    pc = 0;
}
Dog* Singleton::getDog() { 
    if (pd == 0) {
        pd = new Dog("kobe");  
    }
    return pd;
}
Cat* Singleton::getCat() {
    if (pc == 0) {
        pc = new Cat("mimi");
    }
    return pc;
}

可以看到初始化 CatDog 的時機是在第一次呼叫 getCatgetDog 時。現在你就可以刪掉程式中的全域性變數了,當你需要使用 Cat 物件或 Dog 物件時,直接呼叫 Singleton::getCat()Singleton::getDog() 即可。

參考:

相關文章