1. 程式人生 > >C++知識點彙總

C++知識點彙總

C++筆記(一)
變數與函式
 
變數宣告:
extern int i;
函式宣告:
extern float f(float);
float f(float); //extern在此不是必須的
float f(float a); //宣告中起作用的只有型別,與變數名無關
變數定義:(變數在第一次定義時被宣告)
int i;
函式定義:(有函式體的函式宣告就成了函式定義)
float f(float a) { return a + 1; }
有了宣告就必須有定義,有了定義宣告就可以不要,因為定義包含了宣告。
 
函式在定義時可以有未命名的引數。比如某引數在函式體中未被使用,則刪去此引數的名稱後編譯時就不會有變數未使用的警告。
 
func()在C++中表示沒有引數的函式,而在C中表示不確定的引數(這種情況下就會忽略型別檢查)。func(void)在C和C++中都表示沒有引數的函式。
 
C++中可以在程式的任何地方定義變數,但有些格式是不允許的。比如:
for (int i = 0, int j = 0; ...; ...;) // 只允許在for中定義一個變數
while ((char c = cin.get()) != 'q') // 定義變數時不允許使用括號
 
區域性變數都是auto的,因此沒有必要顯式地使用auto來修飾區域性變數。
 
只能在塊中定義register變數。register變數不能被取址。(不推薦使用)
 
定義為static的變數可以在塊外繼續存在,但卻只能在塊內被訪問。比如在函式中定義時可以像全域性變數一樣維持原有的值,但不像全域性變數可以被程式的其他部分修改。(靜態儲存)
 
static修飾全域性變數或函式時表示該變數或函式只在當前檔案中有效(內部聯接)。(靜態可見性)
 
用extern修飾的變數或函式的定義可以在其他檔案中出現(外部聯接)。全域性變數和函式預設都是extern的(C++中的const除外)。
 
const修飾的變數在C++中只在檔案內部有效(內部聯接),若要使它在其他檔案中有效需要加上extern修飾。而在C中的const預設是extern的,因此不同檔案中不允許有重名的const。
 
使用volatile修飾的變數會在每次需要的時候被重新讀取,編譯器不會對其加優化。
 
在成員函式裡要使用同名的全域性變數或函式時,在全域性的函式名或變數名前加::,表示全域性的域。
 
 
 
結構和陣列

 
被巢狀的結構不能訪問巢狀其的結構私有成員,必須先宣告被巢狀的結構,再宣告其為friend,最後定義此結構。
 
物件在域結束後會被自動銷燬。解構函式會被自動呼叫,即使是使用goto語句跳出域也一樣。但是使用C標準庫中的setjmp()和longjmp()跳出時解構函式不會被呼叫。
 
如果給定一個數組的初始值小於陣列的大小,陣列中剩餘的元素會被初始化為0。如:
int b[6] = {0};
但只有陣列定義沒有初始值時就沒有這種效果。
 
結構體的初始化也一樣:
struct X {
  int i;
  float f;
  char c;
};
X x1 = { 1, 2.2, 'c' };
但當結構體中有建構函式時,必須通過建構函式初始化:
struct Y {
  float f;
  int i;
  Y(int a);
};
Y y1[] = { Y(1), Y(2), Y(3) };
 
union中也能定義訪問控制、成員函式和構造/解構函式,但是union不能被繼承。
 
也可以定義匿名的union,這時訪問union中的成員不需要加上域識別符號。
 
 
 
const

 
const int* u;和int const* v都表示指向常量的指標。int* const w;才表示指標常量。即const在*左邊表示指向常量的指標,const在*右邊表示指標常量。也可以理解為const只修飾它左邊的東西。
 
字串陣列應該是常量,因此像char* cp = "howdy";這樣的寫法按理不能修改字串的內容,而應該寫成char cp[] = "howdy";。但為了與C程式碼相容,編譯器通常會容許這個錯誤。
 
只有不是const的返回值能被用作lvalue,因此若不希望函式返回的物件被用作lvalue,可以用const來修飾。
 
在表示式或函式求值的過程中編譯器生成的臨時物件都是const的。因此如果函式要以引用方式接受臨時物件作為引數時,引數引用也需要用const修飾。
 
類中的const只表示在初始化後不能改變,需要在建構函式初始化列表中初始化。如:
Fred::Fred(int sz) : size(sz) {}
這就使得const能在建構函式被定義時就被初始化。對於非const的型別的初始化也可以這樣進行。對於內建型別的初始化也可以用這種像是呼叫建構函式的形式。
 
類中以static修飾的const變量表示被這個類的所有物件共用。必須在定義時就被初始化。在舊的編譯器下,一般使用enum來代替。如static const int size = 100;就用enum { size = 100 };代替。
 
被宣告為const的物件只能呼叫其為const的成員函式。宣告為const的成員函式中不能對物件的內容進行修改或呼叫非const的成員函式。定義時const放在函式引數列表之後(放在前面會與const的返回值混淆)。
 
非const的物件也能呼叫const的成員函式。因此把不會改變物件內容的成員函式定義為const的就能具有最大的通用性。建構函式和解構函式不能是const的。
 
在const的成員函式中修改物件的成員:
1、將this轉換為普通的指標(const成員函式中的this是const指標),就能通過其來改變物件的成員,如:
((Y*)this)->i++;

(const_cast<Y*>(this))->i++;
2、以mutable修飾的成員能在const的成員函式中被修改。
 
 
 
關於預處理

 
#define的常量用const取代,巨集用inline函式取代。
 
在類定義中定義的函式自動為行內函數。也可以使用inline修飾函式使其成為內聯的。inline只有在函式定義時才會發揮作用。因此標頭檔案中的行內函數都是有函式體的。inline的函式也是內部聯接的。
friend的函式也可以是inline的。
 
inline只是對編譯器的優化提示,編譯器不一定會對所有的inline進行內聯。
 
不可替代的預處理功能:
字串化(#,將識別符號轉化為字串):#define DEBUG(x) cout << #x " = " << x << endl
記號貼上(##,連線兩個識別符號形成一個新的識別符號):#define FIELD(a) char* a##_string; int a##_size
 
 
 
static
 
編譯器保證對static的內建型別賦初值。對於static的物件呼叫其預設的建構函式。
 
static物件的解構函式會在main()函式結束或呼叫exit()時被自動呼叫。因此在static物件的解構函式中呼叫exit()可能會導致遞迴呼叫。呼叫abort()時不會自動呼叫static物件的解構函式。
 
全域性的物件是static的。因此全域性物件的構造和解構函式可以用來在進入main()之前和退出main()之後完成一些工作。
 
對於全域性的物件來說,因為其本身是靜態儲存的,因此使用static修飾的效果只是使其變為內部聯接。
 
靜態資料成員的定義:
class A {
  static int i;
public:
  //...
};
int A::i = 1;
只有static const的整型才能在類內部被初始化,其他型別都必須在外部(檔案域中)被初始化。因此本地類(在函式中定義的類)中不能定義靜態成員。
 
static的成員函式在類中定義。為所有該類的物件共用,呼叫時可以直接使用類名::函式名。
 
static的成員函式只能訪問static的成員,因為static的成員沒有this。
 
static初始化的依賴關係(最好避免)。可參考iostream的實現(cin、cout和cerr都是在不同檔案中的static物件)或另一種(居然無名!)方法。
 
 
 
namespace
 
#include <iostream.h>
意味著
#include <iostream>
using namespace std;
因為在標準化之前不存在名字空間,.h中的所有內容都是暴露的
 
namespace定義只能出現在全域性域或另一個namespace中。定義的{}後沒有分號。可以跨越多個檔案定義(不算重複定義)。也可以定義別名。如:
namespace Bob = BobsSuperDuperLibrary;
 
每個編譯單元都有一個未命名的namespace。通過將變數放入這個namespace中就可以不必將它們宣告為static的(內部聯接)。C++中要達到檔案內的靜態就使用這種方法。
 
類中宣告的友元也會進入這個類所在的namespace。
 
using namespace只在當前域內有效(將namespace中的所有名稱注入當前域)。using namespace中引入的名稱可以被覆蓋。
 
聯接方式。比如需要在C++中呼叫C的庫中的函式:
extern "C" float f(int a, char b);
否則的話聯接程式可能無法解析函式呼叫。通常編譯器都會自動處理這種情況。
 
 
 
引用和指標
 
引用必須在建立時被初始化為引用某一個變數,並且一旦在初始化後就不能再改變被引用的物件。
 
函式返回引用時要注意引用的物件不能在函式返回後就不存在了,比如函式內部的變數。
 
拷貝建構函式接受的是本類的引用。定義拷貝建構函式就必須定義建構函式。
 
通過宣告一個private的拷貝建構函式可以防止物件被傳值。
 
指向特定類中成員的指標:
宣告:
int ObjectClass::*pointerToMember;
初始化:
int ObjectClass::*pointerToMember = &ObjectClass::a;
使用:
objectPointer->*pointerToMember = 47;
object.*pointerToMember = 47;
對於成員函式也適用
 
 
 
運算子過載
 
過載的運算子必須接受至少一個自定義型別。接受的引數都為內建型別的運算子無法被過載。
 
運算子作為類的成員函式被過載時,類的物件就作為第一個引數。注意此時函式的返回方式。
 
過載++a會呼叫operator++(a),過載a++會呼叫operator++(a, int),其中第二個int引數是不會被用到的,只是用來區分字首和字尾呼叫。--的過載也是一樣。
 
=和[]只能作為成員函式被過載。()只能作為成員函式被過載,可以帶任意多個引數。(若可以不作為成員函式被過載,則對於內建型別的運算就可以被過載,這是沒有意義的)
 
->和->*也只能作為成員函式被過載,但對返回值有一定的限制。
 
.和.*不能被過載
 
返回值優化:
Integer tmp(left.i + right.i);
return tmp;
這樣編譯器需要三步才能完成(構造,拷貝,析構),而
return Integer(left.i + right.i);
則只需要一步
 
過載時作為成員或非成員函式的選擇:
所有的一元運算子 推薦作為成員
= () [] -> ->*  必須作為成員
+= -= /= *= ^=
&= |= %= >>= <<= 推薦作為成員
所有其他的二元運算子 推薦作為非成員
 
當物件還沒有被建立時,=呼叫的是建構函式或拷貝建構函式,為的是初始化物件;當物件已被建立時,=呼叫的才是operator=。因此
Fee fee(1);
Fee fum(fi);
這樣的寫法要比
Fee fee = 1;
Fee fum = fi;
這樣的寫法清晰。
 
在過載賦值運算子時,首先檢查是否是對自身賦值是個好習慣。
 
當物件中有指標時,拷貝建構函式通常需要連同複製出指標所指向的內容。而當此內容很大時,通常採用引用計數的方法,只在需要修改資料且引用數大於1時才複製內容。這種技術被稱為copy-on-write。
 
當建構函式接受一個其他型別的物件作為引數時,編譯器可以用它來進行自動型別轉換。如果不需要這樣的自動轉換,在建構函式前加上explicit。
 
也可以過載operator 型別名 來定義自動型別轉換。由於這種型別轉換是由源物件完成的(不像建構函式的型別轉換是由目標物件完成的),因此可以完成自定義物件到內建型別的轉換。
 
運算子過載為非成員函式時,運算子兩邊都可以進行自動型別轉換。
 
提供自動型別轉換時注意兩種型別之間只需提供一條轉換路徑,否則會出現二義性錯誤。
 
 
 
new和delete
 
new為物件分配空間並呼叫建構函式。delete呼叫物件的解構函式後釋放物件的空間。
 
delete對於零指標無效,因此人們通常在delete之後將指標設為零,以防止多次delete帶來的問題。
 
delete void*可能會帶來問題。由於編譯器不知道具體的型別,將導致物件的空間被釋放而沒有呼叫解構函式。這可能會引起記憶體洩漏。
 
a* p = new a[100];為陣列中的每個物件分配空間並呼叫建構函式進行初始化。
delete []p;則完成相反的事。delete後的[]表示指標只是陣列的首地址,編譯器會自
動獲取陣列的大小完成釋放工作。由於表示首地址,最好定義為常量:
a* const p =  new a[100];
 
當new找不到空間分配時,會呼叫new-handler。其預設行為是丟擲異常。可以通過使用new.h中的set_new_handler()來定義作為new-handler的函式。
 
operator new和operator delete都可以被過載,用來完成一些自定義的記憶體管理功能:
void* operator new(size_t sz)
void operator delete(void* m)
 
當new和delete作為類的成員函式被過載時,為該類的物件分配空間時就會呼叫這些過載的操作符。
 
用於為陣列分配空間的operator new[]和operator delete[]也能被過載。實際佔用的空間比分配的要多4個位元組,被系統用於儲存陣列的大小。
 
過載的new可以接受任意多個引數,比如定義第二個引數,用於指明在何處分配空間:
void* operator new(size_t, void* loc)
使用時:
X* xp = new(a) X;
其中的a就是第二個引數。採用這種形式的new可能需要顯式呼叫解構函式:
xp->X::~X();
(因為物件可能不是構建在堆上,使用delete只能釋放堆上的空間)
 
 
 
繼承
 
合成和繼承都需要在構造初始化列表中對子物件進行初始化。
 
一旦在子類中定義了一個與超類中同名的函式,超類中所有同名的函式,不管函式特徵是否相同,都會變得不可見。
 
建構函式,解構函式,operator=不會在繼承時進入子類。
 
繼承預設是private的,即超類中的public成員在子類中也會變成private的。若只想暴露某些成員,可以在子類中的public部分使用using。這種情況下同名的過載函式會被全部暴露。
 
 
 
多型
 
C++中使用virtual關鍵字來宣告函式為延遲繫結的(或動態繫結)。函式的定義中不需要virtual。使用了virtual之後, upcast時呼叫的就是子類的過載函式,否則只能呼叫基類的。(C++使用virtual來使得動態繫結成為可選的,這是出於效率的考慮。其他語言如 Java和Python等預設就是使用動態繫結的)
 
擁有純虛擬函式的類是抽象類(提供了一個介面)。純虛擬函式在virtual定義後加上=0。
 
繼承抽象類的子類必須實現基類中所有的純虛擬函式,否則就還是抽象類。
 
純虛擬函式不能內聯定義。在定義後則可以被子類使用。
 
傳值的upcast會把物件切割,即子類會被切到只剩下基類的部分。抽象的基類可以避免這種情況,因為抽象類不允許例項化。
 
建構函式不能是virtual的,而解構函式可以。
 
 
 
模板
 
繼承可以重用物件,而模板可以重用程式碼。
 
定義template時可以使用內建型別。它們的值可以是常量。如:
template<class T, int size = 100>
 
template不僅可以用來建立類模板,還能用來建立函式模板。
 
 
 
雜項
 
之間沒有標點符號的字元陣列會被自動連線起來
 
C++標準中包含的不是STL,而是C++標準庫,它是由STL演變來的。
 
?運算子中:的兩邊必須都有表示式。
 
以,分隔的表示式,最後一個表示式的值會被返回。
 
C++中的型別轉換可以函式的形式,如float(n)等價於(float)n。
 
C++中的顯示型別轉換(提供一種容易辨別的形式):
static_cast:型別收窄時可以避免編譯器的警告資訊;同樣可用於型別放寬、void*的強制轉換和自動隱式轉換。
const_cast:用於轉換const和volatile,比如將一個普通指標指向一個const
reinterpret_cast:把一種型別當成另一種型別
dynamic_cast:用於downcast
 
sizeof是運算子而不是函式,用於型別時要加括號,如sizeof(double),用於變數時不用,如sizeof x。
 
顯式運算子:
邏輯運算子
and &&
or  ||
not !
not_eq !=
位運算子
bitand &
bitor |
xor ^
and_eq &=
or_eq |=
xor_eq ^=
compl ~