Motivations(為什麼要做 operation overloading) and prerequisites (基本知識)
( overloading 的情境) :
Overloading comparison and indexing operators
Overloading assignment and self-assignment operators
Overloading addition operators
Recall MyVector
class
class MyVector
{
private:
int n;
double* m;
public:
// constructors
MyVector();
MyVector(int dim, double v[]);
// copy default constructor
MyVector(const MyVector& v);
// destructor
~MyVector();
// print function
void print();
};
MyVector::MyVector()
{
n = 0;
m = nullptr;
}
MyVector::MyVector(int dim, double v[])
{
n = dim;
m = new double[dim];
for (int i = 0; i < dim; i++)
m[i] = v[i];
}
MyVector::MyVector(const MyVector& v) // called by reference
{
n = v.n;
m = new double[n];
for (int i = 0; i < n; i++)
m[i] = v.m[i];
}
MyVector::~MyVector()
{
delete[] m;
}
void MyVector::print()
{
cout << "(";
for (int i = 0; i < n - 1; i++)
cout << m[i] << ", ";
cout << m[n - 1] << ")\n";
}
可加入一些 member function:
MyVector
中的 objects (也就是向量): (當他們的 dimensions 都相同時)
在這個例子裡,因為是比較兩個 vector 之間的大小,所以理論上應該就是 instance function。
class MyVector
{
private:
int n;
double* m;
public:
// constructors
MyVector();
MyVector(int dim, double v[]);
// copy default constructor
MyVector(const MyVector& v);
// destructor
~MyVector();
// print function
void print();
bool isEqual(const MyVector& v); // 傳reference 會比較省時 const 要記得
};
bool MyVector::isEqual(const MyVector& v)
{
if (n != v.n)
return false;
else
{
for (int i = 0; i < n; i++)
{
if (m[i] != v.m[i])
return false;
}
}
return true;
}
所以可以理解等等 function 的使用就會是 u.isEqual(v)
這樣就可以比較兩個 vector 了。
使用:
int main()
{
double d1[5] = { 1, 2, 3, 4, 5 };
MyVector a1(5, d1); // (1)
double d2[4] = { 1, 2, 3, 4 };
MyVector a2(4, d1); // (2)
MyVector a3(a1); // (3)
cout << (a1.isEqual(a2)? "Y" : "N"); // N
cout << "\n";
cout << (a1.isEqual(a3) ? "Y" : "N"); // Y
cout << "\n";
}
在這邊,可以看到 isEqual
成功的判斷了 a1, a2不同;a1, a3 相同(因為基本上就是直接 deep copy)。
雖然 isEqual
很方便,但是在比較兩個變數是不是相等時,我們很常會使用 if(a1 == a2)
這樣子,不但方便又快速。
但是問題是,因為 MyVector
是我們自己創造的type,電腦並不知道這個==
對於兩個 MyVector
要做甚麼事。
因此 ! 我們需要對這個 ==
去 define 當他遇到兩個 MyVector
時該做甚麼事(這也就叫做 overloading)。
前情提要 Restriction of Overloading :
當你建立一個 object 時,勢必會佔據一段 memory space。
而每個 object 都擁有他們自己的位置。
this 是一個指標,儲存 object 的地址。
#include<iostream>
using namespace std;
class A
{
private:
int a;
public:
void f() { cout << this << "\n"; }
A* g() { return this; }
};
int main()
{
A obj;
cout << &obj << "\n"; // 0053FD0C
obj.f(); //0053FD0C
cout << (&obj == obj.g()) << "\n"; // 1 (true)
return 0;
}
那 this 可以拿來幹嘛?
print()
我們原本寫的 print()
是這樣:
void MyVector::print()
{
cout << "(";
for (int i = 0; i < n - 1; i++)
cout << m[i] << ", ";
cout << m[n - 1] << ")\n";
}
如果用了 this 可以寫成:
void MyVector::print()
{
cout << "(";
for (int i = 0; i < this->n - 1; i++)
cout << this->m[i] << ", ";
cout << this->m[this->n - 1] << ")\n";
}
看起來好像沒差,this->
的概念,就是去那塊記錄的地址取出來,其實等同於(*this).n
。
說了這麼多好像還是沒甚麼優點,this
的其中一個好處 :
在一個函數中,你可以同時使用名字相同的 variable & argument
沒有 this 時:
MyVector::MyVector(int d, int v[])
{
n = d;
for (int i = 0; i < n; i++)
m[i] = v[i];
}
有了 this 之後:
MyVector::MyVector(int n, int m[])
{
this->n = n;
for (int i = 0; i < n; i++)
this->m[i] = m[i];
}
這樣就可以讓程式變得更乾淨,更容易理解那個 n, m是誰,不會在多很多變數。
!!寫程式好習慣!!
this->
this->
會讓程式變得更乾淨變數有 constants,object 也可以有:
double d[3] = {0, 0, 0};
const MyVector ORIGIN_3D(3, d);
// This object represent the original point
我們可以在初始化這個 object 的時候,對她做 const,這時候這個 object 就不能被改動了。
class MyVector
{
private:
int n;
int* m;
public:
MyVector();
MyVector(int dim, int v[]);
void print() const;
}
如果你的 function 裡面,會改動 object 的值,這時候就不能使用 constant。反之,像是 print 只是把他們的值印出,並沒有改動任何東西,這時候就可以在他們的 header 後面加上一個 const。
!!寫程式好習慣!!
const function 只能呼叫 const function,因此要養成好習慣,如果那個 function 是 const,就在後面加上 const。
constant instance variables:
在 class 裡面,instance variable 也可以指派為 const。
class MyVector
{
private:
int n;
int* m;
public:
MyVector();
MyVector(int dim, int v[]);
MyVector(const MyVector& v);
void print() const;
}
但是如果當 constructor 要對這個 variable 做事的時候,就會產生 compilation error。
MyVector::MyVector()
{
n = 0; // error
m = nullptr;
}
這個時候,我們就要使用到 member initializer。
MyVector() : n(0)
{
m = nullptr;
}
MyVector(int dim, int v[]) : n(dim)
{
for(int i = 0; i < n; i++)
m[i] = v[i];
}
MyVector(const MyVector& v) : n[v.n]
{
m = new double[n];
for(int i = 0; i < n; i++)
m[i] = v.m[i];
}
在 function 的後面,加上: n(括號裡面的東西就是要 assign 給 n 的咚咚)
因此,這個 member initializer 也可以對其他的 variable 做 initialize。大概像這樣:
MyVector::MyVector(): n(0), m(NULL)
{
}
instance:
class MyVector
{
private:
const int n;
double* m;
public:
// constructors
MyVector();
MyVector(int dim, double v[]);
// copy default constructor
MyVector(const MyVector& v);
// destructor
~MyVector();
// print function
void print() const;
bool isEqual(const MyVector& v) const; // 傳reference 會比較省時 const 要記得
};
MyVector::MyVector(): n(0), m(NULL)
{
}
MyVector::MyVector(int n, double m[])
{
this->n = n;
this->m = new double[n];
for(int i = 0; i < n; i++)
this->m[i] = m[i];
}
MyVector::MyVector(const MyVector& v)
{
this->n = v.n;
this->m = new double[n];
for(int i = 0; i < n; i++)
this->m[i] = v.m[i];
}
MyVector::~MyVector()
{
delete [] m;
}
void MyVector::print() const
{
cout << "(";
for(int i = 0; i < n - 1; i++)
cout << m[i] << ", ";
cout << m[n-1] << ")\n";
}
An operator is overloaded by "implementing a special instance function".
要怎麼做 overload 呢? 公式是這樣的:
operatorop
op 是那個 operator。
我們來 overload "==" 這個 operator 吧 !
class MyVector
{
////.......
bool operator==(const MyVector& v) const;
}
bool MyVector::operator==(const MyVector& v) const
{
if(this->n != v.n)
return false;
else
{
for(int i = 0; i < n; i++)
{
if(this->m[i] != v.m[i])
return false;
}
}
return true;
}
這時候你就可以透過 "==" 來呼叫 isEqual
,像是這樣。
int main()
{
double d1[5] = { 1, 2, 3, 4, 5 };
MyVector a1(5, d1); // (1)
double d2[4] = { 1, 2, 3, 4 };
MyVector a2(4, d1); // (2)
MyVector a3(a1); // (3)
cout << (a1 == (a2)? "Y" : "N"); // N
cout << "\n";
cout << (a1 == (a3) ? "Y" : "N"); // Y
cout << "\n";
}
這個時候就可以把程式變得更簡潔,更美觀 & 直觀!
或甚至可以這樣寫:
cout << (a1.operator== (a2)? "Y" : "N"); // N
cout << "\n";
cout << (a1.operator== (a3) ? "Y" : "N"); // Y
cout << "\n";
那我們就可以來如法炮製一個 operator<
的 function 了!
bool operator< (const MyVector& v) const
{
if (this->n != v.n)
return false;
else
{
for(int i = 0; i < n; i++)
{
if (this->m[i] >= v.m[i])
return false;
}
}
return true
}
或是 overloading !=
。由於 != 就是 == 的反面,也就是說我們可以利用 == 來做 != 的 overloading。
bool MyVector::operator!=(const MyVector& v) const
{
if(*this == v) // *this 就是我自己!
return false;
else
return true;
// or return !(*this == v)
}
這時候我們就可以看到 this 獨特的使用地方了!
Restriction
在 overloaded 的 operator function 裡面
簡單說就是給程式一個 index,程式會回傳 vector v
的element v_i
值或是更改他的值。
就像是 array 一樣, 這邊也可以使用indexing operator: []
。
int main()
{
double d1[5] = { 1, 2, 3, 4, 5 };
MyVector a1(5, d1);
cout << a1[3] << endl; // 其實 endl 也是一個object
a1[1] = 4;
return 0;
}
我們設想可以這樣子使用 MyVector
作為提取他 v_i
的方式
class MyVector
{
//...
double operator[](int i) const;
};
double MyVector::operator[](int i) const
{
if (i < 0 || i >= n)
exit(1); // terminate the program (in <cstdlib>)
return m[i];
}
exit()
是個新朋友,他可以瞬間終止這個程式。(幫助我們做 i
發生在我們不預期的時候)
()裡面的1,是 exit() 會回傳 1 給 operating system。
回傳 0 : Normal terminal
回傳其他數字: DIFFERENT ERROR
如果我們寫好了上面那段,準備來跑的時候會發現:
int main()
{
double d1[5] = { 1, 2, 3, 4, 5 };
MyVector a1(5, d1);
cout << a1[3] << endl; // 成功傳出來
a1[1] = 4; // error!!!
return 0;
}
這是因為a1[1]
回傳的是一個 value,所以當我們寫a1[1] = 4;
這件事情的時候,就像是在寫4 = 3
一樣了 ! 當然是不行的。
所以要這樣子寫:
class MyVector
{
//...
double operator[](int i) const;
double& operator[](int i);
};
double MyVector::operator[](int i) const
{
if(i < 0 || i >= n)
exit(1);
return m[i];
}
double& MyVector::operator[](int i) // 回傳 reference!!
{
if(i < 0 || i >= n) // same
exit(1);
return m[i];
}
我們再多做一次 overloading,這樣就可以讓a1[1]
的意義變成回傳她的地址,也就是代表那個變數。
這兩個 overloading,可以用是否是 const 來區別,如果當我們傳入的值是 constant,程式就會傳入有 const 的 function 裡,若不是的話就會傳入 non-const function 裡面。
implements:
#include <iostream>
#include <cstdlib>
using namespace std;
// class definition of MyVector
class MyVector
{
friend const MyVector operator+(const MyVector& v, double d);
private:
int n;
double* m;
public:
MyVector();
MyVector(int n, double m[]);
MyVector(const MyVector& v);
~MyVector();
void print() const;
bool operator==(const MyVector& v) const;
bool operator!=(const MyVector& v) const;
bool operator<(const MyVector& v) const;
double operator[](int i) const;
double& operator[](int i);
const MyVector& operator=(const MyVector& v);
const MyVector& operator+=(const MyVector& v);
};
//instance functions
MyVector::MyVector(): n(0), m(NULL)
{
}
MyVector::MyVector(int n, double m[])
{
this->n = n;
this->m = new double[n];
for(int i = 0; i < n; i++)
this->m[i] = m[i];
}
MyVector::MyVector(const MyVector& v)
{
this->n = v.n;
this->m = new double[n];
for(int i = 0; i < n; i++)
this->m[i] = v.m[i];
}
MyVector::~MyVector()
{
delete [] m;
}
void MyVector::print() const
{
cout << "(";
for(int i = 0; i < n - 1; i++)
cout << m[i] << ", ";
cout << m[n-1] << ")\n";
}
// end of MyVector's instance functions
// MyVector's overloaded operators
bool MyVector::operator==(const MyVector& v) const
{
if(this->n != v.n)
return false;
else
{
for(int i = 0; i < n; i++)
{
if(this->m[i] != v.m[i])
return false;
}
}
return true;
}
bool MyVector::operator!=(const MyVector& v) const
{
return !(*this == v);
}
bool MyVector::operator<(const MyVector& v) const
{
if(this->n != v.n)
return false;
else
{
for(int i = 0; i < n; i++)
{
if(this->m[i] >= v.m[i])
return false;
}
}
return true;
}
double MyVector::operator[](int i) const
{
if(i < 0 || i >= n)
exit(1);
return m[i];
}
double& MyVector::operator[](int i)
{
if(i < 0 || i >= n)
exit(1);
return m[i];
}
const MyVector& MyVector::operator=(const MyVector& v)
{
if(this != &v)
{
if(this->n != v.n)
{
delete [] this->m;
this->n = v.n;
this->m = new double[this->n];
}
for(int i = 0; i < n; i++)
this->m[i] = v.m[i];
}
return *this;
}
const MyVector& MyVector::operator+=(const MyVector& v)
{
if(this->n == v.n)
{
for(int i = 0; i < n; i++)
this->m[i] += v.m[i];
}
return *this;
}
// main function
int main()
{
double d1[5] = { 1, 2, 3, 4, 5 };
MyVector a1(5, d1); // (1)
double d2[4] = { 1, 2, 3, 4 };
MyVector a2(5, d2);
const MyVector a3(a1);
a2[0] = 999;
if (a1 == a3)
cout << a2[0] << " " << a3[0];
return 0;
}
如果我們跑了這段程式,會跑出 999 1
,但是如果我們把這段程式
double& MyVector::operator[](int i)
{
if(i < 0 || i >= n)
exit(1);
return m[i];
}
刪掉,就會發現在 a2[0] = 999
那一行發生了 compilation error。
這時候可以用這樣的方式驗證一下這段程式到底在我們的 main function 裡面做了甚麼?
double& MyVector::operator[](int i)
{
COUT << "...";
if(i < 0 || i >= n)
exit(1);
return m[i];
}
就會發現,最後這段 ... 印了兩次,結果像是這樣:
......999 1
因此也可以證明,這兩個const 與 non-const 的函式,當你要做 assignment (a2[0] = 999
)的時候,這兩者是缺一不可的。
上面的 comparison 和 indexing operation ,皆不會使 calling object 改變,但是有些 operation 則會使 calling object 改變,最簡單的就像是 "="。
int main()
{
double d1[3] = { 4, 8 ,7 };
double d2[4] = { 1, 2, 3, 4 };
MyVector a1(3, d1);
MyVector a2(4, d2);
a2.print();
a2 = a1; // dangerous
// syntax error if n = constant
// a2.print();
// a2[0] = 9;
// a.print();
return 0;
}
像是上面這邊的程式,我們本來就不用寫任何東西就可以完成 a2 = a1
這件 assignment,但是這麼做會有一點危險,若 n 為 constant 的時候,就會出現 syntax error。
如果你再寫入下面那三個註解掉的三行程式,結果會顯示:
(1, 2, 3, 4)
(4, 8, 7)
(9, 8, 7)
你會發現 a1 原本是 4 8 7,卻因為 a2[0] 被改成 9 而變成了 9 8 7。這種情形其實就長得像我們之前遇到過的 deep copy & shallow copy。
Default assignment:
= 在預設中其實長這樣:
MyVector& MyVector::operator=(const MyVector& v)
{ // default
this->n = v.n;
this->m = v.m;
}
因此,在 a1 = a2
,的時候就會發生下面這件事(紅色為 assign 後發生的事情)。
原本 a1 的 n 被改成 a2 的 n (也就是 4) ,而 m 這個指標則會改成 a2 的 m,所以 a1 就會指向原本 a2 指向的那個空間了。
那接下來,我們來手動製作這個 overload operator:
const MyVector& MyVector::operator=(const MyVector& v)
{
if(this != &v) // avoid self-assignment
{
if(this->n != v.n)
{
delete [] this->m;
this->n = v.n;
this->m = new double[this->n];
}
for(int i = 0; i < n; i++) // 每個 element copy
this->m[i] = v.m[i];
}
return *this;
}
這麼一來,我們就可以解決原本指向 m 那段空間的 bug
而一開始包著大家的 if 指的就是,如果你傳進來的 parameter 是你自己的話,就不做任何事情,這樣就可以避免當有豬隊友寫 a1 = a1;
時可能會發生的問題了。
那如果有人做了這件事: a1 = a2 = a3;
(可以把他想成 a3 assign 給 a2,再把 a2 assign 給 a3)
為了避免這種情況,我們可以使用 const
來避免 a3 被 assign 給其他人。
另外,可以注意到我們回傳的值是*this
,這是因為我們想要 return 的是一個 reference。
在向量裡面,我們可能會想要加減他們,像是有兩個向量u,v。而我們可能也會想要做一個 operation u += v
使 u_i
會變成 u_i + v_i
(for all i)。
const MyVector& MyVector::operator+=(const MyVector& v)
{
if(this->n == v.n)
{
for(int i = 0; i < n; i++)
this->m[i] += v.m[i];
}
return *this;
}
例如我們要做一個 加法 的 overloading,我們可能要做這幾件事情:
const MyVector&
const MyVector
讓 a1 + a2 + a3
可以運作;但是可以避免 (a1 + a2) = a3
實作:
const MyVector operator+(const MyVector& v, double d)
{
MyVector sum(*this); // creating a local variable
sum += v; // using the overloaded +=
return sum;
}
為什麼是 return 一個 object?
sum 在這個 function 裡面,在函式結果的時候會被release(因為她是 local variable),所以回傳 object的效果就是創立一個新的 object 儲存結果,否則就會被 memory released。
且,我們甚至也可以讓 + 做出不同的變化:
int main()
{
double d1[5] = { 1, 2, 3 };
MyVector a1(3, d1);
MyVector a2(3, d1);
a1 = a1 + a2;
a1.print();
a1 = a2 + 4.2;
a1.print();
return 0;
}
這時候 function 就必須寫成:
class MyVector
{
//...
const MyVector operator+(double d);
const MyVector operator+(const MyVector& v, double d);
}
const MyVector operator+(const MyVector& v, double d)
{
MyVector sum(v);
for(int i = 0; i < v.n; i++)
sum[i] += d;
return sum;
}
因為加法有互換率(也就是 a+b = b+a),這時候如果我們寫成:
a1 = 4.2 + a1;
就會變得很奇怪(因為在原本的 function 沒有這樣的定義),而且我們也不能對 double 裡面的函式去做改變。
因此這時候就必須使用 global function 來做 operator overloading。
const MyVector operator+(const MyVector& v, double d)
{ // need to be friend of MyVector
MyVector sum(v);
for(int i = 0; i < v.n; i++)
sum[i] += d; // pairwise addition
return sum;
}
const MyVector operator+(double d, const MyVector& v)
{
return v + d; // using the previous definition
}
const MyVector operator+(const MyVector& v1, const MyVector& v2)
{
MyVector sum(v1);
sum += v2; // using overloaded +=
return sum;
}
這樣子就可以處理 double 在前,object 在後的情況了!
這時候你就可以寫出像是這種的式子:
a3 = 3 + a1 + 4 + a3;
這次說的東西,真的是完全沒有想像過 !
C++ 也太酷。