Pointer 指標

C 語言最絢爛的星星

 

陳杰翰 JIElite

指標是 C 語言最美麗的特色

指標一點都不可怕!

而且指標非常強大!!!

摸透指標就是摸透 C 語言

摸透 C 語言... 下略

你有沒有想過要怎麼

利用 function 修改 main function 中的變數?

要怎麼將 array 傳入 function?並且進一步做使用?

讓我們一起來打敗魔王吧!(戰勝 C 指標)

但是我們的魔王才沒有這麼可愛

Pay Attention

&

隨時發問

我們的內容將著重在 Pointer 的使用

在進入正式課程之前,我們先來介紹一下先備知識

​​C 語言,是用來操作硬體設備與設計作業系統的。相較於 Java, Python 等程式語言,需要自己管理程式使用的記憶體,其方法就是使用

指標 ( pointer ) 

此外,在 C 語言中很重要的是數值的概念

任何變數存放的都只是一個數值

我們先來瞭解一下記憶體吧!

資料究竟存放在哪裡呢?

變數,陣列都是在程式中用來儲存資料(數值)的。

變數和陣列是什麼呢?在 C 語言中,變數和陣列是一個對應到記憶體的別名。他們對應到某記憶體區塊,就好像幫某個記憶體區塊取名子一樣。所以我們的資料(數值)實際上是儲存在這些記憶體區塊中。

以下取自 wikipedia

In computer programming, a variable or scalar is a sto-rage location paired with an associated symbolic name 

(an identifier), which contains some known or unknown quantity of information referred to as a value

p.s What does it mean when data is scalar?

資料究竟存放在哪裡呢?

在大部分的電腦,儲存資料所佔用的空間是以 byte 作為儲存的單位。

 

以 C 語言為例:char 是最小的儲存型別,會在記憶體中佔用 1 byte 的空間。其餘型別則會依照電腦的系統架構而有所不同。

以4GB記憶體為例

K 代表: 

M代表: 

G 代表: 

這裡的 B 指的是 Byte, 不是 Bit

4G 代表 4 *          = 

4GB 的意思就是記憶體有         個 Bytes

並且每一個 Byte 都有自己對應的位址

( 1 Byte = 8 bits, 所以可以看到 8 個 0 or 1) 

2^{10}
2102^{10}
2^{20}
2202^{20}
2^{30}
2302^{30}
2^{30}
2302^{30}
2^{32}
2322^{32}
2^{32}
2322^{32}

在C語言,我們要怎麼取得這些資料在記憶體中的位址?

(即變數的位址)

& operator

我們可以利用 & operator(referencing operator)

來對我們的變數(variable) 或是 陣列(array) 做操作,來取得變數 或是 陣列 在記憶體中的位址

 

& operator

  • 每一個程式中的資料,在記憶體中都佔有一個 byte 以上 的空間。

  • 資料的第一個 byte 在記憶體儲存的位址,就稱作是那個資料在記憶體中的位址。(int 資料有 4 bytes, 取第一個 byte 的位址作為 int 變數的位址)

  • 因此,對於一個 array 來說,array 的位址就是第一個 element 所使用的第一個 byte 的位址。( draw a graph ) 

小試身手 - 1

任務:觀察 a, b 的位址, 以及 array 的位址

小試身手 - 1 可以看出:

  • 資料的第一個 byte 在記憶體儲存的位址,就稱作是那個資料在記憶體中的位址。(int 資料有 4 bytes, 取第一個 byte 的位址作為 int 變數的位址)

  • 因此,對於一個 array 來說,array 的位址就是第一個 element 所使用的第一個 byte 的位址。( draw a graph ) 

想法:

&array 是整個array的位址,取整個 array 第一個 byte

&array[0] 是 array 中第一個元素(element) 的位址,取第一個元素的位址(會是第一個元素的第一個 byte 的位址)

小試身手 - 2

任務:請利用迴圈印出 array 每一個元素的位址並觀察他們之間有什麼關係?

小試身手 - 2 說明

其實對於 array 來說,可以由位址看出來,只是在記憶體中的連續空間

 

這些例子要看數值

每次都要打 printf 印出來好麻煩喔~

一個高效率的程式設計師最重要的特質就是

使用gdb觀看相關資訊

小試身手跟指標有什麼關係?

指標讓你透過記憶體位址來存取資料

今天我們要從記憶體中存取資料要先知道兩件事情:

1. 從哪裡開始存取  ->  位址

2. 一次存取的大小  ->  指向多大的區塊,指向什麼東西

Q&A 喝口水

指標簡介

  • 指標是一種特殊的型別(就像 int, double, char 一樣)

  • 指標變數的內容是:記憶體位址

  • 如何宣告一個指標呢?

  • 指標如何初始化 (initalization) 與 賦值 (assignment) 

  • 如何使用?

指標是一種特殊型別

  • 指標變數存的內容是記憶體位址,藉由指標可以存取記憶體中的資料相當於存取其他變數的內容。

 

  • 指標變數的內容是記憶體位址,如果我們有方法能夠藉由記憶體位址存取到記憶體的某一個區域就能修改到該區域存放的內容。使用 dereferencing operator:  *  來存取記憶體中的內容

既然指標是一種型別,要使用指標型別的變數之前,也要宣告和初始化

指標的宣告與初始化

  • 指標的宣告必須要有

    • 指向的資料型別 ( referenced type )

      • 用來標記解讀記憶體資料的方式

      • 也就是存取的記憶體空間大小

    • * 標記是指標型別

    • 指標型別+變數名稱

    • eg: int *ptr;   char *str;   double *dptr;

    • 對於沒有用到的指標請初始化為 NULL

  • 指標的初始化

    • 將  變數的位址  初始化給  指標變數

    • int number = 10;

    • int *ptr = &number; 

​& number 實際上是類似 0x7ff..... 這樣的數值,利用 int *ptr = &number 是將 0x7ff..... 這樣的數值,assign 給 ptr 這個指標變數。使指標變數的內容存放 0x7ff.....

指標的宣告與初始化

What's NULL in C ? 

NULL 表達的意思是:這個 pointer 沒有指向任何的東西。大多被定義的數值是 0 

 

參考資料:

存取 NULL pointer

這一段是補充內容,希望大家看完這份投影片後再回來觀看。

關鍵字:dereferencing null pointer

  • https://www.quora.com/What-actually-happens-when-dereferencing-a-NULL-pointer

  • http://stackoverflow.com/questions/4007268/what-exactly-is-meant-by-de-referencing-a-null-pointer

 

 

 

指標的宣告與初始化

初始化:

int number =  10;

int *ptr = &number; (假設是 0x7ffabc12)

 

上面的 int * 整個是一個型別,ptr 才是變數。int *的意思是:現在 ptr 這個變數存放的內容是記憶體位址(數值),我們將從這個位址開始以 int 的角度來看待記憶體中的資料(從記憶體 0x7ffabc12 的位置開始有一個 4 bytes 的 int 資料)。

所以 int *ptr = &number 是將 number 的位址初始化給 ptr 這個指標變數。

指標的宣告與初始化

當然,你也可以這樣做:

 

int *ptr = NULL;         // 先宣告了指標

int number = 10;       // 才宣告指向的變數

ptr = &number;         // 將 number 的位址存到指標變數 ptr

指標的宣告與初始化

combine:

int num = 10;

int *ptr = #

 

int num = 10, *ptr = #

指標的宣告與初始化

假如一次要宣告兩個指標變數:

int *ptr1, ptr2;

上面這樣是不行的,ptr2 會被認為是 int 變數

要使用:

int *ptr1, *ptr2;

int n1 = 10, n2 = 20;

ptr1 = &n1, ptr2 = &n2;

指標指向一個變數是什麼意思?

What does a pointer variable point to variable mean?

結合記憶體說明

try it: 

int  main(){

  // It means pointer a points to variable b.

           int b = 10;   

           int *a = &b;

    // a's content is  1008, b's address is 1008

    // *a is b's content itself.

   //  if b is 10, *a is 10.

       printf("%d\n", *a);

}

如果指標變數 a 記錄了 變數 b 的位址,就稱作 指標 a 指向 b

ex:  char c  = 'a';   char *cptr = &c;   cptr 指向 c !!

宣告時把 *cptr 看在一起(其餘搭配優先權),表示他是指標變數,再由內往外拆開解析。

char (*cptr) ,  指標變數 指向 char型別的變數

 

看完剛剛的例子你一定很困惑,

因為不知道 * 到底是什麼?

*出現在宣告:

int *ptr = NULL:

存取數值:printf("%d\n", *ptr);

* 也可以當作乘法!

* 的角色

  • 宣告的時候,表示指標型別,前面必須接上一個型別 ,代表 C 語言會如何看待這些記憶體位址中的資料。 ( ex: 資料長度 char: 1,  double: 8 bytes )

    • eg: char *ptr = str;

    • eg: double *dptr = &double;

  • 當作取值運算子( dereferencing operator )

    • 只有搭配一個已知的指標變數才是這樣使用

    • eg: int number = 10;  int *ptr = &number;

    • *ptr = 20;  // The number will be 20 !

  • 大家熟悉的乘法運算子

回到記憶體長河

int  main(){

// 這裡的 * 是用在宣告所以標記 a 是指標型態

           int b = 10;   

           int *a = &b;   

// 下面的 *a 因為已經知道 a 是一個指標變數

/* 所以下面的  * 搭配指標變數指的是取記憶體位址是 1008 中的 content !    */ 

       printf("%d\n", *a);

}

但是課本的圖都是這樣

所以更加抽象難懂... Q_Q

 注意 &number 的型別是 (int *)

先前說過指標是一個特殊型別

  • 因為指標可以指向各種型別的資料

    • eg: int (*ptr);  ptr 指向 int 資料

    • eg: double (*dptr);  dptr 指向 double 資料

    • eg: char (*str);  str 指向 char 資料

    • eg: void (*ptr);  ptr 指向不知道類型的指標 

  • 指標也可以指向  指標類型 的資料

    • eg:  int *(*iptr);  iptr 指向一個指標(該指標指向int資料)

  • 因為指標可以指向這麼多種類型的資料,所以可作複雜的存取動作!

 Recall * and &

&: referencing operator,用來取得變數的位址

*:  dereferencing operator,從記憶體位址中存取變數的內容

 

如果是這樣呢?

int number = 10;

printf("%d\n",  *&number);

printf("%d\n",  &*number); 

 * and &

int number = 10;

printf("%d\n",  *&number);

對編譯器來說 &number 是一個 int * 型別,所以可以編譯成功,並且在對這個 int * 型別的資料做 dereferencing ( * (&number) )  is 10

 

printf("%d\n",  &*number); 

對編譯器來說 number 是 int 型別,所以 *number 無法編譯

小試身手 - 3

在這裡,要請你們使用指標來修改變數number內容

指標搭配 const 來使用

型式一:指標變數指向 const 資料

int num = 10;

const int *ptr = #    or

int const *ptr = #

代表 指標指向 const 型別的資料,所以我們不能夠用指標來更改所指向的內容

*ptr = 10;   // 這樣是錯誤的!

形式二:指標變數自身是 const

int n1 = 10;

int n2 = 20;

int * const ptr = &n1;

代表這個 指標變數是 const 的,只能 assign 一次。

ptr = &n2;   // 這樣是錯誤的!

先前看到的都是一個 pointer 指向一個變數,能夠多個 pointer 指向同一個變數嗎?

多個 pointer 指向同一個變數

(常見於使用 linked list 時)

Congratulations!

你已經通過了第一關了!

現在你學會的技能

  • 簡略的記憶體概念

  • 指標的宣告以及初始化

  • 利用取值運算子來存取指到的資料內容

人生最重要的就是這個BUT!!!!!

我們剛剛更改變數 number 的數值,可以不用指標就能夠辦到拉!指標到底有什麼用呢?

再加把勁

指標常見基礎應用

Type1: Function 可以修改 Function 外的變數

Type2: 傳遞連續的記憶體資料。(array, string)

Why? 

  • Type1: 只要我們知道資料存在記憶體的哪一個位址,利用這個位址,我們就可以去直接更改資料!

  • Type2:  在 C 語言,傳遞參數給 function 的時候,都將 copy 參數的內容一次。可是我們傳遞一整個 array 總不會也全部複製一次!

Function 傳遞變數的機制

call getValue 的時候:

傳入 v 的變數

相當於以下行為:

int getValue(){

        int value = 10;

        return value;

}

只是單純從記憶體中找到 v 的 content 再把這個 content 複製給 value 變數

Function 傳遞變數的機制

因為只是單純的將數值複製給 value 變數。所以,我們在 function 內部更改 value 變數的數值都無法影響到外面變數的內容~

╮(╯-╰)╭

 

Function 傳遞變數的機制

C語言來說,傳遞參數給 function 都只是 copy 數值給 參數變數。

只有 pass by value 這種形式

( value可以是一般數值,或是 address number )

在 C 語言,只有數值的概念,變數儲存的資料都只是一個數值

如果我們天傳入的數值是 0x7ff..... 這樣的記憶體位址,我們就可以透過指標加以存取該位址存放的內容

Function 傳遞變數的機制

所以假如我們今天傳入的變數是——

可以利用 dereference 機制索引到記憶體內容的指標變數呢?

(也就是傳入記憶體位址的數值)

利用 Function 更改外部變數內容

Before

After

利用 Function 更改外部變數內容

After

在這裡,你可以使用 gdb 查看 function 參數是什麼?

(gdb) b 10

(gdb) r

(gdb) s

changeValue (value=0x7fff5fbffc08) (數值

(gdb) print value

(gdb) print *value

小試身手 - 4

請寫出一個 function 可以將兩個變數的數值調換,並且適當修改 function 的形式。

思考

上面示範的是利用一個 int 變數的位址當作參數傳入 function,於是這個 function 可以藉由指向 int 變數的指標,取得外界 int 變數的位址並從記憶體中直接更改 int 變數的內容。

但是如果我們今天要更改 function 外的資料,它的型別原本就是一個指標型別呢?像是 int *ptr;

 

 

想翻頁偷看嗎?

思考

如果我們想要藉由 function 來修改 int *ptr 這樣一個指標變數的內容。我們就必須要取得 ptr 這個指標變數的位址(是的!他是一個變數,當然也會有位址)

因此傳入 function 是使用 function ( &ptr ); 這時候我們只要注意定義出適當的參數型別就可以了。我們會定義出一個指標變數,並且這個指標變數 指向 一個指標(該指標指向 int )。

所以就是:

declaration:    function (int *(*ptr_to_ptr));

call:                  function( &ptr );

 

 

其實概念很簡單:

如果我們要用 function 更改外面 int 型別的變數內容,我們就傳入 int 變數的位址作為參數, int 變數的位址其實就是一個指標變數,指向 int 。

 

如果,我們今天要改的是 int * 型別的變數,就要傳入 int * 型別變數的位址,而這個位址的型別其實會是 int **。所以我們要用 function 更改外部 int * 型別變數,要使用 int ** 型別的參數。

回想

  • scanf("%d", &num); 為什麼要有 & operator ? 

  • 再次問男人 $ man scanf 請問你們看到了什麼?

    • const char *:下一節說明 Type2

Congratulations!

你通過了第二關了!

會不會覺得很辛苦?

接著來看看 Part 2 吧!