Day06 郝斌C語言自學視頻之C語言的指針

這篇筆記主要是介紹C語言中的指針,包括指針的重要性、定義、使用等。閱讀本文預計需要 10 分鐘。

指針的重要性

指針是繼流程控制函數之後,又一個重點。可以說指針是 C 語言的靈魂

指針的重要性具體表現

  1. 表示一些複雜的數據結構。
  2. 快遞的傳遞數據,減少內存的耗用。【重點】
  3. 使函數返回一個以上的值。【重點】
  4. 能直接訪問硬件。
  5. 能夠方便處理字符串。
  6. 是理解面嚮對象語言中引用的基礎。

指針的定義

在介紹指針定義時,我們先看一下地址的概念。

地址:地址是內存單元的編號。它是從 0 開始的非負整數。對於32位系統,它的範圍是:4 G [0–(4G-1)]。4 G即2^32。

指針:指針就是地址,地址就是指針。

指針變量:指針變量就是存放內存單元編號的變量,或者說指針變量就是存放地址的變量。

指針和指針變量是兩個不同的概念。但是要注意,通常我們敘述時,會把指針變量簡稱爲指針,實際他們含義並不一樣。

指針的本質就是一個操作受限的非負整數

指針的使用

基本類型指針

格式:
  指針變量類型  指針變量名

例:

/*
    時間:2020年2月24日13:23:59
    功能:
        測試基本類型指針的使用
*/

# include <stdio.h>

int main(void)
{
    int * p;
    int i = 3;
    int j;

    p = &i;  // OK
    j = *p;
    printf("i = %d, j= %d\n", i, j);

    return 0;
}

/*
    在VSCode中的輸出結果是:
--------------------------
i = 3, j= 3
--------------------------
*/

說明
對於語句**int * p;**:

  1. p 是變量的名字, int * 表示 p 變量存放的是 int 類型變量的地址。
  2. int * p; 不表示定義了一個名字爲 *p 的變量。
  3. int * p; 應該這樣理解: p 是變量名,p 變量的數據類型是 int * 類型。所謂 int * 類型,實際就是存放 int 變量地址的類型。

對於語句**p = &i;**:

  1. p 保存了 i 的地址,因此 p 指向 i
  2. p 不是 i, i 也不是 p, 更準確的說,修改 p 的值不影響 i 的值,修改 i 的值,也不影響 p 的值。
  3. 如果一個指針變量指向了某個普通變量,則 *指針變量 就完全等同於普通變量。解釋:如果 p 是個指針變量,並且 p 存放普通變量 i 變量的地址,則 p 指向了普通變量 i。*p 就完全等同於 i。或者說:在所有出現 *p 的地方都可以替換成 i,在所有出現 i 的地方都可以替換成 *p*p 就是以 p 的內容爲地址的變量。

對於語句**j = *p;**,就相當於j = i;

* 的含義

  1. 乘法。
  2. 定義指針變量。int * p;定義了一個名字叫 p 的變量,int * 表示 p 只能存放 int 變量地址。
  3. 指針運算符。該運算符放在已經定義好的指針前面,如果 p 是一個已經定義好的指針變量,則 *p 表示以 p 的內容爲地址的變量。

如何通過被調函數修改主調函數普通變量的值

  1. 實參必須爲該普通變量的地址。
  2. 形參必須爲指針變量。
  3. 在被調函數中通過 * 形參名 = XXX的方式就可以修改主調函數相關的變量。

例:

/*
    時間:2020年2月24日14:45:35
    功能:
        通過被調函數修改主調函數普通變量的值
    總結:
        1. 實參必須爲該普通變量的地址。
        2. 形參必須爲指針變量。
        3. 在被調函數中通過 * 形參名 = XXX 的方式就可以修改主調函數相關的變量。
*/

# include <stdio.h>

void huhuan_1(int, int);
void huhuan_2(int *, int *);
void huhuan_3(int *, int *);

// 不能完成互換功能
void huhuan_1(int a, int b)
{
    int t;

    t = a;
    a = b;
    b = t;

    return; 
}

// 不能完成互換功能
void huhuan_2(int * p, int * q)
{
    int * t; // 如果要互換 p 和 q 的值,則 t 必須是 int *, 不能是 int, 否則會出錯

    t = p;
    p = q;
    q = t;

    return; 
}

// 可以完成互換功能
void huhuan_3(int * p, int * q)
{
    int t;  // 如果要互換 *p 和 *q 的值,則 t 必須定義成 int 不能定義成 int *, 否則語法錯誤

    t = *p;  // p 是 int *, *p 是 int
    *p = *q;
    *q = t;

    return; 
}

int main(void)
{
    int a = 3;
    int b = 5;

    // huhuan_1(a, b);
    // huhuan_2(&a, &b);  // huhuan_2(*p, *q); 是錯誤的, huhuan_2(a, b); 也是錯誤的
    huhuan_3(&a, &b);
    printf("a = %d, b = %d\n", a, b);

    return 0;
}

/*
    在VSCode中的輸出結果是:
--------------------------
a = 5, b = 3
--------------------------
*/

指針和數組

指針和數組分爲指針和一維數組以及指針和二維數組。這裏主要介紹指針和一維數組。

一維數組名:一維數組名是個指針常量。它存放的是一維數組第一個元素的地址。

下標和指針的關係:如果 p 是個指針變量,則 p[i] 永遠等價於 *(p+i)

確定一個一維數組需要幾個參數:需要兩個參數。數組第一個元素的地址和數組的長度。即:如果一個函數要處理一個一維數組,則需要接收該數組的第一個元素的地址和數組的長度。

例 指針和一維數組

/*
    時間:2020年2月24日16:35:50
    目的:
        確定一個數組需要的參數
    總結:
        如果一個函數要處理一個一維數組,
        則需要接收該數組的第一個元素的地址和數組的長度。
*/

# include <stdio.h>

void f(int * pArr, int len)
{
    int i;

    for (i=0; i<len; ++i)
        printf("%d ", *(pArr+i)); 
            /*
                *(pArr+i) 等價於 pArr[i]
                也等價於 b[i]
                也等價於 *(b+i)
            */
    printf("\n");
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int b[6] = {-1, -2, -3, 4, 5, -6};
    int c[100] = {1, 99, 22, 33};

    f(b, 6);

    return 0;
}

/*
    在VSCode中的輸出結果是:
--------------------------
-1 -2 -3 4 5 -6
--------------------------
*/

指針變量的運算

指針變量不能相加,不能相乘,也不能相除。如果兩個指針變量指向的是同一塊連續空間中的不同存儲單元,則這兩個指針變量纔可以相減。

例 指針的運算

/*
    時間:2020年2月24日16:57:27
    指針的運算
*/

# include <stdio.h>

int main(void)
{
    int i = 5;
    int j = 10;
    int * p = &i;
    int * q = &j;
    int a[5];
    p = &a[1];
    q = &a[4];
    printf("p和q所指向的單元相隔%d個單元\n", q-p);

    // p-q 沒有實際意義

    return 0;
}

/*
    在VSCode中的輸出結果是:
--------------------------
p和q所指向的單元相隔3個單元
--------------------------
*/

一個指針變量到底佔幾個字節【非重點】

預備知識sizeof()函數:
用法一:
  sizeof(數據類型)
功能:返回值就是該數據類型所佔的字節數。
例:
sizeof(int) = 4
sizeof(char) = 1
sizeof(double) = 8

用法二:
  sizeof(變量名)
功能:返回值是該變量所佔的字節數。

假設 p 指向 char 類型變量(1 個字節)。
假設 q 指向 int 類型變量(4 個字節)。
假設 r 指向 double 類型變量(8 個字節)。
p q r 本身所佔的字符數是否一樣?
答案: p q r 本身所佔的字符數是一樣的。

總結: 一個指針變量,無論它指向的變量佔幾個字節,該指針變量本身只佔 4 個字節。一個變量的地址是用該變量的首地址表示的。

多級指針

指針的指針就是多級指針了。對於多級指針需要明白, p 是指針變量,如果 qp 的指針,則 *q = p
例 多級指針

/*
    時間:2020年2月24日21:45:22
    多級指針的示例
*/

# include <stdio.h>

int main(void)
{
    int i = 10;
    int * p = &i;
    int ** q = &p;
    int *** r = &q;

    // r = &p;  // error 因爲 r 是 int *** 類型,r只能存放int ** 類型的變量的地址
    printf("%d\n", ***r);
    printf("%d\n", **q);
    printf("%d\n", *p);

    return 0;
}

/*
    在VSCode中的輸出結果是:
--------------------------
10
10
10
--------------------------
*/

對於指針和函數以及指針和結構體這裏先不介紹。

動態內存分配【重點難點】

傳統數組的缺點

傳統數組也叫靜態數組。

  1. 數組長度必須事先指定,且只能是常整數,不能是變量。如:
      int a[5]; // Ok
      int len = 5; int a[len]; // error

  2. 傳統形式定義的數組,該數組的內存程序員無法手動釋放。數組一旦定義,系統爲該數組分配的存儲空間就會一直存在,除非數組所在的函數運行結束。在一個函數運行期間,系統爲該函數中所分配的空間會一直存在,直到該函數運行完畢時函數的空間纔會被系統釋放。

  3. 數組的長度一旦定義,其長度就不能再更改。數組的長度不能在函數運行的過程中動態的擴充或縮小。

  4. A 函數定義的數組,在 A 函數運行期間可以被其他函數使用,但 A 函數運行完畢之後,A 函數中的數組將無法被其他函數使用。即:傳統定義的函數不能跨函數使用

爲什麼需要動態分配內存

動態數組的創造就是爲了解決靜態數組的 4 個缺陷。

靜態內存 VS 動態內存的比較

區別 靜態內存 動態內存
內存分配 系統自動分配 程序員手動分配
內存釋放 系統自動釋放 程序員手動釋放
內存分配位置 中分配 中分配

動態分配內存舉例——動態數組的構造

假設需要動態構造一個 int 型一維數組。
  int * p = (int *)malloc(int len);

  1. 本語句一共分配了兩塊內存,一塊是動態分配的,總共 len 個字節,一個是靜態分配的是 4 個字節,即變量 p 本身所佔的內存。

  2. malloc() 只有一個 int 型的形參,表示要求系統分配的字節數。

  3. malloc() 函數的功能是請求系統分配 len 個字節的內存空間,如果請求分配成功,則返回第一個字節的地址,如果分配不成功,則返回 NULL

malloc() 函數只能返回第一個字節的地址,所以我們需要把這個無任何實際意義的第一個字節的地址(俗稱乾地址)轉化成一個有實際意義的地址,因此 malloc 前面必須加 (數據類型 *),表示把這個無實際意義的第一個字節的地址轉化爲相應類型的地址。

如:
int * p = (int *)malloc(5);
表示將系統分配好的50個字節的第一個字節的地址轉化爲 int * 型的地址,更準確的說是把第一個字節的地址轉化爲 4 個字節的地址,這樣 p 就指向了第一個的 4 個字節, p+1 就指向了第二個的 4 個字節,p+i 就指向了第 i+1 個的 4 個字節,p[0] 就是第一個元素, p[i] 就是第 i+1 個元素。

double * p = (double *)malloc(80);
表示將系統分配好的80個字節的第一個字節的地址轉化爲 double * 型的地址,更準確的說是把第一個字節的地址轉化爲 8 字節的地址,這樣 p 就指向了第一個的 8 個字節, p+1 就指向了第二個的 8 個字節,p+i 就指向了第 i+1 個的 8 個字節,p[0] 就是第一個元素, p[i] 就是第 i+1 個元素。

freep(p);
表示將 p 所指向的內存給釋放掉,p 本身的內存是靜態的。不能由程序員手動釋放,p 本身的內存只能在 p 變量所在的函數運行終止時由系統自動釋放。

例 動態數組的構造

/*
    時間:2020年2月26日21:15:46
    動態數組的構造
*/

# include <stdio.h>
# include <malloc.h>

int main(void)
{
    int a[5];  // 如果int 佔4個字節的話,則本數組總共包含有20個字節,每四個字節被當做了一個int變量來使用
    int len;
    int * pArr;
    int i;

    printf("請輸入你要存放元素的個數:");
    scanf("%d", &len);  //
    pArr = (int *)malloc(4 * len);  // 本行動態的構造了一個一維數組,該數組的長度是len,該數組的數組名是pArr,該數組的每個元素是int 整型。類似於 int pArr[len];
   
    // 對一維數組進行操作, 如:對一維數組進行賦值
    for (i=0; i<len; ++i)
        scanf("%d", &pArr[i]);
   
    // 對一維數組進行輸出
    printf("一維數組的內容是:\n");
    for (i=0; i<len; ++i)
        printf("%d\n", pArr[i]);

    free(pArr);  // 釋放掉動態分配的數組
   
    return 0;
}

跨函數使用內存的問題

靜態內存不可以跨函數使用。或者說是:靜態內存在函數執行期間可以被其他函數使用,但是在函數執行完畢後就不能再被其他函數使用了。

動態內存可以跨函數使用。動態內存在函數執行完畢之後仍然可以被其他函數使用。

說明

  1. 本學習筆記整理自B站郝斌老師的《郝斌C語言自學教程》片段P121-P150

  2. 筆記中所有代碼均在windows10操作系統,在VSCode編輯器中通過測試。具體VSCode C語言開發環境搭建方法請參照我的另一篇CSDN博客——Windows10下利用Visual Studio Code搭建C語言開發環境

後記

如果對你有所幫助,歡迎關注我的公衆號。這個公衆號主要是慢慢分享和記錄自己學習編程的筆記,比如:C,Python,Java等。後續也會分享自己面試以及在職場上的成長心得。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章