C++ Class

前言

類別(Class)由成員(member)組成,包括資料、函數以及建構函數。

所以甚麼是Class?

我們可以把Class當成是一個學生,而這個學生有姓名、身高、體重、年齡,有個函數可以來計算它的BMI,也要有一個函數可以「創建」這個學生。以下為範例:

class student { // member variables string name; double height, weight; int age; // member function double BMI(); // constructor student(); student(string name, double h, double w, int age); };

建構函數(Constructor)

建構函數有兩種寫法,一是直接用函數寫,另外則是利用 : 來初始化成員變數

student::student(string _name, double _h, double _w, int _age) { this->name = _name; this->height = _h; this->weight = _w; this->age = _age; cout << "Construct successfully!"; // 不須 return }

初始化列表(各變數以,分隔):

student::student(string _name, double _h, double _w, int _age) : name(_name), height(_h), weight(_w), age(_age) { cout << "Construct successfully!"; }

通常初始化列表較常用在 const 成員上,因為 const 成員不可賦值

解構函數

通常用在需要釋放記憶體之成員函數:student::~student() {}

成員函數

定義成員函數:

student::BMI() { return this->weight / pow(this->height, 2); }

呼叫成員函數:

student s1("John", 1.8, 80, 18); cout << s1.BMI();

存取標籤比較

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

實作 - Vector2D

在 Vector2D 中,有兩個 double 的值,分別代表座標的 x, y,以及基本的初始化函數:

class Vector2D { private: double x, y; public: Vector2D(double X, double Y) : x(X), y(Y) {} };

通常,我們會將 class 內的變數設為 private,以避免外部經任意手段更改。

因為變數不可存取,我們需要另外寫一個函數來取得他們的值:

public: double getX() const { return this->x; } double getY() const { return this->y; }

this

延續上面的例子,事實上,this 是一個指標型態

Vector2D *this; // this 的原型

因為 this 是指標型態,並且 . 的優先順序在 * 之前,因此我們需要用 (*this).getX() 來呼叫函數,但這樣顯得過於麻煩,因此C++提供了一個好方法:this->getX()

const

我想有人看到 const 可能會不懂它是什麼東西
const 是一個關鍵字,它的用途就是讓指定物件或變數不可修改
講白就是被 const 修飾 (decorate)過的不能被賦值 (assign)

int value = 5; const int const_value = 5; value = 10; // 正確,因為value是個變數 const_value = 10; // 錯誤,因為const_value是個const int

那如果把函數傳入的參數加上個const會變成怎樣呢?

void test_function(const int a) { a = 5; // 錯誤,因為a是const int a++; // 錯誤,同上 std::cout << a << '\n'; //正確 }

那如果再把參數改成傳引用(reference)呢

void test_function(const int& a) { a = 5; // 錯誤,因為a是const int& }

這樣有什麼好處呢?
因為用 const 修飾過,所以參數會受到保護,免得被亂改,傳入引用會讓效率變高且減少空間需求(在物件所佔的空間較大會比較明顯

再之後的get函數中會在函數後面加個 const,那這又是什麼呢?

getX() const { return this->x; }

這個 const 表示getX()不會修改成員變數
如果要函數傳進去的物件是有 const 修飾過的,那這個物件只能調用 const 修飾過的函數 (誰聽得懂,直接上code)

double Vector2D::getX_const() const { return this-> x; } double Vector2D::getX() { return this-> x;} void test(const Vector2D other) { other.getX_const(); // 正確,因為getX_const()是個const的成員函數 other.getX(); // 錯誤,因為const物件不能調用非const的成員函數 }

講了這麼多 const 的用途,看起來挺麻煩的
但適當的加 const 可以讓程式的穩定性更加,速度也會提升 (也可以提醒自己,這個東西是不能亂改的啊),請各位謹記

& - 引用 (reference)

我想各位在 const 篇應該有看到 & 這個符號吧,想必又是看不懂了 (這作者到底怎樣,都喜歡先斬後奏的)

「引用」就是一個變數的別名 (alias),它一定會指向一個變數,且引用是跟變數共用相同記憶體區域的,當讀取這個別名時,就可以得到它所指向的變數 (variable) 的值 (value) 了
呃,有聽沒有懂,直接上code🤣

int value = 5; //宣告變數 (declare variable) int &reference_value = value; //宣告引用並指向value (declare reference) value = 6; std::cout << value << '\n'; //輸出6 reference_value = 7; std::cout << value << '\n'; //輸出7 //證明reference跟variable的記憶體區域是相同的 //可以自己試試,在這裡的&是取地址(address) std::cout << &value << '\n'; std::cout << &reference_value << '\n'; //以下為錯誤示範 int &reference_value_1; //錯誤,因為引用必須要指向一個變數 int &reference_value_2 = 5; //錯誤,因為5並不是個變數

由上述code可得知,引用必須要指向一個變數,且修改引用的值就等於修改引用的變數的值,這有什麼好處呢?
因為需要指向變數,所以就不存在初始化 (initialization)的問題,不用擔心引用裡面會存什麼自己不知道的東西
另外,因為引用是共用記憶體區域的,所以在傳參數時傳入引用是可以減少記憶體開銷的

void swap1(int a, int b) { int temp = a; a = b; b = temp; } void swap2(int &a, int &b) { int temp = a; a = b; b = temp; } int main() { int a{10}, b{20}; swap1(a, b); std::cout << a << " " << b << '\n'; //仍然為10跟20 swap2(a, b) std::cout << a << " " << b << '\n'; //變成了20跟10 return 0; }

上述就是使用引用來達成交換函數的功能,上面也顯示了
一般傳入參數會是複製一份新的給函數使用,而傳引用則是直接使用外部的參數使用,這對記憶體開銷會比較小一點
那問題來了,因為會直接修改到外面的參數,那要怎麼避免呢?請回去看 cosnt 篇 🤣

void test(const int& a) { a = 5; //錯誤,因為a是個const int reference,不可被修改 }

有關&的注意事項:
函數返回值時不可以返回區域引用,會出事的,至於原因各位可以想想

Operator Overloading

接下來,我要隆重介紹一個非常重要的一種函數——運算子(operator)

運算子是一個符號,也是一種函數,它可以告訴編譯器執行指定的數學關係或邏輯運算並產生結果。

在C++中,部分運算子是可以被重載(overloading)的
關於可重載的列表見維基百科 運算子優先級與可重載的運算子列表

基本構型

[return_type] [class]::operator[op]((const) [data_type] (&)[data_name], ...)

直接舉個例子吧。延續上面的 Vector2D,現在我們想要將兩個向量相加,那麼我們應該這樣寫:

const Vector2D operator+(const Vector2D &other);

這樣的意思是要把 *thisother 相加,因此,若我們將其使用後會是這樣:

Vector2D a, b; a.operator+(b); a + b; // 同第2行

定義函數

有兩種寫法:

  1. 直接寫在 class 中
public: const Vector2D operator+(const Vector2D &other) { double x0 = this->x + other.x; double y0 = this->y + other.y; return Vector2D(x0, y0); }
  1. 寫在 class 外
... public: const Vector2D operator+(const Vector2D &other); // 先宣告於class內 } const Vector2D Vector2D::operator+(const Vector2D &other) { double x0 = this->x + other.x; double y0 = this->y + other.y; return Vector2D(x0, y0); }

特別注意:如果是在成員函數內,可直接取用 class 內的 private 成員

那如果我想寫 int + Vector2D 呢?
事實上,operator也可以當作一般函數定義:

const Vector2D operator+(const int &n, const Vector2D &other) { double x0 = n + other.x; double y0 = n + other.y; return Vector2D(x0, y0); }

特別注意,這個函數不須寫在 class 中。

單元運算子

單元運算子分成兩種,一是加在變數前面,另一則是加在後面
加在前面的像是:負號 -x、邏輯反 !x;加在後面的則像是:後綴遞增 x++

  1. 負號的重載
const Vector2D operator-(Vector2D &other) {...}
  1. 後綴遞增的重載
Vector2D Vector2D::operator++(int) {...}

特別注意 int 一定要加

做個小練習

現在請寫一個 class 叫做 complex,裡面包括:

  1. private 的實部 real 以及虛部 imag
  2. 建構子(傳入 double r, i)
  3. 可取得實部及虛部之函數 Re(), Im()
  4. 加減乘除運算(complex ±*/ complex 即可)

繼承 (inherit)

舉另一個例子吧,假設我有一個 class 要來存儲一個交通工具,但我同時有汽車、公車、飛機,如果分開寫,那就太麻煩了。因此,我們先寫好共同的項目,再用「繼承」寫個別的東西。

class Vehicle { public: int fuel, capacity; // constructor Vehicle(const int& f, const int& c) : fuel(f), capacity(c) {} double CP() { return fuel / capacity; }; }; class Car : Vehicle // 繼承 { public: Car(const int& f, const int& c) : Vehicle(f, c) {} // 建構子繼承 };

如此一來,我們不需要把所有交通具都寫出來,而只需要寫一個大綱,在繼承出去寫細項即可。

Car toyota(50, 5); cout << toyota.fuel << "\n" // 50 << toyota.capacity << "\n" // 5 << toyota.CP() << "\n"; // 10

tags: tutorials