1. 程式人生 > >C++陣列處理以及左值與右值探究

C++陣列處理以及左值與右值探究

C++對字元陣列的處理和一般陣列不同,如果不弄清楚,使用過程中就有可能犯迷糊。
那麼究竟有什麼不同呢?先看下一般陣列的情況。

一般陣列

    int a[4]{ 1,2,3,4 };
    int b{ 2 };
    cout << a << endl//輸出第一個元素地址
        << &a << endl//輸出第一個元素地址
        << &a[0] << endl //輸出第一個元素地址
        << *(&a) << endl//輸出第一個元素地址
<< &(*a) << endl//輸出第一個元素地址 << *a << " " << *&(*a) << " " //輸出第一個元素 << << " " << a[0] << endl;//輸出第一個元素

這段程式碼可以通過VS2013的編譯。對於陣列的操作無疑有兩種,一是獲取陣列地址,二是運算元組元素,然而獲取陣列地址的最終目的還是運算元組元素。
〈1〉程式碼中獲取第一元素地址用了5種方法,常用的只有一種cout<<a

,第二元素的址就是a+1,第n個元素地址就是a+(n-1)。
〈2〉.如果第一元素的實體地址是0038FC60,那第二元素則是0038FC64,為什麼不是0038FC61呢?
因為int型別資料佔用4個位元組,所以後一元素地址都比前一元素遞增4,所以第n個元素實體地址實際是a+(n-1)*sizeof(int),其中sizeof(int)返回int型別資料的位元組數。
〈3〉繼續研究這段程式碼,cout<<&a的用法和字元陣列是相同的,但是字元陣列如果寫成這樣cout<<ch(假設char ch[10]{})那輸出就是整個字串。使用陣列名稱時,編譯器自動將其隱式轉換為指標,儲存的地址是陣列的首地址。
〈4〉*(&a) 和&(*a)其實就是a,因為間接定址運算子“*”
和取址運算子“&”互為逆運算,相互抵消還原為a。同理*&(*a)和**(&a)也是如此,從右至左運算,最後都*a,表達的都是第一個元素值。訪問元素常用的方法還是a[0],第n個元素就是a[n-1]。
採用地址偏移法也能訪問陣列中的其他元素,如*(a+1)就是第二元素a[1].
〈5〉但是如果用&(a+1)獲取第二元素地址就會出錯,提示“&”操作的物件必須是左值。那麼這裡要討論一下什麼是左值和右值。

左值和右值

C++沒有嚴格定義左值和右值,簡單來說:
(1)左值是記憶體中持續儲存資料的地址,右值是臨時儲存的表示式結果,因此,左值能夠被定址,而右值不能。
(2)左值可以出現在賦值運算子“=”的左邊或右邊,但右值只能出現在“=”右邊,因此,左值的記憶體資料可以被修改,不能修改的表示式不是左值。
(3)只包含一個命名變數的表示式始終是左值,一般來說,由運算子連線的多變量表達式是右值。
(4)const 修飾的變數是左值,但它是唯一不能出現在“=”左邊的左值。
a[0]=b, a[0],b都是左值
b=b+1, b+1是右值
a[0]=++b, ++b是左值
a[1]=b++, b++是右值
const int val{3};val是左值,必須初始化,不能出現在=左邊,因此不能修改其值
為什麼++b是左值而b++是右值呢?
a[0]=++b,b是自增後再傳遞給a[0],本質還是a[0]=b.
a[1]=b++,b先臨時傳遞給a[1]後再自增,因此a[1]得到是b的臨時原值,而不是b,這之後a[0]!=b。
經過這番探究之後,終於明瞭&(a+1)出錯的原因。如果要獲取第二個元素地址,直接使用a+1就行,或者&a[1],當然還可以用static_cast<void*>(a + 1),這個方法在後面研究字元陣列時還要講到,這方法對於一般陣列作用不大,但對於字元陣列卻不可或缺。

字元陣列

char buffer[]{"My name is Lily."};

    cout << buffer << endl//My name is Lily
        << &buffer << endl//“M”的實體地址
        << buffer[0] << endl//My name is Lily
        << &buffer[1] << endl//y name is Lily
        <<buffer+1<<endl;//y name is Lily

輸出結果寫在註釋裡,對比一般陣列有以幾點發現:
<1>一般陣列單獨使用陣列名稱時,輸出是第一元素地址,而字元陣列單獨單獨使用陣列名稱時,輸出是整個字串。
<2>為方便描述,暫時用“_name”代替陣列名稱。一般陣列,&_name[index]表示元素地址,而字元陣列則表示為從_name[index]元素開始到結束的字串。與第<1>有共通點,一般陣列表示地址的,字元陣列表示字串。
<3>字元陣列不能像一般陣列一樣使用_name+index(陣列名稱+索引) 得到元素地址,只能用static_cast<void*>(_name+index)
static_cast<void*>(&_name[index]).
<4>對於兩類陣列,_name[index]都表示訪問索引為index的元素。
<5>&buffer+1表示緊跟字元陣列後的一個地址,如例中字元陣列長度為16+1個位元組(16是可見的字元,1表示一個終止符’\0’,每個字元佔用一個位元組),因此&buffer+1對應的實體地址實際為&buffer+17.

總結
以上所寫有點混亂,而且一些用法不是常用甚至從來不用的。從用法來總結:

操作 一般陣列 字元陣列
訪問單個元素 _name[index] _name[index]
訪問整個陣列 遍歷所有元素 使用陣列名稱_name
訪問單個元素地址1 &_name[index] static_cast<void*>(&_name[index])
訪問單個元素地址 2 static_cast<void*>(_name+index) static_cast<void*>(_name+index)
整個陣列地址 _name &_name

擴充套件:左值引用與右值引用

單純的左右值並沒有意義,但左傳引用和右值引用卻是個非常有趣的話題。要想比較清楚的瞭解這兩個概念,必須要先說函式的引數傳遞。
函式引數傳遞
假設這個函式的原型所對應的定義是實現兩數之和,那麼呼叫這個函式時,實參是以按值傳遞”的方式傳遞給函式的。

#include<iostream>
using namespace std;
double sum(double, double);
void main()
{
    double val1{ 21 };
    double val2{ 31 };
    cout << val2 << " ";
    cout << sum(val1, val2) << " ";
    cout << val2 << endl;
}
double sum(double rval1, double rval2)
{
    return rval1 + rval2++;
}

〈1〉按值傳遞
所謂按值傳遞,就是建立實參的副本,函式使用的就是這個副本而不是實參本身。因此,函式表面上所有針對實參的操作,實際操作的物件是實參的副本,與實參本身毫無關係。那麼,函式想修改實參的值是不可能實現的,因為它修改的實參副本的值,函式結束後,這個副本會被銷燬。如果函式想實現修改實參的功能,那麼只能使用指標或者引用。

〈2〉指標
假設函式想實現兩數之和,然後val2的值增加1,我們可以這樣做:

#include<iostream>
using namespace std;
double sum(double, double*);
void main()
{
    double val1{ 21 };
    double val2{ 31 };
    double* pval2{ &val2 };
    cout << val2 << " ";
    cout << sum(val1, pval2) << " ";
    cout << val2 << endl;
}
double sum(double val, double* pval)
{
    return val + (*pval)++;            //括號是必須的,字尾++的優先順序比間接定址運算子“*”高
}

實際上這還是按值傳遞,函式建立了指標的副本,副本指標與實參指標指向同一個物件val2,因此副本指標解除引用後的自增運算,間接操作的物件就是val2.這種方式雖然可以實現我們的意圖,但實則還是與按值傳遞並無二致。
按值傳遞存在很大的缺陷,建立副本會產生額外的系統開銷,如果實參是一個擁有大量資料成員的類物件,那麼按值傳遞的這個缺陷就會十分明顯。
有沒有一種方法,可以讓函式直接使用實參本身呢?當然有,這種方法就是引用。
〈3〉左值引用
引用,分為左值引用和右值引用。
左值引用,通俗點就是別名,外號。例如:

double val2{31};          //val2是一個左值
double& rval2{val2};             //定義val2的左值引用

rval2是val2的一個別名,它們是同一個變數。因此上面的函式可以這樣改:

#include<iostream>
using namespace std;
double sum(double&, double&);
void main()
{
    double val1{ 21 };
    double val2{ 31 };
    cout << val2 << " ";
    cout << sum(val1, val2) << " ";
    cout << val2 << endl;
}
double sum(double& rval1, double& rval2)
{
    return rval1 + rval2++;
}

對比其他兩種情況,我們不再需要定義額外的變數,使用起來就跟按值傳遞時沒什麼兩樣,但因為函式呼叫時使用的是變數的別名(左值引用),也沒建立副本的步驟,系統開銷比以上兩種情況都要小。
那麼什麼是右值引用呢?
〈4〉右值引用
右值是一個臨時變數,通常是表示式的求值結果,不能常駐記憶體。下面定義一個右值引用:

double&& val{val1+val2};

事實上這樣定義一個右值引用毫無意義,我們完全可以這樣做:double val{val1+val2};而且double&& val{val1+val2}所定義的val本身是一個左值。當然我們不可能定義這樣的變數double& val{val1+val2},無法用右值初始化一個左值引用。
似乎右值引用沒什麼用處了?當然不是,上面的定義僅僅表示右值引用的形式而已,右值引用的巨大作用體現在函式的引數傳遞中。我們重寫sum()函式:

double sum(double&& rval1,double& rval2)
{return rval1 + rval2++;}

sum()第一個引數現在只能接受右值實參,而不能接受左值。
比如:sum(12+val1,val2)這樣的呼叫,第一個形參直接引用12+val1計算出的結果所在的記憶體(臨時儲存)。對比下面左值引用的情形的形式:

val1+=12
sum(val1,val2);                       //左值引用的形式

左值引用必須增加一步才能實現右值引用的效果,當然這也僅僅是演示右值引用在函式引數傳遞的應用而已,實際上並沒有體現出右值引用的巨大作用。
正如左值引用一樣,右值引用真正的用武之地也是對類的操作。