Home C언어 (9) 포인터
Post
Cancel

C언어 (9) 포인터

C언어 (9) 포인터

C언어에서 가장 중요하고 이해하기 어려운 개념으로 꼽히는 것이 포인터일 것이다.
변수와 배열 이야기를 하며 계속 주소를 언급했는데, 이 주소를 어떻게 쓸 수 있는가?에 대한 이야기다.

중학생 때 처음 C언어를 학원에서 배웠었는데 포인터는 어려우니까 넘어가자~라고 말했었다.
지금 생각해보면 이해가 안되는 말인 것이 포인터를 모르고 배열이나 함수를 완전히 이해할 수는 없다.
더 심화된 내용은 더더욱 이해할 수 없게 된다.
어째서 학원에서 그렇게 말을 했을까? 3~4개월 정도 밖에 안 다녔지만 지금 생각하면 많이 아쉬운 것 같다.
현재는 내가 강사로 일하고 있기에 포인터와 그 응용에 대해 학생이 최대한 이해할 수 있도록 노력한다.
아무튼 그만큼 중요하다는 것이다.

포인터

image

변수 설명과 똑같이 시작을 하자.
변수란 우리가 프로그램을 작성할 때 숫자나 문자 같은 데이터를 저장하기 위한 공간이다.
변수는 저장되는 주소가 있고, 공간이 있다.
그렇다면 이 주소도 이용할 수 있을까??
이 문제를 위해 포인터 변수가 존재한다. 포인터 변수란 주소를 저장하는 변수이다.

그림처럼 변수 num이 있고, num의 주소가 00BAFD64라고 하자.
num = 10이라는 명령은 주소 00BAFD64부터 4바이트의 값을 10으로 바꾸는 것이다.
여기서 pnum이라는 포인터 변수는 num의 주소인 00BAFD64를 저장한다. 코드로 더 봐보자.

1
2
int num = 10;
int* pnum = #

scanf에서 언급했었는데 &연산자는 변수의 주소를 반환하는 연산자이다.
그런데 &는 이전에 and라고 했었는데 지금은 주소를 반환하는 연산자라고 한다.
둘의 차이는 a & b 이렇게 두 개의 피연산자가 있으면 and의 의미이고, &num처럼 피연산자가
하나인 단항 연산자로 쓰이면 피연산자의 주소값을 가져오라는 뜻이 된다.

또 새로 나온 int* 라는것이 있다. 포인터 변수도 변수이기 때문에 먼저 선언을 해야한다.
변수를 선언할 때 앞에 자료형이 붙는데 자료형 다음에 *을 붙이면 포인터형 자료형이다.
int형 변수의 주소를 저장하고 싶으면 int*, float형 변수의 주소를 저장하고 싶으면 float*
이렇게 저장하고 싶은 변수의 주소에 따라 포인터의 자료형이 변한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main() {
	int num = 10;
	char c = 65;
	double f = 12.3;

	printf("num의 주소 : %p\n", &num);
	printf("c의 주소 : %p\n", &c);
	printf("f의 주소 : %p\n", &f);
	printf("\n");
	int *pnum = &num;
	char* pc = &c;
	double* pf = &f;

	printf("pnum의 값 : %p\n", pnum);
	printf("pc의 값 : %p\n", pc);
	printf("pf의 값 : %p\n", pf);
}	

image
이렇게 값을 확인해보면 포인터형 변수에 변수들의 주소값이 똑같이 들어간다는 것을 알 수 있다.
위의 코드를 실행할 때마다 주소의 값은 바뀌지만 크기는 16글자로 같을 것이다.
이는 현재 컴퓨터 시스템이 64비트 체제이기 때문으로, 64비트 시스템에서는 8바이트의 메모리 주소를 가진다.
16진수 2글자가 1바이트 (16*16 = 256) = (2^8) = (8bit)이므로 16글자인 것이다.
만약 32비트 운영체제를 사용하고 있다면 메모리 주소로 4바이트를 가지니까 8글자의 주소가 출력될 것이다.

포인터 변수를 선언할 때 int *a와 int* a는 같은 의미이다.

1
2
int *a
int* a

그런데 여러개의 변수를 선언한다면 주의할 점이 있다.

1
2
int *a, b, c;
int* a, b, c;

윗 줄은 a는 포인터 변수이지만 b, c는 그냥 int형 변수이다.
반면에 밑 줄은 a, b, c모두 포인터 변수이다. 이 사실에 주의해서 포인터 변수를 선언하자.

포인터 변수 참조

이렇게 주소 값을 가진 포인터 변수를 만들었으니, 주소를 통해 값을 참조할 수도 있다.
* 연산자를 간접참조 연산자라고 부르고, &가 피연산자가 1개면 and가 아닌 것처럼 *도 곱셈이 아니다.
* 연산자는 주소를 통해 메모리 공간에 접근한다.

1
2
3
4
5
6
7
#include<stdio.h>
int main() {
	int num = 10;
	int* pnum = &num;

	printf("%d %d", *(&num), *pnum);
}	
1
10 10

라는 결과를 볼 수 있다. 주소를 통해 메모리 공간에 접근하기에 간접 접근이라는 표현을 사용한다.
num과 *(&num)과 *pnum은 같은 공간을 가리키고, 이것을 통해 값을 변경할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main() {
	int num = 10;
	int* pnum = &num;

	printf("%d\n", num);
	*(&num) = 20;
	printf("%d\n", num);
	*pnum = 30;
	printf("%d\n", num);
}	

이 코드를 실행해 보면

1
2
3
10
20
30

로 *(&num) = 20;, *pnum = 30;을 통해서도 num이 바뀌는 것을 확인할 수 있다.
정리를 하면 포인터 변수 pnum에 num의 주소가 저장되니까 pnum은 num을 가리키는 것이다.
이 pnum에 *연산자를 통해 참조하면 num에 접근하는 것과 같은 효과를 낼 수 있다.

포인터 변수의 크기

이전에 변수를 배울 때 char은 1바이트, int는 4바이트, double은 8바이트의 크기를 할당받는다고 했다.
그런데 주소의 크기는 이런 자료형과는 관계 없이 64비트 시스템에선 8바이트, 32비트 시스템에선 4바이트다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int main() {
	int num = 10;
	char c = 65;
	double f = 12.3;

	printf("num의 크기 : %d\n", sizeof(num));
	printf("c의 크기 : %d\n", sizeof(c));
	printf("f의 크기 : %d\n", sizeof(f));
	printf("\n");

	int* pnum = &num;
	char* pc = &c;
	double* pf = &f;

	printf("pnum의 크기 : %d\n", sizeof(pnum));
	printf("pc의 크기 : %d\n", sizeof(pc));
	printf("pf의 크기 : %d\n", sizeof(pf));
}	

image

여기서 생기는 의문이 왜 포인터형 변수에도 int*, char*처럼 자료형이 여러가지가 있느냐이다.
어차피 주소의 크기는 시스템에 따라 8바이트 또는 4바이트인데 이렇게 구별해야하는 이유가 있을까?
조금 더 생각을 해보자.

만약 모든 포인터의 자료형이 pointer*로 통일된다고 가정해보자.
그 다음 *로 참조해 값을 가져오려고 한다.

1
2
3
4
5
6
7
8
int num = 10;
char c = 65;

pointer* pnum = &num;
pointer* pc = &c;

*pnum;
*pc

pnum에는 num의 주소, pc에는 c의 주소가 저장되어 있다.
그런데 메모리 공간에서 값을 가져오려면 주소로부터 얼마만큼 읽어야할지 알아야한다.
int면 4바이트, char이면 1바이트의 공간을 주소값에서부터 읽어야겠지만 pointer* 에는 그에 대한 정보가 없다.
그렇기 때문에 포인터 변수는 int*, char* 등의 자료형을 사용해 변수를 구별하고, 필요한 정보를 얻을 수 있다.

포인터 사용 시 주의점

변수를 선언하고 초기화하지 않는다면 그 변수에는 알 수 없는 값이 들어있다.
똑같이 포인터 변수를 선언하고 초기화하지 않아도 어떤 값은 들어있으나 무엇인지는 모른다.

1
2
3
4
5
6
#include<stdio.h>
int main() {
	int* pnum;

	*pnum = 10;
}	

이렇게 초기화하지 않은 포인터 변수에 10을 넣으려고 하면 무슨 일이 벌어질까?
굉장히 위험한 코드로 만약 pnum이 중요한 영역을 가리키고 있었다면 그 영역이 10으로 덮인다.
그러면 예상치 못한 일이 일어날 것이다.

1
2
3
4
5
6
#include<stdio.h>
int main() {
	int* pnum = 123456; //무작위 값

	*pnum = 10;
}	

비슷하게 이런 코드도 위험할 것이다. 123456이라는 주소에 무슨 정보가 있는지 모르기 때문이다.
이런 것을 방지하기 위해 포인터 변수는 int* pnum = NULL 로 초기화할 수 있다.
NULL은 아무것도 없다는 뜻으로 0과 동일한 의미이다.

또 조심해야 하는 것이 있다.
위에서 포인터 변수의 크기에 대해 이야기했는데, 만약 다른 자료형의 포인터를 쓰면 어떻게 될까?
포인터 변수는 주소에서 얼마만큼 읽어야할지 알기 위해 int*, double* 등으로 구분한다고 했다.

만약 int num이라는 변수를 char *pcnum = &num 또는 double *pdnum = &num로 이용하면 어떻게 될까?
image
num의 값은 &num으로부터 4바이트에 저장되어 있다.
그런데 *pcnum은 &num을 기준으로 1바이트만 읽을 것이고, *pdnum은 &num을 기준으로 8바이트를 읽는다.
특히 *pdnum의 경우 num 변수 범위 밖까지 참조하므로 더 위험하다고 볼 수 있다.


변수의 주소를 저장하는 포인터 변수에 대해서 처음 알아보았다.
아직까지 어려운 것은 크게 없는 것 같은데, 이걸 왜 쓰는거지 싶을 것이다.
어차피 num과 *pnum이 같다면 그냥 num만 쓰면 되는 것이 아닐까?라고 생각할 수 있다.
다음 글에서 포인터와 배열의 관계를 보며 사용하는 것을 더 자세히 알아보자.

This post is written by PRO.