一個例子
C++遵循先定義,后使用的原則。就拿函數的使用來舉例吧。
我看過有些人喜歡這樣寫函數。
#include<iostream> using namespace std; int add(int x, int y) //A { return x + y; } int main() { int re = add(1, 2); //B system("pause"); return 0; }
但我更偏向下面這種。
#include<iostream> using namespace std; int add(int x, int y); //A int main() { int re = add(1, 2); //B system("pause"); return 0; } int add(int x, int y) //C { return x + y; }
C++的編譯是以文件為單位,在某一個特定源文件中,則是從上至下,逐行解析的。
第一種風格中,A處的代碼既是函數的定義(函數的實現),也充當了函數的聲明。函數的定義是函數正真的實體,和邏輯實現。而聲明則是告知編譯器:我這個函數存在,我這個函數外觀是什么樣的(即 :返回值,參數類型和參數個數的相關信息)。
當編譯器分析到B處代碼時,編譯器已經知道了函數存在,則允許出現這個函數的調用,知道了函數的外觀,編譯器還會分析函數調用時是否正確使用了,如參數個數,參數類型等。
這個過程中拆解開來就是:定義 , 聲明 , 使用。顯然第二種風格更好的闡述了這三部分。
那么問題來了?當項目過大后,實體(函數定義,類,結構體,變量定義等)會放在不同的文件中。但是編譯器又是以文件為單位處理的,它在處理main.cpp時,完全不知道lib.cpp的任何信息。
那么,如果要在main.cpp中調用lib.cpp中定義的函數,就必須手動將lib.cpp中的函數頭拷貝聲明到main.cpp中。那如果在100個源文件中調用了lib.cpp中的函數,豈不是要拷貝100份?
這樣累不說,又容易出錯,還不利於后期維護。
於是預處理器說:“你只需將lib.cpp中的需要共享,在其他源文件中使用的實體單獨聲明到一個頭文件lib.h中吧,別的源文件需要使用你的lib.cpp中的實體,只需要在他們的源文件中加上一行預處理指令:#include"lib.h" 就OK了,剩下事交給我”
於是一切變成了這樣:
/*lib.cpp*/ #include"lib.h" int add(int x,int y) { return x+y; }
/*lib.h*/ #ifndef _LIB_H__ #define _LIB_H__
int add(int x, int y);
#endif
/*main.cpp*/
#include<iostream> #include"lib.h" using namespace std; int main() { int re = add(1, 2); cout << re << endl; system("pause"); return 0; }
那么問題又來了:預處理器它到底是怎么幫你的呢,它做了什么手腳?
下面我們就來用VS2013看看預處理的結果。如何查看預處理結果--->點我
如果安裝有g++編譯器,則可以使用命令: g++ -E main.cpp -o main.i 來生成預處理文件
/*lib.i文件*/ int add(int x, int y); int add(int x,int y) { return x+y; }
/*main.i 前面省略87260行,都是iostream頭文件里面的東西,真多! */ int add(int x, int y); using namespace std; int main() { int re = add(1, 2); cout << re << endl; system("pause"); return 0; }
總結:
1、預處理器在.cpp中遇到#include<> 或者 #include " ", 都會將#include<> 或者 #include " "指令替換為他們包含的頭文件中的內容,形成 .i文件。
這就是預處理器對頭文件的處理結果。當然還要考慮到預處理器也是有邏輯的,比如防止重復包含#ifndef .......#define .......#endif
2、頭文件只在預處理期起作用,預處理過后生成 .i 文件,此后頭文件就沒有作用了。
3、預處理指令 的 作用域 為 源文件作用域,也就是每 一條 預處理指令 只在它所在的 .cpp文件有效。
4、預處理不屬於任何名稱空間,名稱空間“管不住”預處理指令。預處理指令不受C/C++的作用域規則,它是一種編譯前期的機制。
5、用戶將一個源文件( .c 或者 .cpp ) 提交給編譯器后,首先執行的是該文件的預處理(處理源文件中的預處理指令),預處理后的結果為編譯單元,這種編譯單元才是編譯器真正 工作的對象!程序員眼中看見的是 源文件(.c 或者 .cpp )和頭文件, 而編譯器眼中只有編譯單元(預處理后形成的.i文件)。但是我 們口頭上說的C/C++編譯器包括預處理器。
如果你不理解C/C++的編譯過程,請點 擊我
以下通過幾個特例說明需要注意事項.
防止頭文件的重復包含
當項目大了后,編譯單元之間的關系變得復雜,就很可能出現頭文件重復包含的情況。雖然大多是情況下,重復包含頭文件是沒有問題的,但是這會使.i文件膨脹 。
C/C++遵循單定義,多聲明的規則。聲明多次沒問題,但是不必要的聲明除了在利於代碼閱讀外的目的下使用外,其他的要盡量避免。
一般采用以下方法。
#ifndef _XXX_H__ #define _XXX_H__ //被包含內容放中間 #endif
C++提供了#pragma once 預處理指令來達到上述效果,但是很多人習慣了C中的寫法,也為了獲得更大的兼容性。
普通全局變量
有時為了讓一個源文件中的全局變量在多個源文件中的共享。
普通全局變量是有外部鏈接性(多個源文件共享一個實體),也就是它可以在所有的.cpp文件中共享使用。因為它將在所有的源文件中共享,所以必須保證不會在其他源文件的任何地方出現相同的鏈接性且相同名稱的全局變量,正所謂一山不容二虎。
下面是錯誤的寫法,編譯不通過,提示錯誤:error : “int sum”: 重定義
相信如果你明白了頭文件和預處理的機制,你就應該知道為什么是錯誤的了。
/*share.cpp*/ #include"share.h" int sum = 100;
/*share.h*/ #ifndef _SHARE_H__ #define _SHARE_H__ int sum; #endif
/*main.cpp*/
#include<iostream> #include"share.h" using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
貼出代碼說良心話。下面就是預處理后的結果。很明顯的:全局變量sum的確重復定義了。在share.i 中 重復定義了2次,在main.i中又定義了一次,一共定義了3次!
/* share.i */ int sum; int sum = 100;
/*main.i 省略iostream 中的代碼 */ int sum; using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
要保證其他源文件能使用普通全局變量,又不會重定義,就要使用extern關鍵字來聲明。extern用來聲明。
改:
/*share.cpp*/ #include"share.h" int sum = 100; //extern int sum = 100; 也是OK的。 //這里 的extern是可選的,加上extern 的唯一目的是,暗示這個變量會被其他文件使用
/*share.h*/ #ifndef _SHARE_H__ #define _SHARE_H__ extern int sum; #endif
/*main.cpp*/
#include<iostream> #include"share.h" using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
全局static變量
static修飾全局變量的目的就是為了將外部鏈接性改為內部鏈接性(僅僅在定義它的文件中共享,對其他文件隱藏自己,定義的實體是源文件私有的)。
這樣避免了對外部鏈接性空間的名稱污染,其他源文件完全可以定義具有同名的外部鏈接的變量,當然也可以定義同名的內部鏈接變量。
static修飾的全局變量 和 全局函數 都不要在對應模塊的頭文件中聲明,因為static是為了隱藏在一個源文件中,而放在頭文件中則是為了方便其他源文件使用,這2者顯然矛盾了。
下面我們嘗試開發一個對int數組排序的庫來說明問題。舉一反三靠大家自己了。
/*sort.cpp*/
/*
我們使用了2個函數完成排序:1、swap用於交換2個值,bubble_sort則是排序的實現。顯然我們只想對其他使用者提供 bubble_sort這一個函數接口,而對外隱藏swap函數。於是將swap修飾為static
*/
#include"sort.h"
static void swap(int &a, int&b); //static 函數僅僅在自己的源文件聲明就夠了,不要在頭文件中聲明。
//為什么 需要在自己的源文件中聲明呢?假如將下面的 swap函數和bubble_sort函數定義的位置交換下,那么
//編譯器在從上往下解析sort.cpp時,會先看見swap在bubble_sort中的調用,而編譯器事先不知道swap的任何聲明和外觀信息。
static void swap(int &a, int&b) { int t = a; a = b; b = t; } void bubble_sort(int arr[], int len) { for (int i = 0; i < len - 1; ++i) { for (int j = 0; j < len - 1 - i; ++j) { if (arr[j]>arr[j + 1]) swap(arr[j], arr[j + 1]); } } }
/*sort.h*/ #ifndef _SORT_H_ #define _SORT_H_ void bubble_sort(int arr[], int len); #endif
#include"sort.h" #include<iostream> using namespace std; int main() { int arr[5] = { 12, 6, -12, 44, -90 }; bubble_sort(arr, 5); for (size_t i = 0; i < 5; ++i) { cout << arr[i] << endl; } system("pause"); return 0; }
全局const常量
全局const默認是具有內部鏈接性,就像使用了static修飾后一樣。(C程序員朋友注意,和C++不同,const常量在C中依舊是外部鏈接性的)
/*test.cpp*/ const int foo = 12; //等價於 static const int foo = 12;
由於const全局常量是內部鏈接性的,所以我們可以將 const定義放在頭文件中,這樣所有包含了這個頭文件的源文件都有了自己的一組const 定義,由於const為文件內部鏈接性,所以不會有重定義錯誤。
/*one.cpp*/ #include"one.h"
/*one.h*/ #ifndef _ONE_H__ #define _ONE_H__ const int x = 1; const int y = 2; #endif
#include"one.h" int main() { return 0; }
預處理后
/*one.i */ const int x = 1; const int y = 2;
/*main.i*/
const int x = 1; const int y = 2; int main() { return 0; }
你會覺得這樣很不經濟,如果這個頭文件被包含100次,那豈不是在這100個源文件都定義了這組全局常量?而且全局常量的存活期又很長。當然是有解決辦法的。那就是使用extern
改:
/*one.cpp*/ #include"one.h" extern const int x = 1; extern const int y = 2;
/*one.h*/ #ifndef _ONE_H__ #define _ONE_H__ extern const int x; extern const int y; #endif
#include"one.h" int main() { return 0; }
預處理后的文件
/*one.i*/ extern const int x; extern const int y; extern const int x = 1; extern const int y = 2;
/*main.i*/ extern const int x; extern const int y; int main() { return 0; }
但是:在C++中,如果const常量只 和 #define宏那樣使用,是不會占用內存的,而是加入編譯器的符號常量表中,這就是C++的const常量折疊折疊現象。但有些操作會迫使const存儲在內存中,比如對const常量取地址等。因此將const常量直接放在頭文件也是OK的,大多數情況也會這樣做。
因此,如果兩個不同的文件中聲明同名的const ,不取它的地址,也不把它定義成extern,那么理想的C++編譯器不會為他分配內存,而只是簡單的把它折疊到代碼中。--《C++編程思想第一卷》
全局函數
C++類 calss 和 結構體struct 中的成員函數受 OOP封裝規則限定。這里只談全局函數:在自定義名稱空間中的,以及在全局(::)名稱空間中的函數。
全局函數默認都是有外部鏈接性的。因此,在一個源文件中定義的函數,其他源文件只要有聲明,就可以使用。
也可以將全局函數使用static 修飾,使其在定義它的源文件中私有化。此時它和使用static 修飾了的全局變量一樣,會隱藏外部鏈接中同名的全局函數,也不會引起重名錯誤。
注意:全局inline函數默認是內部鏈接性,這也就是inline函數為什么可以整體定義放在頭文件中的原因了。這點和宏函數一樣,宏也是僅僅在一個源文件中有效的。
頭文件中放什么?
1、類的定義
2、結構的定義
3、enum的定義
4、內斂函數的定義 和聲明。內斂函數整體都放在頭文件中。這樣包含它的每個源文件才知道怎樣展開。
5、函數的聲明
6、模板
7、#define 宏 和 const 常量(C++不建議使用宏:宏常量使用const替代,宏函數使用inline函數替代)