오리뎅이의 네떡세상

[오리뎅이의 C 포인터 이야기 - 5] 배열의 포인터 변환

오리뎅이의 C 포인터 이야기

안녕하세요? 오리뎅이입니다.

 

 

사진은 제가 인도 뭄바이에 주재원으로 있을 때, 패밀리 회원권을 끊어서 주말마다 가던 BPGC(Bombay Presidency Golf Club)의 클럽 하우스 바로 앞에 있는 야외 풀장 모습입니다. 사진을 보고 있자니, 저 골프장에 주말마다 갔었으면서도 정작 저 풀장은 한번도 들어가 본 적은 없었네요. 뜨거운 열사의 나라 사우디 리야드 골프 코스(RGC)에서 골프를 배우고, 만만치 않게 덮고 습한 인도 뭄바이에서 골프를 즐겼었지만, 정작 한국에 돌아와서는 골프채를 1년에 한번 잡을까 말까하네요. 골프장이 몇 개 없어서 부킹도 어렵던 나라에서는 열심히 골프를 즐겼는데, 동네 방네 발길에 채이는 게 골프장인 한국에서는 골프보다 재미난 놀거리가 많아서인지 안땡겨요. ^^

 

 

포인터와 함께 C 학습을 어렵게 하는 또 하나의 암초같은 존재가 배열입니다. 배열이 처음 배울 때 어려운 이유는 바로 오늘 학습하게 될 배열이 포인터로 변환되는 특성때문입니다. 그런데 어렵게 느껴지게 만드는 배열의 포인터 변환 특성만 정확히 알고 있으면 배열도 포인터와 마찬가지로 딱 보면 척 알 수 있습니다. ^^

배열과 포인터

[오리뎅이의 C 포인터 이야기 - 2] 편에서 C 언어의 object(객체)를 설명하기 위해서 인용했었던 "C11 ISO/IEC 9899:201x 표준 문서6.3.2.1"에는 다음과 같이 array와 관련된 부분이 있습니다.

3 Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

sizeof 연산자 또는 단항 & 연산자의 피연산자 이거나 배열을 초기화하는 데 사용되는 문자열 리터럴 인 경우를 제외하고, "array of type'' type을 가진 표현식 배열 객체(object)의 첫 번째 요소를 가리키는 "pointer to type"  type을 가진 표현식으로 변환(conversion)되고, lvalue가 아니다. 배열 객체가 레지스터 스토리지 클래스를 가지면, 정의되지 않은 동작입니다.

출처: C11 ISO/IEC 9899:201x 표준 문서 6.3.2.1절 3항

참조1. C11 표준 6.3.2.1절 3항 내용

 

딱 3가지 경우를 제외하고, '"array of type" type을 가진 표현식은 배열 객체(object)의 첫 번째 요소를 가리키는 "pointer to type"의 type을 가진 표현식으로 변환(conversion)되며,  lvalue가 아니다' 라고 설명하고 있습니다. 다음의 배열이 포인터로 변환되지 않는, 즉 제외되는 딱 3가지 경우의 예입니다.

 

int iArr[2];                                   // array [3] of int

1.  sizeof 의 피연산자
    printf("%d", sizeof (iArr));                                                           

2. 단항 &(ampersand) 연산자의 피연산자

    printf("%p", &iArr);                                                                       

3. 배열을 초기화하는 데 사용되는 문자열 리터럴
   char cArr[12] = "Hello world";      // 문자열 리터럴 "Hello world"은 'array [12] of char' 타입이고,
                                               // char 배열 변수를 초기화 하는데 사용될 때는 배열 그 자체로 사용    

참조2. 배열이 pointer로 변환 되지 않는 딱 3가지 경우

 

참조2에 기술한 딱 3가지 경우에는 배열은 pointer로 conversion이 일어나지 않고, 배열 그 자체로 사용되는 경우입니다. 배열이 참조2의 딱 3가지 경우로 사용되지 않을 때에는 배열의 첫 번째 요소를 가리키는 pointer로 conversion 됩니다. 

 

배열이 pointer로 변환 되는 경우에도 법칙이 있다

배열과 포인터는 배우 닮았습니다. 배열 인덱스 연산자, [ ]는 식별자의 우측에  포인터 참조 연산자 *는 식별자의 좌측에 위치해서 마치 식별자를 기준으로 거울을 마주 보고 있는듯한 느낌입니다. 포인터와 관련된 배열의 특징들을 나열해 보면 다음과 같습니다.

-, 배열은 그 배열의 첫번째 요소를 가리키는 포인터로 변환 된다.
   N차원의 다차원 배열은 그 배열의 첫 번째 요소인 N-1차원 배열 포인터로 변환된다.
-, 배열의 연산자 [] 하나는 포인터 연산자 * 하나와 서로 매칭된다.
   int *iPtr;      // iPtr은 첫번째 객체로 포인터 type, *iPtr 은 두 번째 객체 int type
   int aArr[2];   // aArr은 배열 객체 그자체 int [2] 배열, *aArr은 배열의 첫 번째 요소 객체 aArr[0] 

-, 배열은 lvalue가 아니다.
   1차원 배열의 요소만이 lvalue이다.
   배열 p가 2차원 배열이면, *p는 1차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 2차원 배열이면, **p는 1차원 배열의 요소이고, lvalue이다.
   배열 p가 3차원 배열이면, *p는 2차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 3차원 배열이면, **p는 1차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 3차원 배열이면, ***p는 1차원 배열의 요소이고, lvalue이다.
   배열은 lvalue가 아니므로 대입 연산의 좌항 피연산자로 사용될 수 없다.
   배열은 lvalue가 아니므로 lvalue conversion 되어 value로 사용될 수 없다.

참조3. 배열의 포인터 변환 관련 특징들

 

배열의 포인터 관련 특징 중 핵심은 붉은색 굵은 글씨체로 마킹한 2가지입니다. 참조2의 딱 3가지 경우를 제외한 경우에 배열이 그 배열의 첫 번째 요소를 가리키는 포인터로 변환되는 것이 이번 이야기에서 계속해서 강조하고 있는 배열의 첫 번째 특징입니다. 두 번째 특징은 배열이 lvalue가 아니라는 것입니다. lvalue가 아니기 때문에 배열은 대입 연산자의 좌항 피연산자가 될 수 없습니다. 또한, 어느 위치에 사용되건 lvaue conversion이 되어 value로 사용될 수도 없습니다.

 

3차원 배열은  첫 번째 요소의 포인터로 변환 된다.

3차원 배열을 사용하여 배열의 포인터 변환 관련 특징들을 도식화하여 좀 더 알기 쉽게 알아 보겠습니다.

 

그림1. 3차원 배열의 포인터 변환 특징

 

1. iaaaArr[2][2][2] 은 3차원 배열입니다.

    . 영어로 타입을 표현해 보면 다음과 같습니다

      - array of [2] array of [2] array of [2] int        // array가 3번 나오고, 마지막에 한번만 int 가 나옵니다.

 

2. iaaaArr 이 &sizeof의 피연산자로 사용되면, pointer conversion 되지 않습니다.

    . &iaaaArr                                  //  그림1에서 가장 바깥쪽 네모 박스 을 가리키는 포인터(주소)입니다.

                                                      //  &iaaaArr + 1 의 더하기 연산을 하면, 의 크기 32만큼 주소가 증가

    . sizeof(iaaaArr)                        //  그림1에서 가장 바깥쪽 네모 박스 의 크기 32를 나타냅니다

 

3. iaaaArr이 &sizeof 피연산자가 아닌 용도로 사용되면, 첫 번째 요소의 포인터로 변환됩니다.

    . int (*piaaArr)[2][2] = iaaaArr;   // iaaaArr은 그림1에서 파란색 네모 박스 가리키는 포인터입니다.

                                                          // 첫 번째 요소를 가리키는 &iaaaArr[0] 로 변환됩니다. 

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

 

4. *iaaaArr은 2차원 배열입니다.

    . &*iaaaArr                                   //  그림1에서 파란색 네모 박스 를 가리키는 포인터(주소)입니다.

                                                          //  &*iaaaArr + 1 의 더하기 연산을 하면, 의 크기 16만큼 주소가 

                                                          //  증가하여, 의 포인터가 됩니다.

    . sizeof (*iaaaArr)                        //  그림1에서 파란색 네모 박스 의 크기 16을 나타냅니다.

    . int (*piaArr)[2] = *iaaaArr;       //  참조 연산자 *를 붙이면, 두 번째 배열 객체 가  됩니다.

                                                          //  iaaaArr[0] 도 2차원 배열이므로, &와 sizeof의 피연산자가 아니면 다시

                                                          //  첫 번째 요소를 가리키는 &iaaaArr[0][0]로 변환됩니다.

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

    

5. **iaaaArr은 1차원 배열입니다.

    . &**iaaaArr                                 //  그림1에서 빨간색 네모 박스를 가리키는 포인터(주소)입니다.

                                                          //  &**iaaArr + 1 의 더하기 연산을 하면, 의 크기 8만큼 주소가

                                                          //  증가하여, 의 포인터가 됩니다.

    . sizeof (**iaaaArr)                      //  그림1에서 빨간색 네모 박스  의 크기 8을 나타냅니다.

    . int *piArr = **iaaaArr;              //  참조 연산자 *를 2개 붙이면, 세 번째 배열 객체 이  됩니다.

                                                          //  iaaaArr[0][0]도 1차원 배열이므로, &와 sizeof의 피연산자가 아니면

                                                          //  첫 번째 요소를 가리키는 &iaaaArr[0][0][0]로 변환됩니다.

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

 

6. ***iaaaArr은 1차원 배열의 첫 번째 요소입니다.

    . &***iaaaArr                               //  그림1에서 검정색 네모 박스를 가리키는 포인터(주소)입니다.

                                                          //  &***iaaArr + 1 의 더하기 연산을 하면, 크기 4만큼 주소가 증가

    . sizeof (***iaaaArr)                    //  그림1에서 검정색 네모 박스 크기 4를 나타냅니다.

    . int iArr = ***iaaaArr;                //  참조 연산자 *를 3개 붙이면, 네 번째 int 객체 ④, 즉, 0이 됩니다.

                                                         //  3차원 배열에 *을 3개 붙이면, 1차원 배열 요소이고, lvalue입니다.

                                                         

3차원 배열은 3차원 포인터마냥 참조 연산자 *를 3개 붙이니 최종 1차원 배열의 요소 객체, lvalue가 되었습니다. 

 

그림1에서 , ②, , 4가지 주소는 크기는 다 다르지만, 시작 위치가 같으므로 모두 같은 주소입니다.

①=&iaaaArr,②=iaaaArr(=&iaaaArr[0]),=iaaaArr[0](=&iaaaArr[0][0]),④=iaaaArr[0][0](=&iaaaArr[0][0][0])

 

처음 배울 때, ,②,,4가지 주소를 출력했는데, 모두 동일한 주소가 나오면, 눈이 땡그래집니다. "오잉!! 왜 다 같게 나오지?" ㅎㅎㅎ. 이제는 눈 땡그래지지 않습니다.

 

1차원 배열은 첫 번째 요소를 가리키는 단일 포인터로 변환된다.

3차원 배열로 알아 본 내용을 1차원 배열과 2차원 배열의 설명과 코드 예제로 복습해 보겠습니다. 

 

그림2. 1차원 배열의 포인터 변환 특징

 

sizeof 연산자의 피연산자 그리고 단항 연산자 &의 피연산자로 배열이 사용될 때, 배열은 배열 그 자체로 사용됩니다. 그림2의 1차원 배열에서 sizeof (iaArr)의 결과는 붉은색 curly bracket, { } 이 나타내는 배열 전체의 크기입니다. &iaArr의 결과도 { } 이 나타내는 배열 전체 크기의 객체(object) 대한 pointer 입니다. iaArr이 sizeof의 피연산자 또는 단항 연산자 &의 피연산자가 아닌 경우에는 iaArr은 그림1에서 배열의 첫 번째 요소, iaArr[0], 즉, 정수 1을 담고 있는 객체를 가르키는 int 형 단일 포인터로 변환됩니다. 배열이 포인터로 변환되더라도 포인터를 저장할 수 있는 메모리 공간을 가진 변수가 아닌 단지 주소를 나타내는 rvalue입니다. 즉, 배열이 포인터로 변환되는 경우 포인터 변수가 아닌 포인터 상수로 변환됩니다. 따라서, 대입연산자의 좌항 피연산자로 사용될 수 없고, iaArr++ 이나 --iaArr과 같은 포인터 자체를 증감하는 연산도 불가능합니다. 

 

1차원 배열의 예제 코드입니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
int main(void)
{
    int iArr[2= {01};   // iArr은 array of [2] int
    int *piArr;             // piArr은 pointer to int
 
    piArr = iArr;           // iArr = &iArr[0], 첫 번째 요소를 가리키는 단일 포인터로 변환
                            // 1차원 배열 요소는 lvaue이므로 *iArr은 lvalue
    *iArr  = 10;            // 대입 연산자의 좌항 피연산자로 사용가능, *iArr = iArr[0] = 10
    *piArr = *iArr + 1;     // lvalue conversion, *iArr = iArr[0] = 10 
                            // piArr = iArr 이므로 iArr[0] = 11
    *(iArr + 1= 20;       // 포인터의 더하기 연산, *(iArr + 1) = iArr[1] = 20
                            // 대입 연산의 좌항 피연산자는 1차원 배열만 가능
 
    printf("iArr[0] = %d, iArr[1] = %d\n", iArr[0], iArr[1]);
 
   return 0;
}
 
-------------------------------------------------------------------------------------------------
 
iArr[0= 11, iArr[1= 20
cs

참조4. 1차원 배열의 포인터 변환 특징

 

2차원 배열은  첫 번째 1차원 배열 요소 포인터로 변환 된다.

그림3. 2차원 배열의 포인터 변환 특징

 

그림3의 2차원 배열에서 sizeof (iaaArr)의 결과는 파란색 curly bracket, { } 이 나타내는 배열 전체의 크기입니다. &iaaArr의 결과도 { } 이 나타내는 배열 전체 크기의 객체(object) 대한 pointer 입니다. 그렇기 때문에 &iaaArr + 1의 더하기 연산 결과는 배열 전체 크기 16 bytes 만큼 증가한 주소가 됩니다. iaaArr이 sizeof의 피연산자 또는 단항 연산자 &의 피연산자가 아닌 경우에는 iaaArr은 그림2에서 은색 curly bracket, { 1, 2 } 을 가리키는 첫 번째 요소인 1차원 배열의 포인터로 변환됩니다. 포인터 참조 연산자가 붙으면, *iaaArr은 1차원 배열 {1, 2}, 그 자체입니다. sizeof나 & 연산자로 사용되면, 역시나 1차원 배열 그 자체를 나타내지만, 그 이외의 경우에는 다시 포인터 변환이 일어나서 1차원 배열의 첫 번째 요소를 가리키는 포인터로 변환되고, **iaaArr은 iaaArr[0][0]으로 숫자 1이 저장된 int 객체를 나타내는 lvalue 입니다.

 

2차원 배열의 예제 코드입니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
 
int main(void)
{
    int iaaArr[2][3= {01234 }; // array of [2] array of [3] int
    int (*paiArr)[3];                    // pointer to array of [3] int
    int *piArr0, *piArr1;
 
    paiArr = iaaArr;            // 이차원 배열은 1차원 배열 포인터로 변환
                                    
    printf("&iaaArr   = %p, sizeof(iaaArr)   = %d, iaaArr  = %p\n"
            &iaaArr,      sizeof(iaaArr),          iaaArr);                                
    printf("&*iaaArr  = %p, sizeof(*iaaArr)  = %d, *iaaArr = %p\n"
            &*iaaArr,      sizeof(*iaaArr),        *iaaArr);                                  
    printf("&**iaaArr = %p, sizeof(**iaaArr) = %d, **iaaArr = %d\n"
            &**iaaArr,      sizeof(**iaaArr),      **iaaArr);                              
                                         
    printf("\n");
    piArr0 = iaaArr[0];         // 1차원 배열은 단일 포인터로 변환
 
    printf("&iaaArr[0] = %p, sizeof(iaaArr[0]) = %d, iaaArr[0] = %p\n"
            &iaaArr[0],      sizeof(iaaArr[0]),      iaaArr[0]);
    printf("**iaaArr     = %d, *iaaArr[0]     = %d, iaaArr[0][0] = %d\n"
            **iaaArr,          *iaaArr[0],          iaaArr[0][0]);  //  0 
    printf("*(*iaaArr+1) = %d, *(iaaArr[0]+1) = %d, iaaArr[0][1] = %d\n"
            *(*iaaArr+1), *(iaaArr[0]+1),           iaaArr[0][1]); //  1 
 
    printf("\n");                                     
    piArr1 = iaaArr[1];         // 1차원 배열은 단일 포인터로 변환
 
    printf("&iaaArr[1] = %p, sizeof(iaaArr[1]) = %d, iaaArr[1] = %p\n"
            &iaaArr[1],      sizeof(iaaArr[1]),      iaaArr[1]); 
    printf("**(iaaArr+1)     = %d, *iaaArr[1]     = %d, iaaArr[1][0] = %d\n"
            **(iaaArr+1), *iaaArr[1],                   iaaArr[1][0]);  //  3 
    printf("*(*(iaaArr+1)+1) = %d, *(iaaArr[1]+1) = %d, iaaArr[1][1] = %d\n"
            *(*(iaaArr+1)+1), *(iaaArr[1]+1),           iaaArr[1][1]);  //  4 
 
   return 0;
}
 
-------------------------------------------------------------------------------------------------
 
&iaaArr   = 000000000062FDF0, sizeof(iaaArr)   = 24, iaaArr  = 000000000062FDF0
&*iaaArr  = 000000000062FDF0, sizeof(*iaaArr)  = 12*iaaArr = 000000000062FDF0
&**iaaArr = 000000000062FDF0, sizeof(**iaaArr) = 4**iaaArr = 0
 
&iaaArr[0= 000000000062FDF0, sizeof(iaaArr[0]) = 12, iaaArr[0= 000000000062FDF0
**iaaArr     = 0*iaaArr[0]     = 0, iaaArr[0][0= 0
*(*iaaArr+1= 1*(iaaArr[0]+1= 1, iaaArr[0][1= 1
 
&iaaArr[1= 000000000062FDFC, sizeof(iaaArr[1]) = 12, iaaArr[1= 000000000062FDFC
**(iaaArr+1)     = 3*iaaArr[1]     = 3, iaaArr[1][0= 3
*(*(iaaArr+1)+1= 4*(iaaArr[1]+1= 4, iaaArr[1][1= 4
cs

참조5. 2차원 배열의 포인터 변환 특징

 

참조5의 코드를 카피해서 본인의 로컬 머신에서 직접 컴파일하고, 실행하셔서 코드 행간 내용과 출력 결과를 비교해 보세요.

 

2차원 배열 iaaArr[2][3]는 int 형 요소 3개를 가진 1차원 배열 2개를 가진 2차원 배열입니다. 크기는 2 x 3 x 4 = 24입니다. 1차원 배열은 int 형 요소 3개이므로 크기가 3 x 4 = 12입니다. int 자료형의 크기는 제 PC에서는 4이군요. 11 ~ 16 라인에서 주소와 크기를 축력해 본 결과가 43 ~ 45 라인에 있습니다. 주소는 모두 동일하게 나옵니다. 크기는 각각 다릅니다.

 

33 ~ 34 라인에서 iaaArr+1 의 포인터 더하기 연산을 하였습니다. 그랬더니, 1차원 배열의 크기 12만큼 주소가 증가해서 두 번째 1차원 배열을 가리키는 포인터가 되었습니다. 2차원 배열에 * 을 2개 붙이니 1차원 배열 첫 번째 요소가 되고, lvalue conversion이 되어 3이 출력 되었습니다.

 

 

인자 배열의 포인터 변환 껌이쥬? ^^. 아래 5중 포인터와 5차원 배열의 극한 비교를 쓱 보시면 배열의 포인터 변호나 쌉 정리 가능!!  

 

그림4. 5중 포인터와 5차원 배열 영어 이름 비교

 

-, 배열은 그 배열의 첫번째 요소를 가리키는 포인터로 변환 된다.
-, 배열은 lvalue가 아니다. 
   1차원 배열의 요소만이 lvalue이다.

 

C에서 배열은 함수의 매개 변수가 될 수 없다 

배열이 C를 공부하는 나를 혼란스럽게 하는 또 한가지 경우가 배열처럼 생긴 모양으로 함수의 매개 변수로 사용되는 포인터 경우입니다. 분명한 것은 C에서는 배열을 함수 인자로 전달할 수 없습니다. 배열 처럼 생긴 함수의 매개 변수는 배열의 탈을 쓴 포인터입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
 
int SummaryArray(int piArr[], int iCnt)                      // piArr은 배열의 탈을 쓴 포인터
{                                                            // int piArr[] = int *piArr 
    int i, iSum = 0;
    
    for (i = 0; i < iCnt; i++)
    {
        iSum += piArr[i];
    }
    
    return iSum;
}
 
int main()
{
    int iArr[10= { 12345678910 };
    int iSum = 0;
 
    iSum = SummaryArray(iArr, sizeof(iArr) / sizeof(int));    // iArr = &iArr[0], 포인터 변환  
                                                               
    printf("Sum = %d\n", iSum);
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
Sum = 55
cs

참조6. 함수의 매개 변수로 사용 된 배열의 탈을 쓴 포인터

 

참조6의 예에서 라의 3의 SummaryArray 함수의 매개 변수로 사용된 int piArr[]은 생김새로만 봐서는 영락 없는 배열의 모습니다. 그러나 바뜨, 이거슨 배일이 아닙니다. 포인터입니다. C에서 배열 자체를 함수의 인자로 전달하는 것을 지원하지 않습니다. C가 함수의 인자 evaluation strategy가 모조리 값을 copy해서 넘기는 call by value만 지원하는 관계로 배열 변수도 값을 copy해서 넘긴다면, 엄청난 크기의 배열을 요소 하나 하나 copy해서 넘기는 건 좀 성능상 아닐 것 같아서 인 것도 같고요. 배열은 lvaue가 아니라서 배열 자체가 대입 연산자의 좌항 피연산자로 사용될 수도 없으니, 값을 copy 한다는 것도 대입의 일종으로 본다면 안되는 게 맞는거 같기도 합니다. 아뭏튼 어째튼 저째튼 C에서는 배열을 함수의 매개 변수로 사용할 수 없습니다. 대신에 뭔 수작인지 몰라도 저런 배열의 탈을 쓴 포인터 문법을 허용합니다. 생김새는 배열처럼 생겨 먹었지만, 포인터라는 것을 기억하기만 하면 한나도 안 어렵습니다.

 

[오리뎅이의 C 포인터 이야기 - 4] 에서 포인터 배열과 배열 포인터의 차이를 알아 보았었고, 배열 포인터 만드는 방법도 확인해 보았었습니다.  기억을 되새김질해 보기 위해서 참조 자료를 소환합니다. 

 

char const *apArr[10];       // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 포인터 배열입니다.
                                   // 식별자 apArr과 [] 사이에 괄호가 없으면 배열

char const (*paPtr)[10];    // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 배열 포인터입니다. 
                                  // 식별자 paPtr 앞에 *가 있고, 이 것들을 ()가 감싸고 있으면 포인터

 

함수의 매개 변수로 사용하는 다차원 배열의 모습을 한 인자도 역시나 배열의 탈을 쓴 포인터입니다. "이 머선 말이고?" ^^ 아래의 예제 코드를 통해서 머선 말인지 한번 확인해 보시죠.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
 
void ArraySwap(int piaArr[][5], int iaCnt)                   // piaArr은 배열의 탈을 쓴 포인터
{                                                            // int piaArr[][5] = int (*piaArr)[5] 
    int iTemp[5], i;
    
    for (i = 0; i < 5; i++)
    {
        iTemp[i]= piaArr[0][i];
        piaArr[0][i] = piaArr[1][i];
        piaArr[1][i] = iTemp[i];
    }
    
    printf("sizeof (piaArr) = %2d, sizeof (*piaArr) = %2d\n",
            sizeof (piaArr),       sizeof (*piaArr));
}
 
int main()
{
    int iaArr[2][5= { {12345}, {678910} };
    int i, j;
 
    printf(":: Before ArraySwap()\n");
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%2d ", iaArr[i][j]);
        }
        printf("\n");
    }
 
    ArraySwap(iaArr, sizeof(iaArr) / ((sizeof(int* 5)));    // iArr = &iaArr[0], 포인터 변환  
                           
    printf("\n:: After ArraySwap()\n");                              
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%2d ", iaArr[i][j]);
        }
        printf("\n");
    }                                                              
        
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
:: Before ArraySwap()
 1  2  3  4  5
 6  7  8  9 10
 
sizeof (piaArr) =  8sizeof (*piaArr) = 20
 
:: After ArraySwap()
 6  7  8  9 10
 1  2  3  4  5
cs

참조7. 2차원 배열의 탈을 쓴 배열 포인터

 

참조 7의 예제 코드에서 ArraySwap() 함수의 매개 변수인 int piaaArr[][5] 은 생긴 것은 딱 2차원 배열의 모습을 하고 있습니다. 배열인 것 같지만, 겉모양에 속으면 안됩니다. ^^.  C는 배열을 함수의 인자로 넘기지 못합니다. 함수 매개 변수에 배열처럼 생긴 것이 있으면, 1차원 배열 모양이건 3차원 배열 모양이건 무조건 포인터입니다.

 

 

배열이 은제는 포인터로 변환되고 은제는 아니되는지 딱 보믄 알것지라~이? ^^. C 포인터 이야기 끝이 보이네요. 다음 이야기에서는 또 하나의 복병 함수 포인터에 대해서 신박한 예제들을 가지고 썰을 풀어 볼 예정입니다. 함수 포인터만 이야기 하면, 공부할 주제들은 다 끝난 것이어요. 오늘도 끝까지 [오리뎅이 C 포인터 이야기]를 읽어 주셔서 고맙습니다.

 

뼈때리는 지적의 댓글도, 힘내라 북돋아 주는 격려의 댓글도, 좀 더 디테일을 원하거나 관련된 지식에 대해 질문을 하거나 하는 댓글도 너무 너무 환영합니데이~~~ 댓글 달아 주이~~~~소!!

 

2021년 1월 30일 수원에서 [오리뎅이]

[오리뎅이의 C 포인터 이야기 - 4] 포인터의 영원한 동반자 배열

오리뎅이의 C 포인터 이야기

안녕하세요? 오리뎅이입니다.

 

 

사진은 제가 사우디 리야드에서 주재원으로 근무하던 마지막 해(2013년)에 가족들은 한국으로 돌아 가고, 혼자서 게스트 하우스에서 생활하던 때에 이른 아침 리야드 시내를 산책하다가 동 터오는 동쪽 하늘을 찍은 사진이에요. 딱  보기에는 마치 해지는 황혼녘은 서쪽 하늘을 생각나게 하는 그림인데, 해 떠오르는 아침 동쪽 하늘 사진이란게 신기하지 않나요?(저만 그런가요? ㅎ) 그 척박한 나라에서의 4년, 그 때는 너무 힘들었었지만 지나고 나니 아릿한 축억으로 남아 다시 돌아가 보고 싶다는 생각도 가끔 드네요.

 

 

오늘 [오리뎅이 C 포인터 이야기]에서는 어찌나 끈끈한지 뗄래야 뗄 수 없는 포인터와 배열의 그 끈끈한 유대관계를 낱낱히 해부해 보겠습니다.  배열과 포인터의 관계를 자세히 들여다 보기 전에 우선 배열이 익숙치 않으신 분들을 위해서 배열의 밥 아조씨, BoB(Basic of Basic) 먼저 살펴 보겠습니다. 

 

배열이 뭐열?

배열이란 char, short, int, double, struct, array 등의 자료형(data type)들에 대해서 같은 type의 자료형유한한 크기로 나열하는 자료형입니다. 배열을 구성하는 같은 type의 자료들을 요소(element)라고 합니다. 다음은 int type 요소를 5개 가진 1차원 배열 변수를 선언한 예입니다.

 

   int iArr[5];            // 1차원 배열 선언 문법  :  type name[size]; 
                           //                                  size =  배열 요소의 수 or 배열 크기

 

배열의 type으로는 void type을 제외 한 기본 자료형(그림1 참조)과 포인터, 열거(enum) 형 또는 크기가 정해진 다른 배열(이 경우 배열을 다차원 배열이라고 함)이 사용될 수 있습니다. 

 

그림1. C언어 기본 자료형

그림1 출처 : m.blog.naver.com/sharonichoya/220339079484

 

크기가 정해지지 않은 배열은 int iArr[]; 와 같이 배열 인덱스 연산자([] : array subscript operator or array index operator) 안에 배열 요소의 수가 생략되고 선언된 배열 타입을 말합니다. 크기가 정해진 배열 타입은 complete type인 반면에 크기가 정해지지 않은 배열 타입은 incomplete type입니다. 크기가 정해진 다른 배열만 배열의 요소가 될 수 있으므로 다차원 배열 초기화 식의 경우 항상 첫 번째 배열 크기만 생략될 수 있습니다. 

 

int iaArr[3][3] = {0, };               // OK
int iaArr[][3] = {0, };                 // OK, 첫 번째 배열 크기 생각 가능, 초기화 요소 수에 의해서 배열 크기 정해짐
int iaArr[3][] = {0, };                 // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생
int iaaArr[2][3][4] = {0, };           // OK
int iaaArr[][3][4] = {0, };            // OK, 첫 번째 배열 크기 생각 가능, 초기화 요소 수에 의해서 배열 크기 정해짐
int iaaArr[2][][4] = {0, };            // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생
int iaaArr[2][3][] = {0, };            // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생

 

배열 변수를 지역 변수로 선언만 하고 초기화를 하지 않으면, 일반 변수를 초기화 하지 않은 것과 마찬 가지로 쓰레기 값(garbage)을 가지고 있을 수 있습니다. 모든 변수의 초기화는 자동차에 타면 안전 밸트 매듯이 습관적으로 해야 합니다. 여러가지 자료형의 1차원 배열 선언 및 초기화 방법에 대해서 예제 코드로 알아 보겠습니다.

 

다음은 char 자료형 1차원 배열 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
 
int main (void)
{  
    char    cArr1[4= {'H','I','H','I'};       //  char type 배열 (array of [4] char)
    char    cArr2[8= "Hello!";                //  char type 배열, 문자열 리터럴로 초기화  
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("cArr1[%d] = %2c ", i, cArr1[i]); 
    }
    printf("\n");
    printf("cArr2 = %s \n\n", cArr2);
    
    printf("sizeof \"Hello!\" : %d\n"sizeof ("Hello!"));
    printf("address &\"Hello!\" : %p\n"&"Hello!");
    printf("\"Hello!\"[4] : %c\n\n""Hello!"[4]);
    
    cArr1[4= 'O';                             //  배열 boundary를 벗어 나서 써도 에러 체크 안된다  
 
    for (i = 0; i < 5; i++){
        printf("cArr1[%d] = %2c ", i, cArr1[i]); 
    }
    printf("\n");    
    
    cArr2[6= ' ';
    cArr2[7= 'h';
    cArr2[8= 'i';                             //  배열 boundary를 벗어 나서 써도 에러 체크 안된다  
    cArr2[9= 0;                               //  배열 boundary를 벗어 나서 써도 에러 체크 안된다 
 
    printf("cArr2 = %s \n", cArr2);
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
cArr1[0=  H cArr1[1=  I cArr1[2=  H cArr1[3=  I
cArr2 = Hello!
 
sizeof "Hello!" : 7 // "Hello!"은 char [7] 타입의 배열이다
address &"Hello!" : 0000000000404035 // 재사용 될 문자열 literal은 .rodata 영역에 저장
"Hello!"[4] : o // "Hello!"의 4 번째 요소 o 출력
 
cArr1[0=  H cArr1[1=  I cArr1[2=  H cArr1[3=  I cArr1[4=  O
cArr2 = Hello! hi
cs

참조1. char 자료형 배열의 초기화

 

문자 자료형의 초기화는 참조1의 5라인과 같이 단일 인용부호로 감싼 character로 초기화 리스트 {}를 이용해서 초기화 할 수도 있고, 6라인과 같이 문자열 리터럴을 이용해서 초기화를 할 수도 있습니다.

 

배열 요소(element)에 대한 접근은 index를 이용합니다. 첫 번째 요소는 0부터 시작하고, 마지막 요소는 (배열 크기 - 1) 입니다. Index를 (배열 크기 - 1)이 아닌 배열 크기 그 자체로 접근하면 배열 경계(boundary)를 벗어나 배열 요소가 아닌 메모리를 참조하게 되니, 이 부분은 배열을 사용할 때 무조건 언제나 항상 자나깨나 염두에 두어야 합니다. ^^ 10~12라인과 같이 index 0 ~ 3을 이용해서 크기 4인 문자 자료형 각각의 요소에 접근할 수 있습니다.

 

문자열 리터럴의 자료형(data type)은 char 배열입니다. arr[5]와 같은 배열의 표현 형식은 아니지만, 라인 5에서 문자 자료형 배열의 초기화에 사용한 "Hello!" 이 자체가 배열입니다. 그래서 16 ~ 18 라인에서와 같이 배열의 일반적인 연산을 수행할 수 있습니다. 42 ~ 44 라인의 출력 결과를 보면, "Hello!"가 char [7] type의 문자 자료형 배열의 결과를 출력되는 것을 볼 수 있습니다.

 

20라인과 29 ~ 30 라인을 보면, 배열의 경계(boundary)를 벗어난 곳을 정상적으로 배열 이름을 통해 접근해서, 새로운 값을 쓰고 있습니다. 그렇지만, 컴파일시에도 warning이 발생하지도 않을뿐더러, 46 ~ 47 라인과 같이 새로운 값을 쓴 것이 결과에 그대로 출력되고 있습니다. 이거슨 buffer overflow에 해당하는 bug입니다. 실제 실행 환경에서 이렇게 사용한다면, 어떤 결과가 나올지 모릅니다. 심한 경우, 프로그램이 오동작으로 뻗어 버릴 수도 있습니다. C에서 배열을 사용할 때 배열 요소 접근에 대한 경계 체크는 프로그래머의 책임임을 명심해야 합니다.

 

다음은 short 자료형 1차원 배열 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
 
int main (void)
{  
    short   sArr1[4= {1357};                 //  short type 배열 (array of [4] short)
    short   sArr2[]  = {2468};                 //  크기 지정 X, 초기화에 의해 정해짐  
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("sArr1[%d] = %2d ", i, sArr1[i]);
    }
    
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("sArr2[%d] = %2d ", i, sArr2[i]);
    }
    
    printf("\n");
    printf("sizeof sArr2  = %2d \n"sizeof sArr2);  //  short 형 자료 4개의 크기가 출력된다   
 
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
sArr1[0=  1 sArr1[1=  3 sArr1[2=  5 sArr1[3=  7
sArr2[0=  2 sArr2[1=  4 sArr2[2=  6 sArr2[3=  8
sizeof sArr2  =  8 //  short 형 자료 4개의 크기 8 출력
cs

참조2. short 자료형 배열의 초기화

 

참조2의 5,6 라인에 short 자료형에 대한 1차원 배열을 선언하고, 선언과 동시에 초기화 리스트를 사용해서 초기화하였습니다. 그런데, 6라인의 sArr2는 [] 안이 비어 있습니다. 즉, 배열 크기가 지정되지 않았습니다. 배열 크기를 지정하지 않는 경우, 배열은 초기화에 사용된 요소의 수로 size가 정해 집니다. 6 라인에서는 short type 요소 4개를 초기화 하였기때문에  20 라인에서 sizeof 로 배열의 크기를 확인했을 때, 29 라인에서 8로 결과가 출력되었습니다.

 

다음은 int, double, struct 자료형 1차원 배열 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
 
typedef struct {
    int A;
    int B;
    int C;
    int D;
}structE;
 
int main (void)
{  
    int     iArr1[4];                                  //  int type 배열, 초기화 X 
    double  dArr1[4= {};                             //  double type 배열 (array of [4] double)
    double  dArr2[4= {[1]=5.0, [3]=7.0};             //  double type 배열, 특정 위치 초기화  
    structE eArr1[4= {{1234},{2468}};    //  struct type 배열 (array of [4] structE)
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("iArr1[%d] = %4d ", i, iArr1[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("dArr1[%d] = %.2f ", i, dArr1[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("dArr2[%d] = %.2f ", i, dArr2[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("eArr1[%d].A = %2d ", i, eArr1[i].A);
        printf("eArr1[%d].B = %2d ", i, eArr1[i].B); 
        printf("eArr1[%d].C = %2d ", i, eArr1[i].C); 
        printf("eArr1[%d].C = %2d \n", i, eArr1[i].D); 
    }
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
iArr1[0=    1 iArr1[1=    0 iArr1[2= 4204169 iArr1[3=    0
dArr1[0= 0.00 dArr1[1= 0.00 dArr1[2= 0.00 dArr1[3= 0.00
dArr2[0= 0.00 dArr2[1= 5.00 dArr2[2= 0.00 dArr2[3= 7.00
eArr1[0].A =  1 eArr1[0].B =  2 eArr1[0].C =  3 eArr1[0].C =  4
eArr1[1].A =  2 eArr1[1].B =  4 eArr1[1].C =  6 eArr1[1].C =  8
eArr1[2].A =  0 eArr1[2].B =  0 eArr1[2].C =  0 eArr1[2].C =  0
eArr1[3].A =  0 eArr1[3].B =  0 eArr1[3].C =  0 eArr1[3].C =  0
cs

참조3. int, double, struct 자료형 배열의 초기화

 

12 라인은 int 자료형 배열 변수를 선언만 하고, 초기화를 하지 않았습니다. 13 라인은 초기화 리스트 안에 값은 없이 초기화를 하였습니다. 이 형식은 gcc에서는 사용될 수 있지만, 표준에서는 허용되지 않습니다. 표준에서는 최소 1개라도 초기화 리스트에 요소가 초기화 되어야만 합니다. 지역 변수로 선언된 배열 변수도 초기화를 하지 않은 경우에는 43라인의 iArr1[2]의 결과 값과 같이 쓰레기 값이 들어 있을 수 있습니다. 반면에 초기화 리스트로 초기화 한 경우에는 특정 값(보통 0)으로 초기화가 됩니다. C 표준에서는 빈 초기화 리스트로 초기화 하는 것이 금지이지만, C++에서는 허용됩니다. 좋은 습관은 빈 초기화 리스트로 초기화 하지 않고, {0,}; 와 같이 첫 번째 요소를 넣어서 초기화 하는 것입니다. 첫 번째, 요소만 0으로 초기화를 하면, 나머지 요소들도 모두 0으로 초기화 됩니다.

 

14 라인은 배열 요소(element) 중 초기화 할 요소만 index(배열에서의 위치를 나타내는 숫자)로 초기화를 하였습니다. 초기화 리스트를 나타내는 중괄호 { } 안에 배열 인덱스 연산자 [] 안에 초기화 하고자 하는 index를 적고 초기화할 값을 대입 연산자로 대입하여 주었습니다. 66라인의 출력 결과를 보면, 초기화 하지 않은 0, 2  index의 요소는 0으로 초기화가 되었습니다.

 

15 라인은 구조체 배열인데요. 앞에 2개 요소만 초기화를 하였습니다.  48, 49 라인을 보면,  부분 초기화를 한 경우에도 초기화 하지 않은 다른  요소들은 모두 0으로 초기화가 된 것을 볼 수 있습니다.

 

다음은 enum 자료형과 char const * 배열 자료형, 그리고 enum days [7] 포인터의 1차원 배열 예제입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<stdio.h>
 
enum days { MON = 0, TUE, WED, THU, FRI, SAT, SUN };
 
int main()
{
    enum days dDays[7= {MON, TUE, WED, THU, FRI, SAT, SUN};
    char const *apDays[] = { "월요일""화요일""수요일""목요일""금요일""토요일""일요일" }; 
    enum days (*paDays)[7= &dDays;
     
    int i;
   
    for(i = MON; i <= SUN; i++){
        printf("하나님께서 %d번째 요일로  %s을 창조 하셨심니더. \n", dDays[i]+1, apDays[dDays[i]]);
    }
    
    printf("\n");
    
    printf("sizeof dDays  = %2d, &dDays  = 0x%08x\n"sizeof dDays, &dDays);
    printf("sizeof apDays = %2d, &apDays = 0x%08x\n"sizeof apDays, &apDays);
    printf("sizeof paDays = %2d, &paDays = 0x%08x\n"sizeof paDays, &paDays);
    printf("pDays[0] = 0x%08x, apDays[6] = 0x%08x\n", apDays[0], apDays[6]);
    
    printf("\n");
    
    for(i = MON; i <= SUN; i++){
        printf("하나님께서 %d번째 요일로  %s을 창조 하셨심니더. \n", paDays[0][i]+1, apDays[paDays[0][i]]);
    }
   
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
하나님께서 1번째 요일로  월요일을 창조 하셨심니더.
하나님께서 2번째 요일로  화요일을 창조 하셨심니더.
하나님께서 3번째 요일로  수요일을 창조 하셨심니더.
하나님께서 4번째 요일로  목요일을 창조 하셨심니더.
하나님께서 5번째 요일로  금요일을 창조 하셨심니더.
하나님께서 6번째 요일로  토요일을 창조 하셨심니더.
하나님께서 7번째 요일로  일요일을 창조 하셨심니더.
 
sizeof dDays  = 28&dDays  = 0x0062fe00
sizeof apDays = 56&apDays = 0x0062fdc0
sizeof paDays =  8&paDays = 0x0062fdb8
pDays[0= 0x00404000, apDays[6= 0x0040402a
 
하나님께서 1번째 요일로  월요일을 창조 하셨심니더.
하나님께서 2번째 요일로  화요일을 창조 하셨심니더.
하나님께서 3번째 요일로  수요일을 창조 하셨심니더.
하나님께서 4번째 요일로  목요일을 창조 하셨심니더.
하나님께서 5번째 요일로  금요일을 창조 하셨심니더.
하나님께서 6번째 요일로  토요일을 창조 하셨심니더.
하나님께서 7번째 요일로  일요일을 창조 하셨심니더.
cs

참조4. enum 자료형 배열, 포인터 배열, 그리고 배열 포인터의 초기화

 

참조4의 라인 7에 enum days 타입의 배열 변수 dDays 를 선언하고 초기화 하였습니다. enum 자료형의 요소는 int 자료형과 같습니다. 즉, enum 자료형 변수 1개의 size가 int 형 size와 같습니다. dDays는 요소 7개를 가진 int [7]과 size가 같습니다. enum 자료형 배열을 초기화 하는 경우, 자료형이 enum 자료형이 아니더라도 warning도 없이 컴파일이 성공되므로 초기화 시나 사용시에 자료형 범위 밖으로 벗어나지 않도록 주의 해야 합니다. 

 

라인 8은 char const 포인터 배열을 배열 크기를 지정하지 않고 초기화 리스트를 이용해서 초기화 하였습니다. 초기화 리스트의 요소로 사용된 문자열 리터럴들은 .rodata 영역에 배치가 되고, 그 주소가 포인터 배열 요소들에 저장됩니다. 라인 9는 enum days [7] 배열에 대한 배열 포인터를 선언하고 &dDays로 초기화 하였습니다.

 

포인터 배열 변수는 배열 포인터 변수와 생김새가 비슷해서 서로 혼동되기가 매우 쉽습니다. 제대로 차이를 딱 파악하지 않으면, 포인터 학습 하는 것을 더 와따리 가따리 하게 할 수 있는 위험 요소입니다. 오리뎅이가 이 위험 요소는 쓱 제거하도록 하겠습니다. ^^

 

배열 인덱스 연산자 []의 결합 우선 순위가 천상지존의 자리에 있기때문에 식별자와 배열 인덱스 연산자 [] 사이에 괄호가 없으면, 무조건 배열 변수입니다.

식별자 앞에 *가 있고, 이것을 소괄호 () 가 둘러 싸고 있으면, 무조건 포인터 변수입니다.

 

아주 비슷하게 생겼지만, 전혀 다른 그들, 포인터 배열과 배열 포인터를 선언 예제로 비교해 보겠습니다.

 

char const *apArr[10];       // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 포인터 배열입니다.
                                   // 식별자 apArr과 [] 사이에 괄호가 없으면 배열

char const (*paPtr)[10];    // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 배열 포인터입니다. 
                                  // 식별자 paPtr 앞에 *가 있고, 이 것들을 ()가 감싸고 있으면 포인터

 

'char const *apArr[10]' 과 같이 변수의 식별자의 좌우에 * 와 [] 연산자가 각각 있는 경우, 연산자의 결합 우선 순위가 []가 높기 때문에 apArr[10] 배열이 되고, 배열 요소의 데이터 타입이 'char const *'가 됩니다.  10개의 char const * 배열 요소를 초기화 하기 위해서는 아래 그림2의 포인터 배열과 같이 10개의 문자열을 담고 있는 객체가 있어야 합니다. 1개의 배열 객체10개의 char const 객체, 도합 11개의 객체를 사용합니다.

 

'char const (*paPtr)[10];' 와 같이 변수의 식별자와 *를 소괄호로 묶어 놓은 경우는 paPtr은 포인터 변수가 되고, 포인터가 가리키는 두 번째 객체의 데이터 타입이 'char const [10]' 이 됩니다. 다음 그림2의 아래 부분과 같이 1개의 포인터 객체1개의 배열 객체, 도합 2개의 객체를 사용합니다.

 

그림2. 포인터 배열과 배열 포인터의 객체(object) 수 비교

그림2와 같이 그림을 그려서 포인터 배열배열 포인터를 비교해 보면, 좀 더 명확하게 차이점이 보입니다. 포인터 배열은 포인터 변수 10개가 stack memory 영역에 연속으로 배치됩니다. "참조될 수 있는 방법이 있고, 무한 공간이 아닌 크기가 한정된 저장 공간 덩어리가 object"입니다. 배열은 1개의 object입니다.  배열의 10개의 요소가 포인터이기 때문에 포인터를 초기화 하기 위해서는 10개의 객체를 필요로 합니다. 10개의 문자열 리터럴 객체는 각각각 Text section의 .rodata 영역에 배치되고, 10개의 포인터는 각 문자열 리터럴의 첫 번째 요소의 주소로 초기화 됩니다. 반면에 배열 포인터는 포인터 변수 객체 1개가 stack 영역에 잡히고, char 배열 요소로 char 변수 10개가 .rodata section에 연속으로 배치됩니다. 딱 2개의 객체만 있으면 됩니다.

 

const 키워드로 한정한 object가 .rodata 영역에 배치되는 것에 대해서 왜~~에? 하시는 분은 [오리뎅이의 C 포인터 이야기 - 1] 편을 다시 한번 쓰윽 둘러봐 주세요.

다음은 크기가 정해진 다른 배열을 요소로 가지는 다차원 배열 예제입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
 
int main(void){
    int iaArr1[2][4= { { 19216805}, { 19216801} }; // [ ]가 2개면, 2차원 배열
    int iaArr2[3][3= { // [ ]가 2개면, 2차원 배열
                           { 147},
                           { 258},
                           { 369
                         };
    int iaArr3[ ][3= { 2242362482510, }; // [ ]가 2개면, 2차원 배열
    int iaaArr1[ ][2][4= { { { 10101111}, { 1012131} }, // [ ]가 3개면, 3차원 배열
                             { { 10101112}, { 1012131} },
                             { { 10101113}, { 1012131} }, 
                           };
    int iaaArr2[3][2][4= { { { 101112}, { 101111} }, // [ ]가 3개면, 3차원 배열
                              { { 101113}, { 101111} }, 
                            };
    int iaaArr3[ ][2][3= { 224326236339, }; // [ ]가 3개면, 3차원 배열
                    
    printf("sizeof iaArr1  : %3d\n"sizeof iaArr1);
    printf("sizeof iaArr2  : %3d\n"sizeof iaArr2);    
    printf("sizeof iaArr3  : %3d\n"sizeof iaArr3);    
    printf("sizeof iaaArr1 : %3d\n"sizeof iaaArr1);    
    printf("sizeof iaaArr2 : %3d\n"sizeof iaaArr2);    
    printf("sizeof iaaArr3 : %3d\n"sizeof iaaArr3);
    
    printf("\n");
    
    int i, j=0, n;
    n = sizeof(iaArr3)/(sizeof(int* 3);
    for(i = 0; i < n; i++){
        printf("%2d x %2d = %2d\n", iaArr3[i][j], iaArr3[i][j+1], iaArr3[i][j+2]);
    }
    
    printf("\n");
    for(i = 0; i < 3; i++){
        printf("List #%d IP = %d.%d.%d.%d, GW = %d.%d.%d.%d\n", i,
                iaaArr2[i][0][0], iaaArr2[i][0][1], iaaArr2[i][0][2], iaaArr2[i][0][3],
                iaaArr2[i][1][0], iaaArr2[i][1][1], iaaArr2[i][1][2], iaaArr2[i][1][3]);                
    }
 
    printf("\n");
    n = sizeof(iaaArr3)/(sizeof(int* 3 * 2);
    for(i = 0; i < n; i++){
        printf("%2d x %2d = %2d, ", iaaArr3[i][0][j], iaaArr3[i][0][j+1], iaaArr3[i][0][j+2]);
        printf("%2d x %2d = %2d\n", iaaArr3[i][1][j], iaaArr3[i][1][j+1], iaaArr3[i][1][j+2]);
    }
 
    return 0;                        
}
 
-------------------------------------------------------------------------------------------------
 
sizeof iaArr1  :  32
sizeof iaArr2  :  36
sizeof iaArr3  :  48
sizeof iaaArr1 :  96
sizeof iaaArr2 :  96
sizeof iaaArr3 :  48
 
 2 x  2 =  4
 2 x  3 =  6
 2 x  4 =  8
 2 x  5 = 10
 
List #0 IP = 10.11.1.2, GW = 10.11.1.1
List #1 IP = 10.11.1.3, GW = 10.11.1.1
List #2 IP = 0.0.0.0, GW = 0.0.0.0
 
 2 x  2 =  4,  3 x  2 =  6
 2 x  3 =  6,  3 x  3 =  9
cs

참조5. 다차원 배열의 초기화

 

다차원 배열이란 2차원 이상의 배열을 의미하며, 배열 요소로 크기가 정해진 다른 배열을 가지는 배열을 의미합니다. 2차원 배열은 크기가 정해진 1차원 배열을 요소로 가지는 배열입니다. 3차원 배열은 크기가 정해진 2차원 배열을 요소로 가지는 배열입니다. 4차원 배열은 크기가 정해진 3차원 배열을 요소로 가지는 배열입니다. N차원 배열은 크기가 정해진 N-1차원 배열을 요소로 가지는 배열입니다.

 

참조5에서는 다차원 배열을 선언과 함께 즉시 초기화 하는 여러가지 방법들을 2차원 배열과 3차원 배열을 이용해서 살펴 보았습니다.  초기화 리스트에 사용하는 중괄호(curly bracket), {} 하나가 배열 차원 1개를 나타내도록 사용됩니다. 아래 그림3의 3차원 배열 예에서와 같이 가장 안쪽의 {}부터 바깥쪽의 {}으로 차원이 올라갑니다. 3차원 배열을 예로 들어 얘기 하자면, 가장 안쪽 {}이 1차원 배열, 그 다음 {}이 2차원 배열, 그 다음 {}이 3차원 배열을 나타냅니다. 더 고차원의 배열이라 하더라도 이 규칙은 다름이 없이 같습니다. 

 

그림3. 3차원 배열의 dimension

그림3에서 보여 지듯이 1차원 배열의 요소만이 lvalue이고, 특정 data type의 value가 될 수 있습니다. 2차원 배열의 요소는 크기가 정해진 1차원 배열입니다. 3차원 배열의 요소는 크기가 정해진 2차원 배열입니다. N차원 배열의 요소는 크기가 정해진 N-1 차원 배열입니다. 배열은 lvalue가 아닙니다. lvalue가 아니니, lvalue로 사용될 수 없고, lvalue conversion이 일어 날 수도 없습니다. 단, 다음 장에서 이야기 하게될 내용이지만, 배열은 lvalue가 아니라서 lvalue conversion은 일어날 수 없는 반면에, pointer conversion이 일어 날 수 있습니다.

 

참조5의 라인 10,11,18을 보면, 2차원 배열과 3차원 배열들이 각각 선언 되었는데, 최고차원의 배열 인덱스 연산자 안에 상수 숫자가 채워져 있지 않고 [ ]로 비워져 있습니다. 위에서도 말씀 드렸지만, 배열 요소로는 크기가 정해진 배열만이 될 수 있기때문에 배열 크기를 생략하고 선언할 수 있는 것은 최고차원만 가능합니다. 이 때, 배열의 전체 크기는 초기화된 요소의 갯수에 의해서 정해집니다. 배열이 sizeof의 피연산로 사용되는 경우는, 아래 배열과 포인터에서 이야기 될 배열이 pointer conversion 되지 않고 사용되는 딱 3가지 case 중에 하나에 해당합니다. 그래서 라인 30과 43에서와 같이 배열의 전체 size를 하위 배열 요소들의 배열크기와 1차원 배열 요소의 데이터 type 크기를 곱한 것으로 나누면 최고차원의 배열 크기를 구할 수 있습니다.

 

int iaArr3[ ][3= { 2242362482510, };  // sizeof (iaArr3) : pointer conversion이 일어나지 않는다
                                                                   // 1차원 배열 크기(3) * sizeof (int) = 1차원 배열 1개의 size
= sizeof(iaArr3)/(sizeof(int* 3);                        // 지정되지 않은 2차원 배열의 크기를 구할 수 있다.

 

참조5의 라인 10과 18은 초기화 리스트 안에서 하위 차원의 배열을 curly bracket으로 구분하지 않고, list로 나열을 하였습니다. 이 경우 나열된 list 요소들은 1차원 배열의 요소 크기부터 상위 차원 배열로 맞추어 가면서 초기화됩니다.

 

int iaaArr3[2][2][3= { 224326236339 };                // curly bracket, {}으로 배열 구분 없이
                                                                                    //  요소들을 list로 초기화한 경우,

int
iaaArr3[2][2][3= { {224}, {326}, {236}, {339} };          // 먼저 1차원 배열 요소 초기화를 하고,

int
iaaArr3[2][2][3= { { {224}, {326} }, { {236}, {339} } };  // 2차원 배열 요소 초기화하는 방식으로
                                                                                     // 상위 차원으로 묶어 간다

 

참조5의 라인 15에서와 같이 배열 요소가 부분 초기화 된 경우, 초기화 하지 않은 나머지 부분의 1차원 배열 요소들은 0으로 초기화 됩니다.

 

 

지금까지 배열의 밥 아조씨, BoB를 살펴 보았습니다. 참 쉽죠? ~~ 잉! 

 

이제 다음 장에서는 포인터와 연관되어 C를 학습하는 데 심각한 에로(?) 사항중의 하나인 배열과 포인터의 관계에 대해서 BoB 아조씨가 울고 갈 정도로 쉽고 쉽게 썰을 풀어 보겠습니다. 채널 고정!!! 큐~~ (이게 아니나?). 이러려고 했으나.... 배열 BoB 아조씨에 지면을 너무 많이 할당한 관계로 배열과 포인터의 관계에 대해서는 다음 시간을 예약하도록 하겠습니다.

 

뼈때리는 지적의 댓글도, 힘내라 북돋아 주는 격려의 댓글도, 좀 더 디테일을 원하거나 관련된 지식에 대해 질문을 하거나 하는 댓글도 너무 너무 환영합니데이~~~ 댓글 달아 주이~~~~소!!

 

2021년 1월 24일 수원에서 [오리뎅이]