目錄
前言
一、sizeof和strlen 的區分比較
二、sizeof,strlen與數組的計算
三、指針運算,筆試難題解析
總結
前言
? ? ? ? 本文作為指針進階的最后一篇文章,給大家帶來了豐富的例題,這其中包括區分比較sizeof和strlen計算各種花樣的數組指針表達式,如果你能答對所有的關于sizeof和strlen的計算例題,那么關于sizeof和strlen的計算你就無敵了。另外最主要的還是指針的運算筆試難題,這些筆試真題就能幫我們更深入的理解指針,最終成為C語言大佬,當然指針還未結束,最后還是需要自己理解和積累,希望本文對大家有所幫助
一、sizeof和strlen 的區分比較
sizeof | strlen |
1.sizeof是操作符 | 1.strlen是庫函數,使用時需包含頭文件<string.h> |
2.sizeof計算操作數所占的內存大小, 單位是字節,返回類型為size_t | 2.strlen是計算字符串長度的,統計的是\0之前字符 的個數,返回類型為size_t |
3.sizeof不關注內存中存放的數據,sizeof中 如果是表達式也不會真正的被計算 | 3.關注內存中是否有\0,如果沒有\0,就會 繼續往后找,可能會導致越界訪問 |
4.sizeof 傳入的參數可以是變量名, 可以是類型名,也可以是整數、浮點數等 | 4.strlen 的形參是一個字符指針,也就是 需要計算的字符串首元素地址 |
strlen的形參
sizeof不關注數據內容體現在以下代碼:
#include <stdio.h>int main()
{int a = 10;printf("%zd\n", sizeof(a));//計算a大小printf("%zd\n", sizeof(int));//直接計算類型大小printf("%zd\n", sizeof(10));//甚至直接計算整數大小return 0;
}
運行結果:
strlen關注內存中是否有\0體現在以下代碼:
#include <stdio.h>
#include <string.h>int main()
{char ch1[] = "abcdef";//字符串賦值末尾自帶一個\0char ch2[] = { 'a','b','c','d','e','f' };//末尾沒有\0char ch3[] = { 'a','b','c','d','e','f' ,'\0' };//末尾手動添加\0printf("%zd\n", strlen(ch1));printf("%zd\n", strlen(ch2));printf("%zd\n", strlen(ch3));return 0;
}
運行結果:
出現38的結果就是因為 ch2 數組中沒有\0,strlen只能在數組后面的內存中去尋找\0,也就是越界訪問了,最終會返回一個隨機值
二、sizeof,strlen與數組的計算
以下就是使用 sizeof 和 strlen 計算數組的題目,你能答對幾道?可不要小看這些計算,一不小心就會犯錯誤,重要的還是理解。
注意:以下涉及的知識與我主頁指針進階(1)數組與指針有關,即數組名為數組首元素地址,但有兩個例外:
1. sizeof(數組名),數組名單獨放在sizeof中,此時數組名表示整個數組,計算的是整個數組大小
2. &數組名,取出的是整個數組的地址,也就是一個數組指針
如不了解,可前去預覽,以便更好的理解以下代碼
注:以下代碼中行末尾注釋的數字為每一行的答案,4/8表示在x86或x64位平臺下不同的結果
例1:小試牛刀
#include <stdio.h>int main()
{int a[] = { 1,2,3,4 };printf("%zd\n", sizeof(a));//16,a單獨放在sizeof中表示計算整個數組大小printf("%zd\n", sizeof(a + 0));//4/8,非單獨表示指針printf("%zd\n", sizeof(*a));//4,解引用首元素地址printf("%zd\n", sizeof(a + 1));//4/8 等價于&a[1]printf("%zd\n", sizeof(a[1]));//4 printf("%zd\n", sizeof(&a));//4/8,&a表示取出的是整個數組的地址,是一個數組指針printf("%zd\n", sizeof(*&a));//16,*與&抵消,相當于a單獨放在sizeof中printf("%zd\n", sizeof(&a + 1));//4/8,數組指針加1,表示跳過整個數組的后一個數組指針 printf("%zd\n", sizeof(&a[0]));//4/8,取出第一個元素地址printf("%zd\n", sizeof(&a[0] + 1));//4/8,等價于&a[1]return 0;
}
例2:
#include <stdio.h>int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zd\n", sizeof(arr));//6,數組名單獨放在sizeof里,表示計算整個數組大小printf("%zd\n", sizeof(arr + 0));//4/8,非單獨放,表示首元素地址printf("%zd\n", sizeof(*arr));//1,解引用首元素地址,指向字符aprintf("%zd\n", sizeof(arr[1]));//1,指向字符bprintf("%zd\n", sizeof(&arr));//4/8,取出的是一個數組指針printf("%zd\n", sizeof(&arr + 1));//4/8,還是一個數組指針printf("%zd\n", sizeof(&arr[0] + 1));//4/8,相當于&arr[1]return 0;
}
例3:
#include <stdio.h>
#include <string.h>int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zd\n", strlen(arr));//隨機值printf("%zd\n", strlen(arr + 0));//隨機值,等于第一行printf("%zd\n", strlen(*arr));//訪問地址為97處的內存,程序崩潰printf("%zd\n", strlen(arr[1]));//訪問地址為98處的內存,程序崩潰printf("%zd\n", strlen(&arr));//隨機值,等于第一行printf("%zd\n", strlen(&arr + 1));//隨機值,等于第一行減6printf("%zd\n", strlen(&arr[0] + 1));//隨機值,等于第一行減1return 0;
}
例4:
#include <stdio.h>int main()
{char arr[] = "abcdef";//以字符串字面量進行賦值,末尾有隱藏了的\0printf("%zd\n", sizeof(arr));//7,計算包括了\0在內的7個字符printf("%zd\n", sizeof(arr + 0));//4/8,非數組名單獨放在sizeof里,等價于&arr[0]printf("%zd\n", sizeof(*arr));//1,解引用首元素的地址,指向字符aprintf("%zd\n", sizeof(arr[1]));//1,指向bprintf("%zd\n", sizeof(&arr));//4/8,取出的是數組指針printf("%zd\n", sizeof(&arr + 1));//4/8,還是一個數組指針printf("%zd\n", sizeof(&arr[0] + 1));//4/8,等價于&arr[1]return 0;
}
例5:
#include <stdio.h>
#include <string.h>int main()
{char arr[] = "abcdef";//末尾有\0printf("%zd\n", strlen(arr));//6printf("%zd\n", strlen(arr + 0));//6printf("%zd\n", strlen(*arr));//訪問地址為97處的內存空間,程序崩潰printf("%zd\n", strlen(arr[1]));//訪問地址為98處的內存空間,程序崩潰printf("%zd\n", strlen(&arr));//6printf("%zd\n", strlen(&arr + 1));//跳過該數組,越界訪問,返回隨機值printf("%zd\n", strlen(&arr[0] + 1));//5return 0;
}
例6:
#include <stdio.h>int main()
{char* p = "abcdef";//p接收的是字符串的首元素地址printf("%zd\n", sizeof(p));//4/8 注意指針變量就是指針,數組名是數組名,這兩者這不一樣printf("%zd\n", sizeof(p + 1));//4/8,等價于&p[1]printf("%zd\n", sizeof(*p));//1,指向字符aprintf("%zd\n", sizeof(p[0]));//1,字符aprintf("%zd\n", sizeof(&p));//4/8,指針變量的地址,相當于一個二級指針printf("%zd\n", sizeof(&p + 1));//4/8,還是一個二級指針printf("%zd\n", sizeof(&p[0] + 1));//4/8,字符串中b的地址return 0;
}
例7:
#include <stdio.h>
#include <string.h>int main()
{char* p = "abcdef";printf("%zd\n", strlen(p));//6printf("%zd\n", strlen(p + 1));//5printf("%zd\n", strlen(*p));//訪問地址為97處的內存空間,程序崩潰printf("%zd\n", strlen(p[0]));//訪問地址為97處的內存空間,程序崩潰printf("%zd\n", strlen(&p));//傳入的是p變量本身的地址,返回隨機值printf("%zd\n", strlen(&p + 1));//傳入的是跳過b變量地址的地址,返回隨機值printf("%zd\n", strlen(&p[0] + 1));//5return 0;
}
例8:二維數組
#include <stdio.h>int main()
{int a[3][4] = { 0 };printf("%zd\n", sizeof(a));//48,數組名單獨放在sizeof中,計算的是整個數組大小printf("%zd\n", sizeof(a[0][0]));//4,表示第一行第一個元素printf("%zd\n", sizeof(a[0]));//16,a[0]表示第一行數組的數組名,單獨放在sizeof中printf("%zd\n", sizeof(a[0] + 1));//4/8,等價于&a[0][1]printf("%zd\n", sizeof(*(a[0] + 1)));//4,等價于a[0][1]printf("%zd\n", sizeof(a + 1));//4/8,a表示數組首元素地址也就是&a[0],a+1就是&a[1]printf("%zd\n", sizeof(*(a + 1)));//16,繼上一行,a+1再解引用相當于a[1]數組名單獨放sizeof中printf("%zd\n", sizeof(&a[0] + 1));//4/8,等價于&a[1]printf("%zd\n", sizeof(*(&a[0] + 1)));//16,繼上一行,&與*抵消,相當于a[1]單獨放在sizeof中printf("%zd\n", sizeof(*a));//16,等價于*&a[0],相當于a[0]數組名單獨放在sizeof中printf("%zd\n", sizeof(a[3]));//16,這里一定記住:sizeof中的表達式不會真正計算,這里不會越界 訪問,因此依舊表示數組名a[3]單獨放在sizeof中,計算的是a[3]整個數組大小return 0;
}
注意:sizeof()中的表達式不會真正的被計算
例如:
三、指針運算,筆試難題解析
題目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;
}
運行結果:
畫圖解析:
- 因為&a得到一個數組指針,+1就跳過一個數組,指向了a[5]的末尾,因此(&a+1)應指向如圖所示的位置,因為此時(&a+1)還是一個數組指針,因此強制轉換為 int* ,再傳給ptr
- int* 類型指針-1往低地址處移動4個字節,所以ptr-1指向的就是5,而 *(a+1) 就等價于 a[1],指向的是數組第二個元素2
題目2:
//在X86環境下
//假設結構體的??是20個字節
//程序輸出的結果是啥?
#include <stdio.h>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;
}
運行結果:
解析:
- 首先定義了一個結構體指針p,p儲存的地址就是被強轉為結構體指針類型的0x100000
- printf("%p\n", p + 0x1),0x1表示16進制數字1,所以p+0x1就是p+1,p為一個結構體類型的指針變量,+1就是跳過一個結構體大小的字節,而結構體大小就是20個字節,20轉換為16進制就是0x14,所以p+1 = 0x100000+0x14 = 0x100014,%p打印,會打印完整地址,因此不足位前面補0,最終結果就是 00100014
- printf("%p\n", (unsigned long)p + 0x1), (unsigned long)p將結構體指針類型的p強制轉換為無符號整形p,其中儲存的地址就會變為無符號整數,因此最終結果就是兩個整數相加,也就是 0x100000+0x1 = 0x100001 ,最后以地址的格式打印出來就是 00100001
- printf("%p\n", (unsigned int*)p + 0x1),這里就是將p強轉為無符號整形指針類型,+1跳過一個無符號整形大小的字節,也就是4個字節,因此最終結果就是 00100004
題目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;
}
運行結果:
解析:
- 首先我們需要關注{ (0, 1), (2, 3), (4, 5) },這里面是3個逗號表達式,逗號表達式結果取決于其最后一位,所以大括號里面實際只有 1,3,5,這三個數
- a[0]就為第一行元素的首地址,所以p[0] 等價于 *(p+0),也就是指向第一行第一個元素1
題目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;
}
運行結果:
解析:
- 如圖所示,a本為 int (*)[5] 類型,卻強制賦值給p int (*)[4] 類型,導致兩者出現上圖分配情況,通過畫圖我們不難找到 p[4][2] 和 a[4][2] 的位置
- 我們知道數組中兩指針相減,那么得到的就是兩指針之間的元素個數,因為p[4][2]地址小于a[4][2],所以得到的是 -4,-4以%d的格式打印就是-4,但是-4以%p打印就不一樣了
- -4以%p打印,打印的是-4在內存中的補碼,以地址的格式打印。-4的補碼就為1111 1111 1111 1111 1111 1111 1111 1100,每四個二進制位以地址的16進制打印就是 FFFFFFFC
題目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;
}
運行結果:
解析:
- (int*)(&aa + 1),&aa+1取出整個數組的地址,可以理解為二維數組的數組指針,+1就跳過整個數組,來到數組的末尾,然后強轉為(int*)類型,賦給ptr1
- (int*)(*(aa + 1)),可以直接理解為(int*)aa[1],*(aa+1)就表示跳過一個元素解引用,也就是aa[1],指向的就是第二個數組元素的首地址,賦給ptr2
- 因此,ptr1,ptr2都被強轉為int*類型,這樣-1就往地址跳過一個整形大小的字節,也就是分別指向10和5
題目6:
//程序運行的結果是啥?
#include <stdio.h>int main()
{char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;
}
運行結果:
解析:
- a是一個字符指針數組,它里面3個元素分別對應后面三個字符串字面量的首字符地址
- 因為a是數組首元素地址,它指向的是一個字符指針,因此接收a需要一個二級指針變量,也就是pa,給pa賦值a,pa開始指向的是a[0]的地址,pa++后,pa向后移動一個地址,指向了a[1]的地址,因此*pa就等于a[1],以%s打印字符串需要字符串首元素地址,a[1]儲存的是at\0的首元素地址,因此最終打印at
題目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;
}
運行結果:
解析:(cpp是三級指針,接收二級指針變量的地址)
- 首先第一處打印,**++cpp,*與前置++優先級相同,結合性從右到左,因此cpp先自增1,cpp就指向cp[1]的地址,然后*++cpp,解引用得到的就是cp[1]的內容c+2,最后* *++cpp,再解引用,就是解引用c+2,指向的就是c[2]的內容,也就是POINT\0的首字符地址,最后以%s打印就是POINT。由于cpp指向發生變化,上圖的需進行修改
- 第二處打印,*-- * ++cpp + 3,我們來一步一步分析,以優先級和結合性,首先是++cpp,那么cpp自增1就指向了cp[2]的地址了,然后*++cpp,解引用cp[2]的地址,得到的就是cp[2]指向的內容c+1,然后--*++cpp,c+1自減1,就是把cp[2]的內容從c+1修改為c,然后*--*++cpp,再解引用,這時解引用的是c,指向的就是c[0]的內容,也就是ENTER\0的首字符地址,最后*--*++cpp+3,加3表示跳過3個字節(因為指針為char類型),此時指向的就是字符E的地址,因此最終打印的結果就是ER。由于以上變化,我們再重新繪圖
- 第三處打印,*cpp[-2] + 3 ==>(等價于) **(cpp-2)+3,首先cpp-2,改變指向的內容為cp[0]的地址,然后*(cpp-2),解引用得到cp[0]的內容c+3,然后**(cpp-2),再解引用得到的就是c+3也就是c[3]所指向的內容,也就是FIRST\0的首字符地址,最后**(cpp-2)+3,加3跳過3個字節,指向字符S的地址,最終打印的就是ST。由于上述操作并未實質改變指針指向的內容,只是表達式的計算,所以不需重新繪圖
- 第四處打印,cpp[-1][-1] + 1 ==> *(*(cpp-1))-1)+1,首先cpp-1,指向的是cp[1]的地址,然后*(cpp-1),解引用得到cp[1]指向的內容c+2,然后*(cpp-1)-1,減一表示把c+2減1,導致改變cp[1]中的內容從c+2變為c+1,然后*(*(cpp-1)-1),再解引用c+1,也就是解引用c[1]的內容,c[1]的內容指向的是字符串字面量NEW\0的首字符地址,最后*(*(cpp-1))-1)+1,加1表示跳過一個字節,此時指向的就是字符E的地址,最終打印的結果就是EW。
總結
? ? ? ? 至此,我就解析完了本文的所有題目,希望對大家有所幫助,也很感謝大家的支持,大家有什么疑問歡迎評論區指出