제 17강) 포인터(Pointer) |
오늘은 정말 정말 정말 정말 정말 정말 정말 정말 정말 정말 정말 정말 중요한 시간입니다.
바로 포인터에 대해서 배우는 시간이기 때문이죠.
1) 공포의 대상, 포인터 |
C언어에서 가장 공포스럽다고 느껴지는 대상인 포인터입니다.
포인터란? (출처 : 위키백과) |
포인터는 다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수를 말한다. 이때 포인터가 가리키는 값을 가져오는 것을 역참조(Dereferencing)라고 한다. |
위의 설명은 "위키페디아"에 나온 설명입니다.
사실 정말 잘 설명되어있습니다. 하지만 이게 무슨 말인가 라고 하시는 분들이 많으시겠죠.
우리는 "4강 변수"에서 변수에 대해서 배울때 다음과 같이 배웠습니다.
변수는 "어떤 데이터를 저장하는 메모리 공간"이라고 이해하시면 됩니다. 즉, 변수를 선언하게 되면 그 변수 선언에 사용한 자료형 만큼의 크기를 가진 메모리 공간을 할당하게 됩니다. |
위와 같이 배웠죠.
int num = 5;
위와 같이 변수를 선언하였다고 합시다.
위의 그림은 메모리 공간을 간단하게 표현해보았습니다.
위의 그림에서 1칸은 1Byte입니다.
위에서 num 변수는 메모리 공간에서 임의의 구간에 4byte 크기로 num이라는 이름으로 공간이 할당되고 5라는 값이 저장됩니다.
그런데 포인터라는 것은 무엇이냐!!
변수의 메모리 공간의 주소 가리키는 변수입니다.
이해가 안되시면 다르게 생각해봅시다.
길동이에겐 집이 있습니다.
평소에는 길동이집 이라고 표현을 하지만 실제적으로 집이 위치한 주소가 있습니다.
일반적으로 변수의 이름을 길동이집이라고 한다면 포인터는 실제적으로 집이 위치한 주소라고 할 수 있습니다.
2) 포인터 사용해보기 - 1 |
포인터는 다음과 같이 사용합니다.
#include <stdio.h>
int main()
{
int num = 5;
int* ptrNum = # // 포인터 연결
return 0;
}
바로 변수명 앞에 *를 붙이게 됩니다.
(*는 별표, 스타, 애스터리스크(Asterisk) 등등으로 불립니다.)
이때 포인터를
int* ptr = #
int *ptr = #
int*ptr = #
3가지 방법으로 선언이 가능합니다.
그리고 전부 같은 말입니다.
포인터도 변수다 |
int형 변수에는 int 값이 들어가듯, int형 포인터에는 int형 변수가, double형 포인터에는 double형 변수가 연결되어야 합니다. |
int* ptrNum = #
ptrNum이라는 int형 포인터변수를 선언했습니다.
이 ptrNum은 num의 주소에 엑세스하게 됩니다.
이제 &에 대해서 이야기해보도록 합시다.
부록) & |
이 &는 이전에도 썼습니다.
int num;
scanf("%d", &num);
바로 scanf에서 우리는 줄곧 써왔습니다.
그럼 이 &은 무슨 기호일까요?
바로 &뒤에 오는 변수의 주소값 이라는 뜻입니다.
그럼 위의 소스는 이렇게 해석이 되겠죠.
num의 주소에 정수값을 넣어준다 라고 생각하시면 됩니다.
부록) 주소 할당 |
메모리 주소는 16진수로 다음과 같은 형식으로 이루어집니다.
0x00000000 |
(꼭 8자리 숫자는 아닙니다. 그 이상이 될 수도 있습니다.)
각 자리는 바이트단위입니다.
만약에 int num이 0x00000000에 할당되어 있다면 이후에 선언될 변수들은 어느 주소에 할당될수 있을까요?
바로 0x00000004 부터 할당이 가능합니다.
int는 4바이트이므로 0x00000000 ~ 0x00000003까지 주소를 차지하고 있게됩니다.
즉, 메모리상에 선언된 첫주소(시작주소)라고 할 수 있습니다.
(하지만 첫 주소가 아닌 중간주소에 접근하면 안됩니다.)
2) 포인터 사용해보기 - 2 |
다시 포인터의 원래 이야기로 돌아옵시다.
int* ptrNum = #
이제 &에 대해서 알아보았으니 한번 풀어봅시다.
ptrNum은 num의 주소에 엑세스 하게 됩니다.
그래서 ptrNum으로 num의 값에 접근할 수 있기때문에 값을 받아올 수도, 변경할 수도 있습니다.
다음 예제를 봅시다.
#include <stdio.h>
int main()
{
int num = 5;
int* ptrNum = # // 포인터 연결
printf("num: %d \n", num); // num의 값
printf("*ptrNum: %d \n", *ptrNum); // 포인터를 이용하여 나온 값
printf("num's address: %p \n", &num); // num의 주소값
printf("ptrNum's address: %p \n", ptrNum); // 포인터가 가리키는 변수의 주소값
return 0;
}
위와 같이 작성하고 실행하면
이렇게 나오게 됩니다.
하나 하나 파헤쳐봅시다.
printf("num: %d \n", num); // num의 값 printf("*ptrNum: %d \n", *ptrNum); // 포인터를 이용하여 나온 값
먼저 1번줄은 이해하실겁니다.
문제는 2번줄이겠지요.
우리가 포인터를 선언할 때 앞에 애스터리스크(*)를 붙여서 선언하였습니다.
그런데 이렇게 선언한 포인터가 가리키는 변수의 값에 접근할 때는 선언할때처럼 앞에 애스터리스크를 붙입니다.
그래서 2번의 결과가 1번의 결과와 같게 됩니다.
printf("num's address: %p \n", &num); // num의 주소값
printf("ptrNum's address: %p \n", ptrNum); // 포인터가 가리키는 변수의 주소값
여기서 1번줄은 num의 메모리 상에 할당된 주소값을 출력합니다. (이때 출력 형식은 %p)
그리고 ptrNum이 가리키는 변수의 주소값을 출력할때는 애스터리스크를 사용하지 않고 그냥 사용합니다.
그래서 2번의 결과와 1번의 결과가 같습니다.
요렇다고 보시면 됩니다.
2) 포인터 사용해보기 - 3 |
포인터를 이용하여 값을 변경해봅시다.
#include <stdio.h>
int main()
{
int num = 5;
int* ptrNum = # // 포인터 연결
printf("num: %d \n", num); // num의 값
printf("*ptrNum: %d \n", *ptrNum); // 포인터를 이용하여 나온 값
printf("num's address: %p \n", &num); // num의 주소값
printf("ptrNum's address: %p \n", ptrNum); // 포인터가 가리키는 변수의 주소값
printf("\n");
*ptrNum = 8; // 값을 5에서 8로 변경!!
printf("num: %d \n", num);
printf("*ptrNum: %d \n", *ptrNum);
printf("num's address: %p \n", &num);
printf("ptrNum's address: %p \n", ptrNum);
return 0;
}
방금 위에서 봤던 예제에서 값만 변경(13번줄)하는 예제로 변경하였습니다.
포인터가 가리키는 변수의 값에 접근할때는 애스터리스크를 사용한다고 했었죠?
그래서 *ptrNum 즉, num의 값에 직접 접근하여 5에서 8로 바꾸었습니다.
그래서 결과도 아름답습니다.
원래 주소값은 계속 바뀝니다. |
예제를 실행 할때마다 주소값은 계속 유동적으로 바뀔겁니다. 그 이유는 변수의 값이 할당되는 메모리 공간이 정해져 있지 않기 때문이죠. 이 메모리 주소는 계속 바뀌지만 해당 변수에 엑세스하는 포인터는 변하지 않습니다. |
그런데 이 포인터는 도중에 연결을 바꿔서 다른 변수에 연결할 수 있습니다.
#include <stdio.h>
int main()
{
int num = 5;
int* ptrNum = # // 포인터 연결
printf("num: %d \n", num); // num의 값
printf("*ptrNum: %d \n", *ptrNum); // 포인터를 이용하여 나온 값
printf("num's address: %p \n", &num); // num의 주소값
printf("ptrNum's address: %p \n", ptrNum); // 포인터가 가리키는 변수의 주소값
printf("\n");
int num2 = 9;
ptrNum = &num2;
printf("num2: %d \n", num2);
printf("*ptrNum: %d \n", *ptrNum);
printf("num2's address: %p \n", &num2);
printf("ptrNum's address: %p \n", ptrNum);
return 0;
}
중간에 num2라는 변수로 연결하였습니다.
이렇게 유연하게 됩니다.
3) 포인터를 왜 사용하는가 |
이렇게만 보면 포인터의 이점을 잘 모를겁니다.
굳이 변수가 있는데 주소를 이용해서 포인터를 써야만 하는가? 라고 생각하실 수 있죠.
1. 값을 전달할 때 주소값만 전달 |
이 부분은 아직 배우지 않은 배열이나 구조(struct)에 관한 이야기이긴 하지만!
값을 전달할 때 배열이나 구조체의 경우에는 크기가 클 수 있습니다.
이때 처음에 해당 값이 메모리상에 할당된 주소값만 포인터로 넘겨주게 되면 상당히 모든 면에서 절약됩니다.
(전체 크기가 20byte라고 한다면 메모리에 할당된 주소(int라고 할때)만 보내면 4byte로 해결이 된다는 것이죠)
2. 메모리에 직접 접근이 가능하다. |
연결된 포인터를 가지고 byte단위로 끊어서 자료를 읽거나 쓸 수 있으며, 복사를 할때도 유용합니다.
3. 자료구조에 유용하다. |
아마 대부분 자료구조를 C로 많이 배웁니다.
이때 포인터는 복잡한 자료구조를 쉽게 작성하도록 만들어줍니다.
4. 함수를 변수마냥 접근할 수 있습니다. |
이것을 함수 포인터라고 하는데요.
변수마냥 쉽게 사용케 해줍니다.
5. 배열, 문자열 |
이 다음에 배울 배열과 문자열은 포인터 그 자체입니다.
6. 프로그램을 유연하게 짤 수 있다. |
포인터를 이용하여 자유롭게 메모리 공간을 왔다갔다 할 수 있습니다.
4) 포인터가 위험한 이유 |
하지만 포인터에도 단점이 있습니다.
그런데 이런 단점이 프로그램에서는 위험한 경우가 발생합니다.
1. 메모리에 직접 접근이 가능하다. |
장점이자 단점입니다.
메모리에 직접 접근하기 때문에 잘못접근하여 시스템 오류가 발생할 수 도 있습니다.
(이것을 시스템 주소에 접근했다고 합니다.)
2. 디버깅이 어렵다. |
변수의 주소값을 참조하다 보니 디버깅을 하는데에 많은 시간이 소요됩니다.
(디버깅은 프로그램의 오류를 찾는 테스트과정입니다.)
3. 포인터에 대한 이해도가 높아야한다. |
위의 2개의 이유를 합한건데요.
직접 메모리에 접근하고 디버깅이 어렵다보니 숙련도가 많아야 하고, 오류가 잘 생깁니다.
다음 시간에는 |
이렇게 하여 포인터를 배웠습니다.
"포인터는 변수의 주소값을 가리키는 변수"라고 생각하시면 됩니다.
다음 시간에는 배열을 배울건데요.
배열을 배우면서 포인터와 연관지어서 더 포인터를 익혀봅시다.
'Study > C언어' 카테고리의 다른 글
처음하시는 분들을 위한 C언어 기초강의 시즌2 - 19 [문자열] (0) | 2018.10.17 |
---|---|
처음하시는 분들을 위한 C언어 기초강의 시즌2 - 18 [배열과 포인터] (0) | 2018.10.17 |
처음하시는 분들을 위한 C언어 기초강의 시즌2 - 07 [입력문과 출력문(printf, scanf, scanf_s)] (0) | 2018.10.17 |
처음하시는 분들을 위한 C언어 기초강의 시즌2 - 08 [변수에 대한 추가적인 이야기] (0) | 2018.10.17 |
처음하시는 분들을 위한 C언어 기초강의 시즌2 - 10 [연산자 이야기] (0) | 2018.10.17 |