【原創】淺談指針(二)

上期鏈接

//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程式過程中,經常使用這種「違背標準的技巧」。

完。下期再見。

Tags: