포인터 보고서

포인터란?

포인터(Pointer), 뭔가 많이 들어본 단어일것이다. C언어의 꽃이라고 불리기도 하고, 포인터 때문에 C언어가 어렵다고 할 정도로 악명이 높은 개념이기도 하니까.

point는 무언가를 가르킨다는 뜻을 갖고있는 영어 단어이다. point의 뜻을 숙지하고, pointer에 대해서 공부해보자.

한국어 위키백과에서는, "포인터(pointer)는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리공간을 가르키는 변수를 말한다" 라고한다.

우리가 int a; 라는 코드를 통해 변수를 만들면 어떻게 될까?
메모리에 a 라는 변수의 공간이 할당될것이다.

컴퓨터는 이 메모리를 접근하면서 사용해야 하기 때문에 지구에 주소체계를 만들어 둔것처럼 메모리에도 주소를 지정해두었다.
이러한 메모리상의 주소를 가르키는 변수를 포인터라고 하는것이다.


포인터를 사용하는 이유

포인터는 왜 사용하는것일까?

1.
저번 함수 보고서에서 우리는 지역 변수와 전역 변수의 개념을 배웠다.
만약 main() 함수의 지역변수를 외부 함수에서 수정해야 한다면 어떻게 해야할까?
애초에 그러한 함수들을 전역변수로 선언해야할까?
그렇다면 그런식으로 접근 해야 할 변수가 매우 많다면? 너무 코드가 지저분해지지 않을까?

2.
#include <stdio.h>

int main(void){
 int i, a = 3;
 for(i = 0;i<3;i++){
  int b = a+i;
  printf("%3dd", b);
 }
 return 0;
}
이런 코드가 있다고 가정해보자.
for문이 한번 돌 때마다 a에 있는 값을 가져온 뒤에 i만큼을 또 더해야한다.

여러분이 화가라고 하자. a의 집을 그리려고 한다.
a의 집을 복사해서 갖고 온 뒤에 그리는게 빠를까?
a의 집 주소를 찾아가서 그리는게 빠를까?

우리에게 물건을 복제할 수 있는 능력은 없지만, 확실한건 무언가를 만드는것보다 직접 그것이 위치한 곳에서 무언가를 하는것이 더 빠를것이다.

이렇듯 포인터는 다른 함수의 지역변수 값을 수정 하거나 어떤 값을 매번 복사해올때 속도를 개선하기 위해서 사용할 수 있다.


포인터 사용법

그렇다면 이러한 포인터를 어떻게 사용해야 할까?

선언

포인터는 다음과 같이 선언 할 수 있다.
<가리킬 변수의 자료형> *<이름> = &<가리킬 변수>;
그래서 int 형 변수 a를 가리키는 포인터는 다음과 같이 짤 수 있다.
int *p = &a;

접근

그렇다면 포인터를 통해 a를 접근하기 위해선 어떻게 해야할까?
#include <stdio.h>

int main(void){
 int a = 10;
 int *p = &a;
 printf("%d\n", a);
 *p = 5;
 printf("%d\n", a);
 return 0;
}
이 코드의 실행결과는 이렇다.
마찬가지로 *p를 통해 접근해도 출력이 가능하다.
#include <stdio.h>

int main(void){
 int a = 10;
 int *p = &a;
 printf("%d\n", a);
 *p = 5;
 printf("%d\n", *p);
 return 0;
}

이제 어떻게 접근해야 하는지는 알았으니까, 위의 코드를 해설하면서, 포인터의 연산자들을 설명하도록 하겠다.

포인터 연산자: &, *

잠시 scanf()를 사용할때를 생각해보자.

scanf("%d", &a);

입력받을 내용의 포맷과 값을 저장할 변수를 &연산자를 통해 넘겼다. 그렇다면 &의 정체는 과연 무엇일까?
&는 주소 연산자로서 피연산자는 저장되어있는 메모리 주소를 뱉어낸다.
간단히 코드로 실험해보자.
#include <stdio.h>

int main(void){
 int a = 10;
 printf("%p\n", &a);
 return 0;
}
(%p는 메모리주소를 출력할때 사용하는 형식지정자이다)
아까 포인터는 가리키는 변수의 메모리주소를 저장한다고 했다. 그렇기 때문에 이렇게 주소 연산자를 통해 주소를 넘기게 되면 포인터는 해당 변수의 주소를 저장하게 되는것이다.

*는 참조 연산자라고 부르는 녀석이다. (사람에 따라서는 포인터 자체가 어떤 변수를 참조 중인데 그 포인터를 통해 참조를 한다고 해서 역참조 연산자라고 부르기도 하는것 같다.)
아까 접근 챕터에서 보았던 코드를 다시 보자.
#include <stdio.h>

int main(void){
 int a = 10;
 int *p = &a;
 printf("%d\n", a);
 *p = 5;
 printf("%d\n", a);
 return 0;
}
*p = 5가 참조 연산자가 사용된 부분이다.
*는 피연산자의 메모리주소에 직접 접근할 수 있게 해주는 녀석이다.
위에서 p가 a를 가리키게 했고, p에서 참조 연산자를 했기 때문에 a의 메모리 주소에 직접 접근해서 값을 수정 할 수 있게 된것이다.

다중포인터

"a를 가리키는 포인터 b를 가리키는 포인터 c", 여기서 포인터 c를 뭐라고 할까?
그렇다. 위에 써있듯 다중 포인터, 그중에서도 2중 포인터라고 한다. 포인터도 변수이기 때문에 포인터를 가리키는 포인터를 만드는게 가능하다.
몇중이냐에 따라서, 선언할때 *의 개수를 다르게 하면된다. 따라서 2중포인터의 선언방법은 다음과 같다.
<자료형> **<이름> = &<가리킬 포인터>;

Call By Reference, Call By Value

처음 포인터를 사용해야 하는 이유에서 언급했던 a와 b를 바꾸는 함수를 만들어 보도록 하겠다.
만약 포인터를 모른다면, 우리는 당연히 이렇게 구현하려고 했을것이다.
#include <stdio.h>

void swap(int a, int b){
 int temp = a;
 a = b;
 b = temp;
}

int main(void){
 int a = 3, b = 10;
 swap(a, b);
 printf("%d %d", a, b);
 return 0;
}
하지만 세상은 그렇게 쉽게 돌아가지 않는 법. 이를 출력하면 이렇게 나온다.

사실 당연한 것이다. a와 b를 swap 함수에 넘겼지만, 이는 a안에 있는 값 3과 b안에 있는 값 10을 넘긴것이고, swap 안에있는 매개변수와 main안에 있는 지역변수는 철저히 다른 녀석이니까.
이게 Call By Value 이다. 값에 의한 호출.

그렇다면 우리가 원하는 기능을 위한 코드는 어떻게 구현 하는 것이 좋을까?
#include <stdio.h>

void swap(int *a, int *b){
 int temp = *a;
 *a = *b;
 *b = temp;
}

int main(void){
 int a = 3, b = 10;
 swap(&a, &b);
 printf("%d %d", a, b);
 return 0;
}
이렇게 구현하면 된다. a의 값과 b의 값을 넘기는 것이 아닌, 이들 각각의 주소를 넘겨서 swap 내에서 main의 지역변수 a, b에 접근이 가능하게 되었고 그 결과 이 둘의 값을 바꿔주는 함수를 만들어 줄 수 있었다.
이것이 바로 참조에 의한 호출, Call By Reference이다.

배열과의 관계

배열은 사실 포인터이다. 사실 배열에 대해서 생각을 한번 해보자.
int a[3]을 하게 되면, a[0]의 시작 주소부터 a[2]까지 접근 할 공간이 생기는것이 아닌가?
그래서,
int a[3] = {1, 2, 3};
int *p = a;
이라고 할 때,

a[1]과 *(p+1)은 완전히 같은 녀석이다.
메모리의 입장에서 한번 생각을 해보자. 배열의 이름은 그 배열의 시작 주소를 의미하고, 거기다가 1을 더했으니 바로 다음 위치에 있는 배열이 되지 않겠는가?

뜬금 없지만, 자바스크립트라는 프로그래밍언어는 약타입이라고해서 타입의 구분이 미미하다.
방금 우리가 배운 내용을 참고하면 그와 비슷한 이런 기괴한 짓도 가능하다.
#include <stdio.h>

int main(void){
 int a = 3;
 int *test = &a;
 *(test+1) = 'h';
 *(test+2) = 'i';
 printf("%d\n", a);
 printf("%c%c", test[1], test[2]);
 return 0;
}

Comments

Popular Posts