Pointer Part 2

- Pointer & Array -

陳杰翰 JIElite

上週內容

  • 數值觀念與記憶體位址
  • 指標簡介
  • 指標應用 - 修改變數內容

但是精彩的才正要開始

上一次我們學會如何宣告一個指標變數

既然指標變數是一個變數,那我們是不是也可以對他進行四則運算?

小試身手 - 5

執行後你將看到以下錯誤

error: invalid operands to binary expression ('int *' and 'int *')

        printf("p + p2: %p\n", p + p2);

                               ~ ^ ~~

warning: format specifies type 'void *' but the argument has type 'long' [-Wformat]

        printf("p - p2: %p\n", p - p2);

                        ~~     ^~~~~~

                        %ld

 

在 C 裡面,允許指標的算術運算

  • 指標搭配常數作加減
  • 指標相減
  • 指標搭配  ++, -- 可以前置也可以後置
  • 注意:運算子的優先權

                       eg: *ptr++, *++ptr;

但是通常這些運算你會搭配 Array 做使用,才能確保不發生錯誤

Pointer & Array

int a[10];

int *p = &a[0]; ( p 指向 int 變數,所以給予 int 變數的位址 )

 

圖片取自:C Programming: A Modern Approach, 2nd Edition

Pointer & Array

int a[10];

int *p = &a[0]; ( p 指向 int 變數,所以給予 int 變數的位址 )

*p = 5;

圖片取自:C Programming: A Modern Approach, 2nd Edition

Pointer 算術運算: 常數加減

int a[10];

int *p = &a[2]; 

int *q = &a[5];

圖片取自:C Programming: A Modern Approach, 2nd Edition

method 1:

        p = q + 3;

method 2: 

        p = p + 6;

我們都知道在宣告的時候,變數前面的 * 用來標記該變數是一個指標變數,像是:  int *ptr;

那為什麼要有前面的型別?用來解讀記憶體內容

int *p = &a[0];

p = p + 3; 

就會將指標移動到下三個的 int 變數位址,依照指標指向的型別作移動,而不是單純將位址的數值作加減。

p = p + 3;  到底是什麼意思?

指標變數指向 Array 中的任一元素都可以,意思是當前指標對誰做操作。

 

Pointer & Array

int *p = &a[8];   

p 是什麼? p + 1是什麼?

*p 是什麼? *p + 1 是什麼?

*(p+1) 是什麼?*(p -1) 是什麼?

圖片取自:C Programming: A Modern Approach, 2nd Edition

小試身手 - 6

如果你也很懶的話: git clone https://gist.github.com/634f18ba9e2648b48585.git  q6

 $ git clone https://gist.github.com/634f18ba9e2648b48585.git  q6

指令解說

  • $ 開頭:代表是在終端機下的指令 (Unix  系列)
  • git clone 使用 git 工具從遠端獲取程式碼 (or 庫)
  • 一串網址.git:從哪裡獲取?
  • q6 代表將獲取到的目錄更名為 q6。你可以試試看不加 q6, git clone 下來的目錄會是怎麼樣?

小試身手 - 6

如果你也很懶的話: git clone https://gist.github.com/634f18ba9e2648b48585.git  q6

為什麼指標可以這樣操作?因為 Array 在記憶體中是連續的空間

指標搭配常數作加減 

即是讓指標移動 ( 非 const pointer ) 

移動的距離依照指標指向的型別而定

eg1: double *p; 一次移動 8個 bytes 的距離

eg2: 

int a[10];

int (*ptr)[10] = &a;

上面的 ptr 指向的是 int [10] 整個陣列~~~

  ptr + 1呢 XD?

在 C 裡面,允許指標的算術運算

  • 指標搭配常數作加減 
  • 指標相減
  • 指標搭配  ++, -- 可以前置也可以後置
  • 注意:運算子的優先權

                       eg: *ptr++, *++ptr;

指標相減

int *p = &a[5];

int *q = &a[1];

What is p - q? 

圖片取自:C Programming: A Modern Approach, 2nd Edition

 小試身手 - 7

git clone https://gist.github.com/da28be5dde2048d95f72.git 

 小試身手 - 7.2

git clone https://gist.github.com/64ec7f9b9673683e8bb2.git 

神奇的事情!?

好孩子不要這樣寫,因為你不知道會發生什麼事情

 

git clone https://gist.github.com/719ab459708f8969a21c.git

此頁可略,只是想告訴大家 多做實驗

(下週上課後砍掉此頁)

指標相減

  • 代表兩者之間的距離
  • 注意回傳型態!見p.7
  • 相減代表的是 index 差距
    • int *p = &a[5];
    • int *q = &a[2];
    • p - q = 5 - 2;
  • 如果沒能確定兩個指標是指向同一個 Array 不要亂相減

在 C 裡面,允許指標的算術運算

  • 指標搭配常數作加減 
  • 指標相減 
  • 指標搭配  ++, -- 可以前置也可以後置
  • 注意:運算子的優先權

                       eg: *ptr++; *++ptr;

指標搭配 ++, -- 使用

int array[10];

int *ptr = &array[0];

在這種情況下,使用 ptr++; ++ptr; 只是將 ptr + 1

所以會將指標移動到下一個 int 也就是 array[1];

反之, ptr--; --ptr; 都是在做指標的移動。

指標搭配 ++, -- 使用

int array[10];

int *ptr = &array[5];

*, ++, -- 有以下 4 種搭配用法

  • *ptr++; *ptr--;
  • (*ptr)++; (*ptr)--;
  • *++ptr; *--ptr;  or  *(++ptr); *(--ptr);
  • ++*ptr; --*ptr;  or  ++(*ptr);  --*(ptr);

指標搭配 ++, -- 使用

  • *ptr++; *ptr--; 
  • (*ptr)++; (*ptr)--;
    • ​( ) 優先於任何運算
  • *++ptr; *--ptr;  or  *(++ptr); *(--ptr);
    • 看誰靠近 operand
  • ++*ptr; --*ptr;  or  ++(*ptr);  --*(ptr);
    • 看誰靠近 operand

小試身手 - 7.3

git clone https://gist.github.com/2b7df42c336e616f3b67.git

在 C 裡面,允許指標的算術運算

  • 指標搭配常數作加減 
  • 指標相減 
  • 指標搭配  ++, -- 可以前置也可以後置 
  • 注意:運算子的優先權

                       eg: *ptr++; *++ptr;

藉由前面的內容,你大概可以知道指標和 Array 是有些關聯的

其實在某些時候,陣列的名稱(有些書會寫作:陣列變數)會被編譯器當成是指標處理。

例如:

對一個 integer array 傳入 function時,實際上編譯器會將 array 看成一個指標,指向 int 變數。所以 array 會被編譯器當成是 int *

What's an Array?

 什麼時候,做什麼樣的操作呢?才會被當成 pointer 處理?

將陣列傳入函式中使用

所以陣列就是指標囉?

大錯特錯!

Bullshxt !

陣列和指標的不同

  • 當你宣告一個陣列的時候,陣列的名稱並不會被配置記憶體儲存。而是類似 C 語言中的 Label 的形式。但是指標變數是會被儲存在記憶體中的。
  • sizeof operator : sizeof 是一個運算子(而非函式),可以在編譯期間,取得一個變數佔有的空間大小。
  • & + array name 會被轉成為一個指標指向整個 array 大小的資料。(可利用 gdb
  • 陣列名稱(陣列變數)不能指向其他地方,因為他不是指標,只是一個固定的 label,標示我從哪裡開始配置一個連續的記憶體當作陣列使用

陣列和指標的不同

  • sizeof operator : sizeof 是一個運算子(而非函式),可以在編譯期間,取得一個變數佔有的空間大小。

sizeof 運算子對指標變數運算的話,取得的會是指標變數的所需的空間大小。

指標變數可以是 int *, double *,  int (*)[10],  int *(*) 。這些變數用來儲存的記憶體的位址。

所以其大小最小要是能夠定址到每一個記憶體的 byte。假如今天是 4GB RAM 及 32-bit 電腦架構, 相當於有 2^32 個 bytes。為了定址這 2^32 個 byte,就要有 32 個 0, 1 來表示定址的位址。32 個 0, 1 及 32bits,相當於是 4bytes。所以在這樣的情況下指標的大小最小就是 4bytes

陣列和指標的不同

  • & + array name 會被轉成為一個指標指向整個 array 大小的資料。
  • 使用 gdb 查看
  • 或是讓編譯器告訴你答案

git clone https://gist.github.com/07ed25cc9a90ccc5318d.git

不會用 gdb 沒關係

讓編譯器來告訴你!

編譯參數加上  -Wall 

回家趕快學 gdb

陣列和指標的不同

git clone https://gist.github.com/07ed25cc9a90ccc5318d.git

 warning: incompatible pointer types initializing 'int *' with an expression of type 'int (*)[5]'

      [-Wincompatible-pointer-types]

        int *ptr = &array;

陣列和指標的不同

  • 陣列名稱(陣列變數)不能指向其他地方,因為他不是指標,只是一個固定的 label,標示我從哪裡開始配置一個連續的記憶體當作陣列使用

陣列和指標的不同

  • 陣列名稱(陣列變數)不能指向其他地方,因為他不是指標,只是一個固定的 label,標示我從哪裡開始配置一個連續的記憶體當作陣列使用

Why? 在 C 語言中有

  • 左值 Lvalue
  • 右值 Rvalue 

左值指的是實際在記憶體中佔有空間的內容。右值則是一個實際運算出的數值。左右之分,是因為放在 assignment 的左右邊而定名。但是!array name 雖然在 C 語言中佔有記憶體空間,可是卻無法修改。我們稱之為 unmodifiable lvalue

陣列和指標的不同

 error: array type 'int [5]' is not assignable

        array = array2;

Pointer decay

陣列其實會包含大小的資訊 (由前面的 sizeof operator 可以看到 )。但是,當你把一個陣列指定 ( assign ) 給指標變數的時候,我們無法藉由指標變數來得知原本陣列的大小。這樣資訊流失的情況,被稱作是指標退化 ( pointer decay )

尤其是,當我們把陣列當作參數傳入函式的時候,一定會發生這種情況。所以你必須做的事情是在使用函式的時候,一併將陣列大小   ( Array size ) 當作變數傳入。

 在什麼時候,我們可以將 Array 和 Pointer 視作同等呢?

指標和 Array 同等的情況

  1. 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。
  2. array name + [index] 相當於 *(pointer + offset);
  3. 用於函式當作宣告的參數型別時,array name 會被當作是指向 array 第一個元素的指標。

指標和 Array 同等的情況

  1. 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。

int a[10];

int *p = NULL;

p = a;

以下取自wikipedia:

An expression in a programming language is a combination of one or more explicit valuesconstantsvariablesoperators, and functions that the programming language interprets (according to its particular rules of precedence and of asso-ciation) and computes to produce ("to return", in a stateful en-vironment) another value.

指標和 Array 同等的情況

2. array name + [index] 相當於 *(pointer + offset);

  • eg:
  • int array[5] = {1, 2, 3, 4, 5};
  • array[3] 即是 *(array + 3)

以往,有人會認為將 array + [index] 寫成 *(pointer + offset)的形式,執行效能會比較快。但是,在現今的編譯器來說,已經做了相當程度的最佳化,編譯出來的組合語言是一樣的。所以在執行效能上沒有太大差別,應該著重於語意表達。

 

指標和 Array 同等的情況

3. 用於函式當作宣告的參數型別時,array name 會被當作是指向 array 第一個元素的指標。
 

Why? 因為 C 語言只有 Pass by Value 的概念,我們只有藉由 Pass address 給 function,藉由 address 和記憶體做連結,才可以取得 array 的內容。( pass by value 是複製 address 的數值給 formal parameter )

指標和 Array 同等的情況

3. 用於函式當作宣告的參數型別時,array name 會被當作是指向 array 第一個元素的指標。
 

以上兩種參數對編譯器來說都會轉成是 int *

為什麼 C 語言在設計的時候,要將傳入的function 的 Array 當作是指標看待?

在 C 語言裡面只有數值 ( value ) 的概念。當你傳遞參數的時候,就是將 argument 的數值複製一次。

如果你今天傳遞 Array 要把整個 Array 複製一次?C 是用來寫底層系統的語言!這開銷太大了!(所以程式設計師要小心管理記憶體)

指標和 Array 同等的情況

  1. 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。
  2. array name + [index] 相當於 *(pointer + offset);
  3. 用於函式當作宣告的參數型別時,array name 會被當作是指向 array 第一個元素的指標。

以上就是你可以將 Array, Pointer 視為同等的情況

以編譯器的角度看

但是你必須將一句話記在心裡

Array 不是 Pointer !!

說了這麼多,我們來談談如何應用在 Function

上一份投影片跟我們說指標間單來說有兩種應用方式

Pointer 的常見應用

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

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

接下來要介紹的就是Type2

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

int scanf(const char *restrict format, ...);

在 C 語言中, string 和 array 有何不同?

Cstring 其實是在一個連續的記憶體中,將每一個資料以 char 的方式儲存與解讀,最後以 null character 標記結尾 <---- 不同處

我們如何傳遞一個 Cstring 給 function?

我們如何傳遞一個 Cstring 給 function?

傳遞第一個元素的位址

Example 

(畫圖講解)

為什麼使用 char * 這樣的指標行別來存取 string 內容?


因為對於 string 來說,每一個元素都是 char 的型別,我們使用指標搭配指標移動可以存取在記憶體中連續的資料,這個資料是 char,所以我們使用 char * 這樣的指標。

1. 可以存取字串中的資料單元

2. 可以藉由遞移指標完成整個字串的查找


const 則是標記我們不會藉由指標更改記憶體中的內容


小試身手8

請實作 print_arr 這個函式,讓他能夠印出一維陣列中的每一個元素。請不要偷看前面的內容,誠實面對自己。你偷看了,寫對了不代表你真的學會!

一維陣列傳入 function 時,靠近 array 名稱 的維度會被衰退為指標做使用,說衰退是因為只知道第一個元素的位址,但是不知道有多少元素

int array[10] ---->  int (*array);

但是由於衰退了

不知道存取到何時才結束( size 的資訊不見了!)

所以為什麼小試身手8 要給定 size,Cstring呢?

  • char str[] ---> char (*str) ---> char *str 
  • char *c[15] ---> char *(*c) ---> char **c
  • char *c  ---> char **c ?
  • char (*c)[64]  ---> ???

其他陣列衰退為指標的例子

  • char str[] ---> char (*str) ---> char *str 
  • char *c[15] ---> char *(*c) ---> char **c
  • char *c  ---> char **c ?  這時候就不是 type2的問題了,如果要使用 function 修改 char *c 的資料才需要傳入 c 的位址 ( char ** )
  • char (*c)[64]  ---> ??? 不需做改變。因為原本只是知道 c 指向一個 char [64] 的區塊,所以 c 可以存放區塊第一個元素的位址。同樣的傳進參數的時候,也可以只知道第一個元素的位址。那 [64] 是做什麼用的?讓我們解析區塊大小,讓編譯器精準定位出元素所在。c+1時,會移動到下一個 char [64] 的區塊。

其他陣列衰退為指標的例子

舉例 char (*c)[5]

p.37

& + array name 會被轉成為一個指標指向整個 array 大小的資料。那 s[1], s[2] 呢?(畫圖

舉例 char (*c)[5]

p.37

& + array name 會被轉成為一個指標指向整個 array 大小的資料。那 s[1], s[2] , s[0][0], s[0][1], s[0][2] 呢?

拜託用 GDB

有沒有多維陣列的感覺?

多維陣列

  • 不管如何,在記憶體中都是一維的!
  • 多維陣列其實是一維陣列中的元素是另一維陣列
  • 傳入的時候一樣傳入陣列名稱,但是他會轉成什麼?

要將陣列傳入 function時,你必須要讓編譯器能夠正確的解析存取位址

前面的例子:  char (*s)[5] 為麼要有 5 ?

如果今天沒有 [5] 你在存取 s[1][2]的時候,經過第一個 row 他會以為一個 row 有幾個元素?那我們怎麼存取到 s[1][2] ? (遞增正確的 row )

傳入陣列的時候,只會將一個維度退化為指標,其他維度都要保留,否則無法提供足夠的資訊給 compiler 去取得正確的位址,進一步解析資訊

如何將二維陣列傳入 function

以 int array[3][4] 為例

method1

優點:無腦好寫

缺點:array如果是[4][4] 就沒辦法使用了!

git clone https://gist.github.com/8e29e0b494e86bb783d9.git

如何將二維陣列傳入 function

以 int array[3][4] 為例

method1

試試看,請問第 12, 13行的結果是什麼?

 

為什麼會這樣?

git clone https://gist.github.com/8e29e0b494e86bb783d9.git

如何將二維陣列傳入 function

以 int array[3][4] 為例

試試看,請問第 12, 13行的結果是什麼?

12 行:因為我們傳入的 arr 會轉成為 int (*)[4] 是一個指標,對 64bits 系統來說,指標的大小會是 8 bytes

13行:因為我們給予編譯器 arr的資訊是 int [3][4] 所以可以知道他每一個 row 有 int [4] 的大小於是就會是 16

sizeof 是編譯時期就能取的資訊的 operator

如何將二維陣列傳入 function

以 int array[3][4] 為例

method2

為了讓它更有彈性!我們的參數是 int (*arr)[4]

 

這樣就能讓 int [1][4], int [3][4] .... int [100][4] 這樣的陣列都可以使用了!

git clone https://gist.github.com/c987c42713b421587d91.git

親手打看看

如何將二維陣列傳入 function

以 int array[3][4] 為例

method2

其實參數 int (*arr)[4] 相當於 int arr[][4]。所以在這裡可以互換!

 

因為我們說 array 在當參數值,靠近名稱的維度會退化!

git clone https://gist.github.com/c987c42713b421587d91.git

親手打看看

如何將二維陣列傳入 function

在這裡就不能以 int arrat[3][4] 為例了

method3

在參數中,int ** 和 int  (*)[4] 顯然是有差距的,後者知道大小 ( array ) ,但是前者只知道他是一個指標,指向另一個指標。

 

常用於動態配置記憶體

git clone https://gist.github.com/ff414ea9a487fde3049e.git

如何將二維陣列傳入 function

在這裡就不能以 int arrat[3][4] 為例了

method3

不覺得這很像是

int main(int argc, char **argv);  

int main(int argc,  char *argv[]);  

// char *argv[] ---> char *(*argv)

為什麼這裡只需要用一個 argc 標記字串的個數

也就是 char *argv[]

Iliffe vector

如何將二維陣列傳入 function

 

method4

既然...... 陣列在記憶體其實只是一個連續的空間

那我們當然可以用一維陣列來模擬二維的行為呀!

 

如何將二維陣列傳入 function

 

method4

用一維陣列模擬二維行為

git clone https://gist.github.com/2ac87731c30ce4499392.git

參考資料

目前為止

int scanf(const char *restrict format, ...);

你們應該懂上面 const char * 的意義了

本投影片沒有涵蓋的內容

  • extern declaration and array:

int array[10];

extern int *array;

extern int array[];

  • A pointer to function 指標也可以指向函式
  • 動態記憶體分配 malloc, free
  • VLA in function

參考資料