指針指針

2021-02-19 大熊的編程世界

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

相關焦點

  • 「C語言指針」指針數組?數組指針?
    今天帶著大家來簡單過一下數組指針和指針數組,這兩兄弟像是兩座大山,重重地壓在很多小白的心頭上。其實沒什麼可糾結的,只要按照最樸素的語言規律。後面是什麼決定這整個東西最終是什麼。類似於肉夾饃,說得再多它終究只是饃的一種,而不可能是肉(淚目)。
  • C:數組與指針,指針與 const
    數組與指針我們都知道一個指針是代表的一個地址,指針,顧名思義,指向一塊區域。那麼數組呢?數組並不是代表一堆變量,數組其實也是一種指針,指向一個地址,一般是指向數組的首地址,也就是 a [0] 的地址。a==&a[0] a 是一個指針,指向數組 a 的首地址。下面四種函數原型都是等價的,第一個參數均為一個地址(指針)。
  • 高級指針話題-函數指針
    -《中國哲學史》前言函數指針是什麼?如何使用函數指針?函數指針到底有什麼大用?本文將一一介紹。如何理解函數指針如果有int *類型變量,它存儲的是int類型變量的地址;那麼對於函數指針來說,它存儲的就是函數的地址。函數也是有地址的,函數實際上由載入內存的一些指令組成,而指向函數的指針存儲了函數指令的起始地址。如此看來,函數指針並沒有什麼特別的。
  • 指針
    指針就是地址,地址就是指針地址就是內存單元的編號指針變量是存放地址的變量指針和指針變量是兩個不同的概念基本類型指針指針和數組指針和函數指針和結構體多級指針1.基本類型指針void* 表示未確定類型的指針。
  • C語言 | 指向指針的指針
    例82:C語言用指向指針的指針的方法對n個整數排序並輸出;要求將排序單獨寫成一個函數;n個整數在主函數中輸入,最後在主函數中輸出。 解題思路:讀者看著道題的時候,首先要觀察一下有什麼規律,然後指向指針的指針在上一道練習題中已經有了鋪墊,讀者可以聯繫上一道題去熟練使用指向指針的指針。
  • 「懸空指針」和「野指針」究竟是什麼意思?
    」和「野指針」。一、懸空指針C語言中的指針可以指向一塊內存,如果這塊內存稍後被作業系統回收(被釋放),但是指針仍然指向這塊內存,那麼,此時該指針就是「懸空指針」。所以在實際的C語言程序開發中,為了避免出現「懸空指針」引發不可預知的錯誤,在釋放內存之後,常常會將指針 p 賦值為 NULL:void *p = malloc(size);assert(p);free(p);// 避免「懸空指針」p = NULL;這麼做的好處是一旦再次使用被釋放的指針 p,就會立刻引發
  • 【滑鼠指針】如何使用自己喜歡的指針包
    我又是你們的 末~地~君~七~號~今天我們來水一期有關滑鼠指針的內容~我們先準備一下需要準備的內容:一個指針包一臺電腦沒戳,就是這麼然後,請看準你的滑鼠指針是否含有一個.inf的後綴名文件,如果你有那麼請繼續往後看
  • 指針自由行:指針上的中國 江蘇蘇州姑蘇
    「指針上的中國」是由《指針自由行》手機APP行為大家獻上的一部集各地美食與美景於一身的系列報導。如果你的身邊也有極佳的景色或者令人垂涎欲滴的美食,歡迎你下載並註冊《指針自由行》手機APP,參加正在進行中的「拍美景 領現金」活動。文會宴是一種將飲宴與吟詩作賦結合起來,以文會友的交流形式。
  • 「C語言指針」指針運算超詳細
    很多童鞋在接觸C語言使用指針的時候,總是本著一個原則:能不用就不用!所以對於指針的認識也難免有所局限,所以禁停啊我們就來普及一個「冷知識」,指針運算。回到正題,指針也是有運算的,只不過指針之只能進行有限的運算,包括賦值(沒有錯,賦值也是一種運算!)和部分算數運算和關係運算。在上一篇文章中我們講過取地址&和取值運算符*,以及談及了指針和指針變量的本質不同,所以顯然 & 和 * 是對指針最基本的一種運算。
  • 讓你不再害怕指針——C指針詳解(經典,非常詳細)
    >int **p; //首先從P 開始,先與*結合,說是P 是一個指針,然後再與*結合,說明指針所指向的元素是指針,然後再與int 結合,說明該指針所指向的元素是整型數據.由於二級指針以及更高級的指針極少用在複雜的類型中,所以後面更複雜的類型我們就不考慮多級指針了,最多只考慮一級指針.
  • C - 指針總結
    在C 中,指針變量只有有了明確的指向才有意義。指針的指針:char* a[]={"hello","the","world"};char** p=a;p ;cout <:指向某一函數的指針,可以通過調用該指針來調用函數。
  • 指向結構體的指針
    在C語言中幾乎可以創建指向任何類型的指針,包括用戶自定義的類型。創建結構體指針是極常見的。請注意,因為r是一個指針,所以像其他指針一樣佔用4個字節的內存。而malloc語句會從堆上分配45位元組的內存。*r是一個結構體,像任何其他Rec類型的結構體一樣。
  • C語言-指針
    指針變量:存放地址編號的變量被稱為指針變量。指針變量的分類:按類型分:一級指針char * p ; //字符指針變量 可以保存字符變量的地址,或字符串的地址。int * p;//整型指針變量 可以保存整型變量的地址long int *p;float *p;//浮點型的指針變量,可以保存float類型變量的地址。double *p //雙浮點型的指針多級指針:指針的指針char **p;//可以保存char * 類型變量的地址。
  • C++ 文件指針!
    簡  介在前面的C語言相關文章中介紹過文件指針的概念,即用一個指針變量指向一個文件,這個指針稱為文件指針
  • LeetCode 例題精講 | 05 雙指針*鍊表問題:快慢指針
    雙指針這個名字在很多題解中都能見到,那麼,還有什麼題目可以用雙指針方法解決呢?實際上,雙指針是一個很籠統的概念。只要在解題時用到了兩個指針(鍊表指針、數組下標皆可),都可以叫做雙指針方法。根據兩個指針運動方式的不同,雙指針方法可以分成同向指針、對向指針、快慢指針等。
  • 快速上手系列-C語言之指針篇(四)函數與指針
    就是定義一個指向函數的指針變量p,p不是固定指向哪個函數的,而是專門用來存放函數入口地址的變量。在例子中,int (*p)(int a, int b),其中p是函數指針。max函數在編譯的時候會被分配一個入口地址,這個函數入口地址即為函數的指針。我們用一個指針變量p指向函數入口地址,然後通過指針變量p調用此函數。
  • C語言基礎知識分享:指針常量和常量指針區別
    在學習C/C++的時候總是記不住指針常量(int * const p;)和常量指針(const int *p)的區別,最近再刷題的時候發現了一個評論,分分鐘就可以記住這兩者的形式和區別,下面分享一下如何記住和區分指針常量和常量指針。
  • 快速上手系列-C語言之指針篇(三)字符串與指針
    2、存儲位置:(1)數組是在內存中開闢了一段空間存放字符串;(2)而字符指針是在文字常量區開闢了一段空間存放字符串,將字符串的首地址付給指針變量str。3、賦值方式:對與數組,下面的賦值方式是錯誤的:char str[10];str="hello";而對字符指針變量,可以採用下面方法賦值:char *a;a="hello";4、可否被修改:(1)指針變量指向的字符串內容不能被修改,但指針變量的值(即存放的地址或者指向)是可以被修改的;例一:指針變量指向的字符串內容不能被修改
  • C語言指針詳解
    第一章 指針的概念 本文引用地址:http://www.eepw.com.cn/article/257964.htm指針是一個特殊的變量,它裡面存儲的數值被解釋成為內存裡的一個地址。要搞清一個指針需要搞清指針的四方面的內容:指針的類型,指針所指向的類型,指針的值或者叫指針所指向的內存區,還有指針本身所佔據的內存區。
  • C 語言程序設計---指針
    基本概念學習 C 語言之指針,必須強烈推薦一本書:《C 和指針》,好好看,把這本書吃透,C 指針就差不多了。5、指針的其他運算(主要是指針的加減運算)指針加/減整型值,其結果是一個指針,且指向空間的類型不變。