web-dev-qa-db-ja.com

C ++プログラムの関数のアドレスは何ですか?

関数は、1つの連続したメモリブロックに格納された命令のセットであるため。

また、関数のアドレス(エントリポイント)は、関数の最初の命令のアドレスです。 (私の知識から)

したがって、関数のアドレスと関数の最初の命令のアドレスは同じであると言えます(この場合、最初の命令は変数の初期化です)。

しかし、以下のプログラムは上記の行と矛盾しています。

コード:

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
char ** fun()
{
    static char * z = (char*)"Merry Christmas :)";
    return &z;
}
int main()
{
    char ** ptr = NULL;

    char ** (*fun_ptr)(); //declaration of pointer to the function
    fun_ptr = &fun;

    ptr = fun();

    printf("\n %s \n Address of function = [%p]", *ptr, fun_ptr);
    printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);
    cout<<endl;
    return 0;
}

1つの出力例は次のとおりです。

 Merry Christmas :) 
 Address of function = [0x400816]
 Address of first variable created in fun() = [0x600e10]

したがって、ここで関数のアドレスと関数の最初の変数のアドレスは同じではありません。なぜそうなのか?

私はグーグルで検索しましたが、正確な必要な答えを思い付くことができず、この言語が初めてなので、ネット上のコンテンツのいくつかを正確にキャッチすることができません。

32
Amit Upadhyay

したがって、ここで関数のアドレスと関数の最初の変数のアドレスは同じではありません。なぜそうですか?

なぜそうなるのでしょうか?関数ポインターは、関数を指すポインターです。とにかく、関数内の最初の変数を指していません。

詳細を説明すると、関数(またはサブルーチン)は、必要に応じて特定のジョブを(通常は複数回)実行する一連の命令(変数定義とさまざまなステートメント/操作を含む)です。関数内に存在するelementsへの単なるポインターではありません。

関数内で定義された変数は、実行可能なマシンコードと同じメモリ領域に保存されません。ストレージタイプに基づいて、存在する変数insideは、実行プログラムのメモリの他の部分に配置されます。

プログラムをビルド(オブジェクトファイルにコンパイル)すると、プログラムのさまざまな部分がさまざまな方法で編成されます。

  • 通常、関数(実行可能コード)は、 コードセグメント と呼ばれる別のセグメントにあり、通常は読み取り専用のメモリ位置です。

  • 割り当てられたコンパイル時間変数OTOHは、 データセグメント に格納されます。

  • 関数ローカル変数は、通常、必要に応じてスタックメモリに格納されます。

そのため、ソースコードに見られるように、関数ポインターが関数に存在する最初の変数のアドレスを生成するような関係はありません。

この点で、 wiki の記事を引用するには、

データ値を参照する代わりに、関数ポインターはメモリ内の実行可能コードを指します。

したがって、TL; DR、関数のアドレスは、実行可能命令が存在するコード(テキスト)セグメント内のメモリ位置です。

52
Sourav Ghosh

関数のアドレスは、呼び出しなどで渡すなど、この関数を渡すための記号的な方法にすぎません。潜在的に、関数のアドレスに対して取得する値は、メモリへのポインタでさえありません。

関数のアドレスは、次の2つの目的に適しています。

  1. 等しいかどうかを比較するp==q

  2. (*p)()を逆参照して呼び出す

他にやろうとすることは未定義であり、動作する場合と動作しない場合があり、コンパイラの決定です。

17
Aganju

さて、これは楽しいことです。 C++の関数ポインターとは何かという非常に抽象的な概念からアセンブリコードレベルに至るまで、特定の混乱のおかげで、スタックについても議論できます。

非常に抽象的な側面から始めましょう。それは明らかにあなたが始めているものの側面だからです。あなたが遊んでいる関数char** fun()があります。さて、このレベルの抽象化では、関数ポインターで許可される操作を確認できます。

  • 2つの関数ポインターが等しいかどうかをテストできます。 2つの関数ポインターは、同じ関数を指している場合は等しいです。
  • これらのポインターに対して不等式テストを実行して、そのようなポインターのソートを行うことができます。
  • 関数ポインターを延期することができますが、その結果、「関数」型になり、操作が非常に複雑になりますが、ここでは無視することにします。
  • 使用した表記法fun_ptr()を使用して、関数ポインターを「呼び出す」ことができます。これの意味は、ポイントされている関数を呼び出すことと同じです。

抽象レベルで行うのはそれだけです。その下では、コンパイラーは自由に実装できますが、適切と思われます。コンパイラがFunctionPtrTypeを必要とする場合、これは実際にはプログラム内のすべての関数の大きなテーブルへのインデックスです。

ただし、これは通常、実装方法ではありません。 C++をアセンブリ/マシンコードにコンパイルするとき、ランタイムを節約するために、できるだけ多くのアーキテクチャ固有のトリックを利用する傾向があります。実際のコンピューターでは、変数(通常はレジスター)を読み取り、そのメモリーアドレスに格納されているコードの実行を開始するためにジャンプする「間接ジャンプ」操作がほとんど常にあります。関数はほぼ単一の命令ブロックにコンパイルされるため、ブロック内の最初の命令にジャンプすると、その関数を呼び出すという論理的な効果があります。最初の命令のアドレスは、C++の関数ポインターの抽象的な概念で必要な比較のすべてを満たしますおよびこれは、ハードウェアが関数を呼び出すために間接ジャンプを使用するのに正確に必要な値です!これは非常に便利なため、ほとんどすべてのコンパイラがそのように実装することを選択します。

しかし、あなたが見ていると思ったポインタが関数ポインタと同じである理由について話し始めるとき、私たちはもう少し微妙な何かに入る必要があります:セグメント。

静的変数はコードとは別に保存されます。それにはいくつかの理由があります。 1つは、コードをできるだけタイトにすることです。変数を保存するために、メモリ空間でコードを斑点にしたくありません。効率が悪いでしょう。ただそれをすり抜けるのではなく、あらゆる種類のものをスキップする必要があります。さらに現代的な理由もあります。ほとんどのコンピューターでは、一部のメモリーを「実行可能」および「書き込み可能」としてマークできます。これを行うと、非常に悪意のあるハッカーのトリックを処理するために非常にに役立ちます。ハッカーがプログラムをだまして独自の関数を上書きする方法を巧みに見つけた場合に備えて、実行可能と書き込み可能の両方を同時にマークしないようにします!

したがって、通常_.code_セグメントがあります(多くのアーキテクチャで一般的な表記方法であるため、そのドット表記を使用しています)。このセグメントには、すべてのコードがあります。静的データは_.bss_のような場所に移動します。そのため、静的文字列は、それを操作するコードからかなり離れた場所に保存されている場合があります(通常、少なくとも4kb離れています。最新のハードウェアでは、ページレベルで実行または書き込み権限を設定できるためです:ページは多くの最新システムで4kb )

これで最後のピース...スタックです。スタックに物事を紛らわしい方法で保存することについて言及しましたが、これは簡単に検討するのに役立つかもしれません。スタックで何が起こっているかをより効果的に示すことができるので、クイック再帰関数を作成します。

_int fib(int x) {
    if (x == 0)
        return 0;

    if (x == 1)
        return 1;

    return fib(x-1)+fib(x-2);
}
_

この関数は、フィボナッチ数列を、かなり非効率的ですが明確な方法で計算します。

fibという1つの関数があります。これは、_&fib_が常に同じ場所へのポインターであることを意味しますが、明らかにfibを何度も呼び出しているので、それぞれに適切なスペースが必要ですか?

スタックには、「フレーム」と呼ばれるものがあります。フレームはnot関数そのものですが、むしろこの特定の関数呼び出しが使用できるメモリのセクションです。 fibなどの関数を呼び出すたびに、そのフレーム用にスタック上にもう少しスペースを割り当てます(または、より厳密に言うと、呼び出し後に割り当てます)。

この場合、fib(x)は、fib(x-1)の実行中にfib(x-2)の結果を保存する必要があることは明らかです。これを関数自体に格納することも、_.bss_セグメントに格納することもできません。これは、何回繰り返されるかわからないためです。代わりに、fib(x-1)が独自のフレームで動作している間(まったく同じ関数と同じ関数アドレスを使用して)fib(x-2)の結果の独自のコピーを格納するために、スタックにスペースを割り当てます。 )。 fib(x-2)が戻ると、fib(x)は単にその古い値をロードします。これは、他の誰にも触れられていないことが確実であり、結果を追加して返します!

これはどのように行われますか?実質的にすべてのプロセッサは、ハードウェアのスタックをある程度サポートしています。 x86では、これはESPレジスタ(拡張スタックポインター)として知られています。プログラムは一般に、これをデータの格納を開始できるスタック内の次のスポットへのポインターとして扱うことに同意します。 'このポインタを動かして、フレームのためのスペースを自分自身で構築してから移動してください。実行が終了したら、すべてを戻すことが期待されます。

実際、ほとんどのプラットフォームでは、関数の最初の命令はnot最終コンパイル済みバージョンの最初の命令です。コンパイラは、このスタックポインタを管理するためにいくつかの追加のopを挿入するため、心配する必要はありません。 x86_64のような一部のプラットフォームでは、この動作はしばしば必須であり、ABIで指定されています!

だから、私たちが持っているすべてでは:

  • _.code_セグメント-関数の命令が保存される場所。関数ポインタは、ここの最初の命令を指します。通常、このセグメントには「実行/読み取り専用」のマークが付けられており、プログラムはロード後にプログラムに書き込むことができません。
  • _.bss_セグメント-静的データは、データになりたい場合は「実行のみ」の_.code_セグメントの一部とすることはできないため、保存されます。
  • スタック-関数がフレームを保存できる場所。フレームは、その1つのインスタンス化に必要なデータだけを追跡し、それ以上は追跡しません。 (ほとんどのプラットフォームは、これを使用して、関数の終了後にtoを返す場所に関する情報を格納します)
  • ヒープ-この質問にはヒープアクティビティが含まれていないため、この回答には表示されませんでした。ただし、完全を期すために、後で驚かないようにここに残しておきます。
10
Cort Ammon

あなたの質問のテキストであなたは言う:

したがって、関数のアドレスと関数の最初の命令のアドレスは同じであると言えます(この場合、最初の命令は変数の初期化です)。

ただし、コードでは、関数の最初の命令のアドレスは取得せず、関数で宣言されたローカル変数のアドレスを取得します。

関数はコードであり、変数はデータです。それらは異なるメモリ領域に保存されます。同じメモリブロックに存在することすらありません。最近のOSによって課されたセキュリティ制限のため、コードは読み取り専用としてマークされたメモリブロックに保存されます。

私の知る限り、C言語はメモリ内のステートメントのアドレスを取得する方法を提供していません。そのようなメカニズムを提供する場合でも、関数の開始(メモリ内の関数のアドレス)は、最初のCステートメントから生成されたマシンコードのアドレスと同じではありません。

最初のCステートメントから生成されるコードの前に、コンパイラーは (関数プロローグ を生成します。これは(少なくとも)スタックポインターの現在の値を保存し、関数のローカル変数のためのスペースを作ります)。これは、C関数の最初のステートメントから生成されるコードの前にいくつかのアセンブリ命令を意味します。

9
axiac

あなたが言うように、関数のアドレスは(システムに依存します)関数の最初の命令のアドレスかもしれません。

それが答えです。命令とデータに同じアドレス空間が使用される典型的な環境では、命令は変数とアドレスを共有しません。

同じアドレスを共有している場合、変数に代入することにより、インストゥルメントが破棄されます!

7
MikeCAT

C++プログラムの関数のアドレスは正確に何ですか?

他の変数と同様に、関数のアドレスはそれに割り当てられたスペースです。つまり、関数によって実行される操作の命令(マシンコード)が格納されるメモリ位置です。

これを理解するには、プログラムのメモリレイアウトをよく見てください。

プログラムの変数と実行可能コード/命令は、メモリ(RAM)の異なるセグメントに保存されます。変数はSTACK、HEAP、DATA、およびBSSセグメントのいずれかに移動し、実行可能コードはCODEセグメントに移動します。プログラムの一般的なメモリレイアウトを見る

enter image description here

これで、変数と命令に異なるメモリセグメントがあることがわかります。それらは異なるメモリ位置に保存されます。関数アドレスは、CODEセグメントにあるアドレスです。

そのため、用語first statementfirst実行可能命令を混同しています。関数呼び出しが呼び出されると、プログラムカウンターは関数のアドレスで更新されます。したがって、関数ポインタは、メモリに保存されている関数の最初の命令を指します。

enter image description here

7
haccks

ここでの他の答えは、関数ポインターが何であるか、そしてそうでないかをすでに説明しています。私はあなたのテストがあなたがそれがしたと思ったことをテストしない理由を具体的に取り上げます。

また、関数のアドレス(エントリポイント)は、関数の最初の命令のアドレスです。 (私の知識から)

これは必須ではありません(他の回答で説明されているように)が、それは一般的であり、通常は直観的でもあります。

(この場合、最初の命令は変数の初期化です。).

OK。

printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);

ここで印刷しているのは、変数のアドレスです。変数を設定する命令のアドレスではありません。

これらは同じではありません。実際、それらはcannot同じではありません。

変数のアドレスは、関数の特定の実行に存在します。プログラムの実行中に関数が複数回呼び出されると、変数は毎回異なるアドレスになります。関数がそれ自体を再帰的に呼び出す場合、またはより一般的には、元の関数を呼び出す...を呼び出す別の関数を呼び出す場合、関数の各呼び出しには独自の変数があり、独自のアドレスがあります。複数のスレッドが特定の時間にその関数を呼び出している場合、マルチスレッドプログラムでも同じことが言えます。

対照的に、関数のアドレスは常に同じです。関数が現在呼び出されているかどうかに関係なく存在します。関数ポインターを使用するすべてのポイントは、通常、関数を呼び出すことです。関数を複数回呼び出してもそのアドレスは変更されません。関数を呼び出すときに、関数が既に呼び出されているかどうかを心配する必要はありません。

関数のアドレスとその最初の変数のアドレスには矛盾する特性があるため、同じにすることはできません。

(注:このプログラムが同じ2つの数字を出力できるシステムを見つけることは可能ですが、1つに遭遇することなくプログラミングキャリアを簡単に進めることができます。コードとデータが格納される Harvardアーキテクチャ がありますこのようなマシンでは、関数ポインターを印刷するときの数字はコードメモリー内のアドレスであり、データポインターを印刷するときの数字はデータメモリー内のアドレスです。2つの数字は同じかもしれませんが、偶然であり、同じ関数への別の呼び出しでは、関数ポインターは同じになりますが、変数のアドレスは変わる可能性があります。

4
Gilles

間違っていなければ、プログラムはメモリの2つの場所にロードされます。 1つ目は、定義済みの関数と変数を含むコンパイル実行可能ファイルです。これは、アプリケーションが占有する最低メモリから始まります。一部の最新のオペレーティングシステムでは、メモリマネージャーが必要に応じてこれらを変換するため、これは0x00000です。コードの2番目の部分は、アプリケーションヒープです。これは、ポインターなどのランタイムに割り当てられた日付が存在する場所であり、ランタイムメモリはメモリ内の異なる場所を持ちます。

4
Robert Humiston

normal関数のアドレスは、命令の開始位置です(vTableが含まれていない場合)。

変数の場合:

  • 静的変数は別の場所に保存されます。
  • パラメータがスタックにプッシュされるまたははレジスタに保持されます。
  • ローカル変数もスタックにプッシュされますまたははレジスタに保持されます。

関数がインライン化または最適化されていない限り。

4
Danny_ds

関数内で宣言された変数は、コード内の場所に割り当てられません自動変数(関数内でローカルに定義された変数)には、スタック内の適切な場所が与えられます関数が呼び出されるときにメモリ、これはコンパイラによってコンパイル時に行われるため、最初の命令のアドレスは変数とは関係ありません実行可能な命令について

2
Amir ElAttar