Pin-Jiun / Programming-Language-CPP

It's the note/experience about C++, and I have the basic ability of C.
0 stars 0 forks source link

9-Reference #12

Open Pin-Jiun opened 1 year ago

Pin-Jiun commented 1 year ago

Pass by value vs Pass by reference

Pass by value (或叫 Call by value)

當一個參數是 pass by value,呼叫者與被呼叫者是兩個獨立的變數但使用相同的數值。如果被呼叫者更改了參數的數值,並不會影響到呼叫者本身。

Pass by reference (或叫 Call by reference)

當一個參數是 pass by reference,對於參數而言,呼叫與被呼叫者,都使用相同的變數。如果被呼叫者改變了參數的數值,同樣的影響會影響到呼叫者的變數。

pass by reference 與 pass by value 最大的不同就是當變數傳遞進去之後,函數中的區域變數與船進去的變數之間是否有連動

C語言

原始的C語言發明人K&R 在其原著 “The C programming language” 中即已說明, C語言只有call by value

C 不直接支持 pass by reference 因為它總使用 pass by value,但是一個程式設計師可以實作 pass by reference 藉由傳遞指標到程式設計師希望 pass by reference 的變數。

也就是說,C語言就是使用 pointer 的方法來達成 pass by reference 的效果,也就是傳遞pointer來Pass by value

Reference存在的理由

那為什麼還需要有reference呢,直接用pointer傳遞不就好? C++中,在開發者的角度來看,為了減少錯誤的產生,則會使用reference 1.reference cannot be NULL,一定會傳東西進去 2.不會誤動到address, 直接使用pointer必須要小心的處理 3.使用Passing by const reference的使用可以更保障程式的開發 4.在操作物件的時候,能有更方便的操作


Reference參考

參考,比較好理解的稱呼為別名alias 可以指派變數或是指標變數,也就是變數的別名(參考) C++標準API中,表示的方式很常會用到reference &

宣告declare

宣告變數參考 type & name = 變數名稱;

    int a = 8;
    int & r = a;
    r = 10;
    cout << a << endl; //10
    cout << r << endl; //10

    a = 6;
    cout << a << endl; //6
    cout << r << endl; //6

假設a這個變數的記憶體位置是0x0123,裡面存的數值是8 當宣告int & r = a;時,會將 r 這個參考別名的記憶體位置也設為和a一樣為0x0123 此時可以將ar視為同一個變數,更動a就會更動r

宣告指標參考 type *& name = 指標名稱;

    int* b = new int;
    *b = 8;
    int* & rp = b;    //rp是一個b的別名(參考),參考的型別是int*
    *rp = 12;
    cout << *b << endl; //12
    cout << *rp << endl; //12

假設b這個指標變數的記憶體位置是0x1234,所指向的位置是0x5678,0x5678記憶體內存的數值是8 當宣告int* & rp = b;時,會把 指向 的記憶體位置0x5678分享給rp這個參考 也就是b這個pointer和rp會指向同一個address

宣告的時候一定要指派,且只能指派一次

    int & c; //error

參考參數


void changeValue1(int & a){a = a+1;}
void changeValue2(int *& b){*b = *b+1;}

int main()
{
    int i = 1;
    changeValue1(i);
    cout << i << endl;      //2

    int* p = new int(6);
    changeValue2(p);
    cout << *p << endl;     //7
}

此時原理大致如下 <html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">

Memory | value | variable | alias -- | -- | -- | -- 0x987 | 1 | i | a 0x986 | 0x985 | p | b 0x985 | 6 |   |  

i會使用reference的方式當作arg傳入給changeValue1這個function,此時對a操作也會更動到i p同理,此時對*b操作也會更動到*p,因為兩者都是指向同一個位置

上述兩者也就是所謂的 Pass by reference

const修飾字

const可以告訴使用者此function不會更動到傳遞進來的reference,只會使用他進行運算 如果強行更改會造成編譯錯誤

void changeValue3(const int & c){c = c+1;}
//error: assignment of read-only reference ‘c’

Return Reference - OOP與reference

假設今天想要使用物件當作參數傳遞,傳遞的方式是Pass by value,也就是copy一份給function

class Circle{
public:    
  int radius;
};

void changeRadius(Circle  a, int r){
    a.radius = r;
}

int main()
{
    Circle c;
    c.radius = 10;
    cout << c.radius << endl;       //10
    changeRadius(c,30);
    cout << c.radius << endl;       //10
    return 0;
}

C++中pass by value的原因可能是以下幾種

1.因為C++是OOP,會將int, float等等當作物件 2.沒有進行區分是否為物件 3.不是所有東西都能pass by reference, 要 lvalue才能pass by reference 4.考慮到c的相容性, 並無直接傳遞reference(?

void foo(type arg)
```c++

arg is passed by value regardless of whether type is a simple type(int, char), a pointer type or a class type
也就是說不管多複雜的物件都會是pass by value

array的話則是因為C語言在當初設定的時候傳遞的是pointer,顧會沿用則不做更動

今天要對物件進行操作有兩種方式
1.回傳物件本身
```c++
class Circle{
public:    
  int radius;
};

Circle changeRadius(Circle  a, int r){
    a.radius = r;
    return a;
}

int main()
{
    Circle c;
    c.radius = 10;
    cout << c.radius << endl;       //10
    c=changeRadius(c,30);
    cout << c.radius << endl;       //30
    return 0;
}

然而此種會多浪費記憶體,複製一份新的Circle物件

2.return reference

class Circle{
public:    
  int radius;
};

Circle & changeRadius(Circle &a, int r){
    a.radius = r;
    return a;
}

int main()
{
    Circle c;
    c.radius = 10;
    cout << c.radius << endl;       //10
    changeRadius(c,30);
    cout << c.radius << endl;       //30
    return 0;
}

這種方式就可以直接對c進行操作,是比較好的方式 也可以寫成用pointer的方式宣告物件

class Circle{
public:    
  int radius;
};

Circle* & changeRadius(Circle *& a, int r){
    a->radius = r;
    return a;
}

int main()
{
    Circle *c = new Circle;
    c->radius = 10;
    cout << c->radius << endl;  //10
    changeRadius(c,20);
    cout << c->radius << endl;  //20
}
Pin-Jiun commented 1 year ago

&& rvalue 參考, 右值參照

C++中reference是指繫結到記憶體中的相應物件上。左值參照是繫結到左值物件上;右值參照是繫結到臨時物件上

int& : lvalue,左值參考, 別名, 運算式的指派會是個有名稱的物件

    int a = 10;
    int& b = a;

但如果右側是臨時的值,會暫時存在記憶體無法被取址的則會error

    int a = 10;
    int b = 5;
    int& c = a + b;//error

int&& : rvalue 參考(rvalue reference), 是指派沒有名稱的物件

    int a = 10;
    int b = 5;
    int&& rr = a + b; // c = 15

int&& 是 rvalue 參考(rvalue reference),rr 參考了 a + b 運算結果的空間,相對於以下的程式來說比較有效率:

int a = 10;
int b = 20;
int c = a + b; // 將 a + b 的結果複製給 c

因為不必有將值複製、儲存至 c 的動作,效率上比較好 特別是當 rvalue 運算式會產生龐大物件的時候,複製就會是個成本考量

例如 s1、s2 若是個很長的 string,那麼 s1 + s2 的結果還會複製給目標 string 的話:

string result = s1 + s2;

改用以下會比較經濟:

string &&result = s1 + s2;

rvalue 是指:


move

C++11標準給了使用者更大的決定權,可以使用move把左值,轉化為右值參照 通過定義在C++標準程式庫 <utility> 中的std::move

#include <iostream> 
#include <string>
using namespace std; 

int main() { 
    string s1 = "abc";
    string s2 = s1;     //  複製 s1 的資料

    cout << s1 << endl; // 顯示 "abc"
    cout << s2 << endl; // 顯示 "abc"
} 

如果使用move, s1 的資料就被移至 s2 了,在這之後不能立即使用 s1 來取值,因為資料轉移出去了 使用 s1 來取值取值結果是不可預期的,只能銷毀 s1,或者是重新指定字串給 s1

#include <iostream> 
#include <string>
#include <utility>

using namespace std; 

int main() { 
    string s1 = "abc";
    string s2 = std::move(s1);    //  轉移 s1 的資料

    // cout << s1 << endl;        // 這時取值結果不可預期
    cout << s2 << endl;           // 顯示 "abc"

    s1 = "xyz";                   // OK
    cout << s1 << endl;           // 這時可以取值
} 

因為 move 這個名稱太平凡了,為了避免名稱衝突,建議包含 std 名稱空間,也就是使用 std::move。

參考資料 https://openhome.cc/Gossip/CppGossip/RvalueReference.html