본문 바로가기

PL/C++

[C/C++] 다차원 배열에서 인덱스 침범에 대한 고찰

개요

배열은 메모리 상에서 연속된 형태로 배치된다.
그렇기 때문에 배열은 랜덤 액세스가 가능하다(단순한 포인터 계산을 통해).

연속으로 배치되는 것은 1차원 배열 뿐만 아니라 2차원, 3차원 등의 다차원 배열도 해당된다.

메모리 상에 잡힌 배열을 시각화한 그림

그렇다면 이쯤에서 우리는 한 가지 의문이 생긴다. 다음의 코드를 보자.

int arr[3][3];

arr[1][2] = 1; // OK
arr[0][5] = 1; // ?

의미적으로는 arr[1][2]과 arr[0][5]가 동일한 원소를 가리켜야 할 것 같다.
하지만 실제로도 그럴까?

어서오세요, UB의 세계에!

결론부터 말하자면 UB이다.
C11의 J.2절 Undefined behavior를 보면 다음과 같은 얘기가 있다.

An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).

따라서, 배열이 메모리에 연속적으로 배치되어 있고 가리키는 공간이 유효한 메모리 공간이라 하더라도 명백한 UB이다.
그렇다면 포인터로 우회해서 접근하는 것은 어떨까? 예컨대, 다음과 같이 접근하는 것이다.

int arr[10][10];
int* p = &arr[0][0];

p[13] = 1;     // is this UB?
*(p + 13) = 1; // what about this?

한번 따져 보자.
C11의 6.5.6절 Additive operators에서 8번을 보면 다음과 같은 얘기가 있다.

When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. ... If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. ...

해당 내용은 포인터와 정수를 더하거나 뺄 때, 피연산자인 포인터와 산술연산의 결과 포인터가 같은 배열을 가리키지 않으면 UB임을 보여주고 있다.

p는 처음에 arr[0]에 해당하는 int[10]을 가리키는 상황이다.
p[13]이라는 표현식은 *(p + 13)과 완벽하게 동치이며, p에 13을 더하면 arr[0]을 벗어난다.
따라서 둘 다 UB이다.

UB인 건 알겠는데, 왜 UB일까?

언어를 설계할 때 이유도 없이 "이런 경우는 다 UB야!" 하진 않았을 것이다.
정확한 이유는 잘 모르겠지만, 아무래도 메모리 모델을 추상화하려는 목적과 관련 있을 것 같다.

2차원 배열을 포인터의 배열로 가지는 머신의 경우처럼, 1차원 배열들이 메모리 상에 인접하지 않다면?
이 경우를 상정하면 위에 해당하는 내용들이 왜 UB인지 납득이 간다.

이런 코드도 피하자

C++에는 std::fill이라는 함수가 있다.
시작 주소, 끝 주소(포함하지 않음), 채울 값 3개를 인자로 넘겨주면 된다.
이 함수를 사용해서 2차원 배열을 어떤 값으로 채우려면 보통 다음과 같이 짤 것이다.

int arr[R][C];

std::fill(&arr[0][0], &arr[0][0] + R * C, val);

이렇게 작성한 경우 위에서 설명한 내용에 의해 UB가 된다.
&arr[0][0]에 R * C의 값을 더한 결과 포인터가 arr[0]의 요소를 가리키지 않기 때문이다.
올바르게 고치려면 다음처럼 하면 된다.

std::fill(&arr[0][0], &arr[R - 1] + C, val);

관련 링크

devblogs.microsoft.com/oldnewthing/20170927-00/?p=97095

wiki.sei.cmu.edu/confluence/display/cplusplus/CTR55-CPP.+Do+not+use+an+additive+operator+on+an+iterator+if+the+result+would+overflow

stackoverflow.com/questions/41189577/multidimensional-array-indexing-using-pointer-to-elements/

stackoverflow.com/questions/25139579/2d-array-indexing-undefined-behavior

stackoverflow.com/questions/6015080/c-c-is-this-undefined-behavior-2d-arrays

stackoverflow.com/questions/16739622/two-and-one-dimensional-arrays-equivalence-in-c

'PL > C++' 카테고리의 다른 글

[C++20] #define sz(x) (int)x.size()는 이제 그만  (0) 2021.01.27