학습목표 : 포인터를 완전히 이해하지 못하면 C언어를 사용할 줄 안다고 할 수 없을 정도로 포인터는 매우 중요합니다. 한편 많은 사람들이 숨을 헐떡이며 넘어가는 구간이기도 합니다. 이 글을 통해 포인터의 필요성은 물론 포인터 필요성과 다양한 활용 방법을 알아보겠습니다.
포인터는 주소를 저장하는 일정한 크기의 메모리 공간입니다. 따라서 언제든지 다른 주소를 저장하거나 포인터끼리 대입할 수 있습니다. 그러나, 일반 변수와는 달리 대입 연산에 엄격한 기준이 적용됩니다. 이런 특징을 이해하는 것은 포인터를 더 잘 활용하는 데 도움을 줍니다.
주소와 포인터의 차이
주소는 변수에 할당된 메모리 저장 공간의 시작 주소 값 자체이고, 포인터는 그 값을 저장하는 또 다른 메모리 공간입니다. 따라서 특정 변수의 주소 값은 바뀌지 않지만 포인터는 다른 주소를 대입하여 그 값을 바꿀 수 있습니다.
예를 들어 다음 코드가 실행될 때 변수 a,b가 메모리에 할당된 상태를 알아봅시다.
#include <stdio.h>
int main(void) {
int a, b;
int *p;
p = &a;
p = &b;
}
이때 변수 a의 주소는 100이고, b의 주소는 200으로 프로그램 실행 중에는 그 값이 바뀌지 않습니다. 그러나 포인터 p는 a, b 중 어떤 주소를 대입하느냐에 따라 가리키는 변수가 바뀝니다. 이 말은 한마디로 주소는 상수 이고, 포인터는 변수라는 것이죠. 따라서 두 포인터가 같은 주소를 저장하는 일, 즉 하나의 변수를 동시에 가리키는 일도 가능합니다.
int a; // 일반 변수 선언
int *pa, *pb; // 가리키는 자료형 (여기서는 int형)이 같은 두 포인터
pa = pb = &a; // pa와 pb에 모두 a의 주소를 저장한다
*pa = 10; // pa가 가리키는 변수 a에 10 대입
printf("%d". *pb); // pb가 가리키는 변수 a 값 10 출력
이 경우 a값을 바꾸거나 연산하는 데 pa와 pb를 모두 사용할 수 있습니다. 주소도 포인터처럼 간접 참조 연산자를 쓸 수 있지만 상수이므로 대입 연산자 왼쪽에 올 수 없습니다. 즉, 상수와 변수는 용도가 분명히 다르므로 주소와 포인터는 서로 구분하여 이해하는 것이 좋습니다.
주소와 포인터의 크기
포인터도 저장 공간이므로 그 크기가 있습니다. 포인터의 크기는 저장할 주소의 크기에 따라 결정되는데 크기가 클수록 더 넓은 범위의 메모리를 사용할 수 있습니다. 포인터의 크기는 컴파일러에 따라 다를 수 있으나 모든 주소와 포인터는 가리키는 자료형과 상관 없이 그 크기가 같다는 것에는 변함이 없습니다. 주소와 포인터의 크기는 sizeof연산자로 확인할 수 있습니다.
#include <stdio.h>
int main(void){
char ch;
int in;
double db;
char *pc = &ch;
int *pi = ∈
double *pd = &db;
printf("char형 변수의 주소 크기 : %d\n", sizeof(&ch));
printf("int형 변수의 주소 크기 : %d\n", sizeof(&in));
printf("double형 변수의 주소 크기 : %d\n", sizeof(&pd));
printf("char * 포인터의 크기 : %d\n", sizeof(pc));
printf("int * 포인터의 크기 : %d\n", sizeof(pi));
printf("double * 포인터의 크기 : %d\n", sizeof(pd));
printf("char * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pc));
printf("int * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pi));
printf("double * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pd));
return 0;
}
포인터의 대입 규칙
포인터는 크기가 모두 같으므로 대입 연산을 쉽게 생각할 수 있습니다. 그러나 다음 규칙에 따라 제한적으로 사용해야 합니다.
1. 포인터는 가리키는 변수의 형태가 같을 때만 대입해야 합니다.
포인터끼리 대입 연산을 수행하면 여러 개의 포인터로 같은 데이터를 다루는 것이 가능합니다. 그러나 규칙을 지키지 않는 대입 연산은 그 결과를 예상할 수 없습니다. 예제를 통해 확인해보겠습니다.
#include <stdio.h>
int main(void){
int a = 10; //변수 선언과 초기화
int *p = &a; //포인터 선언과 동시에 a를 가리키도록 초기화
double *pd; //double형 변수를 가리키는 포인터
pd = p; //포인터 p 값을 포인터 pd에 대입
printf("%lf\n", *pd); //pd가 가리키는 변수의 값 출력
return 0;
}
<실행결과 없음>
5행의 변수 p와 6행의 pd는 모두 포인터지만 가리키는 자료형이 다릅니다. 즉, 컴파일러는 p에 저장된 값을 int형 변수의 주소로 생각하고, pd에 저장된 값을 double형 변수의 주소로 생각합니다. 따라서 pd에 p를 대입한 후에 간접 참조 연산을 수행하면 변수 a에 할당된 영역 이후의 할당되지 않은 영역까지 사용하게 됩니다.
예를 들어 변수 a가 메모리 100번지부터 할당되고 5~8행을 실행한다고 생각해봅시다. 여기서 pd를 통해 간점 참조 연산을 수행하면 메모리 100번지부터 107번지까지 8바이트를 하나의 double형 변수로 생각하고, 그 안에 있는 값을 실수 값으로 해석하므로 알 수 없는 결과를 출력합니다. 특히 104번지부터 107번지까지가 다른 변수에 할당되어 이미 사용되고 있는 경우 *pd를 대입 연산자 왼쪽에 사용하면 그 변수의 값이 바뀌게 됩니다.
2. 형 변환을 사용한 포인터의 대입은 언제나 가능합니다.
포인터가 가리키는 자료형이 다른 경우라도 형 변환 연산자를 사용하면 경고 메시지 없이 대입할 수 있습니다. 물론 대입한 후에 ㅗ인터를 통한 사용에 문제가 없어야 합니다.
double a = 3.4;
double *pd = &a;
int *pi;
pi = (int *)pd;
여기서 pi에 간접 참조 연산을 수행하면 변수 a의 일부를 int형 변수처럼 사용할 수 있습니다. 이런 사용 방법은 포인터로 메모리를 직접 쪼개 쓰는 것이므로 데이터가 메모리에 저장되는 방식을 충분히 이해하고 있어야 합니다. 만약 *pi = 10; 과 같이 a의 일부분에 정수를 저장하면 정수와 실수의 데이터 크기와 저장 방식이 다르므로 a에 저장한 실수 값은 사용할 수 없습니다.
포인터를 사용하는 이유
변수를 사용하는 가장 쉬운 방법은 이름을 쓰는 겁니다. 포인터를 사용하려면 추가적인 변수 선언이 필요하고 주소 연산, 간접 참조 연산 등 각종 연산을 수행해야 합니다. 그러니 포인터를 일부러 즐겨 사용할 필요는 없습니다. 그러나 임베디드 프로그래밍을 할 때 메모리에 직접 접근하는 경우나 동적 할당한 메모리를 사용하는 경우에는 포인터가 반드시 필요합니다.
(임베디드 프로그래밍은 임베디드 시스템(내장형 시스템)을 제어하기 위한 프로그램으로, 오늘날 만드는 거의 모든 생활 기기에서 특정 기능을 제어하기 위해 구현됩니다. 예를 들어 정수기에서 정수, 냉수 등을 구분해 물이 나오게 한다던가, 자동차, 냉장고, 전기밥솥, 스마트워치 등에도 각 기능을 담당하는 하드웨어가 있는데, 이 하드웨어를 제어하는 소프트웨어를 만드는 일을 임베디드 프로그래밍이라고 합니다.)
일단 두 변수의 값을 바꾸는 함수를 통해 포인터의 필요성을 확인해보겠습니다.
#include <stdio.h>
void swap(int *pa, int *pb); // 두 변수의 값을 바꾸는 함수의 선언
int main(void)
{
int a = 10, b = 20; // 변수 선언과 초기화
swap(&a, &b); // a,b의 주소를 인수로 주고 함수 호출
printf("a:%d, b:%d\n", a, b);
return 0;
}
void swap (int *pa, int *pb) // 매개변수로 포인터 선언
{
int temp; // 교환을 위한 임시 변수
temp = *pa; // temp에 pa가 가리키는 변수의 값 저장
*pa = *pb; // pa가 가리키는 변수에 pb가 가리키는 변수의 값 저장
*pb = temp; // pb가 가리키는 변수에 temp 값 저장
}
이 예제는 두 변수의 값을 swap 함수 호출을 통해 바꿉니다. 먼저 9행의 함수 호출에서 바꿀 변수 a와 b의 주소를 인수로 줍니다. 따라서 15행의 매개변수는 포인터로 선언합니다. 함수가 호출되면 포인터 pa, pb는 main 함수의 변수 a와 b의 주소를 저장하므로 각각 a와 b를 가리키는 상태가 됩니다. 이제 swap 함수에서 포인터 pa, pb에 간접 참조 연산을 수행하면 main함수의 변수 a와 b를 자유롭게 사용할 수 있습니다. 17행의 temp 변수는 main 함수의 a, b를 바꾸는데 사용할 변수로 a, b와 같은 int 형으로 선언합니다. 19~21행이 두 변수의 값을 바꾸는 과정이며, 각 문장이 실행될 때마다 각 함수에 있는 변수으 값은 다음처럼 변합니다. 교환 작업은 swap 함수 안에서 포인터를 통해 진행되지만 실제로 바뀌는 값은 main함수의 변수 a와 b가 됩니다. 결국 swap 함수는 포인터를 통해 main 함수의 변수 a, b를 공유하므로 두 변수를 직접 바꾸는 일이 가능해집니다. 이제 두 변수의 값을 바꾸고 싶을 때는 언제든지 swap 함수를 호출하면 됩니다.
마무리
- 주소와 포인터는 상수와 변수의 차이가 있다.
- 포인터의 크기는 주소의 크기와 같다.
- 포인터에 주소를 저장할 때는 가리키는 자료형이 같아야 한다.
- 포인터의 주요 기능 중 하나는 함수 간에 효과적으로 데이터를 공유하는 것이다.
'C언어' 카테고리의 다른 글
혼공C 도전실전 예제 내 풀이 (0) | 2023.08.17 |
---|---|
[C언어] 배열을 처리하는 함수 (0) | 2023.08.15 |
[C언어] 배열과 포인터 (0) | 2023.08.13 |
[C언어] 포인터 (0) | 2023.08.03 |
[C언어] 포인터 (0) | 2023.08.02 |