【原創】淺談指標(二)

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

上期連結

https://www.cnblogs.com/jisuanjizhishizatan/p/15365167.html

前言

最近,指標確實逐漸淡出我們的生活了。但是,指標又是必不可少的,它在日常程式設計中又有著很大的作用。曾經noip初賽的閱讀程式寫結果,還經常考指標題,以及函式的傳參機制,例如*a++這個語句。然而近兩年,這些題目也不再出現了。
指標其實是C++中一個非常值得深究的語法。到目前為止,可能說,沒有人能夠完全理解指標。我們所學的,只是指標中,極小的一部分。
好了,讓我們開始吧,繼續今天的指標學習。

函式的傳參機制

現在,讓我們輸入兩個數,將兩數反轉後輸出。可能有人會問,標準庫不是有一個swap函式嗎?那好,我們就自己寫一個swap函式。

#include<iostream>
using namespace std;
void Swap(int a,int b){
    int temp=a;a=b;b=temp;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(a,b);
  cout<<a<<" "<<b;
  return 0;
}

那好,我們來執行吧。輸入:4 5
輸出:4 5

看來這個函式有點問題啊,沒有交換變數的值。我們嘗試把他寫進主函式,像是這樣:

#include<iostream>
using namespace std;
//void Swap(int a,int b){
//    int temp=a;a=b;b=temp;
//}
int main(){
  int a,b;
  cin>>a>>b;
  //Swap(a,b);
  int temp=a;a=b;b=temp;
  cout<<a<<" "<<b;
  return 0;
}

這樣輸出是正常的,這就奇怪了,為什麼沒有交換變數的值呢?

我們來做個實驗。執行下面程式碼,看看它會輸出什麼?

#include<iostream>
using namespace std;
void Swap(int a,int b){
    int temp=a;a=b;b=temp;
    cout<<&a<<" "<<&b<<endl;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(a,b);
  //int temp=a;a=b;b=temp;
  cout<<&a<<" "<<&b<<endl;
  return 0;
}

我在wandbox線上編譯器執行了這個程式碼,輸出是:
0x7ffdaa89e65c 0x7ffdaa89e658
0x7ffdaa89e68c 0x7ffdaa89e688

在菜鳥線上編譯器的輸出:
0x7ffd6da5b4fc 0x7ffd6da5b4f8
0x7ffd6da5b52c 0x7ffd6da5b528

可以看到,在多個編譯器中,輸出的main函式中ab的地址和swap函式中ab地址是不同的。既然它們被儲存在不同的地址,swap函式中的ab交換了,但是main函式的ab沒有交換。
就像某個上司讓他的部下交換他檔案櫃的檔案,但是上司給部下的檔案是檔案櫃中的影印件,那麼那位部下無論怎麼做,都無法把檔案櫃的檔案交換。這是同樣的道理。
那我們怎麼辦呢?我們可以嘗試用指標解決這個問題。這是唯一的方法。
(更準確的說,使用引用同樣可以解決問題,引用會在下一章予以介紹)

#include<iostream>
using namespace std;
void Swap(int *a,int *b){
    int temp=*a;*a=*b;*b=temp;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(&a,&b);
  cout<<a<<" "<<b<<endl;
  return 0;
}

我們如果向swap函式傳遞a和b的地址,那麼,swap裡面的a和b,其實就是main裡面的a和b。這樣一來,就可以交換了。如果那個上司告訴了他的部下檔案櫃的地址,那麼部下就可以根據地址,找到櫃子裡的檔案,自然也就可以交換櫃子裡的檔案了。
有沒有發現,這裡swap函式中,引數是傳遞的地址,那麼引數前必須加&號。想起來了嗎?scanf也是這樣寫的!實際上,scanf也使用了指標的機制!

連結串列

struct LIST{
    int n;
    struct LIST* next;
};

連結串列中,需要知道下一個元素的地址才能進行查詢下一個元素。因此,需要把指標作為結構體成員。最後一個元素的next置放NULL,通知程式“後面已經沒有元素了”。
如果要查詢節點s的下一個元素,就是(*s).next,注意括號雖然麻煩但是不可省略。
當然,還有一種簡寫形式,即
(*s).next=s->next

如果要刪除元素,我們可以這樣執行:

LIST *x=s->next;
s->next=s->next->next;
free(x);

連用了兩次結構體運算子->。

函式指標

上一篇文章說過,程式是儲存在記憶體中的,自然也可以使用指標指向我們的程式中的函式。這種指標稱作函式指標。

#include<bits/stdc++.h>
using namespace std;
int f(int a){
    printf("a..%d",a);
}
int (*p)(int a);
int main(){
    p=f;
    (*p)(5);
}

int (*p)(int a);一句表示宣告一個叫做a的函式指標。有人會問,這是指向函式的指標,先把表示指標的*號使用括號括起來是不是很奇怪?事實上,由於表示函式的()優先順序比*高,如果不加括號,
int *p(int a);

編譯器會把它當作一個返回值是int*的函式p。就不是函式指標了。

把函式指標當作引數使用

stdlib.h中,有一個函式atexit,作用是“當程式正常退出時執行這一函式”。程式例項:

#include<bits/stdc++.h>
using namespace std;
void f(){
    cout<<"Hello, World!";
}
int atexit(void (*func)(void));
int main(){
    atexit(f);
    return 0;
}

其中,atexit的引數就是一個函式指標,將地址f賦值給了atexit的引數地址func,在結束時執行f。
順便一提,既然函式可以看成指標,那麼能否對其執行取數值操作呢?使用*運算子可以取到地址的數值。答案是不能。在表示式中,如果對函式地址前新增*號,f暫時會變成函式。但由於在表示式中,它又會變為“指向函式的指標”。也就是說,這種情況下,對函式用*運算子無意義。
因此,

#include<bits/stdc++.h>
using namespace std;
int main(){
    (********printf)("hello");
    return 0;
}

這樣的操作也能輸出hello。

從1開始的陣列

眾所周知,C++的陣列從0開始,但是使用某些指標的技巧,可以使陣列下標從1開始計數。

#include<bits/stdc++.h>
using namespace std;
int a[10];
int *p;
int main(){
    p=&a[-1];
    for(int i=1;i<=10;i++)cin>>p[i];
    for(int i=1;i<=10;i++)cout<<p[i]<<" ";
}

程式把p指向了不存在的元素a[-1],這樣,p[1]等於a[0],p[10]等於a[9],就可以讓下標從1到10計算了。
當然,這個程式違反了C標準,標準規定指標只能指向陣列內的元素和陣列最後元素的下一個元素,其他情況均屬於未定義(這與是否發生讀寫無關)。至於為什麼標準允許讓指標指向陣列最後元素的下一個元素(例如在上例中指向不存在的a[10]),大家可以自己探究,我將會在下期給出答案。
順帶一提,fortran的陣列從1開始計數,因此,為了把fortran程式移植到c程式過程中,經常使用這種“違背標準的技巧”。

完。下期再見。

相關文章