1. 程式人生 > >C++ 全域性變數 靜態全域性變數 傻傻分不清

C++ 全域性變數 靜態全域性變數 傻傻分不清

今天上午寫C++程式碼,被神馬全域性變數和靜態全域性變數困住了,出現了各種“重定義”和“連結”相關的error。到底在哪裡宣告,在哪裡定義,哪裡使用extern?傻傻分不清~ 本來我想自己總結一下的,後來看到網上的總結太詳細了,我就直接轉載一下,以下文章轉自網站“指令碼之家”,看這篇文章還順帶複習編譯原理呢~

(1)編譯單元(模組) 
在VC或VS上編寫完程式碼,點選編譯按鈕準備生成exe檔案時,編譯器做了兩步工作:

  1. 第一步,將每個.cpp(.c)和相應的.h檔案編譯成obj檔案;
  2. 第二步,將工程中所有的obj檔案進行LINK,生成最終.exe檔案。

那麼,錯誤可能在兩個地方產生:

  1. 一個,編譯時的錯誤,這個主要是語法錯誤;
  2. 一個,連結時的錯誤,主要是重複定義變數等。

編譯單元指在編譯階段生成的每個obj檔案。 
一個obj檔案就是一個編譯單元。 
一個.cpp(.c)和它相應的.h檔案共同組成了一個編譯單元。 
一個工程由很多編譯單元組成,每個obj檔案裡包含了變數儲存的相對地址等。

(2)宣告與定義 
函式或變數在宣告時,並沒有給它實際的實體記憶體空間,它有時候可保證你的程式編譯通過;函式或變數在定義時,它就在記憶體中有了實際的物理空間。

如果你在編譯單元中引用的外部變數沒有在整個工程中任何一個地方定義的話,那麼即使它在編譯時可以通過,在連線時也會報錯,因為程式在記憶體中找不到這個變數。函式或變數可以宣告多次,但定義只能有一次。

(3) extern作用 
作用一:當它與”C”一起連用時,如extern “C” void fun(int a, int b);,則編譯器在編譯fun這個函式名時按C的規則去翻譯相應的函式名而不是C++的。

作用二:當它不與”C”在一起修飾變數或函式時,如在標頭檔案中,extern int g_nNum;,它的作用就是宣告函式或變數的作用範圍的關鍵字,其宣告的函式和變數可以在本編譯單元或其他編譯單元中使用。

即B編譯單元要引用A編譯單元中定義的全域性變數或函式時,B編譯單元只要包含A編譯單元的標頭檔案即可,在編譯階段,B編譯單元雖然找不到該函式或變數,但它不會報錯,它會在連結時從A編譯單元生成的目的碼中找到此函式。

(4)全域性變數(extern) 
有兩個類都需要使用共同的變數,我們將這些變數定義為全域性變數。比如,res.h和res.cpp分別來宣告和定義全域性變數,類ProducerThread和ConsumerThread來使用全域性變數。(以下是QT工程程式碼) 
複製程式碼 程式碼如下:

/**********res.h宣告全域性變數************/  
#pragma once  

#include <QSemaphore>  

const int g_nDataSize = 1000; // 生產者生產的總資料量  
const int g_nBufferSize = 500; // 環形緩衝區的大小  

extern char g_szBuffer[]; // 環形緩衝區  
extern QSemaphore g_qsemFreeBytes; // 控制環形緩衝區的空閒區(指生產者還沒填充資料的區域,或者消費者已經讀取過的區域)  
extern QSemaphore g_qsemUsedBytes; // 控制環形緩衝區中的使用區(指生產者已填充資料,但消費者沒有讀取的區域)  
/**************************/  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上述程式碼中g_nDataSize、g_nBufferSize為全域性常量,其他為全域性變數。 
複製程式碼 程式碼如下:

/**********res.cpp定義全域性變數************/  
#pragma once  
#include "res.h"  

// 定義全域性變數  
char g_szBuffer[g_nBufferSize];  
QSemaphore g_qsemFreeBytes(g_nBufferSize);  
QSemaphore g_qsemUsedBytes;  
/**************************/  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在其他編譯單元中使用全域性變數時只要包含其所在標頭檔案即可。 
複製程式碼 程式碼如下:

/**********類ConsumerThread使用全域性變數***********/  
#include "consumerthread.h"  
#include "res.h"  
#include <QDebug>  
ConsumerThread::ConsumerThread(QObject* parent)  
: QThread(parent) {  
}  
ConsumerThread::ConsumerThread() {  

}  
ConsumerThread::~ConsumerThread() {  
}  
void ConsumerThread::run()
{  
  for (int i = 0; i < g_nDataSize; i++) 
  {  
      g_qsemUsedBytes.acquire();
      qDebug()<<"Consumer "<<g_szBuffer[i % g_nBufferSize];  
      g_szBuffer[i % g_nBufferSize] = ' ';  
      g_qsemFreeBytes.release();  
  }  
  qDebug()<<"&&Consumer Over";  
}  
/**************************/  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

也可以把全域性變數的宣告和定義放在一起,這樣可以防止忘記了定義,如上面的extern char g_szBuffer[g_nBufferSize]; 然後把引用它的檔案中的#include “res.h”換成extern char g_szBuffer[];。 
但是這樣做很不好,因為你無法使用#include “res.h”(使用它,若達到兩次及以上,就出現重定義錯誤;注:即使在res.h中加#pragma once,或#ifndef也會出現重複定義,因為每個編譯單元是單獨的,都會對它各自進行定義),那麼res.h宣告的其他函式或變數,你也就無法使用了,除非也都用extern修飾,這樣太麻煩,所以還是推薦使用.h中宣告,.cpp中定義的做法。

(5)靜態全域性變數(static) 
注意使用static修飾變數,就不能使用extern來修飾,即static和extern不可同時出現。static修飾的全域性變數的宣告與定義同時進行,即當你在標頭檔案中使用static聲明瞭全域性變數,同時它也被定義了。

static修飾的全域性變數的作用域只能是本身的編譯單元。在其他編譯單元使用它時,只是簡單的把其值複製給了其他編譯單元,其他編譯單元會另外開個記憶體儲存它,在其他編譯單元對它的修改並不影響本身在定義時的值。即在其他編譯單元A使用它時,它所在的實體地址,和其他編譯單元B使用它時,它所在的實體地址不一樣,A和B對它所做的修改都不能傳遞給對方。

多個地方引用靜態全域性變數所在的標頭檔案,不會出現重定義錯誤,因為在每個編譯單元都對它開闢了額外的空間進行儲存。

以下是Windows控制檯應用程式程式碼示例: 
複製程式碼 程式碼如下:

/***********res.h**********/  
static char g_szBuffer[6] = "12345";  
void fun();  
/************************/  

複製程式碼 程式碼如下:

/***********res.cpp**********/  
#include "res.h"  
#include <iostream>  
using namespace std;  

void fun() {  
 for (int i = 0; i < 6; i++) {  
  g_szBuffer[i] = 'A' + i;  
 }  
 cout<<g_szBuffer<<endl;  
}  
/************************/  

複製程式碼 程式碼如下:

/***********test1.h**********/  
void fun1();  
/************************/  

複製程式碼 程式碼如下:

/***********test1.cpp**********/  
#include "test1.h"  
#include "res.h"  
#include <iostream>  
using namespace std;  

void fun1() {  
fun();  

 for (int i = 0; i < 6; i++) {  
  g_szBuffer[i] = 'a' + i;  
 }  
 cout<<g_szBuffer<<endl;  
}  
/************************/  

複製程式碼 程式碼如下:

/***********test2.h**********/  
void fun2();  
/************************/  

複製程式碼 程式碼如下:

/***********test2.cpp**********/  
#include "test2.h"  
#include "res.h"  
#include <iostream>  
using namespace std;  

void fun2() {  
 cout<<g_szBuffer<<endl;  
}  
/************************/  

複製程式碼 程式碼如下:

/***********main.cpp**********/  
#include "test1.h"  
#include "test2.h"  

int main() {  
 fun1();  
 fun2();  

 system("PAUSE");  
 return 0;  
}  
/************************/  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

這裡寫圖片描述

按我們的直觀印象,認為fun1()和fun2()輸出的結果都為abcdef,可實際上fun2()輸出的確是初始值。然後我們再跟蹤除錯,發現res、test1、test2中g_szBuffer的地址都不一樣,分別為0x0041a020、0x0041a084、0x0041a040,這就解釋了為什麼不一樣。

注:一般定義static 全域性變數時,都把它放在.cpp檔案中而不是.h檔案中,這樣就不會給其他編譯單元造成不必要的資訊汙染。

(6)全域性常量(const) 
const單獨使用時,其特性與static一樣(每個編譯單元中地址都不一樣,不過因為是常量,也不能修改,所以就沒有多大關係)。 
const與extern一起使用時,其特性與extern一樣。

extern const char g_szBuffer[];      //寫入 .h中  
const char g_szBuffer[] = "123456"; // 寫入.cpp中