오리뎅이의 네떡세상

[오리뎅이의 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 포인터 이야기 - 3] 오리뎅이 포인터 학습법

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

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

 

 

10년전에 사우디에서 주재원으로 근무할 때, 처음으로 가족 여행 갔었던, 이집트 후루가다의 리조트 방갈로 풍경이에요. 3박4일동안 리조트 방갈로에 묵었었었어요. 5개의 대륙 식당이 있고, 삼시세끼랑 간식까지 다 포함했어서 먹고 놀고, 먹고 놀고 했었었는데, 사진첩 들추어 보니 그때 기억들이 새록 새록 떠오르네요. 첫 가족 해외 여행이었어서 더 기억에 남는 너무 행복했었던 기억이에요. 홍해의 짙푸른 바다 속으로 풍덩 스쿠버 다이빙을 하기도 하고... 아이들 다 크긴 했지만, 다시 또 가족 여행 길에 오를 날이 곧 오겠죠. 그때까지... 공부 열심히... ㅎㅎㅎ

 

포인터를 바라보는 시선

단도 직입적으로다가 C pointer 이야기를 시작해 보겠습니다. 포인터 이렇게 바라보면 매우 쉽게 이해하고, 해석할 수 있습니다. 

 

오리뎅이 포인터 학습법칙 1 : 포인터 변수가 선언되면, 포인터도 하나의 객체(object)로 간주한다.


int *pA;             // 단일 포인터 변수는   * 객체 1개와 pA 객체 1개, 총 2개의 객체가 선언된 것으로 간주한다
int **ppA;          // 이중 포인터 변수는   * 객체 2개와 ppA 객체 1개, 총 3개의 객체가 선언된 것으로 간주한다
                       // 다중 포인터 변수도 동일하게 *을 하나의 객체로 간주한다. 
오리뎅이 포인터 학습법칙 2 : 오른쪽 객체부터 첫번째로 왼쪽으로 순서를 매긴다.
오리뎅이 포인터 학습법칙 3 : 왼쪽을 오른쪽 객체의 type으로 해석한다



int *pA;             // 첫 번째 pA 객체의 타입은 * (int * : pointer to int)), 두번째 * 객체의 타입은 int
int **ppA;          // 첫 번째 ppA 객체의 타입은 * (int ** : pointer to pointer to int), 
                       // 두 번째 * 객체의 타입은 * (int * : pointer to int)

                       // 세 번째 * 객체의 타입은 int

 

그림1. 단일 포인터 변수의 해석법

 

그림2. 이중 포인터 변수의 해석법

 

오리뎅이 포인터 학습법칙 4 : 다중 포인터의 마지막 객체의 타입만 포인터 타입이 아니다.
                                       마지막 객체는 무슨 타입? 포인터 아닌 타입

 

실제로 이중 포인터 이상의 다중 포인터가 실제 실무에서 쓰일 일은 거의 없지만, 만일 쓰인다 하더라도 해석하는 것은 동일한 해석법을 적용하면 됩니다. 아래는 지난 [오리뎅이의 C 포인터 이야기 - 2] 에서 사용했던 예입니다. 다중 포인터의 경우, 매 마지막 객체의 타입만 int이고, 그 이전의 *들과 E는 모두 포인터 타입입니다.

 


int *E;             // pointer to int : pointer object 
1개와 int object 1개

int **E;           // pointer to pointer to int : pointer object 2개와 int object 1개
int ***E;         // pointer to pointer to pointer to int : pointer object 3개와 int object 1개
int ****E;       // pointer to pointer to pointer to pointer to int : pointer object 4개와 int object 1개
int *****E;     // pointer to pointer to pointer to pointer to pointer to int : pointer object 5개와 int object 1개

 

오리뎅이 포인터 학습법칙 5: 포인터 객체의 모습은 *pA 와 같이 변수와 *가 같이 연결되어 나타난다.
 
int *pA;         // 실제 코드 상에 사용될 때 모습,  pA   : 첫 번째 객체,   *pA : 두 번째 객체
int **ppA;      // 실제 코드 상에 사용될 때 모습,  ppA  : 첫 번째 객체,  *ppA : 두번째 객체,  **ppA; 세번째 객체

 

포인터 변수의 모든 객체는 초기화가 된다

일반 변수의 경우는 local에 선언하는 변수는 선언만 했을때는 dummy 값이 들어 있기 때문에, 처음에 값을 쓰는데 사용하는 경우가 아니고 값을 읽어서 사용하는 경우에는 값을 초기화 한 이후에 사용해야 합니다. 그렇지 않으면, dummy 값을 읽어서 사용하게 되기 때문에 의도하지 않은 결과, bug를 양산하게 됩니다. static이나 global 변수 선언의 경우는 메모리의 BSS 영역에 잡히면서 0으로 초기화가 됩니다. 반면에 포인터 변수는 Local이건 static이건 global이건 자우지당간에 선언하면 무조건 초기화를 해야 되유! 초기화가 되는 것을 잘 살펴 보면, 포인터 객체가 어떤 것인지 쉽게 찾을 수 있습니다.  실제 그런지 간단한 예제를 통해서 살펴 보겠어요.

 

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
#include <stdio.h>
 
int main(void)
{
    int iNum  = 100;                    // stack에 int 변수 iNum 선언되고 100으로 초기화  
    int *piNum = &iNum;                 // stack에 int * 변수 piNum 선언되고, &iNum으로 초기화  
 
    printf("&iNum   = %p\n"&iNum );   // iNum stack 주소 출력 
    printf("&piNum  = %p\n"&piNum );      // piNum stack 주소 출력 
    printf("piNum   = %p\n", piNum );   // 첫 번째 객체 piNum은 &iNum으로 초기화 됨   
 
    printf("iNum    = %d\n", iNum );    // 100
    printf("*piNum  = %d\n"*piNum );  // 두 번째 객체 *piNum = iNum, lvalue conversion
    
    *piNum = 10000;                     // 두 번째 *piNum = iNum, lvalue 
    printf("*piNum  = %d\n"*piNum );  // 두 번째 객체 *piNum = iNum, lvalue conversion
    printf("iNum    = %d\n", iNum );    // 10000
 
    return 0;
}
 
----------------------------------------------------------------------------------------
 
&iNum   = 000000000062FE1C   // stack 메모리 영역
&piNum  = 000000000062FE10         // stack 메모리 영역
piNum   = 000000000062FE1C // iNum의 주소로 초기화 되어 &iNum과 같다
iNum    = 100
*piNum  = 100                 // *piNum은 두 번째 객체(iNum) lvalue conversion
*piNum  = 10000 // *piNum은 두 번째 객체(iNum) lvalue conversion
iNum    = 10000 // *piNum이 두 번째 객체 iNum이므로 iNum이 바뀜
 
cs

참조1. 오리뎅이 C 포인터 이야기 첫 번째 예제 코드

 

참조1은 처음 포인터 학습을 위해 아주 흔하게 사용하는 예제 코드입니다. main 함수 내부에 int 타입 변수 iNum과 int * 타입 변수 piNum은 선언하였습니다. iNum은 정수 100으로 초기화 하였고, piNum은 선언과 동시에 &iNum으로 초기화 하였습니다.

 

포인터 변수 piNum의 자료형은 int * 입니다. 이를 우리말로 읽을 때는 보통 "int 포인터"라고 읽습니다. 영어 표현으로는 "pointer to int" 입니다. 직역해 보면, int에 대한 포인터 정도가 될텐데요. "int 포인터"라고 부르는 게 짧고 의미를 그대로 함축한 모습으로 직관적인 것 같습니다.

 

"변수 앞에 &(ampersand)를 붙이면  포인터가 된다"는 내용은 [오리뎅이의 C 포인터 이야기 - 2] 편에서 자세하게 이야기 했었습니다. [오리뎅이의 C 포인터 이야기 - 1] 편에서는 변수들의 주소를 출력해서 어떤 메모리 위치에 배치 되는지 확인해 보기 위해서, printf 문에서 변수 앞에 &를 붙여 주소를 출력하는 데에 사용했었구요. 2편에서는 주소(포인터) 자료형의 크기를 알아 보기 위해서 각각의 자료형 변수들의 자료형 앞에 &를 붙여서 sizeof 연산으로 64bit 운영체제인 오리뎅이 PC에서 주소의 크기가 모두 8 bytes라는 것도 확인해 보았었습니다. 

 

변수 이름 앞에 &(ampersand) 연산자를 붙이면, 연산 결과는 그 변수의 주소(포인터) 타입이 됩니다. 그래서 포인터 변수는 참조1에서 "int *piNum = &iNum;"과 같이 변수의 이름 앞에 &를 붙여 대입 연산으로 초기화 할 수 있습니다. 가장 흔하게 사용되는 포인터 변수의 초기화 방법입니다. 포인터 변수를 초기화 하는 다른 방법들로는 메모리를 동적할당 받아서 할당 받은 메모리의 시작 위치로 초기화 하는 방법과 정수 값 자체를 포인터 타입으로 캐스팅해서 초기화 하는 방법이 있습니다. 다른 초기화 방법들에 대해서는 이후 다른 글에서 자세히 알아 보겠습니다.

 

포인터를 보는 순간 몇 번째 객체에 access 하는 지를 그냥 뭐 딱 안다. 

참조1에서 int 변수 iNum과 포인터 변수 piNum이 main 함수 내부에 local variable로 선언되고 초기화 된 것을 보면서 저희는 그냥 딱 아래 그림3과 같은 이미지를 떠 올릴 수 있지 말입니다. 참조1의 예제 코드를 컴파일해서 실행하면, 실행 환경에서 iNum과 piNum 변수는 그림3의 왼쪽 이미지의 Stack 영역에 iNum과 piNum이 8bytes 공간을 차지하도록 표시되어 있는것 처럼 각각 Stack 영역에 배치가 됩니다.  C 프로그래밍 메모리 Layout에 대해서 깅가밍가 하시는 분은  [오리뎅이의 C 포인터 이야기 - 1] 편을 살짝 읽어 봐 주시~~~소!!

 

그림3. stack에 자리 잡은 변수와 두 번째 객체 *piNum = iNum

iNum과 piNum 각각이 초기화 된 것을 보면 저희는 그림3의 오른쪽과 같이 포인터 변수인 'int *piNum'의 첫 번째 객체 piNum과 두번째 객체 *piNum의 공간적 image를 떠 올려 볼 수 있습니다. 저희는 이제 *를 보면 그게 몇번째 객체이고, 어떤 object(객체)인지를 바로 딱 알 수 있기 때문에 코드에서 어느 위치에서 포인터를 만나도 당황스럽지 않습니다. 정~엉~~말? 참조1의 13 라인에서 *piNum(딱 보면 두 번째 객체 : piNum이 첫 번째, *piNum이 두 번째)이 printf 문에서 사용되었습니다. lvalue conversion이 일어나는 위치에 사용되었으므로 value로 사용됩니다. 두 번째 객체이고, "*piNum = iNum" 이므로 iNum의 value 100이 28번 라인에서 출력된 것을 볼 수 있습니다. 15 라인에서 다시 *piNum이 사용 되었습니다. 그런데 이번에는 대입 연산자 =의 좌항 피연산자로 사용되었습니다. lvalue conversion이 일어나는 위치가 아니므로, 그냥 lvalue로 사용되어 두번째 객체인 *piNum, 즉 iNum의 주소 지정자로 사용되어 iNum 메모리에 10000 을 대입하는데 사용하였습니다.

 

다중 포인터를 만나더라도 사정은 달라지지 않습니다. 저희는 딱 보면, 이게 몇번째 객체를 access 하는 지 그냥 애~~~애~~ 앱니다. ^^.  이중 포인터 예제 하나 더 보시고 가실께요. 이중 포인터 선언하고, 초기화 되는 것을  유심히 살펴 보시고, 코드에서 두번째 객체, 세번째 객체가 각각 어덯게 사용되는지도 딱 보고 판단해 보세요.  ^^ 

 

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
#include <stdio.h>
 
int main(void)
{
    int iNum  = 100, iVal = 200;            // stack에 int 변수 iNum 선언되고 100으로 초기화  
    int *piNum = &iNum;                     // stack에 int * 변수 piNum 선언되고, &iNum으로 초기화
    int **ppiNum = &piNum;                  // stack에 int ** 변수 ppiNum 선언되고, &piNum으로 초기화  
 
    printf("&iNum   = %p\n"&iNum );       // iNum stack 주소 출력 
    printf("&piNum  = %p\n"&piNum );      // piNum stack 주소 출력
    printf("&ppiNum = %p\n\n"&ppiNum );   // ppiNum stack 주소 출력   
    
    printf("piNum   = %p\n", piNum );       // 첫 번째 객체 piNum은 &iNum으로 초기화 됨 
    printf("ppiNum  = %p\n\n", ppiNum );    // 첫 번째 객체 ppiNum은 두번째 객체 주소 &piNum으로 초기화 됨
    
    printf("*ppiNum  = %p\n"*ppiNum );    // 두 번째 객체 *ppiNum = piNum, lvalue conversion
    printf("piNum   = %p\n", piNum );       // 
    printf("**ppiNum = %d\n\n"**ppiNum ); // 세  번째 객체 **ppiNum = *piNum, lvalue conversion
        
    *ppiNum = &iVal; // 이중 포인터는 두 번째 객체를 변경하려고 할때 주로 사용
    
    printf("*ppiNum  = %p\n"*ppiNum );    // 두 번째 객체 *ppiNum = piNum, lvalue conversion
    printf("piNum   = %p\n", piNum );       // piNum은 이중 포인터 변수 ppiNum의 두번째 객체와 같다  
    printf("**ppiNum = %d\n"**ppiNum );   // 세  번째 객체 **ppiNum = *piNum, lvalue conversion
 
    return 0;
}
 
------------------------------------------------------------------------------------------------------
 
&iNum    = 000000000062FE1C
&piNum  = 000000000062FE10
&ppiNum  = 000000000062FE08
 
piNum    = 000000000062FE1C
ppiNum  = 000000000062FE10
 
*ppiNum  = 000000000062FE1C
piNum   = 000000000062FE1C
**ppiNum = 100
 
*ppiNum  = 000000000062FE18
piNum   = 000000000062FE18
**ppiNum = 200
cs

참조2. 이중 포인터는 뭐 별 다르간듀?

 

이중 포인터는 왜 씁니까? 라고 물으~신다면 눈물의 씨앗이라 말하지 마~~요. ^^ 요즘 트로트를 너무 많이 봤더니 후유증이 나타나는 중입니다. 이중 포인터 변수도 그림으로 쓱 싹 그려 보면 요런 이미지가 딱 그려 집니다

 

그림4. stack에 자리 잡은 변수들과 두 번째 객체 *ppiNum과 세 번째 객체 **ppiNum

 

이중 포인터를 쓰는 경우는 대개 세 번째 객체에 접근하려는 목적보다는 두 번째 객체에 접근해서 두 번째 객체의 value(세 번째 객체의 주소)를 변경하려는 경우가 많습니다. 참조2의 16 라인과 22 라인은 이중 포인터 변수인 ppiNum을 통해서 두 번째 객체인 *ppiNum 을 출력해 보고 있습니다. lvalue conversion이 일어나서 두 번째 객체인 piNum의 value인 iNum의 주소 &iNum이 출력됩니다. 31라인, 35라인, 38라인의 출력된 주소가 모두 동일한 것을 볼 수 있습니다. 열 여덟번째 라인과 24 라인은 세 번째 객체인 **ppiNum을 출력하고 있습니다. 20 라인에서 두 번째 객체를 "*ppiNum = &iVal"로 새로운 주소 &iVal로 변경하고 있습니다. 이는 *ppiNum이 두 번째 객체이므로 piNum의 value가 &iNum에서 &iVal로 변경됩니다. 이로 인해서 열 여덟번째 라인과 24 라인에서 출력한 세 번째 객체인 **ppiNum이 100에서 200으로 변경된 것을 확인할 수 있습니다. 이는 세 번째 객체를 변경해서 나타난 결과가 아니라 아래 그림 5와 같이 두 번째 객체를 변경해서 세 번째 객체도 자동 변경 된 것임을 알 수 있습니다.

 

그림5. 이중 포인터에서 두 번째 객체를 바꾸면, 세 번째 객체도 자동빵으로 바뀐다

 

 

포인터가 사용되는 이유 Top 5

포인터 꼭 사용해야 합니까? 포인터 안쓰면 프로그램 못 만듭니까? 물론 고거슨 아닐테지만, C에서는 포인터가 특별한 기능을 가지고 있어서 반드시 사용을 해야 하는 경우도 있고, 사용하는 경우가 사용하지 않는 것보다 훨씬 효율적으로 코딩을 할 수 있는 경우도 있습니다. 다음은 오리뎅이 차트 "포인터가 사용되는 이유 Top 5"입니다.

 

1. 피호출 함수(callee function)에서 호출 함수(caller function)의 local 변수에 access path
2. 자료형의 크기만큼 주소를 반복 증가 또는 반복 감소하기 위한 용도 (like C++ 반복자)
3. 자료구조 linked list (prev, next)
4. Base 주소를 이용한 상대 주소 access (embedded에서 많이 사용)
5. 함수의 포인터

참조3. 포인터가 사용되는 이유 Top 5 (출처 : 오리뎅이 차트)

 

참조3의 출처는 오리뎅이 내 맘대로 차트입니다. 왜냐고 묻지도 따지지도 마시기 바랍니다. ^^. 포인터가 사용되는 이유 Top 5에서 대망의 1위, 예제로 확인 들어갑니다. 이번 예제 코드는 4가지 함수를 main에서 호출하는 코드라서 조금 깁니다. 길지만, 함수 하나 하나를 보면, 어렵지 않은 내용입니다. 위에서부터 함수 하나씩 쭈욱 쭈욱 보시면서 포인터(*)가 나오면, 이거시 어느 객체로 초기화 되었는지를 호출 함수(caller)인 main에서 확인해 보시고, 과연 포인터가 몇 번째 객체에 access 하는 것인지를 딱 보고 딱 맞춰 보세요.  딱 보면 앱~~니다. ^^

 

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <stdio.h>
 
void try_value_swap(int iNum, int iVal)            // 호출 함수(caller)의 local 변수는  
{                                                   // 피호출 함수에서 직접 access 불가
    int temp ;
 
   temp = iNum;
   iNum = iVal;
   iVal = temp;
}
 
void value_swap(int *piNum, int *piVal)             // pointer를 호출 함수의 local 변수에
{                                                   // access 할 수 있는 access path로 이용  
    int temp ;
 
    temp = *piNum; // 두 번째 객체, iNum value를 temp에 저장
    *piNum = *piVal; // 두 번째 객체, iNum = iVal; 과 같음
    *piVal = temp; // 두 번째 객체, iVal = temp; 와 같음
}
 
 
void address_swap(int **ppiNum, int **ppiVal)       // 호출 함수의 단일 pointer 변수에  
{                                                   // access 할 경우에는 이중 pointer 사용 
    int *temp ;
 
    temp = *ppiNum; // 이중 포인터, 두 번째 객체 access
    *ppiNum = *ppiVal; // 두 번째 객체, lvalue = lvalue conversion
    *ppiVal = temp; // 두 번째 객체, piVal = temp; 와 같음
}
 
void value_swap_by_dptr(int **ppiNum, int **ppiVal) // 이중 포인터를 사용하면 호출 함수의  
{                                                   // 일반 변수의 value에도 access 할 수 있다  
    int temp ;                                      // 어떤 특별한 이유가 없는한 이렇게 왜 씀?  
 
    temp = **ppiNum; // 이중 포인터, 세 번째 객체, temp = iNum;
    **ppiNum = **ppiVal; // 세 번째 객체, lvalue = lvalue conversion
    **ppiVal = temp; // 세 번째 객체, iVal = temp; 와 같음
}
 
 
int main(void)
{
    int iNum  = 100, iVal = 200;                                
    
    
    printf("###### try_value_swap() ######\n"); 
    printf("Before : iNum  = %d, iVal = %d\n", iNum, iVal );   
    
    try_value_swap(iNum, iVal);          // 1 일반 변수를 함수 인자로 넘기면 값 복사가 일어 난다
    
    printf("After  : iNum  = %d, iVal = %d\n", iNum, iVal ); 
    
    printf("\n"); 
    
    printf("###### value_swap() ######\n"); 
    printf("Before : iNum  = %d, iVal = %d\n", iNum, iVal ); 
    
    value_swap(&iNum, &iVal);            // 2 주소(포인터)를 인자로 넘기면 주소가 복사된다
    
    printf("After  : iNum  = %d, iVal = %d\n", iNum, iVal );  
 
    printf("\n"); 
               
    printf("###### address_swap() ######\n");
    iNum = 100, iVal = 200;              // iNum, iVal 값 원복  
    int *piNum = &iNum, *piVal = &iVal;
    printf("Before : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("Before : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("Before : piNum  = 0x%p, piVal = 0x%p\n\n", piNum, piVal ); 
    
    address_swap(&piNum, &piVal);        // 3 포인터 변수의 주소를 넘기면 이중 포인터가 된다
    
    printf("After  : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("After  : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("After  : piNum  = 0x%p, piVal = 0x%p\n", piNum, piVal );
 
    printf("\n"); 
               
    printf("###### value_swap_by_dptr() ######\n");
    iNum = 100, iVal = 200;              // iNum, iVal 값 원복
   address_swap(&piNum, &piVal); // piNu, piVal 주소 값 원복
 
    printf("Before : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("Before : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("Before : piNum  = 0x%p, piVal = 0x%p\n\n", piNum, piVal ); 
    
    value_swap_by_dptr(&piNum, &piVal);  // 4 이중 포인터를 넘기면, 세 번째 객체에도 Access 가능
    
    printf("After  : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("After  : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("After  : piNum  = 0x%p, piVal = 0x%p\n", piNum, piVal );
    
    return 0;
}
 
 
------------------------------------------------------------------------------------------------
 
###### try_value_swap() ######
Before : iNum  = 100, iVal = 200
After  : iNum  = 100, iVal = 200
 
###### value_swap() ######
Before : iNum  = 100, iVal = 200
After  : iNum  = 200, iVal = 100
 
###### address_swap() ######
Before : iNum   = 100, iVal   = 200
Before : *piNum = 100*piVal = 200
Before : piNum  = 0x000000000062FE1C, piVal = 0x000000000062FE18 // 주소가 바뀜
 
After  : iNum   = 100, iVal   = 200
After  : *piNum = 200*piVal = 100
After  : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C // 주소가 바뀜
 
###### value_swap_by_dptr() ######
Before : iNum   = 100, iVal   = 200
Before : *piNum = 100*piVal = 200
Before : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C
 
After  : iNum   = 200, iVal   = 100
After  : *piNum = 200, *piVal = 100
After  : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C
 
 
 
cs

참조4. 호출 함수(caller)의 지역 변수에 access하기 위해서 포인터가 사용된다

 

 

참조1의 4가지 함수를 함수 선언과 main에서의 함수 호출을 각각 살펴 보면서 설명을 해 보겠습니다.

 

첫 번째 함수 : try_value_swap(int iNum, int iVal)

    함수 정의 :
             try_value_swap(int iNum, int iVal)          // iNum, iVal은 try_value_swap()의 local 변수
    함수 호출 :
            t
ry_value_swap(iNum, iVal);                     //  iNum, iVal은 main() 의 local 변수

 

try_value_swap 함수에서 iNum의 iVal은 각각 local 변수로 선언됩니다. main 함수에서 함수 호출 시 사용된 iNum과 iValu은 main 함수의 local 변수입니다. main()과 try_value_swap()에서 서로 이름은 같게 사용했지만, 서로 다른 변수입니다. try_value_swap 함수에서 iNum과 iVal의 swap을 시도하지만 이는 try_value_swap 함수의 지역 변수 iNum과 iVal만 서로 swap 될 뿐, main 함수의 iNum과 iVal에는 영향을 미치지 않습니다.

 

두 번째 함수 :  value_swap(int *piNum, int *piVal)

    함수 정의 : 
            value_swap(int *piNum, int *piVal)         // 포인터 변수 piNum = &iNum, piVal = &iVal로 각각 초기화 됨
    함수 호출 : 
           
value_swap(&iNum, &iVal);                      // iNum과 iVal의 주소 &iNum, &iVal 을 인자로 넘김 

 

함수 인자로 주소(포인터)를 넘기고, 함수에서 매개 변수를 포인터 변수로 받으면, 주소가 포인터 변수로 복사되어 포인터 변수가 초기화 됩니다. value_swap 함수에서 local 변수로 선언된 포인터 변수 piNum과 piVal은  각각 main 함수에 선언된 iNum과 iVal의 주소가 복사 되어, &iNum 그리고 &iVal로 초기화가 됩니다. 이거슨 저희가 참조1과 참조2에서 포인터 변수의 초기화에 사용했던 아래 표현식과 동일합니다. 

 

piNum = &iNum;
piVal = &iVal;

 

main 함수의 iNum과 iVal이 각각 포인터 변수 piNum과 piVal의 두 번째 객체로 초기화 되었기 때문에 함수 본문에서 두 번째 객체에 access하면 main 함수의 iNum과 iVal에 access 하게 됩니다.  이렇게 포인터(주소)를 함수 인자와 매개 변수로 사용하면, 피호출 함수에서 호출 함수의 local 영역에 있는 변수에 access 하여 값을 읽거나, 값을 바꿀 수 있는 access path가 만들어 집니다.  오리뎅이 차트 C에서 "포인터를 사용하는 이유 Top 5"의 넘버 원이 바로 이것이었습니다. 함수에 단일 포인터가 사용되었다면, 그거슨 피호출 함수에서 호출 함수의 영역에 있는 일반 변수에 access 하려는 이유로 사용하는 것입니다.

 

세 번째 함수 : address_swap(int **ppiNum, int **ppiVal)

    함수 정의 : 
            address_swap(int **ppiNum, int **ppiVal)     // 이중 포인터 변수 ppiNum = &piNum, ppiVal = &piVal
    함수 호출 : 
            address
_swap(&piNum, &piVal);                      // piNum과 piVal의 주소 &piNum, &piVal 을 인자로 넘김 

 

이중 포인터 대관절 머땀시 쓴답니까? 10중 8,9는 피호출 함수에서 호출 함수의 local 영역에 있는 단일 포인터 변수를 access 하여 값을 읽거나, 값을 바꾸려는 목적으로 이중 포인터를 사용합니다. 함수 매개 변수에 (int **ppiNum,  int**ppiVal) 과 같이 이중 포인터로 선언하고, 함수 본문에서 '*ppiNum = *ppiVal;'과 같이 사용하면, 이중 포인터 변수의 두 번째 객체에 각각 access 합니다. 대입 연산자(=)의 좌항 피연산자로 사용된 *ppiNum은 lvalue로 사용 되어 두 번째 객체인 piNum이 됩니다. 대입 연산자의 우항 피연산자로 사용된  *ppiVal은 역시 두 번째 객체인 piVal이 되는데, lvaule conversion 되어 piVal의 값인 &iVal, 즉 iVal의 주소 값으로 변환됩니다. 

 

그림6. before address_swap()

 

그림7. after address_swap()

실제 iNum의 값과 iVal의 값은 바뀌지 않았지만, 그림7과 같이 piNum과 piVal이 서로 바뀌었기 때문에 ppiNum의 세 번째 객체의 값을 출력해 보면, 값이 swap 된 것처럼 보입니다.  67~69라인과 73~75라인의 출력 결과인 108~110라인과 112~114라인을 보면, iNum과 iVal은 함수 수행 전과 후에 변함이 없습니다. 반면에 *piNum과 *piVal은 출력 전과 후에 값이 서로 바뀌어 출력되는 것을 볼 수 있습니다. 그리고, 그렇게 보이는 이유가 세번째 라인의 출력결과와 같이 piNum과 piVal의 주소가 서로 뒤 바뀌었기 때문임을 알 수 있습니다.

 

네 번째 함수 : value_swap_by_dptr(int **ppiNum, int **ppiVal)

이중 포인터를 함수의 매개 변수로 사용하면, 호출 함수의 local 영역에 있는 두 번째 객체 뿐만 아니라 세 번째 객체에도 접근할 수 있습니다. 네 번째 함수는 함수 본문에서 세 번째 객체에 접근해서 직접 세 번째 객체의 값을 서로 swap 하는 것을 보여 주고 있습니다.  함수 본문에서 '**ppiNum = **ppiVal;' 와 같이 이중 포인터가 사용되어 각각 세 번째 객체에 접근합니다.  직접 세 전째 객체인 iNum과 iVal에 접근하여 서로 swap을 하는 것이기 때문에 첫 번째 함수 value_swap() 과 동일하게 iNum과 iVal이 swap 된 결과가 나오는 것을 볼 수 있습니다. 따라서, 호출 함수의 포인터 변수가 아닌 일반 변수의 값을 access 하고자 하는 경우에는 그냥 단일 포인터를 사용하지, 이중 포인터를 사용할 필요가 없습니다. 이중 포인터를 사용했다는 것은 머니 머니 해도 호출 함수의 local 영역에 선언된 단일 포인터 변수를 access 하고자 할 때 사용하는 경우가 많습니다.

 

 

포인터(*)가 보이면 포인터가 몇개 붙어 있는지에 따라서 딱 이게 몇 번째 객체, 누구를 access 하는 것이구나가 딱 보이시나요? 딱 딱 보이신다고 믿~슙니다.

 

이중 포인터를 사용하는 한가지 예를 더 보고 오늘의 이야기를 마칠까 합니다. C에서 이중 포인터 얘기 하면 절대 빠질 수 없는 존재감 뿜뿜, 바로 바로~~~, linked list의 head 입니다.

 

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <stdio.h>
 
typedef struct _Student Student;
 
struct _Student
{
    char name[20];
    int IdNum;
  int Kor;
  int Eng;
  int Math;
  float Avg;
    Student *next;
};
 
void db_memory_alloc(Student **ppNew)
{
    *ppNew = (Student *)malloc(sizeof(Student));    // 이중 포인터를 사용하여 main 함수 영역에 선언된  pHead를 
}
 
void print_student_data(Student *pNew)
{
    printf("\n================= ID %d : %s's score start =====================\n", pNew->IdNum, pNew->name);
 
    printf("Input student %s's korean score  : %d\n", pNew->name, pNew->Kor );
    printf("Input student %s's english score : %d\n", pNew->name, pNew->Eng );
    printf("Input student %s's math score    : %d\n", pNew->name, pNew->Math );
 
    printf("Input student %s's Average       : %.2f\n", pNew->name, pNew->Avg );
    
    printf("\n================= ID %d : %s's score end   =====================\n", pNew->IdNum, pNew->name);
}
 
void fill_student_data(Student *pNew)
{
    printf("Input student's name: ");
    scanf"%s"&pNew->name );
    printf("Input student's IdNum: ");
    scanf"%d"&pNew->IdNum );
    printf("Input student's korean score: ");
    scanf"%d"&pNew->Kor );
    printf("Input student's english score: ");
    scanf"%d"&pNew->Eng );
    printf("Input student's math score: ");
    scanf"%d"&pNew->Math );
    printf("\n");
    
    pNew->Avg = (pNew->Kor + pNew->Eng + pNew -> Math)/3;
    
    // print_student_data(pNew);    open for debugging purpose
}
 
void insert_data_to_db(Student **ppHead, Student *pNew)
{
    if(*ppHead == NULL){            // 첫 번째 list insert
        pNew->next = NULL;
        *ppHead = pNew;
        return
    }
    
    Student *pTemp = *ppHead;
    Student *pPrev = *ppHead;
    
    while(1){
        if(pTemp->IdNum > pNew->IdNum){
            if(pTemp ==  *ppHead){    // Head 자리에 Insert 
                pNew->next = pTemp;   // Insert할 자료의 next만 head로 바꿔 주면 끝 
                *ppHead = pNew;       // 이중 포인터를 용하여 main 함수에 선언된 pHead를 pNew로 변경 
            }else{                    // middle 위치에  추가 
                pNew->next = pTemp;
                pPrev->next = pNew;
            }
            break
        }else if (pTemp->IdNum < pNew->IdNum){
            pPrev = pTemp;             
            if(pTemp->next == NULL){  // tail에 추가
                pNew->next = NULL;
                pTemp->next = pNew;
                break;
            }
            pTemp = pTemp->next;
        }else{
            printf("Wrong IdNum %d, alread exist!!\n", pTemp->IdNum);
            break;
        }
    }
}
 
    
void display_student_db(Student *pHead){
    printf("\n=======================================================\n");
    printf("Name    IdNum    Kor    Eng    Math    :    Average\n");
    printf("=======================================================\n");
 
    while(pHead != NULL){
 
        printf("%-8s%-9d%-7d%-7d%-8d    %-7.2f\n",pHead->name, pHead->IdNum,
                                                  pHead->Kor, pHead->Eng, pHead->Math, pHead->Avg);
        pHead = pHead->next;
    }
    printf("=======================================================\n");
}
 
 
int main(void)
{
    Student *pHead = NULL;             // main 함수의 local 영역에 포인터 변수 pHead 선언 
    Student *pNew = NULL;              // main 함수의 local 영역에 포인터 변수 pNew 선언
    int Number = 0;
 
    while(1){
        db_memory_alloc(&pNew);         // memory_alloc 함수에서 main 함수 local 영역에 있는 pNew에 
        if(pNew == NULL){               // malloc으로 할당된 주소를 write 할 수 있게 이중 포인터 사용  
            printf("memory allocation fail!!\n");
            return 1;
        }
        
        fill_student_data(pNew);
        
        insert_data_to_db(&pHead, pNew);// memory_alloc 함수에서 main 함수 local 영역에 있는 pHead를   
                                       // insert_data_to_db 함수에서 변경할 수 있도록 이중 포인터 사용  
        Number++;
        if(Number >= 3)
            break;
    }
 
    display_student_db(pHead);
    
    return 0;
}
 
cs

참조5. 이중 포인터를 사용하는 linked list 예제

 

참조5는 학생들의 이름과 번호를 입력하고, 국, 영, 수 점수를 입력하면 평균을 계산하여 DB에 낮은 번호 순서대로 저장하였다가 순서대로 출력해 주는 예제 프로그램입니다. Linked list에서 이중 포인터를 사용하는 예를 보이기 위해서 만들어 본 예제 코드입니다. 복붙해서 실행 시켜 보시고, 데이터를 입력해서 결과가 어떻게 나오는지도 한번 보세요.

 

예제 코드의 두  함수에서 이중 포인터를 사용하고 있습니다. db_memory_alloc() 함수와 insert_data_to_db() 함수에서 각각 사용하고 있습니다. main 함수의 local 영역에 선언한 단일 포인터 변수 pNew와 pHead를 함수에서 access 해서 변경하기 위해서 이중 포인터를 사용하고 있습니다. db_memory_alloc() 함수에서는 18 라인에서 memory를 malloc으로 동적할당해서 *ppNew, 즉, 이중 포인터 변수 ppNew의 두 번째 객체에 write 해 줌으로써 main 함수의 local 변수인 pNew를 초기화 합니다. 

 

insert_data_to_db() 함수에서는 새로운 학생의 data를 IdNum이 작은 순서로 linked list DB에 insert 할 때, pHead의 자리에도 insert할 수 있도록 이중 포인터를 사용합니다. 즉, 참조5의 65~68 라인을 보면, 새로 입력된 학생의 IdNum이 pHead의 IdNum보다 작을 경우, *ppHead 에 pNew를 대입하고, *ppHead를 다음 위치(next)로 바꾸어 주고 있습니다.

 

이번 이야기 [오리뎅이 포인터 학습법]은 여기에서 마치겠습니다. [오리뎅이 포인터 학습법]의 key point는 "포인터가 사용되는 구문을 보면, 딱 몇 번째 객체에 access 하는 것인지를 한눈에 파악하자" 입니다. 오리뎅이가 생각하기에는 포인터 누가 누구를 가리키고, 또 누구는 누구를 가리키고, 이렇게 찾아 찾아 찾아 따라 가는 포인터 학습법은 실체(객체)가 잘 보이지 않아서 직관적이지 않았던 것 같습니다. 이제 저희는 포인터 변수가 나오면, 첫 번째 객체, 두 번째 객체, 세 번째 객체... 등으로 포인터가 access할 수 있는 각 객체들이 무엇인지 확인하고, 코드 본문에서 포인터를 만났을 때, 딱 보면 몇번째 객체에 access 하는 지를 한 눈에 파악할 수 있습니다.  

 

[오리뎅이의 C 포인터 이야기]는 배열, 문자열, 함수 포인터, 복잡한 복합구문으로 이어집니다. 다른 이야기 주제들도 어떻게 설명을 하면 처음 학습 하시는 분들도 무난하게 받아 들이실 수 있을까를 고민 고민한 학습법으로 돌아 오겠습니다. 긴글 끝까지 읽어 주셔서 고맙습니다.

 

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

 

2021년 1월 17일 수원에서 뒤뚱뒤뚱~~~~ [오리]