C++常用特性原理解析

9920 ワード

私の初期の印象では、C++という言語はソフトウェアエンジニアリングの発展過程で、対象言語レベルへのサポートが欠かせない状況で、Cで宇宙を支配しようと誓った極客たちが妥協した高性能の変なカレーだった.
それは雑駁だが、人を魅了し、多くの理由で、私はそれを出してもう一度勉強した.このノートはG++でコンパイルされたアセンブリコードから出発し、参照、クラス(メンバー関数、構造関数)、マルチステート(コンパイル時、実行時)、テンプレートと汎用型を含む一部のC++の一般的なオブジェクト向け特性を原理的に解釈し、まとめた.
Here we go!
参照
これはありふれた話題ですが、C++primer中国語訳本では引用は対象の別名で、別名は何ですか?コード:
int invoke(int a) {
  return ++a;
}

int main(int argc, char **argv) {
  int a = 123;             // movl $123,-20(%rbp)

  int *pa = &a;            // leaq -20(%rbp),%rax
                           // movq %rax,-16(%rbp)
  
  int &ra = a;             // leaq -20(%rbp),%rax
                           // movq %rax,-8(%rbp)

  invoke(a);               // movl -20(%rbp),%eax
                           // movl %eax,%edi
                           // call _Z6invokei

  invoke(*pa);             // movq -16(%rbp),%rax
                           // movl (%rax),%eax
                           // movl %eax,%edi
                           // call _Z6invokei

  invoke(ra);              // movq -8(%rbp),%rax
                           // movl (%rax),%eax
                           // movl %eax,%edi
                           // call _Z6invokei
}

簡単に明らかに、paはaを指すポインタであり、raはaの参照であり、コンパイラのpaとraの定義とパラメータ伝達の仕事はほとんどそっくりであり、それらはスタックに自分の空間があり、aのアドレスが保存されていることがわかるので、参照はポインタで実現されていると言える.リファレンスは、ポインタの言語レベルのカプセル化であり、プログラムの可読性を向上させるために、通常はパラメータ伝達に使用されます.引用のメリットと使い方については、さらに勉強しなければなりません.//TODO
クラス(メンバー関数、コンストラクション関数)
コードを貼る前に、符号という概念を振り返る必要があります.アセンブリ言語では、各命令の前に、命令アドレスのアセンブリアドレスを代表し、指示するための符号を持つことができます.なぜなら、各命令が存在するアセンブリアドレスを計算し、追跡することは極めて困難であるからです.
アセンブリをマシンコードに翻訳する過程で、これらのラベルはラベルが行の具体的なオフセットアドレスに変換され、多くの場合、命令ブロック入口アドレスをマークするために使用され、いわゆる関数のジャンプを行う.忘れた同級生は先に娘を過ごすことができる.
次のコードでは、各関数の後のコメントに、その関数がコンパイルされたラベル名が表示されます.
int invoke(int a) {                   // _Z6invokei
  return ++a;
}

class Animal {
public:
  int age;
  int weight;
  Animal(): age(0), weight(0.0) {}    // _ZN6AnimalC2Ev
  void run() { }                      // _ZN6Animal3runEv
};

class Human {
public:
  Human() {}                          // _ZN5HumanC2Ev
};

int main(int argc, char **argv) {
  Animal cat;                         // leaq -16(%rbp), %rax
                                      // movq %rax, %rdi
                                      // call _ZN6AnimalC1Ev
                                      
  cat.age = 5;                        // movl $5, -16(%rbp) 
  cat.weight = 2;                     // movl $2, -12(%rbp)
                                              
  cat.run();                          // leaq -16(%rbp), %rax
                                      // movq %rax, %rdi
                                      // call _ZN6Animal3runEv
}

前の例に比べて,この波コードにはAnimalクラスとHumanクラスが追加された.
main関数から始めます
  • オブジェクト初期化第1文Animal cat;Animalのオブジェクトcatを初期化し、右側のアセンブリコードから、catが新たに拡張されたスタックフレームの16バイト目のオフセットに複合タイプとして格納されていることがわかる-16(%rbp)catのアドレスをrdiに格納することは明らかである.これがC++がクラスのメンバ関数を呼び出すときに渡す暗黙的なパラメータthisポインタであり,次に符号名_ZN6AnimalC1Evにジャンプして実行を継続し,Animalクラスではその符号名に対する関数がAnimalクラスの構築関数であることがわかる.
  • クラスメンバー賦課これは何の話もないが、Cの構造体メンバーの賦課と同じである.
  • メンバー関数呼び出し対メンバー関数run()の呼び出しであり、コンパイラの処理方式はコンストラクタ呼び出しとそっくりである.

  • 比較G++コンパイル中の異なる関数に対するラベル名:Animalクラス一般関数:invoke()Z 6 invokei一般メンバー関数:run()ZN 6 Animal 3 runEvコンストラクタ:Animal()ZN 6 AnimalC 2 Evev Humanクラス:コンストラクション関数:Human()ZN5HumanC2Ev
    文法の面では、C++は異なる関数の定義と呼び出し方式を規定しており、コンパイラは異なる関数に対して異なる処理方式を使用します.例えば、メンバー関数を呼び出すと、thisポインタが暗黙的に伝達されます.例えば、メンバー関数を直接呼び出すとコンパイルエラーが発生します.コンパイルに成功した後、すべての関数は特定のラベルマークの命令シーケンスにすぎません.ラベルの名前からC++が一意であることがわかります.
    したがって、狭義には、クラスとは、実際には複合タイプであり、メンバー関数とは、呼び出しオブジェクト自体のポインタをデフォルトで渡す一般的な関数であり、コンストラクション関数とは、オブジェクトの初期化時に自動的に呼び出される一般的な関数であり、これらの追加の特性はコンパイル段階で実現される.
    マルチステート(コンパイル時、実行時)
  • リロードアセンブリの観点から、リロードされた複数の関数も複数の異なる符号名に対応しているにすぎない:
  • class Animal {
    public:
      void run() {}                     // _ZN6Animal3runEv 
      void run(int a) {}                // _ZN6Animal3runEi
      void run(char b) {}               // _ZN6Animal3runEc
      void run(int a, Human p) {}       // _ZN6Animal3runEi5Human
    };

    G++は、リロードされた複数の関数の異なるパラメータリストによってラベルに一意の名前を付け、いわゆるコンパイル時マルチステートである.
  • 継承は簡単に実現できますが、1点目はコンパイラがスペースを割り当てるときに子クラス固有メンバー変数とその親クラスメンバー変数の合計サイズを割り当て、2点目はコンパイル時に子クラス構築関数の中で親クラスの構築関数を呼び出します.ここでは例をあげないで,主な編幅は以下の運転時多態に置く.
  • 運転時多態
  • class Animal {
    public:
      virtual void run() {}              // _ZN6Animal3runEv
    };
    
    class Cat : public Animal {
    public:
      void run() {}                      // _ZN3Cat3runEv
    };
    
    int main(int argc, char **argv) {
      Animal *tom = new Cat();           // _ZN3CatC2Ev:
                                         // _ZN6AnimalC2Ev:
                                         // movq $_ZTV6Animal+16, (%rax)
                                         // movq $_ZTV3Cat+16, (%rax)
      
      tom.run();                         // movq %rbx, -24(%rbp)
                                         // movq -24(%rbp), %rax 
                                         // movq (%rax), %rax 
                                         // movq (%rax), %rax
                                         // movq -24(%rbp), %rdx
                                         // movq %rdx, %rdi
                                         // call *%rax
    }

    ここで、new Cat()呼び出す2つのコンストラクタを実行順に選択的に展開すると、2つの重要なアセンブリコードが表示されます(%rax)はtomオブジェクトのスタック内の開始位置を表し、唯一有効な最後のコードmovq $_ZTV3Cat+16, (%rax)Catクラスの_虚関数テーブル_ポインタはcatオブジェクトの開始位置に格納されます.
    さらにtom.run()のアセンブリを見て、追跡すると、最後のコードcall *%raxCatクラスの虚関数テーブルの最初の関数がちょうど呼び出されていることがわかりました.
    これがいわゆるランタイムマルチステートの呼び出しロジックですが、なぜいわゆるものなのでしょうか.このロジックはコンパイル時に実現できるので、tomポインタをCatオブジェクトに向けるとtomがどのrunを呼び出すのかが決定され、tom.run()直接最適化call _ZN3Cat3runEvにコンパイルされる賢いコンパイラもあります.
    では、どのようなランタイムマルチステートがコンパイル段階ではできないのでしょうか.次のコードを見てください.
    int main(int argc, char **argv) {
      Animal *tom;
      if (argc == 0)  
        tom = new Animal();
      else 
        tom = new Cat();
    
      tom->run();
    }

    このとき,コンパイルtom->run()のときにどのrunを呼び出すべきかを知ることは不可能であるため,前のコードに基づいて展開したコンストラクション関数は,実行時にどのコンストラクション関数が呼び出され,tomが指すオブジェクトにどのクラスの虚関数テーブルポインタが格納されているかを知ることができ,これが本当の意味での実行時多態である.
    テンプレートと汎用
    class Cat {};
    class Mouse {}; 
    
    template <typename T>
    class Cave {
    public:
      void capture(T& a) {}; 
    };
    
    int main(int argc, char **argv) {
      Cat tom;
      Mouse jerry;
    
      Cave<Cat> catsCave;                    
      catsCave.capture(tom);               // call _ZN4CaveI3CatE7captureERS0_ 
      Cave<Mouse> miceCave;
      miceCave.capture(jerry);             // call _ZN4CaveI5MouseE7captureERS0_
    }

    以前の関数と記号に対する認識があれば,テンプレートと汎用型の実現を理解することは手当たり次第である.コンパイラは、テンプレートクラスにいくつかの異なるタイプの宣言が指定されていることを認識し、各タイプに対応する一意の関数ラベルと異なる関数実装を生成します.この簡単な例では、コンパイラは猫を捕まえるケージとネズミを捕まえるケージのために異なる捕捉関数をコンパイルします.
    従来の実装方式は、異なるケージのために異なるクラスと関数を宣言することであり、この生成されたアセンブリコードは、テンプレートを使用して汎用的に生成されたアセンブリコードと機能的にそっくりであり、コードの詳細においても差が少なく、異なるのは記号名にすぎない.
    テンプレートと汎用型は言語レベルでこのような簡便で拡張性に優れたプログラミング方式を提供し、このような設計思考はC++が推奨している.
    このノートを書くことで、C++初心者に少しガイドを提供し、私の就職活動の道に役立つことを望んでいます.
    「C++プログラミング言語」の一言を添付します.C++はあなたの成長に伴う言語です.
    批判と討論を歓迎する.