반응형

Pointer(포인터)

 

포인터도 하나의 변수이다. 

 

가끔은 포인터가 변수라는 걸 강조하기 위해 포인터 변수라고도 한다.

 

포인터는 메모리(RAM)에 있는 데이터의 주소를 가지고 있는 변수이다.

 

메모리(RAM)에 있는 데이터들은 모두 주소를 가지고 있다.

 

그리고 포인터는 같은 자료형만 가리킬수 있다.

 

만약 포인터의 자료형이 int형이면 int형만 가리킬 수 있는 것이다.

 

포인터를 사용하는 이유는 

 

간단하게 말해서 데이터의 복사보다는 데이터를 공유하여 작업하고자 할 때 포인터를 사용하게 된다.

 

또한 포인터는 초기화 해주는게 중요한 변수이다.

 

포인터는 메모리의 주소를 저장하는 변수이기 때문에

 

포인터를 잘 활용한다면 시스템 오류를 일으킬 수도 있다.

 

만약 포인터가 아무것도 가리키고 있지 않을 때는 NULL로 설정해준다.

 

NULL 포인터는 아래에 더 자세히 설명되어 있다.

 

정리하자면 포인터는 메모리의 주소를 가지고 있는 변수이다.

 

보통 변수는 데이터 값을 가지고 있지만 포인터 변수는 데이터의 메모리 주소를 가지고 있다는 걸 명심하고,

 

포인터는 같은 자료형만 가리킬수 있다는 것도 명심해둔다.

 

그 이유는 아래에 설명되어 있다.

 

또한 포인터가 아무것도 가리키고 있지 않을 때에는 NULL로 설정해준다.

 


 

※ 포인터 변수 선언 ※

 

포인터 변수를 선언하는 기본 구조는 다음과 같다.

 

[자료형] *[포인터변수명]

 

예를 들면 

int *p; 

 

해석

정수를 가리키는 포인터 변수 p

 


 

※ 주소 연산자 : & ※

 

주소 연산자 &(앰퍼센트)는 변수 명 앞에 적어주면 그 변수의 주소가 들어가게 된다.

int a=3;
int *p;
p = &a;

 

해석

정수형 변수 a를 선언하고 3으로 초기화한다.

정수형 포인터 변수 p를 선언하고 p에 같은 정수형 변수 a의 주소를 저장한다.

 


 

※ 간접 참조 연산자 : * ※

 

간접 참조 연산자 *(에스터리스크)의 의미는 "가리키다"라고 해석하면 된다.

 

좀 더 쉽게 말하자면,

 

*p는 포인터 변수 p가 가리키는 위치에 있는 데이터를 가리킨다라는 의미이다.

 

그 의미는 아래의 예제를 통해 보도록 한다.

 

그리고 *(에스터리스크)로 해당하는 변수의 값을 바꿀 수 있다.

 

*(에스터리스크)는 간접 참조 연산자이기 때문에 간접적으로 데이터 값을 바꿀 수 있게 해주는 것이다.

 

 

※*(에스터리스크)의 의미※

 

int a = 300;

int *p;

p = &a;

printf("%d\n", *p);

 

해석

정수형 변수 a를 선언하고 300으로 초기화한다.

정수형 포인터 변수 p를 선언하고 a의 주소를 저장해준다.

*의 의미는 "가리킨다"라는 의미로써 현재 포인터 변수 p에 a의 주소가 저장되어있지만

*p이기 때문에 a의 데이터 값을 가리킨다는 의미로 해석되어 300을 출력한다.

 


 

※ *(에스터리스크)로 변수 값 바꾸기 ※

 

int a=100;

int *p;

p = &a;

printf("%d\n",*p);

*p = 200;

printf("%d\n",*p);

return 0;

 

해석

정수형 변수 a를 선언하고 100으로 초기화한다.

정수형 포인터 변수 p를 선언하고 같은 정수형 변수 a의 주소를 저장한다.

 


 

※ &(엠퍼센트) 연산자 와 *(에스터리스크) 연산자 ※

 

&(엠퍼센트) 연산자는 변수의 주소를 포인터에 대입할 때 사용되는 연산자이다.

 

*(에스터리스크) 연산자는 포인터를 통해 변수를 간접적으로 참조할 때 사용하는 연산자이다.

 


 

※ NULL 포인터의 사용 ※

 

int *p = NULL;

 

포인터가 아무것도 가리키고 있지 않을 때에는 NULL(0)로 설정하는 것이 좋다.

 

그 이유는 주소 0을 액세스 하려고 하면 시스템에서 자동적으로 오류를 감지하고 이것을 해결할 수 있기 때문이다.

 

주소 0은 보통 CPU가 사용하는 영역이어서 일반 프로그램은 주소 0에 접근할 수 없다.

 

그리고 포인터를 사용하기 전에 NULL인지 아닌지 체크하는 것도 안전한 코드를 작성하는데 도움이 된다.

 


 

※ 포인터는 같은 자료형만 가리킬 수 있다. ※

 

포인터에는 가리키는 자료형에 따라 여러 가지 타입이 존재한다.

 

int형 포인터는 int형만을 가리킬 수 있고,

 

double형 포인터는 double형만 가리킬수 있다.

 

포인터는 메모리의 어디든지 가리킬 수 있지만, 포인터에 의하여 참조되는 객체가 얼마만큼의 크기이고

 

무엇이 어떤 형식으로 저장되어 있는 지를 결정하는 것은 포인터의 타입니다.

 

다음의 코드를 봐보자

 

int a;

double *p;

p = &a;

*p = 36.5;

return 0;

해석

정수형 변수 a를 선언하고, 실수형 포인터 변수 p를 선언한다.

 

포인터 변수 p에 정수형 변수 a의 주소를 저장하고,

 

간접 참조 연산자 *(에스터리스크)를 이용해 정수형 변수 a의 값을 36.5로 바꿔준다.

 

0을 반환한다.

 

 

위에 코드를 보면 문법적으로는 잘못되지 않았지만 실행오류가 존재한다.

 

먼저 double형 포인터 변수에 정수형 변수 a의 주소를 저장한 것과

 

정수형 변수 a의 값을 실수인 36.5로 바꾸었다.

 

위 코드가 정상적으로 실행되게 하려면

 

현재 double형 포인터 변수의 자료형을 int로 바꿔줌으로써 같은 자료형이 되게 맞춰주거나,

 

 int형 변수 a의 자료형을 double로 선언하여 포인터 변수의 자료형과 같게 맞춰줘야 한다.

 

그리고 int형으로 같게 했을 경우엔 데이터 값을 정수형으로 바꿔야 한다.

 


 

※ 포인터 연산 ※

 

포인터도 변수이고 메모리의 주소를 값으로 저장하고 있으므로 더하거나 빼는 것은 의미 있는 연산이다.

 

하지만 메모리의 주소를 값으로 저장하기 때문에 곱셈이나 나눗셈은 불가능하다.

 

결론은 포인터에서 연산은 증가시키거나 감소시키기만 가능하다.

 

하지만 포인터도 다른 변수들과 같이 하나의 변수라고 해서 연산의 의미가 같은 건 아니다.

 

보통 변수들은 ++ 연산자와 -- 연산자를 사용 시 1이 증가되고 1이 감소되지만,

 

포인터는 자료형이 무엇이냐에 따라 증가되는 값과 감소되는 값이 다르다.

 

만약 포인터 변수에 저장된 메모리 주소가 1000번지이고 자료형이 int형이면

 

1000번지로부터 4씩 증가되거나 감소되는 것이다.

 

또한 자료형이 double형이거나 long 형이면 

 

1000번지로부터 8씩 증가 되거나 감소되는 것이다.

 

그렇다면 예제로 한번 보도록 한다.

 

 

char *pc;

int *pi;

double *pd;


pc = (char *)10000;

pi = (int *)10000;

pd = (double *)10000;


printf(%d %d %d", pc, pi, pd);


pc++;

pi++;

pd++;


printf("%d %d %d",pc, pi, pd);

printf("%d %d %d", pc+2, pi+2, pd+2);

return 0;

 

해석

char형을 가리키는 포인터 변수 pc를 선언;

int형을 가리키는 포인터 변수 pi를 선언;

double형을 가리키는 포인터 변수 pd를 선언;

 

절대 주소 사용을 이용해서

 

포인터 변수 pc에 char형으로 10000번지를 시작 주소로 지정해주고,

포인터 변수 pi에 int형으로 10000번지를 시작주소로 지정해주고,

포인터 변수 pd에 double형으로 10000번지를 시작주소로 지정해준다.

 

현재 포인터 변수 pc, pi, pd 각자가 저장하고 있는 메모리 주소를 10진수로 출력한다. 모두 10000번지라고 출력된다.

 

포인터 주소 pc에 저장된 주소에서 pc의 자료형의 크기만큼 증가한다.

포인터 변수 pi에 저장된 주소에서 pi의 자료형의 크기만큼 증가한다.

포인터 변수 pd에 저장된 주소에서 pd의 자료형의 크기만큼 증가한다.

 

자료형의 크기만큼 증가된 포인터 변수 pc, pi, pd의 주소를 출력한다.

 

pc는 char형이어서 1byte가 커지기 때문에 10001번지가 출력된다.

pi는 int형이어서 4byte가 커지기 때문에 10004번지가 출력된다.

pd는 double형이어서 8byte가 커지기 때문에 10008번지가 출력된다.

 

그리고 포인터 변수 pc, pi, pd에 저장된 주소 + 2씩 하여 출력하는데

 

현재 포인터 변수 pc, pi, pd모두 자료형의 크기만큼 1번씩 증가된 상태로 저장되어 있기 때문에

 

pc의 자료형은 char형이므로 저장된 주소(10001) + 1byte * 2 인 10003이 출력되고

pi의 자료형은 int형이므로 저장된 주소(10004) + 4byte * 2 인 10012가 출력되고,

pd의 자료형은 double형이므로 저장된 주소(10008) + 8byte * 2 인 10024가 출력된다.

 

이런 식으로 복잡해 보이지만 천천히 차분한 마음으로 따라오다 보면 어느새 이해가 되었을 것이다.

 

이제 포인터 연산을 좀 쉽게 정리하자면 

 

포인터 변수는 다른 변수들의 증감과 감소와 달리 자료형의 크기에 따라 주소의 번지가 증가되고 감소한다.

 

보통 변수는 1씩 증가하는 방면 포인터 변수는 자료형의 크기가 얼마냐에 따라 증가량과 감소량이 달라진다고 볼 수 있다.

 


※ 간접 참조 연산자와 증감 연산자 ※

 

++나 --와 같은 증감 연산자는 간접 참조 연산자인 *(에스터리스크)와 같이 사용될 수 있다.

 

하지만 주의해야 할 점이 있는데, 증감 연산자를 포인터에 적용할 수도 있고 포인터가 가리키는 대상에 적용할 수도 있다.

 

이것을 잘 구별해야 포인터에 저장된 주소를 증감할 수도 아니면 그 주소의 데이터값을 증감할수도 있다.

 

다음의 글을 보고 외우도록 하자.

 

a = *p++ : 먼저 p가 가리키는 값을 a에 대입한 후에 p를 증가한다.

a = (*p)++ : 먼저 p가 가리키는 값을 v에 대입한 후에 p가 가리키는 값을 증가한다.

a = *++p : 먼저 p를 증가시킨 후에 p가 가리키는 값을 v에 대입한다.

a = ++*p : p가 가리키는 값을 가져온 후에 그 값을 증가하여 a에 대입한다.

 

다음의 코드를 dev c++ 같은 IDE에 직접 타이핑하거나 복붙 하여 

출력 결과와 바로 위에 증감식을 보면서 비교해보면서 이해해보도록 한다.

 

※ 코드 ※

int a = 10;

int *p = &a;

printf("a = %d, p = %p\n",a, p);


(*p)++;

printf("a = %d, p = %p\n",a, p);

printf("a = %d, p = %p\n",a, p);


*p++;

printf("a = %d, p = %p\n",a, p);

return 0;

 

※ 출력 결과 ※

 

a = 10, p = 62FE44

a = 11, p = 62FE44

a = 11, p = 62FE44

a = 11, p = 62FE48

 


 

포인터를 자료형을 맞춰줄 때는 형 변환 방법을 사용하면 되는데 다들 포인터를 배우기 전에 이미 배웠을 것이다.

 

사용법은 

 

변수면 앞에 임시로 바꿔줄 자료형을 ()로 감싸면 된다.

 

int a;

(double)a;

 

위에 코드는 int형으로 선언된 변수 a를 임시적으로 double형으로 바꾼 것이다.

 


 

※ 포인터와 배열 ※

 

포인터와 배열은 아주아주 밀접한 관계를 가지고 있다. 그 이유는 배열의 이름이 포인터이기 때문이다.

 

포인터를 처음 공부하는 사람 중에 배열의 이름이 포인터이다 라는 말이 이해가 안 될 수 있다. 

 

하지만 그렇게 어려운 것은 아니다.

 

말 그대로 a라는 배열 이름이 있다고 가정하에 설명하자면 a는 포인터인 것이다.

 

a가 배열의 시작 주소라고 보면 된다. 물론 이게 뭔 소리인지 이해가 안 될 수도 있다.

 

보통 변수의 주소를 포인터 변수에 저장할 때 주소 연산자 &(엠퍼센트)를 이용한다.

 

하지만 배열의 주소를 포인터 변수에 저장할 때는 주소 연산자 &(엠퍼센트)가 필요 없다.

 

이미 배열 이름 자체가 포인터이기 때문에 다음과 같이 코딩하면 된다.

 

int a[] = {1,2,3,4,5,6};

int *p = a;

int a[] = {1,2,3,4,5,6};
int *p = a;

 

해석 

정수형 배열이 선언되었고 그 배열의 이름은 a이며 원소의 크기는 6이다.

 

같은 정수형 포인터 변수에 a의 주소를 저장하려고 하는데 배열 a는 이미 포인터 이기 때문에 &없이 배열의 이름만 써주면

 

해당하는 배열의 시작 주소가 포인터 변수에 저장되는 것이다. 

 

따라서 배열은 연속된 공간을 차지하기 때문에 

 

포인터 변수에 배열의 시작 주소를 의미하는 배열 이름 a를 저장하면 자동적으로 포인터 변수는 배열의 시작주소를 가리킨다

 


 

※ 포인터를 배열처럼 사용 ※

 

위에 글과 같이 배열을 포인터처럼 사용할 수 있다.

 

그렇다면 포인터도 배열처럼 사용할 수 있는데 바로 예제를 통해 알아보자

 

int a[] = {10,20,30,40,50};

int *p;

p=a;

printf("a[0]=%d a[1]=%d a[2]=%d\n", a[0], a[1], a[2]);

printf("p[0]=%d p[1]=%d p[2]=%d\n", p[0], p[1], p[2]);

해석

정수형 배열 a를 선언하고 10,20,30,40,50으로 초기화해줬다. 배열 원소의 크기는 5이다.

정수형 포인터 p를 선언했다.

배열 a의 시작 주소를 포인터 변수 p에 저장한다.

 

배열 a의 원소들의 값들을 출력한다.

포인터를 이용해 배열 a의 원소에 접근하여 배열처럼 사용해 원소들의 값들을 출력한다.

 

 

위의 코드를 보면 배열의 시작 주소와 같은 의미를 가진 배열의 이름을 포인터 변수에 넣어줌으로써 

포인터 변수를 이용해 배열의 원소에 접근이 가능한 것이다.

 

이렇게 포인터를 간단하게 익혀 보았다. 

 

이쯤에서 이해됐다고 해서 안심하긴 이르다. 아직 활용을 안 해봤기 때문에 활용하다 보면 꽤 애 먹을 거다.

 

반응형

+ Recent posts