Pointer
개요
포인터(Pointer)는 프로그래밍에서 광범위하게 사용되는 개념으로, 특히 C 언어와 같은 저수준 프로그래밍 언어에서 중요한 역할을 한다. 포인터는 메모리의 주소를 저장하고, 이를 통해 다양한 데이터에 간접적으로 접근할 수 있는 변수다. C 언어에서 포인터는 메모리 관리, 배열과 문자열 처리, 동적 메모리 할당, 데이터 구조체 구현 등 여러 분야에서 핵심적인 도구로 사용된다. 이러한 포인터의 사용은 프로그램의 유연성과 효율성을 높이는 동시에, 복잡하고 섬세한 메모리 관리를 가능하게 한다. 본문에서는 포인터가 C 언어에서 어떻게 동작하며, 왜 그렇게 널리 사용되는지에 대해 자세히 살펴보겠다.
본문
사용 이유
포인터를 사용하면 메모리를 효율적으로 사용할 수 있다. 이는 포인터를 사용하면 큰 데이터 구조체나 배열을 다른 함수에 넘길 때, 실제 데이터의 복사본을 만들 필요 없이 해당 데이터의 주소만 전달할 수 있어 메모리 사용량을 줄이기 때문이다.
#include <stdio.h>
typedef struct
{
int largeArray[1000];
} LargeData;
void processLargeDataByValue(LargeData data)
{
printf("첫 번째 원소: %d\n", data.largeArray[0]);
}
int main()
{
LargeData myData;
myData.largeArray[0] = 123;
printf("구조체 크기: %zu 바이트\n", sizeof(LargeData));
processLargeDataByValue(myData);
return 0;
}
예를 들어, 다음과 같은 구조체를 호출하는 함수가 있다고 가정해 보자. C언어에서 int 자료형의 크기는 4바이트이므로 해당 구조체의 크기는 4000 바이트가 될 것이다. 즉, 함수를 호출하는 과정에서 매개변수를 전달할 때 매개변수 myData의 복사본이 생성되어 전달되므로 함수가 호출될 때마다 4000 바이트 메모리를 사용하게 된다(Call by value).
#include <stdio.h>
typedef struct
{
int largeArray[1000];
} LargeData;
void processLargeData(LargeData* data)
{
printf("첫 번째 원소: %d\n", data->largeArray[0]);
}
int main()
{
LargeData myData;
myData.largeArray[0] = 123;
printf("포인터 크기: %zu 바이트\n", sizeof(LargeData*));
processLargeData(&myData);
return 0;
}
반면 구조체의 주소, 즉 포인터 변수를 매개변수로 사용하면 대상 객체의 실제 데이터 대신 해당 객체의 주소만 전달되므로, 포인터 주소의 크기인 8바이트(64비트 시스템 기준)만 사용하게 되어 메모리를 상당히 절약할 수 있다(Call by Reference).
사용법
포인터를 사용하려면 다음과 같은 절차를 따르면 된다.
1. 포인터 선언: 포인터를 선언하기 위해, 포인터가 가리킬 데이터의 타입을 지정한 후 * 연산자를 사용하면 된다.
int* ptr;
2. 포인터 초기화: 포인터에 특정 변수의 주소를 할당하여 초기화하면 된다. 예를 들어, 위와 같이 int 형 변수 a의 주소를 가리키고 싶으면 & 연산자를 사용하여 해당 변수의 주소를 얻게 하면 된다.
int a = 5;
ptr = &a;
3. 포인터 사용: 만약 포인터가 가리키고 있는 참조 대상의 값을 얻고 싶으면 간접 참조 연산자 *를 사용하면 된다.
printf("value pointed to by ptr: %d\n", *ptr);
여기서 포인터는 "특정 변수의 메모리 주소를 할당"한다고 하였다. 그렇다면 위의 변수 a와 ptr이 어떤 구조로 되어 있을지 생각하며, 각 변수에 주소와 값을 확인해 보자.
#include <stdio.h>
int main()
{
int a = 5;
int* ptr = &a;
printf("value of a: %d\n", a);
printf("address of a: %p\n", &a);
printf("value of ptr: %d\n", ptr);
printf("value of *ptr: %d\n", *ptr);
printf("address of ptr: %p\n", &ptr);
return 0;
}
여기서 주목해야 할 것은, 포인터 변수도 하나의 변수이므로 자체의 주소를 갖는다는 점이다. 즉, ptr이 a를 참조하고 있지만, 스스로의 주소도 갖고 있으므로 사용에 유의해야 한다.
배열과의 관계
우리가 자주 사용하는 배열도 포인터와 밀접한 관계가 있다. 다음과 같이 int 형으로 선언된 배열 ary가 있다고 가정해 보자. 해당 배열의 요소를 각각 인덱싱하여 주소값을 보면 한 가지 특징을 알아낼 수 있다.
#include <stdio.h>
int main()
{
int ary[4] = { 0, };
for (int i = 0; i < 4; ++i)
printf("ary%d: %p\n", i, &ary[i]);
return 0;
}
첫 배열의 주소인 0x000000184ACFFC58부터 다른 원소들의 주소가 자료형의 크기(여기선 4)만큼 증가하는 것을 알 수 있다. 이러한 관찰을 통해 배열의 각 요소가 메모리에서 연속적으로 할당되고 있음을 확인할 수 있다. 이 특성은 포인터를 사용하여 배열에 접근할 때 중요하다.
#include <stdio.h>
int main()
{
int ary[4] = { 0, };
int* ptr = ary;
for (int i = 0; i < 4; ++i)
{
printf("ary%d: %p\n", i, &ary[i]);
printf("ptr%d: %p\n", i, ptr++);
}
return 0;
}
즉, 다음과 같이 ary 배열의 첫 번째 요소를 가리키는 포인터 변수 ptr을 초기화하면, &ary[0] (첫 번째 원소의 시작 주소)와 ary(배열 이름)은 메모리 주소로서 동일한 값을 가진다. 해당 코드에서 배열을 가리키는 포인터 변수를 증가시키는 것으로 배열을 순회할 수 있다는 점 체크해 두자(배열을 이용하여 순회하는 것과 포인터를 이용하여 순회하는 것에는 큰 차이가 없다고 한다. 자세한 내용은 해당 링크를 참조하면 좋을 것 같다).
요약
포인터
1. 정의: 메모리의 주소를 저장하는 변수
2. 특징
a. 메모리를 효율적으로 사용할 수 있음
b. 포인터 변수 자체에도 주소가 존재함
c. * 연산자를 이용해 간접 참조를 하여 참조하고 있는 대상의 값을 불러올 수 있음
d. 배열을 포인터로 참조하면, 포인터 변수는 배열의 첫 번째 주소를 가리킴
3. 사용법
a. 포인터 초기화: 참조할 대상과 동일한 자료형의 포인터 변수 초기화
b. 포인터 사용: 상황에 맞게 * 연산자와 & 연산자를 이용
'Language > C' 카테고리의 다른 글
[C] 구조체(Structure) (2) | 2024.01.31 |
---|---|
[C] 메모리 동적 할당 (0) | 2024.01.29 |