【c++自學】第二章 類
第1節 成員函式、物件拷貝、私有成員
一、綜述
二、類基礎:struct、class不混用
三、成員函式:類定義是可以多次include的
四、物件的拷貝:每個成員變數逐個拷貝(可以控制:在Time中重新定義“=”)
五、私有成員:只能被成員函式呼叫
第2節 建構函式詳解、explicit、初始化列表
一、建構函式:建立類物件時系統自動呼叫的名字和類名相同的成員函式。
特點:① 沒有void,無返回值 ② 不可以手動呼叫 ③ 宣告為public ④ 建構函式中如果有引數,建立物件時也要加上
//宣告
Time(int, int, int);
//定義
Time::Time(int a, int b, int c){}
//呼叫
Time mytime = Time(10,12,15);
二、多個建構函式:只要有不同的引數就行
//物件拷貝:呼叫的是拷貝建構函式
Time mytime2 = mytime;
三、函式預設引數(宣告的時候賦初值)
1、預設值只能放在函式宣告中;
2、有一個是預設值,後面的也必須是預設值;
3、有預設值,物件初始化時可以少引數,也可以替換預設值。
四、隱式轉換和explicit
Time mytime3 = 16; //可以呼叫一個引數的建構函式,相當於把一個整型轉為一個物件
在建構函式宣告前加explicit可以阻止隱式型別轉換,一般單引數的建構函式都宣告為explicit。
五、建構函式的初始化列表(提倡!)
Time::Time(int a,int b):Hour(a),Minute(b){}
這種初始化方式比在 { } 裡初始化早,而且省去了賦值的過程,這裡的執行順序取決於成員變數的定義順序!
第3節 inline、const、mutable、this、static
一、在類定義中實現成員函式inline
直接在類定義中實現的成員函式會被當做inline函式處理。
二、成員函式末尾的const(常量成員函式)
成員函式(普通函式不行)的宣告和定義後都需要加const:不能修改成員變數的值。
若const Time abc; 則如果一個成員函式沒有宣告成const,該物件就不能呼叫此成員函式。
帶const的成員函式可以被帶/不帶const的物件呼叫!
三、mutable(不穩定、容易改變的意思)→ 突破const限制
針對二的情況,若想在const成員函式中修改成員變數的值,則需要mutable int a;
四、返回自身物件的引用,this
當呼叫成員函式時,編譯器負責把這個物件的地址(&mytime)傳遞給this形參
1、this指標只在成員函式中使用,全域性/靜態函式中都不能使用;
2、在普通成員函式中,this是一個指向非const物件的const指標(Time * const this);
3、在const成員函式中,this是一個指向const物件的const指標(const Time * const this)。
//宣告
Time& addhour(int tmphour);
//定義
Time& Time::addhour(int tmphour) {
Hour += tmphour; //將Hour改為this->Hour依然正確,可以防止形參和成員變數同名
return *this;
}
//這種返回型別可以連續呼叫
//使用
Time mytime;
mytime.addhour(3);
五、static成員
1、全域性 int g_abc =0; 別的cpp中只要 extern int g_abc; 即可使用該變數;
2、全域性 static int g_abc; 系統自動給初值0,只能本cpp使用;
void func(){
static int abc = 8; //再次呼叫該函式時這條語句不再執行
abc = 5;
}
3、static成員變數:屬於整個類的成員變數,每個物件呼叫的結果相同,不屬於某一個物件!
//宣告
static int mystatic;
//定義(分配記憶體),只能寫在一個cpp裡且要寫在最前面
int Time::mystatic = 15; //可以不給初值
第4節 類內初始化、預設建構函式、=default
一、類相關非成員函式
二、類內初始化
三、const成員變數初始化:在建構函式的初始化列表裡進行!
四、預設建構函式:沒有引數的建構函式
※ 若類中沒有任何建構函式,編譯器會隱式自動定義一個預設建構函式
Time() = default; //適用於預設建構函式
Time() = delete; //禁止預設建構函式
第5節 拷貝建構函式
拷貝建構函式是類的建構函式:第一個引數是該類的引用,如果有其他引數,都有預設值(宣告中)。
Time(const Time &tmptime, int a = 10); //習慣於加const,前面不加explicit
1、成員變數為整型等,直接拷貝;為類型別,呼叫此類的拷貝建構函式;
2、自己寫的拷貝建構函式覆蓋系統自動生成的“合成拷貝建構函式”;
3、其他呼叫拷貝建構函式的情況:
① 將一個物件作為實參傳遞給非引用的形參 ② 從一個函式中返回一個物件
Time func(){
Time tmptime;
return tmptime; //產生了臨時物件並拷貝
}
第6節 過載運算子、拷貝賦值運算子、解構函式
一、過載運算子:一個函式名為“operator運算子”的成員函式,函式體裡寫一個比價邏輯。
二、拷貝賦值運算子
//宣告
Time& operator=(const Time&);
//定義
Time& Time::operator=(const Time& tmpobj) { //const防止修改右側值
std::cout << "呼叫了operator=過載" << std::endl;
return *this;
}
//使用
Time mytime;
Time mytime2
mytime2 = mytime; //不過載是無法使用的
最後一行,等號左側為this物件,右側為operator=的引數!
三、解構函式(物件銷燬時自動呼叫)
無返回值,無任何引數,不能被過載,一個類只有一個!
預設解構函式函式體為空,不會釋放如建構函式中new的記憶體。
建構函式初始化:1、函式體之前 2、函式體之中
解構函式:1、函式體(銷燬new出的記憶體) 2、函式體之後(系統銷燬成員變數等)
第7節 派生類、呼叫順序、訪問等級、函式遮蔽
一、派生類概念
父類(基類、超類) VS 子類(派生類)
class 子類名 : 繼承方式 父類名 {}
二、派生類物件定義時呼叫建構函式的順序:父先子後(解構函式子先父後)
三、public、protected、private
protected訪問許可權:只允許本類或子類的成員函式來訪問。
四、函式遮蔽
子類中如果有一個與父類同名的函式,則子類物件無法訪問父類中的任何與此函式同名的函式;
如果想呼叫父類的函式,在子類的成員函式中用“父類::函式名(...)”強制呼叫。
第8節 基類指標、純虛擬函式、多型性、虛解構函式
一、基類指標、派生類指標
Human *phuman = new Men;
這樣寫父類指標phuman只能呼叫父類的成員函式eat()!
二、虛擬函式
比如父類和子類有同名同參函式 eat( ) 怎麼才能呼叫子類的函式呢?
方法:在父類中,將此函式宣告為虛擬函式,在函式宣告前加 virtual!
一旦父類中宣告為虛擬函式,則子類中相應的函式也自動宣告為虛擬函式。
為了避免在子類中寫錯虛擬函式,【c++11】中可以在子類函式聲明後加 override,這樣只要引數或函式名不一致就會報錯!
若想要子類無法覆蓋父類的虛擬函式,在父類的函式宣告末尾加 final 關鍵字:
virtual void eat() final;
三、多型性(只是針對虛擬函式的!)
※ 體現在具有繼承關係的父類和子類之間,子類重寫父類的成員函式 eat ( ),父類的 eat ( ) 宣告為 virtual 型別即可。通過父類指標,到程式執行時,找到動態繫結的物件,系統內部查一個虛擬函式表,找到函式 eat ( ) 入口地址呼叫,這就是執行時期的多型性。
四、純虛擬函式
純虛擬函式為在父類中宣告的虛擬函式,沒有定義,但是要求子類中必須定義對該函式自己的實現方法(每個都要)。
virtual void eat2() = 0; //只宣告即可
※ 一旦類中有純虛函數了,就不能生成該類的物件了,這個類也被叫做抽象類(不能例項化)。
五、基類的解構函式一般寫成虛擬函式
如果程式碼這麼寫:
Human *phuman = new Men;
delete phuman;
顯然只會呼叫父類的解構函式而不會呼叫子類的解構函式,造成記憶體沒有釋放的嚴重後果,所以:
如果一個類想要做父類,務必把這個類的解構函式寫成 virtual 的!!!
第9節 友元函式、友元類、友元成員函式(打破許可權修飾符的限制)
一、友元函式
//外界函式
void func(const Men& tmp){
tmp.funmen();
}
如果 funmen() 函式定義為 private 許可權,則 func() 中無權呼叫 funmen();
但是隻要讓 func() 成為類 Men 的友元函式,func() 就能訪問類 Men 中所有成員變數和成員函式。
只需在類 Men 中任意位置加上:
friend void func(const Men& tmp);
二、友元類:類可以把其他類定義為友元類,其他類就能訪問該類的所有成員。
1、友元類不能被繼承
2、友元關係是單向的
3、友元關係沒有傳遞性
三、友元成員函式
比如在類A中可以這樣宣告一個類C中的友元函式:
friend void C::ctest(int,A&);
但要記住:只有public的函式才能成為其他類的友元成員函式!
第10節 RTTI、dynamic_cast、typeid、虛擬函式表
一、RTTI(Run Time Identification):執行時型別識別
程式能夠使用父類的指標或引用來檢查這些指標或引用所指物件的實際派生型別。
二、dynamic_cast(基類指標 => 派生類指標,不能轉成其他類型別指標)
Human *phuman = new Men;
phuman->menfunc();
如上程式碼,若menfunc()函式只存在於類Men中,則第二行是無法呼叫的,因為phuman終歸是Human類的一個指標。
Human *phuman = new Men;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
此時加入一行將類Human的指標轉成pmen類的指標即可。
三、typeid
返回指標或引用所指物件的實際型別,主要為了比較兩個指標是否指向同一種類的物件。
Human *phuman = new Men;
Human *phuman2 = new Women;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
此時phuman和phuman2同類型,pmen和phuman、phuman2都不同型別。
※ 若想二、三起作用,父類中必須有一個虛擬函式!!!
※ 只有虛擬函式存在了,這兩個運算子才會使用指標或者引用所繫結的物件的動態型別!
四、type_info類
Human *phuman = new Men;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
cout << typeid(phuman).name() << endl;
cout << typeid(pmen).name() << endl;
const type_info &tp = typeid(*phuman);
cout << tp.name() << endl;
//輸出
//class Human *
//class Men *
//class Men
第11節 基類與派生類關係的詳細再探討
一、派生類物件模型簡介
二、派生類建構函式
如何傳遞引數給基類建構函式?在子類的初始化列表裡進行!
//父類
A(int i):m_valuea(i){}
//子類
B(int i,int j):A(j),m_valueb(i){} //一般成員變數宣告為m_xxx意為member
三、既當父類又當子類
繼承關係一直傳遞,最終son類會包含直接基類的成員以及每個間接基類的成員。
四、不想當基類的類
【c++11】final 加在類名後邊,此類就無法做基類了!
五、靜態型別與動態型別
1、靜態型別:變數宣告時的型別(編譯時已知)
2、動態型別:指標或引用表達的記憶體中物件的型別(執行時才知)
所以只有這種情況才談得到靜態型別和動態型別:
Human *phuman = new Men;
Human &q = *phuman;
六、派生類向基類的隱式轉換
並不存在從基類到派生類的自動型別轉換,若基類中有虛擬函式,可以用dynamic_cast轉換!
七、父類子類之間的拷貝與賦值
用派生類物件定義並初始化基類物件,導致Human的拷貝建構函式執行。
Men men;
Human human(men);
第12節 左值、右值、左值引用、右值引用、move
一、左值和右值
左值:能在賦值語句左側的東西,不是左值,就是右值!
用到左值的運算子:
a)賦值運算子 (a=4)=8 b)取地址& c)string、vector下標、迭代器 d)i++
二、左值引用
//允許
const int &c = 1;
//等價於
int temp = 1;
const int &c = temp;
臨時變數被系統當右值!
三、右值引用
string &&c{"xzh"};
五、總結
返回左值表示式:返回左值引用的函式、賦值、下標、解引用、前置遞增(減)運算子
返回右值表示式:返回非引用型別的函式、算術、關係、位、後置遞增(減)運算子
i++:先產生一個臨時變數temp,temp = i,再i加1,返回的是temp
++i:系統直接給變數i加1,然後返回i本身
int i = 1;
int &r1 = ++i;
int &&r2 = i++;
1、r2是右值引用,但r2是一個左值
2、變數一般都是左值
3、函式的形參都是左值
4、臨時物件都是右值
右值引用的目的:c++11引入,&&代表一種新資料型別,提高了系統效率,把拷貝物件變成了移動物件。
六、move函式:把一個左值強制轉換成一個右值(沒有移動操作)
int i = 1;
int &&r = std::move(i); //r、i穿一條褲子了
第13節 臨時物件深入探討、解析、提高效能手段
程式碼書寫問題產生臨時變數:
1、以傳值方式給函式傳遞引數
結果:多呼叫一次拷貝建構函式和解構函式
方法:把形參改為引用
2、型別轉換
A a;
a = 100;
對於第二行,系統首先建立臨時物件,以100為第一個引數呼叫建構函式,再依次呼叫拷貝賦值運算子和解構函式。
方法:用 A a = 100;這種方式替代
3、函式返回物件的時候
有時候return temp的時候,因為temp是函式內的臨時物件,無法返回到外面,故系統會自動生成一個臨時物件,額外呼叫一次拷貝建構函式和解構函式。
方法:儘量用一個物件去接這個函式的返回物件。
第14節 物件移動、移動建構函式、移動賦值運算子
一、移動建構函式
移動並不是地址遷移,只是所有者的轉移!移動後,被移動的無法再繼續使用了!
class A {
public:
//建構函式
A():m_pb(new B()){
cout << "A的建構函式" << endl;
}
//拷貝建構函式
A (const A& tmp) noexecpt :m_pb(new B(*(tmp.m_pb))){
cout << "A的拷貝建構函式" << endl;
}
//移動建構函式
A(A&& tmp):m_pb(tmp.m_pb){ //臨時物件指向物件B
tmp.m_pb = nullptr; //刪除原來的指向
cout << "A移動建構函式" << endl;
}
//解構函式
virtual ~A(){
delete m_pb; //記得刪除new的記憶體
cout << "A的解構函式" << endl;
}
public:
B *m_pb;
};
習慣性在移動建構函式的宣告、定義後加noexcept,為了使編譯器不丟擲異常。
A a = geta(); //呼叫移動建構函式
A a1(a); //呼叫拷貝建構函式,因為a是左值
A a2(std::move(a)); //呼叫移動建構函式
A &&a3(std::move(a)); //只是a多了個別名a3
二、移動賦值運算子
要想呼叫移動賦值運算子,也需要將左值轉成右值!
//拷貝賦值運算子
A& operator=(const A& tmp) {
if (this == &tmp)
return *this;
delete m_pb;
m_pb = new B(*(tmp.m_pb));
cout << "A的拷貝賦值運算子" << endl;
return *this;
}
//移動賦值運算子
A& operator=(A&& tmp) noexcept{
delete m_pb;
m_pb = tmp.m_pb;
tmp.m_pb = nullptr;
cout << "A的移動賦值運算子" << endl;
return *this;
}
三、合成的移動操作(某些條件下編譯器能自動合成一、二)
1、如果一個類定義了自己的拷貝建構函式、拷貝賦值運算子、解構函式,編譯器就不會自動合成一和二
2、如果一個類沒有一和二,編譯器呼叫拷貝建構函式和拷貝賦值運算子代替
3、只有一個類沒有定義任何自己的拷貝構造成員且每個非靜態成員都可以移動(內建型別或有移動操作的類型別),系統才會自動呼叫一和二
※ 儘量給類增加一和二來減少拷貝建構函式和拷貝賦值運算子的使用!
※ 不要忘記noexcept、nullptr、delete的使用!
第15節 繼承的建構函式、多重繼承、虛繼承