InterView之C/CPP
CPP
引用
什麽是“引用”?申明和使用“引用”要註意哪些問題?
答:引用就是某個目標變量的別名(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;
“引用”與指針的區別是什麽?
指針通過某個指針變量指向一個對象後,對它所指向的變量間接操作。程序中使用指針,程序的可讀性差;而引用本身就是目標變量的別名,對引用的操作就是對目標變量的操作。此外,就是上面提到的對函數傳ref
和pointer
的區別。
什麽時候需要“引用”?
流操作符<<
和>>
、賦值操作符=的返回值
、拷貝構造函數的參數、賦值操作符=的參數
、其它情況都推薦使用引用。
結構與聯合有和區別?
結構
和聯合
都是由多個不同的數據類型成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間), 而結構的所有成員都存在(不同成員的存放地址不同)。- 對於聯合的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的。
其他
已知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] ='\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中函數聲明了foo
為extern "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; ...}
面向對象
的三個基本特征,並簡單敘述之?
封裝
:將客觀事物抽象成類,每個類對自身的數據和方法實行protection
(private
,protected
,public
)繼承
:廣義的繼承有三種實現形式:實現繼承
(指使用基類的屬性和方法而無需額外編碼的能力)、可視繼承
(子窗體使用父窗體的外觀和實現代碼)、接口繼承
(僅使用屬性和方法,實現滯後到子類實現)。前兩種(類繼承)和後一種(對象組合=>接口繼承以及純虛函數)構成了功能復用的兩種方式。多態
:系統能夠在運行時,能夠根據其類型確定調用哪個重載的成員函數的能力,稱為多態性。(見:C++中類的多態與虛函數的使用)
重載
(overload
)和重寫
(override
,有的書也叫做“覆蓋overwrite”
)的區別?
定義
重載
:是指允許存在多個同名函數,而這些函數的參數表不同(或許參數個數不同,或許參數類型不同,或許兩者都不同)。重寫
:是指子類重新定義父類虛函數的方法。
原理
重載
:編譯器根據函數不同的參數表,對同名函數的名稱做修飾,然後這些同名函數就成了不同的函數(至少對於編譯器來說是這樣的)。如,有兩個同名函數:function func(p:integer):integer;和function func(p:string):integer;
。那麽編譯器做過修飾後的函數名稱可能是這樣的:int_func、str_func
。對於這兩個函數的調用,在編譯器間就已經確定了,是靜態的。也就是說,它們的地址在編譯期就綁定了(早綁定),因此,重載和多態無關!重寫
:和多態真正相關。當子類重新定義了父類的虛函數後,父類指針根據賦給它的不同的子類指針,動態的調用屬於子類的該函數,這樣的函數調用在編譯期間是無法確定的(調用的子類的虛函數的地址無法給出)。因此,這樣的函數地址是在運行期綁定的(晚綁定)。
多態
的作用?
隱藏實現細節
,使得代碼能夠模塊化;擴展代碼模塊,實現代碼重用;接口重用
:為了類在繼承和派生的時候,保證使用家族中任一類的實例的某一屬性時的正確調用。
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
的支持。
New
delete
與malloc
free
的聯系與區別?
都是在堆
(heap
)上進行動態的內存操作。用malloc
函數需要指定內存分配的字節數並且不能初始化對象,new
會自動調用對象的構造函數。delete
會調用對象的destructor
,而free
不會調用對象的destructor
.
有哪幾種情況只能用intializationlist
而不能用assignment
?
當類中含有const
、reference
成員變量;基類的構造函數都需要初始化表。
C++是不是類型安全的?
不是。兩個不同類型的指針之間可以強制轉換(用reinterpret cast
)。C#是類型安全的。
main
函數執行以前,還會執行什麽代碼?
全局對象的構造函數會在main
函數之前執行,為malloc
分配必要的資源,等等。
描述內存分配方式以及它們的區別?
- 從
靜態存儲區域
分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量
,static
變量。 - 在
棧
上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集。 - 從
堆
上分配,亦稱動態內存分配
。程序在運行的時候用malloc
或new
申請任意多少的內存,程序員自己負責在何時用free
或delete
釋放內存。動態內存的生存期由程序員決定,使用非常靈活,但問題也最多。 - 代碼區。
struct
和 class
的區別
struct
的成員默認是公有的,而類的成員默認是私有的。struct
和 class
在其他方面是功能相當的。從感情上講,大多數的開發者感到類
和結構
有很大的差別。感覺上結構僅僅象一堆缺乏封裝和功能的開放的內存位,而類就象活的並且可靠的社會成員,它有智能服 務,有牢固的封裝屏障和一個良好定義的接口。既然大多數人都這麽認為,那麽只有在你的類有很少的方法並且有公有數據(這種事情在良好設計的系統中是存在的!)時,你也許應該使用 struct
關鍵字,否則,你應該使用 class
關鍵字。
當一個類A 中沒有生命任何成員變量與成員函數,這時sizeof(A)
的值是多少,如果不是零,請解釋一下編譯器為什麽沒有讓它為零。(Autodesk
)
肯定不是零。舉個反例,如果是零的話,聲明一個class A[10]
對象數組,而每一個對象占用的空間是零,這時就沒辦法區分A[0]
,A[1]…
了。
在8086
匯編下,邏輯地址
和物理地址
是怎樣轉換的?(Intel
)
通用寄存器給出的地址,是段內偏移地址,相應段寄存器地址*10H+通用寄存器內地址,就得到了真正要訪問的地址。
比較C++中的4種類型轉換方式?
重點是static_cast
, dynamic_cast
和reinterpret_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
關鍵字。此時,基類的函數被隱藏(註意別與覆蓋混淆)
InterView之C/CPP