Language/C

[C] 포인터(Pointer)

nowkoes 2024. 1. 22. 15:35

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를 참조하고 있지만, 스스로의 주소도 갖고 있으므로 사용에 유의해야 한다. 


배열과의 관계

출처: w3resource.com/c-programming/c-arrays-and-pointers.php

 

 우리가 자주 사용하는 배열도 포인터와 밀접한 관계가 있다. 다음과 같이 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