오리뎅이의 네떡세상

[오리뎅이의 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일 수원에서 [오리뎅이]