written by chanywa, 2014-11-21
blog : http://chanywa.com
e-mail : chany@chanywa.com

"포인터도 머리 아픈데, 이중포인터는 또 뭐야... ㅠ.ㅠ"

이런 생각을 하는 분들께, 개념정리하는데 도움이 되지 않을까 하는 생각에 몇자 적어봅니다.

저는 사실 '이중포인터' 라는 말은 의미가 없다고 생각합니다.
포인터의 포인터라는 뜻으로 이중포인터라고 이름을 붙인 것 같은데, 실제로는 그냥 똑같은 포인터입니다.
그렇다면, 왜 이중포인터가 존재하며, 이것을 사용하느냐라는 문제를 고민해볼 수 있을 것입니다.

이중포인터는 특별한 용법이나 카테고리라고 한정하기는 힘들기 때문에,
이 글에서는 그 중에서도 서브함수 호출시의 이중포인터 사용에 대해서 잠시 살펴보도록 하겠습니다.

 

 

call by value, call by reference

이중포인터의 존재이유를 살펴보기전에, 먼저 생각해볼 것이 있습니다.
혹시 C 공부를 하면서 "call by value, call by reference" 라는 말을 들어보신 적이 있나요.
제 생각에는 이 개념을 먼저 짚고 넘어가는게 좋을 듯 합니다.
아래 소스를 한번 보도록 하겠습니다.

 

void call_by_value(int val)
{
        val = 20;
}

void call_by_refer(int *ref)
{
        *ref = 20;
}

int main()
{
        int value = 10;
        int refer = 10;

        printf("before : value=%d, refer=%d\n", value, refer);
        call_by_value(value);
        call_by_refer(&refer);
        printf("after  : value=%d, refer=%d\n", value, refer);
}

 

이런 류의 프로그램을 한번쯤 봤다 싶은 분들이 많으실 겁니다.

 

간단히 프로그램을 살펴보겠습니다.

value, refer 변수 둘다 초기값은 10 입니다.
이 동일한 값을 가진 2개의 변수를 서브함수의 인자로 전달하고, 서브함수내에서 다른 값으로 변경한 후의 상태를 확인해보기 위한 것입니다.
두 함수의 차이가 있다면 call_by_value()는 int 변수값을 그대로 전달하고, call_by_refer()는 int 변수의 주소값을 전달합니다.
만약 두 경우의 차이를 모르신다면, 둘 다 20으로 변경된다고 생각하실 것이고, 차이를 아신다면 refer 만 20으로 변경된다고 생각하시겠지요.

 

실행결과는 이렇습니다.

before : value=10, refer=10
after  : value=10, refer=20

 

이 경우의 차이를 잘 모르겠다 싶으시면, 아래 내용을 읽어봐주세요.

 

함수를 호출할 때 전달하는 인자는 변수 자체를 전달하는 것이 아니라, 변수에 저장된 값을 전달합니다. 그리고 서브함수 내에서 별도의 변수를 만들어 값을 담아서 사용하게 되죠..
즉, main()에서의 value와 call_by_value()의 val, main()에서의 refer와 call_by_refer()에서의 ref 변수는 서로 다른 메모리에 존재하는, 사실상 각각 별개의 변수로서 존재하게 됩니다.
그렇기 때문에, call_by_value() 에서 val = 20; 을 실행하더라도 main()에서의 value는 변경되지 않습니다. 그리고 함수가 종료되면서 val는 사라지고, main()로 되돌아왔을때의 value는 여전히 10인 상태입니다.

 

그러면, call_by_refer() 에서 *ref = 20; 을 실행했을 때는 왜 refer에 20이 저장이 되었을까요.

그 이유는 바로, ref 라는 변수에다 저장을 한 것이 아니라, *ref 즉 ref가 가지고 있는 주소값 위치에 20을 저장했기 때문입니다. 왜냐하면 ref는 &refer였으므로 refer의 주소를 가지고 있는데, *ref는 ref가 가지고 있는 주소에 해당하는 메모리의 값을 뜻하기 때문에, refer의 값을 의미합니다.

그러므로, call_by_refer(&refer)를 실행함으로써 main()에서의 refer를 전달받은 것이 아니라, refer 변수의 주소값을 전달받은 것이고, 그 주소값에 해당하는 위치에다 20 이란 값을 저장하게 되니, *ref = 20 은 ref 에 저장하는 것이 아닌, refer에 저장하는 것이 되는 셈입니다.

 

 

double pointer = pointer ?

이중포인터 얘기로 넘어가기전에 한가지만 더 생각해보고 가시죠.

"call_by_refer()와 call_by_value()에 대해서 얘기를 할때, call_by_refer()는 주소값을 넘긴 것이고, call_by_value()는 변수값을 넘긴 것이다."

이 말에 대해서 동의하시나요?
위에서 한창 언급했던 내용들을 이해하셨다면, 아마도 이 말에도 동의하실 것입니다.

 

그럼, 이번에는 여기서 조금만 말을 바꾸어, 다르게 생각을 해보겠습니다.

"call_by_value()는 값을 넘긴 것이고, call_by_refer()도 값을 넘긴 것이다.
다만, call_by_value()에서는 int형 값을 넘긴 것이고, call_by_refer()에서는 int 주소형 값을 넘긴 것이다."

이 말에 대해서는 어떻게 생각하세요?
저는 이전 글과 같은 뜻이라고 생각되는데요, 여러분들도 그렇게 생각되시려나요.

C 문법상으로는 포인터 변수와, 일반 변수를 구분을 하지만, 사실상 그냥 다 변수이고, 다 값입니다.
실제 메모리상에서는 주소와 값이 구분되는 것도 아니고, 2진수와 16진수, 10진수가 구분되는 것도 아닙니다.
그러므로 두 글은 동일한 뜻이긴 합니다만, 저는 후자의 경우가 더 정확한 의미라고 생각합니다.
특히 이중포인터의 이해를 돕고자 하는 측면에서는 더욱 말입니다.

그럼, 이 말을 기억한 채로 이중포인터로 넘어가보겠습니다.

 

 

double pointer !!

포인터의 포인터, 포인터 기호가 2개(혹은 그 이상)인 이중포인터.
"이것도 그냥 포인터일 뿐인데, 이걸 언제, 왜 써야 하나? 머리아프게~"

그런데요, 이 글을 앞부분에서 말씀드렸다시피, 이중포인터나 포인터나 거의 같은 말입니다.
포인터가 어떤 변수의 주소값을 가리킨다라고 하면, 이중포인터는 포인터 변수의 주소값을 가리킨다고 할 수 있습니다.
즉, 둘다 변수의 주소값을 가리키는 것입니다.
다만 포인터 변수는 이미 포인터이다보니, C언어로 표현할 때에 포인터 표시가 2개 이상 연속으로 된다는 것일 뿐입니다.

 

이중포인터의 사용이유에 대한 이해를 돕기 위해, 앞서 봤던 예제 프로그램을 약간만 수정해보기로 하겠습니다.
변수를 포인터 변수로 변경해보는 정도만 해봅니다.

int형 변수였던 value, refer가 이번에는 int 포인터형 변수입니다.
포인터형 변수이니 상수를 직접 넣기보다는 다른 변수를 가리키도록 수정했습니다.

 

int global_val = 30;

void call_by_value(int *val)
{
        val = &global_val;
}

void call_by_refer(int **ref)
{
        *ref = &global_val;
}

int main()
{
        int local_val = 10;
        int *value = &local_val;
        int *refer = &local_val;

        printf("before : *value=%d, *refer=%d\n", *value, *refer);
        call_by_value(value);
        call_by_refer(&refer);
        printf("after  : *value=%d, *refer=%d\n", *value, *refer);
}

 

main() 시작시에 두 변수 모두 동일한 local_val의 주소값을 가리키게 했으니, *value, *refer 는 10이 됩니다.
그리고, 아까전과 동일하게 call_by_value()는 변수를 그대로, call_by_refer()는 변수의 주소값을 인자로 보내고, 각 함수에서는 global_val 의 주소값을 넣어주도록 합니다.
결과는 어떻게 될까요?

 

before : *value=10, *refer=10
after  : *value=10, *refer=30

 

call_by_value()에서의 val은 &global_val을 받긴 하지만 함수를 종료하고 나면 값은 사라집니다. 그러므로, main()에서는 여전히 local_val을 가리키고 있으며, 10이 출력됩니다.
call_by_refer()에서의 ref는 포인터변수의 주소값이므로, *ref 에 &global_val를 넣게 되면 main()에서의 refer 값이 변경되는 셈이므로, local_val이 아니라 global_val을 가리키고 있으며, 30이 출력됩니다.

 

value가 주소값이므로 안사라지는게 맞지 않냐고 생각하는 분도 계시겠지요.
제가 위에서 언급한 적이 있듯이, 값도 값이고 주소값도 값입니다.
중요한 것은 '내가 담고자 하는 변수의 주소값'이 필요한 것인데, 그 변수가 포인터 변수란 말이죠.
그러면 이렇게 정리됩니다.
'내가 담고자 하는 (포인터)변수의 주소값'이 필요한 것이 됩니다.
앞부분에서 여러 차례 언급했었습니다. int형 변수, int 포인터형 변수... 둘다 같은 변수일 뿐이라고 말이죠.

main() 에서 전달하는 변수가 포인터변수였고, 그 포인터변수의 주소값을 넘겨야 하므로, &refer가 되어야 하는 게 맞으며, 그에 맞춰서 call_by_refer(int **ref) 처럼 이중포인터가 되는게 맞겠지요.
아직 좀 헷갈리실 수 있는데, 제가 언급한 몇가지 핵심 위주로 다시 한번 글을 읽어보시길 권해드립니다.

 

 

bonus~

제가 '이중포인터나 포인터나 거의 같다', '변수나 포인터 변수나 같은 변수이다'라는 식의 말들을 언급했었습니다.
아직 의아해 하시는 분들을 위해서 보너스 삼아 한가지 예제를 더 보여드리겠습니다.

직전에 사용된 이중포인터를 사용하는 예제 소스가 있었죠?
이것을 사실 단일포인터만을 사용하여 call by reference 방식으로 사용하는 것입니다.

 

int global_val = 30;

void call_by_value(int *val)
{
    val = &global_val;
}

void call_by_refer(int *ref)
{
    *ref = (int)&global_val;
}

int main()
{
	int local_val = 10;
	int *value = &local_val;
	int *refer = &local_val;

	printf("before : *value=%d, *refer=%d\n", *value, *refer);
	call_by_value(value);
	call_by_refer((int*)&refer);
	printf("after  : *value=%d, *refer=%d\n", *value, *refer);
}

 

call_by_refer(int *ref) 입니다. 하지만, 결과는 올바르게 나옵니다.

 

before : *value=10, *refer=10
after  : *value=10, *refer=30

 

call_by_refer((int*)&refer); 에서 refer가 포인터변수이므로, &refer는 이중포인터인 셈입니다.
하지만 이것을 (int*)로 형변환(casting)합니다.
왜냐하면, 실제로는 이중포인터란 것은 존재하지 않기 때문입니다.
그러면 call_by_refer()에는 refer의 주소값이 int*형 변수로서 ref에 전달되고,
ref = &global_val; 을 하게 되면 main()의 refer에 담긴 주소값이 변경되는 것입니다.

 

물론 좋은 방법은 아닙니다만, 장난삼아서 해본다면
아예 이런 방법으로도 가능합니다.

 

int global_val = 30;

void call_by_value(int *val)
{
    val = &global_val;
}

void call_by_refer(int ref)
{
    *(int*)ref = (int)&global_val;
}

int main()
{
	int local_val = 10;
	int *value = &local_val;
	int *refer = &local_val;

	printf("before : *value=%d, *refer=%d\n", *value, *refer);
	call_by_value(value);
	call_by_refer((int)&refer);
	printf("after  : *value=%d, *refer=%d\n", *value, *refer);
}

 

int형 변수로 전달받았음에도 처리가 가능하죠.
왜냐하면 몇 번이나 언급했듯이,
int 변수나 int* 변수나 다 같은 변수이고, 포인터나 이중포인터나 다 같은 포인터이기 때문입니다.

 

 

^^)/

너무나 간만에 적는 C 관련 포스팅이라, 지루하진 않으셨나 모르겠습니다.
잘못된 내용이나 오타는 지적해주시면 수정하도록 하겠습니다.
그리고 부끄럽지만, 출처를 밝히시면 글을 퍼 가셔도 좋습니다.

두서 없는 글, 읽어주셔서 감사합니다.

Posted by 찬이

댓글을 달아 주세요

  1. 안녕하세요 2014.11.26 17:59 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 깔끔한 정리 정말 감사드립니다. 이중포인터를 사용하는 이유를 단방에 이해했습니다.

    궁금한게 있는대요 맨 마지막 예제에서 21번째 줄 call_by_refer((int)&refer); 내용이요 &로 반환된 주소값을 저장할땐 int 형 변수여도 문제는 없는대

    *(int*)ref 인트형 포인터 변수에 들어가는 기대값이 (int*) 등과같은 형변환으로 변환된 값으로 이루어져도 딱히 메모리적으론 문제가 없는건가요??

    좋은 글 감사합니다.

    • BlogIcon 찬이 2014.11.26 18:06 신고  댓글주소  수정/삭제

      읽어주셔서 감사합니다 ^^

      메모리상에서는 주소든 값이든 항상 동일합니다.
      그냥 0x12345678 이면, 이것을 주소로 해석하든, 자연수로 해석하든, 문자열로 해석하든...
      메모리상으로는 동일합니다.

      다만, 형변환을 하는 과정에서 메모리 사이즈가 다르다던가할 경우에는
      일부 데이터가 소실될 우려가 있기는 합니다만,
      그건 코딩을 잘못하는 경우라고 봐야겠지요.
      기본적으로는 문제가 없습니다.

  2. 안녕하세요 2014.11.27 18:40 신고  댓글주소  수정/삭제  댓글쓰기

    깔끔한 답변 감사합니다. 그럼 예를 들면 int 포인터 형으로 저장된 변수를 char 포인터 변수로 변환시키면 3바이트 만큼 손실이 되기때문에 메모리가 더 작은 변수에 저장하는 기본적인 실수만 안하면 문제가 없다는 뜻이군요! 좋은 정보 얻어갑니다~~

    • BlogIcon 찬이 2014.11.27 19:03 신고  댓글주소  수정/삭제

      예, 제가 말씀드리고자 한 내용은 이해를 하신 듯 한데,
      한가지 제가 보태어 드리자면,
      char 포인터도 int 포인터와 크기는 같다는 점입니다.
      char 이 존재하는 주소라고 해서 1바이트로 표현되는 것은 아닙니다.
      그래서 포인터 변수의 크기는 같습니다.
      따라서, char 포인터이기 때문이다는 말은 틀린 표현이구요.

      위의 소스중 마지막 소스에서 int 대신 모두 char 로 변경하게 되면
      제가 말씀드리고자 하는 경우와 비슷한, 문제가 발생합니다.

      call_by_refer((char)&refer);

      refer가 char라고 해서 이렇게 변환을 하게 되면
      주소값을 char 로 변환하면서 1 byte 만 값으로 전달됩니다.

      void call_by_refer(char ref)
      {
      *(char*)ref = (char)&global_val;
      }

      이 1byte를 전달받은 것을 다시 주소값으로 형변환하게 되면 엉뚱한 주소가 되는 것이죠.
      질문하신 내용에 완전히 적절한 예는 안되겠지만, 대략 이러한 작성자 실수에 의한 경우에는 문제가 발생한다는 점을 말씀드리고 싶었던 것입니다.

  3. BlogIcon Sweetfish 2015.07.09 14:37 신고  댓글주소  수정/삭제  댓글쓰기

    많은 도움이 됐습니다. 감사합니다~

  4. clawlfwlf 2016.03.19 00:28 신고  댓글주소  수정/삭제  댓글쓰기

    예제가 정말 이해하기 쉽네요. 많이 배우고 갑니다~