다차원 배열은 메모리에서 어떻게 포맷됩니까?


185

C에서는 다음 코드를 사용하여 힙에 2 차원 배열을 동적으로 할당 할 수 있다는 것을 알고 있습니다.

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

분명히 이것은 실제로 정수로 구성된 여러 개의 1 차원 배열에 대한 포인터의 1 차원 배열을 만들고 "시스템"은 내가 요청할 때 의미하는 바를 알아낼 수 있습니다.

someNumbers[4][2];

그러나 다음 줄과 같이 2D 배열을 정적으로 선언하면 ... :

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

... 스택에서 유사한 구조가 생성됩니까? 아니면 완전히 다른 형태입니까? (즉, 포인터의 1D 배열입니까? 그렇지 않은 경우, 무엇이며, 이에 대한 참조는 어떻게 파악됩니까?)

또한 "시스템"이라고 말했을 때 실제로 그것을 알아내는 데 책임이있는 것은 무엇입니까? 커널? 아니면 C 컴파일러가 컴파일하는 동안 정렬합니까?


8
가능하다면 +1 이상을 줄 것입니다.
Rob Lachlan

1
경고 :이 코드에는 2D 배열이 없습니다!
이 사이트에 대해 너무 정직합니다.

@toohonestforthissite 실제로. 그것을 확대하려면 : 루핑과 호출 malloc()은 N 차원 배열을 초래하지 않습니다. . 결과적으로 1 차원 배열 을 완전히 분리하기위한 포인터 배열 [포인터 배열 [...]]이 생성됩니다 . 참조 올바르게 다차원 배열을 할당하는 할당 방법을 참조 TRUE N 차원 배열.
Andrew Henle

답변:


145

정적 2 차원 배열은 배열 배열처럼 보입니다. 메모리에 연속적으로 배치되어 있습니다. 배열은 포인터와 같지 않지만, 종종 그것들을 거의 상호 교환 적으로 사용할 수 있기 때문에 때때로 혼란 스러울 수 있습니다. 그러나 컴파일러는 올바르게 추적하므로 모든 것이 잘 정렬됩니다. 언급 한 것처럼 정적 2D 배열에주의해야합니다. int **매개 변수 를받는 함수에 1을 전달하려고하면 나쁜 일이 발생할 수 있기 때문입니다. 다음은 간단한 예입니다.

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

메모리에서 다음과 같습니다.

0 1 2 3 4 5

정확히 다음과 같습니다.

int array2[6] = { 0, 1, 2, 3, 4, 5 };

그러나이 array1기능 에 전달하려고하면 :

void function1(int **a);

경고 메시지가 표시되고 앱이 배열에 올바르게 액세스하지 못합니다.

warning: passing argument 1 of function1 from incompatible pointer type

2D 배열이와 같지 않기 때문 int **입니다. 배열에서 포인터로의 자동 소멸은 말하기를 "한 수준 깊이"만 진행합니다. 함수를 다음과 같이 선언해야합니다.

void function2(int a[][2]);

또는

void function2(int a[3][2]);

모든 것을 행복하게 만듭니다.

이 같은 개념은 n 차원 배열로 확장됩니다 . 응용 프로그램에서 이러한 종류의 재미있는 비즈니스를 활용하면 일반적으로 이해하기가 더 어려워집니다. 그러니 조심하세요


설명 주셔서 감사합니다. 따라서 "void function2 (int a [] [2]);" 정적으로 동적으로 선언 된 2D를 모두 받아들입니까? 그리고 첫 번째 차원이 []로 남아 있으면 배열의 길이를 전달하는 것이 여전히 좋은 습관 / 필수라고 생각합니다.
Chris Cooper

1
@Chris 나는 그렇게 생각하지 않습니다-C swizzle을 스택 또는 전역 할당 배열로 여러 포인터로 만드는 데 어려움을 겪을 것입니다.
Carl Norum

6
@JasonK. - 아니. 배열은 포인터가 아닙니다. 배열은 일부 상황에서 포인터로 "부패"하지만 완전히 동일 하지는 않습니다 .
칼 노룸

1
분명히하기 위해 : 예 Chris "여전히 배열의 길이를 전달하는 것이 좋습니다"별도의 매개 변수로, 그렇지 않으면 std :: array 또는 std :: vector (이전 C가 아닌 C ++ 임)를 사용하십시오. 우리는 @CarlNorum이 새로운 사용자와 개념적으로 Quora의 Anders Kaseorg를 인용하는 데 동의한다고 생각합니다.“C를 배우는 첫 번째 단계는 포인터와 배열이 같은 것임을 이해하는 것입니다. 두 번째 단계는 포인터와 배열이 다르다는 것을 이해하는 것입니다.”
Jason K.

2
@JasonK. "C를 배우기위한 첫 번째 단계는 포인터와 배열이 같은 것임을 이해하는 것입니다." -이 인용문은 매우 잘못되어 오해의 소지가 있습니다! 그것들이 동일 하지 않다는 것을 이해하는 것이 실제로 가장 중요한 단계 이지만, 배열은 대부분의 연산자 의 첫 번째 요소에 대한 포인터로 변환 됩니다 ! ( bytes /가 있는 플랫폼을 찾지 않는 한 그것은 다른 것입니다.sizeof(int[100]) != sizeof(int *)100 * sizeof(int)int
이 사이트에 대해 너무 정직합니다

85

답은 C가 정말하지 않는 생각을 기반으로 2 차원 배열을 - 그것은 배열 - 중 - 배열을 가지고있다. 이것을 선언하면 :

int someNumbers[4][2];

someNumbers4 개의 요소로 구성된 배열을 요구합니다 . 여기서 해당 배열의 각 요소는 유형 int [2](2 자체의 배열 임)입니다.int 의 ).

퍼즐의 다른 부분은 배열이 항상 메모리에 연속적으로 배치된다는 것입니다. 요청하는 경우 :

sometype_t array[4];

그러면 항상 다음과 같이 보일 것입니다.

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 개의 sometype_t물체가 서로 간격을두고 배치되지 않음). 따라서 someNumbers배열 배열에서 다음과 같이 나타납니다.

| int [2]    | int [2]    | int [2]    | int [2]    |

그리고 각 int [2]요소 자체는 다음과 같은 배열입니다.

| int        | int        |

전체적으로 다음과 같은 결과를 얻습니다.

| int | int  | int | int  | int | int  | int | int  |

1
최종 레이아웃을 보면 int a [] []에 int * ...로 액세스 할 수 있다고 생각합니까?
Narcisse Doudieu Siewe

2
@ user3238855 : 형식이 호환되지 않지만 int배열 배열 에서 첫 번째 에 대한 포인터를 얻는 경우 (예 : a[0]또는 &a[0][0]), 예를 선택하면 해당 위치를 순차적으로 액세스하도록 오프셋 할 수 있습니다 int.
caf

28
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

메모리에서 다음과 같습니다.

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

5

컴파일러는 많은 노력을 기울이고 있지만 둘 다 대답합니다.

정적으로 할당 된 배열의 경우 "시스템"이 컴파일러가됩니다. 스택 변수처럼 메모리를 예약합니다.

malloc의 배열의 경우, "시스템"은 malloc (일반적으로 커널)의 구현자가됩니다. 컴파일러가 할당 할 모든 것은 기본 포인터입니다.

컴파일러는 Carl이 상호 교환 가능한 사용법을 파악할 수있는 위치에서 제공 한 예를 제외하고는 선언 된대로 항상 형식을 처리합니다. 따라서 [] []를 함수에 전달하면 정적으로 할당 된 플랫이라고 가정해야합니다. 여기서 **는 포인터를 가리키는 포인터입니다.


@Jon L. 나는 malloc이 커널에 의해 구현되었다고 말하고 싶지는 않지만 커널 프리미티브 (brk와 같은) 위에있는 libc에 의해
Manuel Selva

@ManuelSelva : malloc구현 위치 와 방법 은 표준에 의해 지정되지 않고 구현에 맡겨집니다. 환경. 독립형 환경의 경우 연결 기능이 필요한 표준 라이브러리의 모든 부분과 마찬가지로 선택 사항입니다 (표준 상태가 아니라 요구 사항이 실제로 발생 함). 일부 현대 호스팅 환경의 경우 실제로는 stdlib와 커널 기본을 모두 사용하여 작성한 커널 기능 (전체 기능 또는 Linux)에 의존합니다. 비가 상 메모리 단일 프로세스 시스템의 경우 stdlib 만 가능합니다.
이 사이트에 대해 너무 정직합니다.

2

우리가, 가정 a1a2아래 (C99) 같이 정의 및 초기화 :

int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1메모리에 평범한 연속 레이아웃을 가진 동종 2D 배열이며 표현식 (int*)a1은 첫 번째 요소에 대한 포인터로 평가됩니다.

a1 --> 142 143 144 145

a2이기종 2D 배열에서 초기화되고 type 값에 대한 포인터입니다 int*. 즉, dereference 표현식 *a2이 type 값으로 평가되므로 int*메모리 레이아웃이 연속적 일 필요는 없습니다.

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

완전히 다른 메모리 레이아웃과 액세스 시맨틱에도 불구하고 배열 액세스 표현식에 대한 C 언어 문법은 동종 및 이종 2D 어레이에서 똑같이 보입니다.

  • 표현은 a1[1][0]값을 가져옵니다 144에서a1어레이
  • 표현식 a2[1][0]의 값을 페치한다 244밖으로 a2어레이

컴파일러에 대한 액세스-expression이 있음을 알고 a1유형에 운영 int[2][2]에 대한 액세스-expression이 때, a2종류에 작동합니다 int**. 생성 된 어셈블리 코드는 동종 또는 이종 액세스 시맨틱을 따릅니다.

코드는 일반적으로 유형의 배열이 유형 int[N][M]캐스트되고 유형으로 액세스 되면 런타임에 충돌 int**합니다. 예를 들면 다음과 같습니다.

((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

1

특정 2D 배열에 액세스하려면 아래 코드와 같이 배열 선언에 대한 메모리 맵을 고려하십시오.

    0  1
a[0]0  1
a[1]2  3

각 요소에 액세스하려면 매개 변수로 관심있는 배열을 함수에 전달하기에 충분합니다. 그런 다음 열에 오프셋을 사용하여 각 요소에 개별적으로 액세스하십시오.

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

int main()
{
   f1(a[0]);
   f1(a[1]);
    return 0;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.