数値の二乗を計算するこの方法を理解できません


135

数値の二乗を計算する関数を見つけました:

int p(int n) {
    int a[n]; //works on C99 and above
    return (&a)[n] - a;
}

n 2の値を返します。質問は、それをどのように行うのですか?少しテストの後、私は間ことが判明(&a)[k]して(&a)[k+1]いますsizeof(a)/ sizeof(int)。何故ですか?


6
この情報を見つけた場所へのリンクはありますか?
R Sahu

4
int p(n)?それでもコンパイルできますか?
barak manos 2015

78
これは

26
以上:int q(int n) { return sizeof (char [n][n]); }
ouah

17
@ouahこの質問と仮定するとを意味しcodegolf.stackexchange.com/a/43262/967私は使用はしていない理由sizeofの文字を保存することでした。他のすべての人:これは意図的にあいまいなコードであり、未定義の動作です。@ ouahの答えは正しいです。
ecatmur

回答:


117

明らかにハックですが、*演算子を使用せずに数値を二乗する方法です(これはコーディングコンテストの要件でした)。

(&a)[n] 

int場所へのポインタと同等

(a + sizeof(a[n])*n)

したがって、式全体は

  (&a)[n] -a 

= (a + sizeof(a[n])*n -a) /sizeof(int)

= sizeof(a[n])*n / sizeof(int)
= sizeof(int) * n * n / sizeof(int)
= n * n

11
そして、あなたが明らかに示唆しているように、私は明示的にする必要性を感じていますが、それはせいぜい構文のハックです。乗算演算はまだそこにあります。避けられているのは演算子だけです。
トミー

私はそれが背後で何が起こるかを得ましたが、私の本当の質問は、(&a)[k]がa + k * sizeof(a)/ sizeof(int)と同じアドレスにある理由です
Emanuel

33
古いコジャーとして、コンパイラーがコンパイル時に不明な場合の(&a)オブジェクトへのポインターとしてコンパイラーが処理できるという事実に驚かされます。Cは以前は単純な言語n*sizeof(int)n
Floris

これはかなり巧妙なハックですが、プロダクションコードでは見られないものです(うまくいけば)。
John Odom

14
余談ですが、それはUBでもあります。これは、基礎となる配列の要素も過去も指し示さないようにポインターを増分するためです。
Deduplicator

86

このハックを理解するには、最初にポインターの違いを理解する必要があります。つまり、同じ配列の要素を指す2つのポインターを減算するとどうなるでしょうか。

1つのポインターが別のポインターから差し引かれると、結果はポインター間の距離(配列要素で測定)になります。したがって、をp指すとa[i]q指す場合a[j]、はとp - q等しくなりi - jます。

C11:6.5.6加算演算子(p9):

2つのポインターが減算されると、両方が同じ配列オブジェクトの要素を指すか、配列オブジェクトの最後の要素の1 つ後を指します。結果は、2つの配列要素の添え字の差です。[...]。
換言すれば、式場合PQ点に、それぞれ、i目及びj配列オブジェクトの目の要素、式が(P)-(Q)値を有するi−jタイプのオブジェクトの値の適合を提供しますptrdiff_t

今、私はあなたがポインタへの配列名の変換a、配列の最初の要素へのポインタへの変換を知っていることを期待していますa&aメモリブロック全体のアドレスですa。つまり、配列のアドレスです。以下の図は、理解するのに役立ちます(詳細な説明についてはこの回答をお読みください):

ここに画像の説明を入力してください

これは、なぜa&aが同じアドレスを持ち、どのよう(&a)[i]にi 番目のアレイのアドレス(と同じサイズa)であるかを理解するのに役立ちます。

したがって、ステートメント

return (&a)[n] - a; 

に相当

return (&a)[n] - (&a)[0];  

この違いにより、ポインタ(&a)[n]との間の要素数(&a)[0]nわかりますn int。これは、各要素の配列です。したがって、配列要素の総数はn*n= n2です。


注意:

C11:6.5.6加算演算子(p9):

2つのポインターが差し引かれると、両方とも同じ配列オブジェクトの要素を指すか、配列オブジェクトの最後の要素の1つ前を指します。結果は、2つの配列要素の添え字の差です。結果のサイズは実装定義であり、その型(符号付き整数型)はヘッダーでptrdiff_t定義され<stddef.h>ます。結果がそのタイプのオブジェクトで表現できない場合、動作は未定義です。

(&a)[n]同じ配列オブジェクトの要素も、配列オブジェクトの最後の要素の1つ後もポイントしないため、未定義の動作(&a)[n] - aを呼び出します

また、関数の戻り値の型をに変更することをお勧めpptrdiff_tます。


「両方とも同じ配列オブジェクトの要素を指す」-これは、この「ハック」が結局のところUBではないかどうかという疑問を投げかけます。ポインター算術式は、存在しないオブジェクトの仮定の終わりを参照しています:これは許可されていますか?
マーティンバ

要約すると、aはn要素の配列のアドレスなので、&a [0]はこの配列の最初の要素のアドレスで、aと同じです。さらに、&a [k]は、kに関係なく、常にn要素の配列のアドレスと見なされます。また、&a [1..n]もベクトルであるため、その要素の「位置」は連続しています。最初の要素は位置xにあり、2番目は位置x +(nであるベクトルaの要素の数)にあります。私は正しいですか?また、これはヒープスペースなので、同じn要素の新しいベクトルを割り当てると、そのアドレスは(&a)[1]と同じになりますか?
エマニュエル2015年

1
@エマニュエル; array &a[k]kth番目の要素のアドレスですa。それは(&a)[k]常にk要素の配列のアドレスと見なされます。だから、最初の要素は位置にあるa(又は&a、第2の位置にある)a(配列の要素数+ aであるn(配列要素のサイズ)*)など。また、可変長配列のメモリはヒープではなくスタックに割り当てられることに注意してください。
2015年

@MartinBa; これも許可されますか?いいえ、できません。そのUB。編集を参照してください。
2015年

1
@haccks質問の性質とあなたのニックネームの間の良い一致
Dimitar Tsonev

35

aの(変数)配列ですn int

&aの(変数)配列へのポインタですn int

(&a)[1]最後の配列要素のint1つ後のポインタですint。このポインタはのn int後の要素&a[0]です。

(&a)[2]2つの配列の最後の配列要素のint1つint前のポインタです。このポインタはの2 * n int後の要素&a[0]です。

(&a)[n]配列の最後の配列要素のint1つint前のポインタですn。このポインタはのn * n int後の要素&a[0]です。&a[0]または減算するだけでa、あなたは持っていnます。

もちろん、これは(&a)[n]、配列内または最後の配列要素の1つ後をポイントしない(ポインター演算のC規則で要求される)マシンで機能する場合でも、技術的には未定義の動作です。


まあ、私はそれを得たが、なぜこれがCで起こるのか?これの背後にある論理は何ですか?
2015年

@Emanuelその厳密な答えは、ポインタの計算が距離を測定するのに役立つことです(通常は配列で)。[n]構文は配列を宣言し、配列はポインタに分解します。この結果、3つの個別に役立つもの。
トミー

1
@Emanuel なぜ誰かがこれを行うのかと尋ねる場合、アクションのUBの性質によるものではなく、理由はほとんどありません。そして、それは注目に値する(&a)[n]タイプでありint[n]、そしてこととして表現int*の説明では明確ではなかった場合には、それらの最初のエレメントのアドレスとして発現アレイによる。
WhozCraig、2015年

いいえ、私はなぜ誰かがこれを行うのかを意味していませんでした。つまり、この状況でC標準がこのように動作する理由を意味しました。
エマニュエル

1
@Emanuel Pointer Arithmetic(およびこの場合、そのトピックの副章:ポインター差分)。このサイトでは、ググるだけでなく、質問と回答を読むこともできます。これには多くの有用な利点があり、適切に使用した場合は標準で具体的に定義されています。それを完全に理解するには、リストしたコードのがどのように作成されているかを理解する必要があります。
WhozCraig、2015年

12

同じ配列の2つの要素を指す2つのポインターがある場合、その違いにより、これらのポインター間の要素の数が得られます。たとえば、このコードスニペットは2を出力します。

int a[10];

int *p1 = &a[1];
int *p2 = &a[3];

printf( "%d\n", p2 - p1 ); 

さて、表現を考えてみましょう

(&a)[n] - a;

この式でaはタイプがint *あり、最初の要素を指します。

式に&aはタイプがint ( * )[n]あり、イメージ化された2次元配列の最初の行を指します。aタイプは異なりますが、その値はの値と一致します。

( &a )[n]

は、この画像化された2次元配列のn番目の要素であり、タイプが、int[n]つまり、画像化された配列のn番目の行です。式(&a)[n] - aでは、最初の要素のアドレスに変換され、タイプは「int *」です。

だから、間 (&a)[n]およびan個の要素のn個の行があります。したがって、差はに等しくなりn * nます。


それで、すべての配列の後ろにサイズn * nの行列がありますか?
2015年

@Emanuelこれら2つのポインタの間に、nxn要素の行列があります。そして、ポインターの違いは、ポインターの間にある要素の数であるn * nに等しい値を与えます。
Vlad、モスクワ発

しかし、なぜこのサイズの行列がn * n遅れているのですか?Cで何か用途はありますか?つまり、Cがサイズnの配列を「割り当て」たようなものですが、知らないうちに?その場合、使用できますか?そうでなければ、なぜこのマトリックスが形成されるのでしょうか(つまり、そこに存在するための目的が必要です)。
エマニュエル

2
@Emanuel-このマトリックスは、この場合のポインター演算の動作の説明にすぎません。このマトリックスは割り当てられておらず、使用できません。すでに何度か述べられているように、1)このコードスニペットは実際には使用されないハックです。2)このハックを理解するには、ポインタ演算の仕組みを学ぶ必要があります。
void_ptr 2015年

@Emanuelポインタ演算について説明します。Expreesion(&a)[n]は、ポインター演算による、イメージ化された2次元配列のn要素へのポインターです。
モスクワ出身のヴラド氏

4
Expression     | Value                | Explanation
a              | a                    | point to array of int elements
a[n]           | a + n*sizeof(int)    | refer to n-th element in array of int elements
-------------------------------------------------------------------------------------------------
&a             | a                    | point to array of (n int elements array)
(&a)[n]        | a + n*sizeof(int[n]) | refer to n-th element in array of (n int elements array)
-------------------------------------------------------------------------------------------------
sizeof(int[n]) | n * sizeof(int)      | int[n] is a type of n-int-element array

したがって、

  1. タイプ(&a)[n]int[n]ポインタです
  2. タイプaintポインタです

これで、式(&a)[n]-aはポインターの減算を実行します。

  (&a)[n]-a
= ((a + n*sizeof(int[n])) - a) / sizeof(int)
= (n * sizeof(int[n])) / sizeof(int)
= (n * n * sizeof(int)) / sizeof(int)
= n * n
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.