c++面試題
一 用簡潔的語言描述 c++
在 c 語言的基礎上開發的一種面向對象編程的語言; 應用廣泛; 支持多種編程范式,面向對象編程,泛型編程,和過程化編程;廣泛應用於系統開發,引擎開發;支持類,封裝,重載等特性。
二 c 和 c++ 的區別
- C++ 在 c 的基礎上添加類;
- C主要是面向過程,C + + 主要面向對象;
- C主要考慮通過一個過程將輸入量經過各種運算后得到一個輸出, C++ 主要考慮是如何構造一個對象模型,讓這個模型契合與之對應的問題域, 這樣就可以通過獲取對象的狀態信息得到輸出。
三 什么是面向對象
面向對象是一種對現實世界理解和抽象的方法、思想,通過將需求要素轉化為對象進行問題處理的一種思想。
四 封裝,繼承,多態,虛函數
封裝:封裝是實現面向對象程序設計的第一步,封裝就是將數據或函數等集合在一個個的單元中(我們稱之為類)。封裝的意義在於保護或者防止代碼(數據)被我們無意中破壞。
繼承:繼承主要實現重用代碼,節省開發時間。子類可以繼承父類的一些東西。
多態:是指相同的操作或函數、過程可作用於多種類型的對象上並獲得不同的結果。不同的對象,收到同一消息可以產生不同的結果,這種現象稱為多態。
A. 多態
定義: “一個接口,多種方法”,程序在運行時才決定調用的函數。
實現: C++多態性主要是通過虛函數實現的,虛函數允許子類重寫override(注意和overload的區別,overload是重載,是允許同名函數的表現,這些函數參數列表/類型不同)。
目的: 封裝可以使得代碼模塊化,繼承可以擴展已存在的代碼,他們的目的都是為了代碼重用。而多態的目的則是為了接口重用。為了類在繼承和派生的時候,保證使用家族中任一類的實例的某一屬性時的正確調用。
B.什么是虛函數,什么函數不能聲明為虛函數?
-
那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而采用不同的策略。
-
構造函數。因為要構造一個對象,必須清楚地知道要構造什么,否則無法構造一個對象。析構函數可以為純虛函數。
C.為什么要用純虛函數?
在很多情況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。為了解決這個問題,方便使用類的多態性,引入了純虛函數的概念,將函數定義為純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。
D. 在什么情況下使用純虛函數(pure vitrual function)?
- 當想在基類中抽象出一個方法,且該基類只做能被繼承,而不能被實例化;
- 這個方法必須在派生類(derived class)中被實現;
E. 虛函數與純虛函數的區別
虛函數為了重載和多態。 在基類中是有定義的,即便定義為空。 在子類中可以重寫。
純虛函數在基類中沒有定義, 必須在子類中加以實現。
多態的基礎是繼承,需要虛函數的支持,簡單的多態是很簡單的。
子類繼承父類大部分的資源,不能繼承的有構造函數,析構函數,拷貝構造函數, operator=函數,友元函數等等
五 設計模式
六 常見的STL容器有哪些,算法哪些
STL包括兩部分,容器和算法。(重要的還有融合這二者的迭代器)
1. 容器
即為存放數據的地方。分為兩類。
- 序列式容器:vector,list,deque等
- 關聯式容器: 內部結構基本上是一顆平衡二叉樹。所謂關聯,指每個元素都有兼職和一個實值。
舉例:vector是動態分配存儲空間的容器。
2. 算法
排序,復制等等,與各個容器相關聯。
3. 迭代器
STL的精髓。可以這樣描述,它提供了一種方法,使之能夠按照順序訪問某個容器所含的各個元素,但是無需暴露該容器的內部結構。它將容器和算法分開,好讓這二者獨立設計。
七 開發中常用到的數據結構有哪些。
數組,鏈表,樹。也會用到棧(先進后出)和隊列(先進先出)。
1.數組和鏈表的區別。(很簡單,但是很常考,記得要回答全面)
C++語言中可以用數組處理一組數據類型相同的數據,但不允許動態定義數組的大小,即在使用數組之前必須確定數組的大小。而在實際應用中,用戶使用數組之 前有時無法准確確定數組的大小,只能將數組定義成足夠大小,這樣數組中有些空間可能不被使用,從而造成內存空間的浪費。鏈表是一種常見的數據組織形式,它 采用動態分配內存的形式實現。需要時可以用new分配內存空間,不需要時用delete將已分配的空間釋放,不會造成內存空間的浪費。
從邏輯結構來看:
數組必須事先定義固定的長度(元素個數),不能適應數據動態地增減的情況,即數組的大小一旦定義就不能改變。當數據增加時,可能超出原先 定義的元素個數;當數據減少時,造成內存浪費;鏈表動態地進行存儲分配,可以適應數據動態地增減的情況,且可以方便地插入、刪除數據項。(數組中插入、刪 除數據項時,需要移動其它數據項)。
從內存存儲來看:
(靜態)數組從棧中分配空間(用NEW創建的在堆中), 對於程序員方便快速,但是自由度小;鏈表從堆中分配空間, 自由度大但是申請管理比較麻煩.
從訪問方式來看:
數組在內存中是連續存儲的,因此,可以利用下標索引進行隨機訪問;鏈表是鏈式存儲結構,在訪問元素的時候只能通過線性的方式由前到后順序訪問,所以訪問效率比數組要低。
2. 二叉樹的遍歷
二叉樹的遍歷,就是按照某條搜索路徑訪問樹中的每一個結點,使得每個結點均被訪問一次,而且僅被訪問一次。
- 常見的遍歷次序有:
先序遍歷:先訪問根結點,再訪問左子樹,最后訪問右子樹
中序遍歷:先訪問左子樹,再訪問根結點,最后訪問右子樹
后序遍歷:先訪問左子樹,再訪問右子樹,最后訪問根結點 - 時間復雜度 O(n)
http://www.linuxidc.com/Linux/2015-08/122480.htm
八. const與static的用法
1. const:
- const修飾類的成員變量,表示該成員變量不能被修改。
- const修飾函數,表示本函數不會修改類內的數據成員。不會調用其他非const成員函數。
- const函數只能調用const函數,非const函數可以調用const函數
類外定義的const成員函數,在定義和聲明出都需要const修飾符。
2. static:
2.1 對變量:
a. 局部變量:
在局部變量之前加上關鍵字static,局部變量就被定義成為一個局部靜態變量。位於內存中靜態存儲區; 未初始化的局部動初始化為0. 作用域仍是局部作用域.注:當static用來修飾局部變量的時候,它就改變了局部變量的存儲位置(從原來的棧中存放改為靜態存儲區)及其生命周期(局部靜態變量在離開作用域之后,並沒有被銷毀,而是仍然駐留在內存當中,直到程序結束,只不過我們不能再對他進行訪問),但未改變其作用域。
b. 全局變量.
在全局變量之前加上關鍵字static,全局變量就被定義成為一個全局靜態變量。靜態存儲區,未經初始化的全局靜態變量會被程序自動初始化為0,全局靜態變量在聲明他的文件之外是不可見的。准確地講從定義之處開始到文件結尾。注: static修飾全局變量並未改變其存儲位置及生命周期, 而是改變了其作用域,使得當前文件外的源文件無法訪問該變量.不能被其他文件訪問和修改,其他文件中可以使用相同名字的變量,不會產生沖突.
2.2 對類:
a. 成員變量.
用static修飾類的數據成員實際使其成為類的全局變量,會被類的所有對象共享,包括派生類的對象。因此,static成員必須在類外進行初始化(初始化格式: int base::var=10;),而不能在構造函數內進行初始化,不過也可以用const修飾static數據成員在類內初始化 。
注意:
- 不要試圖在頭文件中定義(初始化)靜態數據成員。在大多數的情況下,這樣做會引起重復定義這樣的錯誤。即使加上#ifndef #define #endif或者#pragma once也不行。
- 靜態數據成員可以成為成員函數的可選參數,而普通數據成員則不可以。
- 靜態數據成員的類型可以是所屬類的類型,而普通數據成員則不可以。普通數據成員的只能聲明為 所屬類類型的指針或引用。
b. 成員函數
注意:
a. 用static修飾成員函數,使這個類只存在這一份函數,所有對象共享該函數,不含this指針。
b. 靜態成員是可以獨立訪問的,也就是說,無須創建任何對象實例就可以訪問。base::func(5,3);當static成員函數在類外定義時不-需要加static修飾符。
c. 在靜態成員函數的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員。因為靜態成員函數不含this指針。
d. 不可以同時用const和static修飾成員函數。
C++編譯器在實現const的成員函數的時候為了確保該函數不能修改類的實例的狀態,會在函數中添加一個隱式的參數const this*。但當一個成員為static的時候,該函數是沒有this指針的。也就是說此時const的用法和static是沖突的。
我們也可以這樣理解:兩者的語意是矛盾的。static的作用是表示該函數只作用在類型的靜態變量上,與類的實例沒有關系;而const的作用是確保函數不能修改類的實例的狀態,與類型的靜態變量沒有關系。因此不能同時用它們。
九. 類的static變量在什么時候初始化,函數的static變量在什么時候初始化。
類的靜態成員在類實例化之前就存在了,並分配了內存。函數的static變量在執行此函數時進行實例化。
十 指針和引用
- 指針是一個變量,存放地址的變量,指向內存的一個存儲單元,引用僅是個別名。
- 引用必須被初始化, 指針不必
- 引用使用時無需加*。
- 引用沒有const修飾,指針有const修飾
- sizeof引用對象得到的是所指對象,變量的大小;sizeof指針得到的是指針本身的大小
- 指針可有多級,引用只可一級
- 內存分配上,程序為指針變量分配內存,不為引用分配內存。
1. 引用作為參數的優點:
(1)傳遞引用給函數與傳遞指針的效果是一樣的。
(2)使用引用傳遞函數的參數,在內存中並沒有產生實參的副本,它是直接對實參操作(注意:正是因為這點原因,所以返回一個局部變量的引用是不可取的。因為隨着該局部變量生存期的結束,相應的引用也會失效,產生runtime error!;
(3)使用指針作為函數的參數雖然也能達到與使用引用的效果,但是,在被調函數中同樣要給形參分配存儲單元,且需要重復使用"*指針變量名"的形式進行運算,這很容易產生錯誤且程序的閱讀性較差;另一方面,在主調函數的調用點處,必須用變量的地址作為實參。而引用更容易使用,更清晰。
2. 注意:
(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)相關聯的時候,其賦值常常與某些其它屬性或者對象的狀態有關,因此有必要將賦值操作封裝在一個業務規則當中。如果其它對象可以獲得該屬性的非常 量引用(或指針),那么對該屬性的單純賦值就會破壞業務規則的完整性。
3. 引用與多態的關系?
引用是除指針外另一個可以產生多態效果的手段。這意味着,一個基類的引用可以指向它的派生類實例
十一 內存
1.內存類別
棧 --由編譯器自動分配釋放, 局部遍歷存放位置
堆 --由程序員分配和釋放.
全局區(靜態區) --全局變量和靜態變量的存儲是放在一起的, 初始化的全局變量和static靜態變量在一塊區域.
程序代碼區 --存放二進制代碼.
在函數體中定義的變量通常是在棧上, 用malloc, 等分配內存的函數分配得到的就是在堆上. 在所有函數體外定義的是全局量, 加了static修飾符后不管在哪里都存放在全局區, 在所有函數體外定義的static變量表示在該文件有效, 不能extern 到別的文件用. 在函數體內定義的static表示只在該函數體內有效.
2. 堆棧溢出的原因:
數組越界, 沒有回收內存, 深層次遞歸調用
3. 內存分配方式
內存分配的三種方式: a 靜態存儲區,程序編譯時便分好, 整個運行期間都存在,比如全局變量,常量; b, 棧上分配; 堆上分配。
4. 避免內存泄漏
原因:動態分配的內存沒有手動釋放完全.
避免:使用的時候應記得指針的長度; 分配多少內存應記得釋放多少, 保證一一對應的關系; 動態分配內存的指針最好不要再次賦值.
十二 常用排序算法.
1. 冒泡:
兩兩比較相鄰關鍵字,如果反序則交換.
for ( int i = 0; i < A. length-1; i++){
for ( int j = i + 1; j < A.length-1; j++){
if ( A[i] > A[j])
swap(A, i, j);
}
}
}
2. 選擇:
先不急於調換位置, 先從A[i]開始逐個檢查, 記下最小數的坐標.掃描完一遍之后根據需要進行交換.
for ( int i = 0; i < A.length-1; i ++){
int min = i;
for ( int j = i + 1; j < A.length; j ++)
if ( A[min] > A[j])
min = j;
}
if ( i != min)
swap(A, i, min);
3. 插入:
將一個記錄插入到已經排好序的有序表中,從而得到一個新的,記錄數增1的 有序表.
for (int i = 1; i < A.length; i ++){
if( A[i] < A[i - 1]){
int tmp = A[i];
int j;
for ( j = i - 1; j >= 0 && A[j] > tmp; j--){
A[j+1] = A[j];
}
A[j+1] = tmp;
}
}
4. 快速排序:
通過一趟排序將待排序記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可以分別對這兩部分記錄進行排序,已達到整個序列有序的目的.
快速排序是一種不穩定的排序方法.
5. 各種排序算法的使用范圍總結:
(1)當數據規模較小的時候,可以用簡單的排序算法如直接插入排序或直接選擇排序。
(2)當文件的初態已經基本有序時,可以 用直接插入排序或冒泡排序。
(3)當數據規模比較大時,應用速度快的排序算法。可以考慮用快速排序。當記錄隨機分布的時候,快排的平均時間最短,但可能出 現最壞的情況,這時候的時間復雜度是O(n^2),且遞歸深度為n,所需的棧空間問O(n)。
(4)堆排序不會出現快排那樣的最壞情況,且堆排序所需的輔 助空間比快排要少。但這兩種算法都不是穩定的,若要求排序時穩定的,可以考慮用歸並排序。
(5)歸並排序可以用於內排序,也可以用於外排序。在外排序時, 通常采用多路歸並,並且通過解決長順串的合並,產生長的初始串,提高主機與外設並行能力等措施,以減少訪問外存額次數,提高外排序的效率。
排序分類 平均時間 時間復雜度 輔助存儲空間
簡單排序 O(n2) O(n2) O(1)
快速排序 O(nlog2n) O(nlog2n) O(nlog2n)
堆排序 O(nlog2n) O(nlog2n) O(1)
歸並排序 O(nlog2n) O(nlog2n) O(n)
十三 請說出const與#define 相比,有何優點?
const 常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查。而對后者只進行字符替換,沒有類型安全檢查,並且在字符替換可能會產生意料不到的錯誤。
十四 c++中靜態函數和靜態變量.
類靜態數據成員在編譯時創建並初始化,在該類的任何對象簡歷之前就存在,不屬於任何對象, 而非靜態類成員則是屬於對象所有.類靜態數據成員只是一個拷貝,為所有此類的對象所共享.
類靜態成員函數屬於某個類,不屬於某個對象,由該類所有對象共享.
- static成員變量實現了同類對象間的信息共享
- static成員類外儲存,求類大小,並不包含在內.
- static成員是命名空間, 屬於類的全局變量.
- static成員只能類外初始化.
- 可以通過類名訪問, 也可通過對象訪問.
靜態成員函數的意義不在於信息共享和數據溝通, 而是在於管理靜態數據成員, 完成對靜態數據成員的封裝.
靜態成員函數只能訪問靜態數據成員, 原因在於非靜態成員函數, 在調用時, this指針被當做參數傳進, 而靜態成員函數屬於類, 不屬於對象, 沒有this指針.
十五 new和malloc的算法.
malloc和free是c語言的標准庫函數, new/delete是c++的運算符.都可用來申請動態內存釋放.
由於malloc/free是庫函數,不是運算符,因此不能將執行構造函數和析構函數的任務強加於malloc/free. c++需要一個能夠完成動態內存分配和初始化工作的運算符new. 主要, new/delete不是庫函數.
c++可以調用c函數, 而c程序只能用malloc/free管理動態內存.
new可以認為是malloc加構造函數的執行. new出來的指針都是直接帶類型信息的, 而malloc返回的都是void指針.
十六 switch 的參數類型不能是
實形
十七 如何引用一個已經定義過的全局變量
引用頭文件和extern關鍵字。 如果采用引用頭文件, 若變量寫錯了,則在編譯期間便會出錯。 如果用extern則在鏈接階段報錯。
十八 頻繁出現的短小函數, 在c和c++中分別如何實現。
c中使用宏定義, c++中使用inline內聯函數
十九 數組與指針的區別。
數組被定義在靜態存儲區(全局變量)或棧上, 指針可指向任意類型數據塊。
二十 c++函數值傳遞的方式。
值傳遞, 指針傳遞, 引用傳遞
二十一 extern“C”作用
實現C++與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+ +中對類似。
二十二 定義一個宏時應注意什么。
定義部分的每個形參和整個表達式都必須用括號括起來,以避免不可預料的錯誤。
二十三 系統會自動打開和關閉的三個標准文件是
stdin, stdout, stderr
二十四 結構與聯合有和區別?
- 結構和聯合都是由多個不同的數據類型成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間), 而結構的所有成員都存在(不同成員的存放地址不同)。
- 對於聯合的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的。
二十五 .h頭文件中的ifndef/define/endif 的作用?
防止該頭文件被重復引用。
二十六 重載(overload)和重寫(overried,有的書也叫做“覆蓋”)的區別?
常考的題目。
1. 從定義上來說:
重載:是指允許存在多個同名函數,而這些函數的參數表不同(或許參數個數不同,或許參數類型不同,或許兩者都不同)。
重寫:是指子類重新定義父類虛函數的方法。
2. 從實現原理上來說:
重載:編譯器根據函數不同的參數表,對同名函數的名稱做修飾,然后這些同名函數就成了不同的函數(至少對於編譯 器來說是這樣的)。如,有兩個同名函數:function func(p:integer):integer;和function func(p:string):integer;。那么編譯器做過修飾后的函數名稱可能是這樣的:int_func、str_func。對於這兩個函數的 調用,在編譯器間就已經確定了,是靜態的。也就是說,它們的地址在編譯期就綁定了(早綁定),因此,重載和多態無關!
重寫:和多態真正相關。當子類重新定義了父類的虛函數后,父類指針根據賦給它的不同的子類指針,動態的調用屬於子類的該函數,這樣的函數調用在編譯期間是無法確定的(調用的子類的虛函數的地址無法給出)。因此,這樣的函數地址是在運行期綁定的(晚綁定)。
二十八 struct 和 class 的區別
答案:struct 的成員默認是公有的,而類的成員默認是私有的。struct 和 class 在其他方面是功能相當的。
二十九 如何判斷浮點數是否為0,或判讀兩個浮點數是否相等。
答案:對於浮點數x,若判斷其是否等於0.5,不可直接用 “==” 號, 正確的做法應該是:
if (fabs(x - 0.5) < DBL_EPSILON)
{
//滿足這個條件,我們就認為x和0.5相等,否則不等
puts("ok");//打印了ok
10 }
具體原因比較復雜,一言以蔽之,計算機無法精確的表示浮點數。
三十 new 和 delete 的實現原理。
有一篇博客分析的很透徹,可點擊這里查看。