Coursera課程筆記—-C++程序設計—-Week3
類和對象(Week 3)
內聯成員函數和重載成員函數
內聯成員函數
- inline + 成員函數
- 整個函數題出現在類定義內部
class B{
inline void func1(); //方式1
void func2() //方式2
{
};
};
void B::func1(){}
成員函數的重載及參數缺省
- 重載成員函數
- 成員函數——帶缺省參數
#include<iostream>
using namespace std;
class Location{
private:
intx,y;
public:
void init(int x=0,int y=0); //存在2個缺省參數
void valueX(int val) {x = val;}//1
int valueX(){return x;}//2
//1和2是重載函數
}
- 使用缺省參數要注意避免有函數重載時的二義性
構造函數
基本概念
- 成員函數的一種
- 名字與類名相同,可以有參數,不能有返回值(void也不行)
- 作用時對對象進行初始化,如給成員變量賦初值
- 如果定義類的時候沒寫構造函數,則編譯器生成一個默認的無參數的構造函數
- 默認構造函數無參數,無任何操作
- 如果定義了構造函數,則編譯器不生成磨人的無參數的構造函數
- 對象生成時構造函數自動被調用。對象一旦生成,就再也不能在其上執行構造函數
- 一個類可以有多個構造函數
- 為什麼需要構造函數
- 執行必要初始化工作,不需要專門再寫初始化函數
- 有時對象沒被初始化就使用,會導致程序出錯
class Complex{
private:
double real,imgae;
public:
void Set(double r, double i);
};//編譯器自動生成默認的構造函數
Complex c1;//默認的構造函數被調用
Complex* pc = new Complex;//默認的構造函數被調用
class Complex{
private:
double real,imgae;
public:
Complex(double r, double i = 0);
};
Complex::Complex(double r, double i){
real = r;
imag = i;
}
Complex c1; //error,缺少構造函數的參數
Complex *pc = new Complex;//error,沒有參數
Complex c1(2); //OK
Complex c1(2,4),c2(3,5);
Complex *pc = new Complex(3,4);
- 可以有多個構造函數,參數個數或類型不同
class Complex{
private:
double real,imgae;
public:
void Set(double r, double i);
Complex(double r,double i);
Complex(double r);
Complex(Complex c1,Complexc2);
};
Complex::Complex(double r, double i)
{
real = r;imag = i;
}
Complex::Complex(double r)
{
real = r; imag = 0;
}
Complex::Complex(Complex c1, Complex c2)
{
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
Complex c1(3),c2(1,0),c3(c1,c2);
//c1={3,0} c2={1,0} c3={4,0};
構造函數在數組中的使用
class CSample{
int x;
public:
CSample(){
cout<<"Constructor 1 Called"<<endl;
}
CSample(int n){
x = n;
cout<<"Constructor 2 Called"<<endl;
}
};
int main()
{
CSample array1[2]; // 1 1
cout<<"step1"<<endl;
CSample array2[2] = {4,5};//2 2
cout<<"step2"<<endl;
CSample array3[2] = {3};//2 1
cout<<"step3"<<endl;
CSample *array4 = new CSample[2];//1 1
delete []array4; // 收回空間
return 0;
}
class Test{
public:
Test(int n){ }//(1)
Test(int n, int m){ }//(2)
Test(){ }//(3)
};
Test array1[3] = {1,Test(1,2)} //三個元素分別用(1),(2),(3)進行初始化
Test array2[3] = {Test(2,3),Test(1,2),1};//2 2 1
Test * pArray[3] = {new Test(4), new Test(1,2)};//1 2 ❌
複製構造函數(copy constructor)
- 基本概念
- 只有一個參數,即對同類對象的引用
- 形如 X::X( X&)或X::X(const X &),二選一,後者能以常量對象作為參數
- 如果沒有定義複製構造函數,那麼編譯器生成默認複製構造函數,默認的複製構造函數完成複製功能
- 如果定義了複製構造函數,默認的複製構造函數將不存在。
class Complex{
private:
double real,imag;
};
Complex c1;//調用缺省無參構造函數
Complex c2(c1);//調用缺省的複製構造函數,將c2初始化成和c1一樣
class Complex{
public:
double real,imag;
Complex(){ }
Complex(const Complex & c){
real = c.real;
imag = c.imag;
cout<<"Copy Constructor called";
}
};
Complex c1;
Complex c2(c1);
- 注意
- 不允許有形如X::X(X)的構造函數
class CSample{
CSample(CSample c){
//error,不允許這樣的構造函數
}
}
-
複製構造函數起作用的三種情況
-
當用一個對象去初始化同類的另一個對象時
Complex c2(c1); Complex c2 = c1; //初始化語句,非賦值語句
-
如果某函數有一個參數是類A的對象,那麼該函數被調用時,類A的複製構造函數將被調用
class A { public: A(){ }; A(A & a){ cout<<"Copy constructor called"<<endl; } } void Func(A a1){ } int main(){ A a2; Func(a2); return 0; }
-
如果函數的返回值時類A的對象時,則函數返回時,A的複製構造函數將被調用
-
-
為什麼要自己寫複製構造函數
- 後續會講解
類型轉換構造函數
- 目的
- 實現類型的自動轉換
- 特點
- 只有一個參數
- 不是複製構造函數
- 編譯系統會自動調用➡️轉換構造函數➡️建立一個臨時對象/臨時變量
class Complex{
public:
double real,imag;
Complex(int i){//類型轉換構造函數
cout<<"IntConstructor called"<<endl;
real = i;
imag = 0;
}
Complex(double r, double i) //傳統構造函數
{
real = r;
imag = i;
}
};
iint main()
{
Complex c1(7,8);//對c1進行初始化,調用傳統構造函數
Complex c2 = 12; //對c2進行初始化,調用類型轉換構造函數,不會生成一個臨時對象
c1 = 9; // 賦值語句,雖然賦值號兩邊類型不同,但是編譯器沒有報錯。
//編譯器以9作為實參,調用類型轉換構造函數,9被自動轉換成一個臨時Complex對象,賦值給c1
}
析構函數(Destructor)
- 回顧:構造函數
- 成員函數的一種
- 名字與類名相同
- 可以有參數,不能有返回值
- 可以有多個構造函數
- 用來初始化對象
- 析構函數
- 成員函數的一種
- 名字與類名相同(在函數名之前加’~’)
- 無參數,無返回值
- 一個類最多只能有一個析構函數
- 在對象消亡時,自動被調用
- 在對象消亡前做善後工作
- 釋放分配的空間等
- 在對象消亡前做善後工作
- 定義類時沒寫析構函數,則編譯器生成缺省析構函數
- 不涉及釋放用戶申請的內存釋放等清理工作
- 定義了析構函數,則編譯器不生成缺省析構函數
class String{
private:
char *p;
public:
String(){
p = new char[10];
}
~String();
};
String::~String(){
delete [] p;
}
- 析構函數和數組
- 對象數組生命周期結束時,對象數組的每個元素的析構函數都會被調用
- 析構函數和運算符delete
- delete運算導致析構函數調用
- 例題總結
- 先被構造的對象,最後被析構掉(平級的情況下)
- 構造函數和析構函數在不同編譯器中
- 個別調用情況不一致
- 編譯器有bug
- 代碼優化措施
- 課程中討論的是C++標準的規定,不牽扯編譯器的問題
- 個別調用情況不一致
靜態成員變量和靜態成員函數
基本概念
- 靜態成員:在說明前面+static關鍵字的成員,有靜態成員變量和靜態成員函數
- 普通成員變量每個對象有各自的一份,而靜態成員變量一共一份,所有對象共享。
- sizeof運算符不會計算靜態成員變量
- 普通成員函數必須具體作用於某個對象,而靜態成員函數並不具體作用於某個對象
- 靜態成員不需要通過對象就能訪問
- 靜態成員變量本質上是全局變量
- 靜態成員函數本質上是全局函數
- 設置靜態成員這種機制,目的是將和某些類緊密相關的全局變量和函數寫進類中,看上去像一個整體,易於維護和理解
如何訪問靜態成員
- 類名::成員名
- 對象名.成員名
- 指針->成員名
- 引用.成員名
靜態成員示例
- 考慮一個需要隨時知道矩形總數和總面積的圖形處理程序
- 可以用全局變量來記錄總數和總面積(造成變量和類之間的關係不直觀,且變量能夠被其他類訪問,存在一定風險)
- 用靜態成員將這兩個變量封裝進類中,更容易理解和維護
- 必須在定義類的文件中對靜態成員變量進行一次說明or初始化,否則編譯能通過,鏈接不能通過
注意事項
- 在靜態成員函數中,不能訪問非靜態成員變量,也不能調用非靜態成員函數
- 考慮到複製構造函數的影響
成員對象和封閉類
成員對象
- 一個類的成員變量是另一個類的對象
- 包含成員對象的類叫封閉類(Enclosing)
class CTyre{ //輪胎類
private:
int radius;
int width;
public:
CTyre(int r,int w):radius(r),width(w){ } //這種風格看起來更好一些
};
class CEngine{ //引擎類
}
class CCar{ //汽車類➡️「封閉類」
private:
int price;//價格
CTyre tyre;
CEngine engine;
public:
CCar{int p, int tr, int tw};
};
CCar::CCar(int p, int tr,int w):price(p),tyre(tr,w){ };
int main()
{
CCar car(20000,17,225);
return 0;
}
-
如果CCar類不定義構造函數,則:
-
CCar car;//error➡️編譯錯誤
-
編譯器不知道car.type該如何初始化
-
car.engine的初始化沒有問題,可以用默認構造函數
-
-
生成封閉類對象的語句➡️明確「對象中的成員對象」➡️如何初始化
封閉類構造函數的初始化列表
-
定義封閉類的構造函數時,添加初始化列表
-
類名::構造函數(參數表):成員變量1(參數表),成員變量2(參數表),…
{
……
}
-
成員對象初始化列表中的參數
- 任意複雜的表達式
- 函數/變量/表達式中的函數,變量有定義
-
調用順序
- 當封閉類對象生成時
- 執行所有成員對象的構造函數
- 執行封閉類的構造函數
- 成員對象的構造函數調用順序
- 和成員對象在類中的說明順序一致
- 與在成員初始化列表中出現的順序無關
- 當封閉類對象消亡時
- 執行封閉類的析構函數
- 執行成員對象的析構函數
- 先構造的後析構,後構造的先析構
友元
友元函數
- 一個類的友元函數可以訪問該類的私有成員
class CCar; // 提前聲明CCar類,以便後面CDriver類使用
class CDriver{
public:
void ModifyCar(CCar* pCar); //改裝汽車
};
class CCar{
private:
int price;
friend int MostExpensiveCar(CCar cars[],int total); //聲明友元
friend void CDriver::ModifyCar(CCar *pCar);
}
void CDriver::ModifyCar(CCar *pCar)
{
pCar->price +=1000; //汽車改裝後價值增加
}
int MostExpensiveCar(CCar cars[],int total)//求最貴的汽車的價格
{
int tmpMax = -1;
for(int i = 0; i < total;++i)
if(cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main()
{
return 0;
}
- 可以將一個類的成員函數(包括構造,析構函數)定義成另一個類的友元
class B{
public:
void function();
};
class A{
friend void B::function();
};
友元類
- A是B的友元類➡️A的成員函數可以訪問B的私有成員
- 友元類之間的關係,不能傳遞,不能繼承
class CCar{
private:
int price;
friend class CDriver; //聲明CDriver為友元類
};
class CDriver{
public:
CCar myCar;
void ModifyCar(){
myCar.price += 1000; //CDriver是CCar的友元類➡️可以訪問其私有成員
}
};
int main(){return 0;}
this指針
this指針作用
- 指向成員函數所作用的對象
- 非靜態成員函數中可以直接使用this來代表指向該函數作用的對象的指針
class Complex{
public:
double real,imag;
void Print(){
cout<<real<<","<<imag;
}
Complex(double r, double i):real(r),imag(i){ }
Complex AddOne(){
this->real++; //=real++
this->Print();//=Print()
return *this;
}
};
int main()
{
Complex c1(1,1),c2(0,0);
c2 = c1.AddOne();
return 0;
}
class A{
int i;
public:
void Hello(){cout<<"hello"<<endl;}
};//編譯器把該成員函數編譯成機器指令後,會變成
//void Hello(A *this){cout<<"hello"<<endl;}
//如果Hello函數變成 void Hello(){cout<<i<<"hello"<<endl;}
//就會出錯
int main()
{
A *p = NULL;
p->Hello(); //結果會怎樣?
}//輸出:hello
//編譯器把該成員函數編譯成機器指令後,會變成
//hello(p)
注意事項
- 靜態成員函數中不能使用this指針
- 因為靜態成員函數並不具體作用於某個對象
- 因此,靜態成員函數的真實的參數的個數,就是程序中寫出的參數的個數
常量對象、常量成員函數和常引用
常量對象
- 如果不希望某個對象的值被改變,則定義該對象的時候可以在前面加const關鍵字
常量成員函數
- 在類的成員函數說明後面可以加const關鍵字,則該成員函數成為常量成員函數
- 常量成員函數執行期間不應修改其所作用的對象。因此,在常量成員函數中不能修改成員變量的值(靜態變量除外),也不能調用同類的非常量成員函數(靜態成員函數除外)
class Sample
{
public:
int value;
void GetValue() const;
void func(){};
Sample(){}
};
void Sample::GetValue() const
{
value = 0;//wrong
func();//wrong
}
int main(){
const Sample o;
o.value = 100; //err,常量對象不可以被修改
o.func();//err常量對象上面不能執行非常量成員函數
o.GetValue();//ok
return 0;
}
常量成員函數的重載
- 兩個成員函數,名字和參數表都一樣,但是一個是const一個不是,算重載
常引用
- 引用前面可以加const關鍵字,成為常引用。不能通過常引用,修改其引用的變量
const int & r = n;
r = 5;//error
n = 4;//ok
- 對象作為函數的參數時,生成該參數需要調用複製構造函數,效率比較低。用指針做參數,會讓代碼的可讀性變差
- 所以可以用對象的引用作為參數。
- 但對象引用作為參數有一定的風險,若函數中不小心修改了形參,則實參也會跟着改變,如何避免?
- 所以可以用對象的常引用作為參數
- 這樣函數中就能確保不會出現無意中更改形參值的語句了
練習題
註:填空題在Coursera提交時,文件中只需出現填進去的內容即可
Quiz 1
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class A {
public:
int val;
A (int n = 0){val = n;}
A & GetObj(){
return *this;
}
};
int main() {
A a;
cout << a.val << endl;
a.GetObj() = 5;
cout << a.val << endl;
}
Quiz 2
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class Sample{
public:
int v;
Sample(int n):v(n) { }
Sample(const Sample &a)
{
v = a.v*2;
}
};
int main() {
Sample a(5);
Sample b = a;
cout << b.v;
return 0;
}
Quiz 3
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
using namespace std;
class Base {
public:
int k;
Base(int n):k(n) { }
};
class Big {
public:
int v;
Base b;
Big(int n = 5):v(n),b(n){ };
};
int main() {
Big a1(5); Big a2 = a1;
cout << a1.v << "," << a1.b.k << endl;
cout << a2.v << "," << a2.b.k << endl;
return 0;
}
Quiz 4 魔獸世界之一:備戰
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
const int WARRIOR_NUM = 5;
/*
string Warrior::names[WARRIOR_NUM] = { "dragon","ninja","iceman","lion","wolf" };
紅方司令部按照 iceman、lion、wolf、ninja、dragon 的順序製造武士。
藍方司令部按照 lion、dragon、ninja、iceman、wolf 的順序製造武士。
*/
class Headquarter;
class Warrior
{
private:
Headquarter * pHeadquarter; //指向英雄所屬陣營的指針
int kindNo;//武士的種類編號 0 dragon 1 ninja 2 iceman 3 lion 4 wolf
int no;//英雄編號
public:
static string names[WARRIOR_NUM]; //存放5種職業名字的數組
static int initialLifeValue [WARRIOR_NUM]; //存放不同英雄的起始生命值(從輸入中採集)
Warrior( Headquarter *p,int no_,int kindNo_);//構造函數
void PrintResult(int nTime); //執行打印數據的工作,若無法繼續創建則輸出結束並停止
};
class Headquarter
{
private:
int totalLifeValue;
bool stopped;
int totalWarriorNum;
int color;
int curMakingSeqIdx; //當前要製造的武士是製造序列中的第幾個
int warriorNum[WARRIOR_NUM]; //存放每種武士的數量
Warrior * pWarriors[1000];//和每個創建的英雄建立鏈接
public:
friend class Warrior;
static int makingSeq[2][WARRIOR_NUM];//武士的製作序列,按陣營的不同分成兩個
void Init(int color_, int lv); //初始化陣營需要顏色和總血量
~Headquarter();
int Produce(int nTime); //創建英雄,輸入時間
string GetColor();
};
Warrior::Warrior(Headquarter *p, int no_, int kindNo_) {
no = no_;
kindNo = kindNo_;
pHeadquarter = p;
}
void Warrior::PrintResult(int nTime) {
string color = pHeadquarter->GetColor();
printf("%03d %s %s %d born with strength %d,%d %s in %s headquarter\n",
nTime, color.c_str(), names[kindNo].c_str(),no,initialLifeValue[kindNo],
pHeadquarter->warriorNum[kindNo],names[kindNo].c_str(),color.c_str()); // string 在printf中輸出的函數調用c_str()
}
void Headquarter::Init(int color_, int lv) {
color = color_;
totalLifeValue = lv;
totalWarriorNum = 0;
stopped = false;
curMakingSeqIdx = 0;
for (int i = 0; i < WARRIOR_NUM; i++) {
warriorNum[i] = 0;
}
}
Headquarter::~Headquarter() {
for (int i = 0; i < totalWarriorNum; i++) {
delete pWarriors[i];
}
}
int Headquarter::Produce(int nTime) {
if(stopped)
return 0;
int searchingTimes = 0;
while(Warrior::initialLifeValue[makingSeq[color][curMakingSeqIdx]] > totalLifeValue &&
searchingTimes < WARRIOR_NUM)
{
curMakingSeqIdx = (curMakingSeqIdx + 1) % WARRIOR_NUM;
searchingTimes++;
}
int kindNo = makingSeq[color][curMakingSeqIdx];
if(Warrior::initialLifeValue[kindNo] > totalLifeValue)
{
stopped = true;
if(color == 0)
printf("%03d red headquarter stops making warriors\n",nTime);
else
printf("%03d blue headquarter stops making warriors\n",nTime);
return 0;
}
//排除所有其他條件後,開始製作士兵
totalLifeValue -= Warrior::initialLifeValue[kindNo];
curMakingSeqIdx =( curMakingSeqIdx + 1) % WARRIOR_NUM;
pWarriors[totalWarriorNum] = new Warrior(this,totalWarriorNum+1,kindNo);
warriorNum[kindNo]++;
pWarriors[totalWarriorNum]->PrintResult(nTime);
totalWarriorNum++;
return 1;
}
string Headquarter::GetColor() {
if(color == 0)
return "red";
else
return "blue";
}
string Warrior::names[WARRIOR_NUM] = {"dragon","ninja","iceman","lion","wolf"};
int Warrior::initialLifeValue[WARRIOR_NUM];
int Headquarter::makingSeq[2][WARRIOR_NUM]={{2,3,4,1,0},{3,0,1,2,4}};//兩個司令部武士的製作順序序列
int main()
{
int t;
int m;
Headquarter RedHead,BlueHead;
scanf("%d", &t); //讀取case數
int nCaseNo = 1;
while(t--){
printf("Case:%d\n",nCaseNo++);
scanf("%d",&m);//讀取基地總血量
for (int i = 0; i < WARRIOR_NUM; i++) {
scanf("%d",&Warrior::initialLifeValue[i]);
}
RedHead.Init(0,m);
BlueHead.Init(1,m);
int nTime = 0;
while (true){
int tmp1 = RedHead.Produce(nTime);
int tmp2 = BlueHead.Produce(nTime);
if( tmp1 == 0 && tmp2 == 0)
break;
nTime++;
}
}
return 0;
}
//老師給的答案讀了好幾遍,大概捋順了……
//現階段自己根本寫不出來這種程序,在第一步抽象出兩個類這塊就感覺很困難
//慢慢加油吧……