Document 7505029

Download Report

Transcript Document 7505029

Chapter 11
Operator Overloading; String and
Array Objects
Part III (補充教材)
學習動機

C++語言在函式間的物件傳遞是該語言
未臻完善的部分,本單元將釐清以下觀
念:
◦ 參考與指標
◦ 函數呼叫的引數與傳回值傳遞
◦ 物件的複製:拷貝建構子與賦值運算子(
operator=)的多載
◦ 物件的引數與傳回值傳遞:
使用指標與參考
參考與指標
指標

C與C++使用指標以達到動態管理記憶體
的效果。
◦ 使用指標,我們必須宣告一個指標變數以儲
存變數的記憶體位址,並對此指標進行反參
照以存取該變數。
◦ 範例:
int a;
int *aPtr = &a;
cout << *aPtr << endl;
aPtr
a
指標應用—(1)

引數傳遞(且引數需要更動)
◦ 範例:
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void main()
{
int x=3, y=2;
swap(x, y);
}
將x, y的記憶體位址當成引數傳
遞給swap(),在swap()中,就可
以透過反參照找到原本在main函
式的變數。
指標應用—(2)

動態配置記憶體
◦ 範例:
void main()
{
int length;
cin >> length;
int *A = new int[length];
…
delete [] A;
}
 指標變數用來儲存新配置記憶體的位址,使用動
態配置記憶體管裡的代價是,使用者必須自行移
除配置的記憶體,否則會造成記憶體遺漏(
memory leak)。
參考
C++引入參考(reference)的觀念,用以
簡化指標中取址與反參照的動作。
 參考可視為一個「常數指標」,存取時
不需做反參照。

◦ 由於是「常數指標」,其在宣告的時候必須
給予初始值,且在宣告後便不得再行更改其
內容。
參考

範例:
int x = 2;
int &y = x;
//宣告y是一個指向x的參考
y = 3;
cout << x << endl;
◦ y視同是一個指向x的常數指標,使用參考
時不需在對其做反參照,因此在此我們將y
視為是x的「別名」;對y進行修改,即對x
本身進行修改。
參考應用—(1)

更動引數
◦ 範例:
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
void main()
{
int x=3, y=2;
swap(x, y);
}
x , y以參考的方式傳遞,a與b即
代表了原本的x與y。
缺點:
參數是否以參考方式傳遞,由函
式來定義,使用者在呼叫該函式
時無從由引數得知,引數在呼叫
後是否有可能被更動。
參考應用—(2)

在函式間傳遞物件
◦ C++是物件導向的程式語言,使用者定義
的類別中可能包含龐大的資料內容。函式
間引數與傳回值若為物件,以傳值(Passby-value)方式傳遞涉及大量的拷貝動作。
◦ 傳遞物件的指標會是比較有效率的作法。
 然而,此方面的設計為C++較脆弱的一環,不當
使用可能造成memory leak。
函式呼叫的引數與傳回值傳遞
函式呼叫的基本觀念

考慮以下這個簡單的例子:
int foo(int x, int c)
{
int temp;
…
return temp;
}
void main()
{
int g = foo(a, b);
}
函式呼叫的基本觀念

以上的程式碼被轉換成低階的組合語言:
push b
push a
call foo()
add sp, 8
mov g, register a
Locals of foo
SP
int、char、double等內建型別的回傳
值由於容量不大且大小固定,可先
寫入暫存器中保存。執行mov指令以
將暫存器中的內容複製到變數g中。
Return address
a
b
main
以物件作引數傳遞

考慮傳遞自訂型別的物件時:
class BigObj
{
char buf[100];
int i;
};
BigObj func(BigObj b)
{
…
return b;
}
int main()
{
BigObj B;
BigObj B2 = fun(B);
}
自訂類別的物件傳遞較為複雜;如
前述,作為引數的物件B,在呼叫
func時,其內容會被複製並整個被
push進stack中。
除了物件本身外,類別定義
的位址與大小也會額外被
push進stack。
以物件作傳回值傳遞
BigObj func(BigObj b)
{
…
return b;
}
int main()
{
BigObj B;
BigObj B2 = func(B);
}
自訂類別的物件大小可能很大,因
此不可能使用暫存器來暫存回傳物
件(因為暫存器的容量通常不大)
以物件作傳回值傳遞的時候,
通常程式會自動產生一個暫
存的物件,將傳回的物件的
內容複製到這個物件。
暫存的物件
b
copy
因此,在此類別的「拷
貝建構子」(copy
constructor)會被呼叫
接著,才由這個物件複製或
指定至main函式中的物件
BigObj B;
BigObj B2 = func(B);
b
暫存的物件T
T的拷貝建構子
呼叫B2的拷貝建構子
建立一個新的物件,其
內容為既存物件的複本
B2
BigObj B;
BigObj B3;
B3 = func(B);
b
暫存的物件T
T的拷貝建構子
呼叫B3的operator=
B3是一個既存的物件,兩個
既存物件的指定,會呼叫其
賦值運算子
B3
以物件作傳回值傳遞

即使在main函式中沒有任何的物件來接收,
此暫存的物件依舊會被建立。
BigObj B;
func(B);
b
暫存的物件T
T的拷貝建構子
◦ 此物件在回傳時建立(呼叫拷貝建構子),在函
式結束呼叫時被清除(呼叫解構子)。
 編譯器有時會特別針對程式碼最佳化,以避免過多的拷貝動作產生。例
如,當發現main函式中會產生一個新物件來接收回傳的物件時,編譯器
可能直接在main中就先建立這個新物件,讓副函式的回傳結果略過暫存
的物件,直接就複製到此新物件中。
物件的複製:
拷貝建構子與賦值運算子(
operator=)的多載
預設的物件拷貝動作

如果類別沒有給予拷貝建構子或多載賦值運算子(=)
,其預設的物件拷貝是將其成員內容逐一的複製。
◦ 範例:
class BigObj
{
char buf[100];
int i;
};
物件B
char buf[100]
void main()
{
BigObj A, B;
A = B;
}
物件A
int i
問題解析(1)

如果當類別中有成員變數是指標變數的時候,考慮以下類別:
class BigObj2
{
public:
BigObj2(int s=100)
{
capacity = (s > 0)? s : 100;
buf = new char[capacity];
}
~BigObj2()
{
delete [] buf;
}
private:
int capacity;
char *buf;
};
在這個類別中有一個指標變數,
指向一個字元的陣列;成員變數
capacity用來記住目前陣列的大小。
問題解析(2)

考慮以下程式碼將發生的情形:
BigObj2 func(BigObj2 X)
{
return X;
}
void main()
{
BigObj2 A, B;
B = func(A);
}
◦ func()接受一個物件做為參考,再將此物件回傳。
◦ main函式宣告兩個物件A與B,並將A傳入func後,
以B物件來接受其回傳值。
 請問:從傳入A至回傳,物件共被拷貝幾次?
問題解析(3)
main()
func()
main()
暫存的物件T
Copy
A

Copy
Constructor
Copy
X
Copy
Constructor
Copy
operator=
B
問題:
◦ 兩個物件間是否真的作了完全的拷貝?
 A物件陣列中的每個元素是否有逐一被拷貝到X
物件的陣列當中?
問題解析(4)

在預設的情況下,兩個指標變數的拷貝僅僅拷貝了記
憶體位址,非針對實體的資料進行拷貝。
物件B
int capacity
Shallow Copy
char *buf
30000
30000
預設的拷貝動作,
是對成員變數逐
一的複製
38000
38000
30000
物件A
問題解析(5)
main()
func()
main()
暫存的物件T
Copy
A
Copy
X
物件T
int capacity
char *buf
Copy
operator=
B
暫存的物件在函式結束呼
叫便會被清除
30000
?
30000
Dangling Pointer
38000
38000
30000
物件B
Memory Leak
自訂拷貝建構子與多載賦值運算子

使用時機
◦ 當成員變數含有指標變數的時候,最後提
供自訂以下函式:
 預設建構子
 不管什麼時機,都盡量提共一個預設建構子
 自訂的拷貝建構子
 多載賦值運算子(operator)
自訂拷貝建構子

呼叫時機:
◦ 從一個既存物件A中建立一個新的物件,且為A的複
本

函式原型:
◦ 沒有回傳值
◦ 一定只有一個參數,例:
const XXX &
 XXX代表類別本身的名稱
 一定是一個const的參考指向傳入的物件;代表該傳入的物件
不可被修改。
自訂拷貝建構子
class BigObj2
{
public:
…
BigObj2(const BigObj2 &source)
直接複製capacity
{
capacity = source.capacity;
根據capacity,再次new
buf = new char[capacity];
一個新的陣列
for (int i=0; i<capacity; i++)
buf[i] = source.buf[i];
}
逐一複製陣列中的元素
private:
…
};
注意:在此的複製並不是指標變數中記憶體位址
的複製,而是額外再建立一塊新的記憶體,並以
程式碼逐一複製元素。物件中的指標變數指向自
己的記憶體空間。
自訂拷貝建構子
Deep Copy
物件B
int capacity
char *buf
30000
自訂的拷貝建構
子,另外建立新
的記憶體空間來
儲存
38000
物件A
多載賦值運算子

呼叫時機:
◦ 將一個既存物件B指定給另一個既存的物件A的時候
 呼叫拷貝建構子
BigObj A = B;
BigObj A(B);
 呼叫賦值運算子(operator=)
BigObj A, B;
A = B;
A = func(B);

與實作拷貝建構子的差別
◦ 因為賦值運算子是拷貝到一個「已經存在」的物件
,表示原物件中已經有一些資料存在,故需增加一
個程式碼來對舊有資料作清除,且需要判斷指定的
物件是否就是自己本身。
多載賦值運算子
class BigObj2
{
public:
…
BigObj2 &operator=(const BigObj2 &source)
{
if (this == &source) //判斷指定的物件是否就是自己本身
return *this;
//例如: A = A;
if (buf != NULL)
//將自己所指向的記憶體釋回
delete [] buf;
capacity = source.capacity;
//以下同拷貝建構子的內容
buf = new char[capacity];
for (int i=0; i<capacity; i++)
buf[i] = source.buf[i];
return *this;
//將自己本身回傳
}
private:
…
};
多載賦值運算子
考慮 A = B
Deep Copy
物件B
兩個物件的指定,
此情況會呼叫
A.operator=()
int capacity
先判斷兩者是否
相等,如果不等,
先清除A中的舊
資料,再重新建
立新的記憶體。
新的記憶體空
間建立,再逐
一作元素的複
製
物件A
char *buf
多載賦值運算子

介面說明;以類別BigObj為例:
◦ 參數:const BigObj &source
 接受一個和自己同型態的物件參考,且指定
const,代表傳入的物件不可修改。
 注意傳入參考使得副函式具有更改到原物件的能力,加
上const可保證傳入的物件不會被副函式任意更動。
 通常賦值運算子會把自己(*this)回傳,回傳的
型態是一個指向該物件的參考。這樣做的目的是
為了實現「連鎖」的指定,例如:
A = B = C;
 此行敘述等同於
A.operator=(B.operator=(C));
物件的引數與傳回值傳遞:
使用指標與參考
使用指標或參考傳遞物件的動機
物件的引數或傳回值傳遞,涉及許多拷貝的
動作,當物件內的資料量很多的時候,造成
執行效能上很大的負擔。
 課題:如何降低過多的物件內容傳遞

◦ 使用指標
 僅傳遞物件的記憶體位址,而非整個物件的複製,可減少
引數與傳回值傳遞時的複製動作。
使用指標傳遞引數

先考慮以下的類別:
class Set
{
成員變數有指標變數,提供預
public:
設建構子、自訂的拷貝建構子
Set(int s=100);
~Set();
以及多載賦值運算子
Set(const Set &source);
Set &operator= (const Set &source);
Set Union(const Set *S);
private:
int capacity;
int *buf;
};
我們試圖實作Union,以執行集合
的聯集,請注意物件以指標的方式
來傳遞。請問:const是何意?
使用指標傳遞引數
Set Set::Union(const Set *S)
{
Set Result(capacity + S->capacity); //or, Set Result(capacity+(*S).capacity);
….
return Result;
}
void main()
{
Set A, B;
Set R = A.Union(&B);
}
◦ B物件並沒有整個把內容傳遞過去,而是將其「記憶體位址」
(注意取址運算子&)取出後複製給Union(注意Union以一個
指標變數的參數來接收)。如果要保護物件不被副函式所更
改,可加const修飾字在參數的最前面(指向常數資料的非常
數指標)
 好處:不需複製物件整個內容,提升執行效能
 缺點:取址再作反參照取值,在程式撰寫上較為不便
使用參考傳遞引數
class Set
{
…
Set Union(const Set &S);
…
};
Set Set::Union(const Set &S)
{
Set Result(capacity + S.capacity); //不需作反參照;X即代表B
….
return Result;
}
void main()
{
Set A, B;
Set R = A.Union(B); //不需取址
}
◦ 改善的方式是改用「參考」。
◦ 參考實際上是一個「常數指標」且不需作反參照,可直接將
參考當成原物件的「別名」來使用
 參考提供副函式找到原傳入物件的能力;意旨副函式可直接更動原引數內
容。因此,如果要限制副函式存取該引數物件的權限,同樣地,可以在參
考的前面加上「const」修飾字,代表該參考指向常數資料(指向常數資料
的常數指標)。
使用參考傳遞傳回值
參考觀念的引入雖帶來便利,但其概念與實質
執行的結果卻十分的隱誨,容易造成觀念的誤
解與誤用;在函式的傳回值使用上,要特別小
心。
 考慮前述的例子:

Set &Set::Union(Set &S) //為避免拷貝動作,因此將傳回值以參考傳回
{
//這樣寫是否會產生邏輯錯誤?
Set Result(capacity + S.capacity);
….
return Result;
}
void main()
{
Set A, B;
Set R = A.Union(B);
}
問題一:傳回區域物件的參考

如下例;Result是一個區域宣告的物件,最後副函式將指向這個
物件的參考傳回:
Set &Set::Union(Set &S)
{
Set Result(capacity + S.capacity);
….
return Result;
}
void main()
{
Set A, B;
Set R = A.Union(B);
}
main()
A.Union回傳的是其區域物件
Result的參考;代表將不會有
一個暫存的物件來儲存Result
的內容,而是直接將Result的
內容複製到main()的R中
A.Union()
B以其參考傳入;
在此S就是代表原
本的物件B
B
S
main()
暫存的物件
Result
copy
x
R
函式建立的區域物件
問題在此!由於Result是一個區域變數,因此會
在函式結束時被清除。意旨,在指定給R前,其
內容就會釋回,因此R將可能得到不正確的內容
問題二:傳回動態配置物件的指標

修改前例;用動態配置的方式來產生物件,最後再回傳之。
Set *Set::Union(Set &S)
{
Set *Result = new Result(capacity + S.capacity);
….
return Result;
}
void main()
{
Set A, B;
Set *R = A.Union(B);
delete R;
}
改以傳回記憶體位址的方式縱然可以解決前面提出的例子,但是以指標方式傳送意指
使用者必須自行管理記憶體。試想在main()如果忘記撰寫「delete R」此行,將使得該
物件變成「memory leak」,因此可能徒增程式實作的複雜度。
問題三:傳回動態配置物件的參考

那麼,如果不以指標傳回,改用參考傳回呢?
Set &Set::Union(Set &X)
{
Set *Result = new Result(capacity + X.capacity);
….
return *Result;
}
void main()
{
Set A, B;
Set R = A.Union(B);
}
A.Union()
A.Union回傳一個堆積(Heap,
或稱Free Store)的物件參考;
將不會有一個暫存的物件來儲
存其內容,而是直接將Result
指向的物件複製到main()的R中
這個動態配置的物件
並不是Union的區域
變數,位於堆積
(Heap)
暫存的物件
Result
x
main()
R
雖然這個物件不會因為函式結束執行而被清除,
但一旦其內容複製給R後,我們也無法再存取到
這個物件(memory leak)。
問題四:傳回動態配置物件的參考,以參考接收

那麼,如果不以指標傳回,改用參考傳回呢?
Set &Set::Union(Set &X)
{
Set *Result = new Result(capacity + X.capacity);
….
return *Result;
}
void main()
{
Set A, B;
Set &R = A.Union(B);
delete &R; //取出R的記憶體位址,將該記憶體空間釋回
}
A.Union()
暫存的物件
Result
這個動態配置的物件
並不是Union的區域
變數,位於堆積
(Heap)
x
main()
R
R就是代表這個位在堆積的物件。然而,一旦main函式
結束執行,並不會自動呼叫類別的解構子。為什麼?因
為R是一個指向物件的參考(常數指標),不是一個真
正的物件,main結束前如果忘記將其釋回,依舊會造成
memory leak。
傳回物件正確寫法一

如果你的類別中包含指標變數,且成員函式有必要回
傳一個新建立的物件時,請提供預設建構子、拷貝建
構子並多載賦值運算子,直接以傳值的方式傳回。
class Set
{
…
Set Union(const Set &B);
…
};
Set Set::Union(const Set &S)
{
Set Result(capacity + S.capacity); //不需作反參照;X即代表B
….
return Result;
}
void main()
{
Set A, B;
Set R = A.Union(B); //不需取址
}
傳回物件正確寫法一:圖示

用傳值的方式來傳遞,雖然產生大量的資料複製動作,卻是唯
一的方式使得資料正確傳遞(問題一),且保證物件在離開函
式時,皆能被系統自動刪除(問題二、三、四)
A.Union()
main()
暫存的物件
Result
函式建立的區域物件
copy
copy
R
傳回物件正確寫法二

另一種方法則是令函式不作傳回值的傳遞;而是將接收傳回值的
變數直接變成是函式的參數,並以參考傳遞,讓副函式可以直接
將傳回值寫進其參數內。
class Set
{
…
void Union(Set &R, const Set &S);
…
};
void Set::Union(Set &R, const Set &S)
{
//結果直接寫進R,因為是用參考傳遞,因此改變可直接對應到原本的引數
//試解釋為何此處的R,不宣告為const?
}
void main()
此方法雖然可在效能上與物件傳遞得到一個兩全其美的
{
平衡,但就使用的觀點而言,這樣的寫法顯得較為「古
Set A, B;
怪」。因為一般習慣上,我們常把參數當成輸入,回傳
Set R;
值視為輸出,這種寫法較不符一般的書寫原則。
A.Union(R, B);
}
什麼時候將物件以參考形式傳回?

物件如果本身已經存在,則將之以參考形式傳回較不
會有問題。
◦ 範例一:
class Set
{
…
Set &Union(const Set &S);
…
};
Set &Set::Union(const Set &S)
{
….
return *this;
//回傳自己本身,此成員函式結束後,*this並不會因此消失
}
void main()
{
Set A, B;
Set R = A.Union(B); //此行等同於R=A; 將A指定給R,因為Union把其所屬的物件回傳
}
什麼時候將物件以參考形式傳回?
◦ 範例二:
class Set
{
…
const Set &Union(const Set &S);
…
};
const Set &Set::Union(const Set &S)
{
….
return S;
//回傳物件參考,此物件參考指向原main函式中的B,本來就已經存在
//且在Union結束後也不會消失
//唯特別注意,其以const傳入(代表內容不可變),回傳也應是const
}
void main()
{
Set A, B;
Set R = A.Union(B); //此行等同於R=B; 將B指定給R,因為Union把指向B的參考回傳
}
總結
結論

現代其他程式語言已漸漸模糊指標的使用,強化參
考的觀念,並提供垃圾回收機制,使得系統有辦法
自動搜尋不被使用的參考,回收不再使用的記憶體
,以減少memory leak的情況
◦ 如Java、C#等
◦ 垃圾回收機制的代價也是增加系統的負擔;因為系統必須多
花時間來檢查過期的記憶體。

C++給予程式設計師充足的彈性與自由來管理,相
對地也令語法變得繁複。掌握正確觀念才可有效管
理記憶體,並開發出有效能的程式碼。
摘要—類別中有指標變數

如果類別中有指標變數,且此類別所宣告的物件有必
要作指定、引數與傳回值傳遞時,請記得務必要類別
中加入:
◦ 預設建構子
◦ 自訂的拷貝建構子
◦ 多載賦值運算子(operator)
摘要—引數傳遞

物件作引數傳遞時,以參考的方式傳遞可減少資料複
製動作。
◦ 引數以參考傳入,函式具有修改更改該引數的能力;如果要限
制該物件為唯讀,請記得在該參數宣告時加註「const」。
摘要—傳回值傳遞

物件作傳回值傳遞時直接以傳值方式傳遞即可
◦ 注意:如果類別中有指標變數時,請加入拷貝建構
子與多載賦值運算子
或者,不傳回任何值,改傳入一個物件的參考
作參數,將傳回值寫入該參數中。
 物件如以參考的形式傳回,則必須把握以下原
則:

◦ 物件已經存在(非在該副函式中才產生或建立)
◦ 物件在副函式結束之後不會因此而被清除。