C++學習(一)——基本語法
阿新 • • 發佈:2019-01-07
封裝
作用域解析運算子
:: 作用域解析運算子
::a++; //全域性變數a
public
struct的成員預設為public
protected
private
class成員預設為private
friend 友元
允許非類的成員函式訪問類的成員變數
一個friend必須在一個類內宣告
也可以把整個類宣告為friend
巢狀友元
類中巢狀的結構或類,並不能訪問類的private成員,如果想訪問的話,必須做如下操作
1.宣告巢狀的類或結構
2.宣告巢狀的類或結構是友元
3.定義巢狀的類或結構
型別轉換
C++是的型別轉換非常嚴格,比如其他型別指標可以賦給void*,但void*不能賦給任何型別指標,必須顯式轉換 顯式轉換 static_cast 用於”良性“和”適度良性“轉換,包括不用強制轉換(例如自動型別轉換) C++不允許從void*賦值,除非用轉換符 void *p; int *pi; pi = p; //錯誤 i = static_cast<int *>(p); const_cast 對const和/或volatile進行轉換 從const轉為非const,從volatile轉換為非volatile reinterpret_cast 轉換為完全不同的意思。為了安全使用它,關鍵必須轉換回原來的型別。轉換成的型別一般只能用於位操作,否則就是為了其他隱祕的目的。這是所有型別轉換中最危險的 dynamic_case 用於型別安全的向下轉換 僅當型別轉換時正確的並且成功時,返回一個所需型別的指標,否則它將返回0。 無論何時進行向下型別轉換,都要判斷是否為0 dynamic_case有開銷,如果有大量的dynamic_case,會影響效能 自動型別轉換 建構函式轉換 如果定義一個建構函式,能以另一型別的物件做為單個引數,那麼這個建構函式允許編譯器執行自動型別轉換 建立一個單一型別引數的建構函式總是會定義一個自動型別轉換函式 阻止建構函式轉換 前面加explicit 運算子轉換 建立一個成員函式,通過在operator後跟隨想要轉換的型別的方法,將當前型別轉換為希望的型別 沒有返回型別,返回型別就是正在過載的運算子的名字 operator A() { return a; } 將當前型別,轉換為A型別 當提供了不止一種的型別轉換的時候,會發生衝突。自動型別轉換應小心使用 自動型別轉換隻發生在函式呼叫中,不在成員選擇期間
初始化與清除
建構函式 建構函式與類同名,沒有返回值,可以有引數 只要有建構函式,所有初始化工作必須通過建構函式完成,編譯器保證不管什麼情況都會呼叫建構函式 預設建構函式——不帶任何引數的建構函式 聚合初始化 如果初始值多餘元素個數,則出錯;如果少於元素個數,其餘元素賦0 int a[5] = {1,2,3,4,5}; int b[5] = {4}; int c[] = {1,2,3,4}; 拷貝建構函式 如果沒有拷貝建構函式,編譯器會生成一個預設拷貝建構函式,執行位拷貝 如果自定義拷貝建構函式,編譯器執行使用者定義的拷貝建構函式 對於組合物件,編譯器會遞迴地為所有成員物件和基類呼叫拷貝建構函式 最好建立自己的拷貝建構函式而不是使用編譯器預設建立的 如果想防止按值傳遞,可以定義一個私有的拷貝建構函式 賦值運算子過載 Object a = b; 呼叫拷貝建構函式 a = c; 呼叫operator=,因為a已經被建立,不能再呼叫拷貝構造函數了 如果物件還沒有被建立,初始化是需要的;否則使用operator= 最好避免用operator=初始化程式碼,而是顯式地呼叫建構函式 所有賦值運算子過載,必須檢測自賦值 if( this == &right ) return *this; 如果沒有顯式建立operator=,編譯器會自動建立,規則與拷貝建構函式相同 析構 解構函式與類同名,前面加~,沒有返回值,沒有引數 非局域的goto語句將不會引發解構函式的呼叫,有goto語句時,需要留意 ??? 顯式地呼叫解構函式 a.A::~A(); 只有在定位new時才使用,其他case不要使用
過載
函式過載
編譯器會根據函式名和引數列表生成內部函式名
不通過返回值生成內部函式名,是因為有時可以忽略返回值
所有函式在呼叫前必須被宣告,編譯器不會猜測函式的宣告
運算子過載
運算子過載
運算子過載只是語法上的方便,是另一種函式呼叫的方式
只有包含使用者定義型別的運算子才能過載,只包含內建型別的運算子不能過載
不能過載沒有意義的運算子,不能改變運算子優先順序,不能改變運算子引數
語法
成員函式
一元運算子過載 沒有引數
二元運算子過載 一個引數
全域性函式
一元運算子過載 一個引數
二元運算子過載 兩個引數
可過載運算子
一元運算子
+ - ~ & !
a++ operator++(a, int)
++a operator++(a)
a-- operator--(a, int)
--a operator--(a)
二元運算子
+ - * / %
^ & | << >>
+= -= *= /= %= ^= &= |= >>= <<=
== != < > <= >=
&& ||
=
不常用的運算子
[] 必須是成員函式,只接受一個引數
-> 必須是成員函式;必須返回一個物件,該物件也有一個指標間接運算子,或者必須返回一個指標,被用於選擇指標間接引用運算子箭頭所指向的內容
->* 同->,只不過必須返回一個物件
() 必須是成員函式,它是唯一允許帶不定引數的函式
new delete ,
不可過載運算子
. .*
過載方針
所有一元運算子 建議過載成成員函式
= () [] -> ->* 必須是成員函式
+= -= *= /= ^= &= |= %= >>= <<= 建議過載成成員函式
所有其他二元運算子 建議過載成非成員函式
常量
值替代
應該完全用const取代#define的值替代
const預設是內部連結,外部檔案無法引用
定義const時用extern修飾,會把const值變為外部連結;其他檔案引用時,用extern宣告
只有聲明瞭extern,或對const常量取址時,編譯器才為const常量分配儲存空間,否則不分配
定義一個const時,必須賦一個初值,這樣才能區分宣告和定義
const陣列,表示不能改變的一塊儲存空間
不能在編譯時使用const陣列的值,但是在執行時可以
C語言的const,表示一個不能被改變的普通變數
C++的const,表示一個常量,並且往往沒有分配儲存空間
指標
指向const的指標
const int *p; p是一個指標,指向const int,p指向的值不能被改變
int const *p; 同上。可讀性不如上面
const指標
int i;
int* const p = &i; p是一個const指標,指向一個int。因為p是const指標,所以必須賦初值。p不能再指向別的int,但指向的int的內容可以改變
const int* const p = &i; p是const指標,指向一個const int
int const* const p = &i; 同上
型別檢查
可以把一個非const物件的地址賦給一個const指標,但不能把一個const物件的地址賦給一個非const指標
引數和返回值
傳遞const值
如果按值傳遞,const限定無意義;如果按地址傳遞,const可以保證地址內容不會被改變
返回const值
如果按值返回const內建型別,const限定無意義,因為編譯器已經不讓它成為一個左值
如果按值返回const使用者定義的型別,那麼這個返回值不能是一個左值,即不能被再次賦值
臨時量
X f1();
void f2(X &x);
f2(f1()); //錯誤
1.編譯器必須產生一個臨時物件儲存f1()的返回值
2.如果f2()的引數是按值傳遞的,則沒有問題;
3.如果f2()的引數是按引用傳遞的,意味著f2()必須取臨時物件的地址,而f2()的引數沒有用const修飾,可能會對臨時物件進行修改,所以會報錯
標準引數傳遞
當傳遞一個引數時,首先選擇按引用傳遞,而且是const引用
當臨時變數按引用傳遞給一個函式時,函式的引數必須是const引用
類
類裡的const
在每個類物件裡分配儲存空間並代表一個值,這個值一旦被初始化後就不能被改變
在這個物件的生命期內,它是一個常量,但對於這個常量來說,在每個不同物件的值可能會不同
類裡普通的const不能賦初值,初始化工作必須在建構函式初始化列表中進行
建構函式初始化列表:在建構函式執行前,初始化列表已經將所有const初始化完畢
A::A( int a, int b = 0, int c = 1 ):size(1){}
編譯期間類裡的常量
內建型別的static const可以看做一個編譯期間的常量
必須在static const定義的地方對它進行初始化
enum hack
早期C++不支援static const,用匿名enum代替,enum不分配儲存空間 enum{ size = 100, };
const物件和成員函式
const物件
在物件的生命週期內,資料成員的值不會被更改
const成員函式
const成員函式,告訴編譯器該函式不會改變物件的任何資料成員,也不會呼叫任何非const的成員函式,所以可以被一個const物件所呼叫
一個沒有被明確宣告為const的成員函式,被看成是將要修改資料成員的函式,不能被const物件所呼叫
必須在宣告和定義函式時,在引數列表後面重申const屬性,否則編譯器會認為他們是不同的函式
不修改資料成員的任何函式,都應該被宣告為const,這樣就可以被const物件呼叫
const成員函式如何改變類的資料成員
按位const
物件的每個位元位都不允許改變
按邏輯const
物件從整體概念上是不變的,但是可以以成員為單位改變
強制轉換常量性 —— 缺少明確的宣告,不建議這樣做
將congst物件的this指標轉換為非const,然後再改變資料成員
((A*)this)->i++;
(const_cast<A*>(this))->i++;
mutable
將需要改變的資料成員,用mutable宣告
volatile
表示變數或物件可能被外部改變,編譯器在優化程式碼時需要注意,每次訪問時必須重新讀取
volatile與const語法一樣,可以修飾資料成員、成員函式和物件本身
編譯器不優化時,volatile可能不會起作用;一旦開始優化程式碼時,volatile宣告可以防止出現重大錯誤
c-v限定詞
表示const或volatile中的任何一個,c-v qualifier
靜態元素
C語言中的靜態元素
函式內部靜態變數
無論什麼時候設計包含靜態變數的函式時,都要記住多執行緒問題
內建型別的靜態變數,編譯器自動初始化該變數為0
靜態物件
靜態物件必須用建構函式初始化,如果定義靜態物件時沒有指定建構函式引數,那麼該類必須有預設建構函式
靜態物件的析構
如果呼叫abort()退出程式的話,靜態物件的解構函式不會被呼叫
靜態物件的解構函式,在main()函式退出,或呼叫exit()被呼叫,所以在靜態物件解構函式中呼叫exit()非常危險,這樣會導致無窮遞迴呼叫
atexit()指定跳出main()或呼叫exit()的操作,atexit()的註冊函式中可以在所有物件析構前被呼叫
全域性物件在main()前被呼叫,在退出main()時被銷燬
如果一個靜態物件從未被呼叫過,那麼該物件不會被建立,退出程式時該物件也不會被析構
C++的靜態成員
內建型別的靜態變數
static int i; 在外部初始化
內建型別的靜態常量
static const int i = 0; 當宣告的地方初始化
物件和陣列靜態變數 在外部初始化
static X x;
static int a[];
物件和陣列靜態常量 在外部初始化
static const X x;
static int a[];
不能在區域性的類中定義靜態成員
靜態成員函式
不能訪問一般的資料成員,只能訪問靜態資料成員
只能呼叫靜態成員函式
靜態成員函式沒有this
靜態/全域性變數初始化的相依性
如果/全域性變數之間的初始化是相互依賴的,在移植時可能會碰到問題
解決方案
1.避免初始化時的互相依賴
2.把關鍵的/全域性物件的定義放在一個檔案中
3.在標頭檔案中增加一個額外的類,這個類負責靜態物件的動態初始化
4.把一個靜態/全域性物件放在能返回物件引用的函式中,這樣能保證呼叫時會被初始化,而且只初始化一次
行內函數
C++中巨集的限制
只是單純的替換,隱藏難以發現的錯誤
前處理器不允許訪問類的資料成員
引數展開的副作用 例如i++ ++i
行內函數
行內函數能夠像普通函式一樣的行為,唯一不同之處是,行內函數會在適當的地方像巨集一樣展開
任何在類中定義的函式自動成為行內函數。應該只使用行內函數而不使用巨集
非類的函式前面加上inline關鍵字也可成為行內函數,但函式體必須和宣告結合在一起,否則inline宣告是無效的
使用行內函數的目的是減少開銷,如果濫用行內函數的話,會造成程式碼膨脹,因為行內函數會像巨集一樣展開
編譯器對行內函數的實現
編譯器會把行內函數的程式碼放入符號表,然後在合適的時機插入到程式中
只有在類宣告結束後,其中的行內函數才會被計算,所以行內函數中可以向前引用
友元的內聯
在類中定義一個友元函式為行內函數,不會改變友元的狀態,它仍是全域性函式而不是類的成員函式
行內函數的限制
函式太複雜
有任何形式的迴圈
語句太多,引起程式碼膨脹
產生遞迴呼叫
顯示或隱式的定址
當地址不需要時,編譯器仍可能內聯程式碼
使用巨集的情況
# #define PRINT(x) cout << #x << " = " << x << endl;
## 標誌貼上
名字空間
建立
namespace只能在全域性範圍定義,但可以巢狀
namespace的結尾不需要有分號
namespace可以在多個檔案中延續定義
可以用一個namespace作為另一個namespace的別名
不能建立namespace的例項
可以在一個namespace的類定義中插入一個友元
使用
作用域運算子
NameSpaceX::A::f();
using namespace std; using指令
把一個namespace的所有名字引入到另一個空間內
如果多個namespace定義了相同的名字,在使用這些名字時,會引起衝突
using std:string; using宣告
把一個名字引入到當前的範圍
會覆蓋其他using namespace中同名的名字
因為沒有型別資訊,所以如果using的名字有過載,則會引入該名字所有的函式
一般在cpp中使用using指令或宣告,這樣不會影響到其他cpp檔案;不要在標頭檔案中使用using namespace,因為這樣包含該標頭檔案的所有cpp檔案都會受到影響
引用
建立引用時,必須被初始化
一旦一個引用被初始化為指向一個物件,就不能改變為另一個物件的引用,而指標可以
不可能有NULL引用
一旦從函式返回一個引用,必須像從函式返回一個指標一樣對待,當函式返回時必須保證引用的記憶體還在
動態物件建立
C語言分配記憶體呼叫庫函式,C++將動態物件建立作為語言的核心,使用運算子
malloc分配的記憶體用delete釋放,行為未定義
對一個物件delete兩次可能會有問題;delete空指標不會有問題
delete void*可能會出錯,因為它只delete,而不執行解構函式
delete陣列:delete [] fp;
new失敗時,不會執行建構函式,一個new-handler的函式會被呼叫,可以用set_new_handler註冊回撥函式
過載new和delete
全域性過載
過載類的new和delete
會覆蓋全域性的過載版本
儘管不必顯式地使用static,但實際上仍是在建立static成員函式
過載類的new和delete的陣列形式
繼承和組合
繼承
private
繼承時,基類中所有的成員都預設是private的
如果希望基類中的成員可視,在派生類的public部分宣告名字即可,會讓基類中改名字的所有版本變為public
public
基類中的所有公共成員在派生類中依舊是公共的
protected
只是為了語言的完備性
外部使用者無法訪問基類的protected成員,只有派生類中可以訪問
可以允許多重繼承
建構函式初始化列表的意義
在進入新類的建構函式之前,保證基類和其他物件成員都已經構造好了
構造和析構的順序
構造是從類層次的最根處開始,在每一層,先呼叫基類的建構函式,再呼叫成員物件的建構函式
成員物件建構函式的呼叫次序,不受建構函式初始化列表的順序影響,而由成員物件在類中宣告的順序決定
析構的順序嚴格按照構造的相反順序
過載基類成員函式
任何時候重定義基類中的一個成員函式,無論是過載還是更改型別,都會將基類中該函式所有版本隱藏起來
非自動繼承的函式
建構函式
拷貝建構函式
編譯器會首先自動呼叫基類的拷貝建構函式,然後再是個成員物件的拷貝建構函式
無論何時我們在建立了自己的拷貝建構函式時,都要正確的呼叫基類拷貝建構函式
operator=
解構函式
向上型別轉換
新類屬於原有類的型別。從派生類到基類的型別轉換,叫做向上型別轉換
指標和引用也可以向上型別轉換
確定有組合還是繼承,最清楚的方法之一,是詢問是否需要從新類向上型別轉換
多型和虛擬函式
捆綁
捆綁 binding
把函式體與函式呼叫相聯絡
早捆綁 early binding
編譯時捆綁
晚捆綁 late binding( 動態捆綁 dynamic binding 執行時捆綁 runtime binding )
執行時捆綁
晚捆綁的實現
編譯器對每個包含虛擬函式的類或從包含虛擬函式的類派生一個類時,建立一個唯一的表VTABLE
編譯器在每個帶有虛擬函式的類中放置一個指標VPTR,指向這個物件的VTABLE
不管有幾個虛擬函式,都只有一個VPTR
如果編譯器知道物件確切的型別,那麼不實現晚捆綁
虛擬函式
定義
對於特定的函式,為了引起晚捆綁,C++要求在基類中宣告這個函式時使用virtual關鍵字
晚捆綁只對virtual函式起作用,而且只在使用含有virtual函式的基類的地址時發生
僅僅在宣告時使用關鍵字virtual,定義時不需要
如果一個函式在基類中被宣告為virtual,那麼在所有的派生類中都是virtual的
編譯器會自動地呼叫繼承層次中最近的virtual定義,保證總可以調到某個虛擬函式
虛擬函式不是相當高效的,C++為了效率,將虛擬函式設計成可選的,像java、Python這樣的語言,虛擬函式是預設的
抽象基類與純虛擬函式
如果僅想對基類進行向上型別轉換,使用它的介面,不希望使用者實際地建立一個基類物件,可以在基類中加入至少一個純虛擬函式,使基類成為抽象類
當繼承一個抽象類時,必須實現所有純虛擬函式,否則繼承出的類也是一個抽象類
如果試圖建立一個抽象類的物件,編譯器會產生錯誤
virtual void function() = 0;
可以在基類中對純虛擬函式提供定義,但不能定義成行內函數
物件切片
當多型地處理物件時,都是傳地址而非傳值
如果對一個物件的值進行向上型別轉換,這個物件會被切片(copy到一個新物件時,去掉原來物件的一部分)
虛擬函式的重定義
不能更改虛擬函式的返回值
如果重新定義一個基類的虛擬函式,其所有過載版本全部隱藏
如果返回一個指向基類指標的引用,則可以重定義為返回派生類的指標或引用
虛擬函式和建構函式
建構函式不能為虛擬函式
虛機制在建構函式中不工作,呼叫本地的版本
虛擬函式和解構函式
解構函式能夠且常常必須是虛的。不把解構函式設為虛擬函式是一個隱匿的錯誤
虛機制在解構函式中不工作,呼叫本地的版本
純虛解構函式
必須在基類中為純虛解構函式提供一個定義,因為基類在析構時必須要有函式體
不要求在派生類中重新定義純虛解構函式,因為編譯器會自動生成
純虛解構函式的唯一效果是使類成為一個抽象類
模板
模板實現了引數化型別的概念
通常把模板的所有宣告和定義都放入一個頭檔案中
在template<...>之後的任何東西都意味著編譯器在當時不為它分配儲存空間,而是一直處於等待狀態直到被一個模板示例告知
首先建立和除錯一個普通類,然後讓它成為模板,一般認為這種方法比一開始就建立模板更容易
函式模板——通用演算法
其他
標頭檔案
#include <> 在預設和指定的路徑尋找標頭檔案
#include <iostream> C++標準形式,沒有.h
#include <stdio.h> 包含C標頭檔案,有.h
#include <cstdio> 包含C標頭檔案,前面加"c"
#include "myproject.h" 現在當前路徑下尋找,然後再按照<>方式尋找
必須加.h
register
告訴編譯器,儘可能快地訪問這個變數,要求編譯器將此變數放入暫存器中
不能保證編譯器會將變數放入暫存器中,甚至也不能保證能提高訪問速度,這只是對編譯器的一個暗示
不能定義全域性或靜態register變數,只能定義區域性register變數
不能得到或計算register變數的地址
可以使用register變數定義函式形參
最好避免使用register變數
asm
允許在C++中寫彙編程式碼
第一個字元是下劃線的識別符號是保留的,不應該使用它們
返回值優化
return Object(); 直接返回一個物件。如果建立一個物件再返回,需要呼叫建構函式,拷貝建構函式和解構函式;直接返回物件則直接把物件建立在外面,只調用建構函式
預設引數
在宣告時給定一個值,如果在呼叫時沒有指定這一引數的值,編譯器會自動插上這個值
int f( int a = 0 )
預設引數只能放在宣告中
只有引數列表的後部引數才是可預設的,不可以在一個預設引數後面,又跟一個非預設的引數
一旦在一個函式呼叫中開始使用預設引數,那麼這個引數後面的所有引數都必須是預設的
佔位符引數
void func( int a, int, int c )
a和c可以被引用,中間的引數不行
呼叫時,也需要為佔位符提供一個值
可以防止告警
選擇過載還是預設引數
基本原則是,不能把預設引數作為一個標誌去決定執行函式的哪一塊,如果出現這種情況,應該把函式分解成兩個或多個過載的函式
預設引數應該是一個在一般情況下放在這個位置的值,這個值出現的可能性比其他值要大,所以客戶程式設計師可以忽略它,或只在需要改變預設值的情況下才去用它
預設引數的意義是使函式呼叫更容易
預設引數的另一個重要應用是,開始定義函式時定義了一組引數,後來發現要增加一些引數。通過把新增的引數設定為預設引數,可以保證以前的程式碼不受影響
替代連結說明
在C++中引用C函式,C++編譯器會把函式名加上引數型別以支援過載,導致連結器找不到函式
extern "C" {}
成員指標
class Object{ int a,b,c; }
Object obj, pobj = &obj;
int Object::*p = &Object::a;
obj.*p = 1;
pobj->*p = 2;
p = &Object::b;
int (Object::*func)(int a) const = &Object::func;