C語言指針學習總結

来源:https://www.cnblogs.com/veeupup/archive/2020/02/28/12380426.html
-Advertisement-
Play Games

[TOC] 這裡對 C 語言的指針進行比較詳細的整理總結,參考網路上部分資料整理如下。 指針概念 電腦中所有的數據都必須放在記憶體中,不同類型的數據占用的位元組數不一樣,例如 int 占用4個位元組,char 占用1個位元組。為了正確地訪問這些數據,必須為每個位元組都編上號碼,就像門牌號、身份證號一樣,每個 ...


目錄

這裡對 C 語言的指針進行比較詳細的整理總結,參考網路上部分資料整理如下。

指針概念

電腦中所有的數據都必須放在記憶體中,不同類型的數據占用的位元組數不一樣,例如 int 占用4個位元組,char 占用1個位元組。為了正確地訪問這些數據,必須為每個位元組都編上號碼,就像門牌號、身份證號一樣,每個位元組的編號是唯一的,根據編號可以準確地找到某個位元組。

我們將記憶體中位元組的編號稱為地址(Address)或指針(Pointer)。地址從 0 開始依次增加,對於 32 位環境,程式能夠使用的記憶體為 4GB,最小的地址為 0,最大的地址為 0XFFFFFFFF。

輸出一個地址:

int a = 100;
char str[20] = "tanweime.com";
printf("%#X, %p\n", &a, str);

---
運行結果:
0XE42523AC, 0XE4252390

%#X%p 表示以十六進位形式輸出,並附帶首碼0X。a 是一個變數,用來存放整數,需要在前面加&來獲得它的地址;str 本身就表示字元串的首地址,不需要加&

C語言中有一個控制符%p,專門用來以十六進位形式輸出地址,不過 %p 的輸出格式並不統一,有的編譯器帶0x首碼,有的不帶

一切都是地址

C語言用變數來存儲數據,用函數來定義一段可以重覆使用的代碼,它們最終都要放到記憶體中才能供 CPU 使用。

數據和代碼都以二進位的形式存儲在記憶體中,電腦無法從格式上區分某塊記憶體到底存儲的是數據還是代碼。當程式被載入到記憶體後,操作系統會給不同的記憶體塊指定不同的許可權,擁有讀取和執行許可權的記憶體塊就是代碼,而擁有讀取和寫入許可權(也可能只有讀取許可權)的記憶體塊就是數據。

CPU 只能通過地址來取得記憶體中的代碼和數據,程式在執行過程中會告知 CPU 要執行的代碼以及要讀寫的數據的地址。如果程式不小心出錯,或者開發者有意為之,在 CPU 要寫入數據時給它一個代碼區域的地址,就會發生記憶體訪問錯誤。這種記憶體訪問錯誤會被硬體和操作系統攔截,強製程序崩潰,程式員沒有輓救的機會。

CPU 訪問記憶體時需要的是地址,而不是變數名和函數名!變數名和函數名只是地址的一種助記符,當源文件被編譯和鏈接成可執行程式後,它們都會被替換成地址。編譯和鏈接過程的一項重要任務就是找到這些名稱所對應的地址。

指針變數

數據在記憶體中的地址也稱為指針,如果一個變數存儲了一份數據的指針,我們就稱它為指針變數。

在C語言中,允許用一個變數來存放指針,這種變數稱為指針變數。指針變數的值就是某份數據的地址,這樣的一份數據可以是數組、字元串、函數,也可以是另外的一個普通變數或指針變數。

現在假設有一個 char 類型的變數 c,它存儲了字元 'K'(ASCII碼為十進位數 75),並占用了地址為 0X11A 的記憶體(地址通常用十六進位表示)。另外有一個指針變數 p,它的值為 0X11A,正好等於變數 c 的地址,這種情況我們就稱 p 指向了 c,或者說 p 是指向變數 c 的指針。

定義指針變數

定義指針變數與定義普通變數非常類似,不過要在變數名前面加星號*,格式為:

datatype *name;

或者

datatype *name = value;

*表示這是一個指針變數,datatype表示該指針變數所指向的數據的類型 。例如:

int *p1;

p1 是一個指向 int 類型數據的指針變數,至於 p1 究竟指向哪一份數據,應該由賦予它的值決定。再如:

int a = 100;int *p_a = &a;

在定義指針變數 p_a 的同時對它進行初始化,並將變數 a 的地址賦予它,此時 p_a 就指向了 a。值得註意的是,p_a 需要的一個地址,a 前面必須要加取地址符&,否則是不對的。

和普通變數一樣,指針變數也可以被多次寫入,只要你想,隨時都能夠改變指針變數的值,請看下麵的代碼:

//定義普通變數
float a = 99.5, b = 10.6;char c = '@', d = '#';
//定義指針變數
float *p1 = &a;char *p2 = &c;
//修改指針變數的值
p1 = &b;p2 = &d;

*是一個特殊符號,表明一個變數是指針變數,定義 p1、p2 時必須帶*。而給 p1、p2 賦值時,因為已經知道了它是一個指針變數,就沒必要多此一舉再帶上*,後邊可以像使用普通變數一樣來使用指針變數。也就是說,定義指針變數時必須帶*,給指針變數賦值時不能帶*

指針變數也可以連續定義,例如:

int *a, *b, *c;  //a、b、c 的類型都是 int*

註意每個變數前面都要帶*。如果寫成下麵的形式,那麼只有 a 是指針變數,b、c 都是類型為 int 的普通變數:

int *a, b, c;

通過指針變數取得數據

指針變數存儲了數據的地址,通過指針變數能夠獲得該地址上的數據,格式為:

*pointer;

這裡的*稱為指針運算符,用來取得某個地址上的數據,請看下麵的例子:

#include <stdio.h>
int main(){    
        int a = 15;    
      int *p = &a;    
      printf("%d, %d\n", a, *p);  //兩種方式都可以輸出a的值    
return 0;}

運行結果:
15, 15

假設 a 的地址是 0X1000,p 指向 a 後,p 本身的值也會變為 0X1000,p 表示獲取地址 0X1000 上的數據,也即變數 a 的值。從運行結果看,p 和 a 是等價的。

上節我們說過,CPU 讀寫數據必須要知道數據在記憶體中的地址,普通變數和指針變數都是地址的助記符,雖然通過 p 和 a 獲取到的數據一樣,但它們的運行過程稍有不同:a 只需要一次運算就能夠取得數據,而 p 要經過兩次運算,多了一層“間接”。

假設變數 a、p 的地址分別為 0X1000、0XF0A0,它們的指向關係如下圖所示:

img

程式被編譯和鏈接後,a、p 被替換成相應的地址。使用 *p 的話,要先通過地址 0XF0A0 取得變數 p 本身的值,這個值是變數 a 的地址,然後再通過這個值取得變數 a 的數據,前後共有兩次運算;而使用 a 的話,可以通過地址 0X1000 直接取得它的數據,只需要一步運算。

也就是說,使用指針是間接獲取數據,使用變數名是直接獲取數據,前者比後者的代價要高。

指針除了可以獲取記憶體上的數據,也可以修改記憶體上的數據,例如:

int a = 15, b = 99, c = 222;
int *p = &a; //定義指針變數
*p = b;      //通過指針變數修改記憶體上的數據
c = *p;      //通過指針變數獲取記憶體上的數據
printf("%d, %d, %d, %d\n", a, b, c, *p);

運行結果:
99, 99, 99, 99

*p 代表的是 a 中的數據,它等價於 a,可以將另外的一份數據賦值給它,也可以將它賦值給另外的一個變數。

*在不同的場景下有不同的作用:*可以用在指針變數的定義中,表明這是一個指針變數,以和普通變數區分開;使用指針變數時在前面加*表示獲取指針指向的數據,或者說表示的是指針指向的數據本身。

也就是說,定義指針變數時的*和使用指針變數時的*意義完全不同。以下麵的語句為例:

int *p = &a;*p = 100;

第1行代碼中*用來指明 p 是一個指針變數,第2行代碼中*用來獲取指針指向的數據。

需要註意的是,給指針變數本身賦值時不能加*。修改上面的語句:

int *p;p = &a;*p = 100;

第2行代碼中的 p 前面就不能加*

指針變數也可以出現在普通變數能出現的任何表達式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5;  //表示把x的內容加5並賦給y,*px+5相當於(*px)+5
y = ++*px;  //px的內容加上1之後賦給y,++*px相當於++(*px)
y = *px++;  //相當於y=(*px)++
py = px;  //把一個指針的值賦給另一個指針

關於 * 和 & 的謎題

假設有一個 int 類型的變數 a,pa 是指向它的指針,那麼*&a&*pa分別是什麼意思呢?

*&a可以理解為*(&a)&a表示取變數 a 的地址(等價於 pa),*(&a)表示取這個地址上的數據(等價於 *pa),繞來繞去,又回到了原點,*&a仍然等價於 a。

&*pa可以理解為&(*pa)*pa表示取得 pa 指向的數據(等價於 a),&(*pa)表示數據的地址(等價於 &a),所以&*pa等價於 pa。

對星號*的總結

在我們目前所學到的語法中,星號*主要有三種用途:

  • 表示乘法,例如int a = 3, b = 5, c; c = a * b;,這是最容易理解的。
  • 表示定義一個指針變數,以和普通變數區分開,例如int a = 100; int *p = &a;
  • 表示獲取指針指向的數據,是一種間接操作,例如int a, b, *p = &a; *p = 100; b = *p;

指針變數的運算

指針變數保存的是地址,本質上是一個整數,可以進行部分運算,例如加法、減法、比較等,請看下麵的代碼:

#include <stdio.h>
int main(){
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //加法運算
    pa++; pb++; pc++;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //減法運算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //比較運算
    if(pa == paa){
        printf("%d\n", *paa);
    }else{
        printf("%d\n", *pa);
    }
    return 0;
}
--------
運行結果:
  &a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
  pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
  pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
  pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
  2686784

從運算結果可以看出:pa、pb、pc 每次加 1,它們的地址分別增加 4、8、1,正好是 int、double、char 類型的長度;減 2 時,地址分別減少 8、16、2,正好是 int、double、char 類型長度的 2 倍。

我們知道,數組中的所有元素在記憶體中是連續排列的,如果一個指針指向了數組中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指針的加減運算就具有了現實的意義。

數組指針

數組(Array)是一系列具有相同類型的數據的集合,每一份數據叫做一個數組元素(Element)。數組中的所有元素在記憶體中是連續排列的,整個數組占用的是一塊記憶體。以int arr[] = { 99, 15, 100, 888, 252 };為例,該數組在記憶體中的分佈如下圖所示:

img

定義數組時,要給出數組名和數組長度,數組名可以認為是一個指針,它指向數組的第 0 個元素。在C語言中,我們將第 0 個元素的地址稱為數組的首地址。以上面的數組為例,下圖是 arr 的指向:

img

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求數組長度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等價於arr[i]
    }
    printf("\n");
    return 0;
}
----
運行結果:
  99  15  100  888  252

第 5 行代碼用來求數組的長度,sizeof(arr) 會獲得整個數組所占用的位元組數,sizeof(int) 會獲得一個數組元素所占用的位元組數,它們相除的結果就是數組包含的元素個數,也即數組長度。

第 8 行代碼中我們使用了*(arr+i)這個表達式,arr 是數組名,指向數組的第 0 個元素,表示數組首地址, arr+i 指向數組的第 i 個元素,*(arr+i) 表示取第 i 個元素的數據,它等價於 arr[i]。

arr 是int*類型的指針,每次加 1 時它自身的值會增加 sizeof(int),加 i 時自身的值會增加 sizeof(int) * i

我們也可以定義一個指向數組的指針,例如:

int arr[] = { 99, 15, 100, 888, 252 };int *p = arr;

arr 本身就是一個指針,可以直接賦值給指針變數 p。arr 是數組第 0 個元素的地址,所以int *p = arr;也可以寫作int *p = &arr[0];。也就是說,arr、p、&arr[0] 這三種寫法都是等價的,它們都指向數組第 0 個元素,或者說指向數組的開頭。

如果一個指針指向了數組,我們就稱它為數組指針(Array Pointer)。

數組指針指向的是數組中的一個具體元素,而不是整個數組,所以數組指針的類型和數組元素的類型有關,上面的例子中,p 指向的數組元素是 int 類型,所以 p 的類型必須也是int *

反過來想,p 並不知道它指向的是一個數組,p 只知道它指向的是一個整數,究竟如何使用 p 取決於程式員的編碼。

更改上面的代碼,使用數組指針來遍曆數組元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *(p+i) );
    }
    printf("\n");
    return 0;
}

引入數組指針後,我們就有兩種方案來訪問數組元素了,一種是使用下標,另外一種是使用指針。

1) 使用下標

也就是採用 arr[i] 的形式訪問數組元素。如果 p 是指向數組 arr 的指針,那麼也可以使用 p[i] 來訪問數組元素,它等價於 arr[i]。

2) 使用指針

也就是使用 (p+i) 的形式訪問數組元素。另外數組名本身也是指針,也可以使用 (arr+i) 來訪問數組元素,它等價於 *(p+i)。

關於數組指針的謎題

假設 p 是指向數組 arr 中第 n 個元素的指針,那麼 p++、++p、(*p)++ 分別是什麼意思呢?

p++ 等價於 (p++),表示先取得第 n 個元素的值,再將 p 指向下一個元素,上面已經進行了詳細講解。

++p 等價於 (++p),會先進行 ++p 運算,使得 p 的值增加,指向下一個元素,整體上相當於 *(p+1),所以會獲得第 n+1 個數組元素的值。

(*p)++ 就非常簡單了,會先取得第 n 個元素的值,再對該元素的值加 1。假設 p 指向第 0 個元素,並且第 0 個元素的值為 99,執行完該語句後,第 0 個元素的值就會變為 100。

字元串指針

C語言中沒有特定的字元串類型,我們通常是將字元串放在一個字元數組中:

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "tanweime";
    int len = strlen(str), i;
    //直接輸出字元串
    printf("%s\n", str);
    //每次輸出一個字元
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

除了字元數組,C語言還支持另外一種表示字元串的方法,就是直接使用一個指針指向字元串,例如:

char *str = "tanweime";

或者:

char *str;str = "tanweime";

字元串中的所有字元在記憶體中是連續排列的,str 指向的是字元串的第 0 個字元;我們通常將第 0 個字元的地址稱為字元串的首地址。字元串中每個字元的類型都是char,所以 str 的類型也必須是char *

下麵的例子演示瞭如何輸出這種字元串:

#include <stdio.h>
#include <string.h>
int main(){
    char *str = "tanweime";
    int len = strlen(str), i;
   
    //直接輸出字元串
    printf("%s\n", str);
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    //使用str[i]
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

這一切看起來和字元數組是多麼地相似,它們都可以使用%s輸出整個字元串,都可以使用*[ ]獲取單個字元,這兩種表示字元串的方式是不是就沒有區別了呢?

有!它們最根本的區別是在記憶體中的存儲區域不一樣,字元數組存儲在全局數據區或棧區,第二種形式的字元串存儲在常量區。全局數據區和棧區的字元串(也包括其他數據)有讀取和寫入的許可權,而常量區的字元串(也包括其他數據)只有讀取許可權,沒有寫入許可權。

記憶體許可權的不同導致的一個明顯結果就是,字元數組在定義後可以讀取和修改每個字元,而對於第二種形式的字元串,一旦被定義後就只能讀取不能修改,任何對它的賦值都是錯誤的。

我們將第二種形式的字元串稱為字元串常量,意思很明顯,常量只能讀取不能寫入。

指針作為函數參數

在C語言中,函數的參數不僅可以是整數、小數、字元等具體的數據,還可以是指向它們的指針。用指針變數作函數參數可以將函數外部的地址傳遞到函數內部,使得在函數內部可以操作函數外部的數據,並且這些數據不會隨著函數的結束而被銷毀。

像數組、字元串、動態分配的記憶體等都是一系列數據的集合,沒有辦法通過一個參數全部傳入函數內部,只能傳遞它們的指針,在函數內部通過指針來影響這些數據集合。

有的時候,對於整數、小數、字元等基本類型數據的操作也必須要藉助指針,一個典型的例子就是交換兩個變數的值。

#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //臨時變數
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

調用 swap() 函數時,將變數 a、b 的地址分別賦值給 p1、p2,這樣 p1、p2 代表的就是變數 a、b 本身,交換 p1、p2 的值也就是交換 a、b 的值。函數運行結束後雖然會將 p1、p2 銷毀,但它對外部 a、b 造成的影響是“持久化”的,不會隨著函數的結束而“恢複原樣”。

需要註意的是臨時變數 temp,它的作用特別重要,因為執行*p1 = *p2;語句後 a 的值會被 b 的值覆蓋,如果不先將 a 的值保存起來以後就找不到了。

這就好比拿來一瓶可樂和一瓶雪碧,要想把可樂倒進雪碧瓶、把雪碧倒進可樂瓶裡面,就必須先找一個杯子,將兩者之一先倒進杯子裡面,再從杯子倒進瓶子裡面。這裡的杯子,就是一個“臨時變數”,雖然只是倒倒手,但是也不可或缺。

用數組作函數參數

數組是一系列數據的集合,無法通過參數將它們一次性傳遞到函數內部,如果希望在函數內部操作數組,必須傳遞數組指針。下麵的例子定義了一個函數 max(),用來查找數組中值最大的元素:

#include <stdio.h>
int max(int *intArr, int len){
    int i, maxValue = intArr[0];  //假設第0個元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}
int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //讀取用戶輸入的數據並賦值給數組元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}
--------
  運行結果:
12 55 30 8 93 27↙
Max value is 93!

參數 intArr 僅僅是一個數組指針,在函數內部無法通過這個指針獲得數組長度,必須將數組長度作為函數參數傳遞到函數內部。數組 nums 的每個元素都是整數,scanf() 在讀取用戶輸入的整數時,要求給出存儲它的記憶體的地址,nums+i就是第 i 個數組元素的地址。

用數組做函數參數時,參數也能夠以“真正”的數組形式給出。例如對於上面的 max() 函數,它的參數可以寫成下麵的形式:

int max(int intArr[], int len){
    int i, maxValue = intArr[0];  //假設第0個元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

int intArr[]雖然定義了一個數組,但沒有指定數組長度,好像可以接受任意長度的數組。

實際上這兩種形式的數組定義都是假象,不管是int intArr[6]還是int intArr[]都不會創建一個數組出來,編譯器也不會為它們分配記憶體,實際的數組是不存在的,它們最終還是會轉換為int *intArr這樣的指針。這就意味著,兩種形式都不能將數組的所有元素“一股腦”傳遞進來,大家還得規規矩矩使用數組指針。

int intArr[6]這種形式只能說明函數期望用戶傳遞的數組有 6 個元素,並不意味著數組只能有 6 個元素,真正傳遞的數組可以有少於或多於 6 個的元素。

需要強調的是,不管使用哪種方式傳遞數組,都不能在函數內部求得數組長度,因為 intArr 僅僅是一個指針,而不是真正的數組,所以必須要額外增加一個參數來傳遞數組長度。

C語言為什麼不允許直接傳遞數組的所有元素,而必須傳遞數組指針呢?

參數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的數據複製到另一塊記憶體上。

對於像 int、float、char 等基本類型的數據,它們占用的記憶體往往只有幾個位元組,對它們進行記憶體拷貝非常快速。而數組是一系列數據的集合,數據的數量沒有限制,可能很少,也可能成千上萬,對它們進行記憶體拷貝有可能是一個漫長的過程,會嚴重拖慢程式的效率,為了防止技藝不佳的程式員寫出低效的代碼,C語言沒有從語法上支持數據集合的直接賦值。

除了C語言,C++、Java、Python 等其它語言也禁止對大塊記憶體進行拷貝,在底層都使用類似指針的方式來實現。

指針作為返回值

C語言允許函數的返回值是一個指針(地址),我們將這樣的函數稱為指針函數。下麵的例子定義了一個函數 strlong(),用來返回兩個字元串中較長的一個

#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
    if(strlen(str1) >= strlen(str2)){
        return str1;
    }else{
        return str2;
    }
}
int main(){
    char str1[30], str2[30], *str;
    gets(str1);
    gets(str2);
    str = strlong(str1, str2);
    printf("Longer string: %s\n", str);
    return 0;
}

用指針作為函數返回值時需要註意的一點是,函數運行結束後會銷毀在它內部定義的所有局部數據,包括局部變數、局部數組和形式參數,函數返回的指針請儘量不要指向這些數據,C語言沒有任何機制來保證這些數據會一直有效,它們在後續使用過程中可能會引發運行時錯誤。請看下麵的例子:

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;
}
int main(){
    int *p = func(), n;
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

前面我們說函數運行結束後會銷毀所有的局部數據,這個觀點並沒錯,大部分C語言教材也都強調了這一點。但是,這裡所謂的銷毀並不是將局部數據所占用的記憶體全部抹掉,而是程式放棄對它的使用許可權,棄之不理,後面的代碼可以隨意使用這塊記憶體。對於上面的兩個例子,func() 運行結束後 n 的記憶體依然保持原樣,值還是 100,如果使用及時也能夠得到正確的數據,如果有其它函數被調用就會覆蓋這塊記憶體,得到的數據就失去了意義。

二級指針

指針可以指向一份普通類型的數據,例如 int、double、char 等,也可以指向一份指針類型的數據,例如 int 、double 、char * 等。

如果一個指針指向的是另外一個指針,我們就稱它為二級指針,或者指向指針的指針。

假設有一個 int 類型的變數 a,p1是指向 a 的指針變數,p2 又是指向 p1 的指針變數,它們的關係如下圖所示:
img
將這種關係轉換為C語言代碼:

int a =100;
int *p1 = &a;
int **p2 = &p1;

指針變數也是一種變數,也會占用存儲空間,也可以使用&獲取它的地址。C語言不限制指針的級數,每增加一級指針,在定義指針變數時就得增加一個星號*。p1 是一級指針,指向普通類型的數據,定義時有一個*;p2 是二級指針,指向一級指針 p1,定義時有兩個*

如果我們希望再定義一個三級指針 p3,讓它指向 p2,那麼可以這樣寫:

int ***p3 = &p2;

四級指針也是類似的道理:

int ****p4 = &p3;

實際開發中會經常使用一級指針和二級指針,幾乎用不到高級指針。

想要獲取指針指向的數據時,一級指針加一個*,二級指針加兩個*,三級指針加三個*,以此類推,請看代碼:

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}
------
  100, 100, 100, 100
&p2 = 0XE19322F8, p3 = 0XE19322F8
&p1 = 0XE1932300, p2 = 0XE1932300, *p3 = 0XE1932300
 &twa = 0XE193230C, p1 = 0XE193230C, *p2 = 0XE193230C, **p3 = 0XE193230C

以三級指針 p3 為例來分析上面的代碼。***p3等價於*(*(*p3))p3 得到的是 p2 的值,也即 p1 的地址;(p3) 得到的是 p1 的值,也即 a 的地址;經過三次“取值”操作後,((p3)) 得到的才是 a 的值。

假設 a、p1、p2、p3 的地址分別是 0X00A0、0X1000、0X2000、0X3000,它們之間的關係可以用下圖來描述:
img
方框裡面是變數本身的值,方框下麵是變數的地址。

指針數組

如果一個數組中的所有元素保存的都是指針,那麼我們就稱它為指針數組。指針數組的定義形式一般為:

dataType *arrayName[length];

[ ]的優先順序高於*,該定義形式應該理解為:

dataType *(arrayName[length]);

括弧裡面說明arrayName是一個數組,包含了length個元素,括弧外面說明每個元素的類型為dataType *

除了每個元素的數據類型不同,指針數組和普通數組在其他方面都是一樣的,下麵是一個簡單的例子:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定義一個指針數組
    int *arr[3] = {&a, &b, &c};//也可以不指定長度,直接寫作 int *parr[]
    //定義一個指向指針數組的指針
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}
------
  運行結果:
  16, 932, 100
    16, 932, 100

指針數組還可以和字元串數組結合使用,請看下麵的例子:

#include <stdio.h>
int main(){
    char *str[3] = {
        "tanwei",
        "譚巍",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

需要註意的是,字元數組 str 中存放的是字元串的首地址,不是字元串本身,字元串本身位於其他的記憶體區域,和字元數組是分開的。

也只有當指針數組中每個元素的類型都是char *時,才能像上面那樣給指針數組賦值,其他類型不行。

為了便於理解,可以將上面的字元串數組改成下麵的形式,它們都是等價的。

#include <stdio.h>
int main(){
    char *str0 = "tanwei";
    char *str1 = "譚巍";
    char *str2 = "C Language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

指針與二維數組

二維數組在概念上是二維的,有行和列,但在記憶體中所有的數組元素都是連續排列的,它們之間沒有“縫隙”。以下麵的二維數組 a 為例:

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

從概念上理解,a 的分佈像一個矩陣:

0   1   2   3
4   5   6   7
8   9  10  11

但在記憶體中,a 的分佈是一維線性的,整個數組占用一塊連續的記憶體:
img
C語言中的二維數組是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最後存放 a[2] 行;每行中的 4 個元素也是依次存放。數組 a 為 int 類型,每個元素占用 4 個位元組,整個數組共占用 4×(3×4) = 48 個位元組。

C語言允許把一個二維數組分解成多個一維數組來處理。對於數組 a,它可以分解成三個一維數組,即 a[0]、a[1]、a[2]。每一個一維數組又包含了 4 個元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

假設數組 a 中第 0 個元素的地址為 1000,那麼每個一維數組的首地址如下圖所示:
img

為了更好的理解指針和二維數組的關係,我們先來定義一個指向 a 的指針變數 p:

int (*p)[4] = a;

括弧中的*表明 p 是一個指針,它指向一個數組,數組的類型為int [4],這正是 a 所包含的每個一維數組的類型。

[ ]的優先順序高於*( )是必須要加的,如果赤裸裸地寫作int *p[4],那麼應該理解為int *(p[4]),p 就成了一個指針數組,而不是二維數組指針。

對指針進行加法(減法)運算時,它前進(後退)的步長與它指向的數據類型有關,p 指向的數據類型是int [4],那麼p+1就前進 4×4 = 16 個位元組,p-1就後退 16 個位元組,這正好是數組 a 所包含的每個一維數組的長度。也就是說,p+1會使得指針指向二維數組的下一行,p-1會使得指針指向數組的上一行。

數組名 a 在表達式中也會被轉換為和 p 等價的指針!

下麵我們就來探索一下如何使用指針 p 來訪問二維數組中的每個元素。按照上面的定義:
1) p指向數組 a 的開頭,也即第 0 行;p+1前進一行,指向第 1 行。

2) *(p+1)表示取地址上的數據,也就是整個第 1 行數據。註意是一行數據,是多個數據,不是第 1 行中的第 0 個元素,下麵的運行結果有力地證明瞭這一點:

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}
---
  16

3) *(p+1)+1表示第 1 行第 1 個元素的地址。如何理解呢?

*(p+1)單獨使用時表示的是第 1 行數據,放在表達式中會被轉換為第 1 行數據的首地址,也就是第 1 行第 0 個元素的地址,因為使用整行數據沒有實際的含義,編譯器遇到這種情況都會轉換為指向該行第 0 個元素的指針;就像一維數組的名字,在定義時或者和 sizeof、& 一起使用時才表示整個數組,出現在表達式中就會被轉換為指向數組第 0 個元素的指針。

4) *(*(p+1)+1)表示第 1 行第 1 個元素的值。很明顯,增加一個 * 表示取地址上的數據。

根據上面的結論,可以很容易推出以下的等價關係:

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }
    return 0;
}

指針數組和二維數組指針的區別

指針數組和二維數組指針在定義時非常相似,只是括弧的位置不同:

int *(p1[5]);  //指針數組,可以去掉括弧直接寫作 int *p1[5];int (*p2)[5];  //二維數組指針,不能去掉括弧

指針數組和二維數組指針有著本質上的區別:指針數組是一個數組,只是每個元素保存的都是指針,以上面的 p1 為例,在32位環境下它占用 4×5 = 20 個位元組的記憶體。二維數組指針是一個指針,它指向一個二維數組,以上面的 p2 為例,它占用 4 個位元組的記憶體。

函數指針

一個函數總是占用一段連續的記憶體區域,函數名在表達式中有時也會被轉換為該函數所在記憶體區域的首地址,這和數組名非常類似。我們可以把函數的這個首地址(或稱入口地址)賦予一個指針變數,使指針變數指向函數所在的記憶體區域,然後通過指針變數就可以找到並調用該函數。這種指針就是函數指針。

函數指針的定義形式為:

returnType (*pointerName)(param list);

returnType 為函數返回值類型,pointerNmae 為指針名稱,param list 為函數參數列表。參數列表中可以同時給出參數的類型和名稱,也可以只給出參數的類型,省略參數的名稱,這一點和函數原型非常類似。

註意( )的優先順序高於*,第一個括弧不能省略,如果寫作returnType *pointerName(param list);就成了函數原型,它表明函數的返回值類型為returnType *

#include <stdio.h>
//返回兩個數中較大的一個
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定義函數指針
    int (*pmax)(int, int) = max;  //也可以寫作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

總結

指針(Pointer)就是記憶體的地址,C語言允許用一個變數來存放指針,這種變數稱為指針變數。指針變數可以存放基本類型數據的地址,也可以存放數組、函數以及其他指針變數的地址。

程式在運行過程中需要的是數據和指令的地址,變數名、函數名、字元串名和數組名在本質上是一樣的,它們都是地址的助記符:在編寫代碼的過程中,我們認為變數名錶示的是數據本身,而函數名、字元串名和數組名錶示的是代碼塊或數據塊的首地址;程式被編譯和鏈接後,這些名字都會消失,取而代之的是它們對應的地址。

定 義 含 義
int *p; p 可以指向 int 類型的數據,也可以指向類似 int arr[n] 的數組。
int **p; p 為二級指針,指向 int * 類型的數據。
int *p[n]; p 為指針數組。[ ] 的優先順序高於 ,所以應該理解為 int (p[n]);
int (*p)[n]; p 為二維數組指針。
int *p(); p 是一個函數,它的返回值類型為 int *。
int (*p)(); p 是一個函數指針,指向原型為 int func() 的函數。

1) 指針變數可以進行加減運算,例如p++p+ip-=i。指針變數的加減運算並不是簡單的加上或減去一個整數,而是跟指針指向的數據類型有關。

2) 給指針變數賦值時,要將一份數據的地址賦給它,不能直接賦給一個整數,例如int *p = 1000;是沒有意義的,使用過程中一般會導致程式崩潰。

3) 使用指針變數之前一定要初始化,否則就不能確定指針指向哪裡,如果它指向的記憶體沒有使用許可權,程式就崩潰了。對於暫時沒有指向的指針,建議賦值NULL

4) 兩個指針變數可以相減。如果兩個指針變數指向同一個數組中的某個元素,那麼相減的結果就是兩個指針之間相差的元素個數。

5) 數組也是有類型的,數組名的本意是表示一組類型相同的數據。在定義數組時,或者和 sizeof、& 運算符一起使用時數組名才表示整個數組,表達式中的數組名會被轉換為一個指向數組的指針。

歡迎訪問我的博客github!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、 1.讓SortedSet集合做到排序還有另一種方式:java.util.Comparator; 2.單獨編寫一個比較器 package com.bjpowernode.java_learning; import java.util.*; ​ public class D90_1_SortedS ...
  • 明確學習目標,不急於求成 當下是一個喧囂、浮躁的時代。我們總是被生活中大量涌現的熱點所吸引,幾乎沒有深度閱讀和思考的時間和機會。我始終認為,學習是需要沉下心來慢慢鑽研的,是長 期的;同時,學習不應該被賦予太多的功利色彩。一個Python 程式員的成長路線圖應該是這樣子的:基礎語法–>語感訓練–>課題 ...
  • python3-cookbook中每個小節以問題、解決方案和討論三個部分探討了Python3在某類問題中的最優解決方式,或者說是探討Python3本身的數據結構、函數、類等特性在某類問題上如何更好地使用。這本書對於加深Python3的理解和提升Python編程能力的都有顯著幫助,特別是對怎麼提高Py ...
  • 基於JSP+Servlet開發超市日常管理系統: 開發環境: Windows操作系統開發工具: Eclipse+Jdk+Tomcat+MYSQL資料庫題目:根據超市日常所做的工作描述,結合超市經理提出的超市管理需求,本超市管理系統主要提供以下功能:超市經理能夠進行商品的添加、商品的查看,商品的信息修 ...
  • 基於JSP+Servlet開發學生成績管理系統 開發環境: Windows操作系統開發工具: MyEclipse+Jdk+Tomcat+Mysql資料庫運行效果圖 源碼及原文鏈接:https://javadao.xyz/forum.php?mod=viewthread&tid=57 ...
  • 異常處理涉及的關鍵字: try: 理解它是掃描器,將可能出現異常的代碼放入其中; 如果在執行的過程中出現異常對象了,掃描器會立即察覺到此異常對象, 但是它沒有處理它的能力,所以會將異常對象給到except(捕獲器)進行處理 except: 理解它是捕獲器,後面可以定義異常類型,並且和as關鍵字配合使 ...
  • swap(a,b) 用於交換a,b兩個變數的值; template void swap ( T& a, T& b ) { T c(a); a=b; b=c; } reverse() reverse函數反轉區間的數據 sort() sort函數對區間的函數進行排序,內部是實現使用快速排序法。 max( ...
  • 概述 對異常的理解 程式在運行過程中出現不正常情況。是對問題的描述,將問題進行對象的封裝。 異常的由來 問題也是現實生活中一個具體的事物,也可以通過Java的類的形式進行描述,並封裝成對象。 對於問題的劃分 一種是嚴重的問題,一種是非嚴重的問題。 + 對於嚴重的:Java通過Error類進行描述。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...