29道C++ 面向對象高頻題整理(附答案背誦版)

1、什麼是類?

在C++中,類是一種用戶定義的數據類型,它可以包含數據成員和函數成員。數據成員用於存儲與類相關的狀態,而函數成員可以定義對這些數據進行操作的方法。可以把類想象爲一個藍圖,根據這個藍圖可以創建對象,這些對象在內存中是類的實例。

比如說,我們可以定義一個Car類來表示汽車。這個類可以有數據成員如brandcolormaxSpeed來存儲汽車的品牌、顏色和最高速度等屬性。同時,Car類可能有函數成員如accelerate()brake()來定義汽車加速和剎車的操作。

在現實生活中,每輛汽車都是根據汽車製造商設計的藍圖製造出來的,藍圖定義了汽車的特性和功能,類似地,在編程中,我們根據類創建對象來表示現實世界中的各種事物和概念。

2、面向對象的程序設計思想是什麼?

面向對象程序設計(OOP)是一種編程範式,它使用“對象”來設計軟件。在OOP中,對象是類的實例,類包含數據(屬性)和可以對數據執行操作的方法(行爲)。面向對象的核心概念包括封裝、繼承和多態性。

  1. 封裝:是指將數據(屬性)和操作數據的代碼(方法)打包在一起,形成一個獨立的對象。這樣可以隱藏對象的內部細節,只暴露必要的操作接口。比如,一個汽車對象封裝了引擎、變速器等細節,只提供加速和剎車等接口。

  2. 繼承:允許新的類(子類)繼承現有類(父類)的屬性和方法。繼承可以複用代碼,並且可以創建層次結構。例如,可以有一個基本的車輛類,然後有子類如汽車、摩托車等,它們繼承基本類的共同特性。

  3. 多態性:指的是不同類的對象可以通過同一接口調用,具有不同的行爲。例如,如果有一個函數接受車輛類的對象,那麼任何車輛的子類對象,如汽車或摩托車,都可以使用該函數,但具體的行爲會根據對象的實際類型而有所不同。

OOP的思想是通過模仿現實世界來組織和設計代碼,使得代碼更加模塊化、易於理解和維護。通過把現實世界的實體映射成程序中的類和對象,開發者可以在更高的層次上思考問題,這樣可以更容易地解決複雜的軟件問題。

3、面向對象的三大特徵是哪些?

面向對象編程(OOP)的三大特徵是封裝、繼承和多態。它們是OOP中最核心的概念,每個特徵都解決了軟件開發中的一些常見問題。

  1. 封裝:封裝是隱藏對象內部複雜性的過程,同時暴露出必要的功能。這可以防止外部代碼直接訪問對象內部的狀態,減少了外部干擾和錯誤使用的可能性。在C++中,通常通過訪問修飾符(private、protected、public)來實現封裝。

    應用場景示例:銀行賬戶類(BankAccount)可能包含私有數據成員來存儲賬戶餘額,並提供公共方法來進行存款和取款,而不允許直接修改賬戶餘額。

  2. 繼承:繼承允許新創建的類(稱爲子類)繼承父類的屬性和方法。繼承可以實現代碼複用,並且可以形成一個類的層次結構。

    應用場景示例:可以有一個通用的Vehicle類,它包含所有交通工具的共通特徵,然後可以有子類如CarTruckMotorcycle,它們繼承Vehicle類並添加特定於它們的屬性和方法。

  3. 多態:多態性意味着可以通過基類的指針或引用來調用派生類的方法。這使得程序可以在不知道對象確切類型的情況下對對象進行操作,從而使程序可以在運行時動態決定對象的行爲。

    應用場景示例:可以定義一個Shape基類,並且有多個派生類如CircleRectangleTriangle。每個派生類都有一個draw()方法的實現。如果有一個Shape類型的數組,程序可以遍歷這個數組,並調用每個形狀的draw()方法,具體調用哪一個實現,取決於數組元素的實際類型。

這三個特性共同支撐起面向對象編程的基礎結構,使得OOP成爲了一個強大和靈活的編程範式。

4、C++中struct和class有什麼區別?

在C++中,struct(結構體)和class(類)在語法上非常相似,但它們有一個主要的默認訪問權限和默認繼承類型的區別:

  1. 默認訪問權限:在class中,默認的成員訪問權限是私有的(private),而在struct中,默認的是公共的(public)。這意味着除非你明確指定,否則class的成員和繼承類型都是私有的,而struct的成員和繼承類型默認是公開的。

  2. 默認繼承類型:當從structclass繼承時,如果沒有顯式指定繼承類型(public、protected或private),struct會默認採用public繼承,而class會默認採用private繼承。

除了這些默認行爲的差異,structclass在C++中是幾乎相同的,它們都可以包含數據成員、成員函數、構造函數、析構函數、成員函數重載、運算符重載等。

在實際使用中,struct通常用於包含數據的簡單的聚合類型,而class通常用於需要封裝和複雜行爲的對象。但這更多是編程風格和傳統的選擇,而不是強制的規則。

例如,如果你有一個只包含數據的點結構,你可能會選擇使用struct

struct Point {
    int x;
    int y;
};

如果你有一個更復雜的數據結構,可能需要封裝和方法來操作數據,你可能會選擇使用class

class Car {
private:
    int speed;
    int gear;
public:
    void accelerate(int increment);
    void decelerate(int decrement);
    // 更多的成員函數和構造函數
};

在現代C++編程中,選擇struct還是class更多是基於你想要表達的意圖,而不是它們的技術區別。

5、動態多態有什麼作用?有哪些必要條件?

動態多態是面向對象編程中的一個核心特性,它允許在運行時通過指向基類的指針或引用來調用派生類的方法,使得相同的操作可以作用於不同類型的對象上,從而表現出不同的行爲。

動態多態的作用非常廣泛,它允許程序代碼更加通用和靈活。例如,你可以設計一個函數,它接受一個基類的引用,然後在運行時,這個函數可以用不同派生類的對象來調用,而且不需要修改函數本身的代碼。這種能力使得代碼重用更加容易,可以構建更加抽象和動態的系統。

動態多態的實現有幾個必要條件:

  1. 繼承:必須有兩個類,一個基類和一個從基類派生出來的子類。

  2. 基類中的虛函數:在基類中必須有至少一個函數被聲明爲虛函數(使用virtual關鍵字)。派生類通常會重寫(override)這個虛函數來提供特定的功能。

  3. 基類的指針或引用:需要通過基類的指針或引用來調用虛函數,這樣C++運行時才能利用虛函數表(v-table)來動態決定調用哪個函數。

  4. 動態綁定:當通過基類的指針或引用調用虛函數時,發生的是動態綁定,這意味着直到程序運行時,才決定調用對象的哪個方法。

舉個例子,假設有一個基類Shape和兩個派生類CircleSquare。基類中有一個虛函數draw()。那麼你可以通過Shape的指針或引用來調用draw(),在運行時,如果指向的是Circle對象,則調用的是Circledraw()實現,如果是Square對象,則調用Squaredraw()實現。

這使得程序能夠對不同類型的對象進行操作,而無需知道對象的確切類型,從而增加了程序的靈活性和可擴展性。

6、C++中類成員的訪問權限

在C++中,類成員的訪問權限是通過訪問修飾符來控制的,主要有三種:publicprotectedprivate

  1. Public(公共):

    • public成員在任何地方都可以訪問。
    • 如果一個類的成員被聲明爲public,那麼這個成員可以在類的內部被訪問,類的對象可以直接訪問它,繼承該類的子類也可以訪問。
  2. Protected(受保護):

    • protected成員在類內部和派生類中可以訪問,但是不能通過類的對象直接訪問。
    • 這意味着如果一個成員聲明爲protected,那麼它對於任何從該類派生的類都是可訪問的,但是不可以通過對象來直接訪問。
  3. Private(私有):

    • private成員只能在類內部被訪問。
    • 這是最嚴格的訪問級別,如果成員被聲明爲private,那麼它只能被類的成員函數、友元函數訪問,即使是子類也無法訪問私有成員。

下面是一個簡單的類定義,展示瞭如何使用這些訪問修飾符:

class MyClass {
public:    // 公共成員
    int publicVariable;

    void publicFunction() {
        // ...
    }

protected: // 受保護成員
    int protectedVariable;

    void protectedFunction() {
        // ...
    }

private:   // 私有成員
    int privateVariable;

    void privateFunction() {
        // ...
    }
};

訪問權限是面向對象設計的一個重要方面,它幫助我們實現封裝。封裝不僅僅是將數據和行爲包裝在一起,還包括對數據的保護,確保只有通過類提供的接口才能訪問和修改數據,防止了外部的非法訪問,降低了代碼的複雜性,並使得維護和擴展更加容易。

7、多態的實現有哪幾種?

在C++中,多態主要通過以下兩種方式實現:

  1. 編譯時多態(靜態多態)

    • 這種多態在編譯時發生,主要通過函數重載和運算符重載實現。
    • 函數重載是在同一作用域內有多個同名函數,但它們的參數類型或數量不同,編譯器根據函數調用時傳入的參數類型和數量來決定調用哪個函數。
    • 運算符重載是一種特殊的函數重載,它允許爲類定義新的操作符函數,使得可以使用傳統操作符來操作對象。
  2. 運行時多態(動態多態)

    • 這種多態在程序運行時發生,主要通過虛函數實現。
    • 虛函數:當一個函數在基類中被聲明爲虛函數時,它可以在任何派生類中被重寫。通過基類的指針或引用調用虛函數時,會根據對象的實際類型來調用相應的函數,即使是在基類類型的引用或指針下也是如此。
    • 純虛函數和抽象類:當在類中聲明一個虛函數但不提供實現,只提供其聲明的時候,這個函數就是純虛函數(使用= 0語法),包含純虛函數的類稱爲抽象類。抽象類不能被實例化,只能被繼承,並且派生類必須提供純虛函數的實現。

動態多態是通過虛函數表(也稱爲V-Table)來實現的,這是一種在運行時用來解析函數調用的機制。當類中包含虛函數時,每個對象會包含一個指向虛函數表的指針,虛函數表中存儲了對應於該對象實際類型的函數地址。這樣,當調用虛函數時,程序能夠動態地決定應該調用哪個函數實現。

這兩種多態的方式都允許同一接口使用不同的實現,使得程序可以在不完全知道對象類型的情況下,對對象進行操作。靜態多態的優點是效率高,因爲函數調用在編譯時就已經解析了;而動態多態的優點是靈活性高,可以在運行時決定調用哪個函數。

8、動態綁定是如何實現的?

在C++中,動態綁定是通過虛函數來實現的。虛函數允許在派生類中重寫基類的行爲。在基類中聲明虛函數時,使用關鍵字virtual,這樣在派生類中就可以覆蓋這個函數以實現不同的行爲。

當我們使用基類的指針或引用來調用一個虛函數時,C++運行時會根據對象的實際類型來決定應該調用哪個函數,這個過程是在運行時發生的,因此被稱爲“動態綁定”。

舉個例子,假設我們有一個Animal基類和兩個派生類DogCatAnimal類中有一個虛函數makeSound()DogCat類分別覆蓋了這個函數,提供了各自的實現。

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Some generic animal sound\n";
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!\n";
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!\n";
    }
};

當我們這樣調用時:

Animal* myAnimal = new Dog();
myAnimal->makeSound(); // 輸出 "Woof!"

即使myAnimal是一個Animal類型的指針,它也會調用Dog類中的makeSound()函數,因爲myAnimal實際指向的是一個Dog對象。這就是動態綁定的工作原理。如果將myAnimal指向Cat類的對象,那麼調用myAnimal->makeSound()將輸出"Meow!"。這種機制使得我們可以寫出更加靈活和可擴展的代碼。

9、動態多態有什麼作用?有哪些必要條件?

動態多態在C++中主要用於允許在運行時選擇使用哪個函數,即使我們在編寫代碼時不知道確切的對象類型。它使得程序可以更加靈活,可以編寫出既通用又可擴展的代碼。通過動態多態,同一個接口可以對應多種不同的實現,這有助於減少代碼冗餘和增強代碼的可維護性。

動態多態的實現有以下必要條件:

  1. 繼承:必須有一個基類和一個或多個派生類。
  2. 虛函數:在基類中必須有虛函數,派生類中可以重寫這些虛函數。
  3. 指針或引用:使用基類類型的指針或引用來操作派生類的對象。

應用場景的例子:考慮一個圖形編輯器,我們可以定義一個Shape基類,並且有多個派生類如CircleRectangle等。Shape類中有一個虛函數draw(),每個派生類都有自己的實現。

class Shape {
public:
    virtual void draw() const = 0; // 純虛函數,使得Shape成爲抽象類
};

class Circle : public Shape {
public:
    void draw() const override {
        // 繪製圓形的代碼
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        // 繪製矩形的代碼
    }
};

在圖形編輯器中,我們可能有一個Shape類型的列表,其中包含了各種形狀的對象。在運行時,我們可以遍歷這個列表,調用每個形狀的draw()函數來繪製它們。這樣,無論列表中有什麼類型的形狀,都會調用正確的繪製函數,這就是動態多態的作用。

10、純虛函數有什麼作用?如何實現?

純虛函數在C++中用於創建抽象類,這種類不能直接實例化,而是用來定義派生類應遵循的接口。當類中至少有一個純虛函數時,這個類就成爲了抽象類。純虛函數定義了一個接口,派生類需要覆蓋這個接口提供具體的實現。

純虛函數的作用主要有兩個:

  1. 定義接口規範:它規定了派生類必須實現的函數,確保所有派生類都遵循同一接口規範。
  2. 阻止基類實例化:它使得不能創建基類的對象,只能創建派生類的對象,這樣可以確保客戶代碼不會錯誤地使用不完整的基類對象。

純虛函數的聲明在C++中是在函數聲明末尾加上= 0。這裏的= 0並不表示函數返回值爲0,而是C++語法中表示函數爲純虛函數的特殊標記。

下面是一個純虛函數的例子:

class Base {
public:
    virtual void doSomething() = 0; // 純虛函數
};

class Derived : public Base {
public:
    void doSomething() override {
        // 提供具體的實現
    }
};

在這個例子中,Base是一個抽象類,因爲它有一個純虛函數doSomething()Derived類繼承自Base並提供了doSomething()的具體實現。這樣,不能直接創建Base類的對象,但可以創建Derived類的對象。

在設計模式中,純虛函數經常用來定義接口或者抽象基類,以便不同的派生類可以提供多樣化的實現,這是實現多態的關鍵部分。

11、虛函數表是針對類的還是針對對象的?同一個類的兩個對象的虛函數表是怎麼維護的?

答:虛函數表,或者稱爲vtable,是針對類的。虛函數表是一個存儲類中所有虛函數地址的數組。當我們定義一個類,並在其中聲明瞭虛函數時,編譯器就會爲這個類生成一個虛函數表。

每一個對象(或者說是實例),只要它的類有虛函數,那麼它就會有一個指向這個類的虛函數表的指針。這意味着,同一個類的各個對象,它們的虛函數表指針都指向同一個虛函數表。所以,雖然每個對象都有自己的虛函數表指針,但是同一個類的所有對象共享同一個虛函數表。

舉個例子,假設我們有一個基類Animal,它有一個虛函數makeSound()。那麼,Animal就有一個虛函數表,其中包含了makeSound()的地址。然後我們創建了兩個Animal對象,catdog。這兩個對象都有一個指針指向Animal的虛函數表,即使是兩個不同的對象,但是它們的虛函數表是相同的。

然後,如果我們有一個子類Cat繼承自Animal,並且重寫了makeSound()函數。那麼,Cat也會有一個虛函數表,其中makeSound()的地址被替換爲Cat類中的makeSound()函數的地址。當我們創建一個Cat對象kitty時,kitty的虛函數表指針就會指向Cat的虛函數表。

12、爲什麼基類的構造函數不能定義爲虛函數?

在C++中,基類的構造函數不能被定義爲虛函數,原因有兩個:

  1. 構造函數的目的是初始化對象。當我們創建一個對象時,構造函數被調用來初始化對象的數據成員。在這個階段,對象纔剛剛開始被構建,還沒有完全形成,因此它還不具備執行虛函數調用的條件(即,動態綁定)。因爲執行虛函數調用需要通過對象的虛函數表指針,而這個指針在構造函數執行完畢後纔會被設置。

  2. 虛函數通常在有繼承關係的類中使用,用於實現多態。在子類對象的構造過程中,首先會調用基類的構造函數,然後纔是子類的構造函數。如果基類的構造函數被定義爲虛函數,那麼在執行基類的構造函數時,由於子類的部分還沒有被構造,所以無法正確地執行子類構造函數中對虛函數的重寫。這就破壞了虛函數的目的,即允許子類重寫基類的行爲。

因此,基於以上原因,C++不允許構造函數爲虛函數。但是,析構函數可以(並且通常應該)被聲明爲虛函數,以確保當刪除一個指向派生類對象的基類指針時,派生類的析構函數能被正確調用,避免資源泄露。

13、爲什麼基類的析構函數需要定義爲虛函數?

在C++中,基類的析構函數應該被定義爲虛函數,主要是爲了能正確地釋放動態分配的資源,避免內存泄漏。

當我們使用基類指針指向派生類對象,並使用delete刪除這個指針時,如果基類的析構函數不是虛函數,那麼只有基類的析構函數會被調用。這樣,派生類的析構函數就沒有機會被調用,導致派生類中的資源沒有被正確釋放,造成內存泄漏。

而如果我們將基類的析構函數定義爲虛函數,那麼在刪除基類指針時,就會根據這個指針實際指向的對象類型,調用相應的析構函數,先調用派生類的析構函數,然後再調用基類的析構函數。這樣就能確保所有的資源都被正確釋放,避免內存泄漏。

舉個例子,假設我們有一個基類Animal和一個派生類CatCat類在堆上分配了一些資源。如果我們用一個Animal指針指向一個Cat對象,然後用delete刪除這個指針,如果Animal的析構函數不是虛函數,那麼只有Animal的析構函數會被調用,Cat的析構函數不會被調用,Cat在堆上分配的資源就沒有被釋放,造成內存泄漏。而如果Animal的析構函數是虛函數,那麼就會先調用Cat的析構函數,釋放Cat的資源,然後再調用Animal的析構函數,這樣就避免了內存泄漏。

14、構造函數和析構函數能拋出異常嗎?

在C++中,構造函數和析構函數都可以拋出異常,但這並不是一個被推薦的做法,原因如下:

構造函數拋出異常:

如果在構造函數中拋出異常,那麼對象的構造過程就會被中斷。這就意味着對象可能處於一個部分初始化的狀態,其成員可能沒有被正確初始化。如果你試圖在後續的代碼中使用這個對象,可能會出現未定義的行爲。

舉個例子,你有一個DatabaseConnection類,其構造函數試圖連接到數據庫。如果連接失敗,構造函數就拋出一個異常。這個時候,如果你在後續的代碼中試圖使用這個DatabaseConnection對象,就可能出現問題,因爲它並沒有正確地初始化。

析構函數拋出異常:

如果在析構函數中拋出異常,情況就更復雜了。析構函數通常在對象生命週期結束時被調用,或者在釋放動態分配的內存時被調用。如果在這個過程中析構函數拋出了異常,而你又沒有正確地捕獲這個異常,那麼程序就可能會中斷,並可能導致資源泄露。

更糟糕的是,如果析構函數是在處理另一個異常時被調用,並在這個過程中又拋出了一個新的異常,那麼C++會立即調用std::terminate,程序會立即終止。

因此,雖然構造函數和析構函數都可以拋出異常,但是在大多數情況下,我們應該儘量避免在這兩個函數中拋出異常,或者至少確保這些異常被正確地捕獲和處理,以避免未定義的行爲

15、如何讓一個類不能實例化?

在C++中,如果你希望一個類不能被實例化,也就是不能創建該類的對象,你可以通過以下兩種方式來實現:

  1. 聲明類的構造函數爲protected或private: 如果一個類的構造函數被聲明爲protected或private,那麼在類的外部就不能直接調用這個構造函數來創建類的對象。只有類本身和它的友元函數或類可以訪問它的私有或保護成員。
class NonInstantiable1 {
private:
    NonInstantiable1() {} // private constructor
};
  1. 將類聲明爲抽象基類(Abstract Base Class, ABC): 如果一個類至少有一個純虛函數,那麼這個類就是抽象基類,無法被實例化。純虛函數是在基類中聲明但不定義的虛函數,它在基類中的聲明形式如下:virtual void func() = 0;。純虛函數使得派生類必須提供自己的實現,否則派生類也將成爲抽象基類。
class NonInstantiable2 {
public:
    virtual void func() = 0; // pure virtual function
};

上述兩種方式都可以讓一個類不能直接實例化,但是可以作爲基類被繼承。在派生類中,你可以提供構造函數的實現或者實現基類中的純虛函數,使得派生類可以被實例化。

由於內容太多,更多內容以鏈接形勢給大家,點擊進去就是答案了

16. 如果類A是一個空類,那麼sizeof(A)的值爲多少?

17. 覆蓋和重載之間有什麼區別?

18. 拷貝構造函數和賦值運算符重載之間有什麼區別?

19. 對虛函數和多態的理解

20. 請你來說一下C++中struct和class的區別

21. 說說強制類型轉換運算符

22. 簡述類成員函數的重寫、重載和隱藏的區別

23. 類型轉換分爲哪幾種?各自有什麼樣的特點?

24. RTTI是什麼?其原理是什麼?

25. 說一說c++中四種cast轉換

26. C++的空類有哪些成員函數

27. 模板函數和模板類的特例化

28. 爲什麼析構函數一般寫成虛函數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章