說明:所有題目均摘錄於網絡以及我所見過的面試題目,歡迎補充!
無特殊說明情況下,下面所有題s目都是linux下的32位C程序。
先來幾個簡單的熱熱身。
1、計算以下sizeof的值。
char str1[] = {'a', 'b', 'c', 'd', 'e'}; char str2[] = "abcde";
char *ptr = "abcde";
char book[][80]={"計算機應用基礎","C語言","C++程序設計","數據結構"};
sizeof(str1)=?
sizeof(str2)=?
sizeof(ptr)=?
sizeof(book)=?
sizeof(book[0])=?
分析:
sizeof(str1)=5,就是5*sizeof(char)=5;
sizeof(str2)=6,字符串都是以'\0'結尾,所以所占字節數為6;
sizeof(ptr)=4,ptr是一個指針,在32位平台上大小為4字節;
sizeof(book)=320,book是一個二維數組,4*80*1
sizeof(book[0])=80,book[0]是第一維數組,因為此80*1
根據sizeof求數組元素的個數也很簡單,拿第一個來說,就是sizeof(str1)/sizeof(char)。
2、上面是求計算他們所占字節數,下面來看看怎么求字符串或數組的實際長度。計算下面strlen值。
char arryA[] = {'a','b','c','\0','d','e'};
char arryB[] = {'a','b','c','d','e'};
char arryC[6] = {'a','b','c','d','e'};
char *str = "abcde";
分析:
strlen(arryA) = 3,strlen遇到'\0'就會返回,無論后面有多少個字符;
strlen(arryB)長度無法確定,沒有人為寫入‘\0’,strlen會繼續計算直到找到結束符,結果未知;
strlen(arryC)=5,指定了數組大小,編譯器會自動在空余地方添加'\0',這其實跟char arryC[6] = {'a','b','c','d','e','\0'};等價。
strlen(str) = 5,不包括結尾的'\0'。
由以上兩個我們來看看strlen和sizeof的區別:
(1)、sizeof是C語言中的一個單目運算操作符,類似++、--等;
用於數據類型,sizeof(type),比如sizeof(int)
用於變量,sizeof(var_name)
注意:sizeof不能用於函數類型、不完全類型或位字段。不完全類型是指具有未知存儲大小的數據類型,比如未知存儲大小的數組類型、
未知內容的結構體或聯合類型,void類型等。例如: sizeof(max),若此時變量max定義為int max(); sizeof(char_v),此時char_v
定義為char char_v[MAX]且MAX未知。
(2)、strlen是個函數,其原型為unsigned int strlen(char *s);
streln的計算必須依賴字符序列中的'\0',通過該字符來判斷字符序列是否結束。
3、忽悠人的char str[]和char *str
(1)下面的操作合法么?出錯的話,會是在那個階段?編譯時期還是運行時期?
char str[] = "hello"; str[0] = 's'; //合法么
char *str = "hello"; p[0] = 's'; //合法么
分析:
這兩個都可以成功編譯,只是第二個會在運行時期出現段錯誤。下面來分析一下:
首先"hello"是一個字符串常量,存儲在靜態數據區域(data段),這是在編譯時期就確定的。第一個是將字符串常量賦值給了一個變量(全局變量在數據段,局部變量在棧區),實際上是將字符串常量拷貝到了變量內存中,因此修改的只是str[]這個變量的值。
第二個是將字符串常量的首地址賦值給p,對p操作就是對字符串常量進行修改!因此出現了段錯誤。
(2)理解了上面的知識,判斷一下下面的true or false?
char str1[] = "abc"; char str2[] = "abc"; const char str3[] = "abc"; const char str4[] = "abc"; const char *str5 = "abc"; const char *str6 = "abc"; char *str7 = "abc"; char *str8 = "abc"; cout << ( str1 == str2 ) << endl; cout << ( str3 == str4 ) << endl; cout << ( str5 == str6 ) << endl; cout << ( str7 == str8 ) << endl;
分析:
結果是: 0 0 1 1
先理解str1,str2,str3,str4,他們是什么?他們是數組名,也就是數組首元素的地址!”str1 == str2“本質就是比較兩個數組的地址是不是相同。上面我們說過,編譯器給他們分配了新的存儲空間來對字符串"abc"進行拷貝,這些變量在內存里是相互獨立的,因此他們的地址肯定不同!
再理解str5,str6,str7,str8,他們是什么?他們是指針,他們的值就是字符串常量的地址!它們都指向“abc"所在的靜態數據區,所以他們都相等。
(3)更深一步:下面程序有問題么?有的話問題出在哪里?如何修改?
#include <stdio.h>
char *returnStr() { char p[]="hello world!"; return p; } int main() { char *str = NULL; str = returnStr(); printf("%s\n", str); return 0; }
分析:
p是個局部變量,只是把字符串"hello word!"進行了拷貝,該局部變量是存放在棧中的,當函數退出時,棧被清空,p會被釋放,因此返回的是一個已經被釋放的內存地址,這樣做是錯誤的。
可以進行如下修改:
#include <stdio.h>
char *returnStr() { char *p = "hello world!"; return p; }
int main() { char *str = NULL; str = returnStr(); printf("%s\n", str); return 0; }
這么寫就不會有問題了,因為"hello world!"存放在靜態數據區,將該區的首地址賦值給指針p並返回,即使returnStr函數退出,也不會對字符串常量所在的內存進行回收,因此可以訪問該字符串常量。
當然了,也可以這么修改:
#include <stdio.h>
char *returnStr() { static char p[] = "hello world!"; return p; }
int main() { char *str = NULL; str = returnStr(); printf("%s\n", str); return 0; }
使用關鍵字static,static修飾的局部變量也會放在data段,即使returnStr函數退出,也不會收回該內存空間。
4、數組作為函數參數傳遞
我們往往會把數組當做函數的入參,看看下面的函數有啥問題:
int func(int a[])
{ int n = sizeof(a)/sizeof(int); for(int i=0;i<n;i++)
{ printf("%d ",a[i]); a[i]++; } }
結果卻發現n的值總是1!為什么會這樣呢?這是因為在C中,將數組傳遞給一個函數時,無法按值傳遞,而是會自動退化為指針。下面的三種寫法其實是等價的:
"int func(int a[20]);" 等價於 "int func(int a[]);" 等價於 "int func(int *a);"。
5、兩數交換的那些坑
下面代碼想實現兩數交換,有什么問題么?
void swap(int* a, int* b) { int *p; p = a; a = b; b = p; }
分析:
程序在運行到調用函數時,會將參數壓棧,並為之分配新的空間,此時傳遞進來的其實是一個副本,如下圖所示:
a的值跟b的值都是地址,交換a和b的值,只是把兩個地址交換了而已,也就說只是改變了副本的地址而已,地址所指向的對象並沒有改變!。
正確的方法應該是這樣的:
void swap(int* a, int* b) { int tmp; tmp = *a; *a = *b; *b = tmp; }
a和b雖然也是副本,但是在函數內部通過該地址直接修改了對象的值,對應的實參就跟着發生了變化。
其實,指針傳遞和值傳遞的本質都是值傳遞,值傳遞是傳遞了要傳遞變量的一個副本。復制完后,實參的地址和形參的地址沒有任何聯系,對形參地址的修改不會影響到實參,但是對形參地址所指向對象的修改卻能直接反映在實參中,這是因為形參所指向的對象就是實參的對象。正因如此,我們在傳遞指針作為參數時,要用const進行修飾,就是為了防止形參地址被意外修改。
6、函數參數為指針應小心
下面的代碼有什么問題?運行結果會怎么樣?
void GetMem(char *p) { p = (char*)malloc(100); } void main() { char *str = NULL; GetMem(str); strcpy(str, "hello word!"); printf(str); }
分析:
程序崩潰。在上面已經分析過了,傳遞給GetMem函數形參的只是一個副本,修改形參p的地址對實參str絲毫沒有影響。所以str還是那個str,仍為NULL,這時將字符串常量拷貝到一個空地址,必然引發程序崩潰。下面的方法可以解決這個問題:
void GetMem(char **p) { *p = (char*)malloc(100); } void main() { char *str = NULL; GetMem(&str); strcpy(str, "hello word!"); printf(str);
free(str); //不free會引起內存泄漏 }
看似有點晦澀,其實很好理解。本質上是讓指針變量str指向新malloc內存的首地址,也就是把該首地址賦值給指針變量str。前面我們說過,指針傳遞本質上也是值傳遞,要想在子函數修改str的值,必須要傳遞指向str的指針,因此子函數要傳遞的是str的地址,這樣通過指針方式修改str的值,將malloc的內存首地址賦值給str。
7、數組指針的疑惑
(1)說出下面表達式的含義?
int *p1[10]; int (*p2)[10];
第一個是指針數組,首先他是一個數組,數組的元素都是指針。
第二個是數組指針,首先他是一個指針,它指向一個數組。
下面這張圖可以很清楚的說明:
(2)寫出下面程序運行的結果
int a[5] = { 1, 2, 3, 4, 5 }; int *ptr = (int *)(&a + 1); printf("%d,%d", *(a + 1), *(ptr - 1));
分析:
答案是2,5。本題的關鍵是理解指針運算,”+1“就是偏移量的問題:一個類型為T的指針移動,是以sizeof(T)為單位移動的。
a+1:在數組首元素地址的基礎上,偏移一個sizeof(a[0])單位。因此a+1就代表數組第1個元素,為2;
&a+1:在數組首元素的基礎上,偏移一個sizeof(a)單位,&a其實就是一個數組指針,類型為int(*)[5]。因此&a+1實際上是偏移了5個元素的長度,也就是a+5;再看ptr是int*類型,因此"ptr-1"就是減去sizeof(int*),即為a[4]=5;
a是數組首地址,也就是a[0]的地址,a+1是數組下一個元素的地址,即a[1]; &a是對象的首地址,&a+1是下一個對象的地址,即a[5]。
8、二級指針疑問
給定聲明 const char * const *pp;下列操作或說明正確的是?
(A)pp++ (B)(*pp)++ (C)(**pp)=\\c\\; (D)以上都不對
分析:
答案是A。
先從一級指針說起吧:
(1)const char p : 限定變量p為只讀。這樣如p=2這樣的賦值操作就是錯誤的。
(2)const char *p : p為一個指向char類型的指針,const只限定p指向的對象為只讀。這樣,p=&a或 p++等操作都是合法的,但如*p=4這樣的操作就錯了, 因為企圖改寫這個已經被限定為只讀屬性的對象。
(3)char *const p : 限定此指針為只讀,這樣p=&a或 p++等操作都是不合法的。而*p=3這樣的操作合法,因為並沒有限定其最終對象為只讀。
(4)const char *const p :兩者皆限定為只讀,不能改寫。
再來看二級指針問題:
(1)const char **p : p為一個指向指針的指針,const限定其最終對象為只讀,顯然這最終對象也是為char類型的變量。故像**p=3這樣的賦值是錯誤的, 而像*p=? p++這樣的操作合法。
(2)const char * const *p :限定最終對象和 p指向的指針為只讀。這樣 *p=?的操作也是錯的,但是p++這種是合法的。
(3)const char * const * const p :全部限定為只讀,都不可以改寫
9、*p++、 (*p)++、 *++p、 ++*p
int a[5]={1, 2, 3, 4, 5};
int *p = a;
*p++ 先取指針p指向的值(數組第一個元素1),再將指針p自增1;
cout << *p++; // 結果為 1
cout <<(*p++); // 1
(*p)++ 先去指針p指向的值(數組第一個元素1),再將該值自增1(數組第一個元素變為2
cout << (*p)++; // 1
cout <<((*p)++) // 2
*++p 先將指針p自增1(此時指向數組第二個元素),* 操作再取出該值
cout << *++p; // 2
cout <<(*++p) // 2
++*p 先取指針p指向的值(數組第一個元素1),再將該值自增1(數組第一個元素變為2)
cout <<++*p; // 2
cout <<(++*p) // 2
參考博客:
1、《C語言中sizeof 與strlen 區別 》:http://www.cnblogs.com/kungfupanda/archive/2012/12/24/2831273.html
2、《常量字符串為什么位於靜態存儲區?》:https://blog.csdn.net/ebw123/article/details/51388340
3、《值傳遞、指針傳遞、引用傳遞的區別》:https://blog.csdn.net/koudan567/article/details/51298511
4、牛客網mlc答案:https://www.nowcoder.com/test/question/done?tid=16211084&qid=1409#summary