Programming and my thoughts

포인터는 고전적인 주제이고, 요즘은 포인터를 몰라도 프로그래밍을 할 수 있지만...

포인터를 모르고서는 프로그래밍의 원리를 제대로 이해할 수 없다.

프로그래밍 기초편에는 포인터에 대한 설명이 나오고, 포인터를 기반으로 하여 파라메터 전달 기법들에 대해 설명한다.


1. CODE 1-1, 포인터의 사용


아주 기본적인 포인터 사용방법을 살펴보자.

#include <stdio.h>

int main() {
    int var = 10;
    int *p = &var;
    printf("var = %d\n", var);
    printf("p = %p\n", p);
    printf("*p = %d\n", *p);
    printf("&p = %p\n", &p);
    printf("&var = %p\n", &var);
}
var = 10
p = 0028FF1C
*p = 10
&p = 0028FF18
&var = 0028FF1C
  • var 은 10 이므로...
  • p 라고 하면 p 가 가리키는 대상(var)의 주소값을 가리킨다.
  • *p 라고 하면 p의 실제값을 가리킨다.
  • &p 라고 하면 p 의 주소값을 기리킨다.
  • &var 은 var 의 주소값을 가리킨다. 위에서 그냥 p 라고 했을 때와 같다.
  • 주목할 것은 p 와 &p 의 값이 다르다는 것.

2. CODE 1-2, 포인터 연산


포인터는 unary operation 이 가능하다. (예 : ++, --)

다만, 포인터 자체가 메모리를 가리키고 있으므로... 

포인터에 ++ 를 하면 가리키는 메모리의 위치가 증가하고, 반대로 -- 를 하면 가리키는 메모리의 위치가 감소한다.

#include <stdio.h>

int main() {
    // int 타입 포인터 p 를 선언하고 주소를 2 라고 지정함.
    int *p = (int*)2;
    // p 의 주소값은 당연히 2 가 된다.
    printf("%p\n", p);
    p++;
    // p++ 하면 이것은 컴퓨터에 따라 다른 결과가 나온다.
    // 내 컴퓨터의 경우 메모리 공간 4 바이트를 더한 6 이 나온다.
    printf("%p\n", p);
}
00000002
00000006

3. CODE 1-3, 동적 메모리 할당


포인터는 메모리를 가리킨다고 했다.

포인터가 새로운 메모리 공간을 가리키게 하려면 메모리 공간을 할당해줘야 한다.

할당이라는 개념은 컴파일 타임에 동작하지 않고... 런타임에 동작하게 된다.

따라서 할당을 하지않고 메모리를 사용해도...

컴파일할 때에는 에러가 나지않지만... 동작해보면 오류가 난다.


포인터에 사이즈 2 이상의 메모리 공간을 할당하게 되면... 그 포인터는 일종의 배열처럼 동작하게 된다. (예 : p[0]=100, p[1]=200)

그래서 이 포인터를 배열 포인터라는 말을 쓰기도 한다.

어떤 포인터로 하여금 이전에 쓰던 배열을 가리키게 하면 그 포인터도 배열 포인터가 된다.

이건 새로운 개념이 아니라...

포인터 자체가 메모리를 가리키는 것인데... 포인터가 배열을 가리키면 당연히 배열처럼 동작하는 매커니즘일뿐이다.


반대로 포인터 배열이라는 것도 있는데... (배열을 만들었는데... 각 배열의 원소가 포인터로 이루어진 것)

이건 그냥 패스하겠다. (사실 내가 까먹었다)

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 3;
    // malloc 은 메모리를 할당하는 함수이다.
    // 참고로 c++ 에서는 new 라는 키워드가 있다.
    // c#, java 에도 new 라는 키워드가 있다.
    int *p = (int*) malloc(size * sizeof(int));
    p[0] = 1;
    p[1] = 2;
    p[2] = 3;
    printf("p=%p\n", p);
    printf("p[0]=%d,p[1]=%d,p[2]=%d\n", p[0], p[1], p[2]);
    // 아래의 free 는 다 사용한 메모리를 다시 반환하는 함수이다.
    // 참고로 c++ 에서는 delete 라는 키워드가 있다.
    // c#, java 는 managed resource 의 경우 자동으로 반환하는 garbage collection(GC) 기능이 있다.
    // unmanaged resource 의 경우 c# 은 프로그래머가 직접 반환해줘야 한다.
    // java 는 이것도 managed 로 취급하며 자동반환... (만능은 아니다)
    // c# 과 java 의 메모리 관리 기법은 좀 다르다고 한다. (이건 다른 포스팅에서...)
    // 메모리 반환을 제대로 안해주면 메모리가 줄줄 샌다.
    // managed 와 unmanaged 의 차이는 다른 포스팅에서 언급하겠다.
    free(p);

    // 여기서 돌발 질문 ?
    // malloc 으로 할당하고 delete 로 반환하면 어떨까 ?
    // 아니면 new 로 할당하고 free 로 반환하면 어떨까 ?
    // 에러도 안나고 잘 동작하지만... 굳이 이렇게 오묘하게 쓸 필요는 없을 것 같다.

    // 또 돌발 질문 ?
    // malloc 이나 new 로 할당하고 다 썼는데 반환안해도 되나요?
    // 큰일난다. memory leak 이라고 해서 장기적으로 치명적인 프로그램 오류를 야기시키게 된다.
    // c, c++ 도 c#, java 처럼 자동으로 반환해주면 참 좋겠지만 순수한 c, c++ 은 그런거 없다 ~
    // 그래서 남자라면 c, c++ 로 코딩을...(?)
    
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p2 = arr;
    
    printf("p2[0]=%d,p2[9]=%d\n", p2[0], p2[9]);
    
    // 아래는 어떨까?
    int *p3 = (int*) malloc(3 * sizeof(int));
    p3[0] = 1;
    p3[1] = 2;
    p3[2] = 3;
    // 아래부터는 최초 선언한 크기 3 보다 실제 사용한 크기가 커진다.
    // 안될 것 같지만... 컴파일은 당연히 잘 되고 (메모리 공간을 동적으로 할당했으므로)
    // 런타임시에도(실행시) 문제가 없다. 
    p3[3] = 4;
    
    // 왜 그럴까 ?
    // OS 는 최초 메모리 할당시 프로그래머가 지정한 3 보다는 큰 넉넉한 사이즈로 할당한다.
    
    // 하지만 아래는 선언한 것보다 훨씬 큰 사이즈의 포인터를 이용하게 되므로 오류가 날 가능성이 높다.
    // 다시말해, 선언한 크기보다 큰 공간을 사용하면...
    // 동작할 수도, 죽을 수도 있다... (양자역학을 좋아하는 사람이라도 이렇게 쓰지는 말자)
    p3[100000] = 100;
    free(p3);
}
p=006318E8
p[0]=1,p[1]=2,p[2]=3
p2[0]=1,p2[9]=10
  • p=006318E8 이건 그냥 주소값이 나왔다.
  • p[0], p[1] 해도 왠지 주소값이 나올 것 같지만... 배열 포인터의 경우 * 가 없어도 실제값을 가리킨다.
  • 왜 일관성없이 이렇게 동작하는걸까 ?
  • 그건 c 언어 만든 사람이 이렇게 만들었기 때문 !

4. CODE 1-4, 함수 포인터


함수 포인터는 함수를 가리키는 포인터를 말한다.

이건 정말 정말 중요하고... 다른 언어에서도 정말 많이 쓰인다.

예를 들어 현대의 javascript 는 callback 으로 시작해서 callback 으로 끝난다고 해도 과언이 아닐 정도로...

callback 함수를 많이 사용하는데... 어느 언어에서든 callback 함수라는 것은 함수 포인터를 이용한 것이라고 보면된다.

python, c#, java 너나 할 것 없이 모두가 함수 포인터를 이용한다.


포인터의 세계에 함수 포인터라고 하는 끝판왕이 있다면... 


중간보스격으로 다차원 포인터를 생각해볼 수 있다...

다차원 포인터라는 개념이 실제로 있고 경우에 따라 많이 쓰이기도 하지만...

이런걸 많이 쓰면 코드 가독성이 떨어지게 된다.

즉... 코딩해놓고 나중에 다시보면 이게 무슨 코드인지 한참을 분석해야 한다.

뿐만 아니라... 다른 팀원이 그 코드를 유지보수하려고 보는 순간... 눈이 핑핑돌고 살려주셈을 외치게 된다.

그러니 왠만하면 다른 방법으로 코딩하자.

(참고로... 2차원 포인터까지는 일반적으로 많이 쓰인다... 3차원부터는 아인슈타인과 괴델의 영역으로 가는 것)


필자가 다차원 포인터를 잘 안쓰다보니 다차원 포인터가 뭔지 까먹었다는 슬픈 전설이... (즉, 몰라서 알려줄 수가 없는 것)

* 한가지 팁 : 혹시나 다차원 포인터를 쓸 때에는 메모리 반환을 할 때에 아주 신중하게 잘해야 한다. 낮은 차원에서 높은 차원 순으로 반환을 해야하며... 그 이유는 생각해보면 쉽게 알 것이다. 이런 이유 때문에라도 다차원 포인터는 신중하게 사용해야하는 것이다. 메모리 할당과 반환은 아주 고전적인 주제이지만... 많은 프로그램이 겪는 문제가 여기에서 시작되는 것이 참 많다. 메모리를 할당하고 반환하지 않아 생기는 문제를 통상적으로 memory leak 이라고 하는데... 이건 추후에 프로그램 디버깅을 통해 잡는 것도 정말 어렵다. 우리가 음주 코딩을 지양해야 하는 것은 이런 까닭이다.


필자는 다 까먹고 잘 모르므로 여기서는 함수 포인터만 설명한다.

#include <stdio.h>

int functionA(int i)
{
    return i + 1;
}

int functionB(int i, int j)
{
    return i + j;
}

int main() {
    // 함수 포인터의 형태...
    // 앞의 int 는 return 값을 말하고
    // 뒤의 (int) 는 함수의 호출 인자 형태를 말한다.
    // 가운데 (*p) 를 보고 이것이 포인터구나 하는 사실을 알고 무릎을 탁 치게된다.
    int (*p) (int);
    p = functionA;
    int x = p(2);
    printf("%d\n", x);
    
    // 응용버전이다.
    // 앞의 int 를 보아 return 값은 int 이고...
    // 뒤의 (int, int) 를 보아 함수의 호출 인자는 int 2 개를 받는다.
    // 가운데 (*p) 를 보고 무릎을 탁 치게된다.
    int (*p2) (int, int);
    p2 = functionB;
    int x2 = p2(1,2);
    printf("%d", x2);
}
3
3

끝 !


여기에 적힌 포인터의 개념을 머릿속에 다 기억하고 있어야할까?

외우는 것은 우리 뇌에 스트레스만 주고 기억력이 오래 지속되지 못한다.

따라서 이해를 해야한다.


1. 포인터에 대한 이해

2. 함수 포인터에 대한 이해


요즘은 순수 c, c++ 로 개발하기보다는 java, c#, python, javascript 등 포인터의 개념이 눈에 드러나지않는 언어로 많이 개발한다.

그래서 포인터를 몰라도 개발자가 되는데에 문제가 없다.

그러나... 포인터의 개념이 눈에 드러나지않을 뿐... 내부적으로는 많은 부분에서 포인터로 동작하는 것이 많다.

그래서 포인터를 이해하는 것이 중요하다.


프로그래밍에서 쓰이는 개념은 우선은 이해를 하고... 

좀 기억이 가물가물하면 레퍼런스를 찾아보고 다시 숙지하는 편이 낫다고 생각한다.

구글로 찾아보면 안나오는게 없는 세상이니 외우는 것 보다는 이해를 하고... 다음에 다시보고 또 다시보는걸로 ~


* 이 포스팅의 예제코드는 windows10 x64 환경의 gcc 4.8.1 에서 작성했습니다.