C++面試寶典
1.new、delete、malloc、free關係
delete會呼叫物件的解構函式,和new對應free只會釋放記憶體,new呼叫建構函式。malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。它們都可用於申請動態記憶體和釋放記憶體。對於非內部資料型別的物件而言,光用maloc/free無法滿足動態物件的要求。物件在建立的同時要自動執行建構函式,物件在消亡之前要自動執行解構函式。由於malloc/free是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構函式和解構函式的任務強加於malloc/free。因此C++語言需要一個能完成動態記憶體分配和初始化工作的運算子new,以及一個能完成清理與釋放記憶體工作的運算子delete。注意new/delete不是庫函式。
2.delete與 delete []區別
delete只會呼叫一次解構函式,而delete[]會呼叫每一個成員的解構函式。在More Effective C++中有更為詳細的解釋:“當delete操作符用於陣列時,它為每個陣列元素呼叫解構函式,然後呼叫operatordelete來釋放記憶體。”delete與New配套,delete []與new []配套
MemTest*mTest1=newMemTest[10];
MemTest*mTest2=newMemTest;
int*pInt1=newint[10];
int*pInt2=newint;
delete[]pInt1; //-1-
delete[]pInt2; //-2-
delete[]mTest1;//-3-
delete[]mTest2;//-4-
在-4-處報錯。
這就說明:對於內建簡單資料型別,delete和delete[]功能是相同的。對於自定義的複雜資料型別,delete和delete[]不能互用。delete[]刪除一個數組,delete刪除一個指標簡單來說,用new分配的記憶體用delete刪除用new[]分配的記憶體用delete[]刪除delete[]會呼叫陣列元素的解構函式。內部資料型別沒有解構函式,所以問題不大。如果你在用delete時沒用括號,delete就會認為指向的是單個物件,否則,它就會認為指向的是一個數組。
3.C和C++ 的共同點?不同之處?
4.繼承的優缺點。
類繼承是在編譯時刻靜態定義的,且可直接使用,類繼承可以較方便地改變父類的實現。但是類繼承也有一些不足之處。首先,因為繼承在編譯時刻就定義了,所以無法在執行時刻改變從父類繼承的實現。更糟的是,父類通常至少定義了子類的部分行為,父類的任何改變都可能影響子類的行為。如果繼承下來的實現不適合解決新的問題,則父類必須重寫或被其他更適合的類替換。這種依賴關係限制了靈活性並最終限制了複用性。
(待補充)
5.C++有哪些性質(面向物件特點)
封裝,繼承和多型。
在面向物件程式設計語言中,封裝是利用可重用成分構造軟體系統的特性,它不僅支援系統的可重用性,而且還有利於提高系統的可擴充性;訊息傳遞可以實現傳送一個通用的訊息而呼叫不同的方法;封裝是實現資訊隱蔽的一種技術,其目的是使類的定義和實現分離。
6.子類析構時要呼叫父類的解構函式嗎?
解構函式呼叫的次序是先派生類的析構後基類的析構,也就是說在基類的的析構呼叫的時候,派生類的資訊已經全部銷燬了定義一個物件時先呼叫基類的建構函式、然後呼叫派生類的建構函式;析構的時候恰好相反:先呼叫派生類的解構函式、然後呼叫基類的解構函式JAVA無解構函式深拷貝和淺拷貝
7.多型,虛擬函式,純虛擬函式
多型:是對於不同物件接收相同訊息時產生不同的動作。C++的多型性具體體現在執行和編譯兩個方面:在程式執行時的多型性通過繼承和虛擬函式來體現;
在程式編譯時多型性體現在函式和運算子的過載上
虛擬函式:在基類中冠以關鍵字 virtual 的成員函式。 它提供了一種介面介面。允許在派生類中對基類的虛擬函式重新定義。
純虛擬函式的作用:在基類中為其派生類保留一個函式的名字,以便派生類根據需要對它進行定義。作為介面而存在 純虛擬函式不具備函式的功能,一般不能直接被呼叫。
從基類繼承來的純虛擬函式,在派生類中仍是虛擬函式。如果一個類中至少有一個純虛擬函式,那麼這個類被稱為抽象類(abstract class)。
抽象類中不僅包括純虛擬函式,也可包括虛擬函式。l抽象類必須用作派生其他類的基類,而不能用於直接建立物件例項。但仍可使用指向抽象類的指標支援執行時多型性。
8.求下面函式的返回值(微軟)
int func(x) { int countx = 0; while(x) { countx ++; x = x&(x-1); } return countx; }
假定x = 9999。 答案:8
思路:將x轉化為2進位制,看含有的1的個數。
9.什麼是“引用”?申明和使用“引用”要注意哪些問題?
答:引用就是某個目標變數的“別名”(alias),對應用的操作與對變數直接操作效果完全相同。申明一個引用的時候,切記要對其進行初始化。引用宣告完畢後,相當於目標變數名有兩個名稱,即該目標原名稱和引用名,不能再把該引用名作為其他變數名的別名。宣告一個引用,不是新定義了一個變數,它只表示該引用名是目標變數名的一個別名,它本身不是一種資料型別,因此引用本身不佔儲存單元,系統也不給引用分配儲存單元。不能建立陣列的引用。
10.將“引用”作為函式引數有哪些特點?
(1)傳遞引用給函式與傳遞指標的效果是一樣的。這時,被調函式的形參就成為原來主調函式中的實參變數或物件的一個別名來使用,所以在被調函式中對形參變數的操作就是對其相應的目標物件(在主調函式中)的操作。
(2)使用引用傳遞函式的引數,在記憶體中並沒有產生實參的副本,它是直接對實參操作;而使用一般變數傳遞函式的引數,當發生函式呼叫時,需要給形參分配儲存單元,形參變數是實參變數的副本;如果傳遞的是物件,還將呼叫拷貝建構函式。因此,當引數傳遞的資料較大時,用引用比用一般變數傳遞引數的效率和所佔空間都好。
(3)使用指標作為函式的引數雖然也能達到與使用引用的效果,但是,在被調函式中同樣要給形參分配儲存單元,且需要重複使用"*指標變數名"的形式進行運算,這很容易產生錯誤且程式的閱讀性較差;另一方面,在主調函式的呼叫點處,必須用變數的地址作為實參。而引用更容易使用,更清晰。
11.在什麼時候需要使用“常引用”?
如果既要利用引用提高程式的效率,又要保護傳遞給函式的資料不在函式中被改變,就應使用常引用。常引用宣告方式:const 型別識別符號 &引用名=目標變數名;
例1
int a ; const int &ra=a; ra=1; //錯誤 a=1; //正確
例2
string foo( ); void bar(string & s);
那麼下面的表示式將是非法的:
bar(foo( )); bar("hello world");
原因在於foo( )和"hello world"串都會產生一個臨時物件,而在C++中,這些臨時物件都是const型別的。因此上面的表示式就是試圖將一個const型別的物件轉換為非const型別,這是非法的。引用型引數應該在能被定義為const的情況下,儘量定義為const 。
12.將“引用”作為函式返回值型別的格式、好處和需要遵守的規則?
格式:型別識別符號 &函式名(形參列表及型別說明){ //函式體 }
好處:在記憶體中不產生被返回值的副本;(注意:正是因為這點原因,所以返回一個區域性變數的引用是不可取的。因為隨著該區域性變數生存期的結束,相應的引用也會失效,產生runtime error! 注意事項:
(1)不能返回區域性變數的引用。這條可以參照Effective C++[1]的Item 31。主要原因是區域性變數會在函式返回後被銷燬,因此被返回的引用就成為了"無所指"的引用,程式會進入未知狀態。
(2)不能返回函式內部new分配的記憶體的引用。這條可以參照Effective C++[1]的Item 31。雖然不存在區域性變數的被動銷燬問題,可對於這種情況(返回函式內部new分配記憶體的引用),又面臨其它尷尬局面。例如,被函式返回的引用只是作為一個臨時變量出現,而沒有被賦予一個實際的變數,那麼這個引用所指向的空間(由new分配)就無法釋放,造成memory leak。
(3)可以返回類成員的引用,但最好是const。這條原則可以參照Effective C++[1]的Item 30。主要原因是當物件的屬性是與某種業務規則(business rule)相關聯的時候,其賦值常常與某些其它屬性或者物件的狀態有關,因此有必要將賦值操作封裝在一個業務規則當中。如果其它物件可以獲得該屬性的非常量引用(或指標),那麼對該屬性的單純賦值就會破壞業務規則的完整性。
(4)流操作符過載返回值申明為“引用”的作用:
流操作符<<和>>,這兩個操作符常常希望被連續使用,例如:cout << "hello" << endl; 因此這兩個操作符的返回值應該是一個仍然支援這兩個操作符的流引用。可選的其它方案包括:返回一個流物件和返回一個流物件指標。但是對於返回一個流物件,程式必須重新(拷貝)構造一個新的流物件,也就是說,連續的兩個<<操作符實際上是針對不同物件的!這無法讓人接受。對於返回一個流指標則不能連續使用<<操作符。因此,返回一個流物件引用是惟一選擇。這個唯一選擇很關鍵,它說明了引用的重要性以及無可替代性,也許這就是C++語言中引入引用這個概念的原因吧。 賦值操作符=。這個操作符象流操作符一樣,是可以連續使用的,例如:x = j = 10;或者(x=10)=100;賦值操作符的返回值必須是一個左值,以便可以被繼續賦值。因此引用成了這個操作符的惟一返回值選擇。
例3
#i nclude <iostream.h> int &put(int n); int vals[10]; int error=-1; void main() { put(0)=10; //以put(0)函式值作為左值,等價於vals[0]=10; put(9)=20; //以put(9)函式值作為左值,等價於vals[9]=20; cout<<vals[0]; cout<<vals[9]; } int &put(int n) { if (n>=0 && n<=9 ) return vals[n]; else { cout<<"subscript error"; return error; } }
(5)在另外的一些操作符中,卻千萬不能返回引用:+-*/ 四則運算子。它們不能返回引用,Effective C++[1]的Item23詳細的討論了這個問題。主要原因是這四個操作符沒有side effect,因此,它們必須構造一個物件作為返回值,可選的方案包括:返回一個物件、返回一個區域性變數的引用,返回一個new分配的物件的引用、返回一個靜態物件引用。根據前面提到的引用作為返回值的三個規則,第2、3兩個方案都被否決了。靜態物件的引用又因為((a+b) == (c+d))會永遠為true而導致錯誤。所以可選的只剩下返回一個物件了。
13.“引用”與多型的關係?
引用是除指標外另一個可以產生多型效果的手段。這意味著,一個基類的引用可以指向它的派生類例項。例4
Class A; Class B : Class A{...}; B b; A& ref = b;
14.“引用”與指標的區別是什麼?
指標通過某個指標變數指向一個物件後,對它所指向的變數間接操作。程式中使用指標,程式的可讀性差;而引用本身就是目標變數的別名,對引用的操作就是對目標變數的操作。此外,就是上面提到的對函式傳ref和pointer的區別。
15.什麼時候需要“引用”?
流操作符<<和>>、賦值操作符=的返回值、拷貝建構函式的引數、賦值操作符=的引數、其它情況都推薦使用引用。以上 2-8 參考:http://develop.csai.cn/c/NO0000021.htm
16.結構與聯合有和區別?
(1). 結構和聯合都是由多個不同的資料型別成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間), 而結構的所有成員都存在(不同成員的存放地址不同)。 (2). 對於聯合的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的。
17.面關於“聯合”的題目的輸出?
a)
#i nclude <stdio.h> union { int i; char x[2]; }a;
void main() { a.x[0] = 10; a.x[1] = 1; printf("%d",a.i); } 答案:266 (低位低地址,高位高地址,記憶體佔用情況是Ox010A)
b)
main() { union{ /*定義一個聯合*/ int i; struct{ /*在聯合中定義一個結構*/ char first; char second; }half; }number; number.i=0x4241; /*聯合成員賦值*/ printf("%c%cn", number.half.first, mumber.half.second); number.half.first='a'; /*聯合中結構成員賦值*/ number.half.second='b'; printf("%xn", number.i); getch(); } 答案: AB (0x41對應'A',是低位;Ox42對應'B',是高位)
6261 (number.i和number.half共用一塊地址空間)
18.關聯、聚合(Aggregation)以及組合(Composition)的區別?
涉及到UML中的一些概念:關聯是表示兩個類的一般性聯絡,比如“學生”和“老師”就是一種關聯關係;聚合表示has-a的關係,是一種相對鬆散的關係,聚合類不需要對被聚合類負責,如下圖所示,用空的菱形表示聚合關係:從實現的角度講,聚合可以表示為:
class A {...} class B { A* a; .....}
而組合表示contains-a的關係,關聯性強於聚合:組合類與被組合類有相同的生命週期,組合類要對被組合類負責,採用實心的菱形表示組合關係:實現的形式是:
class A{...} class B{ A a; ...}
19.面向物件的三個基本特徵,並簡單敘述之?
1. 封裝:將客觀事物抽象成類,每個類對自身的資料和方法實行protection(private, protected,public)
2. 繼承:廣義的繼承有三種實現形式:實現繼承(指使用基類的屬性和方法而無需額外編碼的能力)、可視繼承(子窗體使用父窗體的外觀和實現程式碼)、介面繼承(僅使用屬性和方法,實現滯後到子類實現)。前兩種(類繼承)和後一種(物件組合=>介面繼承以及純虛擬函式)構成了功能複用的兩種方式。
3. 多型:是將父物件設定成為和一個或更多的他的子物件相等的技術,賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。簡單的說,就是一句話:允許將子類型別的指標賦值給父類型別的指標。
20.過載(overload)和重寫(overried,有的書也叫做“覆蓋”)的區別?
常考的題目。從定義上來說:
過載:是指允許存在多個同名函式,而這些函式的引數表不同(或許引數個數不同,或許引數型別不同,或許兩者都不同)。
重寫:是指子類重新定義父類虛擬函式的方法。
從實現原理上來說:
過載:編譯器根據函式不同的引數表,對同名函式的名稱做修飾,然後這些同名函式就成了不同的函式(至少對於編譯器來說是這樣的)。如,有兩個同名函式:function func(p:integer):integer;和function func(p:string):integer;。那麼編譯器做過修飾後的函式名稱可能是這樣的:int_func、str_func。對於這兩個函式的呼叫,在編譯器間就已經確定了,是靜態的。也就是說,它們的地址在編譯期就綁定了(早繫結),因此,過載和多型無關!
重寫:和多型真正相關。當子類重新定義了父類的虛擬函式後,父類指標根據賦給它的不同的子類指標,動態的呼叫屬於子類的該函式,這樣的函式呼叫在編譯期間是無法確定的(呼叫的子類的虛擬函式的地址無法給出)。因此,這樣的函式地址是在執行期繫結的(晚繫結)。
21.多型的作用?
主要是兩個:
1. 隱藏實現細節,使得程式碼能夠模組化;擴充套件程式碼模組,實現程式碼重用;
2. 介面重用:為了類在繼承和派生的時候,保證使用家族中任一類的例項的某一屬性時的正確呼叫。
22.Ado與Ado.net的相同與不同?
除了“能夠讓應用程式處理儲存於DBMS 中的資料“這一基本相似點外,兩者沒有太多共同之處。但是Ado使用OLE DB 介面並基於微軟的COM 技術,而ADO.NET 擁有自己的ADO.NET 介面並且基於微軟的.NET 體系架構。眾所周知.NET 體系不同於COM 體系,ADO.NET 介面也就完全不同於ADO和OLE DB 介面,這也就是說ADO.NET 和ADO是兩種資料訪問方式。ADO.net 提供對XML 的支援。
23.New delete 與malloc free 的聯絡與區別?
答案:都是在堆(heap)上進行動態的記憶體操作。用malloc函式需要指定記憶體分配的位元組數並且不能初始化物件,new 會自動呼叫物件的建構函式。delete 會呼叫物件的destructor,而free 不會呼叫物件的destructor.
24.#define DOUBLE(x) x+x ,i = 5*DOUBLE(5); i 是多少?
答案:i 為30。
25.有哪幾種情況只能用intialization list 而不能用assignment?
答案:當類中含有const、reference 成員變數;基類的建構函式都需要初始化表。
26. C++是不是型別安全的?
答案:不是。兩個不同型別的指標之間可以強制轉換(用reinterpret cast)。C#是型別安全的。
27. main 函式執行以前,還會執行什麼程式碼?
答案:全域性物件的建構函式會在main 函式之前執行。
28. 描述記憶體分配方式以及它們的區別?
1) 從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static 變數。 2) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集。 3) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc 或new 申請任意多少的記憶體,程式設計師自己負責在何時用free 或delete 釋放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活,但問題也最多。
29.struct 和 class 的區別
答案:struct 的成員預設是公有的,而類的成員預設是私有的。struct 和 class 在其他方面是功能相當的。從感情上講,大多數的開發者感到類和結構有很大的差別。感覺上結構僅僅象一堆缺乏封裝和功能的開放的記憶體位,而類就象活的並且可靠的社會成員,它有智慧服務,有牢固的封裝屏障和一個良好定義的介面。既然大多數人都這麼認為,那麼只有在你的類有很少的方法並且有公有資料(這種事情在良好設計的系統中是存在的!)時,你也許應該使用 struct 關鍵字,否則,你應該使用 class 關鍵字。
30.當一個類A 中沒有任何成員變數與成員函式,這時sizeof(A)的值是多少?
答案:如果不是零,請解釋一下編譯器為什麼沒有讓它為零。(Autodesk)肯定不是零。舉個反例,如果是零的話,宣告一個class A[10]物件陣列,而每一個物件佔用的空間是零,這時就沒辦法區分A[0],A[1]…了。
31. 在8086 彙編下,邏輯地址和實體地址是怎樣轉換的?(Intel)
答案:通用暫存器給出的地址,是段內偏移地址,相應段暫存器地址*10H+通用暫存器內地址,就得到了真正要訪問的地址。
32. 比較C++中的4種類型轉換方式?
重點是static_cast, dynamic_cast和reinterpret_cast的區別和應用。
dynamic_casts在幫助你瀏覽繼承層次上是有限制的。它不能被用於缺乏虛擬函式的型別上,它被用於安全地沿著類的繼承關係向下進行型別轉換。如你想在沒有繼承關係的型別中進行轉換,你可能想到static_cast
33.分別寫出BOOL,int,float,指標型別的變數a 與“零”的比較語句。
答案: BOOL : if ( !a ) or if(a) int : if ( a == 0) float : const EXPRESSION EXP = 0.000001 if ( a < EXP && a >-EXP) pointer : if ( a != NULL) or if(a == NULL)
34.請說出const與#define 相比,有何優點?
答案:
Const作用:定義常量、修飾函式引數、修飾函式返回值三個作用。被Const修飾的東西都受到強制保護,可以預防意外的變動,能提高程式的健壯性。
1) const 常量有資料型別,而巨集常量沒有資料型別。編譯器可以對前者進行型別安全檢查。而對後者只進行字元替換,沒有型別安全檢查,並且在字元替換可能會產生意料不到的錯誤。 2) 有些整合化的除錯工具可以對const 常量進行除錯,但是不能對巨集常量進行除錯。
35.簡述陣列與指標的區別?
陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。指標可以隨時指向任意型別的記憶體塊。 (1)修改內容上的差別 char a[] = “hello”; a[0] = ‘X’; char *p = “world”; // 注意p 指向常量字串 p[0] = ‘X’; // 編譯器不能發現該錯誤,執行時錯誤 (2) 用運算子sizeof 可以計算出陣列的容量(位元組數)。sizeof(p),p 為指標得到的是一個指標變數的位元組數,而不是p 所指的記憶體容量。C++/C 語言沒有辦法知道指標所指的記憶體容量,除非在申請記憶體時記住它。注意當陣列作為函式的引數進行傳遞時,該陣列自動退化為同類型的指標。 char a[] = "hello world"; char *p = a; cout<< sizeof(a) << endl; // 12 位元組 cout<< sizeof(p) << endl; // 4 位元組 計算陣列和指標的記憶體容量 void Func(char a[100]) { cout<< sizeof(a) << endl; // 4 位元組而不是100 位元組 }
36.類成員函式的過載、覆蓋和隱藏區別?
答案:a.成員函式被過載的特徵: (1)相同的範圍(在同一個類中); (2)函式名字相同; (3)引數不同; (4)virtual 關鍵字可有可無。 b.覆蓋是指派生類函式覆蓋基類函式,特徵是: (1)不同的範圍(分別位於派生類與基類); (2)函式名字相同; (3)引數相同; (4)基類函式必須有virtual 關鍵字。 c.“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下: (1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。 (2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)
37.求出兩個數中的較大這
There are two int variables: a and b, don’t use “if”, “? :”, “switch”or other judgement statements, find out the biggest one of the two numbers.
答案:( ( a + b ) + abs( a - b ) ) / 2
38.如何打印出當前原始檔的檔名以及原始檔的當前行號?
答案: cout << __FILE__ ; cout<<__LINE__ ; __FILE__和__LINE__是系統預定義巨集,這種巨集並不是在某個檔案中定義的,而是由編譯器定義的。
39. main 主函式執行完畢後,是否可能會再執行一段程式碼,給出說明?
答案:可以,可以用_onexit 註冊一個函式,它會在main 之後執行int fn1(void), fn2(void), fn3(void), fn4 (void); void main( void ) { String str("zhanglin"); _onexit( fn1 ); _onexit( fn2 ); _onexit( fn3 ); _onexit( fn4 ); printf( "This is executed first.n" ); } int fn1() { printf( "next.n" ); return 0; } int fn2() { printf( "executed " ); return 0; } int fn3() { printf( "is " ); return 0; } int fn4() { printf( "This " ); return 0; } The _onexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to _onexit create a register of functions that are executed in LIFO (last-in-first-out) order. The functions passed to _onexit cannot take parameters.
40.如何判斷一段程式是由C 編譯程式還是由C++編譯程式編譯的?
答案: #ifdef __cplusplus cout<<"c++"; #else cout<<"c"; #endif
41.檔案中有一組整數,要求排序後輸出到另一個檔案中
答案:
#i nclude<iostream>
#i nclude<fstream>
using namespace std;
void Order(vector<int>& data) //bubble sort { int count = data.size() ; for ( int i = 0 ; i < count ; i++) { for ( int j = 0 ; j < count - i - 1 ; j++) { if ( data[j] > data[j+1]) { int temp = data[j] ; data[j] = data[j+1] ; data[j+1] = temp ; } } }
void main( void ) { vector<int>data; ifstream in("c:\data.txt"); if ( !in) { cout<<"file error!"; exit(1); } int temp; while (!in.eof()) { in>>temp; data.push_back(temp); } in.close(); //關閉輸入檔案流 Order(data); ofstream out("c:\result.txt"); if ( !out) { cout<<"file error!"; exit(1); } for ( i = 0 ; i < data.size() ; i++) out<<data[i]<<" "; out.close(); //關閉輸出檔案流 }
42.連結串列題:一個連結串列的結點結構
struct Node { int data ; Node *next ; }; typedef struct Node Node ;
(1)已知連結串列的頭結點head,寫一個函式把這個連結串列逆序 ( Intel)
Node * ReverseList(Node *head) //連結串列逆序 { if ( head == NULL || head->next == NULL ) return head; Node *p1 = head ; Node *p2 = p1->next ; Node *p3 = p2->next ; p1->next = NULL ; while ( p3 != NULL ) { p2->next = p1 ; p1 = p2 ; p2 = p3 ; p3 = p3->next ; } p2->next = p1 ; head = p2 ; return head ; } (2)已知兩個連結串列head1 和head2 各自有序,請把它們合併成一個連結串列依然有序。(保留所有結點,即便大小相同) Node * Merge(Node *head1 , Node *head2) { if ( head1 == NULL) return head2 ; if ( head2 == NULL) return head1 ; Node *head = NULL ; Node *p1 = NULL; Node *p2 = NULL; if ( head1->data < head2->data ) { head = head1 ; p1 = head1->next; p2 = head2 ; } else { head = head2 ; p2 = head2->next ; p1 = head1 ; } Node *pcurrent = head ; while ( p1 != NULL && p2 != NULL) { if ( p1->data <= p2->data ) { pcurrent->next = p1 ; pcurrent = p1 ; p1 = p1->next ; } else { pcurrent->next = p2 ; pcurrent = p2 ; p2 = p2->next ; } } if ( p1 != NULL ) pcurrent->next = p1 ; if ( p2 != NULL ) pcurrent->next = p2 ; return head ; } (3)已知兩個連結串列head1 和head2 各自有序,請把它們合併成一個連結串列依然有序,這次要求用遞迴方法進行。 (Autodesk) 答案: Node * MergeRecursive(Node *head1 , Node *head2) { if ( head1 == NULL ) return head2 ; if ( head2 == NULL) return head1 ; Node *head = NULL ; if ( head1->data < head2->data ) { head = head1 ; head->next = MergeRecursive(head1->next,head2); } else { head = head2 ; head->next = MergeRecursive(head1,head2->next); } return head ;
----------
41. 分析一下這段程式的輸出 (Autodesk) class B { public: B() { cout<<"default constructor"<<endl; } ~B() { cout<<"destructed"<<endl; } B(int i):data(i) //B(int) works as a converter ( int -> instance of B) { cout<<"constructed by parameter " << data <<endl; } private: int data; };
B Play( B b) { return b ; }
(1) results: int main(int argc, char* argv[]) constructed by parameter 5 { destructed B(5)形參析構 B t1 = Play(5); B t2 = Play(t1); destructed t1形參析構 return 0; destructed t2 注意順序! } destructed t1
(2) results: int main(int argc, char* argv[]) constructed by parameter 5 { destructed B(5)形參析構 B t1 = Play(5); B t2 = Play(10); constructed by parameter 10 return 0; destructed B(10)形參析構 } destructed t2 注意順序!
destructed t1
43.寫一個函式找出一個整數陣列中,第二大的數 (microsoft)
答案: const int MINNUMBER = -32767 ; int find_sec_max( int data[] , int count) { int maxnumber = data[0] ; int sec_max = MINNUMBER ; for ( int i = 1 ; i < count ; i++) { if ( data[i] > maxnumber ) { sec_max = maxnumber ; maxnumber = data[i] ; } else { if ( data[i] > sec_max ) sec_max = data[i] ; } } return sec_max ; }
44.寫一個在一個字串(n)中尋找一個子串(m)第一個位置的函式。
KMP演算法效率最好,時間複雜度是O(n+m),
46.多重繼承的記憶體分配問題:
比如有class A : public class B, public class C {} 那麼A的記憶體結構大致是怎麼樣的? 這個是compiler-dependent的, 不同的實現其細節可能不同。如果不考慮有虛擬函式、虛繼承的話就相當簡單;否則的話,相當複雜。可以參考《深入探索C++物件模型
47.如何判斷一個單鏈表是有環的?(注意不能用標誌位,最多隻能用兩個額外指標)
struct node { char val; node* next;} bool check(const node* head) {} //return false : 無環;true: 有環一種O(n)的辦法就是(搞兩個指標,一個每次遞增一步,一個每次遞增兩步,如果有環的話兩者必然重合,反之亦然): bool check(const node* head) { if(head==NULL) return false; node *low=head, *fast=head->next; while(fast!=NULL && fast->next!=NULL) { low=low->next; fast=fast->next->next; if(low==fast) return true; } return false; }
48.指標找錯題
分析這些面試題,本身包含很強的趣味性;而作為一名研發人員,通過對這些面試題的深入剖析則可進一步增強自身的內功。 2.找錯題 試題1: 以下是引用片段: void test1() //陣列越界 { char string[10]; char* str1 = "0123456789"; strcpy( string, str1 ); } 試題2: 以下是引用片段: void test2() { char string[10], str1[10]; int i; for(i=0; i<10; i++) { str1= 'a'; } strcpy( string, str1 ); } 試題3: 以下是引用片段: void test3(char* str1) { char string[10]; if( strlen( str1 ) <= 10 ) { strcpy( string, str1 ); } } 解答: 試題1字串str1需要11個位元組才能存放下(包括末尾的’\0’),而string只有10個位元組的空間,strcpy會導致陣列越界;對試題2,如果面試者指出字元陣列str1不能在陣列內結束可以給3分;如果面試者指出strcpy(string,str1)呼叫使得從 str1記憶體起復制到string記憶體起所複製的位元組數具有不確定性可以給7分,在此基礎上指出庫函式strcpy工作方式的給10分; 對試題3,if(strlen(str1) <= 10)應改為if(strlen(str1) <10),因為strlen的結果未統計’\0’所佔用的1個位元組。剖析:考查對基本功的掌握 (1)字串以’\0’結尾; (2)對陣列越界把握的敏感度; (3)庫函式strcpy的工作方式,
49.如果編寫一個標準strcpy函式
總分值為10,下面給出幾個不同得分的答案:2分 以下是引用片段: void strcpy( char *strDest, char *strSrc ) { while( (*strDest++ = * strSrc++) != ‘\0’ ); } 4分 以下是引用片段: void strcpy( char *strDest, const char *strSrc ) //將源字串加const,表明其為輸入引數,加2分 { while( (*strDest++ = * strSrc++) != ‘\0’ ); } 7分 以下是引用片段: void strcpy(char *strDest, const char *strSrc) { //對源地址和目的地址加非0斷言,加3分 assert( (strDest != NULL) &&(strSrc != NULL) ); while( (*strDest++ = * strSrc++) != ‘\0’ ); } 10分 以下是引用片段: //為了實現鏈式操作,將目的地址返回,加3分! char * strcpy( char *strDest, const char *strSrc ) { assert( (strDest != NULL) &&(strSrc != NULL) ); char *address = strDest; while( (*strDest++ = * strSrc++) != ‘\0’ ); return address; } 從2分到10分的幾個答案我們可以清楚的看到,小小的strcpy竟然暗藏著這麼多玄機,真不是蓋的!需要多麼紮實的基本功才能寫一個完美的strcpy啊! (4)對strlen的掌握,它沒有包括字串末尾的'\0'。 讀者看了不同分值的strcpy版本,應該也可以寫出一個10分的strlen函數了,完美的版本為: int strlen( const char *str ) //輸入引數const 以下是引用片段: { assert( strt != NULL ); //斷言字串地址非0 int len=0; //注,一定要初始化。 while( (*str++) != '\0' ) { len++; } return len; } 試題4:以下是引用片段: void GetMemory( char *p ) { p = (char *) malloc( 100 ); } void Test( void ) { char *str = NULL; GetMemory( str ); strcpy( str, "hello world" ); printf( str ); } 試題5: 以下是引用片段: char *GetMemory( void ) { char p[] = "hello world"; return p; } void Test( void ) { char *str = NULL; str = GetMemory(); printf( str ); } 試題6:以下是引用片段: void GetMemory( char **p, int num ) { *p = (char *) malloc( num ); } void Test( void ) { char *str = NULL; GetMemory( &str, 100 ); strcpy( str, "hello" ); printf( str ); } 試題7:以下是引用片段: void Test( void ) { char *str = (char *) malloc( 100 ); strcpy( str, "hello" ); free( str ); ... //省略的其它語句 } 解答:試題4傳入中GetMemory( char *p )函式的形參為字串指標,在函式內部修改形參並不能真正的改變傳入形參的值,執行完 char *str = NULL; GetMemory( str ); 後的str仍然為NULL;試題5中 char p[] = "hello world"; return p; 的p[]陣列為函式內的區域性自動變數,在函式返回後,記憶體已經被釋放。這是許多程式設計師常犯的錯誤,其根源在於不理解變數的生存期。 試題6的GetMemory避免了試題4的問題,傳入GetMemory的引數為字串指標的指標,但是在GetMemory中執行申請記憶體及賦值語句 tiffanybracelets *p = (char *) malloc( num ); 後未判斷記憶體是否申請成功,應加上: if ( *p == NULL ) { ...//進行申請記憶體失敗處理 } 試題7存在與試題6同樣的問題,在執行 char *str = (char *) malloc(100); 後未進行記憶體是否申請成功的判斷;另外,在free(str)後未置str為空,導致可能變成一個“野”指標,應加上: str = NULL; 試題6的Test函式中也未對malloc的記憶體進行釋放。 剖析: 試題4~7考查面試者對記憶體操作的理解程度,基本功紮實的面試者一般都能正確的回答其中50~60的錯誤。但是要完全解答正確,卻也絕非易事。
對記憶體操作的考查主要集中在: (1)指標的理解; (2)變數的生存期及作用範圍; (3)良好的動態記憶體申請和釋放習慣。 再看看下面的一段程式有什麼錯誤: 以下是引用片段: swap( int* p1,int* p2 ) { int *p; *p = *p1; *p1 = *p2; *p2 = *p; } 在swap函式中,p是一個“野”指標,有可能指向系統區,導致程式執行的崩潰。在VC++中DEBUG執行時提示錯誤“Access Violation”。該程式應該改為 以下是引用片段: swap( int* p1,int* p2 ) { int p; p = *p1; *p1 = *p2; *p2 = p; }
50.String 的具體實現
已知String類定義如下: class String { public: String(const char *str = NULL); // 通用建構函式 String(const String &another); // 拷貝建構函式 ~ String(); // 解構函式 String & operater =(const String &rhs); // 賦值函式 private: char *m_data; // 用於儲存字串 }; 嘗試寫出類的成員函式實現。 答案: String::String(const char *str) { if ( str == NULL ) //strlen在引數為NULL時會拋異常才會有這步判斷 { m_data = new char[1] ; m_data[0] = '\0' ; } else { m_data = new char[strlen(str) + 1]; strcpy(m_data,str); } } String::String(const String &another)
{ m_data = new char[strlen(another.m_data) + 1]; strcpy(m_data,other.m_data); } String& String::operator =(const String &rhs) { if ( this == &rhs) return *this ; delete []m_data; //刪除原來的資料,新開一塊記憶體 m_data = new char[strlen(rhs.m_data) + 1]; strcpy(m_data,rhs.m_data); return *this ; } String::~String() { delete []m_data ; }
51.h標頭檔案中的ifndef/define/endif 的作用?
答:防止該標頭檔案被重複引用。
52.#i nclude<file.h> 與 #i nclude "file.h"的區別?
答:前者是從Standard Library的路徑尋找和引用file.h,而後者是從當前工作路徑搜尋並引用file.h。
53.在C++ 程式中呼叫被C 編譯器編譯後的函式,為什麼要加extern “C”?
C++語言支援函式過載,C語言不支援函式過載。C++提供了C連線交換指定符號extern “C”
解決名字匹配問題。
首先,作為extern是C/C++語言中表明函式和全域性變數作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其宣告的函式和變數可以在本模組或其它模組中使用。 通常,在模組的標頭檔案中對本模組提供給其它模組引用的函式和全域性變數以關鍵字extern宣告。例如,如果模組B欲引用該模組A中定義的全域性變數和函式時只需包含模組A的標頭檔案即可。這樣,模組B中呼叫模組A中的函式時,在編譯階段,模組B雖然找不到該函式,但是並不會報錯;它會在連線階段中從模組A編譯生成的目的碼中找到此函式 extern "C"是連線申明(linkage declaration),被extern "C"修飾的變數和函式是按照C語言方式編譯和連線的,來看看C++中對類似C的函式是怎樣編譯的: 作為一種面向物件的語言,C++支援函式過載,而過程式語言C則不支援。函式被C++編譯後在符號庫中的名字與C語言的不同。例如,假設某個函式的原型為: void foo( int x, int y ); 該函式被C編譯器編譯後在符號庫中的名字為_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱為“mangled name”)。 _foo_int_int 這樣的名字包含了函式名、函式引數數量及型別資訊,C++就是靠這種機制來實現函式過載的。例如,在C++中,函式void foo( int x, int y )與void foo( int x, float y )編譯生成的符號是不相同的,後者為_foo_int_float。 同樣地,C++中的變數除支援區域性變數外,還支援類成員變數和全域性變數。使用者所編寫程式的類成員變數可能與全域性變數同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函式的處理相似,也為類中的變數取了一個獨一無二的名字,這個名字與使用者程式中同名的全域性變數名字不同。 未加extern "C"宣告時的連線方式 假設在C++中,模組A的標頭檔案如下: // 模組A標頭檔案 moduleA.h #ifndef MODULE_A_H #define MODULE_A_H int foo( int x, int y ); #endif 在模組B中引用該函式: // 模組B實現檔案 moduleB.cpp #i nclude "moduleA.h" foo(2,3);
加extern "C"聲明後的編譯和連線方式 加extern "C"聲明後,模組A的標頭檔案變為: // 模組A標頭檔案 moduleA.h #ifndef MODULE_A_H #define MODULE_A_H extern "C" int foo( int x, int y ); #endif 在模組B的實現檔案中仍然呼叫foo( 2,3 ),其結果是: (1)模組A編譯生成foo的目的碼時,沒有對其名字進行特殊處理,採用了C語言的方式; (2)聯結器在為模組B的目的碼尋找foo(2,3)呼叫時,尋找的是未經修改的符號名_foo。 如果在模組A中函式聲明瞭foo為extern "C"型別,而模組B中包含的是extern int foo( int x, int y ) ,則模組B找不到模組A中的函式;反之亦然。 所以,可以用一句話概括extern “C”這個宣告的真實目的(任何語言中的任何語法特性的誕生都不是隨意而為的,來源於真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎麼做的,還要問一問它為什麼要這麼做,動機是什麼,這樣我們可以更深入地理解許多問題):實現C++與C及其它語言的混合程式設計。 明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧: extern "C"的慣用法 (1)在C++中引用C語言中的函式和變數,在包含C語言標頭檔案(假設為cExample.h)時,需進行下列處理: extern "C" { #i nclude "cExample.h" } 而在C語言的標頭檔案中,對其外部函式只能指定為extern型別,C語言中不支援extern "C"宣告,在.c檔案中包含了extern "C"時會出現編譯語法錯誤。 C++引用C函式例子工程中包含的三個檔案的原始碼如下: /* c語言標頭檔案:cExample.h */ #ifndef C_EXAMPLE_H #define C_EXAMPLE_H extern int add(int x,int y); #endif /* c語言實現檔案:cExample.c */ #i nclude "cExample.h" int add( int x, int y ) { return x + y; } // c++實現檔案,呼叫add:cppFile.cpp extern "C" { #i nclude "cExample.h" } int main(int argc, char* argv[]) { add(2,3); return 0; } 如果C++呼叫一個C語言編寫的.DLL時,當包括.DLL的標頭檔案或宣告介面函式時,應加extern "C" { }。 (2)在C中引用C++語言中的函式和變數時,C++的標頭檔案需新增extern "C",但是在C語言中不能直接引用聲明瞭extern "C"的該標頭檔案,應該僅將C檔案中將C++中定義的extern "C"函式宣告為extern型別。 C引用C++函式例子工程中包含的三個檔案的原始碼如下: //C++標頭檔案 cppExample.h #ifndef CPP_EXAMPLE_H #define CPP_EXAMPLE_H extern "C" int add( int x, int y ); #endif //C++實現檔案 cppExample.cpp #i nclude "cppExample.h" int add( int x, int y ) { return x + y; } /* C實現檔案 cFile.c /* 這樣會編譯出錯:#i nclude "cExample.h" */
int main( int argc, char* argv[] ) { add( 2, 3 ); return 0; } 15題目的解答請參考《C++中extern “C”含義深層探索》註解:
幾道c筆試題(含參考答案) 1. What is displayed when f() is called given the code: class Number { public: string type; Number(): type(“void”) { } explicit Number(short) : type(“short”) { } Number(int) : type(“int”) { } }; void Show(const Number& n) { cout << n.type; } void f() { short s = 42; Show(s); } a) void b) short c) int d) None of the above 2. Which is the correct output for the following code double dArray[2] = {4, 8}, *p, *q; p = &dArray[0]; q = p + 1; cout << q – p << endl; cout << (int)q - (int)p << endl; a) 1 and 8 b) 8 and 4 c) 4 and 8 d) 8 and 1 第一個選C; 雖然傳入的是short型別,但是short型別的建構函式被生命被explicit,也就是隻能顯示型別轉換,不能使用隱式型別轉換。 第二個選A; 第一個是指標加減,按照的是指向地址型別的加減,只跟型別位置有關,q和p指向的資料型別以實際資料型別來算差一個位置,因此是1。而第二個加減是實際指標值得加減,在記憶體中一個double型別佔據8個位元組,因此是8
54.Sony筆試題
1.完成下列程式 * *.*. *..*..*.. *...*...*...*... *....*....*....*....*.... *.....*.....*.....*.....*.....*..... *......*......*......*......*......*......*...... *.......*.......*.......*.......*.......*.......*.......*....... #include #define N 8 int main() { int i; int j; int k; --------------------------------------------------------- | | | | | | --------------------------------------------------------- return 0; } 2.完成程式,實現對陣列的降序排序 #include void sort( ); int main() { int array[]={45,56,76,234,1,34,23,2,3}; //數字任//意給出 sort( ); return 0; } void sort( ) { ____________________________________ | | | | |-----------------------------------------------------| } 3.費波那其數列,1,1,2,3,5……編寫程式求第十項。可以用遞迴,也可以用其 他方法,但要說明你選擇的理由。 #include int Pheponatch(int); int main() { printf("The 10th is %d",Pheponatch(10)); return 0; } int Pheponatch(int N) { -------------------------------- | | | | -------------------------------- } 4.下列程式執行時會崩潰,請找出錯誤並改正,並且說明原因。 #include #include typedef struct{ TNode* left; TNode* right; int value; } TNode; TNode* root=NULL; void append(int N); int main() { append(63); append(45); append(32); append(77); append(96); append(21); append(17); // Again, 數字任意給出 } void append(int N) { TNode* NewNode=(TNode *)malloc(sizeof(TNode)); NewNode->value=N; if(root==NULL) { root=NewNode; return; } else { TNode* temp; temp=root; while((N>=temp.value && temp.left!=NULL) || (N !=NULL )) { while(N>=temp.value && temp.left!=NULL) temp=temp.left; while(N temp=temp.right; } if(N>=temp.value) temp.left=NewNode; else temp.right=NewNode; return; } } ────────────────────────────────────────
55請你分別畫出OSI的七層網路結構圖和TCP/IP的五層結構圖。
應用層:為應用程式提供服務
表示層:處理在兩個通訊系統中交換資訊的表示方式
會話層:負責維護兩個結點間會話連線的建立、管理和終止,以及資料交換
傳輸層:向用戶提供可靠的端到端服務。UDP TCP協議。
網路層:通過路由選擇演算法為分組通過通訊子網選擇最適當的路徑,以及實現擁塞控制、網路互聯等功能。資料傳輸單元是分組。IP地址,路由器,IP協議。
資料鏈路層:在物理層提供的服務基礎上,資料鏈路層在通訊的實體間建立資料鏈路連線,傳輸一幀為單位的資料包(,並採用差錯控制與流量控制方法,使有差錯的物理線路變成無差錯的資料鏈路。)
物理層:傳輸位元流。傳輸單元是位元。調變解調器。
56請你詳細地解釋一下IP協議的定義,在哪個層上面?主要有什麼作用?TCP與UDP呢 ?
網路層。
57.請問交換機和路由器各自的實現原理是什麼?分別在哪個層次上面實現的?
交換機:資料鏈路層。路由器:網路層。
58.全域性變數和區域性變數有什麼區別?是怎麼實現的?作業系統和編譯器是怎麼知道的 ?
全域性變數的生命週期是整個程式執行的時間,而區域性變數的生命週期則是區域性函式或過程呼叫的時間段。其實現是由編譯器在編譯時採用不同記憶體分配方法。全域性變數在main函式呼叫後,就開始分配,如果是靜態變數則是在main函式前就已經初始化了。而區域性變數則是在使用者棧中動態分配的(還是建議看編譯原理中的活動記錄這一塊)
59.8086是多少位的系統?在資料匯流排上是怎麼實現的?
8086微處理器共有4個16位的段暫存器,在定址記憶體單元時,用它們直接或間接地存放段地址。
程式碼段暫存器CS:存放當前執行的程式的段地址。
資料段暫存器DS:存放當前執行的程式所用運算元的段地址。
堆疊段暫存器SS:存放當前執行的程式所用堆疊的段地址。
附加段暫存器ES:存放當前執行程式中一個輔助資料段的段地址。
由cs:ip構成指令地址,ss:sp構成堆疊的棧頂地址指標。DS和ES用作資料段和附加段的段地址(段起始地址或段值)
8086/8088微處理器的儲存器管理
1.地址線(碼)與定址範圍:N條地址線 定址範圍=2N
2.8086有20地址線 定址範圍為1MB 由 00000H~FFFFFH
3. 8086微處理器是一個16位結構,使用者可用的暫存器均為16位:定址64KB
4. 8086/8088採用分段的方法對儲存器進行管理。具體做法是:把1MB的儲存器空間分成若干段,每段容量為64KB,每段儲存器的起始地址必須是一個能被16整除的地址碼,即在20位的二進位制地址碼中最低4位必須是“0”。每個段首地址的高16位二進位制程式碼就是該段的段號(稱段基地址)或簡稱段地址,段號儲存在段暫存器中。我們可對段暫存器設定不同的值來使微處理器的儲存器訪問指向不同的段。
5.段內的某個儲存單元相對於該段段首地址的差值,稱為段內偏移地址(也叫偏移量)用16位二進位制程式碼表示。
6.實體地址是由8086/8088晶片地址引線送出的20位地址碼,它用來參加儲存器的地址譯碼,最終讀/寫所訪問的一個特定的儲存單元。
7.邏輯地址由某段的段地址和段內偏移地址(也叫偏移量)兩部分所組成。寫成:
段地址:偏移地址(例如,1234H:0088H)。
8.在硬體上起作用的是實體地址,實體地址=段基地址×10H十偏移地址
聯想筆試題 1.設計函式 int atoi(char *s)。 2.int i=(j=4,k=8,l=16,m=32); printf(“%d”, i); 輸出是多少?
60.解釋區域性變數、全域性變數和靜態變數的含義。
61.論述含引數的巨集與函式的優缺點。
普天C++筆試題 1.實現雙向連結串列刪除一個節點P,在節點P後插入一個節點,寫出這兩個函式。 2.寫一個函式,將其中的\t都轉換成4個空格。
61.Windows程式的入口是哪裡?寫出Windows訊息機制的流程。
62.C++裡面是不是所有的動作都是main()引起的?如果不是,請舉例。
4.如何定義和實現一個類的成員函式為回撥函式? 5.解釋堆和棧的區別。
6.C++裡面如何宣告const void f(void)函式為C程式中的庫函式? 7.下列哪兩個是等同的 int b; A const int* a = &b; B const* int a = &b; C const int* const a = &b; D int const* const a = &b; 8.行內函數在編譯時是否做引數型別檢查? void g(base & b){ b.play; } void main(){ son s; g(s); return; } 大唐電信 DTT筆試題 考試時間一小時,第一部分是填空和選擇: 1.數列6,10,18,32,“?”,問“?”是幾? 2.某人出70買進一個x,80賣出,90買回,100賣出,這樁買賣怎麼樣? 3.月球繞地球一圈,至少要多少時間