c/c++面試整理

zhangrxiang發表於2019-05-09

C

引用

什麼是“引用”?申明和使用“引用”要注意哪些問題?

答:引用就是某個目標變數的別名(alias),對應用的操作與對變數直接操作效果完全相同。申明一個引用的時候,切記要對其進行初始化。引用宣告完畢後,相當於目標變數名有兩個名稱,即該目標原名稱引用名,不能再把該引用名作為其他變數名的別名。宣告一個引用,不是新定義了一個變數,它只表示該引用名是目標變數名的一個別名,它本身不是一種資料型別,因此引用本身不佔儲存單元,系統也不給引用分配儲存單元。不能建立陣列的引用。

將“引用”作為函式引數有哪些特點?

(1)傳遞引用給函式與傳遞指標的效果是一樣的。這時,被調函式的形參就成為原來主調函式中的實參變數或物件的一個別名來使用,所以在被調函式中對形參變數的操作就是對其相應的目標物件(在主調函式中)的操作。

(2)使用引用傳遞函式的引數,在記憶體中並沒有產生實參的副本,它是直接對實參操作;而使用一般變數傳遞函式的引數,當發生函式呼叫時,需要給形參分配儲存單元,形參變數是實參變數的副本;如果傳遞的是物件,還將呼叫拷貝建構函式。因此,當引數傳遞的資料較大時,用引用比用一般變數傳遞引數的效率和所佔空間都好。

(3)使用指標作為函式的引數雖然也能達到與使用引用的效果,但是,在被調函式中同樣要給形參分配儲存單元,且需要重複使用”*指標變數名”的形式進行運算,這很容易產生錯誤且程式的閱讀性較差;另一方面,在主調函式的呼叫點處,必須用變數的地址作為實參。而引用更容易使用,更清晰。

什麼時候需要使用“常引用”? 

如果既要利用引用提高程式的效率,又要保護傳遞給函式的資料不在函式中被改變,就應使用常引用。常引用宣告方式:const 型別識別符號 &引用名=目標變數名

int a;
const int &ra = a;
ra = 1; // 錯誤
a = 1; // 正確
string foo( );
void bar(string&s)
//下面的表示式將是非法的:
bar(foo());
bar("hello world");

原因在於foo( )"hello world"串都會產生一個臨時物件,而在C++中,這些臨時物件都是const型別的。因此上面的表示式就是試圖將一個const型別的物件轉換為非const型別,這是非法的。引用型引數應該在能被定義為const的情況下,儘量定義為`const 。

將“引用”作為函式返回值型別的格式、好處和需要遵守的規則?

格式:

型別識別符號 &函式名(形參列表及型別說明)
{ 
  //函式體
}

好處

在記憶體中不產生被返回值的副本;(注意:正是因為這點原因,所以返回一個區域性變數的引用是不可取的。因為隨著該區域性變數生存期的結束,相應的引用也會失效,產生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;賦值操作符的返回值必須是一個左值,以便可以被繼續賦值。因此引用成了這個操作符的惟一返回值選擇。

#include <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而導致錯誤。所以可選的只剩下返回一個物件了。

“引用”與多型的關係?

引用是除指標外另一個可以產生多型效果的手段。這意味著,一個基類的引用可以指向它的派生類例項(見:C++中類的多型與虛擬函式的使用)。

Class A; 
Class B : Class A{
  // ...
}; 
B b;
A &ref= b;

“引用”與指標的區別是什麼?

指標通過某個指標變數指向一個物件後,對它所指向的變數間接操作。程式中使用指標,程式的可讀性差;而引用本身就是目標變數的別名,對引用的操作就是對目標變數的操作。此外,就是上面提到的對函式傳refpointer的區別。

什麼時候需要“引用”?

流操作符<<>>賦值操作符=的返回值、拷貝建構函式的引數、賦值操作符=的引數、其它情況都推薦使用引用。

結構與聯合有和區別?

  1. 結構聯合都是由多個不同的資料型別成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間), 而結構的所有成員都存在(不同成員的存放地址不同)。
  2. 對於聯合的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的。

其他

已知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 =newchar[1] ;
       m_data[0] =` ` ;
   }else{
       m_data =newchar[strlen(str) +1];
       strcpy(m_data,str);
   }
} 

String::String(const String &another){
    m_data =newchar[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 =newchar[strlen(rhs.m_data) +1];
    strcpy(m_data,rhs.m_data);
    return*this ;
}

String::~String(){
    delete []m_data ;
}

.h標頭檔案中的ifndef/define/endif 的作用?

防止該標頭檔案被重複引用。

#include <file.h>#include "file.h"的區別?

前者是從Standard Library的路徑尋找和引用file.h,而後者是從當前工作路徑搜尋並引用file.h

在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
#include "moduleA.h"
foo(2,3);

實際上,在連線階段,聯結器會從模組A生成的目標檔案moduleA.obj中尋找_foo_int_int這樣的符號!

加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 ),其結果是:

  • 模組A編譯生成foo的目的碼時,沒有對其名字進行特殊處理,採用了C語言的方式;
  • 聯結器在為模組B的目的碼尋找foo(2,3)呼叫時,尋找的是未經修改的符號名_foo

如果在模組A中函式宣告瞭fooextern "C"型別,而模組B中包含的是extern int foo( int x, int y ) ,則模組B找不到模組A中的函式;反之亦然。所以,可以用一句話概括extern "C"這個宣告的真實目的(任何語言中的任何語法特性的誕生都不是隨意而為的,來源於真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎麼做的,還要問一問它為什麼要這麼做,動機是什麼,這樣我們可以更深入地理解許多問題):實現C++與C及其它語言的混合程式設計。明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧:

extern "C"的慣用法

在C++中引用C語言中的函式和變數,在包含C語言標頭檔案(假設為cExample.h)時,需進行下列處理:

extern"C"
{
  #include"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, inty);
#endif

/* c語言實現檔案:cExample.c */
#include "cExample.h"
int add( int x, int y ){
  return x + y;
}

// c++實現檔案,呼叫add:cppFile.cpp
extern"C" 
{
  #include "cExample.h"
}

int main(int argc, char* argv[]){
  add(2,3); 
  return0;
}

如果C++呼叫一個C語言編寫的.DLL時,當包括.DLL的標頭檔案或宣告介面函式時,應加extern "C" { }

在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
#include"cppExample.h"
int add( int x, int y ){
  return x + y;
}
 
/* C實現檔案 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
externint add( int x, int y );
int main( int argc, char* argv[] ){
  add( 2, 3 ); 
  return0;
}

關聯聚合(Aggregation)以及組合(Composition)的區別?

涉及到UML中的一些概念:

  • 關聯是表示兩個類的一般性聯絡,比如“學生”和“老師”就是一種關聯關係;
  • 聚合表示has-a的關係,是一種相對鬆散的關係,聚合類不需要對被聚合類負責,如下圖所示,用空的菱形表示聚合關係:

從實現的角度講,聚合可以表示為:

class A {...}  
class B { A* a; .....}
  • 組合表示contains-a的關係,關聯性強於聚合:組合類與被組合類有相同的生命週期,組合類要對被組合類負責,採用實心的菱形表示組合關係:

實現的形式是:

class A{...} 
class B{ A a; ...}

物件導向的三個基本特徵,並簡單敘述之?

  1. 封裝:將客觀事物抽象成類,每個類對自身的資料和方法實行protection(private, protected,public)
  2. 繼承:廣義的繼承有三種實現形式:實現繼承(指使用基類的屬性和方法而無需額外編碼的能力)、可視繼承(子窗體使用父窗體的外觀和實現程式碼)、介面繼承(僅使用屬性和方法,實現滯後到子類實現)。前兩種(類繼承)和後一種(物件組合=>介面繼承以及純虛擬函式)構成了功能複用的兩種方式。
  3. 多型:系統能夠在執行時,能夠根據其型別確定呼叫哪個過載的成員函式的能力,稱為多型性。(見:C++中類的多型與虛擬函式的使用)

過載overload)和重寫(override,有的書也叫做“覆蓋overwrite”)的區別?

定義

  • 過載:是指允許存在多個同名函式,而這些函式的參數列不同(或許引數個數不同,或許引數型別不同,或許兩者都不同)。
  • 重寫:是指子類重新定義父類虛擬函式的方法。

原理

  • 過載:編譯器根據函式不同的參數列,對同名函式的名稱做修飾,然後這些同名函式就成了不同的函式(至少對於編譯器來說是這樣的)。如,有兩個同名函式:function func(p:integer):integer;和function func(p:string):integer;。那麼編譯器做過修飾後的函式名稱可能是這樣的:int_func、str_func。對於這兩個函式的呼叫,在編譯器間就已經確定了,是靜態的。也就是說,它們的地址在編譯期就繫結了(早繫結),因此,過載和多型無關!
  • 重寫:和多型真正相關。當子類重新定義了父類的虛擬函式後,父類指標根據賦給它的不同的子類指標,動態的呼叫屬於子類的該函式,這樣的函式呼叫在編譯期間是無法確定的(呼叫的子類的虛擬函式的地址無法給出)。因此,這樣的函式地址是在執行期繫結的(晚繫結)。

多型的作用?

  • 隱藏實現細節,使得程式碼能夠模組化;擴充套件程式碼模組,實現程式碼重用;
  • 介面重用:為了類在繼承和派生的時候,保證使用家族中任一類的例項的某一屬性時的正確呼叫。

AdoAdo.net的相同與不同?

除了“能夠讓應用程式處理儲存於DBMS 中的資料“這一基本相似點外,兩者沒有太多共同之處。但是Ado使用OLE DB 介面並基於微軟的COM 技術,而ADO.NET 擁有自己的ADO.NET 介面並且基於微軟的.NET 體系架構。眾所周知.NET 體系不同於COM 體系,ADO.NET 介面也就完全不同於ADOOLE DB 介面,這也就是說ADO.NETADO是兩種資料訪問方式。ADO.net 提供對XML 的支援。

New deletemalloc free 的聯絡與區別?

都是在(heap)上進行動態的記憶體操作。用malloc函式需要指定記憶體分配的位元組數並且不能初始化物件new 會自動呼叫物件的建構函式。delete 會呼叫物件的destructor,而free 不會呼叫物件的destructor.

有哪幾種情況只能用intializationlist 而不能用assignment?

當類中含有constreference 成員變數;基類的建構函式都需要初始化表。

C++是不是型別安全的?

不是。兩個不同型別的指標之間可以強制轉換(用reinterpret cast)。C#是型別安全的。

main 函式執行以前,還會執行什麼程式碼?

全域性物件的建構函式會在main 函式之前執行,為malloc分配必要的資源,等等。

描述記憶體分配方式以及它們的區別?

  • 靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數static 變數。
  • 上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集。
  • 上分配,亦稱動態記憶體分配。程式在執行的時候用mallocnew 申請任意多少的記憶體,程式設計師自己負責在何時用freedelete 釋放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活,但問題也最多。
  • 程式碼區。

structclass 的區別

struct 的成員預設是公有的,而類的成員預設是私有的。structclass 在其他方面是功能相當的。從感情上講,大多數的開發者感到結構有很大的差別。感覺上結構僅僅象一堆缺乏封裝和功能的開放的記憶體位,而類就象活的並且可靠的社會成員,它有智慧服 務,有牢固的封裝屏障和一個良好定義的介面。既然大多數人都這麼認為,那麼只有在你的類有很少的方法並且有公有資料(這種事情在良好設計的系統中是存在的!)時,你也許應該使用 struct 關鍵字,否則,你應該使用 class 關鍵字。

當一個類A 中沒有生命任何成員變數與成員函式,這時sizeof(A)的值是多少,如果不是零,請解釋一下編譯器為什麼沒有讓它為零。(Autodesk

肯定不是零。舉個反例,如果是零的話,宣告一個class A[10]物件陣列,而每一個物件佔用的空間是零,這時就沒辦法區分A[0],A[1]…了。

8086 彙編下,邏輯地址實體地址是怎樣轉換的?(Intel

通用暫存器給出的地址,是段內偏移地址,相應段暫存器地址*10H+通用暫存器內地址,就得到了真正要訪問的地址。

比較C++中的4種型別轉換方式?

重點是static_cast, dynamic_castreinterpret_cast的區別和應用。

分別寫出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)

請說出const#define 相比,有何優點?

  • const 常量有資料型別,而巨集常量沒有資料型別。編譯器可以對前者進行型別安全檢查。而對後者只進行字元替換,沒有型別安全檢查,並且在字元替換可能會產生意料不到的錯誤。
  • 有些整合化的除錯工具可以對const 常量進行除錯,但是不能對巨集常量進行除錯。

簡述陣列指標的區別?

陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。指標可以隨時指向任意型別的記憶體塊。

  • 修改內容上的差別
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字串
p[0] = ‘X’; // 編譯器不能發現該錯誤,執行時錯誤
  • 用運算子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 位元組
}

類成員函式的過載覆蓋隱藏區別?

成員函式被過載的特徵:

  • 相同的範圍(在同一個類中);
  • 函式名字相同;
  • 引數不同;
  • virtual 關鍵字可有可無。

覆蓋是指派生類函式覆蓋基類函式,特徵是:

  • 不同的範圍(分別位於派生類與基類);
  • 函式名字相同;
  • 引數相同;
  • 基類函式必須有virtual關鍵字。

隱藏是指派生類的函式遮蔽了與其同名的基類函式,規則如下:

  • 如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
  • 如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)

相關文章