🖥️ Dev/C

[C언어] 포인터의 기본 개념

청월누리 2025. 4. 4. 01:41

C언어의 핵심이자 가장 중요한 개념 중 하나인 포인터의 기본 개념에 대해 정리한 글입니다.


변수와 메모리, 그리고 주소의 개념

변수와 메모리

변수, 메모리, 주소의 관계

변수 (Variable)

  • 특정 데이터를 저장해두는 이름 (label)
    • ex) int a = 10 ➡️ 변수 이름 = a | 값 = 10
  • 모든 변수들은 주소가 존재 ➡️ 변수 이름은 개발자(사람)을 위한 것이라면 주소는 컴퓨터를 위한 값

메모리 (Memory)

  • 프로그램에서 사용하는 모든 변수들이 저장되는 위치 ➡️ 컴퓨터 메모리(램, RAM) 어딘가에 실제로 저장되어 있음
  • 변수가 선언되면, 해당 변수는 메모리 어딘가에 자료형의 크기만큼 공간을 차지하고 값을 저장하고 있음

주소 (Address)

  • 메모리는 매우 많은 칸들로 구성되어 있고, 각 칸마다 고유의 변호가 있으며 이를 주소(address) 혹은 메모리 주소라고 함
  • C언어에서는 어떤 변수에 할당된 메모리 위치를 주소값으로 표현할 수 있으며, &(앰퍼샌드)를 붙여 표현 (변수의 메모리 주소 = 변수가 저장된 메모리의 시작 주소)
    • ex) int a = 10 ➡️ 변수 a의 메모리 주소 = &a
  • OS는 메모리 주소를 byte (1 byte = 8 bit) 단위로 관리
  • 주소는 일반적으로 16진수 형태(ex. 0x005FFEC4)로 표현되며, 시스템이나 컴파일러에 따라 달라질 수 있고, 실행 때마다 바뀌는 경우가 많음

주소값 관리

  • 운영체제와 컴파일러가 프로그램이 시작될 때, 변수들이 사용할 메모리 영역을 배정 ➡️ C언어의 코드 레벨에서는 개발자가 구체적으로 메모리 주소를 결정하지 않음
  • & 연산자를 통해 변수에 할당된 실제 메모리 주소를 가져와 사용할 수 있음

포인터의 개본 개념

포인터의 개요

포인터 (pointer)

  • 포인터는 "메모리 주소를 저장하는 변수"
    • 포인터 = 포인터 변수 = 메모리 주소를 보관하는 변수
  • 포인터 자체도 변수이기 때문에 메모리에 저장되지만, 변수에 담긴 값이 일반적인 데이터 혹은 값이 아니라 "주소"라는 것이 핵심
  • 메모리 주소를 가지고 직접 메모리의 내용에 접근해서 조작 가능 ➡️ 동적 메모리를 할당하거나 연결 리스트와 같은 향상된 자료구조를 생성할 때 사용
포인터를 사용하는 이유
데이터의 복사를 피하고 데이터를 공유하며 작업할 수 있다.
📌 데이터를 복사하지 않는다.➡️ 추가 메모리 공간이 필요하지 않다.
📌 데이터를 공유한다. ➡️ 여러 부분에서 동일한 데이터를 참조한다.

포인터의 선언

#include <stdio.h>

int main() {
  int a = 10;
  int *pa = &a;

  return 0;
}
  • 자료형 뒤에 *을 붙여서 "포인터 변수"임을 표기하고, 변수명을 작성
    • int* pa : 변수 pa가 "int 타입을 가리키는 포인터"라는 선언
    • *의 위치는 표기 스타일에 따라 int* pa 혹은 int *pa 등으로 작성 가능 (의미는 동일)

포인터 선언의 의미

  • 포인터가 주소를 저장할 때, 포인터가 해당 변수(혹은 변수의 메모리 주소)를 "가리킨다"라고 표현 (화살표로 표현)
  • *pa는 포인터 변수인 pa가 가리키고 있는 메모리 주소에 저장된 값을 의미하며, pa는 해당 메모리 주소를 의미
    • 위 코드에서 *pa는 변수 a와 동일하게 취급할 수 있고, pa는 변수 a가 저장된 메모리 주소값을 의미한다.

주소 연산자와 간접 연산자

  • 주소 연산자 (&, address operator) : 변수 앞에 붙여 그 변수의 주소 값을 가져옴 (ex. &a)
  • 간접 연산자 (*, dereference operator)
    • 변수 선언부(ex. int* pa)에서 *은 해당 변수가 포인터라는 것을 의미
    • 표현식(*pa)에서 *은 포인터가 가리키는 대상(값)을 의미
포인터 변수 pa가 있을 때, 변수 pa 자체는 주소를 담고 있고, *pa는 그 주소에 저장된 실제 데이터를 의미한다.

포인터와 메모리 구조

C 프로그램의 메모리 구조

  • 운영체제나 컴파일러 등에 따라 달라질 수 있지만, C 프로그램은 대략적으로 위 그림과 같은 메모리 구조를 가진다.
    • 텍스트(코드) 영역 : 실행할 프로그램의 기계어 코드가 저장되는 영역
    • 데이터 영역 : 전역 변수, 정적(static) 변수 등이 저장되는 영역
    • 힙(heap) 영역 : 동적 메모리 할당(malloc, free 등)을 통해 사용할 수 있는 영역
    • 스택(stack) 영역 : 지역 변수 등이 저장되는 영역으로, 함수가 호출될 때마다 새로운 스택 프레임이 생기고 함수가 끝나면 제거
  • 포인터는 어떤 영역에 있는 변수를 가리키든 "주소"를 담고 있다는 점은 동일
  • 단, 해당 변수가 전역 변수인지, 지역 변수인지, 동적으로 할당한 메모리인지 등에 따라 사용 시 주의할 점이 달라짐

포인터 크기와 자료형

포인터 변수의 크기

  • 포인터의 크기는 해당 시스템 아키텍처에 따라 달라짐
    • 32비트 시스템 : 포인터 사이즈 = 4바이트
    • 64비트 시스템 : 포인터 사이즈 = 8바이트
  • int*, char*, double*동일한 아키텍처에서 포인터 타입의 크기는 모두 동일 (단, 포인터가 가리키는 대상의 타입은 다름)
    • 타입이 다른 포인터들은 내부적으로 "가리키는 대상의 크기"가 다름 ➡️ 이를 이용해 포인터 연산 등을 수행할 때 사용
#include <stdio.h>

int main() {
  printf("int* 의 크기 : %d byte\n", sizeof(int*));
  printf("char* 의 크기 : %d byte\n", sizeof(char*));
  printf("double* 의 크기 : %d byte\n", sizeof(double*));

  printf("\n");

  printf("int 의 크기 : %d byte\n", sizeof(int));
  printf("char 의 크기 : %d byte\n", sizeof(char));
  printf("double 의 크기 : %d byte\n", sizeof(double));

  return 0;
}

포인터 타입의 크기는 모두 동일하고, 포인터가 가리키는 대상의 타입 크기는 다르다.


포인터 사용시 주의할 점

  • 초기화되지 않은 포인터는 위험
    • 포인터를 선언만 하고 주소를 넣어주지 않으면 쓰레기 값(garbage value)이 들어가 있을 수 있음
    • 포인터를 선언하는 즉시 올바른 주소를 대입(int* pa = &a)하거나 NULL로 초기화(int* pa = NULL)해야 함
  • 이미 해제한 메모리를 가리키는 포인터 사용 금지 (dangling pointer)
    • 동적 할당 후, free()가 호출된 메모리 주소를 계속 사용하면 안됨
  • 잘못된 형 변환 주의
    • 포인터는 타입이 매우 중요하기 때문에 가리키는 타입에 맞춰서 사용해야 하며, 필요한 경우에만 올바른 형변환 수행
  • 스택 변수의 주소를 함수 밖에서 사용 주의
    • 지역 변수가 선언된 함수가 끝나면 스택에 있는 그 변수의 메모리는 유효하지 않음

요약 및 정리

  • 포인터는 "메모리 주소를 저장하는 변수"
  • 포인터를 통해 변수의 실제 위치(메모리 주소)에 접근할 수 있으며, 이를 통해 값을 읽거나 쓸 수 있음
  • 주소는 운영체제와 컴파일러가 관리하며, C 코드 레벨에서는 & 연산자를 통해 얻고, 그 값을 pa와 같은 포인터 변수에 저장하여 사용
  • 메모리 관리와 타입에 대한 정확한 이해가 필요