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


134

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を返します。

編集:人々はこの答えを指摘しましたが、構文は少し異なります、すなわちインデックス方法

size = (&arr)[1] - arr;

どちらの質問も有効で、問題へのアプローチが少し異なると思います。多大な助けと徹底した説明をありがとうございました!


13
まあ、それを見つけることはできませんが、厳密に言えばそうです。附属書J.2は明示的に述べています。単項*演算子のオペランドに無効な値があると、未定義の動作になります。ここで&a + 1は有効なオブジェクトをポイントしていないため、無効です。
Eugene Sh。



@AlmaDo構文は少し異なります。つまり、インデックスの部分なので、この質問はそれ自体でも有効であると思いますが、私は間違っているかもしれません。指摘してくれてありがとう!
janojlic

1
ので@janojlicz彼らは、本質的には同じだ(ptr)[x]と同じです*((ptr) + x)
SS

回答:


135

ポインタに1を追加すると、その結果は、ポイントされた型(つまり、配列)のオブジェクトのシーケンス内の次のオブジェクトの場所になります。がオブジェクトをp指す場合、シーケンスの次を指します。の5要素配列(この場合は式)を指す場合、シーケンス内の次の5要素配列を指します。intp + 1intpint&ap + 1int

2つのポインターを減算すると(両方が同じ配列オブジェクトを指している場合、または1つが配列の最後の要素の1つ先を指している場合)、これら2つのポインター間のオブジェクト(配列要素)の数がわかります。

&aはのアドレスを生成しa、タイプint (*)[5](の5要素配列へのポインター)を持っていますint。式は&a + 1次の5要素の配列のアドレスが得られるint次のようにa、また、タイプを有しますint (*)[5]。式*(&a + 1)はの結果を逆参照し、の最後の要素に続く&a + 1最初のアドレスを生成し、typeを持ちます。これは、このコンテキストではtypeの式に「減衰」します。intaint [5]int *

同様に、式aは、配列の最初の要素へのポインターに「減衰」し、型を持ちint *ます。

写真が役立つかもしれません:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

これは、同じストレージの2つのビューです。左側では、それをの5要素配列のintシーケンスとして表示していますが、右側では、それをのシーケンスとして表示していますint。さまざまな表現とその種類も紹介します。

この式の*(&a + 1)結果は未定義の動作になることに注意してください。

...
結果が配列オブジェクトの最後の要素の1つ先を指している場合、それは評価される単項*演算子のオペランドとして使用されません。

C 2011 Online Draft、6.5.6 / 9


13
「使用してはならない」という文章は公式です:C 2018 6.5.6 8.
Eric Postpischil

@EricPostpischil:2018年のパブ前ドラフトへのリンクはありますか(N1570.pdfと同様)?
John Bode

1
@JohnBode:この答えは持っているウェイバックマシンへのリンクを。購入したコピーの公式規格を確認しました。
Eric Postpischil

7
それで、size = (int*)(&a + 1) - a;このコードを書いた場合、完全に有効でしょうか?:o
ギズモ

@Gizmoおそらく要素のタイプを指定する必要があるため、おそらく最初はそれを記述していませんでした。オリジナルはおそらく、さまざまな要素タイプでタイプジェネリックに使用するためのマクロとして定義されて書かれていました。
Leushenko

35

この行は最も重要です:

size = *(&a + 1) - a;

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

Cでのポインタ演算により、配列内の要素数が返され5ます。1つを追加すると&a、の5 int秒後の次の配列へのポインタになりaます。その後、このコードは結果のポインターを逆参照し、aそこから(ポインターに減衰する配列型)を差し引いて、配列内の要素の数を示します。

ポインター演算の仕組みの詳細:

xyzを指しint、値を含むポインタがあるとします(int *)160。から任意の数を引くとxyz、C xyzは、そこから引かれる実際の量が、その数にそれが指す型のサイズを掛けたものであることを指定します。たとえば、5から減算しxyzた場合、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から減算し、を与えることを意味します。&a[5]a+55

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


27

うーん、これはCの初期にはうまくいかなかったと思います。

一度に1つずつ手順を実行します。

  • &a int [5]型のオブジェクトへのポインタを取得します
  • +1 それらの配列があると仮定して、次のそのようなオブジェクトを取得します
  • * そのアドレスを効果的にintへの型ポインタに変換します
  • -a 2つのintポインタを減算して、それらの間のintインスタンスの数を返します。

いくつかのタイプの操作が行われていることを考えると、完全に合法である(つまり、言語弁護士は合法である-実際には機能しない)かどうかはわかりません。たとえば、2つのポインタが同じ配列内の要素を指している場合にのみ、2つのポインタを差し引くことが「許可」されます。*(&a+1)は親配列ですが、別の配列にアクセスして合成されたため、実際にはと同じ配列へのポインタではありませんa。また、配列の最後の要素を越えてポインタを合成することは許可されており、任意のオブジェクトを1つの要素の配列として扱うことができますが*、この合成されたポインタでは、逆参照()の操作は「許可」されません。この場合、動作はありません!

Cの初期の頃(K&R構文、誰か?)は、配列がはるかに速くポインターに減衰したため、*(&a+1)がint **型の次のポインターのアドレスしか返さなかったのではないかと思います。最新のC ++のより厳密な定義により、配列型へのポインターが存在し、配列サイズを認識できるようになります。おそらく、C標準がこれに倣っています。すべてのC関数コードは、引数としてポインタのみを使用するため、技術的に見える違いは最小限です。しかし、私はここで推測しているだけです。

この種の詳細な合法性の質問は、通常、コンパイルされたコードではなく、Cインタープリターまたはlintタイプのツールに適用されます。インタプリタは、実装するランタイム機能が1つ少ないため、2D配列を配列へのポインタの配列として実装する可能性があります。

別の考えられる弱点は、Cコンパイラーが外部配列を調整する可能性があることです。これが5文字(char arr[5])の配列であった場合、プログラムが&a+1「配列の配列」の動作を実行すると想像してください。コンパイラーは、5文字の配列(char arr[][5])の配列が実際には8文字の配列(char arr[][8])の配列として生成されると判断する場合があるため、外側の配列は適切に整列されます。ここで説明するコードは、配列サイズを5ではなく8として報告します。特定のコンパイラがこれを確実に実行するとは言っていませんが、そうする可能性があります。


けっこうだ。しかし、説明が難しい理由から、誰もがsizeof()/ sizeof()を使用していますか?
ジェムテイラー

5
ほとんどの人がします。たとえばsizeof(array)/sizeof(array[0])、配列の要素数を示します。
SSアン、

Cコンパイラーは配列を整列させることができますが、そうした後で配列の型を変更できるとは思いません。アラインメントは、パディングバイトを挿入することにより、より現実的に実装されます。
ケビン

1
ポインターの減算は、同じ配列への2つのポインターに限定されるものではありません。ポインターは、配列の末尾を1つ越えたものでもかまいません。&a+1定義されています。John Bollingerは、*(&a+1)存在しないオブジェクトを逆参照しようとするため、そうではありません。
Eric Postpischil、

5
コンパイラはchar [][5]asを実装できませんchar arr[][8]。配列は、その中で繰り返されるオブジェクトです。パディングはありません。さらに、これはC 2018 6.5.3.4 7の(非規範的)例2を壊します。これは、を使用して配列の要素数を計算できることを示していますsizeof array / sizeof array[0]
Eric Postpischil、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.