const 和 static 的作用


ps: 網上收錄的 講的不錯的兩篇文章 轉載如下:

static的作用:(http://www.cnblogs.com/Kane_zzt/archive/2009/05/18/1459697.html)

1.先來介紹它的第一條也是最重要的一條:隱藏

     當我們同時編譯多個文件時,所有未加static前綴的全局變量和函數都具有全局可見性。為理解這句話,我舉例來說明。我們要同時編譯兩個源文件,一個是a.c,另一個是main.c.

下面是a.c的內容:

 char a = 'A'; // global variable
            void msg()
            {
            printf("Hello\n");
            }
            

下面是main.c的內容:

 int main(void)
            {
            extern char a; // extern variable must be declared before use
            printf("%c ", a);
            (void)msg();
            return 0;
            }
            

程序的運行結果是:

A Hello

     你可能會問:為什么在a.c中定義的全局變量a和函數msg能在main.c中使用?前面說過,所有未加static前綴的全局變量和函數都具有全局可見性,其它的源文件也能訪問。此例中,a是全局變量,msg是函數,並且都沒有加static前綴,因此對於另外的源文件main.c是可見的。

如果加了static,就會對其它源文件隱藏。例如在a和msg的定義前加上static,main.c就看不到它們了。利用這一特性可以在不同的文件中定義同名函數和同名變量,而不必擔心命名沖突。Static可以用作函數和變量的前綴,對於函數來講,static的作用僅限於隱藏,而對於變量,static還有下面兩個作用。

2. static的第二個作用是保持變量內容的持久

     存儲在靜態數據區的變量會在程序剛開始運行時就完成初始化,也是唯一的一次初始化。共有兩種變量存儲在靜態存儲區:全局變量和static變量,只不過和全局變量比起來,static可以控制變量的可見范圍,說到底static還是用來隱藏的。雖然這種用法不常見,但我還是舉一個例子。

 #include <stdio.h>
            int fun(void){
            static int count = 10; // 事實上此賦值語句從來沒有執行過
            return count--;
            }
            int count = 1;
            int main(void)
            {
            printf("global\t\tlocal static\n");
            for(; count <= 10; ++count)
            printf("%d\t\t%d\n", count, fun());
            return 0;
            }

程序的運行結果是:

global local static
1  10
2   9
3   8
4   7
5   6
6   5
7   4
8   3
9   2
10  1

3. static的第三個作用是默認初始化為0.其實全局變量也具備這一屬性,因為全局變量也存儲在靜態數據區

     在靜態數據區,內存中所有的字節默認值都是0x00,某些時候這一特點可以減少程序員的工作量。比如初始化一個稀疏矩陣,我們可以一個一個地把所有元素都置0,然后把不是0的幾個元素賦值。如果定義成靜態的,就省去了一開始置0的操作。再比如要把一個字符數組當字符串來用,但又覺得每次在字符數組末尾加‘\0’太麻煩。如果把字符串定義成靜態的,就省去了這個麻煩,因為那里本來就是‘\0’。不妨做個小實驗驗證一下。

#include <stdio.h>
            int a;
            int main(void)
            {
            int i;
            static char str[10];
            printf("integer: %d; string: (begin)%s(end)", a, str);
            return 0;
            }
            

程序的運行結果如下integer: 0; string: (begin)(end)

最后對static的三條作用做一句話總結。首先static的最主要功能是隱藏,其次因為static變量存放在靜態存儲區,所以它具備持久性和默認值0.

4. 用static聲明的函數和變量小結

static 聲明的變量在C語言中有兩方面的特征:


  1)、變量會被放在程序的全局存儲區中,這樣可以在下一次調用的時候還可以保持原來的賦值。這一點是它與堆棧變量和堆變量的區別。
  2)、變量用static告知編譯器,自己僅僅在變量的作用范圍內可見。這一點是它與全局變量的區別。


Tips:
  A.若全局變量僅在單個C文件中訪問,則可以將這個變量修改為靜態全局變量,以降低模塊間的耦合度;
  B.若全局變量僅由單個函數訪問,則可以將這個變量改為該函數的靜態局部變量,以降低模塊間的耦合度;
  C.設計和使用訪問動態全局變量、靜態全局變量、靜態局部變量的函數時,需要考慮重入問題;
    D.
如果我們需要一個可重入的函數,那么,我們一定要避免函數中使用static變量(這樣的函數被稱為:帶內部存儲器功能的的函數)
      E.
函數中必須要使用static變量情況:比如當某函數的返回值為指針類型時,則必須是static的局部變量的地址作為返回值,若為auto類型,則返回為錯指針。

    函數前加static使得函數成為靜態函數。但此處“static”的含義不是指存儲方式,而是指對函數的作用域僅局限於本文件(所以又稱內部函數)。使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數,是否會與其它文件中的函數同名。

擴展分析:

      術語static有着不尋常的歷史.起初,在C中引入關鍵字static是為了表示退出一個塊后仍然存在的局部變量。隨后,staticC中有了第二種含義:用來表示不能被其它文件訪問的全局變量和函數。為了避免引入新的關鍵字,所以仍使用static關鍵字來表示這第二種含義。最后,C++重用了這個關鍵字,並賦予它與前面不同的第三種含義:表示屬於一個類而不是屬於此類的任何特定對象的變量和函數(Java中此關鍵字的含義相同)

全局變量、靜態全局變量、靜態局部變量和局部變量的區別

變量可以分為:全局變量、靜態全局變量、靜態局部變量和局部變量。


          存儲區域分,全局變量、靜態全局變量和靜態局部變量都存放在內存的靜態存儲區域,局部變量存放在內存的棧區
          
作用域分,  全局變量在整個工程文件內都有效;靜態全局變量只在定義它的文件內有效;靜態局部變量只在定義它的函數內有效,只是程序僅分配一次內存,函數返回后,該變量不會消失;局部變量在定義它的函數內有效,但是函數返回后失效

    全局變量(外部變量)的說明之前再冠以static 就構成了靜態的全局變量。全局變量本身就是靜態存儲方式, 靜態全局變量當然也是靜態存儲方式。 這兩者在存儲方式上並無不同。這兩者的區別雖在於非靜態全局變量的作用域是整個源程序,當一個源程序由多個源文件組成時,非靜態的全局變量在各個源文件中都是有效的。 而靜態全局變量則限制了其作用域,即只在定義該變量的源文件內有效, 在同一源程序的其它源文件中不能使用它。由於靜態全局變量的作用域局限於一個源文件內,只能為該源文件內的函數公用, 因此可以避免在其它源文件中引起錯誤。

  從以上分析可以看出, 把局部變量改變為靜態變量后是改變了它的存儲方式即改變了它的生存期。把全局變量改變為靜態變量后是改變了它的作用域 限制了它的使用范圍。

  static 函數與普通函數作用域不同。僅在本文件。只在當前源文件中使用的函數應該說明為內部函數(static),內部函數應該在當前源文件中說明和定義。對於可在當前源文件以外使用的函數,應該在一個頭文件中說明,要使用這些函數的源文件要包含這個頭文件

  static全局變量與普通的全局變量有什么區別:static全局變量只初始化一次,防止在其他文件單元中被引用;
  static局部變量和普通局部變量有什么區別:static局部變量只被初始化一次,下一次依據上一次結果值;

     static
函數與普通函數有什么區別:static函數在內存中只有一份,普通函數在每個被調用中維持一份拷貝
     
全局變量和靜態變量如果沒有手工初始化,則由編譯器初始化為0。局部變量的值不可知。

Cosnt的作用:(http://www.cnblogs.com/Kane_zzt/archive/2009/05/18/1459697.html)

const給人的第一印象就是定義常量。

(1)const用於定義常量。

     例如:const int N = 100;const int M = 200;
     這樣程序中只要用到 N、M 就分別代表為整型100、200,N、M 為一常量,在程序中不可改變。
     但有人說他編程時從來不用const定義常量。我相信。但他是不懂得真正的編程藝術,用const定義常量不僅能方便我們編程而且能提高程序的清晰性。你是願意看到程序中100、200 滿天飛,還是願意只看到簡單清晰的N、M。相信有沒有好處你慢慢體會。
     還有人說他不用const定義常量,他用#define宏定義常量。可以。但不知道你有沒有發現有時#define宏並沒有如你所願在定義常量。下面我們比較比較const和#define。
     1。
     (a) const定義常量是有數據類型的:
     這樣const定義的常量編譯器可以對其進行數據靜態類型安全檢查,而#define宏定義的常量卻只是進行簡單的字符替換,沒有類型安全檢查,且有時還會產生邊際效應(不如你願處)。所謂邊際效應舉例如下:
           #define N 100
           #define M 200 + N
           當程序中使用 M*N 時,原本想要 100 * (200+ N )的卻變成了 100 * 200 + N。
     (b)#define宏定義常量卻沒有。#define <宏名><字符串>,字符串可以是常數、表達式、格式串等。在程序被編譯的時候,如果遇到宏名就喲內指定的字符串進行替換,然后再進行編譯。
     2。
     有些調試程序可對const進行調試,但不對#define進行調試。
     3。
     當定義局部變量時,const作用域僅限於定義局部變量的函數體內。但用#define時其作用域不僅限於定義局部變量的函數體內,而是從定義點到整個程序的結束點。但也可以用#undef取消其定義從而限定其作用域范圍。只用const定義常量,並不能起到其強大的作用。const還可修飾函數形式參數、返回值和類的成員函數等。從而提高函數的健壯性。因為const修飾的東西能受到c/c++的靜態類型安全檢查機制的強制保護,防止意外的修改。

(2)const修飾函數形式參數

     形式參數有輸入形式參數和輸出形式參數。參數用於輸出時不能加const修飾,那樣會使函數失去輸出功能。因為const修飾的東西是不能改變的。
     const只能用於修飾輸入參數。
     談const只能用於修飾輸入參數之前先談談C++函數的三種傳遞方式。
     C++函數的三種傳遞方式為:值傳遞、指針傳遞和引用傳遞。簡單舉例說明之,詳細說明請參考別的資料。
     值傳遞:
       void fun(int x){
             x += 5;       //修改的只是y在棧中copy x,x只是y的一個副本,在內存中重新開辟的一塊臨時空間把y的值 送給了x;這樣也增加了程序運行的時間,降低了程序的效率。
       }
       void main(void){
             int y = 0;
             fun(y);
             cout<</"y = /"<<y<<endl;  //y = 0;
       }
     指針傳遞:
        void fun(int *x){
             *x += 5;      //修改的是指針x指向的內存單元值
        }
        void main(void){
             int y = 0;
             fun(&y);
             cout<<<</"y = /"<<y<<endl;  //y = 5;
        }
      引用傳遞:
         void fun(int &x){
             x += 5;      //修改的是x引用的對象值 &x = y;
        }
        void main(void){
             int y = 0;
             fun(y);
             cout<<<</"y = /"<<y<<endl;  //y = 5;
        }
      看了傳遞方式后我們繼續來談“const只能用於修飾輸入參數”的情況。

         當輸入參數用“值傳遞”方式時,我們不需要加const修飾,因為用值傳遞時,函數將自動用實際參數的拷貝初始化形式參數,當在函數體內改變形式參數時,改變的也只是棧上的拷貝而不是實際參數。
         但要注意的是,當輸入參數為ADT/UDT(用戶自定義類型和抽象數據類型)時,應該將“值傳遞”改為“const &傳遞”,目的可以提高效率。
         例如:
            void fun(A a);//效率底。函數體內產生A類型的臨時對象用於復制參數 a,但是臨時對象的
                          //構造、復制、析構過程都將消耗時間。
            void fun(A const &a);//提高效率。用“引用傳遞”不需要產生臨時對象,省了臨時對象的
                                 //構造、復制、析構過程消耗的時間。但光用引用有可能改變a,所以加const


         當輸入參數用“指針傳遞”方式時,加const修飾可防止意外修改指針指向的內存單元,起到保護作用。
         例如:
            void funstrcopy(char *strdest,const char *strsrc)//任何改變strsrc指向的內存單元,
                                                             //編譯器都將報錯
            些時保護了指針的內存單元,也可以保護指針本身,防止其地址改變。
         例如:
           void funstrcopy(char *strdest,const char *const strsrc)

(3)const修飾函數的返回值

     如給“指針傳遞”的函數返回值加const,則返回值不能被直接修改,且該返回值只能被賦值給加const修飾的同類型指針。
     例如:
        const char *GetChar(void){};
      賦值 char *ch = GetChar();//錯誤    const char *ch = GetChar();//正確

(4)const修飾類的成員函數(函數定義體)

     任何不會修改數據成員的函數都應用const修飾,這樣當不小心修改了數據成員或調用了非const成員函數時,編譯器都會報錯。
     const修飾類的成員函數形式為:int GetCount(void)  const;
(5)用傳引用給const取代傳值
缺省情況下,C++ 以傳值方式將對象傳入或傳出函數(這是一個從 C 繼承來的特性)。除非你特別指定其它方式,否則函數的參數就會以實際參數(actual argument)的拷貝進行初始化,而函數的調用者會收到函數返回值的一個拷貝。這個拷貝由對象的拷貝構造函數生成。這就使得傳值(pass-by-value)成為一個代價不菲的操作。例如,考慮下面這個類層級結構:

class Person {
 public:
  Person(); // parameters omitted for simplicity
  virtual ~Person(); // see Item 7 for why this is virtual
  ...

 private:
  std::string name;
  std::string address;
};

class Student: public Person {
 public:
  Student(); // parameters again omitted
  ~Student();
  ...

 private:
  std::string schoolName;
  std::string schoolAddress;
};

  現在,考慮以下代碼,在此我們調用一個函數—— validateStudent,它得到一個 Student 參數(以傳值的方式),並返回它是否驗證有效的結果:

bool validateStudent(Student s); // function taking a Student
// by value

Student plato; // Plato studied under Socrates

bool platoIsOK = validateStudent(plato); // call the function

  當這個函數被調用時會發生什么呢?

  很明顯,Student 的拷貝構造函數被調用,用 plato 來初始化參數 s。同樣明顯的是,當 validateStudent 返回時,s 就會被銷毀。所以這個函數的參數傳遞代價是一次 Student 的拷貝構造函數的調用和一次 Student 的析構函數的調用。

  但這還不是全部。一個 Student 對象內部包含兩個 string 對象,所以每次你構造一個 Student 對象的時候,你也必須構造兩個 string 對象。一個 Student 對象還要從一個 Person 對象繼承,所以每次你構造一個 Student 對象的時候,你也必須構造一個 Person 對象。一個 Person 對象內部又包含兩個額外的 string 對象,所以每個 Person 的構造也承擔着另外兩個 string 的構造。最終,以傳值方式傳遞一個 Student 對象的后果就是引起一次 Student 的拷貝構造函數的調用,一次 Person 的拷貝構造函數的調用,以及四次 string 的拷貝構造函數調用。當 Student 對象的拷貝被銷毀時,每一個構造函數的調用都對應一個析構函數的調用,所以以傳值方式傳遞一個 Student 的全部代價是六個構造函數和六個析構函數!

  好了,這是正確的和值得的行為。畢竟,你希望你的全部對象都得到可靠的初始化和銷毀。盡管如此,如果有一種辦法可以繞過所有這些構造和析構過程,應該變得更好,這就是:傳引用給 const(pass by reference-to-const):

bool validateStudent(const Student& s);

  這樣做非常有效:沒有任何構造函數和析構函數被調用,因為沒有新的對象被構造。被修改的參數聲明中的 const 是非常重要的。 validateStudent 的最初版本接受一個 Student 值參數,所以調用者知道它們屏蔽了函數對它們傳入的 Student 的任何可能的改變;validateStudent 也只能改變它的一個拷貝。現在 Student 以引用方式傳遞,同時將它聲明為 const 是必要的,否則調用者必然擔心 validateStudent 改變了它們傳入的 Student。

  以傳引用方式傳遞參數還可以避免切斷問題(slicing problem)。當一個派生類對象作為一個基類對象被傳遞(傳值方式),基類的拷貝構造函數被調用,而那些使得對象的行為像一個派生類對象的特殊特性被“切斷”了。你只剩下一個純粹的基類對象——這沒什么可吃驚的,因為是一個基類的構造函數創建了它。這幾乎絕不是你希望的。例如,假設你在一組實現一個圖形窗口系統的類上工作:

class Window {
 public:
  ...
  std::string name() const; // return name of window
  virtual void display() const; // draw window and contents
};

class WindowWithScrollBars: public Window {
 public:
  ...
  virtual void display() const;
};

  所有 Window 對象都有一個名字,你能通過 name 函數得到它,而且所有的窗口都可以顯示,你可一個通過調用 display 函數來做到這一點。display 為 virtual 的事實清楚地告訴你:一個純粹的基類的 Window 對象的顯示方法有可能不同於專門的 WindowWithScrollBars 對象的顯示方法。

  現在,假設你想寫一個函數打印出一個窗口的名字,並隨后顯示這個窗口。以下這個函數的寫法是錯誤的:

void printNameAndDisplay(Window w) // incorrect! parameter

 // may be sliced!
 std::cout << w.name();
 w.display();
}

  考慮當你用一個 WindowWithScrollBars 對象調用這個函數時會發生什么:

WindowWithScrollBars wwsb;

printNameAndDisplay(wwsb);

  參數 w 將被作為一個 Window 對象構造——它是被傳值的,記得嗎?而且使 wwsb 表現得像一個 WindowWithScrollBars 對象的特殊信息都被切斷了。在 printNameAndDisplay 中,全然不顧傳遞給函數的那個對象的類型,w 將始終表現得像一個 Window 類的對象(因為它就是一個 Window 類的對象)。特別是,在 printNameAndDisplay 中調用 display 將總是調用 Window::display,絕不會是 WindowWithScrollBars::display。

  繞過切斷問題的方法就是以傳引用給 const 的方式傳遞 w:

void printNameAndDisplay(const Window& w) // fine, parameter won’t
{
 // be sliced
 std::cout << w.name();
 w.display();
}

  現在 w 將表現得像實際傳入的那種窗口。

  如果你掀開編譯器的蓋頭偷看一下,你會發現用指針實現引用是非常典型的做法,所以以引用傳遞某物實際上通常意味着傳遞一個指針。由此可以得出結論,如果你有一個內建類型的對象(例如,一個 int),以傳值方式傳遞它常常比傳引用方式更高效。那么,對於內建類型,當你需要在傳值和傳引用給 const 之間做一個選擇時,沒有道理不選擇傳值。同樣的建議也適用於 STL 中的迭代器(iterators)和函數對象(function objects),因為,作為慣例,它們就是為傳值設計的。迭代器(iterators)和函數對象(function objects)的實現有責任保證拷貝的高效並且不受切斷問題的影響。(這是一個“規則如何變化,依賴於你使用 C++ 的哪一個部分”的實例。)

  內建類型很小,所以有人就斷定所有的小類型都是傳值的上等候選者,即使它們是用戶定義的。這樣的推論是不可靠的。僅僅因為一個對象小,並不意味着調用它的拷貝構造函數就是廉價的。很多對象——大多數 STL 容器也在其中——容納的和指針一樣,但是拷貝這樣的對象必須同時拷貝它們指向的每一樣東西。那可能是非常昂貴的。

  即使當一個小對象有一個廉價的拷貝構造函數,也會存在性能問題。一些編譯器對內建類型和用戶定義類型並不一視同仁,即使他們有同樣的底層表示。例如,一些編譯器拒絕將僅由一個 double 組成的對象放入一個寄存器中,即使在常規上它們非常願意將一個純粹的 double 放入那里。如果發生了這種事情,你以傳引用方式傳遞這樣的對象更好一些,因為編譯器理所當然會將一個指針(引用的實現)放入寄存器。

  小的用戶定義類型不一定是傳值的上等候選者的另一個原因是:作為用戶定義類型,它的大小常常變化。一個現在較小的類型在將來版本中可能變得更大,因為它的內部實現可能會變化。甚至當你換了一個不同的 C++ 實現時,事情都可能會變化。例如,就在我這樣寫的時候,一些標准庫的 string 類型的實現的大小就是另外一些實現的七倍。

  通常情況下,你能合理地假設傳值廉價的類型僅有內建類型及 STL 中的迭代器和函數對象類型。對其他任何類型,請遵循本 Item 的建議,並用傳引用給 const 取代傳值。

  Things to Remember

  ·用傳引用給 const 取代傳值。典型情況下它更高效而且可以避免切斷問題。

  ·這條規則並不適用於內建類型及 STL 中的迭代器和函數對象類型。對於它們,傳值通常更合適。
   


免責聲明!

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



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