*&*&*&*&*&指標

Arvin Liu @ 2019 資訊之芽 語法班

指標對初心者來說不好理解
有問題要盡量舉手唷!

( ≧Д≦)

變數是怎麼儲存的?

程式中,很像有這樣一個表...

第0個櫃子

第1個櫃子

第2個櫃子

「放學校的書的櫃子」

「放輕小說的櫃子」

「放『男人都會有一些不可告人的本子』的櫃子」

放了國文課本。

放了「涼宮春日的憂鬱」。

放了

??????。

用精簡的表示方法:

它在哪裡? 它是什麼? 它放了什麼?
第0個櫃子 (0x00) 放學校的書的櫃子 國文課本。
第1個櫃子 (0x01) 放輕小說的櫃子 涼宮春日的憂鬱。
第2個櫃子 (0x02) 放『男人都會有一些不可告人的本子』的櫃子 ?????

地址。我們通常用十六進位表示。

變數名稱。

就....它放/存了什麼。

所以程式到底?

int a = 5;

這短短的一行到底做了什麼?

流程 of                  

int a = 5;
它在哪裡? 它是什麼? 它放了什麼?
0x00000
int x 7
0x00001 空的。 沒放東西。
0x00002 空的。 沒放東西。

1. 找一個沒人用的櫃子。

2. 讓別人知道a在哪裡。

3. 把值放進去。

int a

5

取址& 取值*

& 取址符號 (拿地址)

* 取值符號 (拿數值)

它在哪裡? 它是什麼? 它放了什麼?
0x00000 int x 7
0x00004 float a 1.1
0x00008 double b 1.0

假設程式的記憶體表格:

那麼 &x 就是 0x00000。

&

*

但這裡有個疑問,&x是什麼型態?

我們給一個特別的型態表示 int的地址: int*

所以 & 可以把int 變成 int* , * 可以把int*變回int
所以 & 可以把float 變成 float* , * 可以把float*變回float

它在哪裡? 它是什麼? 它放了什麼?
0x00000 int x 7
0x00004 float a 1.1
0x00008 double b 1.0

假設程式的記憶體表格:

那麼 &x 就是 0x00000

*(0x00000) 呢? 會編譯失敗。 因為0x00000是int,不是int*

*(int *)(0x00000) 呢? 7

*(float *)(0x00004) 呢? 1.1

&*(float *)(0x00004) 呢? 0x00004

int* 的 * 跟取值符號的 *不要搞混喔!!

一個是型態的一部分 (拿來當宣告用的)

一個是取值符號(拿來使用,當運算的)

Example!

#include <iostream>
int main(){
        int x=777;
        std::cout << &x << std::endl;
        //std::cout << *(int *)(0x7fffffffdc54) << std::endl;
        std::cout << *&x << std::endl;
        std::cout << &*&x << std::endl;
        std::cout << *&*&x << std::endl;
        //std::cout << *(0x7fffffffdc54) << std::endl; // CE
        //std::cout << *x << std::endl;  // CE
        //std::cout << **&*&*&x << std::endl; // CE
}                   
/* Output  -- Answers are here.
0x7fffffffdc54
777
777
0x7fffffffdc54
777
*/

Practice Time!

除了練習上面的東西,觀察你變數的地址以外,
試試看 *(int *) 一個隨便給的數字會怎麼樣?

小小提醒:

你們是猜不出0x7fffffffdc54這種東西的,因為每次都不一樣。

所以你們直接註解那行掉就可以囉!

* 小知識:現今電腦都有ASLR(位址空間組態隨機載入)保護機制,所以每一次執行的&x都不一樣。除非你把它關掉,你才可以預先知道&x是什麼。這邊看不懂沒關係,想了解可以自行google看看。

變數地址的變數的宣告

也就是 - 指標宣告

What is Pointer?

指標就是一個變數。
存著別人的地址。

如果你要存int的地址,
那麼它型態就是 int * 。

Pointer Init

它在哪裡? 它是什麼? 它放了什麼?
0x00000 int x 7
0x00008 (int *) a 0x00000
0x00010 (int **) b 0x00008
int x = 7;
int *a = &x;
int **b = &a;
std::cout << *a << std::endl;
*a = 5;
std::cout << **b << std::endl;

5

**b = x , *b =a , *a = x

宣告指標的一些坑

int* a, b;
// 上下兩個等價
int *a;
int b;
// 所以宣告兩個指標要
int *a, *b;

小小總結

  • 宣告時,在變數前加上一個*,即代表此變
    數為一個指標
  • 宣告時, *不要跟著型態,會搞混。(int *a,*b;)
  • 使用時,在一個變數前加上一個&,其值即
    為此變數的位址
  • 使用時,在一個指標前加上一個*,其值即
    為位在此位址之變數本身

題外話:sizeof

sizeof : 知道一個型態的大小是多少Bytes

1 Byte = 8 bits

1 bit 就是一個 0或1。

所以一個byte 就是 00000000 ~ 11111111

通常bit都會八個一組,我們稱之為byte。

各個型態多少byte(s)?

#include <iostream>
int is_admin = 0;
int ary[160];
int main(){
  std::cout << sizeof(char) << std::endl;
  std::cout << sizeof(int) << std::endl;
  std::cout << sizeof(long long) << std::endl;
  std::cout << sizeof(float) << std::endl;
  std::cout << sizeof(int *) << std::endl;
  std::cout << sizeof(double *) << std::endl;
}
// 1 4 8 4 8 8

Practice: 算兩個地址差多少!

#include <iostream>
int a;
int main(){
    char c;
    // 想辦法算出 &a - &c
}

hint : 我們知道指標的大小是 8 bytes,和longlong一樣。

answer : (long long) &a - (long long) &c

記憶體區段錯誤?

Segmentation Fault (會吃RE)

亂亂用 *

它在哪裡? 它是什麼? 它放了什麼?
0x00000 int x 7
0x00004 系統的祕密。 不給看&寫><
0x00008 還沒規劃的地方。 ???????

*(int *)0x00004 或 *(int *)0x00008 因為這兩塊還沒規劃或者電腦不給你看,所以當你在*的時候就會出現記憶體區段錯誤。

(例如程式已停止回應。)

亂亂用 *

所以只要你 * 的位置是系統開給你的,例如宣告變數/陣列,那麼通常就不會出現RE。

你真的懂陣列嘛? part 1

陣列宣告

int a = 5;
它在哪裡? 它是什麼? 它放了什麼?
0x00000 空的。 沒放東西。
0x00004 空的。 沒放東西。
0x00008 int a 5

之後程式自己的記憶體會長這樣子:

int a[4] = {0};

那這樣子呢?

陣列宣告

它在哪裡? 它是什麼? 它放了什麼?
0xFFF00 a[0] 0
0xFFF04 a[1] 0
0xFFF08 a[2] 0
0xFFF0A a[3] 0
...

大概會像這樣子:

int a[4] = {0};

編譯器會讓 a 和 &a 都會等於 0xFFF00, 但 *a 是 a[0]。

陣列取值

int a[4] = {0};
a[1] = 1;

 追根究底,a[1]是什麼意思?
其實就是 *(a + 1) 的意思。(a 是類似int *,詳情請見上一頁)

那,a + 1 是什麼意思呢?
就是a這個指標的地址 + 1個int的大小。

 

根據上面的原則。寫a[1]或寫1[a] 都沒有關係喔!

因為 1 + a = a + 1。

一個型態的大小可以用sizeof(type)來看。例如sizeof(int)就會是4,表示4Bytes。

你真的懂陣列嘛? part 2

為什麼陣列開太小會RE啊?
(執行時期錯誤)

例如我們宣告 int ary[160];
但是題目的N可能會到10000,
因此你就會存取到ary[1600]。

我們已經知道,
亂亂存取很容易吃RE。

它在哪裡? 它是什麼? 它放了什麼?
0x00000 int x 7
0x00004 系統的祕密。 不給看&寫><
0x00008 還沒規劃的地方。 ???????

也就是你戳到0x00004/0x00008的時候,電腦就會因為安全因素讓你RE。

 開int ary[160] -> 看ary[1600]

它在哪裡? 它是什麼? 它放了什麼?
0xF0000 ary[0]在這裡。 0
0xF0004 ary[1]在這裡。 0
.... ... ...
0xF003C ary[159]在這裡。 0
0xF0040 還沒用到 ???
... ... ...
0xF0400 系統的祕密>///< 不給尼看>///<

因為程式沒幫你開到0xF0400,
只要那邊有系統的祕密/還沒規劃,看了就會吃RE!

小小總結

  • 因為陣列大小吃RE的原因就是你存取了一個系統沒預料到的地址。
  • 陣列的記憶體空間是連續的
#include <iostream>
int ary[160];
int is_admin = 0;
int main(){
  int i,x;
  std::cin >> i >> x;
  ary[i] = x;
  if(is_admin){
    std::cout << "How did you do that?\n";
  }
}

你要輸入什麼才可以讓程式輸出

"How did you do that" 呢?

Practice 0x01 - Global Flag

#include <iostream>
int is_admin = 0;
int ary[160];
int main(){
  int i,x;
  std::cin >> i >> x;
  ary[i] = x;
  if(is_admin){
    std::cout << "How did you do that?\n";
  }
}

你要輸入什麼才可以讓程式輸出

"How did you do that" 呢?

Practice 0x02 - Global Flag

Practice 0x03 - Struct Corrupt

#include <iostream>
#include <cstring>
struct person{
  char name[16];
  int is_admin;
};
int main(){
  char tmp[16];
  struct person peipei;
  peipei.is_admin = 0;
  std::cin >> tmp ;
  strcpy(peipei.name,tmp);
  std::cout << "Hi! " << tmp << std::endl;
  if(peipei.is_admin){
    std::cout << "Wow! Peipei is so Dian!" << std::endl;
  }
}

你要輸入什麼才可以讓程式輸出

"Wow! Peipei is so Dian!" 呢?

剛剛的Practice 0x03是一種典型的buffer overflow (BOF)攻擊喔!

為什麼可以征服宇宙?
如果你有程式碼就可以任意改值!無限血量無限金幣...

Challenge 1 - long long strike

#include <iostream>
long long ary[160];
int is_user=0;
int is_admin=0;
int main(){ 
  long long i,x;
  std::cin >> i >> x;
  ary[i] = x; 
  if(is_admin == 1){
    std::cout << "Why Peipei is so dian?\n";
  }
}

你要輸入什麼才可以讓程式輸出

"Why Peipei is so dian?" 呢?

0x00000001????????

例如 0x00000100000000 =
4294967296 (輸入4594967296即可。)

int 和 long long
怎麼存的?

表示成二進位制後逆著寫。
詳情請查詢 little-endian

(big-endian 就是順著寫)

0x00000001????????

longlong 存成
????????10000000

is_user 存 ????????

is_admin 存 10000000
-> is admin 是 00000001 = 1 (逆著)

Function Call & Ptr

Call by Value

void f(int a){
  a = 5;
}
int main(){
  int a = 10;
  f(a);
  cout << a << endl;
}
  • Call by Value是函數呼叫時,參數是用複製的。
  • 因為第二行的a=5是改複製體。所以不會改到main的a。
  • 跟指標沒有關係的幾乎都是Call by Value。

Call by Address

void f(int *a){
  *a = 5;
}
int main(){
  int a = 10;
  f(&a);
  cout << a << endl;
}
  • Call by Address是函數呼叫時,參數用它的指標
  • 因為給的是a的指標,所以f內直接做取值就會改到真正的a。所以輸出就會變5。
  • 注意因為給的是地址,接下來用這個參數都要 *
  • 其實也是一種call by value,因為address也是複製的。

Call by Address (Ary)

//void f(int a[]){
void f(int *a){
  a[1] = 5;
}
int main(){
  int a[10]={0};
  f(a);
  std::cout << a[1] << std::endl;
}
  • 我們知道陣列的變數給的是指標。
    • 也就是int ary[10]的ary是個指標。
  • 那麼,其實它就是一種call by addr。
    • 所以a[1]就會是5,會被改到。
  • 宣告用int a[]還是int *a都一樣。

Call by Reference

void f(int &a){
  a = 5;
}
int main(){
  int a = 10;
  f(a);
  cout << a << endl;
}
  • Call by Reference是函數宣告時在變數前面加個&,告訴程式不要複製。(不要Call by Value)。
  • 因此這樣一來真正的a就會被直接改到。

Example - Swap

Swap - Call by Value

void f(int a, int b){
  int tmp = a ;
  a = b;
  b = tmp;
}
int main(){
  int a = 10, b = 5;
  f(a,b);
  std::cout << a << "," << b << std::endl;
}

  • 行不通,因為f改的都是複製體。

Swap - Call by Address

void f(int *a, int *b){
  int tmp = *a ;
  *a = *b;
  *b = tmp;
}
int main(){
  int a = 10, b = 5;
  f(&a,&b);
  std::cout << a << "," << b << std::endl;
}

  • 注意Call by Address給地址,所以第一行要用int *
  • 因為只有地址,接下來(2~5行)都要用 * 取值。
  • 使用函數的時候,當然就是要傳地址。所以第八行要 &。

Swap - Call by Reference

void f(int &a, int &b){
  int tmp = a ;
  a = b;
  b = tmp;
}
int main(){
  int a = 10, b = 5;
  f(a,b);
  std::cout << a << "," << b << std::endl;
}
  • 注意Call by Reference是宣告的時候要給 &。
  • 其他照舊。

小小總結

  • 如果你只是要改到值,就用call by reference就好。
    • 懶的用的話,放全域變數就好。
  • 如果涉及太多指標操作,再用call by address。
    • 例如下週的linked list。
  • 如果不需要改值,就安心使用call by value吧!看起來會比較簡潔。