系統性學習C語言-第十六講-深入理解指針(6)
- 1. ` sizeof ` 和 ` strlen ` 的對比
- 1.1 ` sizeof `
- 1.2 ` strlen `
- 1.3 ` sizeof ` 和 ` strlen ` 的對比
- 2. 數組和指針筆試題解析
- 2.1 一維數組
- 2.2 字符數組
- 2.3 二維數組
- 3. 指針運算筆試題解析
- 3.1 題目1:
- 3.2 題目2
- 3.3 題目3
- 3.4 題目4
- 3.5 題目5
- 3.6 題目6
- 3.7 題目7
1. sizeof
和 strlen
的對比
1.1 sizeof
在學習操作符的時候,我們學習了 sizeof
,sizeof
計算變量所占內存空間大小的,單位是字節,
如果操作數是類型的話,計算的是使用類型創建的變量所占內存空間的大小。
sizeof
只關注占用內存空間的大小,不在乎內存中存放什么數據。
比如:
#include <stdio.h>
int main()
{int a = 10;printf("%d\n", sizeof(a));printf("%d\n", sizeof a);printf("%d\n", sizeof(int));return 0;
}
1.2 strlen
strlen
是 C語言 庫函數,功能是求字符串長度。函數原型如下:
size_t strlen ( const char * str );
統計的是從 strlen
函數的參數 str
中這個地址開始向后,\0
之前字符串中字符的個數。
strlen
函數會?直向后找 \0
字符,直到找到為止,所以可能存在越界查找。
#include <stdio.h>
int main()
{char arr1[3] = {'a', 'b', 'c'};char arr2[] = "abc";printf("%d\n", strlen(arr1));printf("%d\n", strlen(arr2));printf("%d\n", sizeof(arr1));printf("%d\n", sizeof(arr2));return 0;
}
1.3 sizeof
和 strlen
的對比
2. 數組和指針筆試題解析
2.1 一維數組
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
在對代碼進行分析之前我們先回憶幾個重要知識點。
-
數組名通常代表數組首元素的地址,但是有例外。
-
sizeof(數組名)
,此時數組名代表整個數組。 -
&數組名
,此時取出的地址為整個數組的地址。
在對上面的知識點進行回憶后,我們便可以對代碼進行分析。
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //為數組名的特殊用法,代表整個數組,結果為 16
printf("%d\n",sizeof(a+0)); //數組名并未單獨放在 sizeof 內部,所以代表數組首元素,結果為 4/8
printf("%d\n",sizeof(*a)); //數組名為首元素地址,對其解引用后代表首元素,首元素類型為整形,結果為 4
printf("%d\n",sizeof(a+1));//數組首元素地址 + 1,仍為地址,結果為 4/8
printf("%d\n",sizeof(a[1]));//數組中第二個元素的字節數大小,結果為 4
printf("%d\n",sizeof(&a));//取出整個數組的地址,但仍為地址,結果為 4/8
printf("%d\n",sizeof(*&a));//先取出整個數組的地址,然后再解引用,仍然相當于直接求整個數組的字節數大小,結果為 16
printf("%d\n",sizeof(&a+1));//取出整個數組的地址然后 + 1,仍為地址,結果為 4/8
printf("%d\n",sizeof(&a[0]));//求數組第一個元素的地址的字節數大小,仍為地址,結果為 4/8
printf("%d\n",sizeof(&a[0]+1));//相當于求第二個元素的地址字節數大小,仍為地址,結果為 4/8
這里作者的運行環境為 32位,故地址的字節數為 4。
2.2 字符數組
練習 1:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
與上面一樣的步驟,接下來我們對代碼進行解析。
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//計算整個數組的字節數大小,為 6
printf("%d\n", sizeof(arr+0));//計算數組首元素地址的大小,為 4/8
printf("%d\n", sizeof(*arr));//計算首元素的大小,元素為字符類型,為 1
printf("%d\n", sizeof(arr[1]));//計算首元素的大小,元素為字符類型,為 1
printf("%d\n", sizeof(&arr));//取出整個數組的地址,結果仍為地址,字節數為 4/8
printf("%d\n", sizeof(&arr+1));//取出整個數組的地址,然后 + 1,相當于跳過了整個數組后的地址,仍為地址,字節數為 4/8
printf("%d\n", sizeof(&arr[0]+1));//相當于取出的是整個數組第二個元素的地址,仍為地址,字節數為 4/8
練習 2:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
解析:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//從首元素地址開始計算,由于 strlen 的特性,數組中并沒有存儲 \0,最終結果為隨機值
printf("%d\n", strlen(arr+0));//從首元素開始計算,與上面的解析同理,最終結果仍為隨機值
printf("%d\n", strlen(*arr));//*arr 解析出的結果為 a,a 的 ASCII值為 97,strlen 會將這個值作為地址進行訪問,最終結果為非法訪問
printf("%d\n", strlen(arr[1]));//arr[1] 解析出的結果為 b,與上面的解析同理,最終結果仍為非法訪問
printf("%d\n", strlen(&arr));//從數組的地址開始計算,結果為隨機值,與上面的解析同理
printf("%d\n", strlen(&arr+1));//從跳過整個數組的第一個地址開始計算,結果為隨機值,與上面解析同理
printf("%d\n", strlen(&arr[0]+1));//從數組的第二個元素地址開始計算,結果為隨機值,與上面解析同理
因有非法訪問,所以無法插入實機演示結果
練習 3:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
解析:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//求整個數組的字節數,結果為 7
printf("%d\n", sizeof(arr+0));//求數組首元素地址的字節數,結果為 4/8
printf("%d\n", sizeof(*arr));//求首元素的字節數,元素類型為字符,結果為 1
printf("%d\n", sizeof(arr[1]));//求數組第二個元素的字節數,元素類型為字符,結果為 1
printf("%d\n", sizeof(&arr));//求整個數組地址的字節數,結果為 4/8
printf("%d\n", sizeof(&arr+1));//求跳過整個數組后第一個地址的字節數,結果為 4/8
printf("%d\n", sizeof(&arr[0]+1));//求數組第二個地址的字節數,結果為 4/8
練習 4:
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
解析:
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//從首元素地址開始計算,結果為 6
printf("%d\n", strlen(arr+0));//從首元素地址開始計算,結果為 6
printf("%d\n", strlen(*arr));//*arr 地結果為 a ,對應地 ASCII值 為 97,strlen 會將 97 作為地址進行訪問,結果為非法訪問
printf("%d\n", strlen(arr[1]));//結果為非法訪問,arr[1] 結果為 b,其余解析與上面一樣
printf("%d\n", strlen(&arr));//從整個數組地地址開始計算,結果為 6
printf("%d\n", strlen(&arr+1));//從跳過整個數組地第一個地址開始計算,結果為隨機值
printf("%d\n", strlen(&arr[0]+1));//從數組地第二個元素地址開始計算,結果為 5
練習 5:
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
解析:
char *p = "abcdef";
printf("%d\n", sizeof(p));//計算指針 p 的大小,結果為 4/8
printf("%d\n", sizeof(p+1));//計算第二字符 b 的地址大小,結果為 4/8
printf("%d\n", sizeof(*p));//計算第一元素 a 的字節大小,結果為 1
printf("%d\n", sizeof(p[0]));//計算第一元素 a 的字節大小,結果為 1
printf("%d\n", sizeof(&p));//計算指針 p 的地址大小,結果為 4/8
printf("%d\n", sizeof(&p+1));//計算跳過指針 p 地址后的第一個地址大小,結果為 4/8
printf("%d\n", sizeof(&p[0]+1));//計算 b 的地址大小,結果為 4/8
練習 6:
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
解析:
char *p = "abcdef";
printf("%d\n", strlen(p));//從 p 開始計算,結果為 6
printf("%d\n", strlen(p+1));//從數組的第二個元素開始計算,結果為 5
printf("%d\n", strlen(*p));//*p 解析為 a ,ASCII值 為 97,strlen 會將 97 當作地址進行訪問,結果為非法訪問
printf("%d\n", strlen(p[0]));//p[0] 解析為 a,結果為非法訪問
printf("%d\n", strlen(&p));//從 p 的地址開始計算,結果為隨機值
printf("%d\n", strlen(&p+1));//從跳過 p 的地址后的第一個地址進行計算,結果為隨機值
printf("%d\n", strlen(&p[0]+1));//從數組的第二個元素的地址開始計算,結尾為 5
2.3 二維數組
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
解析:
int a[3][4] = {0};
printf("%d\n",sizeof(a));//計算整個 a 數組的字節數大小,結果為 48
printf("%d\n",sizeof(a[0][0]));//計算第一行第一個元素的大小,元素為整形類型,結果為 4
printf("%d\n",sizeof(a[0]));//a[0] 解析為 a數組 第一行的數組名,計算整個第一行的字節數大小,結果為 16
printf("%d\n",sizeof(a[0]+1));//a[0]+1 解析為 &a[0][1] ,計算第一行第二個元素地址的大小,結果為 4/8
printf("%d\n",sizeof(*(a[0]+1)));//*(a[0]+1) 解析為 a[0][1],計算第一行第二個元素的大小,結果為 4
printf("%d\n",sizeof(a+1));//計算 a數組 第二行地址的大小,結果為 4/8
printf("%d\n",sizeof(*(a+1)));//計算 a數組 第二行的大小,結果為 16
printf("%d\n",sizeof(&a[0]+1));//取出跳過第一行后的第一個地址,也就是第二行的地址,結果為 4/8
printf("%d\n",sizeof(*(&a[0]+1)));//取出第二行的地址然后解引用,求第二行的大小,結果為 16
printf("%d\n",sizeof(*a));// a 為首元素 a[0] 的地址,對其進行解引用表示第一行,求第一行的大小,結果為16
printf("%d\n",sizeof(a[3]));//a 數組并沒有第四行,最終結果為報錯
到此我們再對數組名的意義進行總結:
-
sizeof(數組名)
,這里的數組名表示整個數組,計算的是整個數組的大小。 -
&數組名
,這里的數組名表示整個數組,取出的是整個數組的地址。 -
除此之外所有的數組名都表示首元素的地址
3. 指針運算筆試題解析
3.1 題目1:
#include <stdio.h>
int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;
}
解析:
int *ptr = (int *)(&a + 1);
對于指針 ptr
所指向的地址 &a + 1
代表這跳過整個 a
數組地址后的第一個地址。
所以 *(ptr - 1)
就是對指針 ptr
的前一個地址進行解引用,也就是數組中最后一個元素的地址進行解引用。
對于 *(a + 1)
,a + 1
表示數組中第二個元素的地址,所以 *(a + 1)
表示對第二個元素的地址進行解引用。
最終的結果為 2,5
3.2 題目2
//在X86環境下
//假設結構體的??是20個字節
//程序輸出的結果是啥?
struct Test
{int Num;char *pcName;short sDate;char cha[2];short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}
解析:
要解出正確答案,我們就要清楚 指針 +1 ,會產生怎樣的操作,指針 + 1 會根據指針不同的步長,從而跳過不同的字節數,
所以對于 p + 0x1
跳過的就是結構體的字節數,結構體 Test
的字節數大小為 20 ,所以 p + 0x1
跳過的字節數大小為 20,
20 轉換為 16進制 為 0x14,所以結果為 0x100014,同樣的對于 (unsigned int*)p + 0x1
我們跳過則為 unsigned int 類型的字節數,
為 4 字節,所以最終結果為 0x100004,但是在(unsigned long)p + 0x1
p 被轉換成了無符號長整形類型,不再為指針,
所以這時的 +1 就不能再遵循指針的規則,而是遵循整形的算術規則,正常 + 1,結果為 0x100001,
對于 X86 環境下的地址顯示,32位系統在顯示地址時最多能顯示 8 位,所以我們要在結果前補上兩個 0,
最終結果為:
3.3 題目3
#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0];printf( "%d", p[0]);return 0;
}
解析:
對于這道題,我們一定要辨析好二維數組的初始化方式,題目中二維數組的初始化分組部分使用的是 ()
,而并非 {}
,
而小括號內部使用了逗號表達式,逗號表達式返回的是表達式中最后一個值,
所以實際二維數組的初始化其實應該為這樣 int a[3][2] = { 1, 3, 5 };
所以指針 p
,p = a[0];
取到的是數組第一行地址,第一行包含的元素為 1,3
,所以 p[0]
取到的第一個元素為 1,
最終結果為 1 。
3.4 題目4
//假設環境是x86環境,程序輸出的結果是啥?
#include <stdio.h>
int main()
{int a[5][5];int(*p)[4];p = a;printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);return 0;
}
解析:
首先我們對于 p[4][2]
進行分析, p[4][2]
相當于 *(p + 4)[2]
,這里就和指針步長扯上關系了,指針 p
的類型為 int [4]
,
所以每次 + 1 時,跳過 4 個整形類型的字節,因為 p = a
,所以指針 p
與 數組 a
的首元素地址時一樣的,
所以 p[4][2]
實際上跳過了 a
數組的 18 個元素,而 a[4][2]
跳過了數組的 22 個元素,指針 - 指針計算出的是指針之間的元素個數,
所以結果為 -4,但對于第一個 -4 我們要化成十六進制地址的形式,我們先寫出 -4 的源碼,然后求出補碼。
源碼:10000000 00000000 00000000 00000100
反碼:11111111 11111111 11111111 11111011
補碼:11111111 11111111 11111111 11111100
補碼的十六進制:FFFFFFFC
所以最終的結果為:
3.5 題目5
#include <stdio.h>
int main()
{int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };int *ptr1 = (int *)(&aa + 1);int *ptr2 = (int *)(*(aa + 1));printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));return 0;
}
解析:
我們先對指針ptr1
進行解析,int *ptr1 = (int *)(&aa + 1);
其中 &aa + 1
是跳過整個 aa
數組后的第一個整形指針地址
*ptr2 = (int *)(*(aa + 1));
其中 *(aa + 1)
中 aa
表示數組首元素地址,為 &aa[0]
,+ 1 后指向 &aa[1]
,
所以指針 ptr2
實際代表數組 aa
的第二行。因為 ptr1,ptr2
指針的類型都為整形指針類型,所以 +1,-1 都只會跳過一個整形的地址
所以 *(ptr1 - 1)
是對 aa
數組的最后一個元素地址進行解引用,*(ptr2 - 1)
是對數組第一行最后一個元素地址進行解引用
所以最終的結果為:
3.6 題目6
#include <stdio.h>
int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++;printf("%s\n", *pa);return 0;
}
解析:
我們先來分析字符指針數組 a
,a
的成員有三個,依次分別為 work
,at
,alibaba
。
因為 char**pa = a;
,所以指針 pa
指向數組 a
的首元素地址,pa++;
相當于 a[0] + 1
,數組 a
的第一個元素為 work
,+1后
指向第二個元素 at
,所以最終的打印結果為 at
。
3.7 題目7
#include <stdio.h>
int main()
{char *c[] = {"ENTER","NEW","POINT","FIRST"};char**cp[] = {c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n", **++cpp);printf("%s\n", *--*++cpp+3);printf("%s\n", *cpp[-2]+3);printf("%s\n", cpp[-1][-1]+1);return 0;
}
解析:
這里我們先將所有結構以圖標的形式呈現出來,以便我們更好地進行觀察。
printf("%s\n", **++cpp);
現在我們再對代碼進行分析,**++cpp
,其中 cpp
指針指向 cp
的首元素,在 ++
后指向了 cp
的第二個元素 c + 2
,
所以結果為 POINT
。
printf("%s\n", *--*++cpp+3);
在 *--*++cpp+3
中 ++cpp
此時指向 cp
的第三個元素 c + 1
,解引用符號的結合順序更高,先結合
再與 --
符號進行結合,此時就變成了 *--(c + 1) + 3
,結合后變成 *c + 3
,此時就是對 c
數組的首元素,
ENTER
的第四個元素開始輸出,最終結果為 ER
。
printf("%s\n", *cpp[-2]+3);
此時 cpp
指針在與兩個自增符號進行結合后,已經指向了 cp
數組的第三個元素,所以 cpp[-2]
指向了 cp
數組的第一個元素,
就表示為 *cp+3
,也就是從 FIRST
的第四個字符開始輸出,最終結果為 ST
。
printf("%s\n", cpp[-1][-1]+1);
此時的 cpp[-1]
代表 cp
的第二個元素,簡化為 cp[1][-1]+1
再次簡化為 *((c + 2) - 1) + 1
也就是 *(c + 1) + 1
,
從 NEW
的第二個字符開始輸出,結果為 EW
。