1. 程式人生 > >[C/C++常見筆試面試題] 程式設計基礎 - 面向物件相關、虛擬函式、程式設計技巧篇

[C/C++常見筆試面試題] 程式設計基礎 - 面向物件相關、虛擬函式、程式設計技巧篇

13 面向物件相關

面向物件思想是程式設計歷史上一次偉大的創新,面向物件的提出極大地提高了程式設計的效率,為程式設計的重用性奠定了堅實的基礎,面向物件思想已經廣泛應用在現今主流的程式語言中,如C++、Java、C#等。

 

13.1 面向物件與面向過程有什麼區別?

面向物件

面向物件是把資料及對資料的操作方法放在一起,作為一個相互依存的整體,即物件。對同類物件抽象出其共性,即類,類中的大多數資料,只能被本類的方法進行處理。類通過一個簡單的外部介面與外界發生關係,物件與物件之間通過訊息進行通訊。程式流程由使用者在使用中決定。例如,站在抽象的角度,人類具有身高、體重、年齡、血型等一些特性。人類僅僅只是一個抽象的概念,它是不存在的實體,但是所有具備人類這個群體的屬性與方法的物件都叫人,這個物件人是實際存在的實體,每個人都是人這個類的一個物件。

面向過程

面向過程是一種以事件為中心的開發方法,就是自頂向下順序執行,逐步求精,其程式結構是按功能劃分為若干個基本模組,這些模組形成一個樹狀結構,各模組之間的關係也比較簡單,在
功能上相對獨立,每一模組內部一般都是由順序、選擇和迴圈三種基本結構組成的,其模組化實現的具體方法是使用子程式,而程式流程在寫程式時就已經決定。
例如五子棋,面向過程的設計思路就是首先分析問題的步驟:第一步,開始遊戲;第二步,黑子先走;第三步,繪製畫面;第四步, 判斷輸贏;第五步,輪到白子;第六步,繪製畫面;第七步,判斷輸贏;第八步,返回步驟2;第九步,輸出最後結果。把上面每個步驟用分別的函式來實現,就是一個面向過程的開發方法。

區別

(1) 出發點不同。

面向物件是用符合常規思維方式來處理客觀世界的問題,強調把問題的要領直接對映到物件及物件之間的介面上。而面向過程方法則不然,它強調的是過程的抽象化與模組化,它是以過程為中心構造或處理客觀世界問題的。

(2) 層次邏輯關係不同。

面向物件方法則是用計算機邏輯來模擬客觀世界中的物理存在,以物件的集合類作為處理問題的基本單位,用類的層次結構來體現類之間的繼承和發展。而面向過程方法處理問題的基本單位是能清晰準確地表達過程的模組,用模組的層次結構概括模組或模組間的關係與功能,把客觀世界的問題抽象成計算機可以處理的過程。

(3) 資料處理方式與控制程式方式不同。

面向物件方法將資料與對應的程式碼封裝成一個整體,原則上其他物件不能直接修改其資料,即物件的修改只能由自身的成員函式完成。控制程式方式上是通過“事件驅動”來啟用和執行程式。而面向過程方法是直接通過程式來處理資料,處理完畢後即可顯示處理結果。在控制程式方式上是按照設計呼叫或返回程式,不能自由導航,各模組之間存在著控制與被控制、 呼叫與被呼叫的關係。

(4) 分析設計與編碼轉換方式不同。

面向物件方法貫穿軟體生命週期的分析、設計及編碼之間,是一種平滑過程,從分析到設計再到編碼採用一致性的模型表示,即實現的是一種無縫連線。而面向過程方法強調分析、設計及編碼之間按規則進行轉換,貫穿軟體生命週期的分析、設計及編碼之間,實現的是一種有縫的連線。


13.2 面向物件的基本特徵有哪些?

面向物件方法首先對需求進行合理分層,然後構建相對獨立的業務模組,最後通過整合各模組,達到高內聚、低耦合的效果,從而滿足客戶要求。具體而言,它有3個基本特徵:封裝、繼承和多型。

(1) 封裝是指將客觀事物抽象成類,每個類對自身的資料和方法實行保護。類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。C++中類是一種封裝手 段,採用類來描述客觀事物的過程就是封裝,本質上是對客觀事物的抽象。

(2) 繼承可以使用現有類的所有功能,而不需要重新編寫原來的類,它的目的是為了進行程式碼複用和支援多型。它一般有3種形式:實現繼承、可視繼承、介面繼承。其中,實現繼承 是指使用基類的屬性和方法而無需額外編碼的能力;可視繼承是指子窗體使用父窗體的外觀和實現程式碼;介面繼承僅使用屬性和方法,實現滯後到子類實現。前兩種(類繼承)和後一種 (物件組合=>介面繼承以及純虛擬函式)構成了功能複用的兩種方式。

(3) 多型是指同一個實體同時具有多種形式,它主要體現在類的繼承體系中,它是將父物件設定成為和一個或更多的它的子物件相等的技術,賦值以後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。簡單地說,就是允許將子類型別的指標賦值給父類型別的指標。編譯時多型是靜態多型,在編譯時就可以確定物件使用的形式。


13.3 什麼是深拷貝?什麼是淺拷貝?

如果一個類擁有資源(堆或者是其他系統資源),當這個類的物件發生複製過程時,資源重新分配,這個過程就是深拷貝;反之物件存在資源,但複製過程並未複製資源的情況視為淺拷貝。

例如,在某些狀況下,類內成員變數需要動態開闢堆記憶體,如果實行位複製,也就是把物件裡的值完全複製給另一個物件,如A=B,這時,如果類B中有一個成員變數指標已經申請了記憶體,那麼類A中的那個成員變數也指向同一塊記憶體。這就出現了問題:當B把記憶體釋放 了,如通過解構函式,這時A內的指標就變成野指標了,導致執行錯誤。

深複製的程式示例如下:

#include <iostream> 

using namespace std; 

class CA 
{
public:
    CA(int b,char* cstr);
    CA(const CA& C); 
    void Show();
    〜CA(); 
private: 
    int a; 
    char *str;
};

CA::CA(int b,char* cstr)
{
    a=b;
    str=new char[b]; 
    strcpy(str,cstr);
}

CA::CA(const CA& C) 
{
    a=C.a;
    str=new char[a]; //給str重新分配記憶體空間,所以為深複製 
    if(str!=0)
        strcpy(str,C.str);
}

void CA::Show()
{
    cout<<str<<endl;
}

CA::〜CA()
{
    delete str;
}

int main()
{
    CA A(10,"Hello"); 
    CA B=A; 
    B.Show(); 
    
    return 0;
}

程式輸出結果:

Hello

如果沒有自定義複製建構函式時, 系統將會提供給一個預設的複製建構函式來完成這個過程,就會造成“淺拷貝”。所以要自定義賦值建構函式,重新分配記憶體資源,實現“深拷貝”。


13.4 什麼是友元?

類具有封裝、繼承、多型、資訊隱藏的特性,只有類的成員函式才可以訪問類的標記為 private的私有成員,非成員函式可以訪問類中的公有成員,但是卻無法訪問私有成員,為了使非成員函式可以訪問類的成員,唯一的做法就是將成員都定義為public,但如果將資料成員都定義為公有的,這又破壞了資訊隱藏的特性。

友元正好解決了這一棘手的問題。在使用友元函式時,一般需要注意以下幾個方面的問題:

(1) 必須在類的說明中說明友元函式,說明時以關鍵字friend開頭,後跟友元函式的函式原型,友元函式的說明可以出現在類的任何地方,包括private和public部分。

(2) 友元函式不是類的成員函式,所以友元函式的實現與普通函式一樣,在實現時不用 “::”指示屬於哪個類,只有成員函式才使用“::”作用域符號。

(3) 友元函式不能直接訪問類的成員,只能訪問物件成員。

(4) 呼叫友元函式時,在實際引數中需要指出要訪問的物件。

(6) 類與類之間的友元關係不能繼承。

友元一般定義在類的外部,但它需要在類體內進行說明,為了與該類的成員函式加以區別,在說明時前面加以關鍵字friend。需要注意的是,友元函式不是成員函式,但是它可以訪問類中的私有成員。友元的作用在於提高程式的執行效率,但是它破壞了類的封裝性和隱藏 性,使得非成員函式可以訪問類的私有成員。

如下為一個友元函式的例子:

#include <iostream>
#include <string> 
using namespace std; 

class Fruit
{
public:
    Fruit(const string &nst="apple",const string &cst="green"):name(nst),colour(cst)
    {        
    }
    
    〜Fruit()
    {
    }
    friend istream& operator>>(istream&,Fruit&); 
    friend ostream& operator<<(ostream&,const Fruit&); 
    
    void print()
    {
    cout<<colour<<" "<<name<<endl;
    }

private:
    string name; 
    string colour;
};

ostream& operator<<(ostream &out,const Fruit &s) //過載輸出操作符
{
    out<<s.colour<<" "<<s.name; 
    return out;
}

istream& operator>>(istream& in,Fruit &s) //過載輸入操作符
{
    in>>s.co1our>>s.name; 
    
    if(!in)
        cerr<<"Wrong input!"<<endl; 
        
    return in;
}

int main()
{
    Fruit apple; 
    cin>>apple; 
    cout<<apple; 
    
    return 0;
}


13.5 基類的建構函式/析構兩數是否能被派生類繼承?

基類的建構函式/解構函式不能被派生類繼承。

基類的建構函式不能被派生類繼承,派生類中需要宣告自己的建構函式。在設計派生類的建構函式時,不僅要考慮派生類所增加的資料成員初始化,也要考慮基類的資料成員的初始化。宣告建構函式時,只需要對本類中新增成員進行初始化,對繼承來的基類成員的初始化, 需要呼叫基類建構函式完成。

基類的解構函式也不能被派生類繼承,派生類需要自行宣告解構函式。需要注意的是,解構函式的呼叫次序與建構函式相反。


13,6 類的成員變數的初始化順序是按照宣告順序嗎?

在C++中,類的成員變數的初始化順序只與變數在類中的宣告順序有關,與在建構函式中的初始化列表的順序無關。而且靜態成員變數先於例項變數,父類成員變數先於子類成員變數。

示例程式如下:

class Test 
{
private :
    int nl; 
    int n2;     
public:
    Test();
};

Test::Test():n2(2),nl(1)
{}

當檢視相關彙編程式碼時,就能看到正確的初始化順序了。因為成員變數的初始化次序跟變數在記憶體中的次序有關,而記憶體中的排列順序早在編譯期就根據變數的定義次序決定了。

從全域性看,變數的初始化順序如下:

(1) 基類的靜態變數或全域性變數。

(2) 派生類的靜態變數或全域性變數。

(3) 基類的成員變數。

(4) 派生類的成員變數。


13.7 一個類為另一個類的成員變數時,如何對其進行初始化?

示例程式如下:

class ABC 
{
public:
    ABC(int x, int y, int z); 
private : 
    int a; 
    int b; 
    int c;
};

class MyClass 
{
public:
    MyClass():abc(1,2,3)
    {
        
    } 

private:
ABC abc;
};

上例中,因為ABC有了顯式的帶引數的建構函式,那麼它是無法依靠編譯器生成無參建構函式的,所以必須使用初始化列表:abc(1,2,3),才能構造ABC的物件。


13.8 C++中的空類預設產生哪些成員函式?

C++的空類是指這個類不帶任何資料,即類中沒有非靜態(non-static)資料成員變數,沒有虛擬函式(virtual function),也沒有虛基類(virtual base class)。 直觀地看,空類物件不使用任何空間,因為沒有任何隸屬物件的資料需要儲存。然而,C++標準規定,凡是一個獨立的(非附屬)物件都必須具有非零大小。換句話說,C++空類的大小不為0,而是為1。為了驗證這個結論,可以先來看測試程式的輸出:

#include <iostream>
using namespace std;

class NoMembers
{
};

int main()
{
    NoMembers n;
    cout << "The size of empty class is: "<< sizeof(n) << endl;
}

程式輸出結果:

The size of empty class is: 1

C++中空類預設會產生以下6個函式:預設建構函式、複製建構函式、解構函式、賦值運算子過載函式、取址運演算法過載函式、const取址運算子過載函式等。

class Empty
{
public:
    Empty();//預設建構函式
    Empty( const Empty& );// 複製建構函式
    〜Empty();//解構函式
    Empty& operator=(const Empty&);// 賦值運算子
    Empty* operator&();// 取址運算待
    const Empty* operator&( ) const; // 取址運算子 const
};


13.9 C++提供預設引數的函式嗎?

C++可以給函式定義預設引數值。在函式呼叫時沒有指定與形參相對應的實參時,就自動使用預設引數。

預設引數的語法與使用:

(1) 在函式宣告或定義時,直接對引數賦值,這就是預設引數。

(2) 在函式呼叫時,省略部分或全部引數。這時可以用預設引數來代替。

通常呼叫函式時,要為函式的每個引數給定對應的實參。例如:

void delay(int loops=1000);//函式宣告 

void delay(int loops) //函式定義 
{
    if(loops==0)
    {
        return;
    }
    for(int i=0;i<loops;i++)
        ;
}

在上例中,如果將delay()函式中的loops定義成預設值1000,這樣,以後無論何時呼叫delay()函式,都不用給loops賦值,程式都會自動將它當做值 1000進行處理。例如,當執行delay(2500)呼叫時,loops的引數值為顯性化的,被設定為 2500;當執行delay()時,loops將採用預設值1000。

 

預設引數在函式宣告中提供,當有宣告又有定義時,定義中不允許預設引數。如果函式只有定義,則預設引數才可出現在函式定義中。例如:

oid point(int=3,int=4);//宣告中給出預設值 

void point(int x,int y) //定義中不允許再給出預設值 
{
    cout<<x<<endl;
    cout<<y<<endl;
}

 

如果一組過載函式(可能帶有預設引數)都允許相同實參個數的呼叫,將會引起呼叫的二 義性。例如:

void func(int);//過載函式之一 
void func(int,int=4);//過載函式之二,帶有預設引數 
void func(int=3,int=4);//過載函式三,帶有預設引數 
func(7);//錯誤:到底呼叫3個過載函式中的哪個? 
func(20,30);//錯誤:到底呼叫後面兩個過載函式的哪個?


14 虛擬函式

虛擬函式中的“虛”並不是實際生活中虛擬的意思,因為沒有“實”函式的說法。虛擬函式是面向物件程式設計中函式的一種特定形態,是C++中用於實現多型的一種有效機制。

14.1 什麼是虛擬函式?

指向基類的指標在操作它的多型類物件時,會根據不同的類物件呼叫其相應的函式,這個函式就是虛擬函式。虛擬函式的作用是在程式的執行階段動態地選擇合適的成員函式,在定義了虛擬函式後,可以在基類的派生類中對虛擬函式進行重新定義。如果在派生類中沒有對虛擬函式重新定義,則它繼承其基類的虛擬函式。

#include<iostream> 
using namespace std;

class Base
{
public:
    virtual void Print()
    {
        printf("This is Class Base!\n");
    }
};

class Derived:public Base
{
public:
    void Print()
    {
        printf("This is Class Derived!\n");
    }
};

int main()
{
    Derived Cderived;
    Base Cbase;
    Base *p1 = &Cderived;
    Base *p2 = &Cbase;
    p1->Print();
    p2->Print();
}

程式輸出結果:

This is Class Derived!
This is Class Base!

需要注意的是,虛擬函式雖然非常好用,但是在使用虛擬函式時,並非所有的函式都需要定義成虛擬函式,因為實現虛擬函式是有代價的。在使用虛擬函式時,需要注意以下幾個方面的內容:

(1) 只需要在宣告函式的類體中使用關鍵字virtual將函式宣告為虛擬函式,而定義函式時不需要使用關鍵字virtual。

(2) 當將基類中的某一成員函式宣告為虛擬函式後,派生類中的同名函式自動成為虛擬函式。

(3) 非類的成員函式不能定義為虛擬函式,全域性函式以及類的成員函式中靜態成員函式和建構函式也不能定義為虛擬函式,但可以將解構函式定義為虛擬函式。

(4) 基類的解構函式應該定義為虛擬函式,這樣可以在實現多型的時候不造成記憶體洩漏。基類解構函式未宣告virtual,基類指標指向派生類時,delete指標不呼叫派生類解構函式。有 virtual,則先呼叫派生類析構再呼叫基類析構。

(5) 基類指標動態建立派生類物件,普通呼叫派生類建構函式。


14.2 C++中如何阻止一個類被例項化?

C++中可以通過使用抽象類,或者將建構函式宣告為private阻止一個類被例項化。抽象類之所以不能被例項化,是因為抽象類不能代表一類具體的事物,它是對多種具有相似性的具體事物的共同特徵的一種抽象。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但是動物本身生成物件不合情理。


15 程式設計技巧

程式設計,容易;技巧,容易;程式設計技巧,不容易。

 

15.1 表示式a>b>c足什麼意思?

在弄清這個問題前,先看如下程式碼:

#include <stdio.h> 

int main()
{
    int a=5,b=4,c=3; 
    printf("%d\n",a>b>c); 
    
    return 0;
}

程式輸出結果:

0

對於這種連續運算,根據優先順序,首先進行a>b 的比較判斷,本例中a>b為真,所以返回值為1,接著比較該返回值與c的大小。因為c的值 為3,1>c表示式為假,所以返回值為0。所以,最終的輸出為0。

 

對於賦值運算子,結果又如何呢?以如下程式為例。

#include <stdio.h>
int main()
{
    int b,c;
    int a=(b=(c=020)&&(1—2)); 
    printf("%d %d %d\n",a,b,c); 
    
    return 0;
}

在賦值語句中,c=020,因為以0開頭的數字一般表示的都是八進位制的數值,所以摺合成十進位制的數為16。根據優先順序關係,b的值為(c=020)&&(1=2)的結果,由於c=020是一個賦值語句,所以該賦值語句的返回值為真,即為1,而1==2則為假,返回值為0,所以b的值為0,a=(b=0),所以a的值為0。


15.2 如何實現一個最簡單病毒?

可以把最簡單的病毒理解為一個無限執行的惡意程式,無限執行可以通過無限迴圈實現,而惡意可以通過申請記憶體空間來實現,所以可以用如下程式碼來實現一個最簡單的病毒。

while(l)
{
    int *p=new int[10000000];
}

該程式碼首先新建一個無限迴圈,然後在迴圈內執行一個記憶體申請操作,最終系統記憶體會被該程式佔用完,導致系統出現宕機的情況。


15.3 如何只使用一條語句實現x是否為2的若干次冥的判斷?

如果一個數是2的若干次冪,那麼其二進位制表示中只有一位為1,其他位都為0,該數減去1之後的數的二進位制表示為全1,所以將兩數進行與操作,判斷其最終結果是否為0,為0則說明是2的若干次冥。

程式示例如下:

int i = 512;
cout<<i&(i-1)?false:true<<endl;


15.4 \n是否與\n\r 等價?

換行(\n)就是游標下移一行卻不會移到這一行的開頭,回車(\r)就是回到當前行的開頭卻不向下移一行。

按(Enter〉鍵後會執行"\n\r",這樣就是看到的一般意義的回車了,所以在用16進位制檔案檢視方式看一個文字,就會在行尾發現"\n\r"。

Tab是製表符,就是"\t”,作用是預留8個字元的顯示寬度,用於對齊。