[筆記]介面與實作、運算子多載、左值右值、參數傳遞、回傳多值|C++

運算子多載如何執行?int a = 10; ++a—;這串程式碼為什麼會出錯?居然跟左值右值有關!左值右值真的是一左一右嗎?參數傳遞中的傳指標和傳參考差在哪邊?C++居然無法回傳多值,那我該怎麼回傳多值?

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

一、介面 (interface)與實作 (implementation)分離

實務上,會建議介面和實作分離,一個粗糙的分法是.h檔為介面,.cpp檔為實作,但這樣的分法不完全正確。 介面是指使用者會使用到的東西,例如:public函數的標頭、class的標頭。而function的實作和private (member data)裡的東西,都不屬於是介面,因為使用者不會直接使用到這些東西。


因此通常#include進來的檔案是介面,檔案中不會看太多實作的細節。這樣有些好處:第一、使用者不用知道如何實作,只要學會如何使用介面,即可使用這些功能,所以套件也是介面與實作分開的例子。第二、如果套件的更新只更新實作部分,而不更動到介面部分,這樣使用者的程式碼不需要變動,也能更新套件的功能。而如果套件的介面與實作是合在一起的,每次套件更新,使用者就可能要再更新自己的程式碼,這樣的套件是不會有人想用的。

介面與實作分開並不會影響到編譯器 (complier)的運作,因為編譯器只需要知道函式名、回傳值、參數列,這也剛好是函式的標頭 (header)內容。只有linker or loader (OS的程式,loader也可以包含linker的工作)才需知道實作內容。

至於哪些東西要放在介面?哪些要放在實作?基本上沒有一個有系統的規則,主要以經驗談。大方就是Public的介面更動相對難,Private的實作更動相對簡單。inline, explicit, default argument, virtual, override要放在.h中,.cpp不用放。

 二、函式 (Function)

運算子 (operator)、函式 (procedure)、函數 (function),本質上無太大的差別,只不過運算子可以使用運算子符號來呼叫函式,當然也可以用函式形式來呼叫函式;而函式 (procedure)是指無回傳值 (void)的;函數 (function)則是指有回傳值的。不過函式與函數的區分並沒有太大的意義,因此也常會混用。

(一)運算子多載 (Operator overloading)

函式多載有些前提:(1)需要在同一個scope內,不同scope就不是overloading。(2) 函式名相同,但簽章 (signature)不同。因此對於C++來說,每個多載函式都是獨立的函式,他們都是不同的函式,因為各自有獨特的簽章。函式簽章通常包含:函式名、參數型態、參數排序。

然後也不是所有的程式語言都支援運算子多載,C++有支援,但JAVA不支援,所以在JAVA中必須用函式多載。運算子多載如何運作?以輸入為例:cout << int << double << string;

1. 先overload operator 

cout << int == cout.operator<<(int),這會回傳cout (ostream&),使得可以連續輸出。cout << 之所以可以輸入不同型態的變數,是因為<<為一個多載運算子。

2. 決定優先順序:

(1) Precedence (優先順序)

(2) Associativity (結合性,左右順序)

Cout << int << double << string; == ( ( (Cout << int) << double) << string);

a<b<c必須拆成a<b && b<c,否則會有問題,因為這攸關結合性問題。a<b<c == ((a<b)<c),如果(a<b)為真,即會回傳1,因此((a<b)<c) == (1<c),顯然這不是我們要的結果。

&& 和 || 這兩個operator如果要overloading必須小心,e1 && e2 && e3 && e4……,原本如果e1為false,判斷就會結束,但如果overloading,此性質就會消失。

更多運算子優先順序的例子可以看期中考第8題

(二)左值 (lvalue)與右值 (rvalue)

左值與右值明顯的議題會出現在賦值 (assignment)這一動作,如:a = 1、b = c。首先何謂左值?何謂右值?左值通常是等號左邊的那個變數,也就是a和b都是左值,變數的本質是有名有記憶體位置 (location)的,而左值必須為變數,因此左值一定是有名字的;右值通常是等號右邊的數,也就是1和c都是右值,1本身是一個樣板數字 (literal number),是一個暫時且沒有名字的東西,因此右值可以是沒有名字的東西。因此一個簡單的判斷左右值的方法即為,左值需有名,右值可無名

有些運算子是涉及到賦值,如:=, ++, --, +=, -=, .......。++a == a = a + 1、—b == b = b - 1。以下有一個涉及到左右值的運算子問題:

int a = 10; ++a—;  // == (++(a—))

請問上面的a值為何?9?10?11?錯誤?答案是Compiling error。 因為++a需要一個左值 (lvalue),因為++a的參數需要一個左值且內含一個assignment,但a--會給右值 (rvalue)。

網友補充到,*(new int) = 10;這一運算式是合法,且可以發現左值雖是匿名的,但它有自己的記憶體位置,不過這會造成記憶體遺失的問題。在舉另一個,在運算子多載當中,如果要連續呼叫,則必須回傳指標或參考,如:A& operator<<(A& a){........; return a}。

因此綜合以上討論,有名的變數、回傳指標/參考、動態記憶體 (new type)都可以是左值,且三者的共通點都是在記憶體中有位置 (location),因此,有無名的判斷雖是個簡單的方法,但並非完美的方法,用記憶體位置當作判準會更為完美。

然後++, —盡量不要和巨集 (macro)一起使用,因巨集會執行文字取代,容易會有意外的副作用。關於巨集的副作用,可以參考[筆記]類別、特殊函式、內嵌函式、函式物件|C++

更多左值右值的例子可以看期中考第6題

(三)參數傳遞

C語言的參數傳遞都是call-by-value,而pointer可以call-by-address。C++的call-by-reference和C的call-by-address很像,只不過C中的& (get address) and * (dereference)在C++中,編譯器自動幫你做。


call-by-value的缺點就是效率不好,因為它是用複製的方式在傳參數,但優點就是原本的參數不會因為函數內對引數做任何改變,而有所影響。call-by-reference的效率就會比較好,因為它是傳參考,參考可以視為變數的別名,因此只要改變引數,就會改變原本的變數。有時為了追求效能,但要沒有改變資料,就可以使用const reference 。

(四)回傳多值

C/C++只能回傳一個值,無法回傳多值。所以可能的方法有:

  1. 回傳指標:蠻危險的,因為要避免指到區域變數,或是被區域指標指到,區域變數或指標皆會因為block結束,而跟著被回收。如:int* function(int a[]){...}。
  2. 回傳struct / class:要先自己建立struct / class,將多個變數包在struct / class,struct / object可用call-by-value傳出去。那段要強調的是class / struct可以把很多東西包起來的概念,以達到回傳多值的目的。如:A為類別,A function(A a){...}。
  3. call-by-address / reference:比較安全,但如果回傳值太多,就可能會需要有多個變數,或是用class / struct承載多個值。如:A為類別,void function(A* a){...}。
  4. structured binding C++17:
    1. std::tuple<int, int> foo(){return {1, 2};}
    2. auto [a, b] = foo();

留言