Cのインタビューの質問をいくつか調べてみると、「sizeof演算子を使用せずにCで配列のサイズを見つける方法」という質問と、次の解決策が見つかりました。動作しますが、その理由はわかりません。
#include <stdio.h>
int main() {
int a[] = {100, 200, 300, 400, 500};
int size = 0;
size = *(&a + 1) - a;
printf("%d\n", size);
return 0;
}
予想どおり、5を返します。
編集:人々が指摘した this answer、しかし構文は少し異なります、すなわちインデックス方法
size = (&arr)[1] - arr;
そのため、両方の質問が有効であり、問題に対するアプローチが少し異なると思います。多大な助けと徹底的な説明をありがとう!
ポインターに1を追加すると、結果は、ポイント先の型のオブジェクトのシーケンス(つまり、配列)内の次のオブジェクトの位置になります。 p
がint
オブジェクトを指す場合、_p + 1
_はシーケンス内の次のint
を指します。 p
がint
の5要素配列(この場合、式_&a
_)を指す場合、_p + 1
_は次の5-element array of int
inシーケンス。
2つのポインターを減算すると(両方が同じ配列オブジェクトを指すか、一方が配列の最後の要素を指す場合)、これら2つのポインター間のオブジェクト(配列要素)の数が得られます。
式_&a
_は、a
のアドレスを生成し、タイプint (*)[5]
(int
の5要素配列へのポインター)を持ちます。式_&a + 1
_は、int
に続くa
の次の5要素配列のアドレスを生成し、タイプint (*)[5]
も持ちます。式*(&a + 1)
は_&a + 1
_の結果を逆参照し、int
の最後の要素に続く最初のa
のアドレスを生成し、このコンテキストではタイプ_int [5]
_を持ちますタイプが_int *
_の式に減衰します。
同様に、式a
は、配列の最初の要素へのポインターに「減衰」し、タイプ_int *
_を持ちます。
写真が役立つ場合があります:
_int [5] int (*)[5] int int *
+---+ +---+
| | <- &a | | <- a
| - | +---+
| | | | <- a + 1
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
+---+ +---+
| | <- &a + 1 | | <- *(&a + 1)
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
+---+ +---+
_
これは、同じストレージの2つのビューです。左側では、int
の5要素配列のシーケンスとして表示していますが、右側では、int
のシーケンスとして表示しています。また、さまざまな表現とそのタイプも示します。
式*(&a + 1)
の結果は未定義の動作になります。
...
結果が配列オブジェクトの最後の要素の1つを指す場合、評価される単項*演算子のオペランドとして使用されません。
C 2011オンラインドラフト 、6.5.6/9
この行は最も重要です:
_size = *(&a + 1) - a;
_
ご覧のとおり、最初にa
のアドレスを取得し、それに追加します。次に、そのポインターを逆参照し、a
の元の値を減算します。
Cのポインター演算により、配列内の要素数、または_5
_が返されます。 1つと_&a
_を追加すると、int
の後にある5つのa
sの次の配列へのポインターになります。その後、このコードは結果のポインターを逆参照し、その中からa
(ポインターに減衰した配列型)を減算し、配列内の要素の数を与えます。
ポインター演算の仕組みの詳細:
xyz
型を指し、値_(int *)160
_を含むポインターint
があるとします。 xyz
から任意の数値を減算すると、Cは、xyz
から減算される実際の量が、その数値が指す型のサイズの倍であることを指定します。たとえば、xyz
から_5
_を引いた場合、ポインター演算が適用されなかった場合、結果のxyz
の値はxyz - (sizeof(*xyz) * 5)
になります。
a
は_5
_ int
型の配列であるため、結果の値は5になります。ただし、これはポインターでは機能せず、配列でのみ機能します。ポインターでこれを試みると、結果は常に_1
_になります。
これは、アドレスとこれがどのように定義されていないかを示す小さな例です。左側にはアドレスが表示されます:
_a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced
_
これは、コードが_&a[5]
_(または_a+5
_)からa
を減算し、_5
_を与えることを意味します。
これは未定義の動作であり、いかなる状況でも使用すべきではないことに注意してください。これの動作がすべてのプラットフォームで一貫していることを期待しないでください。また、本番プログラムで使用しないでください。
うーん、私はこれがCの初期にはうまくいかなかったと思う。しかしそれは賢い。
一度に1つの手順を実行します。
&a
_は、int [5]型のオブジェクトへのポインターを取得します+1
_は、それらの配列があると仮定して、次のそのようなオブジェクトを取得します*
_は、そのアドレスをintへの型ポインターに効果的に変換します-a
_は、2つのintポインターを減算し、それらの間のintインスタンスのカウントを返します。いくつかの型操作が行われていることを考えると、それが完全に合法であるかどうかはわかりません(これは言語弁護士が合法であることを意味します-実際には機能しません)。たとえば、2つのポインターが同じ配列内の要素を指している場合、それらを減算することは「許可」されています。 *(&a+1)
は、親配列ではあるが別の配列にアクセスすることで合成されたため、実際にはa
と同じ配列へのポインターではありません。また、配列の最後の要素を超えてポインターを合成でき、任意のオブジェクトを1つの要素の配列として扱うことができますが、この合成では逆参照(_*
_)の操作は許可されません。ポインタ、この場合は動作しませんが!
C(K&R構文、誰か?)の初期には、配列がより速くポインターに崩壊したため、*(&a+1)
はint **型の次のポインターのアドレスのみを返す可能性があります。 。現代のC++のより厳密な定義により、配列型へのポインタが確実に存在し、配列サイズを認識できるようになり、おそらくC規格がそれに追随するようになりました。すべてのC関数コードはポインターを引数としてのみ使用するため、技術的に目に見える違いは最小限です。しかし、私はここで推測しているだけです。
この種の詳細な合法性の質問は通常、コンパイルされたコードではなく、Cインタープリターまたはlintタイプのツールに適用されます。インタプリタは、2D配列を配列へのポインタの配列として実装する場合があります。実装するランタイム機能が1つ少ないためです。その場合、+ 1の逆参照は致命的であり、機能していても間違った答えを返します。
別の潜在的な弱点は、Cコンパイラが外部配列を整列させる可能性があることです。これが5文字の配列(_char arr[5]
_)であった場合、プログラムが_&a+1
_を実行すると、「配列の配列」動作が呼び出されます。コンパイラーは、5文字の配列の配列(_char arr[][5]
_)が実際に8文字の配列の配列(_char arr[][8]
_)として生成されると判断する場合があります。ここで説明しているコードは、配列サイズを5ではなく8として報告します。特定のコンパイラーがこれを確実に行うとは言いませんが、そうするかもしれません。