首頁 > 軟體

C語言全方位講解指標與地址和陣列函數堆空間的關係

2022-04-21 13:00:39

一、一種特殊的變數-指標

指標是C語言中的變數

  1. 因為是變數,所以用於儲存具體值
  2. 特殊之處,指標儲存的值是記憶體中的地址
  3. 記憶體地址是什麼?
  • 記憶體是計算機中的儲存部件,每個儲存單元有固定唯一的編號
  • 記憶體中儲存單元的編號即記憶體地址

需要弄清楚的事實

  • 程式中的一切元素都存在於記憶體中,因此,可通過記憶體地址存取程式元素。

記憶體範例

獲取地址

  • C語言中通過 & 操作符獲取程式元素的地址
  • & 可獲取變數,陣列,函數的起始地址
  • 記憶體地址的本質是一個無符號整數(4位元組或8位元組)

下面看一個簡單的例子:

#include<stdio.h>
int main()
{
    int var = 0;
    printf("var value = %dn", var);
    printf("var address = %pn", &var);
    return 0;
}

下面為輸出結果:

注意事項

  • 只有通過 記憶體地址+長度 才能確定一個變數中儲存的值。

指標定義語法:

type *point;

  • type - 資料型別,決定存取記憶體時的長度範圍
  • * 標誌,意味著定義一個指標變數
  • pointer 變數名,遵循C語言命名規則

例如:

int main()
{
    char* pChar;
    short* pShort;
    int* pInt;
    float* pFloat;
    double* pDouble;
    return 0;   
}

指標記憶體存取:

* pointer

  • 指標存取操作符(*)作用於指標變數即可存取記憶體資料
  • 指標的型別決定通過地址存取記憶體時的長度範圍
  • 指標的型別統一佔用 4 位元組或 8 位元組

即:sizeof(type*) == 4或 sizeof(type*) == 8

下面看一段程式碼,感受一下:

#include <stdio.h>
int main()
{
    int var = 0;
    int another = 0;
    int* pVar = NULL;
    printf("1. var = %dn", var);
    printf("1. pVar = %pn", pVar);
    pVar = &var;  // 使用指標儲存變數的地址
    *pVar = 100;  // *pVar 等價於 var , var = 100;
    printf("2. var = %dn", var);
    printf("2. pVar = %pn", pVar);
    pVar = &another;  // 改變了 pVar 的指向,使得 pVar 儲存 another 的地址
    *pVar = 1000;     // another = 1000;
    printf("3. another = %dn", another);
    printf("3. pVar = %pn", pVar);
    printf("4. add ==> %dn", var + another + *pVar);   // 100 + 1000 + 1000  ==> 2100
    return 0;
}

下面為輸出結果:

注意 NULL 地址為 00000000

小結

  • 指標是C語言中的變數(本質為容器)
  • 指標專用於儲存程式元素的記憶體地址
  • 可使用 * 操作符通過指標存取程式元素本身
  • 指標也有型別,指標型別由 資料型別+* 構成

二、深入理解指標與地址

靈魂三問

  • 指標型別和普通型別之間的關係是什麼?
  • 何看待“記憶體地址+長度才能存取記憶體中的資料”?
  • 不同型別的指標可以相互賦值嗎?

初學指標的軍規

  • Type* 型別的指標只儲存 Type 型別變數的地址
  • 禁止不同型別的指標相互賦值
  • 禁止將普通數值當作地址賦值給指標

注意:指標儲存的地址必須是有效地址

下面看一段程式碼:

#include <stdio.h>
int main()
{
    int i = 10;
    float f = 10;
    int* pi = &f;    // WARNING
    float* pf = &f;  // OK
    printf("pi = %p, pf = %pn", pi, pf);
    printf("*pi = %d, *pf = %fn", *pi, *pf);
    pi = i;   // WARNING
    *pi = 110;  // OOPS
    printf("pi = %p, *pi = %dn", pi, *pi);
    return 0;
}

下面為輸出結果:

這個程式犯了兩個錯誤:

1、將不同型別的指標相互賦值,雖然 int 型別的指標變數儲存的地址是對的,但是其所儲存的值是錯的。

2、 將普通數值當作地址賦值給指標,這會導致嚴重的錯誤,不能正確輸出

編寫函數交換兩個變數的值

  • 想要編寫函數交換變數的值,那麼,必須有能力在函數內部修改函數外部的變數!!!

看下面的程式碼:

#include <stdio.h>
void func(int* p)
{
    *p = 100;   // 修改記憶體中 4 位元組的資料,即:修改一個整型變數的值
}
void swap(int* pa, int* pb)
{
    int t = 0;
    t = *pa;
    *pa = *pb;
    *pb = t;
}
int main()
{
    int var = 0;
    int a = 1, b = 2;
    printf("1. var = %dn", var);
    func( &var );
    printf("2. var = %dn", var);
    printf("3. a = %d, b = %dn", a, b);
    swap(&a, &b);
    printf("4. a = %d, b = %dn", a, b);
    return 0;
}

下面為輸出結果:

小結論

可以利用指標從函數中“返回”多個值 (return只能返回一個值)!!

下面看一段程式碼:

#include <stdio.h>
int calculate(int n, long long* pa, long long* pm)
{
    int ret = 1;
    if( (1 <= n) && (n <= 20) )
    {
        int i = 0;
        *pa = 0;
        *pm = 1;
        for(i=1; i<=n; i++)
        {
            *pa = *pa + i;
            *pm = *pm * i;
        }
    }
    else
    {
        ret = 0;
    }
    return ret;
}
int main()
{
    long long ar = 0;
    long long mr = 0;
    if( calculate(5, &ar, &mr) )
        printf("ar = %lld, mr = %lldn", ar, mr);
    return 0;
}

下面為輸出結果:

這段程式碼中的子函數通過指標,計算了1加到5以及1乘到5的值,這就間接地通過指標從子函數“返回”多個值

小結

指標是變數,因此賦值時必須保證型別相同

指標變數儲存的地址必須是有效地址

通過指標引數

  • 一能夠實現函數交換變數的值
  • 一能夠從函數中“返回”多個值

三、指標與陣列(上)

問題

  • 陣列的本質是一片連續的記憶體,那麼,陣列的地址是什麼?如何獲取?

一些事實

  • 使用取地址操作符&獲取陣列的地址
  • 陣列名可看作一個指標,代表陣列中 0 元素的地址
  • 當指標指向陣列元素時,可進行指標運算(指標移動)

深入理解陣列地址( int a[]= {1, 2, 3, 4, 5}; )

  • &a 與 a 在數值上相同,但是意義上不同
  • &a 代表陣列地址,型別為:int(*)[5]
  • a 代表陣列0號元素地址,型別為: int*
  • 指向陣列的指標: int (*pName)[5] = &a;

下面看一段程式碼:

#include <stdio.h>
int main()
{
    int a[] = {1, 2, 3, 4, 0};
    int* p = a;  // a 的型別為 int*, &a[0] ==> int*
    int (*pa) [5] = &a;
    printf("%p, %pn", p, a);
    p++;
    *p = 100;  // a[1] = 100;
    printf("%d, %dn", *p, a[1]);
    printf("%p, %pn", &a, a);
    p = pa;   // WARNING  !!!!
    p = a;
    while( *p )
    {
        printf("%dn", *p);
        p++;
    }
    return 0;
}

下面為執行結果:

需要注意的是,p 和 pa不是一個指標型別,所以令 p = pa 這種做法是不正確的。

注意

  • 陣列名並不是指標,只是代表了0號元素的地址,因此可以當作指標使用。

四、指標與陣列(下)

指標與陣列的等價用法

假如:

int a[ ] = {1, 2,3, 4,5}

int* p = a;

則以下等價:

a[i] <--> *(a + i) <--> *(p + i) <--> p[i]

下面看一段程式碼,加深理解:

#include <stdio.h>
int main()
{
    int a[] = {1, 2, 3, 4, 5};
    int* p = a;
    int i = 0;
    // a[i] <==> *(a+i) <==> *(p+i) <==> p[i]
    for(i=0; i<5; i++)
    {
        printf("%d, %dn", a[i], *(a + i));
    }
    printf("n");
    for(i=0; i<5; i++)
    {
        printf("%d, %dn", a[i], p[i]);
    }
    printf("n");
    for(i=0; i<5; i++)
    {
        printf("%d, %dn", p[i], *(p + i));
    }
    printf("n");
    printf("a = %p, p = %pn", a, p);
    printf("&a = %p, &p = %pn", &a, &p);
    return 0;
}

下面為輸出結果:

這裡可以看到 a和 p的地址不同,因為它們是兩個不同的指標變數。

字串拾遺

  • 字串常數是 char* 型別,一種指標型別

指標移動組合拳:

int v = *p++;

解讀:

指標存取操作符(*)和自增運算操作符(++) 優先順序相同

所以,先從p指向的記憶體中取值,然後p進行移動

等價於:

int v = *p;

p++;

下面看一段程式碼,體會一下:

#include <stdio.h>
int main()
{
    int a[] = {1, 2, 3};
    int* p = a;
    int v = *p++;
    char* s = NULL;
    printf("%pn", "D.T.Software");
    printf("%pn", "D.T.Software");
    printf("v = %d, *p = %dn", v, *p);
    printf("First = %cn", *"D.T.Software");
    s = "D.T.Software";
    while( *s ) printf("%c", *s++);
    printf("n");
    return 0;
}

下面為輸出結果:

因為D.T.Software 在全域性資料區的起始地址一樣,所以兩次列印出來的地址一樣。

小結

  • 陣列名可看作一個指標,代表陣列中0元素的地址
  • &a與a在數值上相同,但是意義上不同
  • C語言中的字串常數的型別是 char *
  • 當指標指向陣列元素時,才能進行指標運算

五、指標與函數

問題

  • 函數呼叫時會跳轉到函數體對應的程式碼處執行,那麼,如何知道函數體程式碼的具體位置?

深入函數之旅

  • 函數的本質是一段記憶體中的程式碼(佔用一片連續記憶體)
  • 函數擁有型別,函數型別由返回型別和引數型別列表組成
  • 例:
函數申明型別
int sum(int n);int (int)
void swap(int* pa, int* pb)void (int*, int*)
void g(void);void (void)

函數的一些事實

  • 函數名就是函數體程式碼的起始地址(函數入口地址)
  • 通過函數名呼叫函數,本質為指定具體地址的跳轉執行
  • 因此,可定義指標,儲存函數入口地址

函數指標( Type func (Type1 a,Type2 b))

  • 函數名即函數入口地址,型別為 Type(*)(Type1,Type2)
  • 對於 func 的函數,&func 與 func 數值相同,意義相同
  • 指向函數的指標:Type (*pFunc) (Type1, Type2) = func;

函數指標引數

  • 函數指標的本質還是指標(變數,儲存記憶體地址)
  • 可定義函數指標引數,使用相同程式碼實現不同功能

注意

函數指標只是單純的儲存函數的入口地址

因此

  • 只能通過函數指標呼叫目標函數
  • 不能進行指標移動(指標運算)

下面看一段程式碼,理解一下:

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int mul(int a, int b)
{
    return a * b;
}
int calculate(int a[], int len, int(*cal)(int, int))
{
    int ret = a[0];
    int i = 0;
    for(i=1; i<len; i++)
    {
        ret = cal(ret, a[i]);
    }
    return ret;
}
int main()
{
    int a[] = {1, 2, 3, 4, 5};
    int (*pFunc) (int, int) = NULL;
    pFunc = add;
    printf("%dn", pFunc(1, 2));
    printf("%dn", (*pFunc)(3, 4));
    pFunc = &mul;
    printf("%dn", pFunc(5, 6));
    printf("%dn", (*pFunc)(7, 8));
    printf("1 + ... + 5 = %dn", calculate(a, 5, add));
    printf("1 * ... * 5 = %dn", calculate(a, 5, mul));
    return 0;
}

下面為輸出結果:

這裡注意,只有呼叫的時候,才能確定 calculate() 子函數中的 cal 是什麼函數。

再論陣列引數

函數的陣列形參退化為指標!因此,不包含陣列實參的長度資訊。使用陣列名呼叫時,傳遞的是0號元素的地址。

void func(int a[ ]) <--> void func(int* a)

<--> void func (int a[1])

<--> void func (int a[10)

<--> void func(int a[100)

下面看一段程式碼:

#include <stdio.h>
int demo(int arr[], int len)  // int demo(int* arr, int len)
{
    int ret = 0;
    int i = 0;
    printf("demo: sizeof(arr) = %dn", sizeof(arr));
    while( i < len )
    {
        ret += *arr++;
        i++;
    }
    return ret;
}
int main()
{
    int a[] = {1, 2, 3, 4, 5};
    // int v = *a++;
    printf("return value: %dn", demo(a, 5));
    return 0;
}

下面為輸出結果:

定義的形參arr[]可以進行 *arr++ 的操作,這就說明函數的陣列形參退化為指標,因為陣列不可以進行 ++ 的運算。

小結

  • 函數名的本質是函數體的入口地址
  • 函數型別由返回型別和引數型別列表組成
  • 可定義指向函數的指標:Type (*pFunc) (Type1,Type2);
  • 函數指標只是單純的儲存函數的入口地址(不能進行指標運算)

六、指標與堆空間

再論記憶體空間

記憶體區域不同,用途不同

  • 全域性資料區:存放全域性變數,靜態變數
  • 棧空間:存放函數引數,區域性變數
  • 堆空間:用於動態建立變數(陣列)

堆空間的本質

  • 備用的“記憶體倉庫”,以位元組為單位預留的可用記憶體
  • 程式可在需要時從“倉庫”中申請使用記憶體(動態借)
  • 當不需要再使用申請的記憶體時,需要及時歸還(動態還)

問題

  • 如何從堆空間申請記憶體?如何歸還?

預備知識-- void*

  • void 型別是基礎型別,對應的指標型別為 void*
  • void* 是指標型別,其指標變數能夠儲存地址
  • 通過 void* 指標無法獲取記憶體中的資料(無長度資訊)

void* 總結

  • 不可使用void*指標直接獲取記憶體資料。
  • void*指標可與其它資料指標相互賦值。

下面看一段程式碼:

#include <stdio.h>
int main()
{
    char c = 0;
    int i = 0;
    float f = 2.0f;
    double d = 3.0;
    void* p = NULL;
    double* pd = NULL;
    int* pi = NULL;
    /* void* 指標可以儲存任意型別的地址 */
    p = &c;
    p = &i;
    p = &f;
    p = &d;
    printf("%pn", p);
    // void* 型別的指標無法存取記憶體中的資料
    // printf("%fn", *p);
    /* void* 型別的變數可以直接合法的賦值給其他具體資料型別的指標變數 */
    pd = p;
    pi = p;
    // void* 是例外,其他指標型別的變數不能相互賦值
    // pd = pi;
    return 0;
}

下面為輸出結果:

注意幾個問題:

1.void* 指標可以儲存任意型別的地址

2.void* 型別的指標無法存取記憶體中的資料

3.void* 型別的變數可以直接合法的賦值給其他具體資料型別的指標變數

4.void* 是例外,其他指標型別的變數不能相互賦值

堆空間的使用

  • 工具箱:stdlib.h
  • 申請:void* malloc ( unsigned bytes )
  • 歸還:void free( void* p)

堆空間的使用原則

  • 有借有還,再借不難(杜絕只申請,不歸還)
  • malloc申請記憶體後,應該判斷是否申請成功
  • free只能釋放申請到的記憶體,且不可多次釋放(free 釋放的是堆空間的地址)

下面看一段程式碼感受一下:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* p = malloc(4); // 從堆空間申請 4 個位元組當作 int 型別的變數使用
    if( p != NULL )  // 如果申請失敗 p 為 0 ,即:空值
    {
        *p = 100;
        printf("%dn", *p);
        free(p);
    }
    p = malloc(4 * sizeof(int));
    if( p != NULL )
    {
        int i = 0;
        for(i=0; i<4; i++)
        {
            p[i] = i * 10;
        }
        for(i=0; i<4; i++)
        {
            printf("%dn", p[i]);
        }
        free(p);
    }
    return 0;
}

下面為輸出結果:

小結

  • 堆空間是程式中預留且可用的記憶體區域
  • void*指標只能能夠儲存地址,但無法獲取記憶體資料
  • void*指標可與其它資料指標相互賦值
  • malloc申請記憶體後,應該判斷是否申請成功
  • free只能釋放申請到的記憶體,且不可多次釋放

七、指標專題經典問題剖析

多級指標

  • 可以定義指標的指標儲存其它指標變數的地址

如:

Type v;
Type *pv = &v;
Type** ppv = &pv;
type*** pppv = &ppv;

下面看一段程式碼:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int a = 0;
    int b = 1;
    int* p = &a;
    int** pp = &p;
    **pp = 2;   // a = 2;
    *pp = &b;   // p = &b;
    *p = 3;     // b = 3;
    printf("a = %d, b = %dn", a, b);
    return 0;
}

下面為輸出結果:

*pp 就是取 pp 裡面的內容,而 pp 裡面存的內容是 p 的地址,所以 *pp 就相當於p 的內容,而 p 的內容就是 a 的地址,所以說 **p 就相當於 a,**p = 2 也就是把 2 賦值給 a,*pp = &b 即為 p = &b,所以 *p = 3,就是把 3 賦值給 b。

下面再看一段程式碼:

#include <stdio.h>
#include <stdlib.h>
int getDouble(double** pp, unsigned n)
{
    int ret = 0;
    double* pd = malloc(sizeof(double) * n);
    if( pd != NULL )
    {
        printf("pd = %pn", pd);
        *pp = pd;
        ret = 1;
    }
    return ret;
}
int main()
{
    double* p = NULL;
    if( getDouble(&p, 5) )
    {
        printf("p = %pn", p);
        free(p);
    }
    return 0;
}

下面為輸出結果:

這裡特別注意:函數外的一個一級指標指向了這裡申請的堆空間

再論二維陣列

二維陣列的本質是一維陣列 ,即:陣列中的元素是一維陣列!!

因此:

int a[2][2];

a 就是 &a[0]

a[0] 的型別是 int[2]

可知 a 的型別是 int (*)[2]

下面看一段程式碼:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int b[2][2] = {{1, 2}, {3, 4}};
    int (*pnb) [2] = b;  // b 的型別是 int(*)[2]
    *pnb[1] = 30;
    printf("b[0][0] = %dn", b[0][0]);
    printf("b[0][1] = %dn", b[0][1]);
    printf("b[1][0] = %dn", b[1][0]);
    printf("b[1][1] = %dn", b[1][1]);
    return 0;
}

下面為輸出結果:

pnb[0]是[1,2],pnb[1]經過賦值後是[30,4],所以*(pnb[1])就是取該陣列所代表的第0個元素,也就是30。

下面再看一個程式碼:

#include <stdio.h>
#include <stdlib.h>
int* func()
{
    int var = 100;
    return &var;
}
int main()
{
    int* p = func();  // OOPS!!!!
                      // p 指向了不合法的地址,這個地址處沒有變數存在
                      // p 是一個野指標,儲存不合法地址的指標都是野指標
    printf("*p = %dn", *p);
    *p = 200;   // 改變 func 函數中區域性變數 var 的值,是不是非常奇怪???
    printf("*p = %dn", *p);
    return 0;
}

這段程式碼是有問題的, func() 函數執行後, var 這個變數就會被銷燬,所以 p 指向了一個不合法的地址。

小結

  • 可以定義指向指標的指標(儲存指標變數的地址)
  • 一維陣列名的型別為Type* (變數地址型別)
  • 二維陣列名的型別為Type (*)[N](陣列地址型別)
  • 不要從函數中返回區域性變數/函數引數的地址

到此這篇關於C語言全方位講解指標與地址和陣列函數堆空間的關係的文章就介紹到這了,更多相關C語言指標內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com