C++ 中的static關鍵字

        static顧名思義是靜態的意思。在這我要系統地講述一下有關static關鍵字的作用,當然主要是講述它在開發語言C與C++的作用,在其他方面的作用請另找資料。在講解中肯定有不恰當之處,請大家大膽地扔磚,不要手軟,文中的內容引用了不少網上的資料。

        static從宏觀上講主要有兩種用法,一、是面向過程設計;二是面向對象設計。前主要是涉及變量與函數的用法;後者呢包含前者用法的同時,還有在類中的用法。

一、 面向過程設計中的static(C語言)

在講面向過程設計中的static用法時先搞點插曲,從歷史上講,C程序一直由下面幾部分組成:


正文段

        CPU執行的機器指令部分。通常,正文段是可共享的,所以即使是經常環境指針環境表環境字符串執行的程序(如文本編輯程序、C編譯程序、s h e l l等)在存儲器中也只需有一個副本,另外,正文段常常是隻讀的,以防止程序由於意外事故而修改其自身的指令。

初始化數據段

        通常將此段稱爲數據段,它包含了程序中需賦初值的變量。初始化 的全局變量和 靜態變量存放在這裏。例如,C程序中任何函數之外的說明:int maxcount = 99; 使此變量以初值存放在初始化數據段中。

a.初始化的全局變量

b.初始化的靜態變量

非初始化數據段

       通常將此段稱爲bss段,這一名稱來源於早期彙編程序的一個操作符,意思是“block started by symbol(由符號開始的塊)”,未初始化的全局變量 和靜態變量存放在這裏。在程序開始執行之前,內核將此段初始化爲0。函數外的說明:long sum[1000] ; 使此變量存放在非初始化數據段中。

a.未初始化的全局變量

b.未初始化的靜態變量

        需要由程序員分配釋放管理,若程序員不釋放,程序結束時可能由OS回收。通常在堆中進行動態存儲分配。如程序中的malloc, calloc,realloc等函數都從這裏面分配。堆是從下向上分配的。

        由編譯器自動分配釋放管 理。局部變量及每次函數調用時返回地址、以及調用者的環境信息(例如某些機器寄存器)都存放在棧中。新被調用的函數在棧上爲其自動和臨時變量分配存儲空間。通過以這種方式使用棧,C函數可以遞歸調用。遞歸函數每次調用自身時,就使用一個新的棧幀,因此一個函數調用實例中的變量絕不會影響另一個函數調用實例中的變量。

a.局部變量

b.函數調用時返回地址

c.調用者的環境信息(例如某些機器寄存器)

       在這我先把static的內部機制與優勢先提到前面來講述,本來想放到面向對象設計中來講,但是它的重要性改變了初衷:

static的內部機制:

        靜態數據成員要在程序一開始運行時就必須存在。因爲函數在程序運行中被調用,所以靜態數據成員不能在任何函數內分配空間和初始化。

        這樣,它的空間分配有三個可能的地方,一是作爲類的外部接口的頭文件,那裏有類聲明;二是類定義的內部實現,那裏有類的成員函數定義;三是應用程序的main()函數前的全局數據聲明和定義處。

        靜態數據成員要實際地分配空間,故不能在類的聲明中定義(只能聲明數據成員)。類聲明只聲明一個類的“尺寸和規格”,並不進行實際的內存分配,所以在類聲明中寫成定義是錯誤的。它也不能在頭文件中類聲明的外部定義,因爲那會造成在多個使用該類的源文件中,對其重複定義。

        static被引入以告知編譯器,將變量存儲在程序的靜態存儲區而非棧上空間,靜態數據成員按定義出現的先後順序依次初始化,注意靜態成員嵌套時,要保證所嵌套的成員已經初始化了。消除時的順序是初始化的反順序。

static的優勢:

        可以節省內存,因爲它是所有對象所公有的,因此,對多個對象來說,靜態數據成員只存儲一處,供所有對象共用。靜態數據成員的值對每個對象都是一樣,但它的值是可以更新的。只要對靜態數據成員的值更新一次,保證所有對象存取更新後的相同的值,這樣可以提高時間效率。

static局部變量

         靜態局部變量屬於靜態存儲方式,它具有以下特點:

        (1)靜態局部變量 在函數內定義它的生存期 整個程序生命週期,但是其 作用域仍與 自動變量相同 ,只能在定義該變量的函數內使用該變量。退出該函數後,儘管該變量還繼續存在,但不能使用它。

       (2)對基本類型的靜態局部變量若在聲明時未賦以初值,則系統自動賦予0值 。而對自動變量不賦初值,則其值是不定的。

        根據靜態局部變量的特點,可以看出它是一種生存期爲整個程序生命週期。雖然離開定義它的函數後不能使用,但如再次調用定義它的函數時,它又可繼續使用,而且保存了前次被調用後留下的值。因此,當多次調用一個函數且要求在調用之間保留某些變量的值時,可考慮採用靜態局部變量。雖然用全局變量也可以達到上述目的,但全局變量有時會造成意外的副作用,因此仍以採用局部靜態變量爲宜。

#include <stdio.h>
#include <stdlib.h>
 
void Test()
{
    static int tmpValue = 0;
    printf("value = %d\n", ++tmpValue);
}
 
int main()
{
    for(int index = 0; index < 50; ++index)
    {
        Test()
    }
    getchar();
    return 0;
}

全局變量

       全局變量(外部變量)的說明之前再冠以static 就構成了靜態的全局變量。全局變量本身就是靜態存儲方式,靜態全局變量當然也是靜態存儲方式。 這兩者在存儲方式上並無不同。
這兩者的區別在於:

(1). 非靜態全局變量作用域是整個源程序 ,當一個源程序由多個源文件組成時,非靜態的全局變量在各個源文件中都是有效的。

(2). 而靜態全局變量 則限制了其作用域,即只在定義該變量的源文件內有效,在同一源程序的其它源文件中不能使用它。由於靜態全局變量的作用域侷限於 一個源文件內 ,只能爲該源文件內的函數公用, 因此可以避免在其它源文件中引起錯誤。

從以上分析可以看出,把全局變量改變爲靜態變量後是改變了它的作用域,限制了它的使用範圍

static 函數

         如果在一個源文件中定義的函數,只能被本文件中的函數調用,而不能被同一程序其它文件中的函數調用,這種函數稱爲static函數與稱爲靜態函數。
定義一個static函數,只需在函數類型前再加一個“static”關鍵字即可,如下所示:

static 函數類型 函數名(函數參數表) {……}

關鍵字“static”,譯成中文就是“靜態的”,所以內部函數又稱靜態函數。但此處“static”的含義不是指存儲方式,而是指對函數的作用域僅侷限於本文件

使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數,是否會與其它文件中的函數同名,因爲同名也沒有關係

 

二、面向對象設計中的static(C++語言)

static 數據成員

在類內數據成員的聲明前加上關鍵字static,該數據成員就是類內的靜態數據成員。先舉一個靜態數據成員的例子。

#include <stdio.h>
#include <stdlib.h>
 
class Test
{
public:
    Test() {}
    ~Test() {}
   
    Test(const Test& t)
    {
        if(this != &t)
        {
            this->m_nTest1 = t.m_nTest1;
            this->m_nTest2 = t.m_nTest2;
            this->m_nTest3 = t.m_nTest3;
        }
    }
   
    Test& operator=(const Test& t)
    {
        if(this != &t)
        {
            this->m_nTest1 = t.m_nTest1;
            this->m_nTest2 = t.m_nTest2;
            this->m_nTest3 = t.m_nTest3;
        }
        return *this;
    }
   
    void PrintOut()
    {
        //printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nSTest1, m_nSTest2, m_nSTest3);//錯誤
        printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nTest1, m_nTest2, m_nTest3);
    }
   
    static void PrintStatic()
    {
        printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nSTest1, m_nSTest2, m_nSTest3);
        //printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nTest1, m_nTest2, m_nTest3);//錯誤
    }
 
public:
    int m_nTest1;
    static int m_nSTest1;
     
protect:
    int m_nTest2;
    static int m_nSTest2;
   
private:
    int m_nTest3;
    static int m_nSTest3;
};
int Test::m_nSTest1 = 10;
int Test::m_nSTest2 = 20;
int Test::m_nSTest3 = 30;
 
 
int Test::m_nSTest1 = 10;
int main()
{
    Test t;
    t.PrintOut();
    //t.PrintStatic();//錯誤
    Test::PrintStatic();
    getchar();
    return 0;
}


static數據成員有以下特點:

       (1). 對於非static數據成員,每個類對象都有自己的拷貝。而static數據成員被當作是類的成員。無論這個類的對象被定義了多少個,靜態數據成員在程序中也只有一份拷貝 ,由該類型的所有對象共享訪問。也就是說,靜態數據成員是該類的所有對象所共有的。對該類的多個對象來說,靜態數據成員只分配一次內存,供所有對象共用。

       (2). 靜態數據成員存儲在全局數據區。靜態數據成員定義時才分配空間,所以不能在類聲明中定義。在上例中,語句int Test::m_nSTest1= 10;是定義靜態數據成員;

       (3). 靜態數據成員和普通數據成員一樣遵從public,protected,private訪問規則;

       (4). 因爲靜態數據成員在全局數據區分配內存,屬於本類的所有對象共享,所以,它不屬於特定的類對象,在沒有產生類對象時其作用域就可見,即在沒有產生類的實例時,我們就可以操作它;

       (5). 靜態數據成員初始化與一般數據成員初始化不同。靜態數據成員初始化的格式爲:<數據類型><類名>::<靜態數據成員名>=<值> 如:int Test::m_nSTest1 = 10;

        (6). 類的靜態數據成員有兩種訪問形式:<類對象名>.<靜態數據成員名> 或 <類類型名>::<靜態數據成員名>

        (7). 靜態數據成員主要用在各個對象都有相同的某項屬性的時候。比如對於一個存款類,每個實例的利息都是相同的。所以,應該把利息設爲存款類的靜態數據成員。這有兩個好處,第一,不管定義多少個存款類對象,利息數據成員都共享分配在全局數據區的內存,所以節省存儲空間。第二,一旦利息需要改變時,只要改變一次,則所有存款類對象的利息全改變過來了;

        (8). 同全局變量相比,使用靜態數據成員有兩個優勢:

a. 靜態數據成員沒有進入程序的全局名字空間,因此不存在與程序中其它全局名字衝突的可能性;

b. 可以實現信息隱藏。靜態數據成員可以是private成員,而全局變量不能;

static成員函數

#include <stdio.h>
#include <stdlib.h>
 
class Test
{
public:
    Test() {}
    ~Test() {}
   
    Test(const Test& t)
    {
        if(this != &t)
        {
            this->m_nTest1 = t.m_nTest1;
            this->m_nTest2 = t.m_nTest2;
            this->m_nTest3 = t.m_nTest3;
        }
    }
   
    Test& operator=(const Test& t)
    {
        if(this != &t)
        {
            this->m_nTest1 = t.m_nTest1;
            this->m_nTest2 = t.m_nTest2;
            this->m_nTest3 = t.m_nTest3;
        }
        return *this;
    }
   
    void PrintOut()
    {
        //printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nSTest1, m_nSTest2, m_nSTest3);//錯誤
        printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nTest1, m_nTest2, m_nTest3);
    }
   
    static void PrintStatic()
    {
        printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nSTest1, m_nSTest2, m_nSTest3);
        //printf("test_1 = %d test_2 = %d test_3 = %d\n",m_nTest1, m_nTest2, m_nTest3);//錯誤
    }
 
public:
    int m_nTest1;
    static int m_nSTest1;
     
protect:
    int m_nTest2;
    static int m_nSTest2;
   
private:
    int m_nTest3;
    static int m_nSTest3;
};
int Test::m_nSTest1 = 10;
int Test::m_nSTest2 = 20;
int Test::m_nSTest3 = 30;
 
 
int Test::m_nSTest1 = 10;
int main()
{
    Test t;
    t.PrintOut();
    //t.PrintStatic();//錯誤
    Test::PrintStatic();
    getchar();
    return 0;
}

       static 成員函數,它爲類的全部服務而不是爲某一個類的具體對象服務。普通的成員函數一般都隱含了一個this指針,因爲普通成員函數總是具體的屬於某個類的具體對象的。通常情況下,this是缺省的。如函數fn()實際上是this->fn()。但是與普通函數相比,靜態成員函數由於不是與任何的對象相聯繫,因此它不具有this指針 。從這個意義上講,它無法訪問屬於類對象的no-static數據成員,也無法訪問no-static成員函數,它只能調用其餘的靜態成員函數 。

關於靜態成員函數,可以總結爲以下幾點:

(1). 出現在類體外的函數定義不能指定關鍵字static

(2). static成員之間可以相互訪問 ,包括static成員函數訪問static數據成員和訪問static成員函數;

(3). 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員;

(4). 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員,只能訪問靜態的

(5). 由於沒有this指針的額外開銷,因此靜態成員函數與類的全局函數相比速度上會有少許的增長;

(6). 調用靜態成員函數,可以用成員訪問操作符(.)和(->)爲一個類的對象或指向類對象的指針調用靜態成員函數,也可以直接使用如下格式:

<類名>::<靜態成員函數名>(<參數表>)

如:Test::PrintStatic(),調用類的靜態成員函數。

但是,一樣要遵從public,protected,private訪問規則。


附 public,protected,private訪問規則

第一:private, public, protected訪問標號的訪問範圍。

private:只能由1.該類中的函數、2.其友元函數訪問。

不能被任何其他訪問,該類的對象也不能訪問。

protected:可以被1.該類中的函數、2.子類的函數、以及3.其友元函數訪問。

但不能被該類的對象訪問。

public:可以被1.該類中的函數、2.子類的函數、3.其友元函數訪問,也可以由4.該類的對象訪問。

 

注:友元函數包括3種:設爲友元的普通的非成員函數;設爲友元的其他類的成員函數;設爲友元類中的所有成員函數。

第二:類的繼承後方法屬性變化。

private屬性不能夠被繼承。

使用private繼承,父類的protected和public屬性在子類中變爲private;
使用protected繼承,父類的protected和public屬性在子類中變爲protected;
使用public繼承,父類中的protected和public屬性不發生改變;

如下所示:

                     public:           protected:       private:

public繼承            public            protected       不可用 

protected繼承         protected         protected       不可用 

private繼承           private          private          不可用

protected繼承和private繼承能降低訪問權限。

爲了進一步理解三種不同的繼續方式在其成員的可見性方面的區別,下面從三種不同角度進行討論。
  對於公有繼續方式:
  (1) 基類成員對其對象的可見性:
  公有成員可見,其他不可見。這裏保護成員同於私有成員。
  (2) 基類成員對派生類的可見性:
  公有成員和保護成員可見,而私有成員不可見。這裏保護成員同於公有成員。
  (3) 基類成員對派生類對象的可見性:
  公有成員可見,其他成員不可見。
  所以,在公有繼續時,派生類的對象可以訪問基類中的公有成員;派生類的成員函數可以訪問基類中的公有成員和保護成員。這裏,一定要區分清楚派生類的對象和派生類中的成員函數對基類的訪問是不同的。
  對於私有繼續方式:
  (1) 基類成員對其對象的可見性:
  公有成員可見,其他成員不可見。
  (2) 基類成員對派生類的可見性:
  公有成員和保護成員是可見的,而私有成員是不可見的。
  (3) 基類成員對派生類對象的可見性:
  所有成員都是不可見的。
  所以,在私有繼續時,基類的成員只能由直接派生類訪問,而無法再往下繼續。
  對於保護繼續方式:
  這種繼續方式與私有繼續方式的情況相同。兩者的區別僅在於對派生類的成員而言,對基類成員有不同的可見性。
  上述所說的可見性也就是可訪問性。關於可訪問性還有另的一種說法。這種規則中,稱派生類的對象對基類訪問爲水平訪問,稱派生類的派生類對基類的訪問爲垂直訪問。
  一般規則如下:
  公有繼續時,水平訪問和垂直訪問對基類中的公有成員不受限制;
  私有繼續時,水平訪問和垂直訪問對基類中的公有成員也不能訪問;
  保護繼續時,對於垂直訪問同於公有繼續,對於水平訪問同於私有繼續。
  對於基類中的私有成員,只能被基類中的成員函數和友元函數所訪問,不能被其他的函數訪問。
  基類與派生類的關係
  任何一個類都可以派生出一個新類,派生類也可以再派生出新類,因此,基類和派生類是相對而言的。
  基類與派生類之間的關係可以有如下幾種描述:
  1. 派生類是基類的具體化
  類的層次通常反映了客觀世界中某種真實的模型。在這種情況下,不難看出:基類是對若干個派生類的抽象,而派生類是基類的具體化。基類抽取了它的派生類的公共特徵,而派生類通過增加行爲將抽象類變爲某種有用的類型。
  2. 派生類是基類定義的延續
  先定義一個抽象基類,該基類中有些操作並未實現。然後定義非抽象的派生類,實現抽象基類中定義的操作。例如,虛函數就屬此類情況。這時,派生類是抽象的基類的實現,即可看成是基類定義的延續。這也是派生類的一種常用方法。
  3. 派生類是基類的組合
  在多繼續時,一個派生類有多於一個的基類,這時派生類將是所有基類行爲的組合。
  派生類將其本身與基類區別開來的方法是添加數據成員和成員函數。因此,繼續的機制將使得在創建新類時,只需說明新類與已有類的區別,從而大量原有的程序代碼都可以複用,所以有人稱類是“可複用的軟件構件”。


發佈了109 篇原創文章 · 獲贊 29 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章