[筆記]類別、特殊函式、內嵌函式、函式物件|C++

程序導向的C語言也能實作物件導向?C語言要如何模擬類別?C struct與C++ struct與C++ class這三者有什麼差別?類別中有哪六個特殊函式?初始化和賦值有什麼差別?內嵌函式和巨集很像?物件居然可以拿來當成函式?為什麼C++宣告無參數物件時,不用加()?

還是很困惑嗎?文章裡有答案喔~😎

一、類別

(一)物件導向

為什麼會從程序導向走向物件導向?因為現實生活中,很多都是以物件的方式呈現。物件導向主要三個功能:
  1. 封裝 (encapsulation):改善C語言的封裝與權限問題,封裝完整的話,有利於debug,讓錯誤限制在某一個區塊。
  2. 繼承 (inheritance):重複使用程式碼。
  3. 多型 (polymorphism):可以解決switch的問題。同樣函式名與參數,即同樣的函式簽章 (signature),能給出不同的定義。不用動別人的原始碼就可以改變別人函式的功能。

(二)C++ 類別 (class)與結構 (struct)

C++的struct和class兩者幾乎沒有差別,但實務上會希望有所區分。

C++ struct

C++ class

預設public權限

預設private權限

預設public繼承

預設private繼承

不能用在template

可用在template

C++ 的 struct 和 class都可以:

  1. 宣告成員資料
  2. 宣告成員函式
  3. 繼承、多型、建構子、解構子、interface

C++物件把data和operations分開,class = data type (data member) + operation (member function)。

更多存取權限與繼承模式,可以參考[筆記]繼承模式與存取權限|C++

(三)C語言模擬類別 (class)

但C struct和C++ struct就不太一樣,C struct可以(1)宣告成員變數,而(2)成員函式和(3)物件導向的東西都不能使用。不過C仍可使用struct模擬class,C struct要用指向函式的指標來代替成員函式,所以C struct的實例 (instances)中,除了有成員資料,還會有很多的指標變數來儲存函式;而C++ class使用this指標去指向成員函式,以達到每個實例共用同一套成員函式的目的,因此每個class實例只會存放成員資料,故C++ class實例的大小就會比C struct實例來得小。

使用C struct模擬C++ class,struct client_t有四個變數,三個是資料,第四個是指向函式的指標
程式碼來源

如果想要C struct實例共享同一套函式,則可以直接使用extern,使其他檔案都可以這一套函式,對C struct實例做操作。

物件 (object)、實例 (instance)、變數 (memory allocation),三者概念相近。Struct使用dot (.)存取資料,Class也是使用dot (.),但如是指標,則要使用箭號(->)或start (*) + dot (.)。

二、特殊成員函式

類別有6個特殊成員函式 (special member function),C++會自動幫你產生,通常一個要更動,其他特殊函式可能也要更動。
  1. 預設建構子 (default constructor)
  2. 解構子 (destructor)
  3. 複製建構子 (copy constructor)
  4. 複製運算子 (copy assignment operator)
  5. 移動建構子 (move constructor)
  6. 移動運算子 (move assignment operator)

(一)建構子 (constructor)

C語言中常會忘記初始化就直接使用,這樣會出現一些神奇的問題,所以C++有預設的建構子,就可以避免這樣的問題。C++建構子會幫你自動初始化變數,避免用到未初始化的變數。

class::constructor (int a, int b)

:data1(a), data2(b) // Initialization,使用初始列較有效率

{ data1 = a; data2 = b; } // Assignment,較沒效率

1. 初始化 (Initialization)

通常會和宣告(配置記憶體)綁在一起,然後賦值給未初始化的變數,故需要配置+賦值,如:int a =10;

2. 賦值 (Assignment)

用另一個值給已經初始化的變數,所以要先clean up已初始化的變數,故只更新值,如:a = b;

3. 常數變數 (Constant variable)

傳遞參數是初始化還是賦值呢?是初始化,因為傳遞參數時,我們可以使用常數變數,而常數變數只能初始化一次,且不能做賦值 (assignment),故是初始化的動作。如:

  • 傳遞參數:int func(const int a);
  • const int c = 1; //合法
  • const int d; d = 2 // 非法,因為d已經被初始化為0,不能再賦值為2

(二)其他特殊函式

template<class T>
class Handle {
T* p;
public:
Handle(T* pp) : p{pp} {} // default constructor
~Handle() { delete p; } // destructor 

Handle(Handle&& h) :p{h.p} { h.p=nullptr; } // move constructor, 舊的object一定要接上nullptr,因為如果不接上nullptr,新舊參考都指向同一個東西,舊的object會自動呼叫destructor,則舊的object所指向的東西也會一並刪除,此時新的參考所指向的東西就會被free掉。
Handle& operator=(Handle&& h) { delete p; p=h.p; h.p=nullptr; return *this; } // move assignment operator, 少了if(this != &h)

Handle(const Handle&) = delete; // copy constructor, delete 是指no copy
Handle& operator=(const Handle&) = delete; // copy assignment operator, delete 是指no copy

// ...
};

原始碼連結

三、內嵌函式 (Inline function)

內嵌函式和巨集 (macro)很像,都是文字取代機制,可以降低函式指標跳轉的時間。因此內嵌函式可以取代巨集的函式定義的功能,且內嵌函式可以避免巨集的一些問題,尤其是++或--的時候,不過缺點就是記憶體用量變多。

  • 內嵌函式:
    • inline int max (init a, int b){ return a>b ? a : b; }
    • int m = max(++x, ++y); // 這邊x或y只會被加一次
  • 巨集:
    • #define max(a, b) a>b ? a : b
    • int m = ++x > ++y ? ++x : ++y; // 這邊x或y會被加二次

如果要宣告內嵌函式,要在header中宣告+實作,不能將實作分開在cpp中。因為編譯器要知道內嵌函式的實作內容,才能做文字取代,不然編譯器不知道實作內容,就無法插入完整的程式碼。

四、函式物件 (functional object)

函式物件是將物件當成函式來使用,搭配多載運算子"()"一起使用。

  • struct FO{ int operator()() { return 100;}  };
    • C++ struct幾乎和C++ class一樣,多載運算子()
  • Fo fo; std: :cout << fo() << std::endl;
    • Call fo.operator()()
    • 呼叫fo物件的多載運算子()
  • std::cout << FO()() << std::endl; 
    • Call default constructor == FO().operator()()
    • 呼叫預設建構子的多載運算子()
    • FO()是在呼叫default constructor,第二個()是在呼叫多載運算子()
  • std: :cout << FO{}() << std: :endl;
    •  Call default constructor, {} is initializer, == FO{}.operator()()
    • 一樣是呼叫預設建構子的多載運算子(),只不過這裡是使用初始化列{}來初始化物件。

C++11的lambda表達式,由於編譯器會將lambda表達式轉成函式物件,故也是一種函式物件,只不過它是一種匿名的函式。

為什麼C++在宣告沒有引數的物件時,不能加()?因為C++會無法分辨className()為建構子還是函式物件這算是C++先天的缺陷。因此C++認為className()為函式物件,而className為建構子。

五、相關文章

留言