C++の要約学習ノート (3) - オブジェクト指向

 記事からの主な引用:

Axiu の学習ノート (interviewguide.cn)

Nuke.com - 求人検索ツール | 筆記試験質問バンク | 面接体験談 | おすすめインターンシップ募集、就活・就職のワンストップソリューション_Nuke.com (nowcoder.com)

1. オブジェクト指向

オブジェクト指向とは何かを簡単に説明します (Tencent)

参考回答

  1. オブジェクト指向とは、人、ヘッドフォン、マウス、水のカップなど、あらゆるものをオブジェクトとして扱うプログラミングの考え方です。それぞれに属性があります。たとえば、ヘッドフォンは白、マウスは黒、水のカップは円筒形など、これらのオブジェクトが持つ属性変数とその属性変数を操作する関数をクラスにパッケージ化して表現します。

  2. プロセス指向とオブジェクト指向の違い

    プロセス指向:ビジネス ロジックに基づいて上から下にコードを記述し、機能に従ってモジュールを分割し、各モジュールは比較的独立しており、モジュール実装の具体的な方法はサブルーチンを使用することです。ただし、データと処理プロセスは独立した実体であるため、データが変化すると、それに応じて関連するすべての処理も変更する必要があり、その結果、プログラムの可用性が低下します。

    オブジェクト指向:データと関数をバインドしてカプセル化することで、プログラムをより迅速に開発し、重複したコードを書き直すプロセスを減らすことができます。

オブジェクト指向の 3 つの主要な特徴を簡単に説明します。

参考回答

オブジェクト指向の 3 つの主要な特徴は、カプセル化、継承、ポリモーフィズムです。

  1. カプセル化:データデータを操作するメソッドを有機的に組み合わせ、オブジェクトのプロパティと実装の詳細を非表示にしオブジェクトと対話するインターフェイスのみを公開します。カプセル化は本質的に管理の一形態です。兵馬俑をどのように管理しますか? たとえば、何も対処しなければ、兵馬俑や兵馬俑は意のままに破壊されてしまいます。そこで私たちはまず兵馬俑と馬を囲う家を建てました。しかし、私たちの目的は完全にカプセル化されており、他人に見られることは許可されていません。したがって、私たちはチケット販売チャネルをオープンし、合理的な監督メカニズムの下で訪問するためのチケットを購入することができます。クラスについても同様で、他の人に見られたくない場合は、protected/private を使用してメンバーをカプセル化します。一部の共有メンバー関数がメンバーに適切にアクセスできるようにします。したがって、カプセル化は本質的に一種の管理です。

  2. 継承:元のクラスを書き直すことなく、既存のクラスのすべての機能を使用し、これらの機能を拡張できます

    3つの継承方法

    継承方法 私的継承 保護された継承 パブリック継承
    基本クラスのプライベートメンバー 見えない 見えない 見えない
    基本クラスの保護されたメンバー プライベート会員になる まだ保護されたメンバーです まだ保護されたメンバーです
    基本クラスのパブリックメンバー プライベート会員になる 保護されたメンバーになる まだ一般会員です まだ一般会員です
  3. ポリモーフィズム:静的ポリモーフィズム (関数のオーバーロード)動的ポリモーフィズム (仮想関数のみになり得る親クラスの仮想関数をサブクラスがオーバーライドする) が含まれ、一般に動的ポリモーフィズムを指します。ポリモーフィズムは関数の再利用であり、その本質はコードの再利用にも帰着します。

2. 継承

継承と派生

3 つの継承方法: パブリック、プライベート、プロテクト。 

型互換性ルール:基本クラス オブジェクトが必要な場合はどこでも、パブリック派生クラスのオブジェクトで置き換えることができます。(パブリック派生クラスは、コンストラクターとデストラクターを除く基本クラスのすべてのメンバーを取得し、基本クラスのすべての機能を備えています。基本クラスが解決できるものはすべて、パブリック派生クラスも解決できます。)

  1. 派生クラス オブジェクトは暗黙的に基本クラス オブジェクトに変換できます。

  2. 派生クラス オブジェクトは、基本クラスへの参照を初期化できます。

  3. 派生クラスへのポインターは、基本クラスへのポインターに暗黙的に変換できます。

注:置換後、派生クラス オブジェクトは、基本クラスから継承されたメンバーのみを使用でき、基本クラスの役割のみを果たします。P264

スコープ識別子を使用して、派生クラス内の継承されたメンバーを一意に識別し、アクセス目的を達成し、非表示メンバーの問題を解決することもできます。

class Derived: public Base1, public Base2 {} // 
//基本クラスと派生クラスの両方に fun(); 
Derived d; 
d.fun(); // Derived の fun() にアクセス
d.Base1:: fun (); //Base1 の fun() にアクセス
d.Base2::fun(); //Base2 の fun() にアクセス
仮想基本クラス:

共通基底クラスを仮想基底クラスに設定します。このとき、異なるパスから継承した同名のデータメンバはメモリ上にコピーを 1 つだけ保持し、同じ関数のマッピングは 1 つだけ保持します

class Base0 {int var0}; 
class Base1: virtual public Baes0 {int var1} 
class Base2: virtual public Baes0 {int var2} 
class Derived: public Base1, public Base2 {int var}; 
Derived d; 
d.var0 = 2; / / 仮想基本クラスのデータ メンバーに直接アクセスします。

合成と継承の違いは何ですか? ? ?

組み合わせ:全体と部分の関係。たとえば、自動車は(全体として)車輪やエンジンなどの機能(の一部)で構成されており、移動することができます。

継承:特殊-一般関係。たとえば、車は走って人を運ぶことができますが、車の一般的な機能を備え、荷物を牽引することもできるトラックとして派生できます。

クラスの継承、つまり、さまざまなキーワードによって変更された基本クラスのメソッドに対する派生クラスのアクセス権について話しましょう。

C++ は、public、protected、private の 3 つのキーワードによってメンバー変数とメンバー関数のアクセス権を制御します。これらはそれぞれ public、protected、private を表し、メンバー アクセス修飾子と呼ばれます。

キーワード 権限
公共 あらゆるエンティティがアクセス可能
保護された サブクラスとこのクラスのメンバー関数へのアクセスのみを許可します
プライベート このクラスのメンバー関数のみがアクセスを許可されます

クラスのメンバーは、パブリック メンバー、保護されたメンバー、パブリック メンバーの 3 つのタイプに分類できます。クラスは、自身のクラスのパブリック、プロテクト、およびプライベート メンバーに直接アクセスできますが、クラス オブジェクトは自身のクラスのパブリック メンバーにのみアクセスできます。

  1. パブリック継承:派生クラスは、基本クラスのパブリック メンバーとプロテクト メンバーにアクセスできますが、基本クラスのプライベート メンバーにはアクセスできません。派生クラス オブジェクトは、基本クラスのパブリック メンバーにアクセスできますが、プロテクトメンバーとプライベート メンバーにはアクセスできません。基本クラスのメンバー。

  2. 保護された継承: 派生クラスは、基本クラスのパブリック メンバーと保護されたメンバーにアクセスできますが、基本クラスのプライベート メンバーにはアクセスできません。派生クラス オブジェクトは、基本クラスのパブリック メンバー、保護されたメンバー、およびプライベート メンバーにアクセスできません。

  3. プライベート継承: 派生クラスは、基本クラスのパブリック メンバーとプロテクト メンバーにアクセスできますが、基本クラスのプライベート メンバーにはアクセスできません。派生クラス オブジェクトは、基本クラスのパブリック メンバー、プロテクト メンバー、およびプライベート メンバーにアクセスできません。

プライベート、保護、パブリックでの使用とその違い

パブリック アクセス権: クラスのパブリック メンバー変数およびメンバー関数には、クラスのメンバー関数およびインスタンス変数を通じてアクセスできます。

保護されたアクセス権: クラスの保護されたメンバー変数およびメンバー関数には、クラスのインスタンス変数を介してアクセスできません。ただし、クラスのフレンド機能およびフレンドクラスを通じてアクセスできます。

プライベート アクセス権: クラスのプライベート メンバー変数およびメンバー関数には、クラスのインスタンス変数を介してアクセスできません。ただし、クラスのフレンド機能およびフレンドクラスを通じてアクセスできます。

パブリック継承により、すべての基本クラス メンバー (プライベートを除く)、パブリック、および保護されたクラスが派生クラスに含まれます。パブリック フィルターは比較的大きいため、アクセス権は変更されません。
保護された継承
により 
プライベート継承
を通じて

プライベート継承は一度終了しますが、保護されたものは永久に継承できます。

継承関係では、サブクラス オブジェクトと親クラス オブジェクトは相互に値を割り当てることができますか?

(1) サブクラスオブジェクトには、親クラスから継承した変数だけでなく、独自の変数も含まれており、サブクラスオブジェクトを親クラスオブジェクトに代入する際には、両者に共通する部分が代入されます。

(2) 逆に、親クラスのオブジェクトをサブクラスのオブジェクトに代入する場合、親クラスのオブジェクトはサブクラスのオブジェクト固有の変数を提供できないため、エラーが報告されます。

(3) オブジェクトポインタにも同様の使用規則があり、サブクラスポインタを親クラスポインタに直接代入することができますが、親クラスポインタをサブクラスに代入する際には表示型変換が必要です。

例: 基本クラスの親を定義すると、サブクラスの子は親を継承し、次の操作を実行します。

親 = 子; 子 = 親; 子は元の子と同じですか?

Child = Parent; //エラーが報告されます

リヒター置換原理:

  1. サブクラスは親クラスの抽象メソッドを実装できますが、親クラスの非抽象メソッドをオーバーライドすることはできません

  2. サブクラスは独自の機能を追加できます。

  3. クラス メソッドが親クラス メソッドをオーバーライドする場合、メソッドの前提条件 (仮パラメータ) は親クラス メソッドの入力パラメータよりも緩くなります。

  4. 親クラスのメソッドをオーバーライドまたは実装する場合、出力 (戻り値) を減らすことができます。

 多重継承にはどのような問題があるのでしょうか?

  1. プログラムの複雑さが増すため、プログラムの作成と保守が困難になり、エラーが発生しやすくなります。

  2. 継承中に、基本クラス間、または基本クラスと派生クラス間でメンバーが同じ名前を持つ場合、メンバーのアクセスに不確実性が生じます。つまり、同じ名前が曖昧になります

    解決策: (1) スコープ識別子を使用します::。これは、アクセス目的を達成するために、派生クラスで継承されたメンバーを一意に識別します。

    (2) 派生クラスで同じ名前のメンバーを定義し、基本クラスの関連メンバーをオーバーライドします。

  3. 派生クラスが複数の基本クラスから派生し、これらの基本クラスが同じ基本クラスから派生している場合、この共通の基本クラスのメンバーにアクセスするときに別の種類の不確実性、つまりパスの曖昧さが発生します

    解決策: 仮想継承 (仮想基本クラス) を使用して、異なるパスから継承された同じ名前のメンバーがメモリ内にコピーを 1 つだけ持つようにします。

仮想基底クラス:継承されたクラスの前に virtual キーワードを追加して、共通の基底クラスを仮想基底クラスとして設定します。このとき、異なるパスから継承された同じ名前のデータ メンバーのコピーはメモリ内に 1 つだけあり、同じ関数の 1 つのコピーにすぎません。仮想基本クラスをインスタンス化できます。

補足:同名の曖昧さを解決することでも解決できますが、同名のデータメンバのコピーが複数存在します。

仮想継承とは何なのか、それによってどのような問題が解決されるのか、そしてその実装方法について話しましょう。

仮想継承は、C++ の多重継承の問題を解決する手段であり、さまざまな方法で継承された同じ基本クラスがサブクラスに複数のコピーを持つことになります。これには 2 つの問題があります: 1 つ目は、記憶域を無駄にすることです。2 つ目は、あいまいさの問題があります。通常、派生クラス オブジェクトのアドレスは、基底クラス オブジェクトに割り当てることができます。これを実現する具体的な方法は、基底クラス オブジェクトをポイントすることです。継承されたクラスへのクラス ポインター (継承されたクラスには基本クラスのコピーがあります) しかし、多重継承では基本クラスのコピーが複数存在する可能性があるため、あいまいさが生じます。仮想継承は、多重継承における前述の 2 つの問題を解決できます。

#include<iostream>
名前空間 std を使用; 
class A{ 
public: 
    int _a; 
}; 
class B :virtual public A 
{ 
public: 
    int _b; 
}; 
class C :virtual public A 
{ 
public: 
    int _c; 
}; 
class D : public B, public C 
{ 
public: 
    int _d; 
}; 
//ダイヤモンド継承とダイヤモンド仮想継承のオブジェクトモデル
int main() 
{ 
    D d; 
    dB::_a = 1; 
    dC::_a = 2; 
    d._b = 3; 
    d._c = 4; 
    d._d = 5; 
    cout << sizeof(D) << endl; 
    return 0; 
}



ダイヤモンド継承と仮想継承からそれぞれ分析します。

ダイヤモンド相続では、A は B、C、D の間でコピーを持ちます。仮想相続では、A が共有します。

上記の仮想継承テーブルは、実際にはポインターの配列です。B と C は実際には仮想実表ポインタであり、仮想実表を指します。

仮想ベース テーブル: 相対オフセットを保存し、仮想ベース クラスを検索するために使用されます。

C++ におけるダイヤモンド継承問題とは何か、そしてその解決方法について話しましょう

参考回答

  1. 以下の図は、ダイヤモンドの相続問題を説明するために使用できます。

  • クラス B とクラス C があり、どちらも同じクラス A を継承しているとします。さらに、多重継承メカニズムを通じてクラス B と C を継承するクラス D があります。上のグラフの形状がひし形に似ているため、この問題はひし形継承問題と呼ばれています。次に、上の図を具体的なコードに変換します。

    /* *Animal クラスはチャート class A* に対応します */ class Animal { /* ... */ }; // 基本クラス { intweight; public: int getWeight() { returnweight; } }; class Tiger : public Animal { /* ... */ }; class Lion : public Animal { /* ... */ } class Liger : public Tiger, public Lion { /* ... */ }
    
    
    

    上記のコードでは、ダイヤモンドの継承問題の具体的な例を示しています。Animalクラスは最上位クラス(図のA)に相当し、TigerとLionはそれぞれ図のBとCに相当し、Ligerクラス(トラとライオンの交配種ライガー)はDに相当します。

    さて、このような相続構造になった場合にどのような問題が生じるのかということです。

    以下のコードを見て、質問に答えてください。

      int main( ) { Liger lg; /*コンパイル エラー。次のコードはどの C++ コンパイラにも渡されません*/ intweight = lg.getWeight(); }
    
    
    
  • 継承構造では、Tiger クラスと Lion クラスの両方が Animal 基本クラスから継承していることがわかります。そこで質問は、Liger は Tiger クラスと Lion クラスを多重継承するため、Liger クラスには Animal クラスの 2 つのメンバー (データとメソッド) があり、Liger オブジェクト "lg" には Animal 基本クラスの 2 つのサブオブジェクトが含まれることになります。 。

    それで、あなたは、Liger オブジェクトが Animal 基本クラスの 2 つのサブオブジェクトを持つことの何が問題なのかと尋ねます。上記のコードをもう一度見てください。「lg.getWeight()」を呼び出すとコンパイル エラーが発生します。これは、コンパイラが Tiger クラスの getWeight() を呼び出すべきか、Lion クラスの getWeight() を呼び出すべきかわからないためです。したがって、getWeight メソッドの呼び出しは曖昧であり、コンパイルに失敗します。

  1. ダイヤモンドの相続問題について説明しましたが、今度はダイヤモンドの相続問題の解決策を示したいと思います。Animal クラスを継承するときに、Lion クラスと Tiger クラスがそれぞれ virtual でマークされている場合、C++ は、Liger オブジェクトごとに、Animal クラスのサブオブジェクトが 1 つだけ作成されるようにします。次のコードを見てください。

    class Tiger : 仮想パブリック Animal { /* ... */ }; クラス ライオン : 仮想公開動物 { /* ... */ };
    
    
    
  • 唯一の変更は、Tiger クラスと Lion クラスの宣言に「virtual」キーワードを追加したことであることがわかります。これで、Liger のようなオブジェクトには Animal サブオブジェクトが 1 つだけ含まれるようになり、次のコードは正常にコンパイルされます。

    int main( ) { Liger lg; /*Tiger クラスと Lion クラスの定義で "virtual" キーワードを宣言しているため、次のコードはコンパイルされます。 */ intweight = lg.getWeight(); }
    

3. ポリモーフィズム

RTTIとは何ですか?

RTTI はRuntime Type Identificationです。このメカニズムは C++ で導入され、プログラムが実行時に基本クラスのポインターまたは参照に基づいて、ポインターまたは参照が指すオブジェクトの実際の型を取得できるようにします。

RTTI はランタイム型識別であり、その機能は 2 つの演算子によって実装されます。

  1. typeid式の型を返すために使用される演算子は、基本クラスのポインターを通じて派生クラスのデータ型を取得できます。

  2. dynamic_cast型チェック機能を備えた演算子は、基本クラスのポインターまたは参照を派生クラスのポインターまたは参照に安全に変換するために使用されます。

RTTI は、仮想関数を含むクラスにのみ適用されますこの種のクラス階層の場合にのみ、派生クラスのアドレスを基本クラス ポインターに割り当てる必要があるためです。

C++ におけるポリモーフィズムについて簡単に説明します。

狭義には、ポリモーフィズムは静的ポリモーフィズム動的ポリモーフィズムに分けられます。

  1. 静的多態性: コンパイル中にコンパイラによって完了されます。コンパイラは、実際のパラメータの型に基づいてどの関数を呼び出すかを推測します。対応する関数が存在する場合は、その関数が呼び出されます。存在しない場合は、コンパイル中にエラーが報告されます。

    たとえば、単純な加算関数は次のようになります。

    include<iostream>
    名前空間 std を使用します。
    
    int Add(int a,int b)//1 
    { 
        return a+b; 
    } 
    
    char Add(char a,char b)//2 
    { 
        return a+b; 
    } 
    
    int main() 
    { 
        cout<<Add(666,888)<<endl;//1 
        cout<<Add('1','2');//2 
        return 0; 
    }
    
    
    
    

    明らかに、最初のステートメントは関数 1 を呼び出し、2 番目のステートメントは関数 2 を呼び出します。これは関数の宣言順序によるものではありません。信じられない場合は、順序を変更してみてください。

  2. 動的多態性: 実際、動的多態性を実現するには、いくつかの条件、つまり動的バインディング条件が必要です。

    1. 仮想関数。基本クラスには仮想関数が存在する必要があり、仮想関数は派生クラスでオーバーライドされる必要があります。

    2. 仮想関数は、基本クラス型のポインターまたは参照を通じて呼び出されます。

    これについて言えば、書き換えという概念を導入する必要があります。つまり、基底クラスに仮想関数があり、同じプロトタイプ (戻り値、名前、パラメーター) を持つ仮想関数を派生クラスで書き換える必要があります。例外は共分散です。共分散はオーバーライドの特殊なケースです。基本クラスでは、戻り値は基本クラス型の参照またはポインターであり、派生クラスでは、戻り値は派生クラス型の参照またはポインターです。

    //协变测试関数
    #include<iostream> 
    using namespace std; 
    
    class Base 
    { 
    public: 
        virtual Base* FunTest() 
        { 
            cout << "victory" << endl; 
            これを返します。
        } 
    }; 
    
    class Derived :public Base 
    { 
    public: 
        virtual Derived* FunTest() 
        { 
            cout << "yeah" << endl; 
            これを返します。
        } 
    }; 
    
    int main() 
    {
        ベース b; 
        導出された d; 
    
        b.FunTest(); 
        d.FunTest(); 
    
        0を返します。
    }
    

大まかに言うと、ポリモーフィズムとは、複数の種類のオブジェクトを処理するプログラムの機能を指します。

これは、強制多態性、オーバーロード多態性、包含多態性、パラメータ多態性の 4 つの形式で実装できます。

  1. 強制ポリモーフィズム:データ型変換 (暗黙的または明示的) を通じて、ある型のデータを別の型のデータに変換することによって実現されます。

  2. 多重定義のポリモーフィズム: 関数の多重定義、演算子の多重定義。

  3. 包含ポリモーフィズム:仮想関数を使用して包含ポリモーフィズムを実装します。リライト!

  4. パラメトリック多態性:テンプレートを使用して実装され、関数テンプレートとクラス テンプレートに分かれています。

    最初の 2 つは特殊な多型であり、表面的な多型 (静的多型) であり、後の 2 つは一般的な多型であり、実際の多型 (動的多型) です。

  5. 動的多態性の必要条件:

    1. 継承が必要です。

    2. 仮想機能のカバレッジが必要です。

    3. サブクラス オブジェクトを指すには、基本クラスのポインター/参照が必要です。

C++ におけるオーバーロードと書き換え、およびそれらの違いについて簡単に説明します。

参考回答

  1. オーバーライド (動的ポリモーフィズム)

    派生クラスに再定義された関数が存在することを指しますその関数名、パラメーター リスト、および戻り値の型はすべて、基本クラスでオーバーライドされた関数と一致している必要があります。関数本体(中括弧内)のみが異なり、派生クラス オブジェクトがそれを呼び出すと、派生クラスのオーバーライドされた関数が呼び出され、オーバーライドされた関数は呼び出されません。オーバーライドされた基本クラス内のオーバーライドされた関数には、仮想変更が必要です。

    例は次のとおりです。

#include<bits/stdc++.h> 名前空間 std を使用; class A { public: virtual void fun() { cout << "A"; } }; class B :public A { public: virtual void fun() { cout < < "B"; } }; int main(void) { A* a = new B(); a->fun();//出力 B、クラス A の fun はクラス B で書き換えられます }
  1. オーバーロード (静的ポリモーフィズム)

    C++ では、1 つの関数名を使用して複数の関数を定義すること、いわゆる関数のオーバーロードが提案されています。関数のオーバーロードとは、同じアクセス可能な領域で宣言された、異なるパラメーター リスト(異なるパラメーターの型、数値、順序) を持つ同じ名前の複数の関数を指します。どの関数を呼び出すかはパラメーター リストに基づいて決定されます。オーバーロードでは関数は考慮されません。戻り値の型。

#include<bits/stdc++.h> 名前空間 std を使用します。クラス A { void fun() {}; void fun(int i) {}; void fun(int i, int j) {}; void fun1(int i,int j){}; };

基盤となる実装の書き換え (遅延バインディング、動的ポリモーフィズム) (Baidu)

仮想関数のアドレスは実行時にバインドされますが、通常の関数のアドレスはコンパイル時に決定され、オブジェクト呼び出しのアドレスはすでに決定されています。

このダイナミックさは、ポインターを介して段階的に実際の関数を見つけることに反映されます。クラスのメンバー関数が仮想関数として宣言されると、仮想関数ポインター(4/8 バイト)がクラスのオブジェクトに追加され、仮想関数テーブルへ (各オブジェクトには独自の仮想関数ポインターがあり、仮想関数テーブルは 1 つだけあります。すべての仮想関数ポインターは同じ仮想関数テーブルを指します。) 仮想関数テーブルには、仮想関数のアドレスが格納されます。サブクラスは、この仮想関数テーブルを継承します。親クラスの仮想関数を書き換えた後、サブクラスの仮想関数テーブル内の仮想関数エントリ アドレスは、サブクラス内の仮想関数エントリ アドレスに変更されます(書き換えずに変更されず、依然として を指します)。親クラスの仮想関数) 今度は親 クラスのポインタはサブクラスのオブジェクトを指します (参照も可能であり、参照は本質的にポインタ定数です)。親クラス ポインタ、サブクラスのオブジェクトはポインタ基づいて見つけることができ仮想関数ポインタは仮想関数テーブルと仮想関数テーブル内の仮想関数を見つけます

静的/非静的関数はオブジェクトのメモリを占有しません。非静的メンバー変数のみがオブジェクトのメモリを占有します。クラス内に仮想関数がある場合、クラス内にさらに 4/8 バイトがあることがわかります。この 4/ 8 バイトは、仮想関数テーブルを指す仮想関数ポインター (vptr) です。

仮想関数は宣言順に仮想関数テーブルに格納されます。
親クラスの仮想関数はサブクラスの仮想関数の前に格納されます。
多重継承では、各親クラスに独自の仮想関数テーブルがあります
。サブクラスのメンバー関数は、最初の親クラスの仮想関数テーブルに格納されます。

仮想関数テーブルに格納されている内容はいつ書き込まれたものですか? (百度)

参考回答

  1. 仮想関数テーブルは、NULL で終わる仮想関数アドレスを格納する配列です。仮想テーブル(vftable) はコンパイル段階で生成され、オブジェクトのメモリ空間が開かれた後、オブジェクト内の vfptr が書き込まれ、コンストラクターが呼び出されます。つまり、仮想テーブルはコンストラクターの前に書き込まれます。

  2. コンストラクターの前に書き込むことに加えて、仮想テーブルの二次的な書き込みメカニズムも考慮する必要があります。このメカニズムを通じて、各オブジェクトの仮想テーブル ポインターは、自身のクラスの仮想テーブルを正確に指すことができます。ポリモーフィズムがサポートされています。プログラムの実行中に、適切なメンバー関数が選択されます。

オーバーロード (早期バインディング、静的ポリモーフィズム) の基礎となる実装 (Baidu)

C++ は、名前マングリングテクノロジを使用して関数名を変更し、パラメータが異なる同じ名前の関数を区別します。名前のワーピングはコンパイル段階で行われます。

C++ では、同じ名前のオーバーロードされた関数が定義されています。

#include<iostream> 名前空間 std を使用します。int func(int a,double b) { return ((a)+(b)); int func(double a,float b) { return ((a)+(b)); int func(float a,int b) { return ((a)+(b)); int main() { 0 を返します。}

上図から、d は double、f は float、i は int を表していることがわかります。パラメータの最初の文字は、同じ名前の関数を区別するために追加されています。

C言語がC++言語でオーバーロードを実装する方法について話しましょう

参考回答

C 言語では、同じ名前の関数は許可されません。これは、オーバーロードを実現するためにパラメーターの型と戻り値の型を関数のコンパイル名として追加する C++ とは異なり、コンパイル時に関数名が同じになるためです。C 言語で関数のオーバーロードを表示したい場合は、次の方法で実行できます。

  1. 関数ポインターを使用して実装されます。オーバーロードされた関数は同じ名前を使用できませんが、関数のオーバーロード関数は同様に実装されます。

  2. オーバーロードされた関数は、ファイルを開く関数など、可変パラメーターを使用します。

  3. gcc には組み込み関数があり、プログラムはコンパイルされた関数を使用して関数のオーバーロードを実装できます。

例は次のとおりです。

#include<stdio.h> void func_int(void * a) { printf("%d\n",*(int*)a); //出力は int 型、void * は int に変換されることに注意してください } void func_double( void * b) { printf("%.2f\n",*(double*)b); } typedef void (*ptr)(void *); //typedef は関数ポインタを宣言します void c_func(ptr p,void * param) { p(param); //対応する関数を呼び出す} int main() { int a = 23; double b = 23.23; c_func(func_int,&a); c_func(func_double,&b); return 0; }

仮想関数を理解する

仮想関数は動的バインディングの基礎であり、仮想関数は非静的メンバー関数である必要があります仮想関数が導出された後、クラス ファミリ内で動的なポリモーフィズムを実現できます。

関数の書き換え(上書き):基底クラスで仮想関数を宣言し、派生クラスで同じ名前、同じパラメータ、同じ戻り値の関数を宣言して仮想関数を書き換えてポリモーフィズムを実現します。

動的バインディングは、基本クラスへのポインターまたは参照を通じて仮想関数が呼び出された場合にのみ発生します。これは基本クラスのポインタは派生クラスのオブジェクトを指すことができ、基本クラスの参照は派生クラスのエイリアスとして使用できますが、基本クラスのオブジェクトは派生クラスのオブジェクトを表すことができないためです。派生クラス

class Base1{ 
  virtual display(); //仮想関数
} 
class Base2: public Base1 { 
  virtual display(); // 基本クラスの仮想関数をオーバーライド
} 
class Derived: public Base2{ 
  virtual display(); // 基本クラスの仮想関数をオーバーライド
} 
void fun (Base1* ptr) {ptr->display();} //パラメータは基本クラスのポインタです
fun(&base1); //base1 は Base1 のオブジェクト
fun(&base2); //base2 はオブジェクトです
fun( of Base2 &derived); //derived は Derived から派生したオブジェクトです
結果: 
Base1::display() 
Base2::display() 
Derived::display()

仮想テーブル:多態性クラスには仮想テーブルがあります。仮想テーブルには、現在のクラスの各仮想関数のエントリ アドレスが含まれます。各オブジェクトには、現在のクラスの仮想テーブルを指すポインタ (仮想ポインタ vptr)があります。の内容仮想テーブルはコンパイラによって配置されます。(利点: スペースの節約。各オブジェクトは仮想ポインターを保存するだけで済みます。(仮想テーブルの最初のアドレスへのポインター))vptr各オブジェクトのストレージ アドレスは異なりますが、すべて同じ仮想関数テーブルを指します。

動的バインディング P341 の実装:まず、コンストラクターはオブジェクトの仮想ポインターに値を割り当てます。次に、多態性型のポインタまたは参照を通じてメンバ関数を呼び出すと、仮想テーブル内で呼び出される仮想関数のエントリ アドレスが仮想ポインタを通じて見つかります。最後に、エントリ アドレスを通じて仮想関数が呼び出されます。

補足: 非静的メンバー関数およびデストラクターは仮想関数にすることができますが、仮想コンストラクターは宣言できません。

仮想関数ポインタはクラス内のどこに配置されますか? 64 ビット マシンでは、クラスに 10 個の仮想関数があります。sizeof(CLASS A) のサイズはどれくらいですか?

仮想ポインタはオブジェクト ヘッダー内に存在し、サイズは 8 バイト (64 ビット) です。仮想関数をサポートするために、コンパイラには追加の負荷がかかり、仮想テーブルを指す仮想ポインタ (vptr) のサイズは、64 ビット マシンでは 8 バイト、32 ビット マシンでは 4 バイトになります。クラスに仮想関数がいくつあっても、各クラス オブジェクトにはポインターが 1 つだけあります。

sizeof (A のオブジェクト) =非静的データ メンバーのサイズ + 仮想ポインターのサイズ (8 バイト) + データ配置のための追加のスペース; 通常、宣言されるクラスは単なる型定義であり、それ自体のサイズはありません。 sizeof 演算子は、このタイプのオブジェクトのサイズです。

仮想関数テーブルはクラス オブジェクトのメモリ空間を占有しません。

クラス A が空のクラスの場合、sizeof(A) の値は何ですか?

sizeof(A)この空のクラスのさまざまなインスタンスが一意のアドレスを持つように、コンパイラはこの空のクラスのさまざまなインスタンスを区別し、バイトを割り当てる必要があるため、値は 1 です。

コンストラクターとデストラクターを仮想関数にすることはできますか?

コンストラクターを仮想関数として定義することはできません。

話がまとまらない:

  1. ストレージスペースの観点から見ると、仮想関数は vtale に対応しこのテーブルのアドレスはオブジェクトのメモリスペースに格納されます。コンストラクターが仮想関数として設定されている場合、vtable で呼び出す必要がありますが、オブジェクトはインスタンス化されておらず、割り当てられたメモリ領域もありません(逆説)

  2. 使用の観点から: 仮想関数の役割は、親クラスのポインターまたは参照を通じて呼び出されたときに、サブクラスを呼び出すメンバー関数になることができることですコンストラクタはオブジェクト作成時に自動的に呼び出され、親クラスからのポインタや参照では呼び出すことができないため、仮想関数にはできないと規定されています。

  3. 実装の観点から見ると、vbtl はコンストラクターが呼び出された後に作成されるため、コンストラクターを仮想関数にすることはできません。

仮想関数の呼び出しは仮想関数テーブルに依存しており、vptrコンストラクタ内で仮想ポインタを初期化する必要があるため、仮想関数として定義されたコンストラクタを呼び出すことはできません。

基本クラスのデストラクターを仮想関数として定義する必要があるのはなぜですか?

仮想デストラクター: 仮想関数として継承できる親クラスのデストラクターを設定すると、新しいサブクラスを作成し、基本クラス ポインターを使用してサブクラス オブジェクトを指すときに、基本クラスが作成されたときに子を解放できるようになります。ポインタが解放される メモリリークを防ぐためのクラス空間。基本クラスのデストラクターが仮想関数ではない場合、特定の状況下では派生クラスを破棄できません。

C++ のデフォルトのデストラクターは仮想関数ではありません。これは、仮想関数には追加の仮想関数テーブルと仮想テーブル ポインターが必要であり、追加のメモリを占有するためです。継承されないクラスの場合、そのデストラクターが仮想関数である場合、メモリが無駄になります。したがって、C++ のデフォルトのデストラクターは仮想関数ではありませんが、親クラスとして使用する必要がある場合にのみ仮想関数に設定されます。

  1. 派生クラスの型ポインターを使用して派生クラスのインスタンスをバインドします。破棄する場合は、基底クラスのデストラクターが仮想関数であるかどうかに関係なく、通常どおり破棄されます。

  2. 基底クラスの型ポインターを使用して、派生クラスのインスタンスをバインドします。破棄中に、基底クラスのデストラクターが仮想関数でない場合、基底クラスのみが破棄され、派生クラスのオブジェクトは破棄されないため、メモリ リークが発生します。なぜこのような現象が起こるのでしょうか?個人的には、破棄時に仮想関数の動的バインディング機能がない場合、コンパイラはポインタにバインドされているオブジェクトではなく、ポインタの型のみに基づいて静的バインディングを実装するのではないかと考えてます基本クラスのデストラクターが呼び出されます基本クラスのデストラクターが仮想関数の場合、破棄中にポインターによってバインドされたオブジェクトに従って、対応するデストラクターを呼び出す必要があります。

  3. (2の説明)基底クラスのポインタ pを定義します。 p を削除する際、基底クラスのデストラクタが仮想関数の場合、 pで代入されたオブジェクトのみが参照されます。派生クラスの場合、派生クラスのデストラクターが呼び出されます(基本クラスのコンストラクターが最初に呼び出され、次に派生クラスのコンストラクターが呼び出され、次に派生クラスのデストラクターが呼び出されることは間違いありません)。関数、いわゆる最初に構築されてから解放されます); p によって割り当てられたオブジェクトが基底クラスのオブジェクトである場合、基底クラスのデストラクターが呼び出されます。メモリリークが発生しないことを確認します。

    基本クラスのデストラクターが仮想関数ではない場合、p を削除するとき、デストラクターを呼び出すときに、ポインターのデータ型のみが参照され、割り当てられたオブジェクトは参照されないため、メモリ リークが発生します。

コンストラクター内で仮想メソッドを呼び出すことはできますか?

いいえ、ポリモーフィック vptr ポインタは分散的に初期化されるためです。サブクラスが初期化されるとき、親クラスのコンストラクタが最初に呼び出されます。このとき、サブクラスの親クラスの Vptr ポインタは親クラスを指すため、それ以上の状態はありません。

仮想関数と純粋仮想関数、および実装原理について簡単に説明する

  1. C++ の仮想関数の主な機能は、ポリモーフィズム メカニズムを実装することです。ポリモーフィズムについては、簡単に言うと、親クラスのポインタを使用してそのサブクラスのインスタンスを指し、親クラスのポインタを介して実際のサブクラスのメンバ関数を呼び出すことです。このテクノロジにより、親クラス ポインタが「複数の形状」を持つことが可能になります。これは汎用テクノロジです。非仮想関数が呼び出された場合、実際のオブジェクトの型に関係なく、基本クラスの型によって定義された関数が実行されます。非仮想関数は、関数が呼び出されるオブジェクト、参照、またはポインターの型に基づいてコンパイル時に常に決定されます。仮想関数が呼び出された場合、その仮想関数が参照がバインドされている型またはポインターが指す型によって定義されたバージョンである場合、実行時までどの関数が呼び出されたのかはわかりません仮想関数は、基本クラスの非静的メンバー関数である必要があります。仮想関数の機能は、動的バインディングを実現すること、つまり、プログラムの実行フェーズ中に適切なメンバー関数を動的に選択することです。仮想関数が定義された後、仮想関数は基本クラスの派生クラスで再定義できます。派生クラス内 再定義された関数には、仮想関数と同じ数の仮パラメータとパラメータ型が必要です。統一されたインターフェースと異なる定義プロセスを実現します。仮想関数が派生クラスで再定義されていない場合、仮想関数はその基本クラスの仮想関数を継承します。

    class Person{ 
        public: 
            //仮想関数
            virtual void GetName(){ 
                cout<<"personName:xiaosi"<<endl; 
            }; 
    }; 
    class Student:public Person{ 
        public: 
            void GetName(){ 
                cout<<"StudentName: xiaosi"<<endl; 
            }; 
    }; 
    int main(){ 
        //ポインタ
        person *person = new Student(); //基本クラスは
        サブクラスの関数
        person->GetName()を呼び出します ; //StudentName:xiaosi 
    }
    
    
    
    

    仮想関数 (Virtual Function) は、仮想関数テーブル (Virtual Table) を通じて実装されます。V テーブルと呼ばれます。このテーブルでは主にクラスの仮想関数のアドレス テーブルが必要ですが、このテーブルは継承とカバレッジの問題を解決し、実際の関数を正確に反映していることを保証します。このように、仮想関数を持つクラスのインスタンスでは、このテーブルがこのインスタンスのメモリ上に確保されるため、親クラスのポインタを使ってサブクラスを操作する場合には、この仮想関数テーブルが非常に重要になります。これはマップのようなもので、呼び出す必要がある実際の関数を示します。

  2. 純粋仮想関数は、基本クラスで宣言された仮想関数です。基本クラスでは定義されていませんが、派生クラスで独自の実装メソッドを定義する必要があります。基本クラスに純粋仮想関数を実装する方法は、関数プロトタイプ virtualvoid GetName() =0 の後に「=0」を追加することです。多くの場合、基本クラス自体がオブジェクトを生成することは不合理です。たとえば、動物は基本クラスとしてトラやクジャクなどのサブクラスを派生できますが、動物自体が生成するオブジェクトには明らかに無理があります。上記の問題を解決するために、関数が純粋仮想関数として定義されている場合、コンパイラーは多態性を実現するために派生クラスで関数を書き直す必要があることを要求します純粋仮想関数も含むクラスは抽象クラスと呼ばれ、オブジェクトを生成できませんこれにより、上記の 2 つの問題が非常にうまく解決されます。関数を純粋仮想関数として定義すると、その関数は子孫の型がオーバーライドできるインターフェイスを提供しますが、このクラスの関数は決して呼び出されません。純粋仮想関数を宣言するクラスは抽象クラスです。したがって、ユーザーはクラスのインスタンスを作成できず、その派生クラスのインスタンスのみを作成できます。関数は継承クラスで再宣言する必要があります (=0 に従わないでください)。そうしないと派生クラスをインスタンス化できず、多くの場合、関数は抽象クラスで定義されません。純粋仮想関数を定義する目的は、派生クラスが関数のインターフェイスを継承するようにすることです。純粋仮想関数の意味は、すべてのクラス オブジェクト (主に派生クラス オブジェクト) が純粋仮想関数のアクションを実行できるが、クラスは純粋仮想関数の適切なデフォルト実装を提供できないことです。したがって、クラス内の純粋仮想関数の宣言は、サブクラスの設計を伝えます。

  3. または、「純粋な仮想関数の実装を提供する必要がありますが、それをどのように実装するかわかりません。」

//抽象类
class Person{ 
    public: 
        //纯虚関数数
        virtual void GetName()=0; 
}; 
クラス Student:public 人{ 
    public: 
        Student(){ 
        }; 
        void GetName(){ 
            cout<<"生徒名:xiaosi"<<endl; 
        }; 
}; 
int main(){
    学生学生; 
}


抽象クラスを理解するにはどうすればよいですか?

参考回答

  1. 抽象クラスの定義は次のとおりです。

    純粋仮想関数は、基本クラスで宣言された仮想関数です。基本クラスでは定義されていませんが、派生クラスが独自の実装メソッドを定義する必要があります。純粋な仮想関数を基底クラスに実装するには、関数プロトタイプの後に "=0" を追加します。仮想関数を持つクラスは抽象クラスと呼ばれます。

  2. 抽象クラスには次の特徴があります。

    1) 抽象クラスは他のクラスの基底クラスとしてのみ使用でき、抽象クラス オブジェクトを作成できません。

    2) 抽象クラスは、パラメータの型、関数の戻り値の型、または明示的な変換の型として使用できません

    3) 抽象クラスへのポインタと参照を定義でき、このポインタはその派生クラスを指すことができるため、ポリモーフィズムを実現できます。

純粋仮想関数のアプリケーション シナリオには主に次のものが含まれます。

  • 設計パターン: たとえば、テンプレート メソッド パターンでは、基本クラスがアルゴリズムのスケルトンを定義し、一部のステップをサブクラスに延期します。サブクラスで実装する必要があるこれらのステップは、純粋な仮想関数として宣言できます。

  • インターフェイス定義: 純粋な仮想関数のみを含む抽象クラスをインターフェイスとして作成できます。このインターフェイスを実装するすべてのクラスは、これらの関数の実装を提供する必要があります。

純粋仮想関数と抽象クラス

純粋仮想関数:基本クラスで宣言された仮想関数ですが、関数の実装部分は含まれていません。各派生クラスは実際のニーズに応じて定義する必要があります。

virtual void display() const = 0; //純粋な仮想関数は、一般的な仮想関数プロトタイプの後に「= 0」を追加することを意味します。

注:純粋仮想関数は、関数本体が空の仮想関数とは異なり、関数本体がまったくありません。前者が配置されているクラスは抽象クラスであり、直接インスタンス化できませんが、後者が配置されているクラスはインスタンス化できます。

純粋仮想関数を持つクラスは抽象クラスであり、クラス ファミリに統一された操作インターフェイスを提供します(パブリック インターフェイスを確立します) 。抽象クラスを確立する目的は、そのメンバー関数を使用してポリモーフィズムを利用することです。抽象クラスはインスタンス化できない、つまり抽象クラスのオブジェクトを定義することができないため、継承機構を通じて抽象クラスの非抽象派生クラスを生成し、インスタンス化するしかありません。

抽象クラスとインターフェースの違い

インターフェース:インターフェースは概念です。抽象クラスを使用してC++ で実装されます。

違い:

  1. 抽象クラスは物の抽象化、つまりクラスの抽象化ですがインターフェイスは動作の抽象化です簡単な例を挙げると、飛行機と鳥は種類が異なりますが、共通点は「空を飛べる」ということです。したがって、設計するときに、飛行機を飛行機のようなものとして、鳥を鳥のようなものとして設計することはできますが、飛行機能をクラスとして設計することはできないため、それは単なる動作上の機能であり、クラスの抽象的な記述ではありません。物事の。現時点では、飛行はメソッド fly() を含むFlyインターフェイスとして設計でき、Airplane と Bird はそれぞれのニーズに応じて Fly インターフェイスを実装します次に、戦闘機や民間航空機などのさまざまな種類の航空機については、Airplane を直接継承できますが、鳥についても同様で、さまざまな種類の鳥が Bird クラスを直接継承できます。ここから、継承は「ある」関係であるのに対し、インターフェース実装は「ある」関係であることがわかります。クラスが抽象クラスを継承する場合、サブクラスは抽象クラスの型である必要があり、インターフェイスの実装は、鳥が飛べるかどうか(または飛ぶ性質があるかどうかなど)に関係します。 , 飛べるかどうか その後、このインターフェースを実装できますが、飛べない場合は、このインターフェースを実装できません。

  2. デザインレベルが異なりますが、抽象クラスは多くのサブクラスの親クラスとして機能し、テンプレートデザインになります。インターフェイスは動作仕様ですたとえば、あるエレベーターには何らかの警報が設置されており、警報を更新する必要があると、すべての警報を更新する必要があります。つまり、抽象クラスの場合、新しいメソッドを追加する必要がある場合、特定の実装を抽象クラスに直接追加でき、サブクラスを変更する必要はありませんが、インターフェイスの場合、これは不可能です。が変更されると、このインターフェイスのすべての実装が影響を受けます。それに応じてクラスを変更する必要があります。

2つの使用タイミングの違い

1. 抽象クラス: デザイン パターンとテンプレート メソッドを使用すると、アルゴリズム構造を変更せずに、サブクラスでアルゴリズムの特定のステップの特定の実装を再定義できます。

2. インターフェース: 実装クラスにはさまざまな機能が必要ですが、さまざまな機能間に接続がない場合があります。

純粋仮想関数をインスタンス化できるかどうか、またその理由について話しましょう。派生クラスでそれを実装する必要がありますか?その理由は何ですか?

参考回答

  1. 純粋な仮想関数はインスタンス化できませんが、派生クラスを使用してインスタンス化できます。

  2. 仮想関数の原理はvtableを採用しています。クラスに純粋仮想関数が含まれている場合、その vtable は不完全でギャップがあります。

    つまり、「クラスの vftable テーブル内の純粋仮想関数の対応するエントリには、値 0 が割り当てられます。つまり、存在しない関数を指します。コンパイラは、関数を呼び出す可能性を絶対に許可しないため、関数が存在しないため、クラスはオブジェクトを生成できません。その派生クラスでは、この関数がオーバーライドされない限りオブジェクトを生成できません。

    したがって、純粋な仮想関数をインスタンス化することはできません。

  3. 純粋仮想関数は、基本クラスで宣言された仮想関数であり、ポリモーフィズムを実現するには、派生クラスが独自の実装メソッドを定義する必要があります。

  4. 純粋仮想関数は、インターフェイスを実装し、派生クラスの動作を標準化するために定義されます。つまり、このクラスを継承するプログラマがこの関数を実装する必要があることを標準化します。派生クラスはインターフェイスから関数を継承するだけです。純粋仮想関数の重要性は、すべてのクラス オブジェクト (主に派生クラス オブジェクト) が純粋仮想関数のアクションを実行できるが、基本クラスは純粋仮想関数の適切なデフォルト実装を提供できないことです。したがって、クラス内の純粋仮想関数の宣言は、サブクラスの設計者に、「純粋仮想関数の実装を提供する必要がありますが、それをどのように実装するかはわかりません。」と伝えます。

C++ における仮想関数と純粋仮想関数の違いについて説明します。

参考回答

  1. 仮想関数と純粋仮想関数は同じクラス内に定義できますが、純粋仮想関数を含むクラスを抽象クラスと呼びますが、仮想関数のみを含むクラスを抽象クラスとは呼びません。

  2. 仮想関数は直接使用することも、サブクラスによってオーバーロードしてポリモーフィック形式で呼び出すこともできます。純粋仮想関数は宣言されているものの、基本クラスで定義されていないため、使用する前にサブクラスに実装する必要があります。

  3. 仮想関数と純粋仮想関数の両方をサブクラスでオーバーロードし、多態的に呼び出すことができます。

  4. 仮想関数と純粋仮想関数は通常、抽象基本クラスに存在し、統合インターフェイスを提供するために継承されたサブクラスによってオーバーロードされます。

  5. 仮想関数の定義形式:virtual{}; 純粋仮想関数の定義形式:virtual{} = 0; 仮想関数および純粋仮想関数の定義に静的識別子を使用することはできません。理由は非常に簡単です。 static 関数はコンパイル時にコンパイルされませんが、仮想関数は動的にバインドされ、この 2 つによって変更される関数のライフサイクルも異なります。

回答分析

  1. 仮想関数の例を見てみましょう。

    class A { public:      
    virtual void foo()      
    { cout<<"A::foo() が呼び出されます"<<endl; } }; 
    class B:public A { 
    public:      
    void foo() { cout<<"B: :foo() は呼び出されます"<<endl; } }; 
    int main(void) {      
    A *a = new B();      
    a->foo(); // ここで、a は A へのポインタですが、呼び出された関数 (foo) は B!      
    return 0; }
    
    
    

    この例は、仮想関数の典型的な応用例であり、この例を通じて、仮想関数に関するいくつかの概念を理解できるかもしれません。これは、いわゆる「遅延バインディング」または「動的バインディング」に基づいており、クラス関数の呼び出しはコンパイル時ではなく、実行時に決定されます。コードを作成するときには、呼び出される関数が基本クラスの関数であるか派生クラスの関数であるかを判断できないため、この関数は「仮想」関数と呼ばれます。仮想関数は、ポインターまたは参照を使用した場合にのみ多態性効果を実現できます。

  2. 純粋仮想関数は、基本クラスで宣言された仮想関数です。基本クラスでは定義されていませんが、派生クラスで独自の実装メソッドを定義する必要があります。基本クラスに純粋仮想関数を実装する方法は、関数プロトタイプの後に「=0」を追加することです。

    仮想ボイド関数1()=0

    多態性機能の使用を容易にするために、多くの場合、基本クラスで仮想関数を定義する必要があります。

    多くの場合、基本クラス自体がオブジェクトを生成することは不合理です。たとえば、動物は基本クラスとしてトラやクジャクなどのサブクラスを派生できますが、動物自体が生成するオブジェクトには明らかに無理があります。

    上記の問題を解決するために、純粋仮想関数の概念が導入され、関数が純粋仮想関数 (メソッド: virtual ReturnType Function() = 0;) として定義されると、コンパイラーはそれを次のように書き直す必要があります。ポリモーフィズムを実現するための派生クラス。純粋仮想関数も含むクラスは抽象クラスと呼ばれ、オブジェクトを生成できません。これにより、上記の 2 つの問題が非常にうまく解決されます。純粋仮想関数を宣言するクラスは抽象クラスです。したがって、ユーザーはクラスのインスタンスを作成できず、その派生クラスのインスタンスのみを作成できます。

    純粋仮想関数の最も重要な特徴は、継承クラスで再宣言する必要があること (=0 に従わないでください。そうしないと、派生クラスがインスタンス化されません)。また、多くの場合、それらは抽象クラスでは定義されません。

    純粋仮想関数を定義する目的は、派生クラスが関数のインターフェイスを継承するようにすることです。

    純粋仮想関数の意味は、すべてのクラス オブジェクト (主に派生クラス オブジェクト) が純粋仮想関数のアクションを実行できるが、クラスは純粋仮想関数の適切なデフォルト実装を提供できないことです。したがって、クラス内の純粋仮想関数の宣言は、サブクラスの設計者に、「純粋仮想関数の実装を提供する必要がありますが、それをどのように実装するかはわかりません。」と伝えます。

C++ で仮想宣言できない関数はどれですか?

参考回答

virtual として宣言できない一般的な関数には、通常の関数 (非メンバー関数)、静的メンバー関数、インライン メンバー関数、コンストラクター、およびフレンド関数が含まれます。

  1. C++ はなぜ通常の関数を仮想関数としてサポートしないのですか?

通常の関数 (非メンバー関数) はオーバーロードのみ可能であり、オーバーライドはできません。仮想関数として宣言しても意味がないため、コンパイラはコンパイル時に関数をバインドします。

  1. C++ が仮想関数としてコンストラクターをサポートしないのはなぜですか?

    コンストラクターは新しいオブジェクトを作成するために使用され、仮想関数の動作はオブジェクトに基づいています (オブジェクトは詳細を完全に理解していなくても正しく処理できます)。コンストラクターが実行される時点では、オブジェクトはまだ形成されていません。コンストラクターは仮想関数として定義されています

  2. C++ がインライン メンバー関数を仮想関数としてサポートしないのはなぜですか?

    実際、これは非常に単純です。インライン関数は、コード内で直接展開して関数呼び出しのコストを削減します。仮想関数は、継承後にオブジェクトが独自のアクションを正確に実行できるようにします。これを統合することは不可能です。

    インライン関数はコンパイル時に展開されますが、仮想関数は実行時に動的にコンパイルされるため、この 2 つは矛盾しており、インライン関数を仮想関数として定義することはできません。

  3. C++ が静的メンバー関数を仮想関数としてサポートしないのはなぜですか?

    これも非常に単純です。静的メンバー関数にはクラスごとに 1 つのコードしかなく、すべてのオブジェクトがこのコードを共有します。動的バインディングは必要ありません。

    静的メンバー関数はオブジェクトではなくクラスに属しているため、このポインターがなければオブジェクトを識別できません。

  4. C++ はなぜフレンド関数を仮想関数としてサポートしないのですか?

    メンバー関数でない場合、仮想関数にすることはできません。

フレンド機能

クラスのフレンド関数はクラスの外部で定義されますが、クラスのすべてのプライベートおよび保護されたメンバーにアクセスできます。フレンド関数のプロトタイプはクラス定義に表示されますが、フレンド関数はメンバー関数ではありません。

友好的な関係

  1. 友人関係は推移的ではなく、C は B の友人、B は A の友人です。宣言がない場合、C と A には友人関係がありません。

  2. 友人関係は一方通行であり、B は A の友人ですが、A は B の友人ではありません。

  3. 友人関係は継承されません。

4. クラスとオブジェクト

C++ クラス オブジェクト、メモリ サイズは何に関係していますか? 空のクラスが占めるスペースはどれくらいですか?

通常、宣言されるクラスは単なる型定義であり、それ自体にはサイズがありません。sizeof 演算子を使用して、この型のオブジェクトのサイズを取得します。

  1. 非静的データ メンバー(char-1 バイト、int-4 バイトなど)。

  2. 仮想関数ポインタによって占有されるメモリ(仮想ポインタ、64 ビット 8 バイト、32 ビット 4 バイト)

  3. データ整列処理によって占有されるスペース(メンバー変数の中で最大の型サイズに従って計算) -スペース効率と時間効率をトレードします。

仮想関数をサポートするために、コンパイラには追加の負荷がかかり、仮想テーブルを指す仮想ポインタ (vptr) のサイズは、64 ビット マシンでは 8 バイト、32 ビット マシンでは 4 バイトになります。クラスに仮想関数がいくつあっても、クラス オブジェクトにはこのポインタが 1 つしかありません。

注: 基本クラスが空のクラスではない場合、派生クラスのサイズを基本クラスが占有するスペースに追加する必要があります。単一継承、多重継承の空クラス空間のサイズは1バイトですが、仮想継承では8バイトの仮想テーブル(仮想ポインタ)が必要となります。

メンバー関数、静的データ メンバー、コンストラクター、およびデストラクターは、オブジェクトのメモリ空間を占有しません。

回答:空の型オブジェクトには情報が含まれませんが、そのサイズは 0 ではありません。これは、この型のオブジェクトを宣言する場合、メモリ内の一定量の領域を占有する必要があり、占有しない場合は使用できないためです。C++ の空の型の各インスタンスは、1 バイトの領域を占有します。

補足:関数コードはオブジェクト空間の外に格納され、関数コードセグメントはパブリックです。つまり、同じクラスに 10 個のオブジェクトが定義されている場合、これらのオブジェクトのメンバー関数は 10 個の異なる関数コードセグメントではなく、同じ関数コードセグメントに対応します。関数コードのスニペット。

class Base{ 
private: 
     char c; 
     int a; 
public: 
     virtual void func1(); 
}; 
class
Child : public Base{ 
public: 
     virtual void func2(); 
private: 
     int b; 
     //double b; 
};
親クラスサイズ: char c (4、int データと整列)、int a (4)、仮想ポインター (8) -- サイズ 16 サブクラスの
サイズ: int b (4 + 4、仮想ポインター データと整列)、親クラスのサイズ ( 16 )--size 24 //データ アライメント
//サブクラス サイズ: double b(8)、親クラス size(16) --size 24 
//サブクラスにも独自の仮想関数がありますが、親クラスの仮想関数を継承します。テーブルポインタは、親クラスの仮想テーブルと仮想ポインタを継承することにも相当します。
// 親クラスから継承した仮想関数テーブルに、自身の仮想関数アドレスが追加されます。

いくつかの種類のコンストラクターとその機能について説明しましょう。

参考回答

C++ のコンストラクターは、デフォルト コンストラクター、初期化コンストラクター、コピー コンストラクター、および移動コンストラクターの 4 つのカテゴリに分類できます。

  1. デフォルトのコンストラクターと初期化コンストラクター。クラスのオブジェクトを定義する場合は、オブジェクトの初期化を完了します。

    class Student { public: //デフォルトのコンストラクター Student() { num=1001; age=18; } //初期化コンストラクター Student(int n,int a):num(n),age(a){} private: int num ; int age; }; int main() { //デフォルトのコンストラクター Student s1 でオブジェクト S1 を初期化します; //初期化コンストラクター Student s2(1002,18) でオブジェクト S2 を初期化します; return 0; }

    パラメーター化されたコンストラクターでは、コンパイラーはデフォルトのコンストラクターを提供しません。

  2. コピーコンストラクター

    #include "stdafx.h" 
    #include "iostream.h"   
    class Test {      
    int i;      
    int *p; public:      
    Test(int ai,int value) { i = ai; p = new int(value); } ~Test () { delete p; }      
    Test(const Test& t) { this->i = ti; this->p = new int(*tp); } }; //コピー コンストラクターは、このクラスのオブジェクトをコピーするために使用されます int 
    main (int argc, char* argv[]) { Test t1(1,2); Test t2(t1);// オブジェクト t1 を t2 にコピーします。コピーと代入の概念は異なることに注意してください return 0; }

    このクラスのオブジェクトのコピー 代入コンストラクターは、デフォルトで値のコピー (浅いコピー) を実装します。

  3. コンストラクターを移動します。他の型の変数をこのクラスのオブジェクトに暗黙的に変換するために使用されます。次の変換コンストラクターは、int 型 r を Student 型のオブジェクトに変換します。オブジェクトの年齢は r で、num は 1004 です。

    Student(int r) { int num=1004; 整数年齢 = r; }

コピー初期化とダイレクト初期化

  1. ClassTest ct1("ab"); このステートメントは直接初期化に属し、コンストラクターを直接呼び出します。

  2. ClassTest ct2 = "ab"; このステートメントはコピー初期化です。最初にコンストラクターを呼び出して一時オブジェクトを作成し、次にコピー コンストラクターを呼び出して、この一時オブジェクトをパラメーターとして受け取り、オブジェクト ct2 を構築します。

  3. ClassTest ct3 = ct1; このステートメントはコピー初期化です。ct1 はすでに存在するため、関連するコンストラクターを呼び出す必要はありませんが、コピー コンストラクターを直接呼び出してその値をオブジェクト ct3 にコピーします。

  4. ClassTest ct4 (ct1); ct1 はすでに存在しており、コピー コンストラクターが直接呼び出され、オブジェクト ct3 のコピー オブジェクト ct4 が生成されるため、このステートメントは直接初期化です。

直接初期化: パラメーターに従って対応するコンストラクターを直接呼び出します。

コピーの初期化: コンストラクターは一時オブジェクトを作成し、次にコピー コンストラクターを呼び出し、この一時オブジェクトをパラメーターとして受け取り、オブジェクトを構築します。

デストラクターのみを定義します。コンストラクターは自動的に生成されます。

参考回答

デストラクターのみが定義されており、コンパイラーはコピー コンストラクターとデフォルト コンストラクターを自動的に生成します。

デフォルトのコンストラクターと初期化コンストラクター。クラスのオブジェクトを定義する場合は、オブジェクトの初期化を完了します。

class Student { public: //デフォルトのコンストラクター Student() { num=1001; age=18; } //初期化コンストラクター Student(int n,int a):num(n),age(a){} private: int num ; int age; }; int main() { //デフォルトのコンストラクター Student s1 でオブジェクト S1 を初期化します; //初期化コンストラクター Student s2(1002,18) でオブジェクト S2 を初期化します; return 0; }

パラメーター化されたコンストラクターでは、コンパイラーはデフォルトのコンストラクターを提供しません。

コピーコンストラクター

#include "stdafx.h" 
#include "iostream.h" 
class
Test 
{ 
    int i; 
    int *p; 
public: 
    Test(int ai,int value) 
    { 
        i = ai; 
        p = new int(value); 
    } 
    ~ Test() 
    { 
        delete p; 
    } 
    Test(const Test& t) 
    { 
        this->i = ti; 
        this->p = new int(*tp); 
    } 
}; 
int
main(int argc, char* argv[]) 
{ 
    Test t1(1,2); 
    Test t2(t1);// オブジェクト t1 を t2 にコピーします。コピーと割り当ての概念は異なることに注意してください。
    
    0を返す; 
}

代入コンストラクターは、デフォルトで値のコピー (浅いコピー) を実装します。

回答分析

例は次のとおりです。

class HasPtr 
{ 
public: 
    HasPtr(const string& s = string()) :ps(新しい文字列), i(0) {} 
    ~HasPtr() { ps を削除します。プライベート
:
    文字列 * ps; 
    int i; 
};



クラスの外にそのような関数がある場合:

HasPtr f(HasPtr hp) 
{ 
    HasPtr ret = hp; 
    ///... 他の操作
    return ret; 
 
}



関数の実行後、hp と ret の ps メンバーを削除するために hp と ret のデストラクターが呼び出されます。ただし、ret と hp は同じオブジェクトを指しているため、オブジェクトの ps メンバーは 2 回削除されます。未定義のエラーなので、クラスでデストラクターを定義する場合は、独自のコピー コンストラクターとデフォルト コンストラクターを定義する必要があります。

クラスについて話しましょう。デフォルトでどの関数が生成されるか

参考回答

空のクラスを定義する

クラス空
{ 
};



以下の関数がデフォルトで生成されます

  1. 引数のないコンストラクター

    クラスのオブジェクトを定義する場合は、オブジェクトの初期化を完了します。

空() 
{ 
}



  1. コピーコンストラクター

    コピー コンストラクターは、このクラスのオブジェクトをコピーするために使用されます。

空(const 空&コピー) 
{ 
}



  1. 代入演算子

空&演算子 = (const 空&コピー) 
{ 
}



  1. デストラクター (非仮想)

~空() 
{ 
}


 コンストラクターの初期化、関数本体の初期化、メンバー リストの初期化の違い

C++ では、オブジェクトのメンバー変数は、コンストラクターの初期化、関数本体の初期化、メンバーの初期化リストなど、さまざまな方法で初期化できます。これらの初期化方法にはいくつかの違いがあります。以下にそれらの比較を示します。

  • コンストラクターの初期化: 初期化にはコンストラクター内の代入ステートメントを使用します。これは最も一般的な初期化メソッドで、オブジェクトの作成時にコンストラクターを呼び出し、関数本体内で初期化操作を実行します。この方法での初期化は、オブジェクトのコンストラクターの本体内で行われます。

  • class MyClass {
    public:
        MyClass() {
            x = 0; // 构造函数初始化
        }
    private:
        int x;
    };
    
  • 関数本体での初期化: コンストラクター内で、関数本体の代入ステートメントまたは初期化リストを使用して初期化できます。この方法での初期化はコンストラクター本体内で行われますが、コンストラクター本体内の他のステートメントの後で行われます。

  • class MyClass {
    public:
        MyClass() {
            // 构造函数体内初始化
            x = 0;
        }
    private:
        int x;
    };
    
  • メンバー初期化リスト: 初期化リストを使用して、コンストラクターの先頭でメンバー変数を初期化します。この方法による初期化は、コンストラクター本体に入る前に行われるため、不必要な変数の初期化やコピー構築を避けることができます。

  • class MyClass {
    public:
        // 成员初始化列表初始化
        MyClass() : x(0) {
        }
    private:
        int x;
    };
    

    メンバー初期化子リストは、余分な構築とコピー操作を減らし、パフォーマンスを向上させるため、一般に、より良い選択であると考えられています。const メンバー変数や参照メンバー変数など、場合によっては、メンバー初期化リストの使用が必要になります。

通常の関数とメンバー関数の違いは何ですか?

通常の関数はクラスの外部で定義されますが、メンバー関数はクラスの内部で定義されます。

通常の関数はクラスのプライベート メンバーと保護されたメンバーに直接アクセスできませんが、メンバー関数はプライベート メンバーと保護されたメンバーを含むクラスのすべてのメンバーにアクセスできます。

通常の関数は直接呼び出すことができますが、メンバー関数はクラスのオブジェクトを通じて呼び出す必要があります。

メンバー関数には特別なポインター this があり、メンバー関数を呼び出すオブジェクトを指します。通常の関数にはこのポインタがありません。

このポインタは何のためにあるのでしょうか? (テンセント)

this ポインタは、現在のオブジェクトを指すアドレスです。これは主に、クラスのメンバー関数内の現在のオブジェクトのメンバー変数およびメンバー関数にアクセスするために使用されます。

オブジェクトが独自のメンバー関数を呼び出すと、コンパイラはthisを通じて暗黙的にオブジェクトのアドレスをメンバー関数に渡しますこのポインタを介して、メンバー関数は現在のオブジェクトのメンバー変数およびメンバー関数にアクセスし、操作することができます

静的メンバー関数にはこのポインターがなく、特定のオブジェクトに属さないため、このポインターは非静的メンバー関数でのみ使用できます。

C++クラスオブジェクトの初期化シーケンスと多重継承の場合のシーケンスについて説明します。

参考回答

  1. 派生クラスのオブジェクトを作成する場合、基本クラスのコンストラクターが最初に (派生クラスのメンバー クラスよりも前に) 呼び出されます。

  2. クラス内にメンバー クラスがある場合、メンバー クラスのコンストラクターが最初に呼び出されます (クラス自体のコンストラクターよりも前)。

  3. 基本クラス コンストラクター 基本クラスが複数ある場合、コンストラクターが呼び出される順序は、特定のクラスがメンバー初期化テーブルに現れる順序ではなく、クラス導出テーブルに現れる順序になります。

  4. メンバー クラス オブジェクト コンストラクター 複数のメンバー クラス オブジェクトがある場合、コンストラクターが呼び出される順序は、オブジェクトがメンバー初期化テーブルに表示される順序ではなく、クラス内で宣言された順序になります。

  5. 派生クラス コンストラクターは、原則として、基底クラスのデータ メンバーに値を直接割り当てるのではなく、適切な基底クラス コンストラクターに値を渡す必要があり、そうしないと、2 つのクラスの実装が密結合になってしまいます。基本クラスの実装を正しく変更または拡張することはさらに困難になります。(基本クラスのコンストラクターの適切なセットを提供するのは、基本クラスの設計者の責任です)

  6. 要約すると、初期化シーケンスは次のように結論付けることができます。

    親クラス コンストラクター –> メンバー クラス オブジェクト コンストラクター –> 自身のコンストラクター

    メンバ変数の初期化は宣言順序に関係し、コンストラクタの呼び出し順序はクラス派生リストの順序になります。

    破壊の順序は建設の順序と逆です。

上方変換と下方変換を簡単に説明します

  1. サブクラスを親クラスに変換します。アップキャスト、dynamic_cast<type_id>(expression) を使用します。この変換は比較的安全で、データ損失は発生しません。

  2. 親クラスからサブクラスへの変換: 下方変換の場合は、強制変換 を使用できます。親クラスのポインタまたは参照されるメモリにはサブクラスのメンバーのメモリが含まれていない可能性があるため、この変換は安全ではなく、データ損失の原因となります

ディープコピーとシャローコピー、ディープコピーの実装方法について簡単に説明します。

  1. 浅いコピー: 値コピーとも呼ばれ、ソース オブジェクトの値がターゲット オブジェクトにコピーされます。基本的に、ソース オブジェクトとターゲット オブジェクトは 1 つのエンティティを共有しますが、参照される変数名は異なり、アドレスは実際には同じです。簡単な例を挙げると、あなたのニックネームは Xixi で、ファーストネームは Dongdong です。誰かがあなたを Xixi または Dongdong と呼ぶとき、あなたは同意するでしょう。これら 2 つの名前は異なりますが、どちらもあなたを指します。

  2. ディープ コピーでは、コピーするときに、まずソース オブジェクトと同じサイズのスペースを開き、次にソース オブジェクトの内容をターゲット オブジェクトにコピーします。これにより、2 つのポインターが異なるメモリ位置を指すようになります。内部の内容は同じであり、目的を達成するだけでなく、問題も発生しません。2 つのポインタは、デストラクタを順番に呼び出して、それらが指す場所を解放します。つまり、ポインタが追加されるたびに新しいメモリが割り当てられ、ポインタは新しいメモリを指すため、ディープコピーの場合、同じメモリを繰り返し解放するエラーは発生しません。

  3. ディープ コピーの実装: オーバーロードされたコピー コンストラクターとディープ コピーの代入演算子の従来の実装:

    STRING( const STRING& s ) 
    { 
        //_str = s._str; 
        _str = 新しい文字[strlen(s._str) + 1]; 
        strcpy_s( _str, strlen(s._str) + 1, s._str ); 
    } 
    STRING& 演算子=(const STRING& s) 
    { 
        if (this != &s) 
        { 
            //this->_str = s._str; 
            削除[] _str; 
            this->_str = 新しい char[strlen(s._str) + 1]; 
            strcpy_s(this->_str, strlen(s._str) + 1, s._str); 
        *thisを
        返します; 
    }
    
    
    
    

    ここでのコピー コンストラクターは理解しやすく、まずソース オブジェクトと同じ大きさのメモリ領域を解放し、次にコピー対象のデータをターゲットのコピー オブジェクトにコピーします。では、ここでの代入演算子のオーバーロードはどのように行われるのでしょうか?

    この方法は、同じメモリが 2 回解放されるのを防ぐために、異なるポインタが異なるメモリを指すように常にスペースを空けることによって、ポインタのハングの問題を解決します。

コピー コンストラクターと代入演算子のオーバーロードの違いは何ですか?

コピー コンストラクターは、新しいオブジェクトを構築するために使用されます。

代入演算子のオーバーロードは、元のオブジェクトの内容をターゲット オブジェクトにコピーするために使用されます。ターゲット オブジェクトに解放されていないメモリが含まれている場合は、まずそのメモリを解放する必要があります。

コンストラクターとデストラクターは例外をスローできますか?

構文の観点から見ると、コンストラクターは例外をスローできますが、ロジックとリスク管理の観点からは、例外をスローしないようにしてください。スローしないと、メモリ リークが発生する可能性があります。

デストラクタは例外をスローできません。デストラクタが例外をスローすると、メモリの解放やその他の操作など、例外ポイント以降のプログラムが実行されなくなり、メモリ リークの問題が発生します。また、例外が発生すると、C++デストラクタが例外をスローします。通常、オブジェクトのメソッドはリソースを解放するために呼び出されますが、このときにデストラクターも例外をスローすると、つまり、前の例外が処理されずに新しい例外が発生し、プログラムがクラッシュします。

移動コンストラクターについて簡単に説明します。この関数を使用するライブラリはどれですか?

C++11 には新しい move コンストラクターがあります。コピーと同様、移動では、あるオブジェクトの値を使用して別のオブジェクトの値を設定します。ただし、コピーとは異なり、移動ではオブジェクト値の実際の転送 (ソース オブジェクトから宛先オブジェクトへ) が実装されます。つまり、ソース オブジェクトはそのコンテンツを失い、そのコンテンツは宛先オブジェクトによって占有されます。移動操作は、移動された値のオブジェクトが名前のないオブジェクトである場合に発生します。ここでの名前のないオブジェクトは、名前さえ持たない一時変数です。典型的な名前のないオブジェクトは、関数の戻り値または型変換のオブジェクトです。一時オブジェクトの値を使用して別のオブジェクトの値を初期化する場合、オブジェクトのコピーは必要ありません。一時オブジェクトは他の場所で使用されないため、その値は宛先オブジェクトに移動できます。これを行うには、移動コンストラクターと移動代入を使用する必要があります。一時変数を使用してオブジェクトを構築および初期化する場合は、移動コンストラクターを呼び出します。同様に、名前のない変数の値をオブジェクトに代入する場合、移動代入操作が呼び出されます。

移動操作の概念は、オブジェクトが new や delete を使用してメモリを割り当てる場合など、オブジェクトが使用する記憶域スペースを管理するのに役立ちます。このタイプのオブジェクトでは、コピーと移動は異なる操作です。A から B にコピーするということは、B に新しいメモリが割り当てられ、A の内容全体が B に割り当てられた新しいメモリにコピーされることを意味します。A から B への移動は、A に割り当てられたメモリが B に転送されることを意味し、新しいメモリは割り当てられず、単にポインタをコピーするだけです。次の例を見てください。

// コンストラクターと代入を移動
#include <iostream> 
#include <string> 
using namespace std; 

class Example6 { 
    string* ptr; 
public: 
    Example6 (const string& str) : ptr(new string(str)) {} 
    ~Example6 ( ) {delete ptr;} 
    // コンストラクターを移動します。パラメーター x は const Pointer&& x にはできません。
    // x のメンバー データの値を変更する必要があるため、
    // C++98 はサポートしていません。C++0x (C ++11 ) は
    Example6 (Example6&& x) をサポートします: ptr(x.ptr) 
    { 
        x.ptr = nullptr; 
    } 
    // 代入を移動
    Example6& 演算子= (Example6&& x) 
    { 
        delete ptr; 
        ptr = x.ptr; 
        x.ptr= nullptr; 
        *this を返します; 
    }
    // コンテンツへのアクセス: 
    const string& content() const {return *ptr;} 
    // 追加: 
    Example6 Operator+(const Example6& rhs) 
    { 
        return Example6(content()+rhs.content()); 
    } 
}; 
int main () { 
    Example6 foo("Exam"); // コンストラクタ
    // Example6 bar = Example6("ple"); // コンストラクタをコピー
    Example6 bar(move(foo)); // コンストラクタを移動
                                // move を呼び出した後、foo右辺値参照変数になります, 
                                // この時点で、foo が指す文字列は「空」になっています, 
                                // そのため、現時点では foo を呼び出すことはできません
    bar = bar+ bar; // 代入をここに移動します "= " 記号の右側の加算演算は、
                                // 一時的な値、つまり右辺値を生成します。
                                 // したがって、この時点で移動代入ステートメントが呼び出されます
    cout << "foo's content: " << foo.content() << '\n'; 
    return 0; 
}




結果:

foo の内容: 例



「参照データ メンバーは C++ クラス内で定義できますか?」という質問に答えてください。

参照メンバー変数は C++ クラス内で定義できますが、次の 3 つの規則に従う必要があります。

  1. デフォルトのコンストラクターでは初期化できません。参照メンバー変数を初期化するにはコンストラクターを提供する必要があります。そうしないと、初期化されていない参照エラーが発生します。

  2. コンストラクターの仮パラメーターも参照型である必要があります。

  3. コンストラクター内で初期化することはできないため初期化リスト内で初期化する必要があります。

定数関数とは何か、そしてそれが何をするのかを簡単に説明する

クラス メンバー関数の後に const を追加すると、この関数がこのクラス オブジェクトのデータ メンバー (正確には非静的データ メンバー) に変更を加えないことを示します。クラスを設計する場合、データ メンバーを変更しないメンバー関数の最後に const を追加することが原則であり、データ メンバーを変更するメンバー関数には const を追加できません。したがって、const キーワードはメンバー関数の動作をより明確に定義します。const 変更を伴うメンバー関数 (const が関数の前やパラメーター リストではなく、関数パラメーター リストの後に配置されることを意味します) は、データ メンバーのみを読み取ることができます。 、データ メンバーは変更できません。const を変更しないメンバー関数はデータ メンバーの読み取りと書き込みが可能です。また、クラスのメンバー関数の後に const を追加する利点は何ですか? つまり、定数 (つまり const) オブジェクトは const メンバー関数を呼び出すことができますが、const で変更されていない関数を呼び出すことはできません。非 const 型データを const 型変数に代入できるのと同様に、その逆は当てはまりません。

#include<iostream> 
using namespace std; 
 
class CStu 
{ 
public: 
    int a; 
    CStu() 
    { 
        a = 12; 
    } 
 
    void Show() const 
    { 
        //a = 13; //定数関数はデータ メンバーを変更できません
        cout <<a << "私は show()" << endl; 
    } 
}; 
 
int main() 
{ 
    CStu st; 
    st.Show(); 
    system("pause"); 
    return 0; 
}


コピーコンストラクターのパラメーターを渡す方法とその理由は何ですか?

参考回答

  1. コピー コンストラクターのパラメーターは参照によって渡す必要があります

  2. コピー コンストラクターのパラメーターが参照ではない場合、つまり CClass (const CClass c_class) の形式の場合、値渡しメソッドを使用するのと同等であり、値渡しメソッドは次のメソッドを呼び出しますクラス コンストラクター のコピーその結果、コピー コンストラクターへの無限の再帰呼び出しが行われます。したがって、コピー コンストラクターの引数は参照である必要があります。

    明確にする必要があるのは、ポインタの受け渡しは実際には値渡しであるため、上記のコピー コンストラクターを CClass (const CClass* c_class) として記述した場合は機能しません。実際、参照渡しのみが値渡しではなく、他のすべての転送メソッドは値渡しです。

  1. クラス Aクラス B1:パブリック仮想 A;クラス B2:パブリック仮想 A;クラス D:パブリック B1、パブリック B2;

  1. 仮想的に継承されたクラスは、たとえば次のようにインスタンス化できます。

    クラス動物 {/* ... */ };クラス タイガー : 仮想公開動物 { /* ... */ };クラス ライオン : 仮想公開動物 { /* ... */ }

    int main( ){

    ライガーLG;

    /*Tiger クラスと Lion クラスの定義で「virtual」キーワードを宣言しているため、次のコードはコンパイルされます。 */intweight = lg.getWeight();

    }

コピー割り当てと移動割り当てについて簡単に説明します。

参考回答

  1. コピー代入とは、コピーコンストラクターを介して値を代入することで、オブジェクトを作成するときに、同じクラス内で以前に作成したオブジェクトを使用して、新しく作成したオブジェクトを初期化します。

  2. 移動代入では、移動コンストラクターを通じて値が割り当てられます。この 2 つの主な違いは次のとおりです。

    1) コピー コンストラクターの仮パラメーターは左辺値参照ですが、移動コンストラクターの仮パラメーターは右辺値参照です。

    2) コピー コンストラクターはオブジェクトまたは変数全体のコピーを完了しますが、移動コンストラクターはソース オブジェクトまたは変数のアドレスを指すポインターを生成しソース オブジェクトのメモリを引き継ぎ、コピーに比べて時間とメモリ スペースを節約します大量のデータ。

クラスオブジェクトとクラスポインタの違い

  1. 意味:

    クラス オブジェクト:クラスのコンストラクターを使用して、メモリ内の領域を割り当てます(一部のメンバー変数の割り当てを含む)。

    クラス ポインタ:メモリに格納されているクラス オブジェクトを指すメモリ アドレス値です(クラス ポインタは複数の異なるオブジェクトを指すことができます(ポリモーフィズム))。

  2. 使用

    参照メンバー: オブジェクトには「.」演算子を使用し、ポインターには「->」演算子を使用します。

  3. ストレージの場所

    クラス オブジェクト:メモリ スタックを使用し、ローカルの一時変数です。

    クラス ポインタ:メモリ ヒープを使用し、解放しない限り永続変数です。新規/削除

  4. ポインターはポリモーフィズムを実現できますが、オブジェクトを直接使用することはできません。

5. テンプレート

クラス テンプレートと C++ のテンプレート クラスの違いを説明する

参考回答

  1. クラス テンプレートは、実際のクラスではなく、テンプレートの定義です。定義にはユニバーサル型パラメーターが使用されます。

  2. テンプレート クラスは実際のクラス定義であり、クラス テンプレートのインスタンス化ですクラス定義内のパラメータは実際の型に置き換えられます。

回答分析

  1. クラス テンプレートには 1 つ以上の型パラメータを含めることができます。各型の前には、 template <class T1, class T2>class someclass{...} のように class を付ける必要があります。 objectを定義するときは、実際の型名を次のように置き換えます。someclass< int,double> obj;

  2. クラスを使用する場合と同様に、クラス テンプレートを使用するときはそのスコープに注意する必要があり、有効スコープ内のオブジェクトを定義するためにのみ使用できます。

  3. テンプレートは階層を持つことができ、クラス テンプレートは派生テンプレート クラスを派生できる基本クラスとして機能できます。

テンプレート クラスがいつ実装されたかについて話しましょう

  1. テンプレートのインスタンス化: テンプレートのインスタンス化は、明示的なインスタンス化と暗黙的なインスタンス化に分けられます。前者は、開発者が特定のクラスまたは関数の生成に使用する型をテンプレートに明示的に指示する場合であり、後者はコンパイルプロセス中に行われます。コンパイラが決定します。テンプレートのインスタンス化に使用するタイプ。明示的なインスタンス化か暗黙的なインスタンス化かに関係なく、最終的に生成されたクラスまたは関数は、テンプレートの定義に従って完全に実装されます。

  2. テンプレートの具体化:テンプレートが特定の型を使用してインスタンス化された後に生成されるクラスまたは関数がニーズを満たせない場合、テンプレートの具体化を検討できます。元のテンプレートの定義は具体化中に変更することができます。このタイプを使用する場合は、具体化された定義に従って実装されます。具体化は、ある種の特殊な処理に相当します。

  3. コード例:

    ll構造体; 
    }
    
    
    
    
    
    
    
    
    

    操作結果:

    4 
    --8-- 
    1

ファンクターを理解していますか? 効果は何ですか

参考回答

  1. ファンクターは関数オブジェクトとも呼ばれ、関数 function を実行できるクラスです。ファンクターの構文は通常の関数呼び出しとほぼ同じですが、ファンクター クラスとして、operator() 演算子をオーバーロードする必要があります。次に例を示します。

クラス Func{パブリック:

void 演算子() (const string& str) const { 
    cout<<str<<endl; 
}


};

Func myFunc;myFunc("helloworld!");

こんにちは世界!

  1. ファンクターは、通常の関数と同様に、指定された数のパラメーターを渡すことができ、必要なさらに有用な情報を保存または処理することもできます。例を挙げてみましょう。

    Vector<string> があり、長さが 5 未満の文字列の数を数えることがタスクだとします。 count_if 関数を使用する場合、コードは次のようになります。

    bool LengthIsLessThanFive(const string& str) {

    str.length()<5 を返します。    
    
    
    

    int res=count_if(vec.begin(), vec.end(), LengthIsLessThanFive);

count_if 関数の 3 番目のパラメーターは関数ポインターであり、bool 型の値を返します。一般に、特定のしきい値の長さを渡す必要がある場合は、次のように関数を作成します。

bool LenthIsLessThan(const string& str, int len) { 
    return str.length()<len; 
}


この関数は以前のバージョンよりも汎用的に見えますが、count_if 関数のパラメータ要件を満たすことができません。count_if は最後のパラメータとして単項関数 (パラメータが 1 つだけ) を必要とします。ファンクターを使用すると、次のことが突然明らかになります。

class ShorterThan { 
public:
    明示的 ShorterThan(int maxLength) : length(maxLength) {} 
    bool operande() (const string& str) const { 
        return str.length() < length; 
    プライベート
: 
    const int 長; 
};

おすすめ

転載: blog.csdn.net/shisniend/article/details/131908947