web-dev-qa-db-ja.com

このコードは、sizeof()を使用せずに配列サイズをどのように決定しますか?

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;

そのため、両方の質問が有効であり、問​​題に対するアプローチが少し異なると思います。多大な助けと徹底的な説明をありがとう!

130
janojlic

ポインターに1を追加すると、結果は、ポイント先の型のオブジェクトのシーケンス(つまり、配列)内の次のオブジェクトの位置になります。 pintオブジェクトを指す場合、_p + 1_はシーケンス内の次のintを指します。 pintの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

133
John Bode

この行は最も重要です:

_size = *(&a + 1) - a;
_

ご覧のとおり、最初にaのアドレスを取得し、それに追加します。次に、そのポインターを逆参照し、aの元の値を減算します。

Cのポインター演算により、配列内の要素数、または_5_が返されます。 1つと_&a_を追加すると、intの後にある5つのasの次の配列へのポインターになります。その後、このコードは結果のポインターを逆参照し、その中から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_を与えることを意味します。

これは未定義の動作であり、いかなる状況でも使用すべきではないことに注意してください。これの動作がすべてのプラットフォームで一貫していることを期待しないでください。また、本番プログラムで使用しないでください。

34
S.S. Anne

うーん、私はこれが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として報告します。特定のコンパイラーがこれを確実に行うとは言いませんが、そうするかもしれません。

27
Gem Taylor