天天看點

C 語言程式設計 — 進階資料類型 — 指針

目錄

文章目錄

  • 前文清單
  • 指針
  • 聲明一個指針變量
  • 使用指針
  • 空指針
  • 懸空指針
  • 野指針
  • 指針的算術運算
  • 指向指針的指針
  • 将指針作為實際參數傳入函數
  • 從函數傳回指針
  • 一個古老的笑話

前文清單

《程式編譯流程與 GCC 編譯器》

《C 語言程式設計 — 基本文法》

《C 語言程式設計 — 基本資料類型》

《C 語言程式設計 — 變量與常量》

《C 語言程式設計 — 運算符》

《C 語言程式設計 — 邏輯控制語句》

《C 語言程式設計 — 函數》

指針

C 語言是一門 值語義 程式設計語言,差別于 Python 的 引用語義,參數全部是通過值傳遞的。也就是說,傳遞給函數的實際是實參的拷貝。對于 int、long、char 此類基本資料類型以及使用者自定義的結構體資料類型而言是成立的。這種方式适用于絕大多數情況,但也會偶爾出現問題:

  1. 如果我們有一個巨大結構體需要作為參數傳遞,則每次調用函數,就會對實參進行一次拷貝,這無疑是對性能和記憶體的浪費。
  2. 結構體的大小終究是有限且固定的,如果我們想向函數傳遞一組資料,而且資料的大小總是不固定的,例如:數組(包括字元串),結構體就明顯的無能為力了。

為了解決這個問題,C 語言的開發者們想出了一個聰明的辦法。他們把記憶體想象成一個巨大的位元組(Byte)數組,每個位元組都可以擁有一個全局的索引值(資料的首位元組的索引作為整個資料的索引)。這有點像門牌号:第一個位元組索引為 0,第二個位元組索引為 1,等等。

在這種情況下,計算機中的所有資料,包括變量、結構體都有相應的索引值與之對應。是以,除了将資料本身拷貝到函數參數,我們還可以隻拷貝資料的索引值。在函數内部則可以根據索引值找到需要的資料本身。我們将這個索引值稱為位址,存儲位址的變量稱為指針。使用指針,函數可以修改指定位置的記憶體而無需進行拷貝。

因為計算機記憶體的大小是固定的,表示一個位址所需要的位元組數也是固定的。但是位址指向的記憶體的位元組數是可以變化的。這就意味着,我們可以建立一個大小可變的資料結構,并将其指針傳入函數,對其進行讀取及修改。

是以,指針的本質隻是一個數字而已。是記憶體中的一塊資料的開始位元組的索引值。指針的類型用來提示程式員和編譯器指針指向的是一塊什麼樣的資料,占多少個位元組等。

要清晰區分上述繞密碼一般的關系,就要弄清楚指針的本質:

  • 指針:一個變量的位址
  • 指針變量:一個存放其他變量位址的變量

引入了指針之後,C 語言就有了兩種通路變量資料值的方式:

  1. 通過變量名來直接通路
  2. 通過記憶體位址塊的指針來間接通路

指針運算相關的運算符有以下兩種:

  • 取位址運算符

    &

    :擷取變量所占用的存儲空間的位址,為單目運算符(隻有一個操作數)。
  • 取值運算符

    *

    :也稱解引用,擷取指針變量所指向的存儲空間内的資料值。取值運算符的操作數隻能是一個指針變量。

注意:要擷取結構體指針的某個字段的值,需要使用

->

操作符。

NOTE:取值運算和取位址運算互為逆運算。

int a = 3;
int b;
int * p = NULL;

p = &a;
b = *p;
           

前門的文章中提到過,變量 = 變量名 + 變量值,而且 C 語言是值語義的,有别于 Python 的引用語義。是以變量名就是變量在記憶體中的入口位址,變量值就是變量在記憶體空間中實際的數值。在程式中可以使用取位址運算符 & 來擷取變量的入口位址。如下:

#include <stdio.h>

int main(){
    int var1;
    char var2[10] = {10, 9, 8, 7};

    printf("var1: %p\n", &var1);
    printf("var2-0: %p\n", &var2[0]);
    printf("var2-1: %p\n", &var2[1]);
    printf("var2-2: %p\n", &var2[2]);

    return 0;
}
           

運作:

$ ./main
var1: 0x7ffc857d59bc
var2-0: 0x7ffc857d59b0
var2-1: 0x7ffc857d59b1
var2-2: 0x7ffc857d59b2
           

可見,不同變量之間的記憶體空間很可能不是連續的,但同一數值内的順序元素的空間是連續的。

指針的本質也是一個變量,其變量值是另一個變量的入口位址,即一個變量存儲了另一個變量的記憶體位址,是為指針。數組名本質上也是一個指針,并且是常量指針,記錄了數組的入口位址,且不能夠被修改。

指針是 C 語言的核心和靈魂,也是洪水猛獸般存在。雖然指針的概念非常簡單,但是用起來卻變幻多端,神秘莫測,這使得指針看上去比實際要可怕得多。指針類型是基本資料類型的變體,隻需基本資料類型的後面添加

*

字尾即可:

  • int i

    :整型變量
  • int *p

    :整型指針變量
  • int a[n]

    :整型數組變量,具有 n 個整型數值元素
  • int *p[n]

    :整型指針數組變量,具有 n 個指向整型數值的指針元素
  • int (*p)[n]

    :數組指針,指向整型數組的指針變量 p
  • int func()

    :傳回整型數值的函數
  • int *func()

    :傳回整型指針的指針函數
  • int (*p)()

    :函數指針,指向函數的指針
  • int **p

    :指向整型指針的指針變量
type *var-name;
           
  • type 是指針的基類型,是一個有效的 C 資料類型
  • var-name 是指針變量的名稱
  • *

    用來聲明指針類型變量
int    *ip;    /* 一個整型的指針 */
double *dp;    /* 一個 double 型的指針 */
float  *fp;    /* 一個浮點型的指針 */
char   *ch;     /* 一個字元型的指針 */
           

需要注意的是,不管指針的基類型是什麼,指針變量的數值的類型都是一個代表記憶體位址的十六進制數。指針的基類表示了指針所指向的變量或常量的資料類型。

使用指針

使用指針時會頻繁進行以下幾個操作:

  1. 定義一個指針變量
  2. 把變量的記憶體位址指派給指針
  3. 通路指針變量存儲的數值(記憶體位址)
#include <stdio.h>
 
int main ()
{
   int  var = 20;   /* 實際變量的聲明 */
   int  *ip;        /* 指針變量的聲明 */
 
   ip = &var;  /* 在指針變量中存儲 var 的位址 */
 
   printf("Address of var variable: %p\n", &var  );
 
   /* 在指針變量中存儲的位址 */
   printf("Address stored in ip variable: %p\n", ip );
 
   /* 使用指針通路值 */
   printf("Value of *ip variable: %d\n", *ip );
 
   return 0;
}
           
Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20
           

空指針

在聲明指令變量的時候,如果沒有确切的記憶體位址可以指派,那麼為指針變量賦一個 NULL 值是一個良好的程式設計習慣,稱為空指針。NULL 指針是一個定義在标準庫中的值為零的常量。

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
   printf("ptr 的位址是 %p\n", ptr);
   return 0;
}
           
ptr 的位址是 0x0
           

在大多數的作業系統上,不允許程式通路位址為 0x0 的記憶體,因為該記憶體是作業系統保留的。但按照慣例,如果指針變量的數值為 NULL 時,則假定它不指向任何東西。

判斷一個空指針的方式:

if(ptr)     /* 如果 p 非空,則完成 */
if(!ptr)    /* 如果 p 為空,則完成 */
           

懸空指針

如果指針指向的内容被被釋放了,但是指針變量依舊儲存着這塊記憶體的位址,該指針就是 “懸空指針”:

void *p = malloc(size);
assert(p);

free(p);  // 現在 p 是懸空指針
           

如果我們再次對懸空指針進行釋放,很可能會因為記憶體位址沖突,導緻不可預知的錯誤,而且這種錯誤一旦發生,很難定位。

是以我們應該養成良好的程式設計習慣,杜絕懸空指針的出現:

void *p = malloc(size);
assert(p);

free(p); 
p = NULL;	// 避免懸空指針
           

這麼做的好處是:一旦再次使用被釋放的指針 p,就會立刻引發 “段錯誤”,我們馬上就會引起注意并對其進行改正了。

野指針

懸空指針是指向被釋放掉記憶體的指針,而野指針則是不确定其具體指向的指針,常見于未初始化的指針:

void *p;  // 此時 p 是野指針
           

因為野指針可能指向任意記憶體段,是以它可能會損壞正常的資料,也有可能引發其他未知錯誤。是以,野指針的危害性甚至比懸空指針還要嚴重。

我們在定義指針時,一般都要杜絕野指針的出現,即便在沒有初始化數組的情況下也要使用 NULL 為指針變量進行初始化:

void *p = NULL;
void *data = malloc(size);
           

指針的算術運算

C 指針的本質是一個十六進制數值,是以可以對指針執行算術運算,可以對指針進行四種算術運算:

++

--

+

-

  • 指針的每一次遞增,它會指向下一個元素的存儲單元。
  • 指針的每一次遞減,它會指向前一個元素的存儲單元。
  • 指針在遞增和遞減時的步進(跳躍的位元組數)取決于指針所指向的變量的資料類型,比如 int 就是 4 個位元組。

我們喜歡在程式中使用指針代替數組,因為變量指針可以遞增,而數組不能遞增,數組可以看成一個指針常量。下面的程式遞增變量指針,以便順序通路數組中的每一個元素:

#include <stdio.h>


const int MAX = 3;


int main(){
    int var[] = {10, 100, 200};
    int i;
    int *ptr;

    /* 數組名就是一個指針,直接複制給指針類型變量 */
    ptr = var;
    for(i = 0; i < MAX; i++){
        printf("Address: var[%d] = %p\n", i, ptr);
        printf("Value: var[%d] = %d\n", i, *ptr);
        /* 移動到下一個位置 */
        ptr++;
    }
    return 0;
}
           
./main
Address: var[0] = 0x7ffe48f272d0
Value: var[0] = 10
Address: var[1] = 0x7ffe48f272d4
Value: var[1] = 100
Address: var[2] = 0x7ffe48f272d8
Value: var[2] = 200
           

可見,每遞增一次,移動了 4 Byte。

同樣地,對指針進行遞減運算,即把值減去其資料類型的位元組數,如下所示:

#include <stdio.h>

const int MAX = 3;

int main(){
    int var[] = {10, 100, 200};
    int i;
    int *ptr;

    /* 獲得數組最後一個元素的指針,再複制給指令類型變量 */
    ptr = &var[MAX - 1];
    for(i = MAX; i > 0; i--){
        printf("Address: var[%d] = %p\n", i - 1, ptr);
        printf("Value: var[%d] = %d\n", i - 1, *ptr);

        /* 移動到下一個位置 */
        ptr--;
    }
    return 0;
}
           
./main
Address: var[2] = 0x7ffdbab78f88
Value: var[2] = 200
Address: var[1] = 0x7ffdbab78f84
Value: var[1] = 100
Address: var[0] = 0x7ffdbab78f80
Value: var[0] = 10
           

指針可以時要關系運算符進行比較,如

==

<

>

。如果 p1 和 p2 指向兩個相關的變量,比如同一個數組中的不同元素,則可對 p1 和 p2 進行大小比較。下面的程式修改了上面的執行個體,隻要變量指針所指向的位址小于或等于數組的最後一個元素的位址

&var[MAX - 1]

,則把變量指針進行遞增:

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指針中第一個元素的位址 */
   ptr = var;
   i = 0;
   while ( ptr <= &var[MAX - 1] )
   {
 
      printf("Address of var[%d] = %x\n", i, ptr );
      printf("Value of var[%d] = %d\n", i, *ptr );
 
      /* 指向上一個位置 */
      ptr++;
      i++;
   }
   return 0;
}
           

指向指針的指針

指向指針的指針是一種多級間接尋址的實作,或者說是一個指針鍊。通常,一個指針包含一個變量的位址。當我們定義一個指向指針的指針時,第一個指針包含了第二個指針的位址,第二個指針指向包含實際數值的記憶體位置。

C 語言程式設計 — 進階資料類型 — 指針

一個指向指針的指針變量必須如下聲明,在變量名前放置兩個

*

号。例如,下面聲明了一個指向 int 類型指針的指針:

int **var;
           

當一個目标值被一個指針間接指向到另一個指針時,通路這個值需要使用兩個星号運算符,如下面執行個體所示:

#include <stdio.h>
 
int main ()
{
   int  var;
   int  *ptr;
   int  **pptr;

   var = 3000;

   /* 擷取整型變量 var 的位址 */
   ptr = &var;

   /* 擷取指向整型變量的指針變量 ptr 的位址 */
   pptr = &ptr;

   printf("Value of var = %d\n", var );
   printf("Value available at *ptr = %d\n", *ptr );
   printf("Value available at **pptr = %d\n", **pptr);

   return 0;
}
           

将指針作為實際參數傳入函數

C 語言允許您傳遞指針給函數,隻需要簡單地聲明函數參數為指針類型即可。

#include <stdio.h>
#include <time.h>
 
void getSeconds(unsigned long *par);

int main ()
{
   unsigned long sec;

   getSeconds(&sec);
   
   /* 輸出實際值 */
   printf("Number of seconds: %ld\n", sec);
   return 0;
}

void getSeconds(unsigned long *par)
{
   /* 擷取目前的秒數 */
   *par = time(NULL);
   return;
}
           
int * myFunction(){}
           
#include <stdio.h>
#include <time.h>
#include <stdlib.h> 
 
/* 要生成和傳回随機數的函數 */
int * getRandom( )
{
   static int r[10];
   int i;
 
   /* 設定種子 */
   srand((unsigned)time(NULL));
   for ( i = 0; i < 10; ++i)
   {
      r[i] = rand();
      printf("%d\n", r[i] );
   }
 
   return r;
}


int main ()
{
   /* 一個指向整數的指針 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf("*(p + [%d]) : %d\n", i, *(p + i) );
   }
   return 0;
}
           

繼續閱讀