본문 바로가기

Programming Language/C

포인터

1. 포인터의 개념

포인터를 제외한 변수를 생각했을 때, 변수는 그 자체로 자신의 자료형에 맞는 값을 저장합니다. 그 예로 int형 변수, double형 변수 등이 있습니다. 포인터 변수는 기존의 변수와는 다르게 메모리에 주소 값을 저장합니다. 또한 단순히 주소 값만 저장하는 것이 아니라 어떤 자료형의 주소 값인지도 함께 저장합니다.

수업시간에 교수님께서 사용하시는 레이저 포인터는 칠판의 어딘가를 가리킬 때 사용합니다. 마찬가지로 컴퓨터의 포인터도 메모리의 주소를 가리키고 해당 메모리에 직접 접근할 수 있게 도와줍니다.

int a = 5;

다음과 같이 a라는 변수가 초기화 되었다고 가정해보겠습니다. 이와 같이 정의하는 방식은 위 코드 다음에 다른 코드를 작성했을 때 단순히 a를 사용해서 5라는 값을 찾을 수 있습니다. 마찬가지로, 포인터 변수를 사용해서도 5라는 값을 찾을 수 있습니다.

int a = 5;
int* b = &a;

b는 포인터 변수입니다. &는 변수 앞에 붙어서 변수의 메모리 시작 주소 값을 구할 수 있게 해줍니다. 따라서, int* b = &a; 라고 작성하면 포인터 b는 a의 주소, 즉 a 변수를 가리키게 되어 포인터 b를 이용해 5라는 값에 접근할 수 있는 것입니다. 값을 처리할 수 있는 방법이 다양해졌다고 생각하면 좋을 것 같습니다.

사견을 조금 덧붙이자면 int a = 5;와 같은 방법은 지도를 펼쳐 놓고 손으로 딱 짚어서 "여기가 국립현대미술관 서울관이야." 와 같은 설명 방법이고, int* b = &a; 와 같은 방법은 "서울 종로구 삼청로 30. 이 주소에 무엇이 있는지는 너가 가서 알아봐." 와 같은 방법이라는 느낌이 든다.
(처음 학습할 때, 포인터 개념이 어렵게 느껴져 개인적으로 이렇게 이해하고 넘어간 부분이라 정확한 설명은 아닙니다.^^)

그림으로 한 번 더 포인터의 개념을 설명하면 다음과 같다.

1.1. 포인터 관련 연산자

연산자 설명
주소 연산자(&) 변수 앞에 붙어서 변수의 메모리 시작 주소 값을 가져올 때 사용합니다. 흔히 포인터 변수에 값으로 들어갈 수 있는 요소입니다.
포인터(*) 포인터 변수를 선언할 때 사용하며 포인터 연산자라고 부르기도 합니다.
간접 참조 연산자(*) 선언된 포인터 변수가 가리키는 변수의 값을 구할 수 있습니다.

포인터와 간접 참조 연산자는 엄연히 다른 연산자입니다. 둘은 생긴 것만 같지 기능은 다릅니다.

#include <stdio.h>

int main(void) {
    int a = 5;
    int* b = &a;
    printf("%d\n", *b);
    system("pause");
}

위 코드에서 int* b = &a; 와 같이 선언할 때 사용하는 * 은 포인터 변수임을 알려주기 위한 목적을 가집니다.
반면, printf("%d\n", \*b); 와 같이 선언 이후에 *b 라고 작성하게 되면 이것은 포인터 변수 b가 가리키는 주소의 값을 의미합니다. 곧 *b 는 5와 같다는 뜻이 됩니다.

또한, int a = 5;와 같이 변수를 할당한다면 메모리 주소 상에는 아래와 같이 기록이 됩니다.

int형은 4바이트의 크기를 가지기 때문에 메모리 주소를 1바이트 씩 표현할 때 4칸을 차지하는 것입니다.

1.2. 예제1 : 배열의 각 원소의 주소 값 출력하기

#include <stdio.h>

int main(void) {
    int a[] = { 1,2,3,4,5,6,7,8,9,10 };
    int i;
    for (i = 0; i < 10; i++) {
        printf("%d\n", &a[i]);
    }
    system("pause");
}

결과:

17823652
17823656
17823660
17823664
17823668
17823672
17823676
17823680
17823684
17823688
계속하려면 아무 키나 누르십시오 . . .

a라는 배열을 만들어서 for문을 통해 a[0]부터 a[9]까지 각각의 주소를 출력할 수 있도록 만들었습니다. 변수를 할당했을 때 실제 메모리 주소 상에는 어떻게 기록이 되는지 앞서 살펴보았습니다. 위 코드의 결과를 보면 모든 숫자들이 4씩 증가하는 것을 확인할 수 있습니다. 이것은 int형이 4바이트의 크기를 가지기 때문에 주소 값이 4씩 증가한다는 것을 의미합니다. 또한 주소 연산자 &는 메모리 시작 주소 값을 구할 수 있게 해주기 때문에, 결과로 나온 각 숫자들은 배열의 각 원소 별 시작 주소 값입니다.

결과적으로, 4바이트씩 총 10개를 차지하기 때문에 40바이트를 차지하는 배열임을 주소연산자를 통해 확실히 확인할 수 있습니다.

2. 포인터의 기능

포인터는 컴퓨터 시스템의 특정한 메모리에 바로 접근할 수 있도록 해줍니다. 따라서, 기존에 존재하던 중요한 메모리 영역에 접근하지 않도록 해야 합니다.

예를 들어 다음과 같은 코드가 있다고 가정해보겠습니다.

int* a = 17823672;
*a = 0;

우리는 17823672 라는 주소가 시스템 상에서 어떤 역할을 하고 있는지 확실하게 모르는 상황입니다. 그런데 이 특정한 주소 값을 포인터 변수가 가리키도록 만든 다음, 그 값을 간접참조연산자를 통해서 0으로 바꿔버리면 어떤 일이 발생할지 모릅니다. 따라서, 위와 같은 코드는 굉장히 위험한 코드이므로 기존에 존재하던 중요한 메모리 영역에 함부로 접근해서 다른 값을 쓰지 않도록 유의해야 합니다.

2.1. 다중 포인터

포인터는 다중으로 사용하는 것이 가능합니다. 즉, 포인터의 포인터가 존재할 수 있는 것입니다.

예제를 통해서 다중 포인터에 대해서 조금 더 자세히 알아보겠습니다.

#include <stdio.h>

int main(void) {
    int a = 5;
    int* b = &a;
    int** c = &b;
    printf("%d\n", **c);
    system("pause");
}

결과:

5
계속하려면 아무 키나 누르십시오 . . .

먼저 int* b = &a; 를 보면, 포인터 b가 변수 a의 주소값을 가리키고 있습니다. 포인터 변수 b또한 컴퓨터 메모리에 기록되는 일종의 변수입니다.

int** c = &b; 를 보면, 포인터의 포인터 변수임을 알려주기 위해 포인터 연산자 *을 두 번 사용하고 있습니다. 즉, 포인터 변수 c는 포인터 변수 b를 가리키는 포인터임을 알리는 것입니다. 조금 더 명확히 하자면, 포인터 변수 b의 메모리 값을 가리키는 것입니다.

포인터 변수 c가 가지고 있는 주소가 b이고 그 b가 가리키는 값을 또 한 번 참조해야 하기 때문에 간접참조연산자를 두 번 넣어줌으로써 결과적으로 5라는 값을 출력할 수 있는 것입니다.
다중 포인터는 위의 예제와 같이 최대 두 번만 사용할 수 있는 것이 아니라 더 많은 양의 포인터를 겹쳐서 사용하는 것이 가능합니다. 이러한 다중 포인터는 코드 난독화 기법(Code Obfuscation)으로 사용되기도 합니다.

2.2. 배열과 포인터의 관계

배열을 선언한 뒤에는 배열의 이름 자체를 포인터 변수처럼 사용할 수 있습니다.

#include <stdio.h>

int main(void) {
    int a[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* b = a;
    printf("%d\n", b[2]);
    system("pause");
}

결과:

3
계속하려면 아무 키나 누르십시오 . . .

int* b = a;를 보면 a에 주소 연산자가 붙어 있지 않은 것을 확인할 수 있다. a라는 일종의 배열 이름 자체를 주소값으로 사용하고 있는 것인데, 이는 내부적으로 배열의 이름 자체가 주소값 자체를 가지고 있기 때문에 가능하다. 즉, 배열과 포인터는 사실 내부적으로 거의 동일하며, 주소 연산자를 사용하지 않은 채로 int* b = a;와 같이 사용이 가능한 것이다.