Computer >> 컴퓨터 >  >> 프로그래밍 >> C 프로그래밍

C 언어 포인터 완벽 가이드: 다양한 포인터 연산과 주요 문제점

포인터란 무엇일까요?

포인터(pointer)는 다른 변수의 메모리 주소를 값으로 저장하는 변수입니다. 즉, 포인터는 특정 변수가 위치한 메모리의 직접적인 주소를 담고 있습니다. 일반 변수나 상수와 마찬가지로, 포인터도 사용하기 전에 반드시 선언해야 합니다.

다음과 같은 변수 선언문이 있다고 가정해 보겠습니다.

int qty = 179;

이 변수가 메모리에 저장되는 모습은 다음과 같습니다.

C 언어 포인터 완벽 가이드: 다양한 포인터 연산과 주요 문제점

포인터 선언 방법

int *p;

이 선언은 'p'가 정수형 변수의 주소를 저장하는 포인터 변수임을 의미합니다.

주소 연산자(&)로 포인터 초기화하기

포인터 변수는 주소 연산자(&)를 사용하여 초기화합니다.

int qty = 175;
int *p;
p = &qty;

C 언어 포인터 완벽 가이드: 다양한 포인터 연산과 주요 문제점

간접 참조 연산자(*)로 값 접근하기

포인터가 가리키는 변수의 값을 읽으려면 간접 참조 연산자(indirection operator, *)를 사용합니다.

C 언어 포인터 완벽 가이드: 다양한 포인터 연산과 주요 문제점

'*'는 "주소에 저장된 값(value at address)"이라고 생각하면 이해하기 쉽습니다. 따라서 아래 두 문장은 다음과 같은 의미를 가집니다.

p = &qty;   /* p에 qty의 주소를 대입 */
n = *p;     /* n = qty 와 동일한 결과 */

C 언어의 다양한 포인터 연산

C 언어에서 사용할 수 있는 주요 포인터 연산은 다음과 같습니다.

  • 대입(Assignment) – 주소 연산자(&)를 사용하여 포인터에 주소를 대입할 수 있습니다.

  • 값 찾기(Value Finding) – 역참조(dereferencing)라고도 하며, * 연산자를 통해 포인터가 가리키는 위치에 저장된 값을 얻습니다.

  • 포인터 자신의 주소 얻기 – 포인터 변수도 일반 변수처럼 자신만의 주소와 값을 가집니다. 주소 연산자를 활용하면 포인터 자체가 어디에 저장되어 있는지 알 수 있습니다.

  • 포인터에 정수 더하기 – + 연산자로 포인터에 정수를 더하거나, 정수에 포인터를 더할 수 있습니다. 두 경우 모두 정수는 포인터가 가리키는 자료형의 바이트 크기만큼 곱해진 후 원래 주소에 더해집니다.

  • 포인터 증가(Increment) – 포인터를 증가시키면 배열의 다음 요소로 이동하게 됩니다.

  • 포인터에서 정수 빼기 – – 연산자로 포인터에서 정수를 뺍니다. 정수는 가리키는 자료형의 바이트 크기만큼 곱해진 후 원래 주소에서 빼집니다.

  • 포인터 감소(Decrement) – 포인터를 감소시키면 이전 위치를 가리키게 됩니다. 전위(prefix)와 후위(postfix) 형태 모두 사용할 수 있습니다.

  • 뺄셈(Subtraction) – 두 포인터의 차이를 구할 수 있으며, 주로 배열 내 두 요소가 얼마나 떨어져 있는지 계산하는 데 사용됩니다.

  • 비교(Comparison) – 관계 연산자(==, !=, <, > 등)를 사용하여 두 포인터의 값을 비교할 수 있습니다.

포인터 연산 예제 프로그램

다음은 C 언어에서 포인터 연산이 실제로 어떻게 동작하는지 보여주는 프로그램입니다.

#include <stdio.h>

int main(void) {
    int x, y;
    int *p;              /* 포인터 선언 */

    x = 10;
    p = &x;              /* 포인터에 x의 주소 대입 */
    y = *p;              /* 포인터를 통해 값 읽기 */

    printf("x의 값 = %d\n", x);
    printf("x가 저장된 주소 = %u\n", (unsigned)&x);
    printf("포인터로 읽은 x의 값 = %d\n", *p);
    printf("포인터에 저장된 x의 주소 = %u\n", (unsigned)p);
    printf("y에 복사된 x의 값 = %d\n", y);

    *p = 25;             /* 포인터를 통해 x의 값 변경 */
    printf("변경 후 x의 값 = %d\n", x);

    return 0;
}

실행 결과

위 프로그램을 실행하면 다음과 같은 결과가 출력됩니다.

x의 값 = 10
x가 저장된 주소 = 5000
포인터로 읽은 x의 값 = 10
포인터에 저장된 x의 주소 = 5000
y에 복사된 x의 값 = 10
변경 후 x의 값 = 25

C 언어에서 자주 발생하는 포인터 문제점

포인터는 강력한 도구이지만, 잘못 사용하면 심각한 오류를 일으킬 수 있습니다. 대표적인 문제 상황은 다음과 같습니다.

  • 와일드 포인터(Wild Pointer) – 선언만 하고 주소를 대입하지 않은 채 사용하면 예측할 수 없는 메모리 영역에 접근하게 됩니다.

  • 널 포인터 역참조 – NULL을 가리키는 포인터에 * 연산자를 적용하면 프로그램이 비정상 종료될 수 있습니다.

  • 댕글링 포인터(Dangling Pointer) – 이미 해제된 메모리나 범위를 벗어난 지역 변수를 계속 가리키는 포인터로, 정의되지 않은 동작을 유발합니다.

  • 메모리 누수(Memory Leak) – malloc() 등으로 동적으로 할당한 메모리를 free()로 해제하지 않으면 메모리가 계속 낭비됩니다.

  • 잘못된 포인터 산술 – 유효한 배열 범위를 벗어나는 포인터 연산은 프로그램 충돌이나 데이터 손상으로 이어질 수 있습니다.

이러한 문제를 예방하려면 포인터 선언 시 항상 초기화하고, 사용 전에 NULL 여부를 확인하며, 동적으로 할당한 메모리는 반드시 해제하는 습관을 들이는 것이 좋습니다.

마무리

포인터는 C 언어의 핵심 개념으로, 메모리 주소를 직접 다룰 수 있게 해주는 강력한 도구입니다. 대입, 역참조, 증감, 뺄셈, 비교 등의 연산을 정확히 이해하고 발생 가능한 문제점을 미리 인지한다면, 안전하고 효율적인 C 프로그래밍이 가능합니다.