C++ 指標常見用法小結

GitChat的部落格發表於2018-04-12

本文主要面向 C++ 初學者。

指標在 C\C++ 語言中是很重要的內容,並且和指標有關的內容一向令初學者頭大。在本教程中,我總結了一些關於指標和陣列的用法(尤其是指標和二維陣列)。初學者經常遇到的很多關於指標和陣列的問題應該可以在本文找到答案。

本場 Chat 只有文章,沒有交流。

  本文參考資料 C++ Primer, 5e; Coursera北大資料結構與演算法課程。

1. 概論

  指標在C\C++語言中是很重要的內容,並且和指標有關的內容一向令人頭大。針對初學者,我總結了一些關於指標和陣列的用法(尤其是指標和二維陣列)。初學者大部分關於指標和陣列的問題應該可以再本文找到答案,高階用法我也沒有接觸到,就這樣吧。

2.指標基礎

  指標是指向另外一種型別的複合型別。

  指標本身就是一個物件,允許對指標進行賦值和拷貝;指標無需在定義時賦初值。

指標定義

  "&"是取地址操作符。

int num=1;int *p=# //(&是取地址操作符)

利用指標訪問物件

  使用解引用操作符“*”。

cout<<*p<<endl;

  輸出結果為1。

指標的狀態

  • 指向一個物件
  • 指向緊鄰物件所佔空間的下一個位置
  • 空指標 int *p=nullptr;
  • 無效指標

指標作為條件判斷引數

  例如:

if(p){}

  只要指標p不是0,那麼條件就為真。

  另外值得注意的是,對於兩個型別相同的指標,可以用“==”或者“!=”來比較。若兩個指標存放的地址相同,則它們相等,否則不等。

3. 指標進階

指向指標的指標

  由於指標是物件,所以指標也有自己的地址。因此,C++語言允許把一個指標指向另一個指標。  例子:

int i=9;int *p1=&i;int **p2=&p1;cout<<i<<endl<<*p1<<endl<<**p2<<endl;

  結果是列印3個9。

指標與const限定符

  這裡有2個初學者容易混淆的概念,即指向常量的指標常量指標。根據其英文名字可能比較容易記住:

  指向常量的指標(pointer to const)是說這個指標是一個普通的指標,它指向了一個常量,如果你願意,它也可以指向其他物件,並且可以令一個指向常量的指標指向另一個非常量;

const double pi=3.1415;double *p1=&pi;//error for p1 is a general pointerconst double *p2=&pi;//correct; and p2 can poinnt to other objects*p2=6.28;//error for pi is a const variable and p2 is const

  常量指標(const pointer)是說這個指標本身就是一個常量物件,所以它不能指向其他物件,但是不意味著它不能改變所指向物件的值。

int num=9;int *const p1=&num;//correct, but remember that p1 cannot point to other objects*p1=18;//correct. You can use the const pointer to change the value of a unconst variableconst double e=2.71;const double *const p2=&e;//p2 is a const pointer points to a const object

4. 一維陣列的定義與初始化

定義

int arr[10];//含有10個整型的陣列int *arr2[3];//含有3個整型指標的陣列

  一般情況下,陣列的元素被預設初始化。

顯示初始化

int arr[]={1,2,3};int arr2[4]={1,2,3,4};

  可以用字串字面值初始化字元陣列,但是需要記得字串字面值結尾有一個空字元

char arr[5]={'h','e','l','l','o'};//correctchar arr[5]="hello";//error, initilizer-string for the chars array is too long

訪問陣列元素

  使用下標訪問陣列元素,注意陣列的下標從0開始

  C++ 11標準增加了 range for語句可以遍歷陣列元素:

for(auto i : arr)//auto用來自動確定型別{    cout<<i<<endl;}

  使用range for的好處在於不用擔心陣列越界。

5. 指標和陣列

  可以用一個指標指向陣列元素:

int arr[]={1,2,3,4,5,6,7,8,9,0};int *p=&arr[0];//此時p是一個指向陣列首元素的指標

  陣列有一個特性,很多用到陣列的地方,編譯器會自動把陣列名替換為一個指向陣列首元素的指標

int arr[]={1,2,3,4,5,6,7,8,9,0};int *p=&arr[0];//此時p是一個指向陣列首元素的指標cout<<p<<endl;//result 0x69fee4cout<<arr<<endl;//result 0x69fee4

  注意:由於陣列名arr是一個常量,因此*arr++是沒有意義的,並且編譯器會報錯,因為arr++試圖修改arr的值。但是(arr+2)是有意義的,因為這並沒有試圖修改arr的值。同理,如果令p=&arr[0],那麼我們也是可以使用p++的,因為p不是常量。

標準庫函式begin & end

  儘管可以得到尾後指標,但這種做法極易出錯。C++11標準引入了兩個名為begin和end的函式:

int a[]={1,2,3,4,5,6};int *beg=begin(a);//pointer to the first elementint *last=end(a);//pointer to the position next to the last element//output the elements of the arraywhile(beg!=end){    cout<<*beg<<endl;    ++beg;}

6. 指標運算

  給一個指標加上(減去)某個整數N,結果依然是指標。新指標與原來的指標相比前進或者後退了N個位置。

  兩個指標相減的結果是它們之間的距離。

  為了更好的理解這個問題,舉如下例子:

#include <iostream>using namespace std;int main(){    int a[3][3]={{6,1,7},    {2,5,4},    {8,3,9}    };    cout<<a<<endl;//1    cout<<a+1<<endl;//2    cout<<&a+1<<endl;//3    cout<<*a<<endl;//4    cout<<*a+1<<endl;//5    return 0;}

  程式結果(你的結果可能會有所不同)  0x69fec0  0x69fecc  0x69fee4  0x69fec0  0x69fec4

  第一個列印結果為0x69fec0,給a+1後,結果為0x69fecc,變大了12。為什麼會變大12呢?要知道每個整數都是4個位元組,為什麼不是變大4呢?答案是:由於a是一個二維陣列,所以a指向的第一個元素是一個含有3個整數的陣列,因而加1後是指向下一個子陣列,所以地址的值會變大12.

  同理,&a是指向一個二維陣列,因此加1後地址值會變大0x 24=36。

  而a指向第一個子陣列的第一個元素——這是一個整數,因此加1後地址值變大了4。同時你可以發現a和a的地址是一樣的,這是因為第一個子陣列和第一個整型元素的起始地址是一樣的。

7. 多維陣列和指標

  嚴格來說,C++中是沒有多維陣列的,通常所說的多維陣列其實是陣列的陣列。

  在本文第6部分的例子中展示了二維陣列的初始化方法,更高維的陣列初始化方法是類似的。這裡再次詳細說明一下陣列名和指標的關係。以本文第6部分的例子為例:

  a是指向二維陣列第一個元素即第一個子陣列的指標,等價於&a[0];

  a[0]是指向a[0][0]的指標,等價於*a;

  a[0][0]指向第一個整型元素‘6’, 等價於**a;

  &a指向整個二維陣列;

  總之,*會將指標降一級,&會把指標升一級

下標訪問

  多維陣列同樣可以下標訪問,例如a[0][0]的值是6.

8. 指標形參

  當使用指標作為函式引數的時候,執行的是指標拷貝的操作,拷貝的是指標的值。拷貝之後兩個指標是不同的指標,但是它們所指向的物件是一樣的,因此可以通過操作指標來改變指標所指向物件的值。

void change(int *p){    *p=32;}

限制指標的功能

  很多情況下我們使用指標是為了避免拷貝物件,但是並不希望更改物件的值。這種情況下,使用const限定符限制指標的功能是一個不錯的選擇。

void test(const int *p){    ...    ...}

9. 陣列形參

  由於陣列不允許拷貝,因此我們無法以傳值的方式傳遞一個陣列;因為陣列名相當於陣列第一個元素的指標,因此可以通過傳遞指標的形式來在函式中運算元組。

  以下3個宣告是等價的:

void print(const int *);void print(const int[]);void print(const int[5]);

  由於陣列是以指標形式傳遞給函式的,因此一開始的時候函式並不知道陣列的維度。因此有時候有必要顯示傳遞一個維度引數。

  當函式不需要對陣列元素進行寫操作的時候,陣列形參最好用const限定符限制指標功能,詳見本文第8部分示例。

傳遞多維陣列

  所謂多維陣列其實是陣列的陣列。和一維陣列一樣,我們實際傳遞的是陣列的指標。下面2個宣告是等價的:

void print(int (*p)[3],int rowsize){...}void print(int (p[][3],int rowsize){...}

  這樣的例子可能沒有什麼直觀的感受,下面我們用一個詳細的例子來說明。

#include <iostream>using namespace std;void print1(int (*p)[3])//注意*p兩邊的括號不可缺少。{    cout<<p[1][1]<<endl;}void print2(int p[][3]){    cout<<p[0][0]<<endl;}int main(){    int a[2][3]={{1,2},{3,4}};    print1(a);    print2(a);    return 0;}

  注意,print1(int (p)[3])函式裡面,形參p兩邊的括號必不可少。

  p[3]表示3個指標構成的陣列  (p)[3]表示指向含有3個整數陣列的指標。

  結果  4  1

  你可能對這種方法並不是很滿意,為什麼呢?因為使用這種方式必須顯示指定第二維的維度,而有些時候這個維度是無法獲得的。比如有以下題目:

摘抄自:北大poj 描述   在一個m×n的山地上,已知每個地塊的平均高程,請求出所有山頂所在的地塊(所謂山頂,就是其地塊平均高程不比其上下左右相鄰的四個地塊每個地塊的平均高程小的地方)。

輸入   第一行是兩個整數,表示山地的長m(5≤m≤20)和寬n(5≤n≤20)。   其後m行為一個m×n的整數矩陣,表示每個地塊的平均高程。每行的整數間用一個空格分隔。

輸出   輸出所有上頂所在地塊的位置。每行一個。按先m值從小到大,再n值從小到大的順序輸出。

樣例輸入10 50 76 81 34 661 13 58 4 405 24 17 6 6513 13 76 3 208 36 12 60 3742 53 87 10 6542 25 47 41 3371 69 94 24 1292 11 71 3 8291 90 20 95 44

樣例輸出0 20 42 12 43 03 24 35 25 47 28 08 49 3

  如果題目要求你必須寫一個函式來處理,是不是感覺之前講解的引數傳遞方法就不實用了呢?因為你也不知道一開始第二個維度是多少啊!

  其實萬變不離其宗,我們只要想辦法傳入一個地址,就可以通過這個地址訪問到這個陣列的所有元素。這裡最重要的就是要搞清楚本文第6部分和第7部分講解的關於指標運算的問題,這對於初學者可能會感覺有點亂,但是隻要慢慢去想,你就會發現這一切都是那麼的自然。

  為了解決這個問題,首先看看如何訪問整個二維陣列。

  原則上只要傳入了第一個元素的指標,我們就可以通過對這個指標進行運算從而遍歷整個陣列。

void print (int *a,int m,int n){    for(int i=0;i!=m;++i)    {        for(int j=0;j!=n;++j)            cout<<*(a+i*n+j);        cout<<endl;    }}

  假設有一個55的二維陣列a。這個函式,我們可以傳入print(a,5,5)。還記得嗎?在第7部分我們說過a就是指向a[0][0]的指標。這樣就可以列印整個陣列了。我記得我剛學習這裡的時候總是糾結於,我是不是在定義函式的時候形參是不是應該是print(intp, int m, int n)呢?其實是沒有必要的,因為二維陣列名並不是指向指標的指標*,你需要的只是一個入口而已。

  下面給出傻瓜式程式:

#include <iostream>using namespace std;static int m,n;void hill(int **a){    for(int i=0;i!=m;++i)    {        for(int j=0;j!=n;++j)        {            int num=*((int*)a+i*n+j);            if(i==0)            {               if(j==0)               {                   if(num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))                   {                       cout<<i<<" "<<j<<endl;                   }               }               else if(j==n-1)               {                   if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i+1)*n+j))                   {                       cout<<i<<" "<<j<<endl;                   }               }               else               {                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))                    {                        cout<<i<<" "<<j<<endl;                    }               }            }            else if(i==m-1)            {                if(j==0)               {                    if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j+1))                    {                        cout<<i<<" "<<j<<endl;                    }               }               else if(j==n-1)               {                   if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1))                   {                       cout<<i<<" "<<j<<endl;                   }               }               else               {                    if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1))                    {                        cout<<i<<" "<<j<<endl;                    }               }            }            else            {               if(j==0)               {                   if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j) && num>=*((int*)a+i*n+j+1))                   {                       cout<<i<<" "<<j<<endl;                   }               }               else if(j==n-1)               {                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))                        cout<<i<<" "<<j<<endl;               }               else               {                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))                        cout<<i<<" "<<j<<endl;               }            }        }    }}int main(){    cin>>m>>n;    int a[m][n];    for(int i=0;i!=m;++i)    {        for(int j=0;j!=n;++j)        {            cin>>a[i][j];        }    }    //int *p=*a;    hill((int**)a);    return 0;}

10. 返回指標和陣列

返回指標

#include<iostream>using namespace std;int a[]={11,21,31,41};int *f(){    return a;}int main(){    cout<<*f()<<endl;//result is 11}

  上面的例子展示瞭如何返回一個指標。或許把函式定義寫成如下形式更好理解。

int* f(){... ...}

  這種形式展示了函式 f 的返回型別是指標而不是讓人誤以為函式名是 *f

  永遠不要試圖返回區域性物件的指標。因為區域性變數(物件)的生命週期在函式呼叫結束後會消失,此時你返回的地址的內容可能已經發生了變化。

  如果確實需要返回區域性變數的指標,你需要把這個變數宣告為static

返回陣列

  由於指標不能拷貝,因此函式不能返回陣列,但是可以返回陣列的指標或者引用。其實上面的例子就是一個返回陣列的例子,故不再敘述。

11. 結語

  本文主要講述了指標和陣列的用法。我覺得這對於初學者還是一個比較全面的教程,希望大家喜歡。


本文首發於GitChat,未經授權不得轉載,轉載需與GitChat聯絡。

閱讀全文: http://gitbook.cn/gitchat/activity/59eef7ff16fc0231837675b0

一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視

相關文章