程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 你好,C++(40)7.1 一切指針都是紙老虎:徹底理解指針,7.1紙老虎

你好,C++(40)7.1 一切指針都是紙老虎:徹底理解指針,7.1紙老虎

編輯:C++入門知識

你好,C++(40)7.1 一切指針都是紙老虎:徹底理解指針,7.1紙老虎


第7章 C++世界的奇人異事

在武俠小說中,初入武林的毛頭小子總是要遇到幾位奇人,發生幾件異事,經過高人的指點,經歷一番磨煉,方能武功精進,從新手成長為高手。在C++世界,同樣有諸多的奇人異事。在C++世界中游歷學習的我們,是否也同樣期望著遇到幾位奇人,經歷幾件異事,而後從一個C++新手成長為C++高手呢?

武林中的奇人異事可遇而不可求,但是C++世界中的奇人異事卻可以為你一一引見。

7.1  一切指針都是紙老虎:徹底理解指針

C++世界中什麼最難?指針!C++世界中什麼最強?指針!

指針作為C++世界中一種特殊的訪問數據的方式,因為其使用方式的靈活而使得它在C++世界中顯得威力無比;然而,也正是因為它的靈活,使得它成為初學者最難掌握的C++技能。它就像一只吊睛白額“大老虎”,雖然威力無比但是難以掌握控制,用好了可以方便高效地解決問題,但如果使用不當卻又很可能給程序帶來災難性的後果。今天就來打倒指針這只“紙老虎”,徹底掌握控制指針。

7.1.1  指針的運算

從本質上講,指針也是一種數據,只不過這種數據有點特殊而已。我們通常所見的數據就是各種數值數據文字數據等,而指針所表示的是內存地址數據。既然是數據,那麼自然就涉及到了數據的運算。像普通數據一樣,指針也可以參與部分運算,包括算術運算、關系運算和賦值運算,而我們最常用的就是指針的算術加減運算。

如果指針的值是某個內存位置的地址值,那麼我們就說指針指向這個內存位置。而指針的加減運算,實際上是讓指針的指向發生偏轉,指向另外的內存位置。通過這種指針的偏轉,可以靈活地訪問到該指針起始位置附近的內存。如果這種偏移是在某個范圍內連續發生的話,則可以通過指針訪問到某一連續內存區域的數據。例如,在3.6節中介紹過數組,數組名實際上就是數組數據所在內存區域的首地址,表示數組在內存中的起始位置。可以通過把首地址賦值給指針,然後對該指針進行加減運算,使指針發生偏轉指向數組中的其他元素,從而遍歷整個數組。例如:

int nArray[3] = { 1, 2, 3 };   // 定義一個數組
int* pIndex = nArray;          // 將數組的起始地址賦值給指針pIndex
 cout<<"指針指向的地址是:"<<pIndex<<endl;       // 輸出指針指向的地址
cout<<"指針所指向的數據的值是:"<<*pIndex<<endl; // 輸出這個位置上的數據

pIndex++;   // 對指針進行加運算,使其指向數組中的下一個值
cout<<"指針指向的地址是:"<<pIndex<<endl;        // 輸出指針指向的地址
cout<<"指針所指向的數據的值是:"<<*pIndex<<endl;  // 輸出數據

這段程序執行後,可以得到這樣的輸出:

指針指向的地址是:0016FA38

指針所指向的數據的值是:1

指針指向的地址是:0016FA3C

指針所指向的數據的值是:2

從輸出結果中可以看到,pIndex指針初始指向的地址是0016FA38,也就是nArray這個數組的首地址。換句話說,也就是pIndex指向的是數組中的第一個數據,所以輸出“*pIndex”的值是1。而在對指針進行加1運算後,指針指向的地址變為0016FA3C,它向地址增大的方向偏移了4個字節,指向了數組中的第二個數據,輸出“*pIndex”的值自然也就變成了2。

這裡大家肯定會奇怪,對指針進行的是加1的運算,怎麼指針指向的地址卻增加了4個單位?這是因為指針的加減運算跟它所指向的數據的真正數據類型相關,指針加1或者減1,會使指針指向的地址增加或者減少一個對應的數據類型的字節數。比如以上代碼中的pIndex指針,它可以指向的是int類型的數據,所以它的加1運算就使地址增加了4個字節,也就是一個int類型數的字節數。同樣的道理,對於可以指向char類型數據的char*類型指針,加1會使指針偏移1個字節;而對於可以指向double類型數據的double*類型指針,加2會使指針偏移16(8*2)個字節。指針偏轉流程如圖7-1所示。

 

圖7-1  指針運算引起的指針10:43:4010:43:41偏轉

除了指針的加減算術運算之外,常用到的還有指針的關系運算。指針的關系運算通常用“==”或“!=”來判斷兩個相同類型的指針是否相等,也就是判斷它們是否指向同一地址上的同一數據,以此作為條件或循環結構中的條件判斷語句。例如:

int nArray[3] = { 1, 2, 3 };    // 定義一個數組
int* pIndex = nArray;           // 將數組的起始地址賦值給指針pIndex
int* pEnd = nArray + 3;         // 計算數組的結束地址並賦值給pEnd
while( pIndex != pEnd )         // 在while的條件語句中判斷兩個指針是否相等,
                               // 也就是判斷當前指針是否已經偏轉到結束地址
{
    cout<<*pIndex<<endl;        // 輸出當前指針指向的數據
    // 對指針進行加1 運算,
   // 使其偏移到下一個內存位置,指向數組中的下一個數據
    ++pIndex;                  
}

在以上這段代碼中,利用表示數組當前位置的指針pIndex跟表示結束位置的指針pEnd進行相等與否的比較,如果不相等,則意味著pIndex尚未偏移到數組的結束位置,循環可以繼續對pIndex進行加1運算,使其偏移至下一個位置指向數組中的下一個元素;如果相等,則意味著pIndex正好偏移到數組的結束位置,while循環已經遍歷了整個數組,循環可以結束。

另外,指針變量也常和nullptr關鍵字進行相等比較,來判斷指針是否已經被初識化而指向正確的內存位置,也就是判斷這個指針是否有效。雖然我們提倡在定義指針的同時就完成對它的初始化,可有時在定義指針的時候,並沒有合適的初始值可以賦給它,但如果讓它保持最開始的隨機值,又會產生不可預見的結果。在這種情況下,我們會在定義這個指針的同時將這個指針賦值為nullptr,表示這個指針還沒有被初始化,處於不可用的狀態。而等到合適的時候,再將真正有意義的值賦值給它來完成這個指針的初始化,這時指針的值將不再是nullptr,也就意味著這個指針處於可用的狀態。所以,將nullptr跟某個指針進行相等比較,是判斷這個指針是否可用的常用手段。下面是一個典型的例子:

int* pInt;          // 定義一個指針,這時的指針是一個隨機值,指向隨機的一個內存地址
// 將指針賦值為nullptr,表示指針還沒有合適的值,處於不可用的狀態
pInt = nullptr;      

//…

int nArray[10] = {0};
pInt = nArray;      // 將數組首地址賦值給指針
if( nullptr != pInt ) // 判斷指針是否已經完成初始化處於可用狀態
{
    // 指針可用,開始使用指針訪問它指向的數據
}

因為通過指針可以直接訪問它所指向的內存,所以對尚未初始化的指針的訪問,有可能帶帶來非常嚴重的後果。而將指針與nullptr進行相等比較,可以有效地避免指針的非法訪問。雖然在業務邏輯上這不是必須的,但這樣做可以讓我們的程序更加健壯,所以這也是一條非常好的編程經驗。

7.1.2  靈活的void類型和void類型指針

C++是一種強類型的語言,其中的變量都有自己的數據類型,保存著與之相應類型的數據。比如,一個int類型的變量可以保存數值1,而不能保存數值1.1,它需要一個與之相應的double類型的變量來保存。相應數據類型的變量保存相應的數據,本來相安無事過的好好的。但是,在C++世界中卻出現了一個異類,那就是void類型。從本質上講,void類型並不是一個真正的數據類型,我們並不能定義一個void類型的變量。void更多的是體現一種抽象,在程序中,void類型更多的是用於“修飾”和“限制”一個函數。例如,如果一個函數沒有返回值,則可用void作為這個函數的返回值類型,代替具體的返回值數據類型;如果一個函數沒有形式參數列表,也可用void作為其形式參數,表示這個函數不需要任何參數。

跟void類型對函數的“修飾”作用不同,void類型指針作為指向抽象數據的指針,它可以成為兩個具有特定類型的指針之間相互轉換的橋梁。眾所周知,在用一個指針對另一個指針進行賦值時,如果兩個指針的類型相同,那麼可以直接在這兩個指針之間進行賦值;如果兩個指針的類型不同,則必須使用強制類型轉換,把賦值操作符右邊的指針類型轉換為左邊的指針類型,然後才能進行賦值。例如:

int* pInt;                // 指向整型數的指針
float* pFloat;            // 指向浮點數的指針
pInt = pFloat;            // 直接賦值會產生編譯錯誤
pInt = (int*)pFloat;      // 強制類型轉換後進行賦值

 但是,當使用void類型指針時,就沒有類型轉換的麻煩。void類型指針顯得八面玲珑,任何其他類型的指針都可以直接賦值給void類型指針,例如:

void* pVoid;              // void類型指針
pVoid = pInt;             // 任何其他類型的指針都可以直接賦值給void類型指針
pVoid = pFloat;

雖然任何類型的指針都可以直接賦值給void類型指針,但這並不意味著void類型指針也可以直接賦值給其他類型的指針。要完成這個賦值,必須經過強制類型轉換,讓“無類型”變成“有類型”。例如:

pInt = (int*)pVoid; // 通過強制類型轉換,將void類型指針轉換成int類型指針
pFloat = (float*)pVoid; // 通過強制類型轉換,將void類型指針轉換成float類型指針

雖然通過強制類型轉換,void類型指針可以在其他類型指針之間自由轉換,但是,這種轉換應當遵循一定的規則,void類型指針所轉換成的其他類型,必須與它所指向的數據的真實類型相符。比如把int類型指針賦值給void類型指針,那麼這個void類型指針指向的就是int類型數據,這時如果再把這個void類型指針強制轉換成double類型指針並通過它訪問它所指向的數據,那麼很可能得到錯誤的結果。因為void類型指針對它所指向的內存數據類型並沒有要求,所以它可以用來代表任何類型的指針,如果函數可以接受任何類型的指針,那麼應該將其參數聲明為void類型指針。例如內存復制函數:

void* memcpy(void* dest, const void* src, size_t len);

在這裡,任何類型的指針都可以作為參數傳入memcpy()函數中,這也真實地體現了內存操作函數的意義,因為它操作的對象僅僅是一片內存,而不論這片內存上的數據是什麼數據類型。如果memcpy()函數的參數類型不是void類型指針,而是char類型指針或者其他類型指針,那麼在使用其他類型的指針作為參數調用memcpy()函數時,就需要進行指針類型的轉換以適應它對參數類型的要求,糾纏於具體的數據類型,這樣的memcpy()函數明顯不是一個“純粹的、脫離低級趣味的”內存復制函數。

最佳實踐:11:06:42指針類型的轉換

雖然指針類型的轉換可能會帶來一些不可預料的麻煩,但在某些特殊情況下,例如,需要將某個指針轉換成函數參數所要求的指針類型,以達到調用這個函數的目的時,指針類型的轉換就成為一種必需。

在C++中,可以使用C風格的強制類型轉換進行指針類型的轉換。其形式非常簡單,只需要在指針前的小括號內指明新的指針類型,就可以將指針轉換成新的類型。例如:

int* pInt;                    // int*類型指針
float* pFloat = (float*)pInt; // 強制類型轉換成float*類型指針

在這裡,我們通過在int類型指針pInt之前加上“(float*)”而將其強制轉換成了一個float類型指針。雖然這種強制類型轉換的方式比較直接,但是卻顯得非常粗魯。因為它允許我們在任何類型之間進行轉換,而不管這種轉換是否合理。另外,這種方式在程序語句中很難識別,代碼閱讀者可能會忽略類型轉換的語句。

為了克服C風格類型轉換的這些缺點,C++引進了新的類型轉換操作符static_cast。在C風格類型轉換中,我們使用如下的方式進行類型轉換:

(類型說明符)表達式

現在,使用static_cast應該寫成這樣:  

static_cast<類型說明符>(表達式)  

其中,表達式是已有的舊數據類型的數據,而類型說明符就是要轉換成的新數據類型。在使用上,static_cast的用法與C風格的類型轉換的用法相似。例如,兩個int類型的變量相除時,為了讓結果是比較精確的小數形式,我們需要用類型轉換將其中一個變量轉換為double類型。如果用C風格的類型轉換,可以這樣寫:

int nVal1 = 2;
int nVal2 = 3;
double fRes = ((double)nVal1)/nVal2;   

 如果用static_cast進行類型轉換,則應該這樣寫:  

double fRes = static_cast<double>(nVal1)/nVal2; 

使用C++風格的類型轉換,不論是對代碼閱讀者還是對程序都很容易識別。我們應該在代碼中盡量避免進行類型轉換,但如果類型轉換無可避免,那麼使用C++風格的類型轉換在一定程度上既可增加代碼的可讀性,也是對類型轉換損失的一種補償。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved