スタック配列のポインターへのポインターにアクセスできないのはなぜですか?


35

次のコードを見てください。これは、配列をとしてchar**関数に渡そうとします。

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

私はそれが明示的にキャストしてコンパイルするために得ることができるという事実&test2char**、このコードが間違っていることをすでにヒント。

それでも、私はそれについて正確に何が間違っているのだろうと思っています。動的に割り当てられた配列へのポインターへのポインターを渡すことはできますが、スタック上の配列のポインターへのポインターを渡すことはできません。もちろん、次のように最初に配列を一時変数に割り当てることで、問題を簡単に回避できます。

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

それはキャストに動作しない理由それでも、缶誰かが私に説明char[256]char**直接?

回答:


29

testポインタではないからです。

&testchar (*)[256]互換性のないタイプの配列へのポインタを取得しますchar**(配列はポインタではないため)。これにより、未定義の動作が発生します。


3
しかし、なぜCコンパイラは型の何かを渡すことを許可char (*)[256]するのchar**ですか?
ComFreek

@ComFreek私は最大の警告と-Werrorでそれを許可しないと思います。
PiRocks

@ComFreek:実際には許可されていません。明示的ににキャストすることで、コンパイラにそれを受け入れるように強制する必要がありchar**ます。そのキャストがないと、コンパイルできません。
アンドレアス

38

testポインタで&testはなく配列であり、配列へのポインタです。ポインタへのポインタではありません。

配列はポインターであると言われたかもしれませんが、これは誤りです。配列の名前は、オブジェクト全体(すべての要素)の名前です。最初の要素へのポインタではありません。ほとんどの式では、配列は最初の要素へのポインターに自動的に変換されます。これは便利なことが多いので便利です。ただし、このルールには3つの例外があります。

  • 配列はのオペランドですsizeof
  • 配列はのオペランドです&
  • 配列は、配列の初期化に使用される文字列リテラルです。

では&test、配列はのオペランドで&あるため、自動変換は行われません。の結果&testは、256 ではなく、char型を持つ配列へのポインタです。char (*)[256]char **

charfrom へのポインタへのポインタを取得するにはtest、最初にへのポインタを作成する必要がありますchar。例えば:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

これについて考えるもう1つの方法testは、オブジェクト全体に256の配列全体という名前を付けることcharです。&testポインタを指定しないため、にはアドレスを取得できるポインタがないため、これはを生成できませんchar **。を作成するにはchar **、最初にが必要char *です。


1
この3つの例外のリストはすべて網羅されていますか?
ルスラン

8
@ルスラン:はい、C 2018年6.3.2.1に基づきます
Eric Postpischil

ああ、そしてC11には、とに_Alignof加えて言及された演算子もsizeofありました&。なぜ彼らがそれを削除したのだろう…
ルスラン

@Ruslan:それは間違いだったので削除されました。_Alignofは、型名のみをオペランドとして受け入れ、配列またはその他のオブジェクトをオペランドとして受け入れません。(理由はわかりません。構文的にも文法的にものように見えますが、そうsizeofではありません。)
Eric Postpischil

6

タイプはtest2ですchar *。だから、のタイプが&test2あることになるchar **パラメータの種類と互換性があるxprintchar()
タイプはtestですchar [256]。だから、のタイプの&test意志もchar (*)[256]ありませんパラメータの型と互換性xprintchar()

私はあなたのアドレスの面で違い見せしましょうtestとしますtest2

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

出力:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

ここで注意すべき点:

出力(メモリアドレス)test2&test2[0]数値的に同じであり、その種類もある同じですchar *
しかし、test2および&test2は異なるアドレスであり、それらのタイプも異なります。
タイプはtest2ですchar *
タイプは&test2ですchar **

x = &test2
*x = test2
(*x)[0] = test2[0] 

test&testおよびの出力(メモリアドレス)&test[0]数値的には同じですが、タイプが異なります。
タイプはtestですchar [256]
タイプは&testですchar (*) [256]
タイプは&test[0]ですchar *

出力が示すように&testと同じ&test[0]です。

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

したがって、セグメンテーション違反が発生しています。


3

はポインタで&testはないため、ポインタへのポインタにはアクセスできません。これは配列です。

配列のアドレスを取得し、配列と配列のアドレスをにキャストして(void *)比較すると、それらは(可能なポインターpedantryを除いて)同等になります。

あなたが本当にやっていることはこれに似ています(ここでも、厳密なエイリアスを禁止しています):

putchar(**(char **)test);

これは明らかに間違っています。


3

コードはの引数xprintcharを含むメモリを指すことを期待しています(char *)

最初の呼び出しでは、それtest2は使用されたストレージを指し、実際にはを指す値で(char *)あり、後者は割り当てられたメモリを指します。

ただし、2番目の呼び出しでは、そのような(char *)値が格納される場所がないため、そのようなメモリを指すことはできません。キャスト(char **)追加したが(変換についてはコンパイルエラーを削除しているだろう(char *)(char **))が、それはストレージが含まれて空中から出現することはないだろう(char *)テストの最初の文字を指すように初期化。Cでのポインターのキャストは、ポインターの実際の値を変更しません。

必要なものを取得するには、明示的に行う必要があります。

char *tempptr = &temp;
printchar(&tempptr);

私はあなたの例がはるかに大きなコードの断片であると思います。例として、次の呼び出しで次の文字が出力されるように、渡された値が指す値printcharをインクリメントしたい場合があり(char *)ますx。そうでない場合は、(char *)印刷する文字へのポインティングだけを渡したり、文字自体を渡したりしてみませんか?


いい答えです; これをまっすぐに保つための最も簡単な方法は、配列のアドレスを保持するCオブジェクト(つまり、を取得するためにアドレスを取ることができるポインターオブジェクト)があるかどうかを考えることであることに同意しますchar **。配列変数/単にオブジェクトであるアドレスは暗黙的であると、どこに格納されていない、アレイ。他のストレージを指すポインター変数とは異なり、それらにアクセスするための追加の間接レベルはありません。
Peter Cordes

0

どうやら、アドレスをtest取ることはアドレスを取ることと同じですtest[0]

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

それをコンパイルして実行します:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

したがって、セグメンテーション違反の最終的な原因は、このプログラムが絶対アドレスを逆参照しようとすることです 0x42(別名'B')であり、これにはプログラムが読み取る権限がありません。

コンパイラ/マシンが異なるとアドレスは異なります:オンラインで試してください!、しかし、何らかの理由で、これはまだ得られます:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

ただし、セグメンテーション違反の原因となるアドレスは非常に異なる場合があります。

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]

1
のアドレスをtest取ることは、のアドレスを取ることと同じではありませんtest[0]。前者には型char (*)[256]があり、後者には型がありchar *ます。これらは互換性がなく、C標準では異なる表現を持つことが許可されています。
Eric Postpischil

でポインタをフォーマットするときは%p、(これvoid *も互換性と表現の理由から)に変換する必要があります。
Eric Postpischil

1
printchar(&test);クラッシュする可能性がありますが、動作はC標準で定義されておらず、他の状況で他の動作が観察される場合があります。
Eric Postpischil

「したがって、セグメンテーションフォールトの最終的な原因は、このプログラムが絶対アドレス0x42(「B」とも呼ばれる)を逆参照しようとすることであり、これはおそらくOSによって占有されています。」:読み取りを試みているセグメントフォールトがある場合場所、つまり、そこに何もマップされていないことを意味し、OSによって占有されているわけではありません。(ただし、読み取り権限のない実行専用としてマップされているものがある可能性はありますが、そうではありません。)
Eric Postpischil

1
&test == &test[0]型に互換性がないため、C 2018 6.5.9 2の制約に違反しています。C標準では、この違反を診断するための実装が必要であり、その結果の動作はC標準では定義されていません。つまり、コンパイラはそれらを等しいと評価するコードを生成する可能性がありますが、別のコンパイラは生成しない可能性があります。
Eric Postpischil

-4

の表現char [256]は実装に依存します。と同じであってはなりませんchar *

&testchar (*)[256]をキャストしてchar **未定義の動作を生成します。

一部のコンパイラでは、期待どおりの動作をする場合とそうでない場合があります。

編集:

gcc 9.2.1でテストした後printchar((char**)&test)、実際にtest はにキャストされた値として渡されるようchar**です。まるで指示だったかprintchar((char**)test)printchar機能、xアレイテストの最初の文字へのポインタではなく、最初の文字に二重のポインタです。二重逆参照x配列の最初の8バイトが有効なアドレスに対応していないため、結果、セグメンテーション違反が発生します。

プログラムをclang 9.0.0-2でコンパイルすると、まったく同じ動作と結果が得られます。

これは、コンパイラのバグ、またはコンパイラ固有の結果である可能性のある未定義の動作の結果と見なされる場合があります。

別の予期しない動作は、コード

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

出力は

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

奇妙な行動はそれでxあり、*xあり、同じ値持っています。

これはコンパイラのものです。これが言語によって定義されているとは思えません。


1
の表現char (*)[256]は実装依存であるということですか?の表現はchar [256]、この質問には関係ありません。単なるビットの束です。しかし、配列へのポインターの表現がポインターへのポインターの表現と異なることを意味する場合でも、それはポイントを逃します。それらが同じ表現を持っている場合でも、OPのコードは機能しません。これは、で行われるように、ポインターへのポインターを2回逆参照できるがprintchar、表現に関係なく配列へのポインターは逆参照できないためです。
Eric Postpischil

@EricPostpischilのキャストchar (*)[256]にはchar **、コンパイラによって受け入れられますが、ので、期待する結果が得られないchar [256]と同じではありませんchar *。私は、エンコーディングが異なると想定しました。それ以外の場合、期待される結果が得られます。
chmike

「期待される結果」の意味がわかりません。結果がどうあるべきかについてのC標準での唯一の仕様は、アライメントがに対して不十分であるchar **場合の動作は未定義であり、それ以外の場合、結果がに変換される場合char (*)[256]、元のポインターと等しいと比較することです。「期待される結果」と(char **) &testは、さらにに変換されたchar *場合に、と等しいことを意味する場合があり&test[0]ます。これは、フラットなアドレス空間を使用する実装ではありそうもない結果ではありませんが、純粋に表現の問題ではありません。
Eric Postpischil

2
また、「char(*)[256]型の&testをchar **にキャストすると、未定義の動作が発生します。」不正解です。C 2018 6.3.2.3 7では、オブジェクトタイプへのポインターを、オブジェクトタイプへの他のポインターに変換できます。ポインターが参照されたタイプ(参照されたタイプのchar **is char *)に対して正しく位置合わせされていない場合、動作は未定義です。それ以外の場合、値は部分的にしか定義されていませんが、上記の私のコメントに従って、変換が定義されています。
Eric Postpischil

char (*x)[256]と同じではありませんchar **x。理由x*x同じポインタ値を印刷することがx単に配列へのポインタです。 あなた*x は配列であり、それをポインタコンテキストで使用すると、配列のアドレスに減衰します。コンパイラーのバグはありません(または何をするのか(char **)&test)ので、型で起こっていることを理解するために必要な少しの体操だけです。(cdeclは「xをcharの配列256へのポインターとして宣言する」と説明しています)。を使用char*してaのオブジェクト表現にアクセスすることchar**もUBではありません。何でもエイリアスできます。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.