1. 程式人生 > >C語言之漫談指標(上)

C語言之漫談指標(上)

C語言之漫談指標(上)

 

在C語言學習的途中,我們永遠有一個繞不了的坑,那就是——指標。

在這篇文章中我們就談一談指標的一些基礎知識。

 

綱要:

  • 零.談指標之前的小知識
  • 一.指標與指標變數
  • 二.指標變數的型別
  • 三.指標的解引用
  • 四.野指標
  • 五.指標運算
  • 六.指標與陣列
  • 七.二級指標
  • 八.指標陣列
  • 九.相關例題 

零.談指標之前的小知識


在談指標之前我們先來說一說  計算機的儲存器.

 

我們在碼程式碼時, 每當宣告一個變數,計算機都會在儲存器中某個地方為它建立空間。

如果在函式(例如main()函式)中宣告變數,計算機會把它儲存在一個叫棧(Stack)的儲存器區段中;

如果你在函式以外的地方宣告變數,計算機則會把它儲存在儲存器的全域性量段(Globals)。

 

程式記憶體分配的幾個區域:

  1. 棧區(stack):在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些

   儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有

   限。 棧區主要存放執行函式而分配的區域性變數、函式引數、返回資料、返回地址等。
  2. 堆區(heap):一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。分配方式類似

      於連結串列。
  3. 資料段(靜態區)(static)存放全域性變數、靜態資料。程式結束後由系統釋放。

  4. 程式碼段:存放函式體(類成員函式和全域性函式)的二進位制程式碼。

 

 

如下圖:

 

 

 當然了,這張圖對於一些初學者並不是很友好。但是接下來一張圖就友好了很多。

 

 那麼,我們如何知道我們的變數儲存在哪?

這時我們就需要用到 & 操作符了。

 

這時可能有人問了,我這兩次 x 的地址為什麼不相同,這是因為:

我們每一次程式開始執行的時候,系統都會給我們的變數重新分配地址,

程式結束的時候銷燬地址,再次從頭開始執行時再次重新分配,結束時再次銷燬。

 

有了一定的知識之後,我們便開始正文。

 

一.指標以及指標變數。

1.指標

  在電腦科學中,指標(Pointer)是程式語言中的一個物件,利用地址,它的值直接指向
(points to)存在電腦儲存器中另一個地方的值。由於通過地址能找到所需的變數單元,可以
說,地址指向該變數單元。因此,將地址形象化的稱為“指標”。意思是通過它能找到以它為地址
的記憶體單元。

  對於上面的概念我們再簡練一些便可以概括為:

  指標 :記憶體單元的地址(編號)。如上例中 x 的地址 (指標) 為 0x00D3FD34,

2.指標變數

   指標變數:儲存地址 (指標) 的變數。

3.指標和指標變數的關係

  1、指標就是地址,地址就是指標。

  2、地址就是記憶體單元的編號。

  3、指標變數就是存放記憶體地址的變數。

    指標變數的值就是變數的地址。指標與指標變數的區別,就是變數值與變數的區別。

  4、為表示指標變數和它指向的變數之間的關係,用指標運算子"*"表示。如:

//分別定義了 int、float、char 型別的指標變數
int *x = 1;
float *f = 1.3;
char *ch = 'z';

     要注意的一點就是 此時 x、y、z 就是指標變數的名字,而不是 *x、*y、*z。

如下:

#include <stdio.h>
int main()
{

    int a = 10;  //在記憶體中開闢一塊空間來放制a

    int* p = &a;//這裡我們對變數a,取出它的地址,可以使用 & 操作符。

            //將a的地址存放在p變數中,p就是一個之指標變數。

    return 0;
}

4.指標變數的大小

指標變數所佔空間的大小和該指標變數指向的資料型別沒有任何直接關係,而是跟其所在地址的所佔空間的大小有關。

同一編譯器下,同一編譯選項下所有型別的指標變數大小都是一樣的,

指標變數的大小是編譯器所對應的系統環境所決定的。

如:

  指標變數的大小在16位平臺是2個位元組,在32位平臺是4個位元組,在64位平臺是8個位元組。

 

二.指標變數的型別

提前宣告一下:為了方便講述後面的知識,下文將以指標代替指標變數!

我們先來看以下程式碼:

    int a = 1;
    p = &a;

現在我們都知道了 p 是一個儲存著a的地址的指標變數。

可是它的型別是什麼呢?

 這時,我們就需要看它所儲存的地址中的變數是什麼型別了。

儲存型別      指標型別
int ---> int* short ---> short* float ---> float* char ---> char* double ---> double* long ---> long*

我們可以看到:

指標的定義方式是: type + * 。

其實: char* 型別的指標是為了存放 char 型別變數的地址。

    short* 型別的指標是為了存放 short 型別變數的地址。

       int* 型別的指標是為了存放int 型別變數的地址。

 


那麼指標型別又意味著什麼呢?

我們來看看下述程式碼:

#include <stdio.h>

int main()
{
    int n = 10;
    char* pc = (char*)&n;
    int* pi = &n;
    printf("%p\n", &n);
    printf("%p\n", pc);
    printf("%p\n", pc + 1);
    printf("%p\n", pi);
    printf("%p\n", pi + 1);
    return 0;
}

 

 

 我們可以發現:

  int* + 1 向後跳過了4個位元組---恰好為一個int 型的大小

  char* + 1 向後跳過了1個位元組---恰好為一個char 型的大小


而這,是不是巧合呢——答案當然是否定的。

所以,我們可以得到一個結論:

  針的型別決定了指標向前或者向後走一步有多大(距離)。

 

三.指標的解引用

1.指標的解引用

現在我們有了變數儲存的地址,可是我們要怎麼樣用到它呢?

這時就需要指標的解引用操作符了—" * "

int main()
{
    int a = 1;
    int* b = &a;
    printf("%d", a);
    printf("%d", *b); //*p 就相當於把p所指向的空間的內容拿出來
    return 0;
}

我們會發現其結果都相同。

我們也可以利用解引用的方式來改變一個變數的值:

int main()
{
    int a = 1;
    int* b = &a;
    printf("改變之前: %d\n", a);
    *b = 2;
    printf("改變之後: %d\n", a);
    return 0;
}

 

 

 如果上面圖中的語言有點抽象,那我們可以舉一個形象的例子:

  有一個人叫張三,有一天他在XX賓館中定了一間房,且房子的門號為100,到這天晚上的時候,他覺得有點寂寞,

於是打電話喊了他好朋友小劉來找他玩,張三在描述地址時是這樣說的:我在XX賓館100號房間……但小劉那時有事,所以等到小劉來賓館的時候

已經是第二天中午了,可時張三在第二天早上就退了房,且李四又住了進來,所以當小劉開啟賓館的100室見到的還會是張三嗎?肯定不會了

 

住在100室的人--------a

100室------&a、b

張三-------1

李四--------2

不知大家這回理解了沒有

 

2.指標的型別與解引用的關係

我們來看看這一個例子:

#include <stdio.h>
//在此程式執行時,我們要重點在除錯的過程中觀察記憶體的變化
int main()
{

    int n = 0x11223344;

    char* pc = (char*)&n;

    int* pi = &n;

    *pc = 0; 

    *pi = 0;

    return 0;
}

 

 

 所以,我們又可以推出:

  指標的型別決定了,對指標解引用的時候有多大的許可權(能操作幾個位元組)。

  比如: char* 的指標解引用就只能訪問一個位元組,而 int* 的指標的解引用就能訪問四個位元組。

 

四.野指標

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

1.野指標的危害 

  1、指向不可訪問的地址
  2、指向一個可用的,但是沒有明確意義的空間
  3、指向一個可用的,而且正在被使用的空間,造成癱瘓

2.野指標成因:

  1. 指標未初始化:

#include <stdio.h>
int main()
{

    int* p;//區域性變數指標未初始化,預設為隨機值

    *p = 20;

    return 0;
}

  2. 指標越界訪問:

#include <stdio.h>
int main()
{

    int arr[10] = { 0 };

    int* p = arr;

    int i = 0;

    for (i = 0; i <= 11; i++)
    {

        //當指標指向的範圍超出陣列arr的範圍時,p就是野指標

        *(p++) = i;
    }

    return 0;
}

  3.指標指向的空間釋放:

int func()
{
    int *p = malloc(sizeof(int));
    free(p);//沒有將p值為NULL的操作 
}

3.如何避免野指標 

  1. 指標初始化
  2. 小心指標越界
  3. 指標指向空間釋放及時置NULL
  4. 指標使用之前檢查有效性

 

五.指標運算

1.指標+-整數

 在我們指標變數大小那塊我們便展示了一個例子,接下來我們繼續看一個:

#define N_VALUES 5

int main()
{
    float values[N_VALUES];
    float* vp;
    //指標+-整數;指標的關係運算
    for (vp = &values[0]; vp < &values[N_VALUES];)
    {
        *vp++ = 0;
    }
    for (int i = 0; i < N_VALUES; i++)
    {
        printf("%f ", values[i]);
    }
    return 0;
}

此例是運用指標來放置陣列變數

到這,我們便又掌握了一種運算元組的方法。(詳見六.指標與陣列)

 

2.指標-指標

 在之前我們模擬strcpy()的實現中,便用到此方法,下面我們繼續再來寫一下

//1. 計數器的方法
int my_strlen(char* str)
{
    int count = 0;
    while (*str != '\0')
    {
        count++;
        str++;
    }
    return count;
}
//2.遞迴實現
int my_strlen(const char* str)
{
    if (*str == '\0')
        return 0;
    else
        return 1 + my_strlen(str + 1);
}
//3.指標-指標
int my_strlen(char* s)
{
    char* p = s;
    while (*p != '\0')
        p++;
    return p - s;
}

詳情參見:C語言之庫函式的模擬與使用

在指標-指標中我們需要注意的一點就是:兩個指標一定要指向的是同一塊連續的空間

 

下面以一個例項來說明:

int main()
{
    int arr[10] = { 0 };
    printf("%d\n", &arr[0] - &arr[9]);//-9
    char ch[5] = {0};
    //指標-指標   絕對值的是指標和指標之間的元素個數
    printf("%d\n", &arr[9] - &ch[3]);//err
    //指標-指標 計算的前提條件是:兩個指標指向的是同一塊連續的空間的
    return 0;
}

我們會看到編譯器報一個警告

 

3.指標的關係運算

#define N_VALUES 5

int main()
{
    float values[N_VALUES];
    float* vp;
    //指標的關係運算

    for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
    {

        *vp = 0;
    }

    for (int i = 0; i < N_VALUES; i++)
    {
        printf("%f ", values[i]);
    }
    return 0;
}

我們來看看上面這個例子有沒有什麼問題

 

 

 

 

六.指標與陣列

我們先在這看一個例項:

#include <stdio.h>
int main()
{

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

    printf("%p\n", arr);

    printf("%p\n", &arr[0]);

    return 0;
}

在執行結果中,我們居然發現,這兩個地址居然一樣!

由此我們可以得到:陣列名錶示的是陣列首元素的地址。

那麼我們用指標來接收陣列時便可寫成這個樣子

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

int *p = arr;//p存放的是陣列首元素的地址
int main()
{
    int arr[5] = { 1, 2, 3, 4, 5 };
    int* p = arr;
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        printf("%d ", *(p + i));//通過指標來訪問陣列元素
    }
    printf("\n");
    for (i = 0; i < 5; i++)
    {
        printf("&arr[%d] = %p < === > %p\n", i, &arr[i], p+i);//列印兩地址看是否相同
    }
    return 0;
}

所以 p+i 其實計算的是陣列 arr 下標為i的地址。

那我們就可以直接通過指標來訪問陣列。

如下:

int main()
{
    int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    int* p = arr; //指標存放陣列首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ",*(p + i));
    }
    return 0;
}

 

七.二級指標

我們知道,存放地址變數的叫做指標變數,那存放指標變數的呢,自然就叫做二級指標變量了啊!

 

由此我們可以繼續推下去:

int main()
{
    int a = 10;
    int * p = &a;//p是一級指標
    int * * pp = &p;//pp是二級指標

    **pp = 20;
    printf("%d\n", a);//
    //int*** ppp = &pp;//三級指標
    //int**** pppp = &ppp;//四級指標
  //……………………
  //…………………… return 0; }

 

 

 

八.指標陣列

在談這個主題之前,我們先來想一想 指標陣列到底是指標還是陣列?

陣列

整形陣列 - 存放整形的陣列

字元陣列 - 存放字元的陣列

所以:

指標陣列 - 存放的指標

如:

int main()
{
    //int arr[10] = {0};//整形陣列
    //char ch[5] = { 'a', 'b' };//字元陣列
    //指標陣列
    int a = 10;
    int b = 20;
    int c = 30;

    //arr就是指標陣列
    //存放整形指標的陣列
    int* arr[3] = { &a, &b, &c };//int* 
    char* ch[5];//存放字元指標的陣列

    return 0;
}

 

九.相關例題

 在我們解決問題時,第一個遇到用指標的問題應該是這個:

寫一個函式可以交換兩個整形變數的內容。

#include <stdio.h>
void Swap1(int x, int y)
{
    int tmp = 0;
    tmp = x;
    x = y;
    y = tmp;
}

int main()
{
    int num1 = 1;
    int num2 = 2;
    Swap1(num1, num2);
    printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
    return 0;
}

當時,我們說,函式裡的 x,y 只是對於資料的一份臨時拷貝,而與真實資料並沒有本質的聯絡。

所以,我們把函式改寫成了:

void Swap2(int* px, int* py)
{
    int tmp = 0;
    tmp = *px;
    *px = *py;
    *py = tmp;
}

這時,我們或許知道了這樣是怎麼把值給交換過來的

我們將地址先傳過去,然後再進行解引用賦值操作,整個函式過程都與我們傳過去的變數息息相關

 

例題1:

#include <stdio.h>
int main()
{
    int a = 0x11223344;
    char* pc = (char*)&a;
    *pc = 0;
    printf("%x\n", a);
    return 0;
}

這一題要用到我們之前談到過的大小端儲存模式-----詳見: C語言之資料在記憶體中的儲存

首先 a在記憶體中按小端儲存的是 44 33 22 11

char* 只能解引用一個位元組,所以獲取的是 44 這個位元組

而現在 *pc=0 就是把這個位元組的值由 44 變為了 0;

所以該程式在Vs的編譯器結果是 0x 11 22 33 00

 

例題2:

#include <stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5 };
    short* p = (short*)arr;
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        *(p + i) = 0;
    }

    for (i = 0; i < 5; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

首先我們要知道 short* 解引用只能解引用 2 個位元組

而 int 型別為 4 個位元組

所以第一個for迴圈只改變了陣列 arr 的前兩個變數(改為了0)

所以最後的結果為: 0 0 3 4 5 

 

 

 

到此,我們便掌握了指標的一些基礎知識

下節,我們將談到一些指標的高階應用

詳見:C語言之漫談指標(下)

 

 

|------------------------------------------------------------------|

|因筆者水平有限,若有錯誤之處,還望多多指正。|

|------------------------------------------------------------------|

&n