C++變量作用域、生存期、存儲類別


寫C、C++代碼的小伙伴一定在頭疼變量的作用域、生存期、存儲類別問題。什么靜態、外部、寄存器、局部、全局搞得一頭霧水。今天咱們就來梳理一下他們的變態關系(什么不得了的事情???

1、變量的作用域

說白了,作用域就是一個”代碼塊“,也就是大括號包裹的那一段東西。包括函數體、控制語句塊這些。大家應該都有所耳聞。

#include<stdio.h>
int x = 5; // 全局變量
int main() {
    printf("%d ",x);
    int x = 6; // 局部變量
    printf("%d ",x); // 輸出結果是5還是6?
    return 0;
}

這段代碼算很經典了。它展示了不同定義位置的變量的作用域。

首先一個輸出肯定是5,毫無懸念。但是下面那個就不同了,因為在main函數中又出來一個同名的局部變量。C++遵循向上覆蓋原則,在一個代碼塊的子塊中定義的變量,覆蓋掉原塊中定義的變量。所以,下面那個輸出應該是6。一旦在子塊中定義了同名變量,這個塊外部的變量就不可見了。所以,實際編程要避免這種情況

但C++提供了一種叫做“作用域運算符”的東西,也就是::。這個運算符可以使得全局變量重新可見。比如我在x = 6定以后想再用全局變量的值,就可以這樣寫:printf("%d", ::x);這樣輸出就是5了。這個運算符大家應該都見過,在類和namespace中經常使用。但不管怎么說,除非是在class和namespace中,其它的情況應該嚴格避免內外作用域的變量重名

說了這么多,作用域的概念給大家:作用域,就是從變量定義開始到所在代碼塊或文件結束為止,對編譯器可見的范圍

如果對上面這些概念不理解,就往下看,局部變量和全局變量的概念。

2、局部變量和全局變量

局部變量,是指在一個函數內部定義的變量。它的作用域從定義(或聲明)開始,到函數結束。它只對函數本身可見,對函數外部不可見。任何手段都無法訪問函數內部的變量。因為除了main函數之外,其它函數都不是程序實體,它只提供了一個模塊運行的模板,里面的變量只是為了這個函數服務,函數外部引用它沒有意義。

為了方便往下講,先說一下生存期的概念。作用域是個靜態概念,表示變量的可見范圍,是對編譯器而言的,而生存期是動態概念,表示變量在內存中的創建、使用、銷毀過程,是運行時概念。生存期是指變量從創建到被操作系統回收的這一段時間。縮句的話,作用域是代碼范圍,生存期是時間范圍。

回到正題,局部變量的生存期,就是從函數創建它開始,到函數調用完畢,被操作系統回收這一段時間

我們知道,一個進程有一段棧空間,函數調用的過程是用棧管理的。棧保存函數的參數、局部變量、返回地址。當一個函數被調用時,CPU首先把當前執行地址保存到一個特殊寄存器中,作為這一函數的返回地址。然后,CPU跳轉到函數入口地址,棧頂指針擴展一幀,棧空間隨之增長。把剛才的返回地址保存到棧中,並從棧中加載參數。之后就是執行過程。執行完畢后,從棧中取出返回地址,CPU跳回這一地址。最后棧頂指針回退一幀,這一函數的棧空間將被釋放,局部變量也就隨之銷毀。

上面這一段看不懂也沒關系,如果你們學過匯編語言和操作系統,就會明白這是一個怎樣的過程。總之,需要記住局部變量是在函數調用並執行到定義語句時創建,函數返回時銷毀

全局變量,是指不在任何函數內定義的變量。它定義在文件的頂層,對任何函數都可見(我們從現在起,假設任何函數中沒有和全局變量重名的局部變量)。

全局變量的作用域是從定義開始到這個文件結束。其實這種說法不精確,因為全局變量可以被其他文件調用,這就是外部變量,我們稍后再講。

和局部變量不同,全局變量是在進程創建(注意進程創建和main函數調用是兩個概念,進程創建包括代碼加載、數據加載、內存空間分配過程,是操作系統完成的,而main函數是進程調用的)時同時創建的。所以,全局變量在main函數調用之前就已經存在了。一個進程的地址空間分為代碼段、數據段、用戶段,代碼段就是機器指令(參看馮諾依曼體系結構),數據段就是我們所說的全局變量,而用戶段是供進程運行過程中動態分配內存的。我們剛才說的棧空間就在用戶段。所以說,全局變量的存儲位置和局部變量完全不同。

全局變量又分為已初始化的和未初始化的。已初始化的全局變量,操作系統會自動為其初始化值,放在數據段前部,而未初始化的全局變量,則會放在數據段的后部,並自動清零。所以,你看到未初始化的全局變量初始值都是0。

全局變量的生存期從進程創建開始,一直到進程運行完畢,所有內存被操作系統回收位置為止。

另外,在不是函數的代碼塊中創建的局部變量,也是類似的。比如

int s = 0;
for (int i = 0; i < 10; i++) {
    s += i;
    int t = s;
}
printf("%d %d %d", i, s, t); // 錯誤,i和t只對for循環體可見

3、變量存儲類別

說完了生存期、作用域的概念,我們再來看變量存儲類別。

變量的存儲類別分為自動(auto),靜態(static),外部(extern)和寄存器(register)四種。

3.1、自動變量

注意,雖然叫auto變量,但是,auto關鍵字在C++11中已經不再是“自動變量”的意思,而是“自動類型推斷”。所以,不要試圖用auto關鍵字來創建自動變量

比如:

auto a = 1;
printf("%d\n", a); // a的類型自動推斷為整型
auto int b = 2; // 錯誤,auto是指自動類型推斷,不可以與類型標識符連用
printf("%d\n", b);

我們剛才所說的變量,都是自動變量。所謂自動變量,就是按照操作系統的內存分配和回收規則來管理的變量,不需要程序員干預,動態管理。如果是局部變量,則由棧空間保存,全局變量則由數據段保存。

3.2、靜態變量和外部變量

這個是難點中的難點,大家一定要仔細看。

3.2.1、靜態局部變量

我們剛才說的局部變量,是自動局部變量,也就是說作用域和生存期是捆綁的,一旦出了作用域,則銷毀,不再可見。而靜態局部變量則不同。它和全局變量一樣,是放在數據段中的,只初始化一次,下次調用這個函數時,保留原來的值,繼續使用。如下:

int count() {
    static int cnt = 0;
    return ++cnt;
}

如果cnt是個自動局部變量,則每次調用函數的時候,都要初始化為0,所以每次的返回值都是1。但定義成靜態局部變量就不同了,它在函數調用結束后並不銷毀,保留原來的值,下一次調用時,初始化語句是不起作用的,所以它返回的是函數被調用的次數。

本質上,靜態局部變量和全局變量的生存期完全相同,只是作用域不同。剛才說了,作用域是相對於編譯器來說的,所以靜態局部變量編譯器提供的“語法糖”,為避免全局變量重名造成干擾而引入的機制。

3.2.2、外部變量和靜態全局變量

在講靜態全局變量之前,我們來先講一下外部變量。

我們剛才說的全局變量,僅僅是對本文件可見嗎?No,它對其他文件也可見。我們編譯C++程序的時候,往往不止一個源文件,如果1.cpp要調用2.cpp的某個全局變量,怎么辦呢?

答案就是,使用extern聲明。比如main.cpp的內容:

#include<stdio.h>
int a[20];
void operate(); // 函數聲明
int main() {
    operate();
    for (int i = 0; i < 20; i++) {
        printf("%d ",a[i]);
    }
}

operate.cpp中內容:

extern int a[20]; // extern聲明
void operate() {
    a[0] = 0;
    a[1] = 1;
    for (int i = 2; i < 20; i++) {
        a[i] = a[i-1] + a[i-2];
    }
}

編譯命令:g++ main.cpp operate.cpp -o main.exe

運行結果:0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

是不是很神奇?就像一個文件調用其他文件定義的函數一樣,其他文件的全局變量也是可以使用的,只不過需要聲明為extern。所以,外部變量是這樣一個概念。把它類比成函數聲明就行。

說完了外部變量,我們來說靜態全局變量。剛才說到,全局變量可以被其他文件所引用,如果我們不想讓它被其他文件引用,怎么辦?答案就是,把這個全局變量加上static修飾。這時,static已經不再是靜態的概念,而是阻止其它文件調用的意思。所以,靜態全局變量這個稱呼太過直譯,有點誤導。正確的叫法是文件內變量。

最初C++團隊想增加一個intern關鍵字來表示這種變量,但是遭到廣大程序員強烈反對,本着關鍵字能少即少的原則,就對static進行了“重載”,賦予了這么一個功能。其實我覺得還不如直接叫intern,static總是讓人誤會。

static也可以修飾函數,這表示,其他文件不可調用這個函數。如果把operate.cpp中的operate()定義成static void operate(),則編譯報錯。

3.3、寄存器變量

說完了最難的,咱們來放松一下。寄存器是CPU的一些存儲部件,它用來存放立即就要參加CPU運算的數據。它的讀寫速度是納秒級別,比內存、緩存都快至少2~3個數量級。

用register關鍵字聲明的變量,叫做寄存器變量。表示,通知編譯器把這個變量直接放在寄存器中而不是內存中,對一些頻繁訪問的變量,這樣可以加快速度。但是,僅僅是通知編譯器這樣做,而編譯器可能不會理會你的聲明,而仍然把變量放在內存中。因為寄存器個數非常少,我們PC機的64位x86 CPU,只有8個通用寄存器,其中還有兩個不是存放普通數據的。即使是寄存器較多的MIPS CPU,也只有32個。

另外需要注意的一點,就是寄存器變量不可以使用取地址運算符&,也不可以用指針指向它。因為寄存器沒有地址,指針只能保存內存地址值,而不能保存寄存器。

4、類中的static

4.1、靜態字段

類中的字段分為普通字段和靜態字段。普通字段在類對象創建時被創建,它的生存期和類對象是捆綁的,同生共死。而靜態字段,不依賴於對象創建,它被保存在數據段中。引用靜態字段,可以用成員運算符,也可以用作用域運算符。

class Point {
private:
    int x;
    int y;
public:
    static int cnt; // 其實這是不安全的,容易被篡改,應該設為private。但是作為一個例子來講解靜態字段
    Point(int xx, int yy) {
        cnt++; // 對當前擁有的對象個數計數
        x = xx;
        y = yy;
    }
    ~Point() {
        cnt--;
    }
};
static int Point::cnt = 0; // 靜態變量初始化必須在類外進行,除非它是const的

int main() {
    printf("%d ", Point::cnt); // 類名訪問
    Point p(3, 4);
    printf("%d ", p.cnt); // 對象名訪問
    Point *pp = new Point(5, 6);
    printf("%d ", pp->cnt); // 指針訪問
    delete pp;
    printf("%d ", Point::cnt);
    return 0;
}

4.2、靜態方法

靜態方法和普通方法不同,它也是所有類對象共享的。可以通過類名調用,也可以通過對象名調用。把剛才的類改一下:

class Point {
private:
    int x;
    int y;
    static int cnt;
public:
    Point(int xx, int yy) {
        cnt++; // 對當前擁有的對象個數計數
        x = xx;
        y = yy;
    }
    static int getCnt() {
        return cnt;
    }
    /*
    static int getX() {
        return x; 
    }
    */ // 這個函數是錯誤的,靜態方法不可引用非靜態字段
    ~Point() {
        cnt--;
    }
};
static int Point::cnt = 0;

int main() {
    printf("%d ", Point::getCnt()); // 類名訪問
    Point p(3, 4);
    printf("%d ", p.getCnt()); // 對象名訪問
    Point *pp = new Point(5, 6);
    printf("%d ", pp->getCnt()); // 指針訪問
    delete pp;
    printf("%d ", Point::getCnt());
    return 0;
}

注意,靜態方法不可訪問非靜態字段,也不可調用非靜態方法。

4.3、靜態內部類

內部類分為普通內部類和靜態內部類兩種。它們唯一的不同就是,普通內部類是外部類的“奴隸”,它不僅受制於外部類,而且一切字段不能對外部類隱藏,即使是private。而靜態內部類則不同,相當於放在某個類內部的外部類,private字段是不可訪問的。所以靜態內部類也叫嵌套類。和Java是一樣的。

比如:

class aaa {
private:
    class bbb {
    private:
        int a;
    };
    static class ccc {
    private:
        int a; // a對外不可見
    };
public:
    bbb bb;
    ccc cc;
    int bbbb() {
        return bb.a; // 正確
    }
    int cccc() {
        return cc.a; // 錯誤
    }
};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM