C | 指標

就良同學發表於2023-01-05

1.什麼是指標

指標是一種變數,也稱指標變數,它的值不是整數、浮點數和字元,而是記憶體地址。指標的值就是變數的地址,而變數有擁有一個具體值。因此,可以理解為變數直接引用了一個值,指著間接地引用了一個值。一個存放變數地址的型別稱為該變數的“指標”。

指標變數的大小?

32位系統為例,每個位元組(即一個記憶體單元)都擁有一個地址編號,地址範圍為0x00000000~0xffffffff。當指標變數佔4個位元組(即32bit)時,剛好能夠表示所有地地址編號。

不管什麼型別的指標,其大小隻和系統編譯器有關係。

image

2.指標的定義與使用

2.1 指標的定義

在C語言中,所有變數在使用前都需要宣告。例如,宣告一個指標變數的語句如下:

int *qPtr, q;

q是整型變數,表示要存放一個整型型別的值;qPtr是一個整形指標變數,表示要存放一個變數的地址,而這個變數是整數型別。qPtr叫做一個指向整型的指標。

在宣告指標變數時,“*”只是一個指標型別識別符號,指標變數的宣告也可以寫成 int* qPtr。

定義指標三步驟(來自傳智播客):

  1. *與符號相結合代表是一個指標變數,比如*p;
  2. 要儲存誰的地址,就寫出它的宣告語句,比如int a, int a[10];
  3. 用*p替換掉變數名稱,即int a→int *p,int a[10]→int (*p)[10](陣列指標);

指標變數可以在宣告時賦值,也可以在宣告後賦值。例如,在宣告時為指標變數賦值的語句如下:

int q = 12;
int *qPtr = &q;

也可以在宣告後為指標變數賦值:

int q = 12, *qPtr;
qPtr = &q;

2.2 指標的使用

指標變數主要透過取地址運算子&和指標運算子*來存取資料。例如,&a指的是變數a的地址(取址),*ptr表示ptr所指向的記憶體單元存放的內容(取值)。

#include<stdio.h>

int main(){
    int q=12;
    int *qptr;
    qptr = &q;
    printf("q的地址是:%p\nqptr中的內容是:%p\n", &q, qptr);
    printf("q的值是:%d\n*qptr的值是:%d\n", q, *qptr);
    // 運算子'&'和'*'是互逆的
    printf("&*qptr=%p, *&qptr=%p\n因此有&*qptr=*&qptr\n", &*qptr, *&qptr);
    return 0;
}

image

3.指標的寬度(步長)

#include<stdio.h>

int main(){
    int num = 0x01020304;
    char *p1 = (char *)&num;
    short *p2 = (short *)&num;
    int *p3 = &num;

    printf("%#x\n", *p1);
    printf("%#x\n", *p2);
    printf("%#x\n", *p3);
    return 0;
}

image

透過*取指標變數所指向那塊記憶體空間內容時,取得記憶體的寬度和指標變數本身指向變數的型別有關。

image

image

題目:

int a[5] = {1, 2, 3,4 , 5};
int *ptr = (int *)(&a+1);
printf("%d,%d", *(a+1), *(ptr-1));

輸出結果為:A.2,5 B.2,4 C.1,5 D.1,4

分析:

&a+1:跨過的是整個陣列的寬度

&a[0]+1:跨過的是陣列內單個元素的寬度

所以&a+1指向的記憶體地址已經不屬於陣列了,然後int *ptr = (int *)(&a+1)將&a+1強轉為int*型,所以ptr-1將後退一個4個位元組即一個陣列元素大小,即指向a[4]=5

4.野指標和空指標和萬能指標

4.1 野指標

野指標就是指標指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)。

#include<stdio.h>

int main(){
    int *p ;

    *p = 200;
    printf("%d\n", *p);
    return 0;
}

image

上述程式碼出現問題的原因:指標變數未初始化

任何指標變數剛被建立時不會自動成為 NULL 指標,它的預設值是隨機的。

所以,指標變數在建立的同時應當被初始化,要麼將指標設定為 NULL ,要麼讓它指向合法的記憶體。

如果沒有初始化,編譯器會報錯‘point’ may be uninitializedin the function。

4.2 空指標

#include<stdio.h>

int main(){
     // 將0號地址編號0x00000000號記憶體賦予指標內部值,等價於賦予NULL
    int *p = NULL;

    *p = 200; // 因為p儲存了0號記憶體的地址,這個地址為記憶體的起始地址,是不可使用的,非法
    printf("%d\n", *p);
    return 0;
}

image

NULL是C語言標準定義的一個值,這個值其實就是0,只不過為了使得看起來更加具有意義,才定義了這樣的一個宏,中文的意思是空,表明不指向任何東西。

任何程式資料都不會儲存在地址為0的記憶體塊中,它是被作業系統預留的記憶體塊。

空指標的作用:

如果指標使用完畢,需將指標賦予NULL;在使用指標前需判斷指標是否為NULL

4.3 萬能指標

void *p,可以儲存任意的地址。

void p:不可遺定義void型別的變數,因為編譯器不知道給該變數分配多大的記憶體空間;

void *p:可以定義void *變數,因為指標都是4個位元組(32位系統)。

#include<stdio.h>

int main(){
    int a = 10;
    void *p = (void *)&a;
    printf("%d\n", *p);
    return 0;
}

程式將報錯,無法編譯。因為雖然p記憶體內確實儲存的是變數a的地址,但是由於p指向void型變數,導致根據p指標內部儲存地址去取相應位置值時不知道取多大的記憶體大小。

#include<stdio.h>

int main(){
    int a = 10;
    void *p = (void *)&a;
    printf("%d\n", *(int *)p);
    return 0;
}

image

*(int *)p:將指標p強轉為int *型,此時根據p指標內部儲存地址去取相應位置值時,將讀取4個位元組大小。

5.const修飾的指標變數

引子:const int a = 10;const修飾變數a,表示不能再透過a修改a記憶體裡面的內容。

image

5.1 指向常量的指標

const修飾*,表示不能透過該指標修改指標所指記憶體的數值,但是指標指向可以變。

image

image

5.2 指標常量

修飾p,指標指向不能變,指標指向的記憶體可以被修改。

image

注:const int * const p =&a;表示p指標指向記憶體區域不能被修改,同時p的指向也不能被改變。

6.多級指標

#include<stdio.h>

int main(){
    int a = 10;
    int *p = &a;
    int **q = &p;
    // 透過q獲取a的值
    printf("%d\n", **q);
    return 0;
}

image

7.指標陣列與陣列指標

7.1 指向陣列元素的指標

例如定義一個整型陣列和一個指標變數,語句如下。

int a[5]={10,20,30,40,50};
int *aPtr;

這裡的a是一個陣列,它包含了5個整型資料。變數名a就是陣列a的首地址,它與&a[0]等價。如果令aPtr=&a[0]或者
aPtr=a,則aPtr也指向了陣列a的首地址。

也可以在定義指標變數時直接賦值,如以下語句是等價的。

int *aPtr=&a[0];
int *aPtr;
aPtr =&a[0];

與整型、浮點型資料一樣,指標也可以進行算術運算,但含義卻不同。當一個指標加1(或減)1並不是指標值增加(或減少)1,而是使指標指向的位置向後(或向前)移動了一個位置,即加上(或減去)該整數與指標指向物件的大小的乘積。例如對於aPtr+=3,如果一個整數佔用4個位元組,則相加後aPtr=2000+4*3=2012(這裡假設指標的初值是2000)。同樣指標也可以進行自增(++)運算和自減(--)運算。

也可以用一個指標變數減去另一個指標變數。例如,指向陣列元素的指標aPtr的地址是2008,另一個指向陣列元素的指標bPtr的地址是2000,則a=aPtr-bPtr的運算結果就是把從aPtr到bPtr之間的元素個數賦給a,元素個數為(2008-2000)/4=2(假設整數佔用4個位元組)

我們也可以透過指標來引用陣列元素。例如以下語句。

*(aPtr+2);

如果aPtr是指向a[0],即陣列a的首地址,則aPtr+2就是陣列a[2]的地址,*(aPtr+2)就是30。

注意:指向陣列的指標可以進行自增或自減運算,但是陣列名則不能進行自增或自減運算,這是因為陣列名是一個常量指標,它是一個常量,常量值是不能改變的。

#include<stdio.h>

int main(){
    int a[5] = {10, 20, 30, 40, 50};
    int *aPtr, i;
    aPtr = &a[0];
    for(i=0;i<5;i++){ //透過陣列下標引用元素的方式輸出陣列元素
        printf("a[%d]=%d\n", i, a[i]);
    }
    for(i=0;i<5;i++){ //透過陣列名引用元素的方式是輸出陣列元素
        printf("*(a+%d)=%d\n", i, *(a+i));
    }
    for(i=0;i<5;i++){ //透過指標變數下標引用元素的方式輸出陣列元素
        printf("aPtr[%d]=%d\n", i, aPtr[i]);
    }
    for(aPtr=a, i=0; aPtr<a+5; aPtr++, i++){ //透過指標變數偏移的方式輸出陣列元素
        printf("*(aPtr+%d)=%d\n", i, *aPtr);
    }
    return 0;
}

image

7.2 指標陣列

定義:指標陣列其實也是一個陣列,只是陣列中的元素是指標型別的資料。換句話說,指標陣列中的每一個元素都是一個指標變數。

定義指標陣列的方式如下:

int *p[4]

例1:使用指標陣列儲存字串並將字串列印輸出。

#include<stdio.h>

int main(){
    // 定義指標陣列
    const char *s[4] = {"ABC", "DEF", "GHI", "JKL"};
    int n = 4;
    int i;
    const char *aPtr;
    // 方法1:透過陣列名輸出字串
    for(i=0;i<n;i++){
        printf("第%d個字串:%s\n", i+1, s[i]);
    }
    // 方法2:透過指向陣列的指標輸出字串
    for(aPtr=s[0],i=0;i<n;aPtr=s[i]){
        printf("第%d個字串:%s\n", i+1, aPtr);
        i++;
    }
    return 0;
}

執行結果圖示:

result

注:常量與指標間的轉換 warning: ISO C++ forbids converting a string constant to 'char*'

Q:為什麼s[i]列印的是值而不是地址?

A:%s佔位符的特點就是隻要告訴他字串的首地址,就可以讀取整個字串

例2:利用指標陣列實現對一組變數的值按照從小到大排序,排序時交換變數的指標值。

/* 利用指標陣列實現對一組變數的值按照從小到大排序,排序時交換變數的指標值。 */
#include<stdio.h>

int main(){
    int a, b, c, d, e;
    int *s[5] = {&a, &b, &c, &d, &e};
    int i,j;
    int n=5;
    int *p;
    // 使用者輸入5個數(大小任意)
    printf("請輸入5個任意正整數(空格分隔):\n");
    scanf("%d%d%d%d%d", &a, &b, &c, &d, &e);
    printf("排序前:\n");
    for(i=0;i<n;i++){
        printf("%d\n",*s[i]);
    }
    // 排序:氣泡排序
    for(i=0;i<n-1;i++){ // 執行n-1趟
        for(j=0;j<n-i-1;j++){ // 每一趟需要執行n-1-i次比較操作
            if(*s[j] > *s[j+1]){
                p = s[j];
                s[j] = s[j+1];
                s[j+1] = p;
            }
        }
    }
    printf("排序後:\n");
    for(i=0;i<n;i++){
        printf("%d\n",*s[i]);
    }
    return 0;
}

執行結果圖示:

image

7.3 陣列指標

定義:陣列指標是指向陣列的一個指標。如下定義:

int (*p)[4]

其中,p是指向一個擁有4個元素的陣列的指標,陣列中每個元素都為整型。與前面剛剛介紹過的指標陣列做比較,這裡定義的陣列指標多了一對括號,*p兩邊的括號不可以省略。這裡定義的p僅僅是一個指標,不過這個指標有點特殊,這個p指向的是包含4個元素的一維陣列。

陣列指標p與它指向的陣列之間的關係可以用下圖來表示。

image

如果有如下語句:

int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
p=a;

陣列指標p與陣列a中元素之間的關係如圖所示。其中,(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分別儲存的是元素值為1、2、3、4的值。p、p+1和p+2分別指向二維陣列的第一行、第二行和第三行,p+1表示將指標p移動到下一行。

image

*(p+1)+2表示陣列a第1行第2列的元素的地址,即&a[1][2],*(*(p+1)+2)表示a[1][2]的值即7,其中1表示行,2表示列。

image

Q:為什麼(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分別儲存的是元素值為1、2、3、4的值而不是它們的地址呢?

A:指標[i] == *(指標+i)

(*p)[0] == *(*p+0)→p本來表示的是第0行一整行的資料,出現在表示式中將自動轉為指向a[0][0]的指標→*p+0還是指向a[0][0]的指標→*(*p+0)即對地址取值。

下面程式設計輸出以上陣列指標的值和陣列的內容。

#include<stdio.h>

int main(){
    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    int (*p)[4] = a; // 宣告陣列指標p(p是一個指向'內含4個整型元素的陣列'的指標)
    int row, col;
    // 輸出陣列的內容
    for(row=0;row<3;row++){
        for(col=0;col<4;col++){
            printf("a[%d][%d]=%-4d", row, col, *(*(p+row)+col));
        }
        printf("\n");
    }
    // 輸出陣列指標的值
    for(row=0;row<3;row++, p++){
        for(col=0;col<4;col++){
            printf("(*p[%d])[%d]=%p\t", row, col, (*p+col));
        }
        printf("\n");
    }
    return 0;
}

執行結果圖示:

image

註釋:[] == *()→如,p[0] 等價於 *(p+0)

8.指標函式與函式指標

8.1 指標函式

指標函式是指函式的返回值是指標型別的函式。例如,以下是一個指標函式的宣告:

float *func(int a, int b);

func是函式名,前面的'*'表明返回值的型別是指標型別,因為前面的型別識別符號是float,所以返回的指標是指向浮點型的。

例:假設若干個學生的成績存放在二維陣列中,要求輸入學生編號,利用指標函式實現其成績的輸出。

#include<stdio.h>

int *FindAddress(int (*ptrScore)[4], int index);
void Display(int *, int n);

int main(){
    int score[3][4] = {{83, 78, 79, 88}, {71, 88, 92, 63}, {99, 92, 87, 80}};
    int n = 4;
    int row;
    int *p;
    while(1){
        printf("請輸入學生編號(1 or 2 or 3),輸入0退出程式:\n");
        scanf("%d", &row);
        if(row == 0){
            break;
        }else if(row == 1 || row == 2 || row == 3){
            printf("第%d名學生的各科成績分別為:\n", row);
            p = FindAddress(score, row-1);
            Display(p,n);
        }else{
            printf("輸入不合法,請重新輸入!\n");
        }
    }
    return 0;
}

int *FindAddress(int (*ptrScore)[4], int index){
    /*查詢某條學生成績記錄地址函式。透過傳遞的行地址找到要查詢學生成績所在行,並返回該行的首元素地址*/
    int *ptr;
    ptr = *(ptrScore+index);
    return ptr;
}

void Display(int *ptr, int n){
    /*輸出學生成績的實現函式。利用傳遞過來的指標輸出每門課的成績*/
    int col;
    for(col=0; col<n; col++){
        printf("%4d", *(ptr+col));
    }
    printf("\n");
}

image

注:p = FindAddress(score, row-1);二維陣列的陣列名錶示啥?

若a是一維陣列,則a指向的是第一個元素。

若a是二維陣列,也可以將a看成一個一維陣列,那麼其元素是其行向量。則a指向的是第一個行向量。

8.2 函式指標

指標可以指向變數、陣列,也可以指向函式,指向函式的指標就是函式指標。

1)函式指標的呼叫

例1:透過一個函式求兩個數的乘積,並透過函式指標呼叫該函式。

#include<stdio.h>

int Mult(int a, int b);

int main(){
    int a, b;
    int (*func)(int, int);
    printf("請輸入2個數:\n");
    scanf("%d%d", &a, &b);
    /*方法1:函式名呼叫*/
    printf("%d * %d = %d\n", a, b, Mult(a, b));
    /*方法2:函式指標呼叫*/
    func = &Mult; // 因為函式名本身就是地址,所以&可以省略
    printf("%d * %d = %d\n", a, b, func(a, b));
    return 0;
}

int Mult(int x, int y){
    return x*y;
}

image

2)函式指標作為函式引數的使用

例2:利用函式指標作為函式引數,實現選擇排序演算法的升序排列和降序排列。

#include<stdio.h>
void SelectSort(int *, int, int (*)(int, int)); //選擇排序,函式指標作為引數呼叫
int Ascending(int, int); // 是否進行升序排列
int Descending(int, int); // 是否進行降序排列
void swap(int *, int *);
void Display(int a[], int n);

int main(){
    int a[10] = {13, 23, 11, 4, 9, 16, 22, 23, 9, 10};
    printf("排序前數列:\n");
    Display(a, 10);
    printf("升序排列:\n");
    SelectSort(a, 10, Ascending);
    Display(a, 10);
    printf("降序排列:\n");
    SelectSort(a, 10, Descending);
    Display(a, 10);
    return 0;
}

void swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

void Display(int a[], int n){
    int i;
    for(i=0;i<n;i++){
        printf("%4d", a[i]);
    }
    printf("\n");
}

int Ascending(int a, int b){
    if(a>b){
        return 1;
    }else{
        return 0;
    }
}
int Descending(int a, int b){
    if(a<b){
        return 1;
    }else{
        return 0;
    }
}

void SelectSort(int *ptr, int n, int (*compare)(int, int)){
    /*選擇排序基本思想(升序):每一趟排序從n-i個元素中選取關鍵字最小的元素作為有序序列的第i個元素*/
    int i, j, k;
    // 將第i個元素與後面n-i個元素進行比較,將關鍵字最小的元素放在第i個位置
    for(i=0;i<n;i++){
        j=i; // 初始時,關鍵字最小的元素下標為i
        for(k=j+1;k<n;k++){
            if(compare(*(ptr+j), *(ptr+k))){
                j=k;
            }
        }
        if(j!=i){
            swap(ptr+j, ptr+i);
        }
    }
}

image

其中,函式SelectSort(a,N,Ascending)中的引數Asscending是一個函式名,傳遞給函式定義void SelectSort(int *p,int n,int(*compare)(int,int))中的函式指標compare,這樣指標就指向了Asscending。從而可以在執行語句(*compare)(a[j], a[j+1])時呼叫函式Ascending(int a,int b)判斷是否需要交換陣列中兩個相鄰的元素,然後呼叫swap(&a[j],&a[j+1])進行交換。

8.3 函式指標陣列

假設有3個函式f1、f2和f3,可以把這3個函式作為陣列元素存放在一個陣列中,需要定義一個指向函式的指標陣列指向這
三個函式,程式碼如下:

void (*f[3])(int)={f1,f2,f3};

f是包含3個指向函式指標元素的陣列,f[0]、f[1]和f[2]分別指向函式f1、f2和f3。透過函式指標f呼叫函式的形式如下。

f[n](m); /*n和m都是正整數*/

例:宣告一個指向函式的指標陣列,並透過指標呼叫函式。

#include<stdio.h>

void f1(int n); /*函式f1宣告*/
void f2(int n); /*函式f2宣告*/
void f3(int n); /*函式f3宣告*/

int main(){
    void (*f[3])(int)={f1,f2,f3}; /*宣告指向函式的指標陣列*/
    int flag;
    printf("呼叫函式請輸入1、2或者3,結束程式請輸入0。\n");
    scanf("%d",&flag);
    while(flag){
        if(flag==1||flag==2||flag==3){
            f[flag-1](flag); /*透過函式指標呼叫陣列中的函式*/
            printf("請輸入1、2或者3,輸入0結束程式.\n");
            scanf("%d",&flag);
        }else{
            printf("請輸入一個合法的數(1~3),輸入0結束程式.\n");
            scanf("%d",&flag);
        }
    }
    printf("程式結束.\n");
    return 0;
}
void f1(int n) /*函式f1的定義*/
{
    printf("函式f%d:呼叫第%d個函式!\n",n,n);
}
void f2(int n) /*函式f2的定義*/
{
    printf("函式f%d:呼叫第%d個函式!\n",n,n);
}
void f3(int n) /*函式f3的定義*/
{
    printf("函式f%d:呼叫第%d個函式!\n",n,n);
}

image

函式指標不能執行像f+1、f++、f--等運算。

相關文章