학습 목표 : C언어는 메모리에 접근해서 우리가 원하는 방식으로 데이터를 사용할 수 있는 언어입니다. 지금까지 메모리에 접근하는 가장 쉬운 방법은 변수를 사용하는 거이었는데, 이 절에서는 메모리의 주소 값을 이용하는 '포인터'에 대해 알아보겠습니다.
지금까지 변수 선언으로 메모리에 공간을 확보하고, 그곳을 데이터를 넣고 꺼내 쓰는 공간으로 사용했습니다. 변수명은 그러한 메모리 공간을 식별할 수 있는 이름이었고요. 그러나 변수는 선언된 블록({}), 함수 내부로 사용이 제한되어 있습니다. 같은 변수명을 사용했다 하더라도 블록이나 함수가 다르면 별도의 저장 공간을 확보하므로 전혀 다른 변수로 사용되는 것이죠. 그래서 사용 버위를 벗어난 경우도 데이터를 공유할 수 있는 새
로운 방법인 포인터 개념을 이 장에서 설명하겠습니다.
메모리의 주소
메모리라는 것은 우리가 데이터를 넣고 꺼내 쓰는 공간으로, 그 위치를 식별할 수 있어야 합니다. 찬장 어딘가에 라면을 넣어 놓기는 했는데, 그것을 어디다 넣어 놨는지 찾을 수 없다면 먹을 수 없을 겁니다.
다행히도 프로그램이 사용하는 메모리의 위치를 주소 값으로 식별할 수 있습니다. 메모리의 위치를 식별하는 주소 값은 바이트 단위로 구분됩니다. 이 값은 0부터 시작하고 바이트 단위로 1씩 증가하므로 2바이트 이상의 크기를 갖는 변수는 여러 개의 주소 값에 걸쳐 할당됩니다. 예를 들어 int형 변수 a가 메모리 100번지부터 할당되었다면 100번지부터 103번지까지 4바이트에 걸쳐 할당됩니다. 변수 선언 이후에는 4바이트 전테를 a라는 이름으로 사용합니다. 따라서 a = 10; 과 같은 문장은 메모리의 100번지부터 103번지까지 4바이트 공간에 10을 저장하며, a + 20;과 같은 수식은 메모리 100번지부터 103번지까지 저장된 값과 20을 더하는 연산을 수행합니다. 결론은 지금까지는 변수명으로 메모리 공간이나 값을 간단히 사용할 수 있었던 것이죠.
주소 연산자 : &
이제 저장된 공간을 이름이 아닌 주소로 사용하는 방법을 살펴보겠습니다. 여기서 주소란 변수가 할당된 메모리 공간의 시작 주소를 의미합니다. 시작 주소를 알면 그 위치부터 변수의 크기만큼 메모리를 사용할 수 있습니다. 주소는 주소 연산자 &를 사용해서 구합니다. 밑의 코드를 통해 주소 연산자 &의 사용방법을 익히고 변수가 할당된 메모리의 상태를 확인해보겠습니다.
#include <stdio.h>
int main(void) {
int a;
double b;
char c;
printf("int형 변수의 주소 : %u\n", &a);
printf("double형 변수의 주소 : %u\n", &b);
printf("char형 변수의 주소 : %u\n", &c);
return 0;
}
4~6행이 변수를 선언하는 부분입니다. 변수 선언문이 실행되면 각 자료형의 크기만큼 메모리에 저장 공간이 할당됩니다. 만약 변수가 메모리 어디에 할당되었는지 궁금하다면 주소 연산자 &를 사용하면 됩니다. 주소 연산자 &는 단항 연산자 이며, 변수만을 피연산자로 사용하여 시작 주소를 구합니다.
주소 연산자 &를 사용하여 변수에 할당된 메모리의 시작 주소를 확인하고 시작 주소에 변수의 크기를 더하면 변수가 메모리의 어디서부터 어디까지 할당되었는지 확인할 수 있습니다. 예제의 결과를 바탕으로 각 변수의 상태를 알아보겠습니다. 8행은 int형 변수 a의 주소를 출력한 것이므로 변수 a는 6487580번지부터 6487583번지까지 4바이트에 할당되었을 겁니다. double형 변수 b는 6487568번지 부터 6487575번지까지 8바이트가 할당되었으며 char형 변수 c는 6487567 한 바이트에 할당되었을 것 입니다.
(주소는 보통 16진수로 표기를 합니다. 따라서 주소를 출력할 때는 전용 변환 문자인 %p를 사용하는 것이 좋습니다. %p는 주소 값의 데이터 크기에 따라 자릿수를 맞춰 16진수 대문자로 출력합니다. 만약 시스템에서 주소 값 자체를 4바이트로 처리한다면 16진수 한 자리는 4비트에 해당하므로 주소 값 10번지는 0000000A와 같이 16진수 8자리로 출력합니다. 여기서는 설명의 편의를 위해 주소 값을 10진수로 출력하며 주소는 음수가 없으므로 %u 변환 문자를 사용합니다.
포인터와 간접 참조 연산자 : *
이제 변수에 할당된 메모리 주소를 활용하는 방법을 살펴보겠습니다. 메모리의 주소는 필요할 때마다 계속 주소 연산을 수행하는 것 보다 한 번 구한 주소를 저장해서 사용하면 편리한데, 포인터가 바로 변수의 메모리 주소를 저장하는 변수입니다. 따라서 주소를 저장할 포인터도 변수처럼 선언하고 사용합니다. 다만 선언할 때 변수 앞에 *만 붙여주면 됩니다.
#include <stdio.h>
int main(void)
{
int a; //일반 변수 선언
int *pa; //포인터 선언
pa = &a; //포인터에 a의 주소 대입
*pa = 10; //포인터로 변수 a에 10 대입
printf("포인터로 a 값 출력 : %d\n", *pa);
printf("변수명으로 a 값 출력 : &d\n", a);
return 0;
}
6행이 포인터를 선언하는 부분입니다. 일반 변수명을 만드는 규칙에 따라 포인터 이름을 짓고, 변수명 앞에 *를 붙입니다. *는 포인터임을 표시하는 기호입니다. 그리고 자료형을 적는데, 포인터의 자료형은 변수의 자료형을 적습니다. 예를들어 int형 변수의 주소를 저장하면 int를 사용하고 double형 변수의 주소를 저장하면 double을 사용합니다. 5행에서 선언된 변수의 형태가 int형이므로 int를 사용하여 포인터를 선언합니다.
포인터 변수가 선언되면 일반 변수와 마찬가지로 메모리에 저장공간이 할당되고 그 이후에는 변수명으로 사용할 수 있습니다. 8행은 포인터에 a의 시작 주소를 저장하는 문장입니다. 만약 변수 a가 메모리 100번지 부터 103번까지 할당되었다면 주소 값 100이 pa에 저장됩니다.
이제 포인터 pa는 변수 a가 메모리 어디에 할당되었는지 그 위치를 기억하고 있습니다. 이렇게 포인터가 어떤 변수의 주소를 저장한 경우 가리킨다 라고 하고 둘의 관계를 pa → a 처럼 화살표로 간단히 표현합니다. x→y 의 경우에는 x는 y를 가리켜요 라고 말할 수 있고 x는 포인터 이며 변수 y의 주소를 저장하고 있다는 뜻이 됩니다.
포인터가 어떤 변수를 가리키면 포인터로 가리키는 변수를 사용할 수 있습니다. 즉, 포인터 pa로 변수 a를 사용할 수 있습니다. 포인터가 가리키는 변수를 사용할 때는 포인터에 특별한 연산자를 사용하는데, 이를 간접 찹조 연산자(*)라고 합니다. 또는 포인터 연산자라고도 합니다.
9행이 간접 참조 연산자를 사용하는 문장입니다. 포인터 pa가 a를 가리키므로 *pa에 10을 대입하면 결국 a에 10을 대입하는 것과 같습니다. 그렇기 때문에 *pa를 출력하면 a값이 출력되고, a를 출력한 결과와 같음을 확인할 수 있습니다.
*pa는 변수 a의 쓰임과 마찬가지로 대입 연산자의 왼쪽에 올 때는 pa가 가리키는 변수의 저장 공간으로 사용되고, 오른쪽에 올 때는 pa가 가리키는 변수의 값으로 사용됩니다. 물론 연산하거나 출력할 때도 값을 사용합니다. 변수 a와 간접 참조 연산식 *pa가 사용되는 예는 마지막에 정리해두었습니다.
입력할 때는 생각해볼 문제가 있습니다. scanf 함수는 입력할 변수가 메모리 어디에 할당되었는지 저장 공간의 위치를 알아야 합니다. 따라서 입력할 변수의 주소를 인수로 줍니다. 포인터 pa를 통해 변수 a에 입력할 때도 마찬가지입니다. *pa는 a와 같으므로 &a는 &*pa 와 같습니다. 즉, 간접 참조 연산자로 pa가 가리키는 변수를 구하고 다시 주소 연산자로 주소를 구합니다. 그런데 pa가 a의 주소를 저장하고 있으므로 바로 pa를 사용해도 됩니다.
scanf("%d", &a); // &a로 변수 a의 저장공간 찾기
scanf("%d", pa); // 포인터 pa 값은 &a
여러 가지 포인터 사용해보기
포인터가 어떤 변수를 가리키게 되면 그 이후에는 간접 참조 연산자를 통해 가리키는 변수를 자유롭게 쓸 수 있습니다. 예제를 통해 다양한 포인터의 사용법을 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
int a = 10, b = 15, total; //변수 선언과 포기화
double avg; //평균을 저장할 변수
int *pa, *pb; //포인터 동시 선언
int *pt = &total; //포인터 선언과 초기화
double *pg = &avg; //double형 포인터 선언과 초기화
pa = &a; //포인터 pa에 변수 a의 주소 대입
pb = &b; //포인터 pb에 변수 b의 주소 대입
*pt = *pa + *pb; //a 값과 b값을 더해 total에 저장
*pg = *pt / 2.0; //total 값을 2로 나눈 값을 avg에 저장
printf("두 정수의 값 : %d, %d\n", *pa, *pb);
printf("두 정수의 합 : %d\n", *pt);
printf("두 정수의 평균 : %.1lf\n", *pg);
return 0;
}
먼저 7행에서 2개의 포인터를 선언합니다. 가리키는 변수의 형이 같은 경우에는 포인터를 연속으로 선언할 수 있습니다. 즉, pa와 pb가 모두 int형 변수의 주소를 저장하는 포인터라면 콤마를 사용하여 한 번에 선언할 수 있습니다. 단, 각 변수가 포인터임을 뜻하는 기호 *는 변수마다 붙여야 합니다. (만약 *를 붙이지 않는다면 포인터가 아닌 일반 변수로 선언됩니다. 8, 9행과 같이 포인터의 선언과 동시에 초기화하는 것도 가능합니다. 예제에는 total 변수의 주소를 초깃값으로 대입했습니다. 포인터는 가리키는 변수의 자료형과 동일하게 선언합니다. 9행의 포인터 pg는 6행에 선언된 변수 avg의 주소를 저장하므로 가리키는 자료형은 avg와 동일한 double을 사용합니다. 11, 12행은 7행에서 선언한 포인터에 각각 a와 b의 주소를 저장합니다. 만약 변수 a, b, total, avg의 메모리 시작 주소 값이 각각 100, 200, 300, 400 번지라면 pa, pb, pt, pg도 각각 100, 200, 300, 400의 주소를 갖게 됩니다. 이후부터는 포인터에 간접 참조 연산자를 사용하여 가리키는 변수를 사용할 수 있습니다. 14행의 *pt는 포인터 pt가 가리키는 변수 total 이 되며, *pa 는 a, *pb는 b가 됩니다. 따라서 *pt = *pa + *pb; 는 변수 a와 b의 값을 더해 total에 저장합니다.
그 이후의 문장들도 모두 같은 방식으로 사용됩니다. 결국 일반 변수를 a, b, total, avg와 같은 이름으로도 사용할 수 있고, 그 변수들을 가리키는 포인터를 간접 참조해도 사용할 수 있음을 보여줍니다.
const를 사용한 포인터
const 예약어를 포인터에 사용하면 이는 가리키는 변수의 값을 바꿀 수 없다는 의미로, 변수에 사용하는 것과는 다른 의미를 가집니다.
#include <stdio.h>
int main(void)
{
int a = 10, b = 20;
const int *pa = &a;
printf("변수 a의 값 : &d\n", *pa);
pa = &b;
printf("변수 b의 값 : &d\n", *pa);
pa = &a;
a = 20;
printf("변수 a의 값 : &d\n", *pa);
return 0;
}
6행에서 포인터 pa를 선언할 때 const로 상수화했습니다. 만약 const가 일반 변수처럼 포인터 값을 고정시킨다면 9행에서 pa는다른 변수의 주소를 저장할 수 없습니다. 그러나 출력 결과에서 pa는 const의 사용과는 무관하게 변수 b의 주소를 저장하고 그 값을 간접 참조하여 출력하고 있습니다.
그렇다면 포인터에 사용된 const의 의미는 무엇일까요? 바로 pa가 가리키는 변수 a는 pa를 간접 참조하여 바꿀 수 없다는 것 입니다. 만약 12행에서 *pa = 20;과 같이 pa를 통해 a 값을 바꾸고자 한다면 에러 메시지가 뜨게 됩니다.
마무리
포인터는 메모리를 사용하는 또 다른 방법이다.
주소 연산자 &로 변수가 할당된 메모리의 위치를 확인한다.
포인터로 가리키는 변수를 사요할 때 간접 찹조 연산자 *를 쓴다.
'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.02 |