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;
Operator precedence in C:
但是通常這些運算你會搭配 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;
Operator precedence in C:
指標相減
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;
Operator precedence in C:
指標搭配 ++, -- 使用
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;
Operator precedence in C:
藉由前面的內容,你大概可以知道指標和 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 同等的情況
- 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。
- array name + [index] 相當於 *(pointer + offset);
- 用於函式當作宣告的參數型別時,array name 會被當作是指向 array 第一個元素的指標。
指標和 Array 同等的情況
- 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。
int a[10];
int *p = NULL;
p = a;
以下取自wikipedia:
An expression in a programming language is a combination of one or more explicit values, constants, variables, operators, 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 同等的情況
- 作為 expression 時 ( 非用於宣告的時候 ),被編譯器當作是指向第一個元素的指標。
- array name + [index] 相當於 *(pointer + offset);
- 用於函式當作宣告的參數型別時,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