오리뎅이의 네떡세상

'linked list head'에 해당되는 글 1건

  1. [오리뎅이의 C 포인터 이야기 - 3] 오리뎅이 포인터 학습법 2

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