owned this note
owned this note
Published
Linked with GitHub
# 指標篇 筆記
>「指標」扮演「記憶體」和「物件」之間的橋樑
## C 語言規格
### Object
首先可以先探討 C 語言中的 object。講到 object 大家一定會想到物件導向,但其實在 C 語言規格書中有個專業術語就叫 『object』。
* object 定義: 在執行時期,資料儲存的區域,可以明確表示數值的內容
> region of data storage in the execution environment, the contents of which can represent values
聽起來很抽象,有人舉了這樣的例子。可以假設 object 是一個箱子,那我們可以這樣定義:
* 箱子本身 (object)
* 箱子的規格 (type)
* 箱子的內容 (value)
* 箱子的名子 (identifier)
* 箱子的位置 (address)
如果我在一個函式內定義一個變數 `int a = 1` ,在執行到時,編譯器會在 stack segment 內分配一個記憶體空間,這個空間會有一個對應的記憶體地址,而在這個變數結束它的生命週期 (lifetime) 之前,地址是固定的。
因此可以利用指標 `int *ptr = &a` 作為「記憶體」和「物件」之間的橋樑。這也是 C 語言特別的地方,可以透過記憶體映射的方式直接控制硬體。
### Lifetime
而 C 語言有以下三種物件的生命週期類型。
> An object has a storage duration that determines its lifetime. There are three storage durations: static, automatic, and allocated.
1. **Static lifetime**: 使用 static 定義的變數、全域變數,這類型的變數會在一開始編譯時就放入 process 獨立記憶體區間的 `.data` 區域內。
2. **Automatic Lifetime**: 區域變數,生命週期是在函式的執行期間,當函式結束時,記憶體會被釋放。其實就是進入函式時,stack 分配的。
3. **Dynamic Lifetime**: 使用 malloc、calloc 或 realloc 動態分配的記憶體。物件的生命週期由 programmer 控制,直到使用 free 釋放為止。
* 物件的生命週期結束後,其記憶體位址可能被釋放或重複使用,因此不要在物件生命週期外存取該位址,以免引發未定義行為(Undefined Behavior)。
### Derived declarator types
* pointer、array、function 其實都是一樣的,都是指向記憶體的地址。
> Array, function, and pointer types are collectively called derived declarator types. A declarator type derivation from a type T is the construction of a derived declarator type from T by the application of an array-type, a function-type, or a pointer-type derivation to T.
* Incomplete type
```cpp
int Arr[100] // OK
int Arr[] // Incomplete type
struct GraphicsObject; // OK
struct GraphicsObject *initGraphics(int width, int height); // OK
struct GraphicsObject obj; // Incomplete type
struct GraphicsObject *obj; // OK
```
所以在還未定義前,是不能將其實例化的,但可以先宣告指標。而這個指標的大小就是機器的地址大小。
> [What are 'objects' in C language?](https://stackoverflow.com/questions/72210270/what-are-objects-in-c-language)
## 頭腦體操
觀察以下程式碼:
```cpp
(*(void(*)())0)();
```
我們將其一一分解:
* `void (*)()`: 指向『無返回值且無參數』的指標型態
* `(void(*)())0` 將 0 強制轉換成 `void (*)()` 類型
* `(*(void()())0)`: 將強制轉換後的函式指標解 dereference
* `(*(void()())0)();`: 呼叫這個函式指標所指向的函式,也就是執行地址 0 處的程式碼。
那想當然會發生 Segmentation fault ,因為記憶體位置 0 是不可被執行的 ([Invalid Memory Access](https://hackmd.io/19RAR1OFSqOYt2dg0G-2Pw?view#Valgrind)),而透過這樣一一分解可以讓我們更了解這串複雜的指標型態。
在看一個例子:
```cpp
void ( *signal(int sig, void (*handler)(int)) ) (int);
--------------------------------------------------------------------------------------
signal -- signal
signal( ) -- is a function
signal( sig ) -- with a parameter named sig
signal(int sig, ) -- of type int
signal(int sig, handler ) -- and a parameter named handler
signal(int sig, *handler ) -- which is a pointer
signal(int sig, (*handler)( )) ) -- to a function
signal(int sig, (*handler)(int)) ) -- taking an int parameter
signal(int sig, void (*handler)(int)) ) -- and returning void
*signal(int sig, void (*handler)(int)) ) -- returning a pointer
( *signal(int sig, void (*handler)(int)) )( ) -- to a function
( *signal(int sig, void (*handler)(int)) )(int) -- taking an int parameter
void ( *signal(int sig, void (*handler)(int)) )(int); -- and returning void
```
> [How to read this prototype?](https://stackoverflow.com/questions/15739500/how-to-read-this-prototype)
## void pointer
* `void *` 可以指向任何類型的資料,而其存在的目的就是為了強迫使用者使用 ==顯式轉型== 或是 ==強制轉型==,以避免 Undefined behavior 產生。
* 兩個不同類型的指標是不能兼合的,會產生 Undefined behavior,
```cpp
int *ptr1;
float *ptr2;
ptr1 = ptr2;
-----------------------------------------------------------------------
warning: assignment to ‘int *’ from incompatible pointer type ‘float *
```
* 利用 void 指標就可以指向各種類型
```cpp
int *ptr1;
void *ptr2;
ptr1 = ptr2;
```
* 也可以對 void 指標強制轉行成各種類型
```cpp
int *ptr1;
void *ptr2;
ptr1 = (int *)ptr2;
```
### 注意要點
1. 若函式沒有給定返回值的類型,C 語言是默認返回值的類型為 `int`。
```cpp
warning: return type defaults to ‘int’
```
2. 若函式的參數是任意型態的指標,則應該使用 `void *`,可以在許多 C 語言操作記憶體的函式庫中看到 void 指標,因為參數是一塊記憶體空間,不用管這塊類型為何。
```cpp
void *memcpy(void *dest, const void *src, size_t n);
```
### Memory alignment
當指標的型態不一致時,用 void 轉型就會是 Undefined behavior,以下例子說明。拿 `ptr2` 指標指向型態為 `short int` ,其是用 2 個 bytes 表示,而後將其強制轉型成 `int` ,用 4 個 bytes 表示。這時候就會發現輸出的結果是錯的,原因就是需要把原本用 2 個 bytes 表示的數值存放到用 4 個 bytes 表示,這時就會引發對齊錯誤(alignment fault)。而每個機器的對齊方式會不同。
```cpp
short int n2 = 5;
void *ptr2 = &n2;
int b = * (int *) ptr2;
printf("b = %d\n", b);
------------------------
$ b = 327685
```
## Pointer to pointer
**C 語言的函式呼叫只有 call by value**。而我們以前聽到用指標當作參數傳入稱為 call by reference,嚴格來講應該算是語意不對,因為指標作為參數其實就是將記憶體地址作為 value 傳入函式。
接著我們來看指標與指標的指標作為參數傳入函式的差異。
### 指標作為參數
```cpp
int B = 2;
void func(int *funcP) { funcP = &B; }
int main() {
int A = 1;
int *ptrA = &A;
func(ptrA);
printf("(*ptrA) = %d\n", *ptrA);
return 0;
}
--------------------------------
$ (*ptrA) = 1
```
1. 指標 `ptrA` 指向 `A` 的地址。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa}
structptr [label="ptrA|<ptr> &A"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
}
```
2. 進入函式時,其實就是將 `ptrA` 副本一份出來,也就是 `funcP` 指標,它也指向 `A` 的地址。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa}
structp [label="funcP|<p>&A"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
structp:p -> structa:A:nw
}
```
3. 在函式將這個副本 `funcP` 指向 `B`。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb}
structp [label="funcP|<p>&B"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
structp:p -> structb:B:nw
}
```
最後就返回主程式,但可以從圖中看到原本的 `ptrA` 並沒有指向 `B` ,而是由副本指向它。而副本因他的 lifetime 結束了,因此並沒有真的將引入的參數指向 `B` 。
### 指標的指標作為參數
```cpp
int B = 2;
void func(int **funcP) { *funcP = &B; }
int main() {
int A = 1;
int *ptrA = &A;
func(&ptrA);
printf("(*ptrA) = %d\n", *ptrA);
return 0;
}
--------------------------------
$ (*ptrA) = 2
```
1. 與前面一樣,指標 `ptrA` 指向 `A` 的地址。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb}
structptr [label="ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
}
```
2. 將 `&ptrA` 引入函式,而參數類型就是 `**funcP` ,也就是指標的指標。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb}
structadptr [label="pointer to ptrA|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
structadptr:adptr -> structptr:name_ptr:nw
}
```
3. 進入函式後一樣會副本一份,但這時副本的是 `ptrA` 的地址。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb}
structp [label="funcP|<p> &ptrA"]
structadptr [label="&ptrA(temp)|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structptr:ptr -> structa:A:nw
structadptr:adptr -> structptr:name_ptr:nw
structp:p -> structptr:name_ptr:nw
}
```
4. 在函式內 `*funcP = &B` 就是將 `funcP` 指向的指標的值改成 `B` 的地址。也就是說 `ptrA` 就指向 `B` 了。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb}
structp [label="funcP|<p> &ptrA"]
structadptr [label="&ptrA(temp)|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &B"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structptr:ptr -> structb:B:nw
structadptr:adptr -> structptr:name_ptr:nw
structp:p -> structptr:name_ptr:nw
}
```
### 參數的差異 (指標 VS. 指標的指標)
| 特性 | 指標作為參數 | 指標的指標作為參數 |
| -------- | -------- | -------- |
| 修改副本變數的值 | 可以(透過將dereference) | 可以(透過兩次dereference) |
| 修改指標的指向 | 無法修改原來指標的地址 | 可以修改指標,讓其指向其他變數 |
| 用途 | 改變某個變數的值 | 改變指標本身的指向
## 指標內居然可以隱藏資料?
我們先來看一個簡單的例子,有一個型態為 `int` 的陣列,現在我要把它裡面所有元素的地址都印出來。可以看到地址的最後一個 byte 一定是 $0$ 或 $4$ 或 $8$ 或 $C$。原因很簡單,因為 `int` 型態是 4 個 byte ,所以地址一定是可以除盡 4 的數字。而這些可以除盡 4 的數字有一個特色,那就是**最後 2 個 bit 一定是 0**。因此就有一些應用會把資料除存在這兩個 bit 上。
```cpp
int arr[4] = {1, 2, 3, 4};
for(int i = 0; i < 4; i++) {
printf("%p\n", (arr + i));
}
---------------------------------
0x7ffc02e66270
0x7ffc02e66274
0x7ffc02e66278
0x7ffc02e6627c
```
### Linux 的紅黑數
Linux 核心內的紅黑數結構中,存在一個結構成員 `__rb_parent_color`,這個成員非常特別,它儲存了:
* 父節點的地址
* 節點本身的顏色
```cpp
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
```
我們知道紅黑數的節點顏色非黑即紅,因此只需要一個 bit 做表示,但 C 語言沒有一個 bit 的資料類型,因此利用上述的原理,我們可以將顏色的判別放到地址的最後一個 bit 內。
```cpp
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3))
#define __rb_color(pc) ((pc) & 1)
#define rb_color(rb) __rb_color((rb)->__rb_parent_color)
```
> [Hide data inside pointers](https://arjunsreedharan.org/post/105266490272/hide-data-inside-pointers)
## Pointer VS. Array
基本上指標與陣列是類似的東西,畢竟陣列其實就是一串數組的開頭地址。但還是有些許例外:
* **extern**: 當在標頭檔宣告了一個 `extern char str[]`,這時如果想要在 c 文件中使用的話就需要進行定義 `char str[]`。但是我們能不能定義成 `char *str` 呢?答案是不行的,因為編譯器會認為 `str` 是一個數組,而不是指標。
* **definition/statement**: 如果已經宣告陣列的大小,也同樣不能以指標的形式使用。
### 指標與數組的差異
* 數組
* 是一塊固定大小的連續記憶體區間
* `char str[] = "Hello"`,在定義時(編譯階段)就會分配記憶體並儲存字串內容
* 指標
* 是一個變數,用於儲存地址
* `char *str = "Hello"`,指標指向常數的字串 "Hello" 的起始地址,這時記憶體的內容是不能修改的
```cpp
char str1[] = "Hello"; // 數組
str1[0] = 'A'; // 合法
printf("%s\n", str1); // 輸出:Aello
char *str2 = "Hello"; // 指標
str2[0] = 'A'; // Undefined behavior
printf("%s\n", str2); // Segmentation fault (core dumped)
```
為了更直觀觀察,可以看下面實驗。可以看到 `arr1` 與 `arr2` 都在編譯時期就分配好記憶體空間,而 `arr3` 與 `arr4` 為指標,其大小就是 8 bytes (64 位元電腦)。
```cpp
char arr1[5];
char arr2[5] = {'A', 'B', 'C', 'D', 'E'};
char *arr3;
char *arr4 = "ABCDE";
printf("%ld, %ld, %ld, %ld\n", sizeof(arr1), sizeof(arr2), sizeof(arr3), sizeof(arr4));
------------------------------------------------------------------------------------
$ 5, 5, 8, 8
```
### 小問題
:::info
Q1: 這個靜態記憶體區間是有固定大小的嘛,因為我們知道作業系統會在 process 創建時分配一塊記憶體空間,裡面就有包含靜態記憶體區間,那我想知道的是若我的陣列大小剛剛好沒有超過這個空間,但仍然還是很大,那會擠壓到 stack 或 heap 的空間嘛?
:::
* 靜態記憶體區間是**有固定大小的**。應該說每個區域都有固定大小,只有 stack 與 heap 是由這個固定大小的上與下增長。
* 而當陣列配置的大小超過這個範圍就會導致編譯失敗。因此不會擠壓到 stack 與 heap 的大小
:::info
Q2: 該如何將兩個字串合併到一個新的字串?
:::
首先要先考慮到會有什麼問題。在新的字串定義上,我們可以寫成指標的型態 `char *newStr;`,但這樣只是宣告了指標,但它指向的地址並沒有初始化,因此這個值是未定義的,而 `strcpy` 函式需要確保目標指標指向的是**有效的可寫記憶體區域**。
那可以用陣列宣告嘛,當然可以。但是若兩個字串的長度加起來超過一開始宣告的長度,就會發生問題。而我們知道初始化陣列的長度是不能在執行階段才決定。因此想利用 `sizeof` 抓取長度也不行。
```cpp
char newStr[100]; // not work
char newStr[strlen(s) + strlen(t)]; // illegal
strcpy(r, str1);
strcat(r, str2);
```
解決方案就是利用 `malloc` 動態分配記憶體空間,接著為了安全起見,需要檢查是否失敗,這要就可以安全的進行複製與拼接了。最後要記得動態分配 `malloc` 函式等,需要程式員手動釋放記憶體空間。
```cpp
char str1[10] = "Hello", str2[10] = "World";
char *newStr = malloc(strlen(str1) + strlen(str2) + 1); // use sbrk; change program break
if (!newStr)
exit(1); /* print some error and exit */
strcpy(newStr, str1);
strcat(newStr, str2);
printf("newStr = %s\n", newStr);
free(newStr);
```
:::info
Q3: 考慮以下程式碼
```
char str[128];
```
為何 `str == &str` 呢?
:::
* `str`: 只要沒有使用 `&` 或 `sizeof` 編譯器就會將其轉成 pointer to type,在這裡 type 就是 char。而這個 type 是根據 array 的第一個元素來決定的
* `char *`
* `&str`: 它是一個 pointer to an array,因為使用了 `&` ,`str` 就不會被解讀為 pointer to type,而是一個 object。而對這個 object 取地址,也就是這個陣列的初始地址。
* `char (*)[128]`
### 總結
* 如果是用在 expression,array 永遠會被轉成一個 pointer
* 意思就是其實 array 本質上就是第一個元素的地址
* 用在 function argument 以外的 declaration 中它還是一個 array,而且「不能」被改寫成 pointer
* 而這個 array 算是一個連續的記憶體區間,也就是數組
* function argument 中的 array 會被轉成 pointer
* 在取值時,array 是直接將基底地址加上 offset,而 pointer 則是 CPU 會先取得地址,再加上 offset
## Function pointer
所謂 Function pointer 就是函式的指標,如同其他類型的指標一樣,它指向這段函式所在的初始記憶體位置。在宣告 Function pointer 時,需要注意以下幾點:
* 回傳型態
* 參數數量
* 參數型態
下面舉例:
```cpp
int functA(int a, float b); // 宣告
int functB(int a, float b); // 宣告
int (*functPtr)(int, float); // 宣告函式指標
functPtr = &functA; // 指向 functA
functPtr = &functB; // 指向 functB
```
* 應用場景
在排序時,往往一套算法,從小排到大與從大排到小要分開非常麻煩。因此可以利用 function pointer 去做比較。根據輸入的 function pointer 來決定比較函式。這在 Linux 裡也非常常見,可參考[研讀 Linux 核心原始程式碼的 lib/list_sort.c 筆記](https://hackmd.io/A6U5qYEXSXKi4Uvsrb96_Q?view)
```cpp
// 比較函數指標類型
typedef int (*CompareFunc)(int a, int b);
// 從小到大排序的比較函數
int ascending(int a, int b) {
return a > b; // 當 a > b 時,需交換
}
// 從大到小排序的比較函數
int descending(int a, int b) {
return a < b; // 當 a < b 時,需交換
}
void bubbleSort(int arr[], int n, CompareFunc compare) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
// 使用比較函數決定是否交換
if (compare(arr[j], arr[j + 1])) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
```
### Function Designator
Function Designator 基本上就是 function name。具體來說,如果我定義一個函式 `int func()`,則 `func` 就是 Function Designator。
當我們在使用 Function Designator 時,編譯器會把它當成(decays)一個指向函式開頭地址的指標,就如同陣列。
* 下面有三種呼叫函式的方法,三種都會執行,來看差異:
* `foo()`: `foo` 從 Function Designator 變成 Function pointer,再去做 function call。
* `(&foo)()`: `(&foo)` 就是 `foo` 函式的初始地址,那還是會從 Function Designator 變成 Function pointer,再去做 function call。
* `(*foo)()`: `foo` 從 Function Designator 變成 Function pointer 後去做 dereference 完又變成 Function Designator,這時候又會被編譯器轉成 Function pointer,因此可以在做 function call。
* `funcPtr()`: Function pointer 做 function call。
```cpp
void foo(void) { printf("Hello\n");}
void (*funcPtr) () = &foo;
foo();
(&foo)();
(*foo)();
funcPtr();
---------------------------
Hello
Hello
Hello
Hello
```
可以看到上面非常混亂,但簡單來說就是編譯器會把它轉成 Function Pointer 就對了,但除了使用 `sizeof` ,若是在 `sizeof` 裡面放 Function Designator 其實是不合法的,但我實作後會回傳 1。
> 注意到 designator 的發音,KK 音標為 [͵dɛzɪgˋnetɚ]
## 字串
字串有兩種宣告方式:
* `char str[] = "Hello";`
* `char *str = "Hello";`
詳細的差異已經在前面[指標與數組的差異](https://hackmd.io/2Zd7d5cYTcy1X_OdPLciCA?view#%E6%8C%87%E6%A8%99%E8%88%87%E6%95%B8%E7%B5%84%E7%9A%84%E5%B7%AE%E7%95%B0)說明了。
### 函式內回傳指標
考慮以下程式碼。如果我在函式內宣告一個字串,接著回傳回主程式。這樣的行為會發生 Segmentation fault。原因是 `str` 字串是 local 變數,它會存放在 stack 裡,而不是 static 區域內。也就是說,當 `func` 結束時,這塊記憶體空間就無效了(`ret` 時,stack 去 pop 了)。,因此返回這樣的指標會導致未定義行為。
```cpp
char *func(void)
{
char str[5];
str[0] = 'A';
str[0] = '\0';
return str;
}
int main(void)
{
char *newStr = func(); // Segmentation fault (core dumped)
printf("%s\n", newStr);
return 0;
}
```
* 解法一: 跟上面幾題類似,需要安全地動態分配空間給這個陣列。這樣這個陣列就會儲存在 heap,而不是 stack。
```cpp
char *func1(void) {
char *str = malloc(STR_SIZE); // 在堆區分配空間
if (!str) {
printf("Memory allocation failed\n");
exit(1);
}
str[0] = 'A';
str[1] = '\0'; // 確保是以 null 結尾的字串
printf("%s\n", str);
return str;
}
```
* 解法二:使用靜態變數,也就是將原本的字串變成 static,這樣就會被編譯器存入 static 區域內。
```cpp
char *func2(void) {
static char str[5]; // 靜態變數,存活於整個程式執行期間
str[0] = 'A';
str[1] = '\0'; // 確保是以 null 結尾的字串
printf("%s\n", str);
return str;
}
```
### 字串標準庫能不能接收 NULL pointer?
人們常說字串標準庫的使用要非常小心是為什麼呢。可以來看一個在 [ strlen.c for glibc version 2.7](https://elixir.bootlin.com/glibc/glibc-2.17/source/string/strlen.c) `strlen` 的實作。可以看到迴圈在遇到字元 `\0` 之前是不會停下的。並且也沒有在做 NULL pointer 的檢查。因此我們作為程式員必須作到兩件事:
1. 要確保字串的結尾是有 `\0` 的
2. 要確保引入的參數不是 NULL pointer
```cpp
/* Return the length of the null-terminated string STR. Scan for
the null terminator quickly by testing four bytes at a time. */
size_t
STRLEN (const char *str)
{
const char *char_ptr;
const unsigned long int *longword_ptr;
unsigned long int longword, himagic, lomagic;
/* Handle the first few characters by reading one character at a time.
Do this until CHAR_PTR is aligned on a longword boundary. */
for (char_ptr = str; ((unsigned long int) char_ptr
& (sizeof (longword) - 1)) != 0;
++char_ptr)
if (*char_ptr == '\0')
return char_ptr - str;
...
```
### Memory layout

## Lvalue and Rvalue
Lvalue(locator value)是「物件」的表示式,也就是表示佔據記憶體中某個可辨識位置的物件。。這個物件可以是一般的 object type 或是 incomplete type。而每個表達式都可以分類為 Lvalue 或 Rvalue。因此若表達式不是 Lvalue ,就是 Rvalue。
:::success
在 C 語言規格書中明確提到 Lvalue 的 `L` 是 locator。而 Rvalue 則表示為表達式的值。而非網路上所說的左值(left)與右值(right)。
> The name ‘‘lvalue’’ comes originally from the assignment expression E1 = E2, in which the left operand E1 is required to be a (modifiable) lvalue. It is perhaps better considered as representing an object ‘‘locator value’’. What is sometimes called ‘‘rvalue’’ is in this International Standard described as the ‘‘value of an expression’’.
:::
當我們執行以下程式時,會出現以下錯誤,告訴我們 Lvalue 需要一個可以被 assign 的操作值。`a` 變數就是 Lvalue,而常數 5 則是 Rvalue,因為這個常數不會儲存在記憶體裡,頂多在暫存器裡操作。
> 因為要寫回 data,需要有地方 (location) 可寫入
```cpp
int a = 5;
5 = a;
(a + 10) = 20;
```
> error: lvalue required as left operand of assignment
### 改變 Lvalue
在一些特殊情況下 Lvalue 可能被改成 Rvalue。例如使用 `const` 讓變數變為常數操作下,就不能再對其賦值。
```cpp
const int a = 10;
a = 5;
```
> error: assignment of read-only variable ‘a’
## NULL pointer
空指標 (null pointer) 是一個特殊的指標,它被設計為不指向任何有效的記憶體位置。它是一個安全機制,用來表示一個指標目前未被初始化或不指向任何有效的資料。
* NULL pointer 通常被定義為 `((void *)0)` 或是 `0`,要看實作環境決定。
### Example
空指標有兩種類型,`void` 與非 `void` 類型:
* `((void*)0)`: NULL pointer constant
* `((int *)0)`: NULL pointer to type,type 可以替換為其他型態
這兩者的區別就是 `void` 的 NULL pointer constant 可以賦值給任意型態的指標,而 NULL pointer to type 就只能賦值給對應型態的指標,否則就會出現警告。以下是文章中的例子。
```cpp
int *a = 0; // 沒問題,空指標常數
int *b = (int*) 0; // 沒問題,右邊是同型別的空指標
int *c = (void*) 0; // C沒問題,右邊是空指標常數,不過C++會掛
int *d = (long*) 0; // 會有warning提示 (incompatible pointer type)
int *e = (int*) a; // C不保證是空指標,C++會掛
```
而在 function pointer 中也是一樣的概念,值得注意的是第三項。我們對 `0` 轉成 NULL pointer 類型,所以變成 NULL pointer constant,接著在顯式地將其轉型成 function pointer 的型態。
```cpp
typedef void (*func)(void); // func f = void (*f)();
func a = 0; // 沒事,a是空指標常數
func b = (void *)0; // C 沒問題,C++會掛
func c = (void *)(void *)0; // C 沒問題,C++會掛
```
> [空指標常數(null pointer constant)](https://www.ptt.cc/bbs/C_and_CPP/M.1461840781.A.192.html)
```
int a = 1; // &a = 0x1000
int *ptr = &a;
type: int
value = 1
Identifier: a
Address: 0x1000
```
## References
> [你所不知道的C語言:指標篇](https://hackmd.io/@sysprog/c-pointer#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84C%E8%AA%9E%E8%A8%80%EF%BC%9A%E6%8C%87%E6%A8%99%E7%AF%87)