관리 메뉴

IT 컴퓨터공학 자료실

중급편 3. 다차원 배열의 실현 본문

컴퓨터공학/포인터

중급편 3. 다차원 배열의 실현

윤맨1 2015. 6. 30. 11:17

                                          포인터 토대가 된 책

                                          다차원 배열의 실현

다차원 배열과 C 언어

C 언어는 Fortran이나 BASIC으로 친숙한 다차원 배열을 다루는 수단이 엄격하게 말하면 존재하지 않는다. 이차원 좌표의 개별 값을 저장하는 수단으로 int x [10] [10];과 같은 표현으로 데이터를 확보하고 싶은 것은 어떤 프로그래밍 언어로도 같다. 그러나 C 언어에서 다른 언어로 일반적인 "다차원 배열 '이라는 개념 자체가 존재하지 않기 때문이다. 이유는 정확히 '배열의 배열'이다.

그러나 이것은 보통에 결함이 아니다. 대신 데이터 구조로 '디스플레이'로 불리는 데이터 구조를 정의 할 수 있기 때문이다. 디스플레이는 "배열에 대한 포인터 배열 '이다. 즉, 배열에 대한 참조를 가지는 포인터 배열을 정의 할 것이다. 이 모습을 보자.

int x [10] [10];

라는 정의는 10 × 10 = 100 개의 int 형 데이터를 가지는 영역을 확보한다. 그리고 x 라는 기호는 그 확보 된 메모리의 선두를 가리키는 포인터와 같은 역할을하지만, 그러나 명백하게 포인터와는 구별되어 선언된다.

그러나 프로그램에 100 개의 int를 지속적으로 확보하고 있는지 여부에 관계없이 10 개의 int 씩 10 개 확보되어있는 것만으로도 좋다. 이 모델은 다음과 같다.

int D0 [10];
int D1 [10];
.....
int D9 [10];
int * D [10] = {D0, D1, ..., D9};

D [5] [5] = 55;

이 때, 더블 포인터는 이차원 배열은 다른 한 것임에도 불구하고, 참조의 방법이다 D [5] [5] = 55; 는 이차원 배열의 참조와 전혀 구별 할 수 없다. 그래서이 두 가지를 구분하여 부르는 것이 매우 중요하다. 다음으로는 '두 (다) 차원 배열'은 ' int x [10] [10]; "과 같이 선언 된 것만을 불러 (변수 이름은 x를 고정하여 사용) 포인터를 사용한 물건을 "디스플레이"라고 부른다 (변수 이름은 D를 고정하여 사용)로한다. 그래서 선언하고 실제 데이터의 구성은 뭐니해도 "이차원 배열 '과'디스플레이 '는 같이 참조 할 수있다. 이 사실은 매우 혼란을 일으키는 쉽다. 왜냐하면 컴파일러는 양자를 완전히 구별하는 것이다.

또한 참조뿐만 아니라 양자 모두 선언 int * x [10]; 이 마치 선언 된 것처럼, 분해 해 줄 수있는 것이다. 그러나 "이차원 배열」과 「디스플레이」에서는 다음과 같이 다르다.

다차원 배열에서는
x [5]는 int * 형태를 가지는 것처럼, x 중 50 번째 요소를 참조하는 포인터로 컴파일된다.
디스플레이는
D [5]는 이미 확보 된 포인터 배열의 5 번째의 것을 참조하고 그 결과로 50 번째를 명중 요소가 시작되어 배열에 대한 포인터이다.

즉 양자는 전혀 다른 것이다. 그래서 디스플레이에서 유추 (뿐만 아니라 한 차원에서 유추)에서 x 가 마치 더블 포인터 인 것처럼 느껴지지만, 이것은 잘못이다.다음 프로그램은 컴파일시 경고가 나오고, 게다가 참조 된 포인터가 올바른 것을 가리 키지 않는다.

int ** p;
p = x;
printf ( "% d \ n", p [5] [5]);

이차원 배열에 대한 포인터는 다음의 형태가된다.

int (* p) [10];
p = x;
x [5] [5] = 55;
printf ( "% d \ n", p [5] [5]);   / * 이차원 배열과 동등 참조 * / 
printf ( "% d \ n", (* p) [55]); / * 놀라 할 일에이를 수도있다 * /

int (* p) [10] 는 10 개의 요소를 가지는 "배열의 포인터"이다 (≠ 포인터 배열 * p [10] ). 즉, 최상위 것만 포인터 할 수있는 셈이다 ( "배열의 배열"→ "배열의 포인터"). 이때 배열 크기 10도 선언에 포함되어 있기 때문에, p [5] , p [5] [5] 는 다음과 같이 잘못 계산 가능하다.

  1. p 는 배열의 포인터이며, 예를 들어 주소가 1000으로한다.
  2. 배열의 크기는 int 데이터에서 10 개이다. 즉, 4 * 10 = 40이다.
  3. 그래서 배열 5 개째에 대한 포인터 인 p [5] 는 1000 + 5 * 40 = 1200 이되고, 계산 가능하다.
  4. 그렇다면 배열 다섯 번째 것들 5 번째 int 데이터 p [5] [5] 도 주소는 1200 + 5 * 4 = 1220 되고, 주소를 계산할 수있다.
  5. 따라서 참조 가능하다.

하지만 p 는 포인터이다. 그래서 즉시 (* p) 와 참조하면, 이것은 "배열의 포인터를 참조」→ 「배열」가된다. (* p) [55] 는 참조도 가능해질 것이다. 이것은 다음과 같다.

  1. p 는 배열의 포인터이며, 예를 들어 주소가 1000으로한다.
  2. (* p) 는 배열의 포인터에 대한 참조이다. 즉, "배열"을 나타낸다. 그래서 (* p) 은 주소 1000에서 시작하는 배열을 나타낸다.
  3. 할당되는 대상은 이차원 배열 (일차원과 확보 방법은 변하지 않는다)이다. 그래서 (* p) [55] 은 주소 1000 int 배열에 대한 그 55 번째 문제에 대한 참조이다.
  4. 따라서 이것도 참조 가능하다.

그러나 다음과 같이 배열 크기를 생략하고 "배열의 포인터"를 선언 할 수있다.

int (* p) [] = x;
x [5] [5] = 55;
printf ( "% d \ n", p [5] [5]);   / * 여기에는 표시되지 않습니다 * / 
printf ( "% d \ n", (* p) [55]); / * 이곳은 OK * /

왜냐하면 p [5] [5] 를 참조하려면 p [5] 가 계산 가능하여야한다. 그러나 현재 p 는 "크기가 모르는 배열에 대한 포인터 '이기 때문에, p [5] 는 "크기가 모르는 배열의 다섯 번째 것"주소가되어 계산 불능이다. 따라서 p [5] [5] 는 참조 할 수 없다. 그러나 나중에 액세스 (* p) [55] 는 (* p) 는 이미 '배열'의 선두를 가리키고있다. 그래서 그 55 번째 요소는 참조 가능한 것이다.

이곳의 상황을 포인터의 주소 값을 살펴보면하여 다시 정리해 보자.

 (* p) [10](* p) []
p10001000
(* p)10001000
p [5]1000 + 5 * 10 * 4계산 불가
p [5] [5]1200 + 5 * 4p [5]이 계산 불능 
그래서 계산 불가
(* p) [55]1000 + 55 * 41000 + 55 * 4

재미있는 것은 p == * p 이다. 이것은 포인터가 가리키는 대상이 배열이며 포인터가 없기 때문에 간접 참조가 생성되지 않을 것이다 (반대로 말하면 포인터의 경우에는 * p 가 간접 참조를하여 p! = * p 가된다) .

위 표의 결과와 이차원 배열의 기호 x 는 실질적으로 일차원 배열이기 때문에 다음 캐스트는 성공하게된다.

int * p = (int *) x;

그래서 다음과 같이 쓸 수있다.

int x [10] [10];
int i;
int * p = (int *) x;

for (i = 0; i <100; i ++) {
   * p ++ = i;
}
printf ( "x [5] [5] = % d \ n", x [5] [5]);

이것은 x 가 실질적으로 한 차원 포인터임을 보여주고있다.

이 사정은 함수 인수의 경우도 마찬가지이다. 이차원 배열과 포인터는 혼동되지 않고, 그러나 int (* p) [10] 와 일치한다.

void sub1 (int p [] [10]) {
  ............
}

void sub2 (int ** p) {
  ............
}

void sub3 (int (* p) [10]) {
  ............
}

int main (void)
{
   int x [10] [10];

   sub1 (x);   / * OK * / 
   sub2 (x);   / * Warning이 나오고, 결과도 세분화 폴트 * / 
   sub3 (x);   / * OK * /
}

이차원 배열의 선언에서 데이터가 연속적으로 확보된다는 점과 배열에 대한 포인터로 분해 해 사용할 수에서 다음 프로그램은 합법적이다.

int x [10] [10];
int i;
int * p = x [0];   / * x [0]은 첫 번째 데이터에 대한 포인터이다. * / 
                / * 그래서 그 형태는 int *이므로 대입있다. * /
for (i = 0; i <100; i ++) {
    p [i] = i;
}
for (i = 0; i <10; i ++) {
    printf ( "% d : % d \ n", i + 50, x [5] [i]);
}

이 때, 제대로 50 ~ 59의 값이 표시된다.

배열과 포인터의 "혼동"

배열은 그 실현에 대해 지금까지 보아온 것처럼, 포인터와 깊은 관계를 가지고있다. 이를 위해 양측은 " 문맥에 의해 혼동하는 "수있다. 이것은 전장 "extern 선언의 덫 '에서 보았 듯이 아주 미묘한 문제를 가지고있다.

배열과 포인터를 "적극적으로 혼동"최대의 장면은 함수의 인수이다. 즉,

int x [10];
int * p = x;

sub (x);   / * 모두 OK * /
sub (p);

이 때, 함수 sub () 의 선언은 다음과 상관 없음 잘 똑같은 코드가 생성된다. 즉, 포인터와 배열은 "혼동 해"같은 것이다.

void sub (* p);
void sub (p []);
void sub (p [10]);

이상의 관계는 일차원 배열과 포인터의 호환성을 나타 내기 때문이다. 그러나 이차원 배열과 더블 포인터 사이에는 지금까지 보아온 것처럼, 이러한 호환성은 없다.

int x [10] [10];
int ** p = x; / * 할당 할 수없는 * /

sub (x);  
sub (p);   / * 오류 * /

이때 함수​​ sub ()의 인수 선언은 "요소 수십 배열에 대한 포인터 '이 아니면 안되지만, 다음 잘함. 함수 sub ()에 대해 생성 된 코드는 동일하다.

void sub (int p [10] [10]);
void sub (int p [] [10]);
void sub (int (* p) [10]);   

그러나 다음과 같은 함수 내부에서 p [5] [5] 와 같은 참조 할 수 없기 때문에 문제가있다.

void sub (int p [] []);
void sub (int (* p) []);

또한 당연히 더블 포인터 인수 선언은 과오이다.

void sub (int ** p);

이것은 호출 측에서 보면 배열과 포인터는 사실 다른 실체를 전달하는 것이다.

/ * sub (x); * / 
MOV AX, x의 선두 어드레스   / * x라는 기호를 나타내는 주소가 AX에 들어갈 * /
PUSH AX
CALL sub

/ * sub (p); * / 
MOV AX, p     / * 변수 p의 값을 취득 해, AX에 넣는다 * /
PUSH AX
CALL sub

즉, 수신자 (함수)에서 동일하게 처리 할 수​​ 있도록 굳이 C 언어에서 배열과 포인터를 언어 구문으로 혼동하고있는 것이다. 이것을 잊어서는 안된다.

배열 크기 선언의 문제

"배열의 포인터"를 선언 할 때 배열 크기를 생략 할 수는보고왔다. 그러나이 선언은 벌써 있듯이 p [5] [5] 와 같은 참조는 할 수 없다는 큰 단점이있다. 즉, "배열 크기"는 중요한 기능을 가지고있는 것이다.

void sub (int * p [10]);
int x [5] [10];

sub (x);   / * 배열 크기가 일치하지 않는 * /

지금까지 보아온 것처럼, 배열 크기는 int *의 형태를 가지는 x [5] 를 생성 할 때의 계산에 사용된다. 따라서 컴파일러는 "어떤 크기의 배열을 가리키는 것인가」에 긴장한 대응한다. 컴파일러는이 모순을 지적하는 것이다.

그러나 선언에 배열 크기를 고정 해 버리면 문제가있을 수도있다. 예를 들어 행렬 연산을 할 때 등 다양한 배열 크기 계산이있는 것이 좋다. 그러나 C 언어에서는 이것이 어려운 것이다. 왜냐하면 배열 크기 불일치가 항상 문제가되어,이를 방지하는 방법은 사실상 존재하지 않는다.

그래서 프로그래머는별로 다차원 배열을 사용하지 않는 경향이있다. 이 상황을 해명 해 보자.

하지만 유형 검사를 회피하기위한 '캐스트'라는 중대한 기교가있다. 당연히, 배열 크기를 포함한 배열 포인터에서도 '캐스팅'이 가능하다. 이런 일도있다.

int x [3] [4] 
    = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int (* p) [6] = (int (*) [6]) x;
int i;
for (i = 0; i <6; i ++) {
    printf ( "% d \ n", p [1] [i]);
}

이것은 배열 크기에 캐스팅하고, 억지로 3 × 4 배열을 2 × 6 배열에 읽어 바꾸고있다. 컴파일러는 아무것도 불평하고 올바른 결과를 얻을 수있다.

것은 다음과 같이 일차원 배열을 이차원 배열로 읽어 바꾸는 일도 가능하다.

int x [12] 
    = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (* p) [4] = (int (*) [4]) x;
int i;
for (i = 0; i <4; i ++) {
    printf ( "% d \ n", p [2] [i]);
}

여기까지 오면 상당히 나쁘다. 그러나이 캐스트는 사악한 비해서 메리트가 거의 없다. 왜냐하면 "캐스트이기 때문", "동적으로 크기를 변경할 수 없다"이다. 사실은 이런 식으로 동적 배열 크기를 변경하고자하는 것이다.

int n;
int x [3] [4] 
    = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int (* p) [ n ] = (int (*) [ n ]) x;
int i;
for (i = 0; i < n ; i ++) {
    printf ( "% d \ n", p [1] [i]);
}

만약 이것이 가능하다면 다차원 배열의 크기 문제는 한꺼번에 해결되는 것이지만, 정적 컴파일러 언어 인 C 언어는이를 허락하지 않는다. "항상 컴파일시에 배열 크기는 고정하는 것 '이 C 언어의 설계 인 것이다. 글쎄, 이것을 실현하기 위해서는 변수 확보에 대한 동적기구가 필요하며, C 언어의 본질에 대한 변경이 있기 때문에 실현 될 가능성은 전혀 없다. 그러므로 C에서 다차원 배열의 실현은 매우 더러운 것을하지 않으면 할 수없는 것입니다.

더러운 기술의 예로는 다음과 같은 일을 보자 : 일차원 배열로서 범용으로 사용되는 버퍼를 정의 해 놓고, 거기에 세트 된 데이터를 이차원 배열을 요구하는 함수에 전달합니다.

첫째, 지금까지해온 것처럼, 배열 크기의 고정 된 서브 루틴 캐스트로 버퍼를 건네 보자.

void sub (char array [3] [10])
{
  int i;
  for (i = 0; i <3; i ++) {
    printf ( "% d : % s \ n", i + 1 array [i]);
  }
}

char Buffer [30] 
= "abcdefghi \ 0" "bcdefghij \ 0" "cdefghijk \ 0";
/ * 인접한 문자열은 컴파일시에 결합되는 * /

int main ()
{
  / * 캐스트로 전달 * /
  sub ((char (*) [10]) Buffer);
}

그러나 문제의 본질은 서브 루틴에서 배열 크기가 고정되어있는 것이다. 문자열이므로 종료 '\ 0 "에 의해 표시가되어 있으며, 배열 크기 정보는 다부っ 있다고 봐도 좋다. 그래서이를 방지하고 싶다.

그러므로 서브 루틴은 배열 포인터를 인수에 취하는 것이 아니라 두 번 포인터를 인수에 취하게하고 거기에 맞춰서 호출을한다. 즉, 호출 원으로 한 차원 버퍼를 더블 포인터의 구조로 변환하는 것이다. 그리고 배열의 크기와 배열 수에 상당하는 데이터도 인자로 넘겨주는 것이다.

void sub (char ** dp, int row)
{
  int i;
  for (i = 0; i <row; i ++) {
    printf ( "% d :");
    for (j = 0; dp [i] [j]! = '\ 0'; j ++) {
       putchar (dp [i] [j]);
    }
  }
}

char Buffer [30] 
= "1234567 \ 0" "1234567890 \ 0" "cdefghijk \ 0";
/ * 인접한 문자열은 컴파일시에 결합되는 * /

int main ()
{
  / * 더블 포인터를 만드는 * /
  char * dp [3] = {& Buffer [0], & Buffer [8] & Buffer [19]};
  sub (dp, 3);
}

하지만이 경우는 상당히 기교적이고있다. 더 이상의 문자열을 버퍼에 넣고 그들을 문자열의 배열로 취급하고 (하나 하나의 문자열의 길이가 다른) 때로는 유효 할 것이다. 실제로 argv [] []를 만들 때 등 이런 처리가된다. 그에 대해 잘 수학적 서브 루틴을 쓸 때 어떤 이차원 배열과 행렬 크기를 건네주고 싶은 것도있다.이른바 '행렬 연산'등이 그 전형 것이다. 이때에는 열 수는 고정이며 줄은 변하지 않기 때문에 지금과 같은 기술을 사용하는 것도 손이지만, 실질적으로는 지나친 것이다. 솔직하게 일차원 배열로 처리하는 것이 좋을 것이다.

int * MatAdd (int * a, int * b, int row, int col)
{
    int i, j;
    for (i = 0; i <row; i ++) {
        for (j = 0; j <col; j ++) {
            a [i * col + j + = b [i * col + j];
            / * 물론 이렇게도 좋은 * / 
            / * * a + + = * b ++; * /
        }
    } 
    return a;
}

int main ()
{
    / * 3x3 행렬로 만든다 * /
    int MatA [] = {0, 1, 0, 1, 2, 1, 0, 1, 0};
    int MatB [] = {1, 0, 1, 0, 2, 0, 1, 0, 1};
    int * MatAdded;
    int i;

    MatAdded = MatAdd (MatA, MatB, 3, 3);
    for (i = 0; i <3; i ++) {
        printf ( "% d % d % d \ n", MatAdded [i * 3] 
                              MatAdded [i * 3 + 1] 
                              MatAdded [i * 3 + 2]);
    }
}

이렇게 귀찮은 다차원 배열은 C에서 크게 사용되지 않는다. 사용되는 경우도 전역 변수로 정의하고 귀찮은 인수 전달을 방지하는 것이 대부분이다.

다차원 배열 샘플

다차원 배열의 실례로 달력의 구조를 생각해 보자. 일년 365 일 12 개월 최대 5 주 최대 31 일이된다. 각 날짜가 각각 최고 기온 (0도 이상)을 저장한다. 그러나 월별, 주별 집계 속도를 위해 일요일부터 시작 매주 데이터를 구조화하는 것이라면,이 데이터 구조는 다음과 같다.

int Kion [12] [5] [7];

이때 월별로 생각 주안에는 일요일부터 시작하고, 1 일이 금요일이라는 같은 월초 일주일도있다. 1 월이 이런 경우라면

int Kion [12] [5] [7] = {{ 
        {-1, -1, -1, -1, -1, 10, 8}

등등 잘못된 데이터로 출현하지 않는 적당한 값으로 -1을 넣어 둔다. 마찬가지로, 1 월의 일수는 28 일부터 31 일까지의 범위에서 변동하기 때문에, 일이 없어지면 역시 -1을 종료 마커로 넣어 둔다. 이 종료 마커 사용은 문자열의 경우와 완전히 동일하다.

하지만 2 월은 28 일 밖에없고, 만약 2 월 1 일이 일요일의 경우는 2 월 4 주 밖에없는 것이다. 이 때,

int Kion [12] [5] [7] = { 
        { 
           / * 1 월의 초기화 데이터 * /
        },
        {   / * 2 월의 초기화 데이터 * /
                {10, 5, 7, 8, 1, 4, 5}
                {7, 5, 3, 2, 4, 4, 1}
                {2, 5, 7, 8, 3, 1, 1}
                {1, 4, 7, 2, 1, 2, 5}
                {-2}
        },
        ..................
};

라는 식으로 주 자체를 무효로하는 특별한 데이터 -2를 사용하여 잘못된 일주일을 표현하는 것도 가능하다. 초기화 데이터를 결여 배열 데이터는 어차피 0으로 초기화되기 때문에 문제는 없다.