이 포인터 사용을 예측할 수없는 이유는 무엇입니까?


108

저는 현재 포인터를 배우고 있으며 교수님이이 코드를 예제로 제공했습니다.

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

그는 우리가 프로그램의 행동을 예측할 수 없다는 의견을 썼습니다. 하지만 정확히 예측 불가능하게 만드는 것은 무엇입니까? 나는 그것에 대해 잘못된 것이 없습니다.


2
교수의 코드를 올바르게 재현 했습니까? 이 프로그램이 "예측할 수없는"동작을 생성 할 수 있다고 공식적으로 주장하는 것은 가능하지만 그렇게하는 것은 의미가 없습니다. 그리고 나는 어떤 교수가 학생들에게 "예측할 수없는"것을 설명하기 위해 너무 난해한 것을 사용하지 않을 것이라고 생각합니다.
AnT 2015-08-04

1
궤도의 @Lightness Races : 컴파일러는 필요한 진단 메시지를 발행 한 후 잘못된 형식의 코드를 "수락"할 수 있습니다. 그러나 언어 사양은 코드의 동작을 정의하지 않습니다. 즉 s, 초기화 오류로 인해 프로그램이 일부 컴파일러에서 허용되면 공식적으로 예측할 수없는 동작이 발생합니다.
개미

2
@TheParamagneticCroissant : 아니요. 현대에는 초기화가 잘못 구성되어 있습니다.
궤도

2
@The Paramagnetic Croissant : 위에서 말했듯이 언어는 "컴파일 실패"를 위해 잘못된 형식의 코드를 요구하지 않습니다. 컴파일러는 단순히 진단을 내리기 위해 필요합니다. 그 후에 그들은 계속해서 코드를 "성공적으로"컴파일 할 수 있습니다. 그러나 이러한 코드의 동작은 언어 사양에 정의되어 있지 않습니다.
AnT 2015-08-04

2
교수님이 주신 답이 무엇인지 알고 싶습니다.
Daniël W. Crompton

답변:


125

프로그램의 동작은 형식이 잘못 되었기 때문에 존재하지 않습니다.

char* s = "My String";

이것은 불법입니다. 2011 년 이전에는 12 년 동안 사용되지 않았습니다.

올바른 줄은 다음과 같습니다.

const char* s = "My String";

그 외에 프로그램은 괜찮습니다. 교수님은 위스키를 적게 마셔야합니다!


10
-pedantic 사용 : main.cpp : 6 : 16 : 경고 : ISO C ++는 문자열 상수를 'char *'로 변환하는 것을 금지합니다. [-Wpedantic]
marcinj

17
@black : 아니요, 변환이 불법이라는 사실은 프로그램을 잘못 구성하게 만듭니다. 과거에는 더 이상 사용되지 않습니다 . 우리는 더 이상 과거가 아닙니다.
궤도에서 가벼운 경주

17
(12 년 사용 중단의 목적 이었기 때문에 어리석은 일입니다.)
Lightness Races in Orbit

17
@black : 형식이 잘못된 프로그램의 동작은 "완벽하게 정의" 되어 있지 않습니다 .
궤도

11
그럼에도 불구하고 문제는 특정 버전의 GCC가 아니라 C ++에 관한 것입니다.
궤도

81

대답은 컴파일하는 C ++ 표준에 따라 다릅니다. 모든 코드는 다음 줄을 제외하고 모든 표준에서 완벽하게 잘 구성되어 있습니다 ‡ :

char * s = "My String";

이제 문자열 리터럴에는 유형이 const char[10]있으며 상수가 아닌 포인터를 초기화하려고합니다. char문자열 리터럴 계열을 제외한 다른 모든 유형의 경우 이러한 초기화는 항상 불법이었습니다. 예를 들면 :

const int arr[] = {1};
int *p = arr; // nope!

그러나 C ++ 11 이전 버전에서는 문자열 리터럴의 경우 §4.2 / 2에 예외가있었습니다.

와이드 문자열 리터럴이 아닌 문자열 리터럴 (2.13.4)은 " pointer to char " 유형의 rvalue로 변환 될 수 있습니다 . [...]. 두 경우 모두 결과는 배열의 첫 번째 요소에 대한 포인터입니다. 이 변환은 명시 적으로 적절한 포인터 대상 유형이있는 경우에만 고려되며 lvalue에서 rvalue로 일반적으로 변환해야하는 경우에는 고려되지 않습니다. [참고 : 이 변환은 더 이상 사용되지 않습니다 . 부록 D를 참조하십시오. ]

따라서 C ++ 03에서 코드는 더 이상 사용되지 않지만 명확하고 예측 가능한 동작을 가지고 있습니다.

C ++ 11에서는 해당 블록이 존재하지 않습니다.으로 변환 된 문자열 리터럴에 대한 예외가 없으므로 방금 제공 char*int*예제 처럼 코드 형식이 잘못되었습니다 . 컴파일러는 진단을 내릴 의무가 있으며, 이상적으로 이와 같은 경우 C ++ 유형 시스템에 대한 명확한 위반 인 경우, 좋은 컴파일러가이 점을 준수 할뿐 아니라 (예 : 경고를 발행하여) 실패 할 것으로 기대합니다. 노골적인.

코드는 이상적으로 컴파일되지 않아야합니다. 그러나 gcc와 clang 모두에서 수행됩니다 (이 유형 시스템 구멍이 10 년 넘게 사용되지 않음에도 불구하고 거의 이득없이 깨질 수있는 코드가 많기 때문이라고 가정합니다). 코드의 형식이 잘못되었으므로 코드의 동작이 무엇인지 추론하는 것은 의미가 없습니다. 그러나이 특정 사례와 이전에 허용 된 이력을 고려할 때 결과 코드를 const_cast다음과 같이 암시적인 것처럼 해석하는 것이 비합리적인 확장이라고 생각하지 않습니다 .

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

그것으로, 당신이 실제로 s다시 는 만지지 않기 때문에 나머지 프로그램은 완벽하게 괜찮 습니다. 읽기 created- const비를 통해 객체를 const포인터 것은 무방하다. 이러한 포인터를 통해 생성 된 객체를 작성 하는 const것은 정의되지 않은 동작입니다.

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

s코드의 어느 곳에서나 수정이 없기 때문에 프로그램은 C ++ 03에서는 괜찮습니다. C ++ 11에서는 컴파일에 실패해야하지만 어쨌든 수행합니다. 컴파일러에서 허용하는 경우에도 정의되지 않은 동작은 없습니다 † . 컴파일러가 여전히 C ++ 03 규칙을 [잘못] 해석하고 있다는 점을 감안하면 "예측할 수없는"동작으로 이어질 수있는 것은 없습니다. 글을 쓰면 s모든 베팅이 해제됩니다. C ++ 03 및 C ++ 11 모두에서.


† 다시 말하지만, 정의상 잘못된 형식의 코드는 합리적인 동작을 기대
하지 않습니다. ‡ 그렇지 않은 경우를 제외하고 Matt McNabb의 답변을 참조하십시오.


여기에서 "예측할 수없는 것"은 교수가 표준을 사용하여 컴파일러가 잘못된 형식의 코드 (진단을 발행하는 것 이상)로 무엇을 할 것인지 예측할 수 없다는 것을 의미한다고 생각합니다. 예, C ++ 03이 처리해야한다고 말한 것처럼 처리 할 수 ​​있으며 ( "No True Scotsman"오류가 발생할 위험이 있음) 상식을 통해 이것이 현명한 컴파일러 작성자의 유일한 것이라는 확신을 가지고 예측할 수 있습니다. 코드가 컴파일되는지 여부를 선택합니다. 그런 다음 다시 문자열 리터럴을 non-const로 캐스팅하기 전에 반전하는 의미로 처리 할 수 ​​있습니다. 표준 C ++는 상관하지 않습니다.
Steve Jessop

2
@SteveJessop 나는 그 해석을 사지 않는다. 이것은 정의되지 않은 동작이나 표준에서 진단이 필요하지 않다고 표시하는 잘못된 형식의 코드 범주가 아닙니다. 매우 예측 가능해야하는 단순한 유형 시스템 위반입니다 (C ++ 03에서 정상적인 작업을 컴파일하고 수행하며 C ++ 11에서 컴파일하지 못함). 컴파일러 버그 (또는 예술적 라이선스)를 사용하여 코드를 예측할 수 없다고 제안 할 수는 없습니다. 그렇지 않으면 모든 코드가 도식적으로 예측할 수 없습니다.
Barry

나는 컴파일러 버그에 대해 말하는 것이 아니라 표준이 코드의 동작 (있는 경우)을 정의하는지 여부에 대해 이야기하고 있습니다. 나는 교수가 똑같이하고 있다고 생각한다. 그리고 "예측할 수 없다"는 것은 현재의 표준이 행동을 정의하지 않는다고 말하는 엉뚱한 표현 일 뿐이다. 어쨌든 그것은 교수가 이것이 정의되지 않은 행동을 가진 잘 구성된 프로그램이라고 잘못 믿고있는 것보다 더 가능성이있는 것 같습니다.
Steve Jessop

1
아니 그렇지 않아. 표준은 형식이 잘못된 프로그램의 동작을 정의하지 않습니다.
Steve Jessop

1
@supercat : 공정한 요점이지만 이것이 주된 이유라고 생각하지 않습니다. 표준이 형식이 잘못된 프로그램의 동작을 지정하지 않는 주된 이유는 컴파일러가 올바르게 형식화되지 않은 구문을 추가하여 언어 확장을 지원할 수 있기 때문입니다 (Objective C처럼). 실패한 컴파일 후 청소에서 총 horlicks를 만들기 위해 구현을 허가하는 것은 단지 보너스 :-)이다
스티브 Jessop

20

다른 답변이 프로그램으로 인해 할당에 C ++ 11에서 잘못 형성되는 것이 덮여있는 const charA와 배열 char *.

그러나 프로그램은 C ++ 11 이전에도 잘못 구성되었습니다.

operator<<오버로드에 있습니다 <ostream>. 에 대한 요구 사항 iostream을 포함 할 수는 ostreamC ++ 11에 추가되었습니다.

역사적으로 대부분의 구현에는 구현의 용이성 또는 더 나은 QoI를 제공하기 위해 어쨌든 iostream포함 ostream되었습니다.

그러나 대한 부합하는 것 iostream만을 정의 ostream정의하는 않고 클래스를 operator<<오버로드.


13

내가이 프로그램에서 볼 수있는 유일한 약간 잘못된 점은 문자열 리터럴을 가변 char포인터 에 할당해서는 안된다는 것입니다 .하지만 이것은 종종 컴파일러 확장으로 받아 들여집니다.

그렇지 않으면이 프로그램은 나에게 잘 정의 된 것처럼 보입니다.

  • 매개 변수 (예 : with cout << s2) 로 전달 될 때 문자 배열이 문자 포인터가되는 방법을 지정하는 규칙 은 잘 정의되어 있습니다.
  • 배열은 operator<<a char*(또는 a const char*)에 대한 조건 인 null로 종료됩니다 .
  • #include <iostream>는를 <ostream>차례로 정의 operator<<(ostream&, const char*)하므로 모든 것이 제자리에있는 것처럼 보입니다.

12

위에서 언급 한 이유로 컴파일러의 동작을 예측할 수 없습니다. ( 컴파일에 실패 해야 하지만 그렇지 않을 수 있습니다.)

컴파일이 성공하면 동작이 잘 정의 된 것입니다. 프로그램의 동작을 확실히 예측할 수 있습니다.

컴파일에 실패하면 프로그램이 없습니다. 컴파일 된 언어에서 프로그램은 소스 코드가 아니라 실행 가능합니다. 실행 파일이 없으면 프로그램도없고 존재하지 않는 행동에 대해 이야기 할 수 없습니다.

그래서 나는 당신의 교수의 진술이 잘못되었다고 말하고 싶습니다. 이 코드에 직면했을 때 컴파일러의 동작을 예측할 수는 없지만 프로그램 의 동작과는 다릅니다 . 그래서 그가 새끼를 고를 거라면, 그가 옳은지 확인하는 것이 좋습니다. 또는 물론, 당신은 그를 잘못 인용했을 수도 있고 그가 말한 것을 번역 할 때 실수가있을 수도 있습니다.


10

다른 사람들이 언급했듯이이 코드는 이전 버전에서는 유효했지만 C ++ 11에서는 불법입니다. 결과적으로 C ++ 11 용 컴파일러는 적어도 하나의 진단을 발행하는 데 필요하지만 컴파일러의 동작 또는 빌드 시스템의 나머지 부분은 그 이상으로 지정되지 않습니다. 표준의 어떤 것도 오류에 대한 응답으로 컴파일러가 갑작스럽게 종료되는 것을 금지하지 않으며, 링커가 유효하다고 생각할 수있는 부분적으로 작성된 개체 파일을 남겨서 손상된 실행 파일을 생성합니다.

좋은 컴파일러는 생성 될 것으로 예상되는 개체 파일이 유효하거나 존재하지 않거나 유효하지 않은 것으로 인식 될 수 있는지를 종료하기 전에 항상 확인해야하지만 이러한 문제는 표준의 관할권을 벗어납니다. 역사적으로 실패한 컴파일이로드 될 때 임의의 방식으로 충돌하는 합법적으로 보이는 실행 파일을 초래할 수있는 일부 플랫폼이 있었지만 (아직도있을 수 있지만) 링크 오류가 종종 그러한 동작을하는 시스템에서 작업해야했습니다) , 나는 구문 오류의 결과가 일반적으로 예측할 수 없다고 말하지 않습니다. 좋은 시스템에서 시도 된 빌드는 일반적으로 코드 생성시 컴파일러의 최선의 노력으로 실행 파일을 생성하거나 실행 파일을 전혀 생성하지 않습니다. 일부 시스템은 빌드 실패 후 이전 실행 파일을 남겨 둡니다.

개인적으로 선호하는 것은 디스크 기반 시스템이 출력 파일의 이름을 바꾸고, 드물게 실행 파일이 새 코드를 실행하고 있다고 착각 할 때 발생할 수있는 혼동을 피하면서 유용 할 수있는 드문 경우를 허용하는 것입니다. 시스템은 프로그래머가 정상적인 이름으로 유효한 실행 파일을 사용할 수없는 경우로드되어야하는 프로그램을 각 프로젝트에 대해 지정할 수 있도록합니다 [이상적으로는 사용 가능한 프로그램이 없음을 안전하게 표시하는 것]. 임베디드 시스템 도구 세트는 일반적으로 그러한 프로그램이 무엇을해야하는지 알 수있는 방법이 없지만, 많은 경우에 시스템을위한 "실제"코드를 작성하는 사람은 쉽게 적응할 수있는 일부 하드웨어 테스트 코드에 액세스 할 수 있습니다. 목적. 이름 변경 동작을 본 적이 있는지 모르겠지만

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.