스택 배열의 ​​포인터에 액세스 할 수없는 이유는 무엇입니까?


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;
}

이미 명시 적으로 캐스팅 &test2하여 컴파일 할 수 있다는 사실은 char**이 코드가 잘못되었음을 암시합니다.

아직도, 나는 그것에 대해 정확히 무엇이 잘못되었는지 궁금합니다. 동적으로 할당 된 배열에 대한 포인터에 대한 포인터를 전달할 수 있지만 스택의 배열에 대한 포인터에 대한 포인터는 전달할 수 없습니다. 물론 배열을 임시 변수에 먼저 할당하여 문제를 쉽게 해결할 수 있습니다.

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

그래도 누군가 char[256]char**직접 전송 하지 않는 이유를 설명해 줄 수 있습니까?

답변:


29

때문에이 test포인터가 아닙니다.

&test배열이 포인터가 아니기 때문에 char (*)[256]호환되지 않는 유형의 배열에 대한 포인터를 가져 char**옵니다. 결과적으로 정의되지 않은 동작이 발생합니다.


3
그러나 왜 C 컴파일러는 다음과 같은 유형의 무언가를 전달할 char (*)[256]char**있습니까?
ComFreek

@ComFreek 나는 최대 경고와 -Werror로 허용하지 않는다고 생각합니다.
PiRocks

@ComFreek : 실제로 허용되지 않습니다. 컴파일러가 명시 적으로 캐스팅하여 그것을 수락하도록 강요해야합니다 char**. 캐스트가 없으면 컴파일되지 않습니다.
Andreas

38

test포인터가 아닌 배열이며 배열에 대한 포인터 &test입니다. 포인터를 가리키는 포인터가 아닙니다.

배열이 포인터라는 말을 들었을 수도 있지만 이는 잘못된 것입니다. 배열의 이름은 전체 객체, 모든 요소의 이름입니다. 첫 번째 요소에 대한 포인터가 아닙니다. 대부분의 표현식에서 배열은 첫 번째 요소에 대한 포인터로 자동 변환됩니다. 그것은 종종 유용한 편의입니다. 그러나이 규칙에는 세 가지 예외가 있습니다.

  • 배열은의 피연산자입니다 sizeof.
  • 배열은의 피연산자입니다 &.
  • 배열은 배열을 초기화하는 데 사용되는 문자열 리터럴입니다.

에서는 &test, 배열의 피연산자는 &자동 전환이 발생하지 않도록. 의 결과 는 형식 이 아닌 형식 &test이 256 char인 배열에 대한 포인터 입니다.char (*)[256]char **

포인터에 대한 포인터를 얻으려면 char에서 test먼저 포인터를 만들 필요가있다 char. 예를 들면 다음과 같습니다.

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

이것에 대해 생각하는 또 다른 방법 test은 전체 객체의 이름, 즉 전체 배열의 이름을 256으로 인식하는 것 char입니다. 포인터의 이름을 지정하지 않으므로에 &test주소를 가져올 수있는 포인터가 없으므로을 (를) 생성 할 수 없습니다 char **. 를 만들려면 char **먼저가 있어야합니다 char *.


1
이 세 가지 예외 목록이 철저합니까?
Ruslan

8
@Ruslan : 예, C 2018 6.3.2.1 3.
Eric Postpischil

아, 그리고 C11에는 및 _Alignof이외에도 언급 된 연산자 가있었습니다 . 그들이 왜 그것을 제거했는지 궁금합니다.sizeof&
Ruslan

@Ruslan : 실수 였기 때문에 제거되었습니다. _Alignof유형 이름 만 피연산자로 허용하고 배열이나 다른 객체를 피연산자로 허용하지 않습니다. (왜 그런지 모르겠다. 문법적 sizeof
으로나

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 *)하고 비교하면 가능한 포인터 포인터를 제외하고 동일합니다.

실제로하고있는 것은 이것과 비슷합니다 (다시 말해서 엄격한 앨리어싱 제외).

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

이것은 분명히 잘못된 것입니다.


3

귀하의 코드는 인수 가 포함 된 메모리를 가리키는 인수 x를 기대합니다 printchar.(char *) .

첫 번째 호출에서 이는 사용 된 스토리지 test2를 가리 키므로 실제로 (char *)는를 가리키는 값 이며, 후자는 할당 된 메모리를 가리 킵니다.

그러나 두 번째 호출에서는 이러한 (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

"따라서 분할 오류의 궁극적 인 원인은이 프로그램이 OS가 점유하고있는 절대 주소 0x42 ( 'B'라고도 함)를 역 참조하려고하기 때문입니다." 위치는 OS에 의해 점유되는 것이 아니라 아무것도 맵핑되지 않음을 의미합니다. (읽기 권한이없는 실행 전용으로 매핑 된 것을 제외하고는 그렇지 않습니다.)
Eric Postpischil

1
&test == &test[0]형식이 호환되지 않으므로 C 2018 6.5.9 2의 제약 조건을 위반합니다. C 표준은이 위반을 진단하기위한 구현이 필요하며 결과 동작은 C 표준에 의해 정의되지 않습니다. 즉, 컴파일러는 동일한 것으로 평가하는 코드를 생성 할 수 있지만 다른 컴파일러는 그렇지 않을 수 있습니다.
Eric Postpischil

-4

표현은 char [256]구현 의존적이다. 같지 않아야합니다.char * .

주조 &test유형 char (*)[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의 코드는 작동하지 않습니다. 왜냐하면 포인터에 대한 포인터는 에서처럼 두 번 역 참조 될 수 printchar있지만 표현에 관계없이 배열에 대한 포인터는 불가능합니다.
Eric Postpischil

에서 주조 @EricPostpischil char (*)[256]로는 char **컴파일러에 의해 허용되고 있지만,이 때문에 예상 된 결과를 얻을 수없는 char [256]A와 동일하지 않습니다 char *. 인코딩이 다르다고 가정했습니다. 그렇지 않으면 예상 결과가 나옵니다.
chmike

“예상 결과”가 무슨 뜻인지 모르겠습니다. 결과에 대한 C 표준의 유일한 사양은에 대한 정렬이 부적절 char **하면 동작이 정의되지 않으며, 그렇지 않으면 결과가로 다시 변환 char (*)[256]되면 원래 포인터와 동일하다는 것입니다. "예상 결과"라는 말은을 (를 (char **) &test) 더 변환 char *하면를 비교 하는 것을 의미 할 수 있습니다 &test[0]. 플랫 주소 공간을 사용하는 구현에서는 가능성이 높지 않지만 순수한 표현의 문제는 아닙니다.
Eric Postpischil

2
또한 "char (*) [256] 유형을 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은 이것을 "char의 배열 256에 대한 포인터로 x 선언"으로 설명합니다). 심지어 사용 char*A는 객체의 표현을 액세스하는 char**UB 아니다; 별명을 지정할 수 있습니다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.