史上最強C語言教學----指標(初階)

2022-01-01 08:00:01

目錄

1、指標是什麼?

2. 指標和指標型別

2.1 指標+-整數

2.2 指標的解除參照

3. 野指標

3.1 野指標成因

3.2 如何規避野指標

4. 指標運算

4.1 指標+-整數

4.2 指標-指標

4.3 指標的關係運算

5. 指標和陣列

 6. 二級指標

7. 指標陣列


1、指標是什麼?

指標是什麼?

指標理解的2個要點:

  • 1. 指標是記憶體中一個最小單元的編號,也就是地址,即我們常說的指標即地址
  • 2. 平時口語中說的指標,通常指的是指標變數,是用來存放記憶體地址的變數,即指標即變數

總結:指標就是地址,口語中說的指標通常指的是指標變數。

那麼我們就可以這樣理解:

記憶體

指標變數

我們可以通過&(取地址操作符)取出變數的記憶體其實地址,把地址可以存放到一個變數中,這個 變數就是指標變數。

#include <stdio.h>
int main()
{
	int a = 10;//在記憶體中開闢一塊空間
	int* p = &a;//這裡我們對變數a,取出它的地址,可以使用&操作符。
	   //a變數佔用4個位元組的空間,這裡是將a的4個位元組的第一個位元組的地址存放在p變數
	中,p就是一個之指標變數。
		return 0;
}

總結:

指標變數,用來存放地址的變數。(存放在指標中的值都被當成地址處理)。

那這裡的問題是:

  • 一個小的單元到底是多大?(1個位元組)
  • 如何編址?

經過仔細的計算和權衡我們發現一個位元組給一個對應的地址是比較合適的。

對於32位元的機器,假設有32根地址線,那麼假設每根地址線在定址的時候產生高電平(高電壓)和低電平(低電壓)就是(1或者0);

那麼32根地址線產生的地址就會是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111

這裡就有2的32次方個地址。

每個地址標識一個位元組,那我們就可以給 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空閒進行編址。

同樣的方法,那64位元機器,如果給64根地址線,那能編址多大空間,自己計算,計算方法如上所示,此處不再計算。

這裡我們就明白: 在32位元的機器上,地址是32個0或者1組成二進位制序列,那地址就得用4個位元組的空間來儲存,所以 一個指標變數的大小就應該是4個位元組。 那如果在64位元機器上,如果有64個地址線,那一個指標變數的大小是8個位元組,才能存放一個地 址。

總結: 指標是用來存放地址的,地址是唯一標示一塊地址空間的。 指標的大小在32位元平臺是4個位元組,在64位元平臺是8個位元組。

2. 指標和指標型別

這裡我們在討論一下:指標的型別 我們都知道,變數有不同的型別,整形,浮點型等。

那指標有沒有型別呢? 準確的說:有的。 當有這樣的程式碼:

int num = 10;
p = &num;

要將&num(num的地址)儲存到p中,我們知道p就是一個指標變數,那它的型別是怎樣的呢? 我們給指標變數相應的型別。

char  *pc = NULL;
int   *pi = NULL;
short *ps = NULL;
long  *pl = NULL;
float *pf = NULL;
double *pd = NULL;

這裡可以看到,指標的定義方式是: type + * 。

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

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

int* 型別的指標是為了存放 int 型別變數的地址。 那指標型別的意義是什麼?下面我們會進行講解!

2.1 指標+-整數

#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;
}

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

2.2 指標的解除參照

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0;   //重點在偵錯的過程中觀察記憶體的變化。
	*pi = 0;   //重點在偵錯的過程中觀察記憶體的變化。
	return 0;
}

下圖為執行完*pc = 0之後n的記憶體空間

由圖可以看出,我們執行完該語句後,只改變了一個位元組的空間所對應的值!

 下圖為執行完*pi = 0之後的記憶體空間,與我們定義的指標型別一致,因為我們定義的pc是char型別的指標變數,char只佔據四個位元組的記憶體空間。

 由圖可以看出,我們這次是改變了四個位元組的空間所對應的值,與我們定義的指標型別所一致,因為pi是int 型別的指標變數,int在記憶體中佔據四個位元組的空間。

總結:

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

比如: char* 的指標解除參照就只能存取一個位元組,而 int* 的指標的解除參照就能存取四個位元組。

3. 野指標

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

3.1 野指標成因

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. 指標指向的空間釋放(後續會講,此處暫時不講)

3.2 如何規避野指標

  • 1. 指標初始化
  • 2. 小心指標越界
  • 3. 指標指向空間釋放即使置NULL
  • 4. 避免返回區域性變數的地址
  • 5. 指標使用之前檢查有效性
#include <stdio.h>
int main()
{
    int* p = NULL;
    //....
    int a = 10;
    p = &a;
    if (p != NULL)
    {
        *p = 20;
    }
    return 0;
}

4. 指標運算

4.1 指標+-整數

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

 指標+-整數實際上是跨越指標所指變數型別的位元組數乘以我們加的數位,比如在上面的例子中,我們一開始是將整型指標p進行了+1操作,然後我們對p+1進行以地址形式列印後,相比原來的p,增加了4個位元組,同理,後面的p2是char型別的指標,我們對其進行+1操作後,相比原來的p2的地址,增加了1個位元組。

4.2 指標-指標

#include<stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p1 = &arr[4];
	int* p2 = &arr[0];
	printf("%d", p1 - p2);
	return 0;
}

上面進行相減後得出的結果是4,為什麼會得出4這個結果呢?因為p1和p2之間相差4個整型元素,這個地方為什麼我們要強調是整形元素呢?因為我們在運運算元兩側的指標型別均是整型指標,實際上指標型別的變數進行相減是指標所代表的地址進行相減,將得出的值除以指標所指向的空間所佔據的位元組數,這樣將大家不是很容易理解,有一點點抽象,我給大家簡單舉一下例子,比如在上面這個例子中p1中所儲存的地址值是16,而p2中所儲存的值是0,那麼我們將p2減去p1後得出的16除以p1和p2所指向的資料型別即整型型別,每個整型元素所佔據的位元組數為4,所以16除以4之後的結果為4,即最終在螢幕上的輸出結果為4,那麼很多小夥伴就問了,在上面這個例子中,我們將兩個指標的型別進行強制轉換為char型別後得出的結果是不是就會改變為16了呢,因為根據上面的我給出的推理方式確實應該是這樣的,因為char型別佔據的位元組數為1,16除以1之後的結果仍為16,接下來我們程式碼展示一下是不是就像我們的推理一樣,得出的結果是16.

 同我們的推導一樣,這就印證了我們的推導是正確的!

4.3 指標的關係運算

既然指標變數中所儲存的是地址,地址從本質上來說也只是一串資料,那麼也是可以進行比較大小的!此處不再進行舉例給大家進行展示,因為不難理解,但是下面會給大家進行強調一個要點!

標準規定:

允許指向陣列元素的指標與指向陣列最後一個元素後面的那個記憶體位置的指標比較,但是不允許與 指向第一個元素之前的那個記憶體位置的指標進行比較。

5. 指標和陣列

我們看一個例子:

#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;
}

執行結果:

可見陣列名和陣列首元素的地址是一樣的。

結論:陣列名錶示的是陣列首元素的地址。(2種情況除外,陣列章節講解了)

那麼這樣寫程式碼是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是陣列首元素的地址

既然可以把陣列名當成地址存放到一個指標中,我們使用指標來存取一個就成為可能。

例如:

#include <stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
    int* p = arr; //指標存放陣列首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], 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;
}

 6. 二級指標

指標變數也是變數,是變數就有地址,那指標變數的地址存放在哪裡? 這就是二級指標 。

對於二級指標的運算有:

  • *ppa 通過對ppa中的地址進行解除參照,這樣找到的是 pa , *ppa 其實存取的就是 pa .
int b = 20;
*ppa = &b;//等價於 pa = &b;
  • **ppa 先通過 *ppa 找到 pa ,然後對 pa 進行解除參照操作: *pa ,那找到的是 a .
**ppa = 30;
//等價於*pa = 30;
//等價於a = 30;

7. 指標陣列

指標陣列是指標還是陣列?

答案:是陣列。是存放指標的陣列。

陣列我們已經知道整形陣列,字元陣列。

int arr1[5];
char arr2[6];

 那指標陣列是怎樣的?

int* arr3[5];//是什麼?

arr3是一個陣列,有五個元素,每個元素是一個整形指標。