iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0
Software Development

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

Day 21 - 我們這一班

Object-oriented programming

  • 在之前,我們使用的 programming 的方式會被稱為:procedural programming。
  • 而在學會 objected-oriented programming 是一種基於 procedural programming 的延伸,但是兩者的不同是「觀點的不同」。
  • 在 C裡面,通常會用 structure;而在 c++ 裡面則是使用 classes
  • classes 就與 structure 一樣,可以讓我們自定義 data type,而在 class 裡面的變數則會被稱為 objects
  • 使用 class / OOP 可以讓程式更好的模組化,在大型程式裡面會使用的比較多。

Outline

  • Basic concepts
  • Constructors and the destructor
  • Friends and static members
  • Object pointers and the copy constructor

Basic concepts

之前有寫過一個 Point 的 structure (其實就是二維陣列上的向量)

那我們今天來做一個 multi-dimensional vector

struct MyVector
{
	int n; 
	int* m; // 為了要動態的儲存 因為是 n 維向量,你不知道會是多少
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

void MyVector::print(
{
	cout << "(";
	for (int i = 0; i < n - 1; ++i)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

int main()
{
	MyVector v;
	v.init(3);
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m;
	return 0;
}

這樣子寫其實已經可以滿足我們原本想要的需求,但是還是有幾個缺點

  • 如果今天你是跟同學一起寫這段程式的話,他們可能會忘記 initialize vector,這樣我們就不知道 m 會指向哪裡了。
  • 同學也可以不用你的 print ,自己再寫一個 for loop 去 cout
  • n 跟 m 亂用
  • 忘記 delete

所以我們可能希望我們寫的 structure 可以

  • initializer 可以自動地被呼叫
  • vector 只能用我們的方式印
  • n 、m 不能被分離式的修改(修 n 時 m 也會被修)
  • 動態配置的空間可以自動被釋放掉

這時候 class 就可以派上用場了,因為它可以:

  • member function 一定會被自動呼叫
  • 可以 hide 一些 member ,還有放一些 public member
  • ...還有更多

在使用之前,我們必須先知道:

variable 分為兩種

  • Instance variables (default)
  • Static variables

function 分為兩種

  • Instance functions (default)
  • Static functions

Definition of class

  • class 可以說是把 struct 做一點改變而已。但是如果你直接把上面的程式中的 struct 改成 class
class MyVector
{
	int n; 
	int* m; // 為了要動態的儲存 因為是 n 維向量,你不知道會是多少
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

void MyVector::print(
{
	cout << "(";
	for (int i = 0; i < n - 1; ++i)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

int main()
{
	MyVector v;
	v.init(3);
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m;
	return 0;
}

這時候你會發現,好像沒辦法 compile

主要是因為 class 需要設定 visbility (class 與 struct 最大差別)

我們必須在 class 裡面設定三種 member

  • Public : 可以在任何時候呼叫
  • Private : 只能在 class 裡面呼叫
  • Protected : 未來可能會遇到

在預設值下,所有的 member 都是 private,所以我們要做打開或是關起來這件事情。

就像這樣:

class MyVector
{
private:
	int n; 
	int* m; // 為了要動態的儲存 因為是 n 維向量,你不知道會是多少
public:	
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

void MyVector::print(
{
	cout << "(";
	for (int i = 0; i < n - 1; ++i)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

int main()
{
	MyVector v;
	v.init(5); // 這時候就可以
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m; // 這樣不行
	return 0;
}

像是在 main function 裡面,如果我們要存取 init 這個函數,因為我們已經把它改成 public ,因此如果你宣告他就可行,但是下面的 delete [] v.m ,因為我們把 m 設置成 private ,這時候就不能叫到他了(只能在 class 裡面存取他)。

data hiding (Encapsulation 封包)**

  • 為什麼要設置 private ? 原因是因為我們想要保障 n、m 不會被亂動,程式才不會出錯。
  • 同理,設置 public 的原因,也是想要限制使用者不要亂用其他的方式,像是用 cout << + for loop 而不是我們自己寫出來的 print。
  • 在 99.9% 的情況下, instance variable 會被設為 private, instance function 會被設為 public。

簡單說,我們把一坨東西封包起來(像是手機或是電視),再告訴使用者要怎麼使用(說明書),你沒辦法拿他做其他的事情(就像是電視就只能看電視)。

Instance function overloading: (函式多載)

也就是說可以傳入多種情況,函式都可以運行,且可做不同結果

class MyVector
{
private:
	int n; 
	int* m; // 為了要動態的儲存 因為是 n 維向量,你不知道會是多少
public:	
	void init();
	void init(int dim);
	void init(int dim, int value)
};

void MyVector::init()
{
	n = 0;
	m = nullptr;
}

void MyVector::init(int dim) // dim = dimension
{
	init(dim, 0);
}

void MyVector::init(int dim, int value)
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; i++)
		m[i] = value;
}

除此之外,class 也可以:

  • 把object 丟進去其他 function,或是做為回傳值。
  • 在其他的 class 裡面使用自定義的 class 作為 data type。(class 中的 class)

Constructors and the destructor

還記得我們剛剛想要完成的幾件事嗎?

  • [ ] initializer 可以自動地被呼叫
  • [x] vector 只能用我們的方式印
  • [x] n 、m 不能被分離式的修改(修 n 時 m 也會被修)
  • [ ] 動態配置的空間可以自動被釋放掉

我們目前大概只完成了第2 3 項,1 4 則需要下面的方式來達成。

Constructors 建構子 :

他是一個在 class 中的 function,可以做到: (在物件被建立的時候)

  • 自動的呼叫
  • 不能重複呼叫
  • 不能被手動的呼叫

因此他可以達成我們 自動呼叫且初始化的功能。

特性:

  • Constructor 的名字就跟 class 一樣。

    class MyVector
    {
    private:
    	int n; 
    	int* m; 
    public:	
    	MyVector(); // Constructor
    	MyVector(int dim);
    	MyVector(int dim, int value)
    };
    
  • 且他不會回傳任何東西(連 void 都沒有)。

  • 可以 overloading

  • 沒有 parameter 的 constructor (Myvector )會被稱為 default constructor,如果你沒有建立一個 constructor的話,系統會自己幫你建立一個(也就是說 class 裡面一定會做一個 constructor),且裡面不會做任何事情

所以把 constructor 加到我們剛剛做的程式裡:

class MyVector
{
private:
	int n; 
	int* m; 
public:	
	MyVector init(); 
	MyVector inti(int dim, int value = 0); // 如果只傳一個 parameter -> value 用 0 來做
	void print;
};
MyVector::MyVector()
{
	n = 0;
	m = nullptr;
}

Myvector::Myvector(int dim, int value)
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; i++)
		m[i] = value;
}

void Myvector::print()
{
	cout << "(";
	for (int i = 0; i < n - 1; ++i)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";	
}

使用:

int main()
{
	Myvector v1(1);
	Myvector v2(3, 8); // 現在宣告的時候就可以順便初始化
	v1.print();
	v2.print();
	return 0;
}

這樣子我們就可以做到自動的初始化了

剩下的 release 動態配置的空間則會使用到 destructor →


Destructors

destructors:

  • (在物件被消滅的時候) 被呼叫
    • 被消滅的定義: variable 的生命週期結束
  • 需要他的原因是因為在 variable 生命週期結束的時候,我們不會有機會去對他做甚麼事情。
  • 跟 constructor 一樣,destructor 也會自動地被系統呼叫(如果你沒有定義 destructor)。

Define:

  • 在class() 前面加上 ~ 就可以宣告一個 destructor了。
class MyVector
{
private:
	int n; 
	int* m; 
public:	
	~MyVector();
};

MyVector::~Myvector()
{
	delete [] m;
}

MyVector::MyVector(int dim, int value)
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; i++)
		m[i] = value;
}

int main()
{
	if (true)
		MyVector v1(1); 
	return 0;
}

如此一來,我們宣告一個 object 的時候,就不會發生 memory leak?

Order?

class A
{
public:
	A(){ cout << "A\n";}
	~A(){ cout << "a\n";}
};

class B
{
private:
	A a;
public:
	B(){ cout << "B\n";}
	~B(){ cout << "b\n";}
};

int main()
{
	B b;
	return 0;
}

螢幕會印出:

A
B
b
a

這就是如果有 class 包含在 class 裡面的 constructor 和 destructor 呼叫的順序。


Friends and static members

Getter and setter

在大多數的情況下,instance variable 會是 private 的,因此為了存取他們,我們必須使用 getter & setter 來對他們做一些事。

  • getter 會回傳一個 private instance variable 的值
  • setter 則可以更改他們的值
class MyVector
{
private:
	int n;
	int* m;
public:
	int getN { return n; }
	void setN(int v){ n = v; }
};

friends

我們可以想像,如果今天我們想要開一個權限(像是你手機的密碼),你不可能會跟陌生人說吧,所以一定是開給你感情比較好的朋友。

朋友可以是:

  • global function
  • class

像是下面這個 class

class MyVector
{
	//....
friend void test();
friend class Test;
};

我們可以知道 friend 的幾個特性: (以上面為例):

  • 在 test() 和 class Test 裡面,可以使用 MyVector 的 private members
  • MyVector 無法使用 Test 的 private members (單向)(就跟交朋友一樣?)
  • friend 可以在 private 或是 public宣告(沒差)

所以我們可以在 test 裡面使用 n

void test{
	MyVector v;
	v.n = 100; // 因為是朋友!
	cout << v.n
}

在 Test 裡面也可以使用:

class Test{
public:
	void test(MyVector){
		v.n = 203;
		cout << v.n;
	}
};

Static members

  • 在 class 裡面,每一個 object 都擁有自己的 instance variables 和 functions。
    (這時候就會稱這些 variables & functions 是 object-specific)

  • 但是反過來,member variable 或是 function 可以是一個 class 的屬性( attribute) 或是 operation。

    (這個時候這些 variable 和 function 就會被稱為 class specific)

    他們就會被稱為 static members (靜態成員: 保持不變)

舉個例子: 像是 windows 中的視窗,每一個視窗都是一個 object。這些視窗都有自己的名稱、自己專屬的大小 這些特性就會被稱為 object-specific attribute。而每一個視窗,都會有一些相同的東西,像是每個視窗都會有 title bar、這些 bar 都有一樣的顏色、都會有 — [ ] X (縮小 / 全螢幕/ 關閉) 等按鈕,這些他們擁有的相同屬性,就可以稱為一個 class-specific attribute。

class Window
{
private:
	int width;
	int height;
	int locationX;
	int locationY;
	int status; // 0: min, 1:usual, 2: max
	static int barColor; // 0: gray....
	//.....
public:
	static int getBarColor();
	static void setBarColor(int color);
	//....
};

另外,我們必須在 global 的環境下 初始化一個 static variable(因為全部都要用),像是這樣:

int Window::barColor = 0; // default
int Window::getBarColor()
{
	return barColor; 
}
void Window::setBarColor(int color)
{
	barColor = color;
}

那如果我們要在 int main() 存取 static member要用

class name::member name

如果是要存取 instance 的 member:

object name.member name

使用 static member:

int main()
{
	Window w;
	cout << Window::getBarColor();
	cout << "\n";
	Window::setBarColor(1);
	return 0;
}

所以現在我們就有了四種 member type:

  • instance variable & instance functions
  • static variable & instance functions

他們倆者的關係是這樣:

  • 在 instance function 裡面,可以存取 static members
  • 但是在 static function 裡面,不可以存取instance member
  • 我們也可以像: w.getBarColor() 這樣子透過 object 存取 static member,但是 非常不建議 這麼使用,最好用 class 呼叫他

!!寫程式好習慣!!

  • 如果你今天要寫一個每一個 object 都要使用的特性或是 function,這時候就要使用 static member → 可以維持一致性,

  • 不要 object 來呼叫 static member

  • 盡量用 class 來呼叫:

    int Window::getBarColor()
    {
    	return Window::barColor;
    }
    

static members' function:

  1. 剛剛我們知道了 static members 可以維持 object 的一致性
  2. 他還有另一個功能就是可以 計算一個 object 被建立了幾次

instance :

找出有幾個人被 constuct

class A
{
private:
	static int count;
public:
	A() {A::count++; } // 因為被大家共用,所以只要有人被建立的時候,就++
	static int getCount(){ return A::count; }
};

int A::count = 0;

int main(int argc, char const *argv[])
{
	A a1, a2, a3;
	cout << A::getCount() << "n"; // 3 
	return 0;
}

instance:

找出有幾個人目前還活著(alive)

class A
{
private:
	static int count;
public:
	A() {A::count++; }
	~A() {A::count--; }
	static int getCount(){ return A::count; }
};

int A::count = 0; 

int main(int argc, char const *argv[])
{
	if (true)
		A a1, a2, a3;
	cout << A::getCount() << "n"; // 0
	return 0;
}

另外,在中間的那一行就是前面所說的,static variable 要在 global 初始化。


Object pointer

  • 因為 class 是一種我們自定義的 data type

  • 且 pointer 可以指向任何一種 data type

    • 因此 pointer 是可以指向一個 object 的。 (store the address of an object)

    instnace:

    int main()
    {
    	MyVector v(5);
    	MyVector* ptrv = &v; // object pointer
    	return 0;
    
    }
    
  • object pointer 的使用?

    • 因為 pointer 就是存取 object (例如說 a ) 的位置(其實就是代表 a 的意思),所以我們可以用 *ptrA 去存取 a 中的 function,像是這樣: (*ptrA).print()

    • 但是有另一個更簡單存取的方式,就是直接用 ->

      所以上面的存取就可以寫成 ptrA ->print();

  • WHY OBJECT POINTERS?

    1. 當我們要做一個 object array 的時候,可以用 pointer 來延遲 constructor 的呼叫,就會害我們失去初始化的機會。

      像是這個:

      int main()
      {
      	MyVector v[3]; // an object array
      	v[0].print(); // run-time error 因為 m 是 nullptr
      	return 0;
      }
      

      如果我們先呼叫了v[3],會因為我們沒辦法初始化,會導致 array 裡面n = 0, m = nullptr。

      所以可以用 Dynamic object arrays 這個方法來解決:

      • object pointer可以幫我們做 動態記憶體配置
      int main()
      {
      	MyVector* ptrV = new MyVector(5); //呼叫 constructor
      	ptrV->print();
      	delete ptrV;
      	return 0;
      }
      
      • 也可以做出一個 動態的 array

      ❌ 因為我們宣告了 5 個 object,可是只叫了一個 pointer

      int main()
      {
      	MyVector* ptrV = new MyVector[5]; // 宣告動態 array
      	ptrV[0].print(); // run-time error 
      	delete [] ptrV;
      	return 0;
      }
      

      ✅ 我們宣告了 5 個 pointer,每一次都 create一個 object,再去做 constructor (中間的 for loop),就可以完成。

      int main()
      {
      	MyVector* ptrArray[5]; //no constructor invocation
      	for(int i = 0; i < 5; i++)
      		ptrArray[i] = new MyVector(i + 1); // constructor
      	ptrArray[0]->print();
      	// some delete statements
      	return 0;
      }
      
    2. Passing object into a function

      如果我們今天要寫一個程式,讓我們把每一個 vector 的質相加

      MyVector sum(MyVector v1, MyVector v2, MyVector v3 )
      {
      	// assume that their dimensions are identical
      	int n = v1.getN();
      	int* sov = new int [n]; //sov = sum of vectors
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1.getM(i) + v2.getM(i) + v3.getM(i); // 把他們相加
      	MyVector sumOfVec(n, sov); // constructor -> 後面要寫
      	return sumOfVec;
      }
      
      int MyVector::getN() { return n; }
      int MyVector::getM(int i) { return m[i]; }
      MyVector::MyVector(int d, int v[]) // sov 是一個 array
      {
      	n = d;
      	for (int i = 0; i < n; ++i)
      		m[i] = v[i];
      }
      

      在這個程式裡面,有 4 個 MyVector object 被創造,但是如果有更多的 object ,這時候就有點麻煩。

      所以可以改成用 pointer 來寫:

      MyVector sum(MyVector* v1, MyVector* v2, MyVector* v3)
      {
      	int n = v1->getN();
      	int* sov = new int [n];
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1->getM[i] + v2->getM[i] + v3->getM[i];
      	MyVector sumOfVec(n, sov);
      	return sumOfVec;
      }
      

      如此一來,我們就只需要創造一個 object 就好了。

      但是很有可能因為 object 不夠多,所以用 pointer 的時候花的時間會更多,這樣就不太划算,因此建議 object 比較少的時候可以直接使用 object 來比較快。

    3. Passing object references

      MyVector sum(const MyVector& v1,const MyVector& v2,const MyVector& v3)
      {
      	int n = v1->getN();
      	int* sov = new int [n];
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1.getM[i] + v2.getM[i] + v3.getM[i];
      	MyVector sumOfVec(n, sov);
      	return sumOfVec;
      }
      

      同樣的,我們也可以傳入 reference ,而下面就把 references 當作一般變數,直接用 .getM[i] 就可以了。

      而在 argument 的部分,因為我們不想要這些 reference 被更改,所以我們要用 const 來保護他們不被更改。

      很多時候我們寫出來的程式,不是為了要做出甚麼功能,而是要避免一些事情的發生。


Copying an object

class A
{
private:
	int i;
public:
	A() { cout << "A"; }
};
void f(A a1, A a2, A a3)
{
	A a4;
}

int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n";
	f(a1, a2, a3); // A
	return 0;
}

這段程式會傳出:

AAA
===
A

為什麼當我們呼叫 f 的時候,只會傳出 A 而已? 而不是傳出 4 個 A(4 次 constructor)

In general, when we pass by value, a local variable will be created.

  • when we pass by value for an object, a local object is created.
  • the constructor should be invoked.
int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n";
	A a4 = a1; // nothing
	return 0;
}

那如果我們把程式改成這樣,就會甚麼東西都不會傳出。

這是因為:

Creating an object by "copying" and object is a special operation. It happens:

  • when we pass an object into a function using call by value mechanism.

    f(a1, a2, a3);
    
  • when we assign an object to another object.

    A a4 = a1;
    
  • when we create an object with another object as the argument of the constructor.

    A a5(a1);
    

COPY CONSTRUCTOR

這一個機制會被稱為 "copy constructor" (也就是用 copy object 的方式來建立 object)(這也是一個 default copy constructor),他甚麼事情都不會做。我們也可以手動的設定他要做甚麼事情:

  • 在 C++ 裡面,copy constructor 的 parameter 一定要是 constant reference。
  • 如果 calling by value,copy constructor 會被宣告無數(parameter)次。
class A
{
private:
	int i;
public:
	A() { cout << "A"; }
	A( const A& a) { cout << "a"; }
};

void f(A a1, A a2, A a3)
{
	A a4;
}

int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n; // ===
	f(a1, a2, a3); // aaaA
	A a4(a1); // a
	A a4 = a1; // a
	return 0;
}

像是在 f(a1, a2, a3); 的時候,就會因為 copy constructor 被啟動,所以就會印出 a。而像下面的 A a4(a1);,也是會印出 aA a4 = a1; 的結果也是相同的。

WHY COPY CONSTRUCTORS?

我們自己也可以手動的寫出 copy constructor,大概會長:

MyVector::MyVector(const MyVector& v)
{
	n = v.n;
	m = v.m;
}

Shallow copy :

這個就是一般的 default copy constructor 會做的事情(前提: member中沒有 array 或是 pointer)。

那如果有 array 或是 pointer,因為 m 這個指標是指向一塊空間,但是如果是一個 array 的話,就會產生不同的指標指向同樣的空間的情況

int main()
{
	MyVector v1(5, 1);
	MyVector v2(v1); //??
}

這時候記憶體中會變成:

這時候如果我們改了 v1 的時候,v2 都會一起被改。

Deep copy:

為了避免我們上述講的情形,我們就需要在 copy 的時候把一個一個 element 改掉。

首先我們要手動的宣告一個 dynamic array, 讓 m 儲存他的地址。最後在把 v.m[i] 的值取出來。

MyVector::MyVector(const MyVector& v)
{
	n = v.n;
	m = new int[n]; // deep copy
	for(int i = 0; i < n; i++)
		m[i] = v.m[i];
}

心得

以前學 python 的時候碰到 class 的機率很少,幾乎沒寫過。
現在終於知道在幹嘛了。


上一篇
Day 20 - Self-defined Data types(in C) 自訂資料型態
下一篇
Day 22 - 運算過載,warning ! warning !
系列文
三十天內用C++寫出一個小遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言