1. 程式人生 > >初階C-指標總結與坑

初階C-指標總結與坑

指標的定義

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

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

  1. 指標的大小在32位平臺上是4個位元組,在64位平臺上是8個位元組。
  2. 指標是存放地址才出現的,地址是為了標示一塊地址空間的。
  3. 指標讓地址有地方存放,指標讓記憶體的訪問更加方便。

指標和指標型別

有這麼一個程式碼

int num = 10;
p = #

要將&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型別的變數的地址。

普通的指標變數,指標型別,表示指標指向的變數的型別。一個普通的指標,知道對應這個變數的房間號,以及房間的大小。

void*的使用:只知道房間號,但是不關注房間大小。

  • void*型別不能解引用
  • void*型別的指標不能和整數進行相加減
  • void*型別的兩個指標也不能相減

然而這樣一個程式碼

int a = 10;
char* p = &a;
printf("%d\n",*p);

列印的結果是多少?列印的結果還是10。但是int*和char*型別是不相容的,但是我們在存放資料時,要考慮到大端和小端的情況,低位在低地址 *p的解引用 對於char*得看它是怎麼放置的。

通俗來說,int*本身有4個位元組,但是非要讓它char*去表示,那麼只能去考慮一個位元組。打印出它的第一個地址儲存的資料。但這第一個地址是怎麼存放資料的,我們要考慮大端與小端的情況。因為就一個 0a,可能在4個位元組中,最末尾的位置,也可能是第一個位置。這都是根據機器設定的位元組序來得出結果的。

這裡說一下機器的位元組序:

大端:低位在高地址上

小端:低位在低地址上

指標 +/- 整數

首先站在上帝視角,對指標進行運算是一件非常危險的事情,這也是一個埋下大坑的開始!!

指標的加減與普通的數字加減不太一樣。

char* p = NULL;
p = p + 1;
//結果為1
short* p = NULL;
p = p + 1;
//結果為2
int* p = NULL;
p = p + 1;
//結果為4
double* p = NULL;
p = p + 1;
//結果為8

指標和整數+1並不是地址+1,而是跳過當前指向的元素。-1是往低地址跳過一個元素(可以說是往前跳)

指標-指標
int arr[] = {1, 2, 3, 4};
int* p1 = arr;
int* p2 = arr + 3;
printf("%d\n",p2 - p1);

指標相減,就是指標加減整數的逆運算。指標相減就是看指標之間隔了多少個元素。

語法是允許,但是很多情況下是沒有意義的。比如兩個不同型別的去相減,沒有什麼實際的意義。除非兩個指標指向了同一個連續的記憶體空間,此時才是有意義的。

指標的解引用

解引用:"*"的作用是引用指標指向的變數值,引用其實就是引用該變數的地址,“解”就是把該地址對應的東西解開,解出來,就像開啟一個包裹一樣,那就是該變數的值了,所以稱為“解引用”。

也就是說,解引用是返回記憶體地址中儲存的值。

指標的型別決定了對指標解引用的時候有多大的許可權。

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

這裡補充一個const的使用。

const int* p => p 指向的內容不能修改

int const *p => p指向的內容不能修改

int* p const p => p 的指向不能修改

二級指標

二級指標並沒有很難理解,就是將一直指標看成一個常量,在引用一個指標。

可以類比於二維陣列,它其實還是一個一維陣列,只是每個元素是一個一維陣列。

二級指標

當我們覺得不太好理解時,可以這樣

typedef int* int_ptr
int a = 10;
int_ptr p = &a;
int_ptr* pp = &p;

將int*定義成一個變數來代替,這樣我們看著就比較方便了。

指標和陣列名

指標和陣列名往往是可以相互轉化的。

int arr[] = {1,2,3,4}
int* p = arr + 1;

此時的含義是,陣列的首地址元素+1,跳過這個當前指向的元素,此時*p為2。

&arr => 陣列指標:是一個指標,指向一個數組的指標。

指標陣列 vs 陣列指標

int a[] = { 1, 2, 3, 4};
printf("%p\n",a);//指向的int
printf("%p\n",&a);
//得出的地址是一樣的,改變一下
printf("%p\n",&a + 1);
//這直接跳過了整個陣列,指向了整個陣列
int* a[] = { 0 };//每個元素是一個指標

指標的大坑

第一組

int a[] = { 1, 2, 3, 4};
printf("%d\n",sizeof(a));//陣列的大小,16
printf("%d\n",sizeof(a + 0));//a此時變成了指標,4(32位系統下)
printf("%d\n",sizeof(*a));//a變成首元素的指標,得到了一個整數,4
printf("%d\n",sizeof(a + 1));//a此時變成了指標,4
printf("%d\n",sizeof(a[1]));//第二個元素的大小,4
printf("%d\n",sizeof(&a));//此時是陣列指標,指標就一定佔4個位元組!
printf("%d\n",sizeof(*&a));//此時先位陣列指標,指向整個陣列,每個陣列內元素解引用,還原陣列,16
printf("%d\n",sizeof(&a + 1));//陣列指標+1還是陣列指標,還是為4
printf("%d\n",sizeof(&a[0]));//a[0]取元素,整數,取完地址為int*,還是為4

第二組

char a[] = { 'a', 'b', 'c', 'd', 'e', 'f'};
printf("%d\n",sizeof(a));//型別為char型,佔1個位元組,共6個
printf("%d\n",sizeof(a + 0));//變成指標,4
printf("%d\n",sizeof(*a));//轉成指標,解引用,得到是字元a,1
printf("%d\n",sizeof(a[1]));//1
printf("%d\n",sizeof(&a));//陣列指標,4
printf("%d\n",sizeof(&a + 1));//還是4,陣列指標+1還是指標
printf("%d\n",sizeof(&a[0] + 1));//char* 還是一個指標

printf("%d\n",strlen(a));// strlen從當前位置找到\0,如果找不到則越界,那麼就是未定義行為
printf("%d\n",strlen(a + 0));//strlen()函式都當成char*指標來處理,陣列中沒有\0,依然是未定義行為
printf("%d\n",strlen(*a));//得到一個字元,不能放到char*裡,間接級別不同,所以依然是未定義行為
printf("%d\n",strlen(a[1]));//得到一個字元,都是未定義i行為
printf("%d\n",strlen(&a));//依然是個未定義行為
printf("%d\n",strlen(&a + 1));//未定義行為
printf("%d\n",strlen(&a[0] + 1));//未定義行為

第三組

char* p = 'abcdef';
printf("%d\n",sizeof(p));//只要是指標,4個位元組
printf("%d\n",sizeof(p + 1));//還是指標,4個位元組
printf("%d\n",sizeof(*p));//解引用,char型別,得到a,1個位元組
printf("%d\n",sizeof(p[0]));//等價與*p,1
printf("%d\n",sizeof(&p));//指標取地址,就是二級指標,4
printf("%d\n",sizeof(&p + 1));//二級指標,4
printf("%d\n",sizeof(&p[0]));//a取地址,就是指向a的指標,+1就是指向b的指標,還是4

printf("%d\n",strlen(p));//6,‘\0’不取,strlen()不算在其長度中
printf("%d\n",strlen(p + 1));//5,p指向a,p+1指向b,從b開始到結束,只佔了5個
printf("%d\n",strlen(*p));//得到是一個字元,不能用strlen(),未定義行為
printf("%d\n",strlen(p[0]));//未定義行為
printf("%d\n",strlen(&p));//二級指標不能進行,還是未定義行為
printf("%d\n",strlen(&p + 1));//未定義行為
printf("%d\n",strlen(&p[0] + 1));//a的指標,+1為b的指標,此時從b到結束,5
第四組
char a[] = "abcdef";
printf("%d\n",sizeof(a));//7,還有'\0'得算上,strlen()到\0結束,不計算入內
printf("%d\n",sizeof(a + 0));//此時變為了指標,4
printf("%d\n",sizeof(*a));//解引用得到字元a,1
printf("%d\n",sizeof(a[1]));//1
printf("%d\n",sizeof(&a));//還是4,陣列指標
printf("%d\n",sizeof(&a + 1));//4,還是陣列指標
printf("%d\n",sizeof(&a[0] + 1));//是一個char*指標,4

printf("%d\n",strlen(a));//6,不計算\0
printf("%d\n",strlen(a + 0));//還是指向首元素的地址,6
printf("%d\n",strlen(*a));//解引用得到字元,未定義
printf("%d\n",strlen(a[1]));//未定義行為
printf("%d\n",strlen(&a));//陣列名取地址,變為陣列指標,得到的是首元素地址,依次往後去找。得到的還是6
printf("%d\n",strlen(&a + 1));//陣列指標+1跳出整個陣列了,找不到\0了,未定義行為
printf("%d\n",strlen(&a[0] + 1));//指向b,直到\0,為5
第五組
int a[3][4] = { 0 };
printf("%d\n",sizeof(a));//3*4*4=48
printf("%d\n",sizeof(a[0][0]));//4
printf("%d\n",sizeof(a[0]));//16,第一行
printf("%d\n",sizeof(a[0] + 1));//4,陣列指標,指標就是4
printf("%d\n",sizeof(*(a[0] + 1)));//*(p + 1) => p[1],還是指標,即a[0][1] + 1,解引用,為4
printf("%d\n",sizeof(a + 1));//變為指標,還是為4
printf("%d\n",sizeof(*(a + 1));//*(a + 1) => a[1],所以陣列為第二行,16
printf("%d\n",sizeof(&a[0] + 1));//得到為陣列指標,+1還是陣列指標,4
printf("%d\n",sizeof(*(&a[0] + 1)));//陣列a[0],&為陣列指標,在+1跳到第二個元素,在解引用為a[1],16
printf("%d\n",sizeof(*a));//*a => a[0] ,16
printf("%d\n",sizeof(a[3]));//下標越界,sizeof在編譯過程中得到型別,長度為4個元素的陣列。

這些大坑多種形式,真的需要沒事就看看,指標在C中是一個很重要的工具,在之前的靜態順序表中已經出現過,在連結串列中還出現了二級指標,所以需要多理解,多思考!