2

私は C をいじっていましたが、argv の型を char * から int に変更してみて、どうなるか見てみようと思いました。私はこれを書きました:

#include <stdlib.h>
#include <stdio.h>
int main(int argc, int  argv )
{
        printf("arg is %d \n", argv);
}

このプログラムから本当に奇妙な出力が得られます。どんな引数で実行しても、いつでも乱数を吐き出すようです。出力は次のとおりです。

[14:30:00][maksim]~/learnProg/cDance$ ./dink
arg is -2058142376 
[14:30:01][maksim]~/learnProg/cDance$ ./dink 2141
arg is 2111473256 
[14:30:04][maksim]~/learnProg/cDance$ ./dink 2141
arg is -8005928 

(プログラムは dink と呼ばれます)。どうしたの?これをコンパイルするとき、C は何をしますか? double や構造体など、int 以外のデータ型を使用するとどうなりますか?

4

7 に答える 7

4

他の人が指摘したように、動作は未定義です (そのため、何かが起こる可能性があります)。

ただし、3 つの「典型的な」動作を見てみましょう。引数を渡す 3 つの一般的な方法は次のとおりです。

  • スタック上
  • 汎用レジスター
  • 特殊目的レジスター

Intel x86 システムでは、ほとんどの場合、最初の方法が使用されます (ただし、2 番目または 3 番目の方法が使用される場合もあります)。MIPS ベースのプロセッサは、主に 2 番目を使用します。

システムが 1 つ以上のスタックを使用する場合、通常の呼び出し方法は次のとおりです。

  • 呼び出し元 ( を呼び出す OS 提供のルーチンmain) で、通常は右から左、つまり逆の順序で引数をプッシュします。スタック プッシュは通常 (常にではありません) *--sp = value;C のように見え、スタック ポインターは上位アドレスから下降します。
  • ターゲット関数を呼び出します ( main)
  • ターゲット関数で、「スタック」または「パラメーター スタック」または「現在のスレッド スタック」またはシステムが使用するものからパラメーターを取得します。それらは逆の順序でプッシュされたため、 、 などのアドレスにありますsp[0]sp[1]呼び出しメカニズムがパラメーター受け渡しメカニズムと同じスタックを使用する場合、インデックスは 1 または 2 またはそれ以上 (sp[2]最初の引数であり、インスタンスでありsp[3]、2 番目である)。

この場合、argcはおそらく正しく出力されますがargv、発信者がプッシュしたものを誤って解釈し、奇妙な外観のint. 基礎となるシステムが十分に洗練されている (型をチェックしている) 場合、呼び出し元が type の値をプッシュしたchar **が、あなたが type の値にアクセスしていることを検出し、int何らかの実行時エラーが発生する可能性があります。ほとんどのシステムは単純に、タイプチェックをスキップして、できるだけ早く間違った答えを返すことを好みます。したがって、奇妙に見える が得られますが、int実際には、呼び出し元が渡そうとした実際のポインター値に基づいています (少なくとも部分的には以下を参照)。

システムが汎用レジスタを使用する場合 (スタックを使用する代わりに、またはスタックを使用する前に、GPR を使用するシステムは、多くのパラメーターを使用する場合にスタックにフォールバックすることが多く、すべての可変関数 (つまり、機能を使用するもの) にそれらを使用する場合があり<stdarg.h>ます)。呼び出しメソッドは次のようになります。

  • 呼び出し側で、引数 ( int argcvalue とchar **argvvalue) を最初の 2 つの引数レジスタに移動します (たとえば、SPARC では%o0and 、MIPS では and ) 。%o1$a0$a1
  • ターゲット関数を呼び出す
  • ターゲット関数で、引数レジスタの値にアクセスします

この場合、コードは通常、スタックベースのシステムと同じように動作します。レジスタ内の引数はメモリ内の引数よりも必要な CPU サイクルが少ない傾向があるため、実行速度が向上します。(これが、一部の Intel コンパイラがレジスタで 1 つまたは 2 つの引数を渡す場合がある理由です。)

ただし、システムが特殊目的のレジスタを使用している場合は、明らかに新しい動作が得られます。浮動小数点値がレジスターに格納されるとしましょうf(一部の SPARC システムに当てはまります。x86 には代わりに MMX および SSE レジスターがあります)。ポインター値はaレジスターに入れられます (680x0 CPU のように)。整数値はdレジスタに格納されます (680x0 ですが、実際にはほとんどの 680x0 システムは単に「スタック」を使用しますが、レジスタを使用するシステムがあると仮定しましょう)。今回、 Thing 呼び出しmainは 1 つの整数argcと 1 つのポインターを渡す必要があるため、次argvのようにします。

  • 整数引数argcをデータ レジスタに移動するd0
  • ポインタ引数argvをポインタ レジスタに移動するa0
  • 電話main

では、コンパイラに 2 つの整数main()引数を期待するように指示しました。これらの引数はそれぞれレジスタおよびに到着します。CPUレジスタには何がありますか? 誰が知っていますか、呼び出したものは呼び出しの直前にそれを設定しませんでした. 最後に何らかの値を埋め込んだ人から、それが持っている値は何でもあります。register にあるため、値は意図した に関連付けられなくなりました。d0d1d1mainargva0

スタックまたは GPR ベースの呼び出しシステムを使用している場合でも、考慮すべき点が他にもいくつかあります。

  • ポインターが 64 ビットで、プレーンなints が 32 ビットしかない場合はどうなるでしょうか? この場合、呼び出し元は 64 ビット値をプッシュするか、64 ビット値をパラメータ レジスタに書き込みます。しかしmain、32ビットしか見えません。実際に与えられたものの半分が表示されます。
  • ポインタが 32 ビットでプレーンなints が 64 ビットの場合はどうなるでしょうか? 確かに、これは通常とは異なる実装ですが、ここでは、32 しか提供されていない値の 64 ビットすべてを見ることになります。「余分な」32 ビットはすべてゼロである可能性があります (これは、GPR のパラメーターでは一般的です)、またはの呼び出し元が registerに入力されd1たときにregister を検査する場合と同様に、32 ビットの無関係な値である可能性があります。maina0
  • そしてもちろん、32 ビットと 64 ビットだけが可能なサイズであるということはありません。IBM AS/400 システムでは、ポインターの長さはなんと 128 ビット (16 バイトのタグ付きポインター) であり、実行時に広範な型チェックが行われます。これらのマシンは、単に高速であるだけでなく、コードが正しいことを確認するために動作します。

もう1つの注目すべき可能性があります。同様の C++ コードを ( 以外の関数でmain) ビルドすると、通常、リンクに失敗します。その理由は、C++ コンパイラがオーバーロードされた関数を処理するために「名前マングリング」と呼ばれる手法を使用することが多いためです。1つと 1 つの引数fを取り、返すという名前の関数は、リンク時のシンボル を生成します。2 つの を受け取って返すという名前の関数は、代わりにリンク時のシンボルを生成します。これを行う C コンパイラは見たことがありませんが、可能でした。この場合、コンパイラはリンク時に、プログラムが定義されているかどうかをチェックします。intchar **intZ1fiPPCfintintZ1fiiZ4mainippC—<code>int main(int, char **)—そうであれば、それらの引数を提供する呼び出し元にリンクします。またはZ4mainv—<code>int main(void)—をチェックし、その場合、引数を提供しない呼び出し元のリンクをチェックします。どちらの関数も見つからない場合、リンカーは、間違った関数を記述したことを検出しmain、実行可能ファイルをまったく生成しない可能性があります!

于 2013-05-27T19:35:09.303 に答える
3

未定義の動作が発生します。つまり、が起こっても正当です。 main次のように宣言する必要があります。

int main(void)

またはとして:

int main(int arg, char** argv)

または実装で指定された形式として。

ISO C99 標準のセクション J.2 から:

次の状況では、動作は未定義です。

...

  • mainホスト環境のプログラムは、指定された形式 (5.1.2.2.1) のいずれかを使用して名前が付けられた関数を定義しません。
于 2013-05-27T18:42:04.030 に答える
2

argv文字列へのポインターの配列へのポインターとしてプログラムに渡されます。

嘘をつき、それが であるとコンパイラに伝えるとint、ポインタのバイトは として解釈され、intメモリアドレスが得られます。(64 ビット システムでは、おそらくクラッシュするでしょう)

のふりをするとfloat、コンパイラはおそらくそれらのバイト/ビットを IEE-754 でエンコードされた浮動小数点値として解釈し、別の奇妙な数値になります。(正確に何が起こるかは、呼び出し規約によって異なります)

ポインターと同じ幅ではない型であるふりをすると、おそらくクラッシュします。

物語の教訓は、

C は、指定したとおりに実行します。物事をどのように解釈するかを伝えるのはあなた次第です。

于 2013-05-27T18:36:15.393 に答える
0

C の main() 関数は、引数 count の整数と、char の配列へのポインターを受け取ります。

出力は、このポインターに含まれるメモリアドレスです。他の変数型にキャストすると、それらにも「ゴミ」が含まれます。

通常の状況では、可能であればポインターのキャストは避けるべきです。

于 2013-05-27T18:40:56.743 に答える
0

良い...

Argv は配列です。C では、配列は単なるポインターです。ポインターは、内部的にはメモリ位置の単なる整数です。したがって、あなたが見た数字はメモリ内の場所です。(ネガは署名されていないためだと思います)

于 2013-05-27T18:37:02.503 に答える
0

Let's first understand what exactly argv is.

Consider the standard main() format. It is int main(int argc, char *argv[]) Here argv is an array of character pointers. Since name of an array is a constant pointer to it's first member, we will say argv is a pointer to it's first member. i.e. argv is pointer to character pointer.

Now please note name doesn't matter here. It can be anything beside argv. What matters is second argument to main() is a pointer to character pointer. i.e. The second argument is pointer to pointer to character.

So when program starts execution, a memory address is passed as a second argument to main() which is an address of another pointer. And that 'another' pointer is a memory address of very first character of very first argument. And that argument happens to be program's name.

So when you say int main(int argc, int argv ) you are casting an address in int value. If sizeof(int) == sizeof(int *) then that's not a problem at all. The value won't be demoted in that case.

Now when you say printf("arg is %d \n", argv); you are simply printing that address. That's it! No matter what are your arguments given to a program that address is a random value. That's why you are getting the random no.s which are actually the addresses of first member of argv array. i.e. The no. printed is an address of program name which in turn is an address of it's first char. (Since program name is again an array so is a constant pointer to it's first member. i.e. the very first character)

To verify this add this line to you code snippet:

printf("%c\n", **(char **)argv);  

You will see . being printed which was indeed the very first character of very first argument ./dink

于 2013-05-27T19:37:00.480 に答える