iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0
Software Development

三十天內用C++寫出一個小遊戲系列 第 22

Day 22 - 運算過載,warning ! warning !

  • 分享至 

  • xImage
  •  

Outline

  • Motivations(為什麼要做 operation overloading) and prerequisites (基本知識)

    ( overloading 的情境) :

  • Overloading comparison and indexing operators

  • Overloading assignment and self-assignment operators

  • Overloading addition operators


Motivations and prerequisites

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:

  • 我們使用一個 member function 來比較 MyVector 中的 objects (也就是向量): (當他們的 dimensions 都相同時)
    • u = v : 每一個 element 都一樣
    • u < v : 每一個 element 都符合 (ui < vi) (每一個 u 的 element 都比 v 小)
    • u ≤ v : 每一個 element 都符合 (ui ≤ vi)

在這個例子裡,因為是比較兩個 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 :

  • 不是全部的 operator 都可以被 overloaded
  • 不能改變 operand 的個數
  • 不能創造新的 operator

this

當你建立一個 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是誰,不會在多很多變數。

!!寫程式好習慣!!

  • 只要你寫到 instance variables 或是 function 時,一律使用 this->
  • this-> 會讓程式變得更乾淨

Constant object

變數有 constants,object 也可以有:

  • constant object:
double d[3] = {0, 0, 0};
const MyVector ORIGIN_3D(3, d);
// This object represent the original point

我們可以在初始化這個 object 的時候,對她做 const,這時候這個 object 就不能被改動了。

  • instance function:
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";
}

Overloading comparison and indexing operator

An operator is overloaded by "implementing a special instance function".

要怎麼做 overload 呢? 公式是這樣的:

operatorop

op 是那個 operator。

Comparison

我們來 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 裡面

  • 傳入的 parameter 會被限制(像是 == 的 parameter不能超過一個)
  • 傳入的 type 不會限制
  • 回傳的 type 不會限制
  • 做甚麼事情也不會限制

Indexing operator

簡單說就是給程式一個 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)的時候,這兩者是缺一不可的。

Overloading assignment and self-assignment operators

上面的 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;
}
  1. 先把 m 所指到的空間 release
  2. 再把 n assign 到 v.n
  3. 最後再把 m 指到新的 n 的這段空間

這麼一來,我們就可以解決原本指向 m 那段空間的 bug

而一開始包著大家的 if 指的就是,如果你傳進來的 parameter 是你自己的話,就不做任何事情,這樣就可以避免當有豬隊友寫 a1 = a1; 時可能會發生的問題了。

那如果有人做了這件事: a1 = a2 = a3; (可以把他想成 a3 assign 給 a2,再把 a2 assign 給 a3)

為了避免這種情況,我們可以使用 const 來避免 a3 被 assign 給其他人。

另外,可以注意到我們回傳的值是*this ,這是因為我們想要 return 的是一個 reference。

Preventing assignments and copying

  • 有時候,我們想要避免 object 的 assignment,以防止使用者亂 assignment,這時候只要把 assignment operator 放入 private member 裡面就可以了。
  • 同樣的,要避免 copy 這件事情,也是把它放到 private member 裡面就好了。
  • 而我們在前面遇到的 copy constructor、assignment operator、還有 destructor這三者的使用時機就是:
    • 當 class 裡面沒有用到 pointer → 全部都不需要
    • 當 class 裡面有用到 pointer → 全部都需要

Self-assignment operators

在向量裡面,我們可能會想要加減他們,像是有兩個向量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 addition operators

例如我們要做一個 加法 的 overloading,我們可能要做這幾件事情:

  • parameter 使用 const MyVector&
  • 每一對 element 要一個一個的加 (u_i + v_i)
  • 不能更改 calling 和 parameter object
  • Return const MyVectora1 + 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;
}

Instance function vs. global function

因為加法有互換率(也就是 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++ 也太酷。


上一篇
Day 21 - 我們這一班
下一篇
Day 23 - 字串又來了,我還是沒吃到串燒
系列文
三十天內用C++寫出一個小遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言