【原創】淺談指標(四)

計算機知識雜談發表於2021-10-05

前幾篇文章的連結:
淺談指標(一)https://www.cnblogs.com/jisuanjizhishizatan/p/15365167.html
淺談指標(二)https://www.cnblogs.com/jisuanjizhishizatan/p/15365823.html
淺談指標(三)https://www.cnblogs.com/jisuanjizhishizatan/p/15367297.html

前言

“淺談指標”系列的大概是最後一篇了吧。如果以後我再發現一些其他用法,也會開幾篇新文章,不過短時間內應該就不會更新了吧。

從何說起呢?上次我們把free和delete講完,delete中如果是delete一個陣列,那麼需要這樣寫:
delete []p;

這裡使用了空的方括號。那麼,C++裡,還有什麼地方使用空方括號呢?我們一起探究一下。

空方括號[]的使用

初始化陣列

假設我們要開發一個日曆軟體,其中必定要儲存每個月的天數。這些數值最好以常量陣列的形式儲存,我們可以這樣寫:
const int Days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

確實,我們可以往那個方括號中寫上13,但是我們也可以不寫,這時候,編譯器會自動確定元素個數。

函式引數

我們這次編寫一個函式,形式為print(array,size),用於輸出array陣列的內容。

void print(int array[],int size){
    for(int i=0;i<size;i++){
        cout<<array[i];
    }
}

在函式的引數之中,我們使用空的方括號來說明這是一個陣列。如果你在方括號中手動定義元素個數,例如void print(int array[10],int size),元素個數10也會被編譯器無視。
這和本文的主題——指標有何關係呢?我們來看看。
實際上,C++不支援把陣列當作引數傳遞。看似我們print函式傳遞的是陣列,實際上,傳遞的是“指向陣列首元素的指標”。也就是說,我們的引數int array[]和引數int *array是等價的。當往引數傳遞陣列時,陣列會被退化為指標。

有人會想,我們可以直接使用sizeof輸出,就不需要呼叫方指定size引數了。很可惜,這種寫法是錯誤的。

#include<iostream>
using namespace std;
void print(int array[]){
    for(int i=0;i<sizeof(array);i++){
        cout<<array[i];
    }
}
int main(){
    int a[5]={1,2,3,4,5};
    print(a);
}

輸出結果一定不是12345。因為,array在函式中,已經不再是陣列了,而是指標,因此array中儲存的是地址。在32位系統上,sizeof(array)是4。在64位系統上,sizeof(array)是8。不管怎樣,輸出的一定不是我們期望的結果。
因此,當我們開發此類函式的時候,一定要注意sizeof的問題。當然,我們可以把陣列作為結構體的成員進行傳遞,但是這樣做速度將會減慢(因為要複製整個陣列元素而不是一個地址)。

類和指標

建構函式和解構函式

在閱讀文章之前,想必大家都對C++的類,建構函式和解構函式有所瞭解。
關於建構函式和解構函式,可以看我之前的一篇文章:https://www.cnblogs.com/jisuanjizhishizatan/p/15313713.html
建構函式和解構函式的定義:

建構函式就是在一個類被建立的時候自動執行的函式。
解構函式就是在一個類被銷燬的時候自動執行的函式。

new和delete

當執行new的時候,分配一個類的記憶體,會自動執行建構函式。
當執行delete的時候,釋放類的記憶體,就會自動執行解構函式。
我們看如下程式碼:

#include<bits/stdc++.h>
using namespace std;
class A{
public:
  int a;
  A(){
    cout<<"created"<<endl;
  }
};
A *test;
int main(){
  cout<<"main"<<endl;
  test=new A;
}

一般來說,在類宣告的時候就呼叫建構函式了,test是全域性變數,因此一定在main之前執行,輸出created。
但是,由於test是指標,直到new的時候,test才被分配記憶體,因此在main之後輸出。
解構函式的原理類似。

malloc

如果把上面程式碼的new換為malloc,程式會輸出什麼?答案是隻輸出main。
有人可能會很奇怪,不就是把new換成malloc嗎?其實,只有new可以自動呼叫建構函式,malloc不會呼叫建構函式,因此,C++中一般更常用new而不是malloc。

強制轉型問題

上文提到了malloc。那麼我就順便說點關於malloc強制轉型的問題吧。在C語言中,由於沒有new,只能用malloc分配記憶體。例如下面的語句:
p=malloc(100*sizeof(int));

malloc的返回值是void*。在C語言裡,任何數都可以賦值給void*,void*也可以賦值給任何指標型別。因此,上面的程式碼理所應當這樣寫。如果使用強制轉換,就顯得沒有必要:
p=(int*)malloc(100*sizeof(int));

並且《征服C指標》這樣寫:

C語言預設把沒有定義的函式的返回值解釋為int。假設忘記寫了#include<stdlib.h>,又對malloc的返回值作了強制轉型,C編譯器可能不會發出警告。那些運氣好,現在還能跑起來的程式,如果遷移到了指標的大小完全不同的機器上去,應該就跑不起來了。

C語言中,一般不對返回值作強制轉型。然而C++就不一樣了。我們嘗試執行不帶強制轉型的malloc,結果是:
error: invalid conversion from 'void*' to 'int*' [-fpermissive]

C++不允許把void*的指標賦值給其他型別。因此,如果要使用malloc,必須使用強制轉型。相比較而言,還是new來的方便。
順便提一句,NULL在stdio.h有些時候定義為(void*)0(當然更常見的是直接定義為0,沒有void*),因此這種情況下就不能把NULL賦值給其他指標型別。對此C++規定把NULL直接定義為常量0,並專門擴充了nullptr。

nullptr

nullptr和C語言的NULL完全等價。

NULL的問題

假設有兩個過載函式

void f(int i);
void f(int* i);

f(NULL);

執行最後一行的時候,由於0可以解釋為指標也可以解釋為整數,兩個過載都可以匹配,導致出錯。

nullptr

nullptr完全可以代替NULL賦值給指標,表示空地址。但是,不能把nullptr賦值給普通整數,
int n=nullptr;是非法的。
如果上面過載例子中把nullptr傳入f的引數,結果會呼叫指標版本的f,這樣不容易造成二義性。

完。

相關文章