錯題集
1.定義一個函數指針,指向的函數有兩個int形參並且返回一個函數指針,返回的指針指向一個有一個int形參且返回int的函數?
答案:int (*(*F)(int, int))(int)
解析:首先,一個函數指針,指向的函數有兩個int形參,這個就是(*F)(int, int),這返回的是一個指針
返回一個函數指針,返回的指針指向一個有一個int形參且返回int的函數;把上面的結果當成一個指針,相當於再做一次上面的步驟,所以結果爲:int (*(*F)(int, int))(int)
2.下面哪個指針表達式可以用來引用數組元素a[i][j][k][l]()
答案:*(*(*(*(a+i)+j)+k)+l)
解析:
a:整個四維數組的地址
*(a+i) = a[i]
*(a+i)+j = a[i] +j
*((a+i)+j) = a[i][j]
*((a+i)+j)+k = a[i][j]+k
*( *((a+i)+j)+k ) = a[i][j][k]
*( *((a+i)+j)+k )+l = a[i][j][k]+l
*( *( *((a+i)+j)+k )+l ) = a[i][j][k][l]
3.採用多路複用I/O監聽3個套接字的數據時,如果套接字描述符分別是:5,17,19,則
select(int maxfd,struct fd_set* rdset,NULL,NULL)中的maxfd應取爲()
答案:20
解析:Maxfd是三個套接字描述符中最大數字加上1
4.下面代碼的輸出結果是()
int main(){
int pid;
int num=1;
pid=fork();
if(pid>0){
num++;
printf("in parent:num:%d addr:%x\n",num,&num);
}
else if(pid==0){
printf("in child:num:%d addr:%x\n",num,&num);
}
}
答案:父子進程中輸出的num不同,num地址相同
解析:虛擬地址空間。num地址的值相同,但是其真實的物理地址卻不一樣。linux下實現了一下,發現地址值真的一樣。fork之後子進程複製了父進程的數據、堆棧。但是由於地址重定位器之類的魔法存在,所以,看似一樣的地址空間(虛擬地址空間),
其實卻是不同的物理地址空間。同時可以驗證c程序中輸出的地址空間其實都是虛擬地址空間。
5.不考慮任何編譯器優化(如:NRVO),下述代碼的第10行會發生
#include <stdio.h>//1
class B//2
{//3
};//4
B func(const B& rhs){//5
return rhs;//6
}//7
int main(int argc,char **argv){//8
B b1,b2;//9
b2=func(b1);//10
}//11
答案:一次拷貝構造函數,一次析構函數,一次(拷貝賦值運算符)operator=
解析:一次拷貝構造函數發生在func函數調用完成,返回B類型的對象時,因爲返回的不是引用類型,所以會生成一個對象,不妨稱爲TEMP,將返回的對象通過拷貝構造函數複製給TEMP,然後,返回值所對應的對象會被析構。如果返回值是引用類型,則不會調用拷貝構造函數。賦值運算符在func函數執行完成後,將上面提到的TEMP,通過賦值運算符賦值給b2,值得注意的是賦值運算符重載函數如果不自己定義,
程序會認爲是調用缺省的賦值運算符重載函數。
6.請問下面的程序一共輸出多少個“-”?
int main(void)
{
int i;
for (i = 0; i < 2; i++) {
fork();
printf("-");
}
return 0;
}
答案:8
解析:一共調用了6次printf,但是會輸出8個-。因爲父進程的輸出緩衝也會被子進程複製
fork(); // i=0
printf("-"); // buffer="-"
fork(); // i=1
printf("-"); // +1
printf("-");
printf("-")
fork(); // i=1
printf("-"); // +1
printf("-")
7.閱讀以下代碼:
class parent
{
public:
virtual void output();
};
void parent::output()
{
printf("parent!");
}
class son : public parent
{
public:
virtual void output();
};
void son::output()
{
printf("son!");
}
son s;
memset(&s , 0 , sizeof(s));
parent& p = s;
p.output();
執行結果是()
答案:沒有輸出結果,程序運行出錯
解析:memset 將s所指向的某一塊內存中的每個字節的內容全部設置爲ch指定的ASCII值, 本代碼中, 不虛函數鏈表地址也清空了, 所以p.output調用失敗。 output函數的地址編程0
8.關於c++的inline關鍵字,以下說法正確的是
答案:定義在Class聲明內的成員函數默認是inline函數
解析: 內聯函數:
Tip: 只有當函數只有 10 行甚至更少時纔將其定義爲內聯函數.
定義: 當函數被聲明爲內聯函數之後, 編譯器會將其內聯展開, 而不是按通常的函數調用機制進行調用.
優點: 當函數體比較小的時候, 內聯該函數可以令目標代碼更加高效. 對於存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用內聯.
缺點: 濫用內聯將導致程序變慢. 內聯可能使目標代碼量或增或減, 這取決於內聯函數的大小. 內聯非常短小的存取函數通常會減少代碼大小, 但內聯一個相當大的函數將戲劇性的增加代碼大小. 現代處理器由於更好的利用了指令緩存, 小巧的代碼往往執行更快。
結論: 一個較爲合理的經驗準則是, 不要內聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因爲有隱含的成員和基類析構函數被調用!
另一個實用的經驗準則: 內聯那些包含循環或 switch 語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或 switch 語句從不被執行).
有些函數即使聲明爲內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.(遞歸調用堆棧的展開並不像循環那麼簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支持內聯遞歸函數). 虛函數內聯的主要原因則是想把它的函數體放在類定義內, 爲了圖個方便, 抑或是當作文檔描述其行爲, 比如精短的存取函數.
-inl.h文件:
Tip: 複雜的內聯函數的定義, 應放在後綴名爲 -inl.h 的頭文件中.
內聯函數的定義必須放在頭文件中, 編譯器才能在調用點內聯展開定義. 然而, 實現代碼理論上應該放在 .cc 文件中, 我們不希望 .h 文件中有太多實現代碼, 除非在可讀性和性能上有明顯優勢.
如果內聯函數的定義比較短小, 邏輯比較簡單, 實現代碼放在 .h 文件裏沒有任何問題. 比如, 存取函數的實現理所當然都應該放在類定義內. 出於編寫者和調用者的方便, 較複雜的內聯函數也可以放到 .h 文件中, 如果你覺得這樣會使頭文件顯得笨重, 也可以把它萃取到單獨的 -inl.h 中. 這樣把實現和類定義分離開來, 當需要時包含對應的 -inl.h 即可。
A 如果只聲明含有inline關鍵字,就沒有內聯的效果。 內聯函數的定義必須放在頭文件中, 編譯器才能在調用點內聯展開定義. 有些函數即使聲明爲內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.
B 內聯函數應該在頭文件中定義,這一點不同於其他函數。編譯器在調用點內聯展開函數的代碼時,必須能夠找到 inline 函數的定義才能將調用函數替換爲函數代碼,而對於在頭文件中僅有函數聲明是不夠的。
C 當然內聯函數定義也可以放在源文件中,但此時只有定義的那個源文件可以用它,而且必須爲每個源文件拷貝一份定義(即每個源文件裏的定義必須是完全相同的),當然即使是放在頭文件中,也是對每個定義做一份拷貝,只不過是編譯器替你完成這種拷貝罷了。但相比於放在源文件中,放在頭文件中既能夠確保調用函數是定義是相同的,又能夠保證在調用點能夠找到函數定義從而完成內聯(替換)。
對於由兩個文件compute.C和draw.C構成的程序來說,程序員不能定義這樣的min()函數,它在compute.C中指一件事情,而在draw.C中指另外一件事情。如果兩個定義不相同,程序將會有未定義的行爲:
爲保證不會發生這樣的事情,建議把inline函數的定義放到頭文件中。在每個調用該inline函數的文件中包含該頭文件。這種方法保證對每個inline函數只有一個定義,且程序員無需複製代碼,並且不可能在程序的生命期中引起無意的不匹配的事情。
D 正確。 定義在類聲明之中的成員函數將自動地成爲內聯函數,例如:
class A { public: void Foo(int x, int y) { ... } // 自動地成爲內聯函數 }
EF 在每個調用該inline函數的文件中包含該頭文件。這種方法保證對每個inline函數只有一個定義,且程序員無需複製代碼,並且不可能在程序的生命期中引起無意的不匹配的事情。最好只有一個定義!