指標(Pointer)

  • C和C++會用到pointer的地方: |C | C++| |---| ---| |Pass by Address| Reference| |Pass Array to Function |Vector| |char * |std::string| |Dynamic Allocation(malloc(),linked list)| STL container| |Function Pointer| Function Object| |N/A |Iterator| |N/A |Polymorphism| |N/A |Polymorphism object in container|

  • pointer使用的位元數,取決於os的位元,如64bit的os的pointer使用8 bytes

# include <stdio.h>
int main(void){
    void *ptr;
    /* Output the size   */
    printf("Size of Pointer : %d Bytes",sizeof(ptr));
    return 0;
}
  • 變數的資料型態決定了變數分配到的記憶體大小;變數本身的值是指儲存於記憶體中的某個數值,可以透過變數名稱取得這個數值,這個數值又稱為 rvalue 或 read value;而變數的位址值則是指變數所分配到的記憶體之位置,變數本身又稱為 lvalue或 location value。

  • 如果想知道變數的記憶體位址為何,可以使用 & 運算子,& 是取址運算子(Address-of operator),可以取出變數的記憶體位址。

#include <stdio.h>

int main(void) {
    int var = 10;
    printf("變數 var 的值:%d\n", var);
    printf("變數 var 的記憶體位址:%p\n", &var);
    return 0;
}
  • 指標(Pointer)則提供了間接性,指標可指向特定的記憶體位址,而不直接操作變數或物件。
    • 指標是儲存記憶體位址的資料型態,實際上我們須認識電腦管理記憶體好比一個陣列,每一列都有以位元編碼的位址,每一位址都可儲存位元編碼的資料。
    • 例如,我們宣告並指派初值 22給整數變數a,編譯器將變數a放在 0110的記憶體位址裡稍後我們再宣告另一個指向 a 的指標變數 aPtr假設編譯器 aPtr 放在 1001 的記憶體位址裡。
#include <stdio.h>

int main() {
    int a = 22;
    int *aPtr = NULL;
    aPtr = &a;
    printf("a: %d\n", a);   //a = 22
    // aPtr的值等於a的位址
    printf("a address: %p\n", &a);
    printf("aPtr value: %p\n", aPtr);
    printf("aPtr address: %p\n", &aPtr);
    return 0;
}
  • 指標擁有兩種操作特性,一是操作指標所儲存的位址,一是操作指標所指向位址之資料,可以使用提取 (Dereference)運算子 * 來提取指標指向位址的資料。
#include <stdio.h>

int main(void) {
    int var = 10;
    int *ptr = &var;

    printf("指標 ptr 儲存的值:%p\n", ptr);
    printf("取出 ptr 指向的記憶體位置之值:%d\n", *ptr);

    return 0;
}
  • 如果已經取得了記憶體位置,當將某個值指定給 *prt 時,該記憶體位置的值也會跟著改變,相當於告訴程式,將值放到 ptr 指向的記憶體位址。
    • 當指標 ptr 儲存的值與變數 var 指向的記憶體位置相同時,當對 *ptr 進行指定的動作時,就會將值直接存入該記憶體位置,因此再 透過變數 var 取出的值也就改變了。
#include <stdio.h>

int main(void) {
    int var = 10;
    int *ptr = &var ;

    printf("var = %d\n", var); // var = 10
    printf("*ptr = %d\n", *ptr);    //*ptr = 10

    *ptr = 20;

    printf("var = %d\n", var);  // var = 20
    printf("*ptr = %d\n", *ptr); //*ptr = 20

    return 0;
}
  • 如果宣告指標但不指定初值,則指標指向的位址是未知的,存取未知位址的記憶體內容是危險的,例如:
int *ptr; //指向未知的位址((通常是記憶體區段錯誤)
*ptr = 10;

// 建議修改如下,使用NULL,其代表(void*)0,不建議使用0以免誤認為變數造成岐義性。
// c++11建議改用nullptr避免岐義性
int *iptr = NULL;
  • 標宣告常犯的錯誤,在指標宣告時,可以靠在名稱旁邊,也可以靠在關鍵字旁邊,例如:
// 兩者均為合法的宣告方式,但建議使用第一種宣告
int *ptr1 = NULL;
int* ptr2 = NULL;

// 第一個為指標,第二個為單純的變數
int* prt1 = NULL, ptr2 = 0;

// 兩者均為指標變數
int *ptr1 = NULL, *ptr2 = NULL;
  • 有時候,只希望儲存記憶體的位址,然後將之與另一個記憶體位址作比較,這時並不需要關心型態的問題,可以使用 void* 來宣告指標。
    • 由於 void 型態的指標沒有任何的型態資訊,所以只用來持有位址資訊,不可以使用 * 運算子對 void 型態指標提取值,而必須作轉型動作至對應的型態。
#include <stdio.h>

int main(void) {
    int var = 10;
    void *vptr = &var ;

    // 下面這句不可行,void型態指標不可取值
    // printf("%d\n", *vptr);

    // 轉型為int型態指標並指定給iptr
    int *iptr = (int*) vptr;
    printf("%d\n", *iptr);

    return 0;
}

指標的運算

  • 指標的加法與減法與一般數值的加減法不同,在指標運算上加 1 ,是表示前進一個資料型態的記憶體長度,例如在 int 型態的指標上加 1,是表示在記憶體位址上前進 4 個位元組的長度。在減法上觀念也是相同,對指標減 1 即是在記憶體位址上退後一個資料型態單位的長度。
#include <stdio.h>

int main(void) {
    int *ptr = NULL;
    double *dptr = NULL;

    printf("ptr location:%p\n", ptr);   //location null
    printf("ptr + 1:%p\n", ptr + 1);    // 0x4, 因為整數長度為4 bytes
    printf("ptr + 2:%p\n", ptr + 2);    // 0x8

    printf("double ptr location: %p\n", dptr);  //location null
    printf("dptr + 1:%p\n", dptr + 1);  //0x8,因為倍精度浮點數為8 bytes
    printf("dptr + 2:%p\n", dptr + 2);  //0x10 同上
    return 0;
}
  • pointer通常與整數做計算,其計算為 ptr = initial_address - n * (sizeof(data_type))
    • Pointer - Pointer = Integer
    • Pointer - Integer = Pointer
int *ptr=(int *)1000;
ptr=ptr-3;
printf("New Value of ptr : %u",ptr); //994
/* ptr = ptr - 3 * sizeof(int)
 *     = 1000 - 3 * 2
 */    = 994

指標與一維陣列 (pointer and array)

  • 在宣告一個陣列之後,陣列名稱用來參考至陣列的第一個元素的記憶體位址,例如在下面的程式中將指出,陣列 arr 與 &arr[0] 指向的位置是相同的。
    • 因此可以把arr解釋為(arr+0)
#include <stdio.h>

int main(void) {
    int arr[] = {7,5,4};
    inty *b = arr;

    // 兩者記憶體址位相同
    printf("arr :\t\t%p\n", arr);
    printf("&arr[0] :\t%p\n", &arr[0]);

    print("arr[0] = %d\n", arr[0]); //7
    printf("b[0] = %d\n", *b);      //7
    printf("b[0] = %d\n", *(b+0));  //7
    return 0;
}
  • 陣列的索引其實是相對於第一個記憶體位址的位移量,下面這個程式以指標運算與陣列索引操作,顯示出相同的對應位址值。
    • 在這個程式中,將陣列的第一個元素位址指定給 ptr,然後對 ptr 作遞增運算,每遞增一個單位,其與陣列相對應索引的元素之記憶體位址都相同,這可以說明陣列索引的背後意義。
#include <stdio.h>
#define LENGTH 10

int main(void) {
    int arr[LENGTH] = {0};
    // 因為arr存放的是記憶體位址,所以使用 *ptr=arr而非*ptr=&arr
    // 也可使用*ptr = & arr[0]
    int *ptr = arr;

    for(int i = 0; i < LENGTH; i++) {
        printf("&arr[%d]: %p", i ,&arr[i]);
        printf("\t\tptr + %d: %p\n", i, ptr + i);
    }
    return 0;
}

// output
&arr[0]: 0x7ffe75728d00     ptr + 0: 0x7ffe75728d00
&arr[1]: 0x7ffe75728d04     ptr + 1: 0x7ffe75728d04
&arr[2]: 0x7ffe75728d08     ptr + 2: 0x7ffe75728d08
&arr[3]: 0x7ffe75728d0c     ptr + 3: 0x7ffe75728d0c
&arr[4]: 0x7ffe75728d10     ptr + 4: 0x7ffe75728d10
&arr[5]: 0x7ffe75728d14     ptr + 5: 0x7ffe75728d14
&arr[6]: 0x7ffe75728d18     ptr + 6: 0x7ffe75728d18
&arr[7]: 0x7ffe75728d1c     ptr + 7: 0x7ffe75728d1c
&arr[8]: 0x7ffe75728d20     ptr + 8: 0x7ffe75728d20
&arr[9]: 0x7ffe75728d24     ptr + 9: 0x7ffe75728d24
  • 也可以利用指標運算來取出陣列的元素值。
#include <stdio.h>
#define LENGTH 5

int main(void) {
    int arr[LENGTH] = {10, 20, 30, 40, 50};
    int *ptr = arr;

    // 以指標方式存取資料
    for(int i = 0; i < LENGTH; i++) {
        printf("*(ptr + %d): %d\n", i , *(ptr + i));
    }
    putchar('\n');

    // 以陣列方式存取資料
    for(int i = 0; i < LENGTH; i++) {
        printf("ptr[%d]: %d\n", i, ptr[i]);
    }
    putchar('\n');

    // 以指標方式存取資料
    for(int i = 0; i < LENGTH; i++) {
        printf("*(arr + %d): %d\n", i , *(arr + i));
    }

    // 以陣列方式存取資料
    for(int i = 0; i < LENGTH; i++) {
        printf("arr[%d]: %d\n", i, arr[i]);
    }
    putchar('\n');

    return 0;
}

// output
*(ptr + 0): 10
*(ptr + 1): 20
*(ptr + 2): 30
*(ptr + 3): 40
*(ptr + 4): 50

ptr[0]: 10
ptr[1]: 20
ptr[2]: 30
ptr[3]: 40
ptr[4]: 50

*(arr + 0): 10
*(arr + 1): 20
*(arr + 2): 30
*(arr + 3): 40
*(arr + 4): 50

arr[0]: 10
arr[1]: 20
arr[2]: 30
arr[3]: 40
arr[4]: 50

指標與const

  • 使用指標常數的目的是怕程式製作的時候不小心更改了指標變數的內容。
基本上,不考慮()的話,就是從\*切成兩半,const在\*左邊就是形容指向的內容,const在\*的右邊就是形容指標。
const int*       ptrToConst;        // 指標指向固定整數
const (int *)    ptrToConst;        // 指標指向固定整數
const int* const constPtrToConst    // 固定指標指向固定整數
(int *) const    constPtr;          // 固定指標指向整數

void Foo( int       *       ptr,            // 單純的指標
          int const *       ptrToConst,     // 指標指向固定整數
          int       * const constPtr,       // 固定指標指向整數
          int const * const constPtrToConst );  //固定指標指向固定整數

const int a[5] = {1, 2, 3, 4, 5};   // const 陣列宣告
int FindNum(const int array[], int num, int conut);

指標指向固定整數(pointer to const int)

  • const int *ptrToConst
    • 宣告一個 pointer指向 const int,所以無法藉由指標更改變數值 +如果一個未指定為const的pointer指向一個const int時,仍可使用pointer修改其值!!
    • 此方法可想成我們固定追踨(指標)住在高雄(位址)的小明(變數),即使小明搬到台北(位址)去了,我們還是追踨小明。
const int v0 = 10;
int* ptr = &v0;
/* 雖然v0為const,不可直接修改其值,
 * 但是因為pointer ptr沒有指定const屬性
 * 因此仍然可以用ptr修改v0的內容!!
*/
v0 = 11;    // error
*ptr = 11;  //合法的行為,且v0會變11!!
printf("v0 = %d\n", v0);    //11
printf("*ptr = %d\n", *ptr);    11
int v0 = 10;
const int* ptr = &v0;
v0 = 20;

printf("v0 = %d\n", v0);    //20
printf("*ptr = %d\n", *ptr);    //20
  • pointer to const value用於函數參數
void func(const int b[]){
    // b[1] = 10; 錯誤,因為b為const,所以不可修改其內容值
}

void func2(const int* b){
    // b[1] = 10; 錯誤,因為b指向const values,不能改值
}

void func3(int *const b){
    b[1] = 10;  //合法,因為只限定b不可指向其它位址,但可換值
}

int main(){
      int arr[] = {5,7,2};
      func(arr);
      printf("arr[1] = %d\n", arr[1]);
      return 0;
}

固定指標(const pointer)

  • 固定指標即指標不可指向別的位址,因此必須在宣告時給予初始值.
    • 但是被指向的變數可以修改其值。
    • 此方法可想成我們固定追踨(指標)住在高雄(位址)的人數(變數),即使人數一直變動,但是我們仍然追踨高雄。
    int v0 = 10;
    int v1 = 11;

    int* const ptr2 = &v0;  //必須給初始值,否則之後無法修改
    printf("*ptr2 = %d\n", *ptr2);  //10

    // ptr2 = &v1; //錯誤,不可修改指標指向的變數
    v0 = 20;       // 合法,可修改變數的值
    printf("*ptr2 = %d\n", *ptr2);  //20
  int arr[] = {5,4,6};
  int arr2[[] = {11,22,33};
    int* const ptr = arr;
    // 印出 5
    printf("arr[1] = %d %d\n", arr[1], *(ptr+1));
    arr[1] = 3;

    // 因為array不是const,可修改其內容值,印出 3
    printf("arr[1] = %d %d\n", arr[1], *(ptr+1));

    // 因為只有指向位址不能變,值可以變,所以可改值,印出9
    *(ptr+0) = 9;   //改到arr[0]而不是ptr的位置
    printf("arr[0] = %d %d\n", arr[0], *(ptr+0));

    // ptr = arr2;  // 非法的操作,因為ptr是const pointer
    return (EXIT_SUCCESS);
  • const pointer用於函數參數的傳遞
void func3(int *const b){
    b[1] = 10;  //合法,因為只限定b不可指向其它位址,但可換值
}

int main(){
      int arr[] = {5,7,2};
      func3(arr);
      printf("arr[1] = %d\n", arr[1]); //10
      return 0;
}

指標用於函數參數傳遞小結

  • 如果傳遞參數的為單純的型態如int, double等,使用copy by value傳值,因為cpu會從cache取值,速度較傳址快,除非有其它考量。

  • 如果是複雜的型態如array, struct等,須使用pass by pointer以提升效能。

  • 如果不希望傳遞的參數在函式內被改值的話,必須明確加上const說明變數或指標之值不在函式內變動。

    • 一般的API中,若是沒有寫const的的pointer函式參數傳遞,通常代表pointer的內容在函式中會改變。

指標總整理

  • ()[]是第一優先權左結合,而*是第二優先權右結合。在看變數宣告時,如同運算式的推演過程,必須遵守C程式語言對*()[]的優先權定義。
    • 看見[]就說array[] of
    • 看見*就說pointer to
    • 看見變數後面的()就說function() returning
char *x;         // x: a pointer to char
char x[3];       // x: an array[3] of char
char x();        // x: a function() returning char
char *x[3];      // x: an array[3] of pointer to char
char (*x)[3];    // x: a pointer to array[3] of char
char **x;        // x: a pointer to pointer to char
char *x();       // x: a function() returning pointer to char
char *x()[3];    // x: a function() returning array[3] of pointer to char
char (*x[])();   // x: an array[] of pointer to function() returning char
char (*x())();   // x: a function() returning pointer to function() returning char
char (*(*x)[])(int, int); // x: a pointer to array[] of pointer to function(int,int) returning char

指標指向自已位址

#include "stdio.h"

void p2self(void** p) {
    /* 為了處理所有型別的pointer而使用void* */
    (*p) = (void*)&(*p);
}

int main() {
    int* pi;
    printf("%p\n",&pi);
    p2self((void**)&pi);
    printf("%p\n",pi);
}

results matching ""

    No results matching ""