C 語言是最常使用指針的語言之一,我們在初學 C 語言時可能就會因為指針這個概念而頭疼,我在這裡將重述指針在 C 語言裡的作用及使用過程。
儘管其它高級語言中可能並沒有明顯地使用指針的痕跡,但實際上指針仍然蘊含在那些高級語言的細微之處,可以說涉及到對地址的引用操作離不開指針的概念。
真正的大佬只認可 C (圖:Liunx 之父,圖片來自網絡,侵權則刪)
指針的定義在計算機科學中,指針(英語:Pointer),是程式語言中的一類數據類型及其對象或變量,用來表示或存儲一個存儲器地址,這個地址的值直接指向(points to)存在該地址的對象的值。——維基百科
在1964年,哈羅德·勞森發明了最早的指針。他在PL/I中實現出了這個概念,其他高級程式語言也很快跟進,使用了這個想法。
如果想要理解指針的概念,我們就不能不去觀察它的使用過程。
首先我們需要回憶下我們學習 C 語言指針時的痛苦記憶,我在此提出以下幾個一堆問題:
當我們在 C 語言中使用指針時,我們究竟在使用什麼?操作指針意味著什麼?為什麼為指針變量進行賦值時,有時被賦值的變量前需要加&?加&與不加&的區別在哪裡?為什麼使用scanf()為變量進行輸入時,需要在變量前加&,而printf()則不需要呢?
借著解答這些問題的過程我們來深刻理解指針的操作。
scanf() 函數對變量輸入需要 &當我們需要對變量使用scanf進行輸入時,例:
#include<stdio.h>
int main(){
int a;
scanf("%d", &a);
printf("%d", a);
return 0;
}變量前需要添加&,這個符號意味著取地址,意味著把a這個變量的地址傳入scanf()函數內,之所以需要傳入地址,而不是一個普通變量是因為這個scanf()函數它是個函數。
你以為我說了句廢話?其實不是的。
讓我們回憶下我們剛學到函數時,我們的老師是否有向我們提過,函數的參數傳遞是值傳遞或者地址傳遞,即使沒有學過或者忘記了並不要緊,我將再次重述函數的 C 語言函數參數傳遞知識。
C語言函數參數傳遞當我們創建一個自定義的函數,然後為這個函數傳遞一個變量進去,並且期望自定義函數能夠改變這個變量的值時,我們最終會發現這個期望極其容易落空。
#include<stdio.h>
void changValue(int a){
a = 2;
}
int main(){
int a = 1;
changValue(a);
printf("%d", a);
return 0;
}對a的輸出結果實際上還是 1 ,changeValue並不能真的改變a的值。
因為這裡新建的changeValue函數對a執行的是值傳遞,意思是只將a的值傳遞給函數中新聲明的a,這兩個實際上並不相等,如果這個世界上存在著和你長相完全一樣的人,那他也不會成為你,因為你們有著完全不同的生活經歷。
但是我們可以通過傳遞地址的方式,從源頭上改變a的值。
#include <stdio.h>
void changeValue(int *a)
{
*a = 2;
}
int main()
{
int a;
a = 1;
changeValue(&a);
printf("%d", a);
return 0;
}這裡的輸出結果就是 2 了,因為我們是將a的地址,也即&a作為參數傳遞進了changeValue函數,函數中聲明了一個指針變量a,這個指針指向的就是傳進函數內a的地址。
如果我有臺時光機,穿越回過去,把一個和你嬰兒時期完全一樣的複製人嬰兒和嬰兒時期的你進行交換,他就能成為你。
我們應該要明晰一點,我們存儲在電腦上的數據是存放在電腦的硬碟或者其它存儲媒介中,不同的文件格式有不同的編碼方式,但它們最終是按照電腦的編碼方式才能存放在電腦的存儲媒介中的。如果電腦不獲取到文件存放在存儲媒介上的地址,我們能改變文件內容嗎?自然不能。
C 語言內變量的地址也是如此,我們傳進函數參數如果是普通的變量,那麼函數只會把它的值複製一份給函數體中聲明的形參,但是如果傳遞的是地址,即使是地址的副本,函數內的形參也會依據這個地址找到存放的數據,改變地址所指向的值也就是在改變存放著的值,那麼原先參數所表示的值也會改變。
遊戲中,我使用的角色和你使用的角色無論怎麼打,都不會影響到現實中的,但如果你順著網線從電腦那頭爬到了我這頭,我是一定會報警的,這裡就體現了函數的值傳遞和地址傳遞。
這一點我們需要注意函數如果想要改變一個變量,並且這個改變能夠體現到函數外,就需要變量的地址,而不是變量的值。
scanf()的作用在於為變量輸入一個值,實際上是需要改變變量的,那麼scanf()就需要變量的地址。
printf()的作用只是列印變量的值,它沒有改變變量,也就不需要傳遞變量的地址。
這裡我們順便提一嘴C語言的函數作用域。
C語言函數作用域C語言的函數作用域使得函數內的聲明的變量的生命只會存在該函數內,離開了該函數即被銷毀,這個銷毀是從存儲空間上的銷毀,所以如果期待一個有著全局作用域的指針變量去保存函數內的聲明的變量地址是不理想的。例:
#include<stdio.h>
void changeA(int *a, int b){
b = 3;
a = &b;
}
void changeA1(int *a, int *b){
int c = 3;
*b = c;
*a = *b;
}
int main()
{
int *a;
int b = 0;
a = &b;
changeA(a, b);
printf("%d, %d\n", *a, b);
changeA1(a, &b);
printf("%d, %d", *a, b);
return 0;
}它的輸出結果如下所示:
我們來分析下changeA函數的作用。
void changeA(int *a, int b)我在聲明函數changA時同時又聲明了形參int *a, int b,我在上節中提到值傳遞與地址傳遞,這裡的形參獲取到的是實參*a的地址,但是在函數內,我卻將指針a又重新指向了b的地址,這個b所依賴的是值傳遞,它只是保存了實參b的值,而非地址,實際上它是在這個函數中創建的。
當changA()這個函數執行完會發生什麼?形參b會被銷毀,b的地址也就不存在了,那麼形參指針a指向哪裡呢——一個存放空數據的地址,在C語言內空數據可以等於 0,這也就解釋了為什麼輸出結果為什麼是0。
changA1()則不同了,它裡面的形參a、b實際保存的是實參a、b的地址。地址b的解引用*b也只是保存了局部變量c的值而已,並未重新指向其它地址,同時形參a也獲取了*b的值,同時指針a仍然指向b,即使函數中未寫*a = *b,輸出結果仍然是3, 3。
指針的使用指針從它的名字的含義可以看出它需要指向某個事物,這個事物可以看成是變量的地址。如果一個指針偏偏不願指向某個變量的地址,它就會成為「野指針」,也就是一個指向不明的指針。
指針變量的寫法貌似有點奇怪,我們聲明指針變量時使用int *a之類的語句,可是如果把變量b賦值給它時,使用的是a = &b,輸出a的值時,使用的卻是printf("%d", *a),僅僅是賦值輸出就有不同講究了,初學時顯然容易感到困惑。
#include<stdio.h>
int main(){
int *a;
int b = 3;
a = &b;
printf("%d\n", b);
scanf("%d", a);
printf("%d", *a);
return 0;
}輸出結果:
指針變量賦值已經夠難了,又加了個輸入。
我們首先來分析int *a,這是一個非常標準的聲明語句,int表示類型,*是一個標識,表明b這個變量是一個指針變量,實際上我覺得寫成int * a這樣反而更明確一點,這樣就直接表示a是int型的指針或者int 指針型,不過C語言的類型並沒有指針型這一說法。
賦值語句a = &b的含義是將b的地址賦值給a,因為a是指針,指針需要的是地址。
那麼輸出時需要使用printf("%d", *a);而不是printf("%d", a),則是因為我們想要看到是a指向的地址上存放的值,而不是a指向的地址本身,儘管地址可以輸出,但是每次存放數據的位置是會變化的,輸出地址在這裡並不重要。printf("%d",*a)中*a表示對a這個地址的解引用,也就是獲取a地址上存放的數據之含義。
實際上這個時候,a地址等於的是&b,而b的值才等於*a。
#include <stdio.h>
int main()
{
int *a;
int b = 3;
a = &b;
printf("%d\n", a);
printf("%d", &b);
return 0;
}下圖是如上的輸出,表明&b == a。
特殊的指針——數組變量名
正因為a變量在賦值後實際是一個地址值,所以在scanf()語句中,它並沒有使用&。為什麼我膽敢宣稱數組變量名是一個特殊的指針呢,因為數組變量名與指針具有相似的地方——它們均指向一個地址。
當我們為一個數組進行輸入時,是不需要使用取址符的,因為數組變量名指向數組存儲的首個單元的地址。
#include<stdio.h>
int main(){
int a[3] = {1, 2, 3};
printf("%d\n", a);
printf("%d", &a[0]);
return 0;
}下圖是如上的輸出,輸出了a與&a[0],它們的值相同,說明變量名a相當於數組單元首個數據所存放位置的地址。
指針變量的輸入也是不需要取址符的,因為指針變量相當於它所指向的變量的地址,而上述的例子正說明了數組變量名也是地址,因為它們指向的地址相等。
實際上,數組變量名是在編譯時被轉換為指向數組首個單元地址的指針,如果失去這個轉換的過程,自然數組變量名也就不是指針了,但是我們可以選擇忽略這個轉換的過程,而直接說數組變量名等同於指向數組首個單元地址的指針。
指針與結構體變量結構體在聲明時會劃分一塊地址,有點像數組,但是數組變量名是首單元地址,而結構體變量名卻是結構體變量起始地址所在的數據。
#include <stdio.h>
typedef struct
{
int data;
int data2;
} Stu;
int main()
{
Stu a;
a.data = 2;
a.data2 = 3;
printf("%d", a);
return 0;
}看起來結構體變量名類似於「反數組變量名」
另外如果使用結構體指針,需要某結構體變量內的某個值,可以有兩種寫法:
#include <stdio.h>
typedef struct
{
int data;
int data2;
} Stu;
int main()
{
Stu a;
Stu *b;
a.data = 2;
a.data2 = 3;
b = &a;
printf("%d\n", a);
printf("%d\n", b->data);
printf("%d\n", (*b).data);
return 0;
}b->data與(*b).data是這兩種寫法的體現,實際上後者依然是先對指針變量b來個解引用再取值的過程,只不過需要注意運算符的優先級,將*寫在括號內,提升它的優先級。前者寫法則減少了敲鍵盤次數,實乃懶癌患者福音。
typedef 提供的可能性在聲明結構類型我們可以使用類似如下語句:
#include<stdio.h>
typedef struct {
int data;
} * Stu;
int main(){
Stu b;
return 0;
}需要注意的是這個時候聲明變量b時,變量b實際上是一個指針變量,這是typedef提供的功能,那麼我們就可以用b指向一個具有相同類型的結構體變量的地址了。
#include<stdio.h>
typedef struct {
int data;
} * Stu, Stu1;
int main()
{
Stu b;
Stu1 a;
a.data = 2;
b = &a;
printf("%d", b->data);
return 0;
}
小節指針變量再如何特殊,它也只是一個指向地址的變量,差異的發生是因為它所處的環境不同導致不同的結果,但它們作為指針的本質卻是相同的。
隨心所寫,有所誤人子弟之處煩請指正,小子當垂淚涕零以感激。
相關參考 :
C語言中文網·《結構體與指針》http://c.biancheng.net/cpp/html/94.html
C語言中文網·《C語言和內存》http://c.biancheng.net/cpp/u/c20/
CSDN· hasakei_《scanf為什麼要取地址,而不直接使用變量名》 https://blog.csdn.net/weixin_39846515/article/details/79177776