ポインタ減衰への配列とは何ですか?


384

ポインタ減衰への配列とは何ですか?配列ポインターとの関係はありますか?


73
単項プラス演算子は、「崩壊演算子」として使用することができます:ほとんど知られて考えるとint a[10]; int b(void);、その後、+aint型のポインタで、+b関数ポインタです。参照を受け入れるテンプレートに渡したい場合に便利です。
ヨハネスシャウブ-litb 2009

3
@litb-括弧は同じことを行います(たとえば、(a)はポインタに評価される式である必要があります)。
マイケルバー09/09/22

21
std::decayC ++ 14からは、単項+を超えて配列を減衰させる、それほど曖昧ではない方法になります。
legends2k 2015年

21
JohannesSchaub-litb @この質問は、CおよびC ++の両方のタグ付けされているので、私はもののことを明確にしたい+a+bC ++で法的です、それはCで違法である(C11 6.5.3.3/1「単項のオペランド+または-オペレータがなければなりません算術型 ")
MM

5
@legeそうです。しかし、それは単項+のトリックほど知られていないと思います。私がそれを言った理由は、それが崩壊したからというだけではなく、それが遊ぶためのいくつかの楽しいものだからです;)
ヨハネス・シャウブ-litb

回答:


283

配列はポインタに「減衰」すると言われています。として宣言されたC ++配列はint numbers [5]再ポイントできませんnumbers = 0x5a5aff23。つまり、とは言えません。さらに重要なのは、用語「崩壊」はタイプと次元の喪失を意味します。ディメンション情報(カウント5)を失うことでnumbers崩壊しint*、タイプはint [5]もうありません。減衰が発生しない場合については、こちらをご覧ください。

配列を値で渡す場合、実際に行っているのは、ポインターのコピーです。配列の最初の要素へのポインターがパラメーターにコピーされます(その型は、配列要素の型のポインターでもある必要があります)。これは、アレイの減衰特性により機能します。減衰すると、sizeof配列全体のサイズを提供しなくなります。これは、配列が本質的にポインタになるためです。これが、(他の理由の中でも)参照またはポインタで渡すことが推奨される理由です。

配列を渡す3つの方法1

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

最後の2つは適切なsizeof情報を提供しますが、最初の1つは適切ではありませんが、配列引数が減衰してパラメーターに割り当てられているためです。

1定数Uはコンパイル時に既知である必要があります。


8
最初の値渡しはどうですか?
rlbond 2009

10
by_valueは、配列の最初の要素へのポインタを渡します。関数パラメーターのコンテキストでT a[]は、と同じですT *a。by_pointerは、ポインター値が修飾されることを除いて、同じことを渡しますconst。(配列の最初の要素へのポインターではなく)配列へのポインターを渡したい場合、構文はT (*array)[U]です。
ジョンボード

4
「その配列への明示的なポインタを使用して」-これは正しくありません。場合は、aの配列でありchar、その後、a型でありchar[N]、且つに減衰しますchar*。しかし&aタイプのものでありchar(*)[N]、かつますない崩壊。
Pavel Minaev

5
@FredOverflow:したがって、U変更がある場合、2か所で変更することを覚えておく必要がない場合、またはサイレントバグのリスクがある場合...自律性!
Orbitの軽量レース、2014年

4
「配列を値で渡す場合、実際に行っているのはポインタのコピーです」配列は値、ピリオドで渡すことができないため、これは意味がありません。
juanchopanza

103

配列は基本的にC / C ++のポインターと同じですが、完全ではありません。配列を変換したら:

const int a[] = { 2, 3, 5, 7, 11 };

ポインタへ(キャストなしで機能するため、場合によっては予期せず発生する可能性があります):

const int* p = a;

sizeof配列内の要素をカウントする演算子の機能が失われます。

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

この失われた能力は「減衰」と呼ばれます。

詳細については、配列の崩壊に関するこの記事をご覧ください。


51
配列は基本的にポインタと同じではありません。彼らは完全に異なる動物です。ほとんどのコンテキストでは、配列を扱うことができているかのように、それはポインターたし、ポインタを扱うことができているかのように、それが配列だったが、それは彼らが得るとして近くにあります。
ジョン・ボーデ

20
@ジョン、私の不正確な言葉はご容赦ください。私は長い裏話に夢中になることなく答えに到達しようとしていました、そして「基本的に...しかし、かなりではない」は私が大学で得たのと同じくらい良い説明です。興味のある方なら誰でも、あなたの投票されたコメントからより正確な写真を得ることができると思います。
システムの一時停止

「キャストなしで機能する」とは、型変換について話すときの「暗黙的に起こる」と同じことを意味します
MM

47

これは標準が言うことです(C99 6.3.2.1/3-その他のオペランド-左辺値、配列、および関数指定子):

sizeof演算子または単項&演算子のオペランドである場合、または配列の初期化に使用される文字列リテラルである場合を除いて、型 '' array of type ''の式は、型 '' pointer to type ''は、配列オブジェクトの初期要素を指し、左辺値ではありません。

これは、ほとんどの場合、配列名が式で使用されると、自動的に配列の最初の項目へのポインターに変換されることを意味します。

関数名は同じように機能しますが、関数ポインターの使用ははるかに少なく、より特殊化された方法で行われるため、配列名からポインターへの自動変換ほど混乱は生じません。

C ++標準(4.2配列からポインターへの変換)では、変換要件が緩和されます(強調は次のとおりです)。

「NTの配列」または「Tの未知の境界の配列」タイプの左辺値または右辺値は、「T へのポインタ」タイプの右辺値に変換できます。

そのため、変換はCで常に行われるように行われる必要はありません(これにより、関数をオーバーロードしたり、テンプレートを配列型に一致させたりできます)。

これが、Cで関数のプロトタイプ/定義で配列パラメーターを使用しないようにする理由でもあります(私の意見では、一般的な合意があるかどうかはわかりません)。それらは混乱を引き起こし、とにかくフィクションです-ポインターパラメーターを使用すると、混乱が完全になくなるわけではありませんが、少なくともパラメーター宣言は嘘をつきません。


2
「タイプ「配列のタイプ」を持つ式」が「配列の初期化に使用される文字列リテラル」であるコード行の例は何ですか?
Garrett

4
@ギャレットchar x[] = "Hello";。6つの要素の配列は"Hello"減衰しません。代わりxにサイズ6を取得し、その要素はの要素から初期化され"Hello"ます。
MM

30

「ディケイ」とは、配列型からポインタ型への式の暗黙的な変換を指します。ほとんどのコンテキストでは、コンパイラーが配列式を検出すると、式のタイプを「TのN要素配列」から「Tへのポインター」に変換し、式の値を配列の最初の要素のアドレスに設定します。 。この規則の例外は、配列がsizeofor &演算子のオペランドである場合、または配列が宣言で初期化子として使用されている文字列リテラルである場合です。

次のコードを想定します。

char a[80];
strcpy(a, "This is a test");

aのタイプは「80要素配列のchar」であり、式「This is a test」はタイプ「16要素配列のchar」です(Cでは、C ++文字列リテラルはconst charの配列です)。ただし、への呼び出しではstrcpy()、どちらの式もsizeoforのオペランドではない&ため、それらの型は暗黙的に「charへのポインター」に変換され、それらの値はそれぞれの最初の要素のアドレスに設定されます。strcpy()プロトタイプに見られるように、受け取るのは配列ではなくポインタです。

char *strcpy(char *dest, const char *src);

これは、配列ポインタと同じではありません。例えば:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

どちらも ptr_to_first_elementptr_to_array同じ持っている値を、aのベースアドレス。ただし、以下に示すように、それらは異なるタイプであり、扱いが異なります。

a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]

発現がことを覚えておくa[i]ように解釈される*(a+i)(アレイタイプがポインタ型に変換された場合にのみ動作する)ので、両方a[i]ptr_to_first_element[i]同じように機能します。表現(*ptr_to_array)[i]はと解釈され*(*a+i)ます。表現*ptr_to_array[i]ptr_to_array[i]文脈に応じて、コンパイラの警告やエラーにつながる可能性があり、彼らが評価することを期待しているなら、彼らは間違いなく間違ったことをするでしょうa[i]

sizeof a == sizeof *ptr_to_array == 80

繰り返しになりますが、配列がのオペランドである場合、配列はsizeofポインター型に変換されません。

sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
                                                  is on your platform

ptr_to_first_element charへの単純なポインタです。


1
ではありませんか?(\ 0の長さ14 + 1)"This is a test" is of type "16-element array of char""15-element array of char"
chux-モニカ

16

Cの配列には値がありません。

オブジェクトの値が予期されているがオブジェクトが配列である場合は常に、代わりにそのタイプの最初の要素のアドレスが使用されます pointer to (type of array elements)ます。

関数では、すべてのパラメーターが値で渡されます(配列も例外ではありません)。関数で配列を渡すと、その配列は「ポインタに減衰」します(sic)。配列を他のものと比較すると、再び「ポインタに減衰」します(sic)。...

void foo(int arr[]);

関数fooは配列の値を予期しています。しかし、Cでは、配列に値はありません!だから、foo代わりに配列の最初の要素のアドレスを取得します。

int arr[5];
int *ip = &(arr[1]);
if (arr == ip) { /* something; */ }

上記の比較でarrは、値がないため、ポインタになります。intへのポインタになります。そのポインタは変数と比較できますip

繰り返しますが、配列のインデックス構文では、arrは「ポインタまで減衰」しています。

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

配列がポインタに減衰しないのは、それがsizeof演算子、または&演算子(「アドレス」演算子)のオペランド、または文字配列の初期化に使用される文字列リテラルとしてのみです。


5
「配列には価値がない」-それはどういう意味ですか?もちろん、配列の値...彼らしているオブジェクトを持っている、あなたはなど、彼らに、C ++で、参照をポインタを持っている、とすることができます
パベルMinaev

2
厳密には、「値」はCではオブジェクトのビットを型に応じて解釈するものとして定義されていると思います。配列型でその有用な意味を理解するのに苦労しています。代わりに、ポインターに変換すると言うこともできますが、これは配列の内容を解釈するのではなく、単にその場所を取得するだけです。取得するのは、配列の値ではなく、ポインター(およびアドレス)の値です(これは、「文字列」の定義で使用される「含まれる項目の値のシーケンス」になります)。とは言え、私が「配列の値」と言うのは、1つがポインタを取得することを意味する場合に公平だと思います。
ヨハネスシャウブ-litb 2009

とにかく、オブジェクトの値と式の値( "rvalue"のように)のように、少しあいまいさがあると思います。後者の方法で解釈すると、配列式には必ず値があります。それは、それを右辺値に減衰させた結果であり、ポインター式です。ただし、前者の方法で解釈すると、もちろん配列オブジェクトには意味がありません。
ヨハネスシャウブ-litb 2009

1
少し修正したフレーズの+1。配列の場合、それはトリプレットであって単なるカプレット[場所、タイプ]ではありません。配列の場合、3番目の場所について他に何か考えましたか?何も考えられない。
legends2k 2014

1
@ legends2k:配列の3番目の場所を使用して、それらがカプレットしか持たない特殊なケースにならないようにしました。たぶん、[場所、タイプ、無効 ]の方が良かったでしょう。
pmg 14

8

配列が回転してポイントされているときです;-)

実際、配列をどこかに渡したいが、代わりにポインタが渡された場合(地獄が配列全体を渡すのは誰のためか)、貧しい配列はポインタに腐っていると人々は言う。


うまく言った。ポインタまで減衰しない素敵な配列、または減衰を妨げられる配列は何でしょうか?Cの例を引用できますか?ありがとう。
Unheilig 2014年

@Unheilig、確かに、配列を構造体に真空パックして、構造体を渡すことができます。
マイケルクレリン-ハッカー、2014年

「仕事」とはどういう意味かわかりません。配列を越えてアクセスすることは許可されていませんが、実際に何が起こるかを期待している場合は期待どおりに機能します。その動作(ただし、公式には未定義ですが)は保持されます。
マイケルクレリン-ハッカー、2014年

減衰は、配列をどこにも渡していない多くの状況でも発生します(他の回答で説明されています)。たとえば、a + 1
MM

3

配列減衰とは、配列がパラメーターとして関数に渡された場合、ポインターと同じように扱われる(「減衰する」)ことを意味します。

void do_something(int *array) {
  // We don't know how big array is here, because it's decayed to a pointer.
  printf("%i\n", sizeof(array));  // always prints 4 on a 32-bit machine
}

int main (int argc, char **argv) {
    int a[10];
    int b[20];
    int *c;
    printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
    printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
    printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
    do_something(a);
    do_something(b);
    do_something(c);
}

上記には2つの複雑化または例外があります。

まず、CおよびC ++で多次元配列を処理すると、最初の次元のみが失われます。これは、配列がメモリ内で隣接して配置されるため、コンパイラはメモリのブロックへのオフセットを計算できるように、最初の次元を除くすべてを知っている必要があるためです。

void do_something(int array[][10])
{
    // We don't know how big the first dimension is.
}

int main(int argc, char *argv[]) {
    int a[5][10];
    int b[20][10];
    do_something(a);
    do_something(b);
    return 0;
}

次に、C ++では、テンプレートを使用して配列のサイズを推定できます。Microsoftはこれをstrcpy_sのようなセキュアCRT関数のC ++バージョンに使用します。同様のトリックを使用して、配列内の要素の数を確実に取得できます。


1
減衰は、配列を関数に渡すだけでなく、他の多くの状況で発生します。
MM

0

tl; dr:定義した配列を使用する場合、実際には最初の要素へのポインターを使用します。

したがって:

  • あなたが書くとき、arr[idx]あなたは本当にただ言っているだけです*(arr + idx)いる。
  • 関数は、配列パラメーターを指定しても、実際には配列をパラメーターとして受け取ることはなく、ポインターのみを受け取ります。

このルールの並べ替えの例外:

  • 内の関数に固定長配列を渡すことができます struct
  • sizeof() ポインタのサイズではなく、配列が占めるサイズを指定します。

0

関数の引数として配列を渡すには4つの方法があると思うと、とても大胆になるかもしれません。また、これはあなたの熟読のための短いが実用的なコードです。

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

// test data
// notice native array init with no copy aka "="
// not possible in C
 const char* specimen[]{ __TIME__, __DATE__, __TIMESTAMP__ };

// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array) { 
    // a pointer
    assert(array != nullptr); 
} ;

// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF]) { 
    // decayed to a pointer
    assert( array != nullptr ); 
}

// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
{
   // dealing with native pointer 
    assert( array != nullptr ); 
}

// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
{
    // array is not a pointer here
    // it is (almost) a container
    // most of the std:: lib algorithms 
    // do work on array reference, for example
    // range for requires std::begin() and std::end()
    // on the type passed as range to iterate over
    for (auto && elem : array )
    {
        cout << endl << elem ;
    }
}

int main()
{
     // ONE
     as_pointer(specimen);
     // TWO
     by_value_no_size(specimen);
     // THREE
     pointer_to_array(&specimen);
     // FOUR
     reference_to_array( specimen ) ;
}

また、これはC ++とCの優位性を示していると思うかもしれません。少なくとも参照によって配列を渡すことの参照(しゃれが意図されています)において。

もちろん、ヒープ割り当て、例外、std :: libのない非常に厳密なプロジェクトがあります。C ++のネイティブ配列処理は、ミッションクリティカルな言語機能であると言われるかもしれません。

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