'포인터'는 메모리의 주소를 가지고 있는 변수이다. 컴퓨터에서 메모리는 바이트 단위로 주소가 매겨져 있다.
마을에 건물들이 가지고 있는 주소를 이용하여 원하는 집을 찾을 수 있는 것과 같이 컴퓨터 메모리도 주소로 접근할 수 있고, 주소를 이용하여 값을 저장하기도 하고, 값을 읽기도 한다.
우리가 변수를 만들 때, 변수는 어디에 만들어질까? 변수는 자료들을 정장하기 때문에 메모리에 만들어지는 것이 확실하다. 메모리의 각 바이트마다 고유한 주소가 매겨진다. 이 주소를 사용하여 우리는 메모리의 바이트에 접근할 수 있다. 만약 시스템이 20바이트의 메모리를 가지고 있다고 하자. 그럼 첫 번째 바이트 주소는 0, 두 번째 바이트 주소는 1, 마지막 바이트 주소는 19이다. 프로그램에서 변수를 만들면 이 변수는 컴파일러에 의해 메모리 공간의 비어있는 위치를 차지한다. 변수가 차지하는 메모리 공간의 크기는 변수의 자료형에 따라 달라진다. 일반적인 PC 환경에서 char형 변수는 1바이트, int형 변수는 4바이트, float형 변수는 4바이트, double형 변수는 8바이트를 차지한다.
주소연산자 &
이 변수들의 주소를 알 수 있는 방법은 변수의 주소를 계산하는 연산자 &가 있다. 주소 연산자 &는 변수의 이름을 받아서 변수의 주소를 반환한다. 예를 들어 int i; 라고 변수를 정의했으면, 변수 i의 주소는 &i 라고 하면 알 수 있다.
또한 실제로 출력되는 주소를 보면 우리의 예상과는 상당히 다르다. 메모리 공간에 변수를 배치하는 것은 컴파일러와 운영체제의 권한이다. 여기서 한 가지 주의할 점은 주소를 출력하는 형식 지정자는 %p이다. %p는 주소를 16진수로 출력한다. %u를 사용하여도 된다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
int i = 10;
char c = 69;
double f = 12.3;
printf("i의 주소: %p\n", &i); //변수 i의 주소 출력
printf("c의 주소: %p\n", &c); //변수 c의 주소 출력
printf("f의 주소: %p\n", &f); //변수 f의 주소 출력
return 0;
}
포인터의 선언
포인터는 가리킨다는 뜻의 동사 point에 er을 붙인 것이다. 따라서 가리키는 것이라는 뜻이다. 포인터는 변수의 주소를 가지고 있는 변수이다. 포인터는 변수이지만 저장하고 있는 것이 보통의 변수처럼 데이터가 아니라, 메모리의 주소이다. 포인터도 변수이다. 변수임을 강조하기 위하여 때때로 '포인터 변수'라고 한다. 따라서 사용하기 전에 선언되어야 한다. 포인터의 이름은 일반적인 변수 이름과 같은 규칙으로 만들면 된다. 포인터를 선언하려면 포인터가 가리키게 되는 대상을 먼저 쓰고 *를 붙인 다음, 포인터의 이름을 쓴다. 밑의 예를 보자. (이때 *는 수식에서는 곱셈 기호이지만 여기서는 곱셈과 아무 상관 없다. )
int *p;
p는 정수 데이터를 가리키는 포인터이다. 현재는 선언만 하고 아직 초기화하지 않았으므로 포인터는 아무 의미 없는 값을 가지고 있다. ( int *p 라고 써도 되고, int* p라고 써도 된다.) 어떠한 타입의 데이터를 가리키는 포인터도 만들어보자.
char *pc //문자를 가리키는 포인터 pc
float *pf //실수 float형을 가리키는 포인터 pf
double *pd //실수 double형을 가리키는 포인터 pd
int *p1, p2, p3; //p1은 포인터 변수 p2 와 p3은 정수형 변수가 된다.
int *p1, *p2, *p3 //p1, p2, p3는 포인터 변수가 된다.
//포인터 변수를 한 줄에 선언할 때는 *를 모든 변수에 붙여주어야 한다.
포인터 초기화하기
포인터가 생성된 직후에는 초기화되어 있지 않다. 따라서 포인터를 사용하기 전에는 반드시 초기화가 필요하다. 포인터에 주소가 저장되려면 &연산자를 이용하여 변수의 주소를 계산해 포인터에 대입할 수 있다.
int i = 10; // 정수 변수 i가 선언되고 10으로 초기화된다.
int *p; // 정수 포인터가 선언된다.
p = &i; // 포인터 p에 변수 i의 주소를 저장한다.
이때 변수 i의 주소가 4였다고 가정하고 p = &i의 연산을 실행하면, 변수 i의 주소인 4가 p에 대입된다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
int i = 10; //정수 변수 i에 10을 저장한다.
double f = 12.3; //실수 변수 f에 12.3을 저장한다.
int *pi = NULL; //정수 포인터 pi를 NULL로 초기화 한다.
double *pf = NULL; //실수 포인터 pf를 NULL로 초기화 한다.
pi = &i; //pi에 i의 주소를 저장한다.
pf = &f; //pf에 f의 주소를 저장한다.
printf("%p %p\n", pi, &i);
printf("%p %p\n", pf, &f);
return 0;
}
간접 참조 연산자 *
포인터가 단순히 메모리의 주소만 저장할 수 있는 변수라면 별료 유용하지 않을 것이다. 포인터가 유용한 이유는 포인터를 통하여 값을 읽어오거나 값을 변경할 수 있기 때문이다. 포인터 p가 가리키는 주소에 있는 데이터를 읽으려면 p앞에 *기호를 붙여서 *p하면 된다. 이것을 포인터를 통하여 메모리를 간접 참조한다고 한다. 다음 코드를 살펴보자.
int i = 10; // 정수 변수 i가 선언되고 10으로 초기화된다.
int *p; // 정수 포인터가 선언된다.
p = &i; // 포인터 p에 변수 i의 주소를 저장한다.
printf("%d \n", *p); // 10이 출력된다.
( *는 포인터가 가리키는 변수의 값을 반환한다.)
포인터 p가 가리키는 변수가 i라면 *p는 변수 i와 동일하다. 따라서 *p를 출력하면 변수 i의 값이 출력되고 *p에 어떤 값을 저장하면 변수 i에 그 값이 저장된다. 결론적으로 포인터를 이용하면 변수를 간접적으로 참조할 수 있는 것이다.
(포인터를 배우기 전까지는 변수에 저장된 내용을 변경하려면 변수 이름을 사용하여 변수의 내용에 직접 접근하는 방식 뿐이었다. 그러나 포인터를 사용하게 되면 포인터를 이용하여 간접적으로 변수의 내용을 변경할 수 있다. 이 두 가지 방법의 결과는 동일하다.)
*p를 좀 더 자세히 분석하여보자. *p는 p가 가리키는 위치에 있는 데이터를 가져오라는 의미이다. 만약 p가 int형 포인터이면 p가 가리키는 위치에 정수가 있다고 가정하고 4바이트를 읽어 들인다. 만약 double형 포인터이면 p가 가리키는 위치에 실수가 있다고 생각하고 8바이트를 읽어들이는 것이다. 이것이 포인터의 타입이 필요한 이유이다. 만약 포인터의 타입이 없다면 포인터를 이용하여 데이터를 읽어들일 때, 몇 개의 바이트를 읽어야 할지 알 수 없게 될 것이다.
'C언어' 카테고리의 다른 글
혼공C 도전실전 예제 내 풀이 (0) | 2023.08.17 |
---|---|
[C언어] 배열을 처리하는 함수 (0) | 2023.08.15 |
[C언어] 배열과 포인터 (0) | 2023.08.13 |
[C언어] 포인터 완전 정복하기 (0) | 2023.08.12 |
[C언어] 포인터 (0) | 2023.08.03 |