Stories

Detail Return Return

深入理解指針Part1——C語言 - Stories Detail

”指針是C語言的精髓!“

——出自學校教《C語言程序設計》的老師

1 內存和地址

1.1 內存

為了理解指針,首先要從內存和地址講起。

在講之前,先舉一個現實世界中的例子。大學宿舍都有門牌號,當需要找到某個學生時,我們只需要知道宿舍的門牌號就可以了。

在計算機中內存很重要,程序經常需要從內存中讀取和寫入數據。在購買電腦的時候,內存的大小常有8/16/32GB等,這些空間又是如何被管理的?

其實也是把內存劃分為一個個的內存單元,每個內存單元的大小是1字節(byte)。

其中,每個內存單元,相當於一個學生宿舍,一個字節空間裏面能放8個比特位,就好比同學們住的八人間,每個人是一個比特位。

每個內存單元也都有一個編號(這個編號就相當於宿舍房間的門牌號),有了這個內存單元的編號,CPU就可以快速找到一個內存空間。

生活中我們把門牌號也叫地址,在計算機中我們把內存單元的編號也稱為地址。C語言中給地址起了新的名字叫:指針

所以我們可以理解為:內存單元的編號 = 地址 = 指針

1.2 如何理解編址

CPU與內存之間有大量的數據交互,這些交互通過地址總線、數據總線、控制總線等,我們這裏關注的是地址總線。這裏可以簡單理解,32位機器上有32根地址總線,每根線有0、1兩種狀態,能表示2^32種含義,每一種含義都代表了1個地址。CPU通過地址總線獲取到了內存地址後,就可以通過其他總線對內存進行操作。

2 指針變量和地址

2.1 取地址操作符(&)

理解了內存和地址的關係,回到C語言中,創建變量其實就是向內存申請空間。例如int a = 10,就是創建了整形變量a,內存中申請4個字節,用於存放整數10,其中每個字節都有地址。

那我們如何得到a的地址呢?這就需要用到取地址操作符(&)。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//打印整形變量a的地址
//環境為x64
int main()
{
    int a = 10;
    printf("a的地址是:%p\n", &a);
    return 0;
}

如圖,&a取出的是a所佔4個字節中地址較小的字節的地址。

雖然整型變量佔用4個字節,我們只要知道了第一個字節地址,順藤摸瓜訪問到4個字節的數據是完全可行的。

2.2 指針變量和解引用操作符(*)

2.2.1 指針變量

那我們通過取地址操作符(&)拿到的地址是一個數值,比如:0000009E504FFC84,這個數值有時候也是需要存儲起來,方便後期再使用的,那我們把這樣的地址值存放在哪裏呢?答案就是:指針變量中。

比如:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//環境為x64
int main()
{
    int a = 10;
    int* pa = &a;//取出a的地址並存儲到指針變量pa中
    printf("a的地址是:%p\n", pa);
    return 0;
}

指針變量也是一種變量,這種變量就是用來存放地址的,存放在指針變量中的值都會理解為地址。

2.2.2 拆解指針類型

我們看到指針變量pa的類型是int* ,該如何理解指針的類型呢?

int a = 10;
int* pa = &a;

這裏pa左邊寫的是int**是在説明pa是指針變量。而前面的int是在説明pa指向的是整型(int)類型的對象。

那如果有一個char類型的變量chch的地址,要放在什麼類型的指針變量中呢?自然是放在char*類型的指針變量中。

2.2.3 解引用操作符(*)

我們將地址保存起來,未來是要使用的,那怎麼使用呢?

在現實生活中,我們使用地址要找到一個房間,在房間裏可以拿去或者存放物品。

C語言中其實也是一樣的,我們只要拿到了地址(指針),就可以通過地址(指針)找到地址(指針)指向的對象,這裏必須學習一個操作符叫解引用操作符(*)。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//環境為x64
int main()
{
    int a = 10;
    int* pa = &a;
    printf("修改前:a = %d\n", a);
    *pa = 5;
    printf("修改後:a = %d\n", a);
    return 0;
}

上面代碼中第7行就使用瞭解引用操作符,*pa的意思就是通過pa中存放的地址,找到指向的空間,*pa其實就是a變量了;所以*pa = 0,這個操作符是把a改成了0。

有同學肯定在想,這裏如果目的就是把a改成0的話,寫成a = 0;不就完了,為啥非要使用指針呢?

其實這裏是把a的修改交給了pa來操作,這樣對a的修改,就多了一種的途徑,寫代碼就會更加靈活,後期慢慢就能理解了。

2.3 指針變量的大小

1.2中提到:

在32位機器上有32根地址總線,每根線有0、1兩種狀態,能表示2^32種含義。

那我們把32根地址線產生的2進制序列當做一個地址,那麼一個地址就是32個bit位,需要4個字節才能存儲。如果指針變量是用來存放地址的,那麼指針變量的大小就得是4個字節的空間才可以。

同理64位機器,假設有64根地址線,一個地址就是64個二進制位組成的二進制序列,存儲起來就需要8個字節的空間,指針變量的大小就是8個字節。

通過原理可以得知,指針變量的大小與其類型無關,只要指針類型的變量在相同的平台下,大小都是相同的。

3 指針變量類型的意義

既然指針變量的大小和類型無關,只要是指針變量,在同一個平台下,大小都是一樣的,為什麼還要有各

種各樣的指針類型呢?其實指針類型是有特殊意義的。接下來我們一起探討。

3.1 指針的解引用

下面,我們來對比兩段代碼:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//代碼1
int main()
{
    int n = 0x11223344;
    int* pi = &n;
    *pi = 0;
    return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//代碼2
int main()
{
    int n = 0x11223344;
    char* pc = &n;
    //char* pc = (char*)&n;(可寫成這種顯式類型轉換)
    *pc = 0;
    return 0;
}

淺看一下,兩段代碼都希望將整形變量n的值改為0,代碼1是正常的寫法肯定沒問題,但是代碼2很怪,用了一個char*類型的指針變量pc來存儲整形變量n的地址,這會導致什麼,打開調試在內存中看一看。

我們先看看代碼1:

創建整形變量n,內存中申請了4個字節的空間,存儲16進制數0x11223344,然後*pc = 0將n的4個字節全部改為0,任務完成。

接下來看代碼2:

我們發現,代碼2只是將n的第一個字節改為0,其他3個字節的值原封不動的保留了下來。

結論:指針的類型決定了,對指針解引用的時候有多大的權限(一次能操作幾個字節)。

比如:char*的指針解引用就只能訪問一個字節,而int*的指針的解引用就能訪問四個字節。

3.2 指針 + / - 整數

先看一段代碼,調試觀察地址的變化。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
    int n = 10;
    char* pc = (char*)&n;
    int* pi = &n;
    printf("&n  = %p\n", &n);
    printf("pc  = %p\n", pc);
    printf("pc+1= %p\n", pc + 1);
    printf("pi  = %p\n", pi);
    printf("pi+1= %p\n", pi + 1);
    return 0;
}

代碼運行的結果如下:

我們可以看出,char*類型的指針變量+1跳過1個字節,int*類型的指針變量+1跳過了4個字節。這就是指針變量的類型差異帶來的變化。指針+1,其實就是跳過1個指針指向的元素。指針可以+1,那也可以-1。

結論:指針的類型決定了指針向前或者向後走一步有多大(距離)。

3.3 void*指針

在指針類型中有一種特殊的類型是void*,可以理解為無具體類型的指針(或者叫泛型指針),這種類型的指針可以用來接受任意類型地址。但是也有侷限性,void*類型的指針不能直接進行指針的+-整數和解引用的運算。

一般void*類型的指針是使用在函數參數的部分,用來接收不同類型數據的地址,這樣的設計可以實現泛型編程的效果。使得一個函數來處理多種類型的數據,在後面會經常遇到。

4 指針運算

指針的基本運算有三種,分別是:

  • 指針+-整數
  • 指針-指針
  • 指針的關係運算

4.1 指針+-整數

因為數組在內存中是連續存放的,只要知道第一個元素的地址,順藤摸瓜就能找到後面的所有元素。

下面我們用指針來讀取一個數組:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針+-整數
int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));//p+i 這裏就是指針+整數
    }
    return 0;
}

4.2 指針-指針

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針-指針
int my_strlen(char* s)
{
    char* p = s;
    while (*p != '\0')
        p++;
    return p - s;
}

int main()
{
    printf("%d\n", my_strlen("abc"));
    return 0;
}

4.3 指針的關係運算

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針的關係運算
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (p < arr + sz)//指針的大小比較
    {
        printf("%d ", *p);
        p++;
    }
    return 0;
}

正文完

user avatar sunplay Avatar qishiwohendou Avatar daqianduan Avatar buildyuan Avatar aipaobudeshoutao Avatar chaoxi_67109d31bc42f Avatar cbuc Avatar immerse Avatar xiaoal Avatar baqideyaling Avatar zhishuangdemaipian Avatar shiwangdehongshu Avatar
Favorites 20 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.