C++オブジェクトモデル4-関数の呼び出し

5051 ワード

一般メンバー関数の呼び出し
C++の設計ガイドラインの1つは、通常のメンバー関数の呼び出しが少なくともグローバル関数と同じ効率を有することである.実際、C++コンパイラの実装は、通常のメンバー関数をグローバル関数の呼び出しに変換することです.次のコードを参照してください.
#include
using namespace std;

class A{
public:
    const char *name = "class A";
    int val = 100;
    void print(){
        cout << "name = " << name << " val = " << val << endl;
    }
};

void printA(A *a){
    cout << "name = " << a->name << " val = " << a->val << endl;
}

int main(){
    A a;
    a.print();
    printA(&a);
    return 0;
}

この変換には、コンパイラが3つのことをする必要があります.
  • 関数パラメータを変更し、thisポインタを最初のパラメータとして関数に入力します.
  • のメンバーへのアクセスは、thisポインタを介しています.
  • メンバー関数を外部関数に書き換えます.

  • これを検証するには、アセンブリコードを表示します.
        a.print();
    00007FF7C69C461D  lea         rcx,[a]  
    00007FF7C69C4622  call        A::print (07FF7C69C123Ah)  
        printA(&a);
    00007FF7C69C4627  lea         rcx,[a]  
    00007FF7C69C462C  call        printA (07FF7C69C1221h)
    

    print関数にはパラメータがないのに、printA関数の呼び出し方法と同じthisポインタが渡されていることがわかります.
    静的メンバー関数
    静的メンバー関数はC++の後に導入され,導入されない前に類似の関数呼び出し方法もある.次に例を示します.
    #include
    using namespace std;
    
    class A{
    public:
        int val;
        static void print(){
            cout << "hello world" << endl;
        }
    };
    
    int main(){
        ((A *)0)->print();
        return 0;
    }
    

    C++コンパイラがメンバー関数をグローバル関数に変更すると、thisポインタが渡され、メンバーの値にアクセスしないため、このような呼び出しには問題がないことが想像できます.これにより、thisポインタがない静的グローバル関数と通常のメンバー関数の最大の違いも引き出されます.総じて、静的メンバー関数には次のような特徴があります.
  • 静的メンバー関数にはthisポインタがありません.つまり、クラス内の非静的メンバー関数にアクセスできません.
  • 静的メンバー関数はconst(thisポインタなし)修飾もvirtual(虚関数テーブルポインタにアクセスできない)修飾も使用できません.
  • はクラスオブジェクト呼び出しを使用できますが、クラスオブジェクトのメンバーにアクセスできません.クラス名呼び出しも使用できます.
  • 静的メンバー関数は非メンバー関数と同等であり、コールバック関数を提供する必要がある場合、静的メンバー関数をコールバック関数として使用することができる.

  • 虚関数の呼び出し
    虚関数は、通常のメンバー関数の呼び出しとはあまり差がありません.違いは、虚関数の呼び出しは虚関数テーブルを通過する必要があります.次の例を参照してください.
    #include
    using namespace std;
    
    class A{
    public:
        void * vptr;
        const char *name = "class A";
        int val = 100;
        virtual void print_name(){
            cout << "name = " << name << endl;
        }
        virtual void print_val(){
            cout << "val = " << val << endl;
        }
    };
    
    void a_name(A *a){
        cout << "name = " << a->name << endl;
    }
    
    void a_val(A *a){
        cout << "val = " << a->val << endl;
    }
    
    typedef void(*func)(A*);
    
    int main(){
        A *a = new A();
        void * vt[2] = {(void *)a_name,(void *)a_val};
        a->vptr = vt;
        a->print_name();
        a->print_val();
        (*((func *)(a->vptr)))(a);
        (((func *)a->vptr)[1])(a);
        (*((func *)a->vptr)[1])(a);
        (*((func *)(a->vptr) + 1))(a);
        return 0;
    }
    

    ここで注意すべきことは、まず、配列名が配列にアドレスを取ったこと、すなわちchar*name[]に相当し、nameを使用することはchar**を使用することに相当することです.2つ目は[i]オペレータが*(name+i)に相当することです.3つ目は、関数ポインタに対して*解参照を適用しなくてもよいし、使用しなくてもよい.ほとんど使わない.
    上の問題を明らかにすると、コードが理解しやすくなります.上のコードはコンパイラの処理を手動でシミュレートし、虚関数テーブルと虚関数ポインタを生成し、虚関数ポインタを使用して虚関数を呼び出し、多態を実現することができます.検証する場合は、アセンブリコードを見てprint_を呼び出します.valのアセンブリコードは次のとおりです.
        a->print_val();
    00007FF6D83F5256  mov         rax,qword ptr [a]  //rax = a  
    00007FF6D83F525B  mov         rax,qword ptr [rax]  //rax = *a = vptr
    00007FF6D83F525E  mov         rcx,qword ptr [a]  //rcx = a
    00007FF6D83F5263  call        qword ptr [rax+8]  //vptr       
    

    虚関数の場合、thisポインタを入力する必要があるか、虚関数テーブルのインデックスを入力する必要があるかがわかります.
    継承システムではthisポインタを調整する必要がある場合もありますが、この場合は複雑で、議論を展開しないで、簡単に言えば、マルチ継承システムでは虚析構関数を適用しないと問題が発生します.
    #include
    using namespace std;
    
    class A{
    public:
        int a_val;
    };
    
    class B{
    public:
        int b_val;
    };
    
    class C:public A,public B{
    public:
        int c_val;
    };
    
    int main(){
        B *b = new C();
        delete b;
        return 0;
    }
    

    実はこれがthisポインタの調整に現れた問題で、C言語ではfree関数はmallocが割り当てたポインタでなければならず、このポインタは変更できないことを知っています.ここで発生したエラーはこのthisポインタは、割り当てを開始するメモリ(newが最下位でmallocを呼び出す)を指していないため、free(deleteが最下位でfreeを呼び出す)の呼び出しに失敗しました.ここで、プログラムを正常に実行するには、親の構造関数を虚関数として宣言するだけです.この例では、継承システムでは、親の構造関数を虚関数として宣言することが望ましいことを示します.
    メンバー関数へのポインタ
    クラスメンバーを指すポインタと同じように、C++にもメンバー関数を指すポインタがありますが、この個人的な感覚ではわかりますが、普段はあまり使われていません.
    #include
    using namespace std;
    
    class A{
    public:
        int val;
        void f1(){
            cout << "A::f1()" << endl;
        }
    
        virtual void f2(){
            cout << "A::f2()" << endl;
        }
    };
    
    class B :public A{
    public:
        virtual void f2() override{
            cout << "B::f2()" << endl;
        }
    };
    
    int main(){
        void(A::*p1)() = &A::f1;
        void(A::*p2)() = &A::f2;
        printf("%p %p
    "); A *a = new B(); (a->*p1)(); (a->*p2)(); return 0; }

    この呼び出しがおかしいのは、オブジェクトをバインドして呼び出さなければならないためで、文法的には少しおかしいからです.