0 크기의 동적 배열에 대한 포인터를 증가시키는 것이 정의되어 있지 않습니까?


34

AFAIK는 0 크기의 정적 메모리 배열을 만들 수는 없지만 동적 배열로 만들 수는 있습니다.

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

내가 읽은 p것처럼 과거의 한 요소처럼 작동합니다. p가리키는 주소를 인쇄 할 수 있습니다 .

if(p)
    cout << p << endl;
  • 반복기 (과거의 마지막 요소)로 할 수 없으므로 포인터 (과거의 마지막 요소)를 역 참조 할 수는 없지만 확실하지 않은 것은 포인터를 증가시키는 것 p입니까? 반복자와 같이 정의되지 않은 동작 (UB)이 있습니까?

    p++; // UB?

4
UB "... 다른 상황 (즉, 같은 배열의 요소를 가리 키거나 끝을 지나지 않는 포인터를 생성하려는 시도)은 정의되지 않은 동작을 호출합니다 ...." from : en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten

3
글쎄, 이것은 std::vector0 항목을 가진 것과 비슷 합니다. begin()는 이미 같으 end()므로 처음을 가리키는 반복자를 증가시킬 수 없습니다.
Phil1970

1
@PeterMortensen 편집 내용이 마지막 문장의 의미가 바뀌 었다고 생각합니다 ( "내가 확신하는 것-> 이유가 확실하지 않습니다"), 다시 확인해 주시겠습니까?
Fabio는 Reinstate Monica가

@PeterMortensen : 편집 한 마지막 단락이 약간 읽기 어려워졌습니다.
Itachi Uchiwa 19

답변:


32

배열의 요소를 가리키는 포인터는 유효한 요소 또는 끝을 지나는 요소를 가리킬 수 있습니다. 둘 이상의 끝을 지나가는 방식으로 포인터를 증가 시키면 동작이 정의되지 않습니다.

0 크기의 배열의 경우 p이미 끝을지나 하나를 가리 키므로 증가시킬 수 없습니다.

+연산자 에 대해서는 C ++ 17 8.7 / 4를 참조하십시오 ( ++동일한 제한 사항이 있음).

f식이 n 개의 요소를 가진 배열 객체의 P요소 x[i]를 가리키고 x,식이 P + J있고 J + P( J값이있는 경우) 0≤i + j≤n 인 경우 ( j가설 적) 요소를 x[i+j]가리킴; 그렇지 않으면 동작이 정의되지 않습니다.


2
따라서 유일한 경우 x[i]x[i + j]두 경우 i와 동일 j하며 값이 0입니까?
Rami Yen

8
@RamiYen x[i]x[i+j]if 와 같은 요소 j==0입니다.
interjay

1
음, 나는 C ++ 의미론의 "황혼 지대"를 싫어하지만 ... +1입니다.
einpoklum 오전

4
@ einpoklum-reinstateMonica : 실제로 황혼 영역이 없습니다. N = 0의 경우에도 일관된 C ++입니다. N 요소의 배열에는 배열 뒤를 가리킬 수 있으므로 N + 1 유효한 포인터 값이 있습니다. 즉, 배열의 시작 부분에서 시작하여 포인터를 N 번 증가시켜 끝까지 갈 수 있습니다.
MSalters

1
@MaximEgorushkin 내 대답은 현재 언어가 허용하는 것에 관한 것입니다. 대신 허용하고 싶은 주제는 주제가 맞지 않습니다.
interjay

2

나는 당신이 이미 답을 가지고 있다고 생각합니다. 좀 더 깊게 보이면 : 당신은 끝이없는 반복자를 증가시키는 것이 UB라고 말했습니다 :이 대답은 반복자가 무엇입니까?

반복자는 포인터를 가진 객체이며 반복자가 실제로 포인터를 증가시키는 것을 증가시킵니다. 따라서 많은 측면에서 반복자는 포인터 측면에서 처리됩니다.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p는 arr의 첫 번째 요소를 가리 킵니다.

++ p; // p는 arr를 가리킨다 [1]

반복자를 사용하여 벡터의 요소를 순회 할 수있는 것처럼 포인터를 사용하여 배열의 요소를 순회 할 수 있습니다. 물론 그렇게하려면 마지막 요소의 첫 번째 요소와 마지막 요소에 대한 포인터를 가져와야합니다. 방금 살펴본 것처럼 배열 자체를 사용하거나 첫 번째 요소의 주소를 사용하여 첫 번째 요소에 대한 포인터를 얻을 수 있습니다. 우리는 또 다른 특별한 배열 속성을 사용하여 최첨단 포인터를 얻을 수 있습니다. 존재하지 않는 요소의 주소를 배열의 마지막 요소를 지나서 가져갈 수 있습니다.

int * e = & arr [10]; // arr의 마지막 요소를 지나서 포인터

여기서는 첨자 연산자를 사용하여 존재하지 않는 요소를 인덱싱했습니다. arr에는 10 개의 요소가 있으므로 arr의 마지막 요소는 인덱스 위치 9에 있습니다.이 요소로 수행 할 수있는 유일한 것은 주소를 가져 오는 것입니다. e를 초기화하기 위해 수행합니다. 최첨단 반복자 (§ 3.4.1, p. 106)와 마찬가지로 최첨단 포인터는 요소를 가리 키지 않습니다. 그 결과, 최첨단 포인터를 역 참조하거나 증분시킬 수 없습니다.

이것은 Lipmann의 C ++ primer 5 edition에서 발췌 한 것입니다.

그래서 UB는하지 마십시오.


-4

가장 엄밀한 의미에서 이것은 정의되지 않은 동작이 아니라 구현 정의입니다. 따라서 비주류 아키텍처를 지원할 계획이라면 바람직하지 않지만 그렇게 할 수 있습니다 .

interjay가 제시 한 표준 인용문은 UB를 나타내는 좋은 인용문이지만 포인터 포인터 산술을 다루기 때문에 내 의견으로는 두 번째로 가장 좋은 것입니다 (재미있게, 하나는 명시 적으로 UB이지만 다른 하나는 그렇지 않습니다). 질문의 작업을 직접 다루는 단락이 있습니다.

[expr.post.incr] / [expr.pre.incr]
피연산자는 [...]이거나 완전히 정의 된 객체 유형에 대한 포인터입니다.

오, 잠깐만 요, 완전히 정의 된 객체 타입? 그게 다야? 정말, 타이핑 ? 그래서 당신은 전혀 물체가 필요하지 않습니까?
실제로 뭔가가 잘 정의되어 있지 않다는 힌트를 찾으려면 약간의 읽기가 필요합니다. 지금까지는 완벽하게 허용되는 것처럼 읽히지 만 제한은 없습니다.

[basic.compound] 3하나가 가질 수있는 포인터 유형에 대해 설명하고 다른 세 가지 중 하나가 아니라면 작업 결과는 3.4 : invalid pointer에 분명히 해당 합니다.
그러나 포인터가 유효하지 않다고 말하지는 않습니다. 반대로, 포인터가 정기적으로 유효하지 않은 매우 일반적인 정상 조건 (예 : 저장 기간 종료)이 나열됩니다. 그것은 분명히 허용 가능한 일입니다. 그리고 실제로 :

[basic.stc] 4
유효하지 않은 포인터 값을 통한 간접 처리 및 유효하지 않은 포인터 값을 할당 해제 함수에 전달하면 동작이 정의되지 않습니다. 유효하지 않은 포인터 값을 사용하면 구현 정의 동작이 있습니다.

우리는 거기에 "다른 것"을하고 있으므로 정의되지 않은 행동이 아니라 구현에 따라 정의되므로 일반적으로 허용됩니다 (구현이 명시 적으로 다른 것을 말하지 않는 한).

불행히도, 그것은 이야기의 끝이 아닙니다. 최종 결과는 여기서부터 더 이상 변경되지 않지만 더 혼란스러워지고 "포인터"를 더 오래 검색합니다.

[basic.compound]
객체 포인터 유형의 유효한 값은 메모리의 바이트 주소 또는 널 포인터를 나타냅니다. T 유형의 객체가 주소 A에 있으면 값을 얻는 방법에 관계없이 해당 객체를 가리키고 있습니다.
[참고 : 예를 들어, 배열 끝을 지나는 주소는 해당 주소에있을 수있는 배열 요소 유형의 관련되지 않은 객체를 가리키는 것으로 간주됩니다. [...]].

다음과 같이 읽으십시오. 포인터 가 메모리의 어딘가를 가리키는 한 괜찮습니다.

[basic.stc.dynamic.safety] 포인터 값은 안전하게 파생 된 포인터입니다. [blah blah]

다음과 같이 읽으십시오 : 좋아, 안전하게 파생 된 것, 무엇이든. 이것이 무엇인지 설명하지 않으며 실제로 필요하다고 말하지도 않습니다. 안전하게 파생 된 목. 분명히 안전하지 않은 포인터를 계속 사용할 수 있습니다. 나는 그것들을 역 참조하는 것이 그렇게 좋은 생각은 아니지만 아마도 그것들을 갖는 것은 완벽하게 허용 될 것이라고 추측하고있다. 달리 말하지 않습니다.

구현은 편안한 포인터 안전성을 가질 수 있으며,이 경우 포인터 값의 유효성은 그것이 안전하게 파생 된 포인터 값인지에 의존하지 않습니다.

아, 그래서 내가 생각한 것만 중요하지 않을 수 있습니다. 그러나 기다려라. 즉, 그럴 수도 있습니다 . 내가 어떻게 알아?

대안 적으로, 구현은 엄격한 포인터 안전성을 가질 수 있는데,이 경우, 안전하게 파생 된 포인터 값이 아닌 포인터 값은 참조 된 완전한 객체가 동적 저장 기간이고 이전에 도달 가능하다고 선언되지 않은 한 유효하지 않은 포인터 값이다

잠깐, declare_reachable()모든 포인터 를 불러야 할 수도 있습니까? 내가 어떻게 알아?

이제 intptr_t잘 정의 된 로 변환하여 안전하게 파생 된 포인터를 정수로 표현할 수 있습니다. 물론 정수 인 경우 원하는대로 증가시키는 것이 합법적이고 잘 정의되어 있습니다.
그리고 예, intptr_t다시 포인터 로 변환 할 수 있습니다 . 이는 또한 잘 정의되어 있습니다. 원래의 값이 아니기 만하면 더 이상 안전하게 파생 된 포인터를 가지고 있다고 보장 할 수 없습니다. 그럼에도 불구하고 구현 표준을 정의하면서 표준의 서한에 이르기까지 이것은 100 % 합법적 인 일입니다.

[expr.reinterpret.cast] 5
정수형 또는 열거 형의 값을 포인터로 명시 적으로 변환 할 수 있습니다. 포인터가 충분한 크기 [...]의 정수로 변환 된 후 같은 포인터 유형 [...] 원래 값으로 다시 변환되었습니다. 포인터와 정수 사이의 매핑은 그렇지 않으면 구현 정의됩니다.

캐치

포인터는 단지 일반적인 정수이며 포인터로만 사용됩니다. 오, 그게 사실이라면!
불행히도, 전혀 사실 이 아닌 아키텍처가 존재 하며, 단지 유효하지 않은 포인터를 생성하는 것 (포인터 레지스터에 포인팅하지 않고 포인터를 참조하는 것)만으로도 트랩이 발생합니다.

이것이 "구현 정의"의 기초입니다. 그것은 당신이 원할 때마다 포인터를 증가시키는 것은 물론 표준이 다루고 싶지 않은 오버플로를 일으킬 있다는 사실입니다 . 응용 프로그램 주소 공간의 끝이 오버플로의 위치와 일치하지 않을 수 있으며 특정 아키텍처의 포인터에 대한 오버플로와 같은 것이 있는지조차 알지 못합니다. 대체로 가능한 이점과 관련이없는 악몽입니다.

다른 한편으로는 과거의 과거 객체 조건을 다루는 것이 쉽습니다. 구현은 주소 공간의 마지막 바이트가 차지되도록 객체가 할당되지 않았는지 확인해야합니다. 따라서 보장하는 것이 유용하고 사소한 것으로 잘 정의되어 있습니다.


1
당신의 논리에 결함이 있습니다. "그래서 객체가 전혀 필요하지 않습니까?" 단일 규칙에 중점을 두어 표준을 잘못 해석합니다. 이 규칙은 프로그램이 잘 구성되어 있는지 여부에 관계없이 컴파일 시간에 관한 것입니다. 런타임에 관한 또 다른 규칙이 있습니다. 런타임에만 특정 주소에서 객체의 존재에 대해 이야기 할 수 있습니다. 프로그램은 모든 규칙 을 충족해야합니다 . 컴파일 타임의 컴파일 타임 규칙과 런타임 타임의 런타임 규칙
MSalters

5
"괜찮아, 누가 돌봐! 포인터가 메모리의 어느 곳을 가리키는 한 괜찮아?"와 비슷한 논리적 인 결함이 있습니다. 아니요. 모든 규칙을 따라야합니다. "한 배열의 끝이 다른 배열의 시작"이라는 어려운 언어는 구현 에 메모리를 연속적으로 할당 할 수있는 권한을 부여합니다 . 할당간에 여유 공간을 유지할 필요는 없습니다. 이는 코드 가 한 배열 객체의 끝과 다른 배열의 시작과 같은 값 A를 가질 수 있음을 의미 합니다 .
MSalters

1
"트랩"은 "구현 정의"동작으로 설명 할 수있는 것이 아닙니다. interjay는 +오퍼레이터 ( ++유동) 에 대한 제한 사항을 찾았습니다. 즉, "end-after-the-end"뒤의 포인팅이 정의되지 않았 음을 의미합니다.
Martin Bonner는 Monica

1
@PeterCordes : basic.stc, 단락 4를 읽으 십시오 . 그것은 말한다 "간접 [...] 정의되지 않은 동작. 다른 용도로 사용 유효하지 않은 포인터 값이이 구현 정의 동작을" . 나는 다른 의미로 그 용어를 사용함으로써 사람들을 혼동하지 않습니다. 정확한 표현입니다. 정의 되지 않은 동작 은 아닙니다 .
데이먼

2
사후 증가에 대한 허점을 발견했을 가능성은 거의 없지만 사후 증가에 대한 전체 섹션을 인용하지는 않습니다. 나는 지금 그 자신을 조사하지 않을 것입니다. 있는 경우 의도하지 않은 것으로 동의했습니다. 어쨌든, ISO C ++이 플랫 메모리 모델 인 @MaximEgorushkin에 대해 더 많은 것들을 정의했다면 임의의 것을 허용하지 않는 다른 이유 (포인터 랩 어라운드와 같은)가 있습니다. 64 비트 x86에서 포인터 비교를 서명 또는 서명하지 않아야합니까?에
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.