cheoly's language study blog

C 언어 포인터 완벽 정리 – 이 글 하나로 끝내기

C언어
반응형
SMALL

파이썬만 쓰다가 다시 C 언어를 보면 제일 먼저 막히는 게 바로 포인터(pointer)다.
개념만 애매하게 잡고 넘어가면, 나중에 구조체·동적 메모리·파일 입출력·함수 포인터에서 계속 발목을 잡는다.

이 글에서는

  • 포인터가 메모리에서 어떻게 동작하는지
  • *, &, 배열, malloc, NULL과의 관계
  • 자주 터지는 실수와 디버깅 포인트

까지 한 번에 정리해본다.

C 언어 포인터를 설명하는 일러스트로, 큰 C 로고와 코드 창에 'int p' 문구가 표시되어 있으며 포인터 구조를 나타내는 다이어그램이 포함된 그래픽


1. 포인터란 무엇인가?

포인터 = “값” 대신 “주소”를 저장하는 변수

일반 변수는 값(value) 을 저장하고,
포인터 변수는 값이 저장된 메모리 주소(address) 를 저장한다.

int x = 10;      // 값 10을 저장하는 int 변수
int *p = &x;     // x가 저장된 "주소"를 저장하는 포인터 변수
  • &x : 변수 x주소
  • p : 그 주소를 저장하는 포인터 변수
  • *p : 포인터 p가 가리키는 주소에 있는

즉,

  • x → 값: 10
  • &x → 주소: 예) 0x1000
  • p → 주소: 0x1000
  • *p → 값: 10

2. 포인터 선언과 기본 문법

2-1. 기본 선언

int *p;      // int 를 가리키는 포인터
char *cp;    // char 를 가리키는 포인터
double *dp;  // double 을 가리키는 포인터
  • *“포인터 타입이다” 라는 의미
  • int *p, q; 처럼 쓰면 p만 포인터고, q는 그냥 int 이다 → 조심!
int* p, q;   // 혼동의 시작: p는 int*, q는 int
int *p, *q;  // 둘 다 포인터

2-2. 포인터에 주소 대입

int x = 42;
int *p = &x;   // x의 주소를 p에 저장
  • 항상 같은 타입끼리 맞춰야 한다.
  • int* 포인터에는 int 변수의 주소만 넣는다.

3. &* 정확히 이해하기

3-1. & 연산자: 주소 연산자

int x = 10;
printf("%p\n", (void*)&x);  // x가 저장된 주소 출력
  • &x : 변수 x메모리에 있는 위치

3-2. * 연산자: 간접 참조(역참조)

int x = 10;
int *p = &x;

printf("%d\n", *p);  // 10
*p = 20;             // x의 값을 20으로 변경
printf("%d\n", x);   // 20
  • p는 “주소”를 저장
  • *p는 그 주소에 있는 “값”에 접근

✔ 포인터를 사용할 때 항상 “지금 이게 주소인지, 인지”를 구분해서 생각하면 헷갈림이 많이 줄어든다.


4. 포인터와 배열의 관계

C에서 배열 이름은 상수 포인터 비슷하게 동작한다.

int arr[3] = {10, 20, 30};

printf("%p\n", (void*)arr);     // arr[0]의 주소
printf("%p\n", (void*)&arr[0]); // 동일한 주소

4-1. 배열 → 포인터 변환

int *p = arr;    // 사실은 &arr[0] 와 같은 의미
printf("%d\n", p[1]);    // 20
printf("%d\n", *(p + 1)); // 20
  • arr[i]*(arr + i)
  • p[i]*(p + i)

4-2. 하지만 배열과 포인터는 완전히 같지는 않다

int arr[3] = {1, 2, 3};
int *p = arr;

sizeof(arr); // 3 * sizeof(int) => 배열 전체 크기
sizeof(p);   // 포인터 변수 크기 (보통 4 또는 8바이트)
  • 배열: 메모리에 연속된 공간 + 크기가 컴파일 시에 결정
  • 포인터: 그 공간을 가리키는 주소만 저장

5. 포인터 연산(pointer arithmetic)

포인터에 +1을 하면 주소가 1 증가하는 게 아니라
타입 크기만큼 이동한다.

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30
  • 만약 sizeof(int) == 4라면,
    p + 1 은 실제 주소로 +4 만큼 이동한 것이다.

✔ 그래서 포인터 연산은 배열 순회에 매우 유용하다.


6. NULL 포인터와 초기화

포인터는 사용 전에 반드시 초기화해야 한다.
초기화되지 않은 포인터는 “쓰레기 주소”를 가리키며,
접근 시 바로 세그멘테이션 폴트(segmentation fault) 로 이어질 수 있다.

int *p = NULL;   // 초기화
if (p == NULL) {
    // 아직 유효한 주소가 없음
}
  • NULL 은 “아무 것도 가리키지 않는다”는 의미의 특별한 주소 값
  • 동적 할당 실패 시에도 보통 NULL을 반환한다.

7. 동적 메모리와 포인터 (malloc, free)

힙(heap) 영역에 메모리를 할당할 때 포인터가 필수로 등장한다.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int) * 3);  // int 3개 공간 할당

    if (p == NULL) {
        // 메모리 할당 실패
        return 1;
    }

    p[0] = 10;
    p[1] = 20;
    p[2] = 30;

    printf("%d %d %d\n", p[0], p[1], p[2]);

    free(p);   // 반드시 해제!
    return 0;
}
  • malloc : 메모리를 할당하고 그 첫 번째 바이트의 주소를 반환
  • free : 더 이상 사용하지 않을 때 반드시 해제해야 함
  • malloc 으로 받은 주소를 잃어버리면 → 메모리 누수(memory leak)

8. 이중 포인터(double pointer) 맛보기

이중 포인터는 “포인터를 가리키는 포인터”다.

int x = 10;
int *p = &x;   // x를 가리키는 포인터
int **pp = &p; // p를 가리키는 포인터

printf("%d\n", **pp);   // 10

언제 쓰냐?

  • 포인터를 함수 인자로 넘겨서 그 포인터 자체를 변경하고 싶을 때
  • 2차원 배열처럼 데이터를 관리할 때
  • 동적 할당된 배열의 배열을 다룰 때

이건 글이 길어지니, 이중 포인터만 따로 글 하나를 파도 될 정도다.


9. 함수 인자와 포인터: call by value vs call by reference 흉내

C는 무조건 call by value지만, 포인터를 사용해서
마치 “참조 호출”처럼 동작하게 만들 수 있다.

#include <stdio.h>

void add_one(int *p) {
    (*p)++;
}

int main(void) {
    int x = 10;
    add_one(&x);    // x의 주소를 넘김
    printf("%d\n", x);  // 11
    return 0;
}
  • 함수는 p라는 포인터의 복사본을 받지만
  • 그 포인터가 가리키는 원래 변수 x를 직접 수정할 수 있다.

파이썬의 “mutable 객체를 함수에서 수정하는 느낌”과 비슷하다고 보면 이해가 편하다.


10. 포인터에서 자주 하는 실수들

10-1. 초기화 안 된 포인터 사용

int *p;       // 초기화 안 됨 (쓰레기 값)
*p = 10;      // 세그폴트 가능성 100%

→ 반드시 NULL 또는 유효한 주소로 초기화.


10-2. 해제 후 포인터를 계속 사용 (use-after-free)

int *p = malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p);   // 정의되지 않은 동작(UB)

free(p); p = NULL; 패턴을 습관처럼 넣는 게 좋다.


10-3. 배열 범위 밖 접근

int arr[3] = {1, 2, 3};
int *p = arr;

printf("%d\n", p[3]);  // 쓰지 말 것! 범위 밖 접근

→ C는 범위 체크를 안 해준다.
디버깅 어려운 버그의 주요 원인.


10-4. 잘못된 캐스팅

int x = 10;
double *dp = (double*)&x;  // 타입이 전혀 다른 포인터

이런 코드는 가능하면 쓰지 않는 게 좋다.
메모리 레이아웃을 완전히 이해하고 쓸 필요가 있을 때만, 조심해서 사용.


11. 포인터를 진짜 이해했는지 스스로 체크해보기

아래 질문들에 스스로 답해볼 수 있다면,
포인터는 “완벽까지는 아니어도 꽤 잘 이해한 상태”다.

  1. int *pint (*p)[3] 의 차이를 설명할 수 있는가?
  2. arr, &arr[0], &arr 의 차이를 말할 수 있는가?
  3. malloc으로 2차원 배열을 만드는 코드를 직접 짤 수 있는가?
  4. NULL 포인터와 초기화되지 않은 포인터의 차이를 구분하는가?
  5. 함수 인자로 포인터를 넘겨서, 원래 변수 값을 바꾸는 코드를 직접 짤 수 있는가?

이 글에서 다룬 것은 그 중에서도 가장 기초이자 필수적인 부분이다.
다음 글에서는 이 내용을 바탕으로

  • 이중 포인터로 2차원 배열 다루기
  • 함수 포인터와 콜백 구조

를 이어서 정리해볼 예정이다.

 

 

반응형
LIST

C언어 포인터, 동적할당, 메모리 관리

C언어
반응형
SMALL

오늘은 C언어 포인터에 대해 알아보겠습니다. 포인터란 무엇일까요? 포인터는 메모리의 주소를 저장하는 변수로, 메모리 관리를 효율적으로 할 수 있게 도와줍니다. 특히, 동적 메모리 할당을 통해 프로그램의 메모리 사용을 최적화할 수 있습니다.

 

C언어 포인터의 개념

C언어에서 포인터는 변수의 주소를 가리키는 변수입니다. 포인터를 사용하면 메모리의 특정 위치에 직접 접근할 수 있어, 데이터 구조를 효율적으로 관리할 수 있습니다. 포인터는 메모리의 주소를 저장하므로, 메모리의 내용을 직접 수정하거나 읽을 수 있는 강력한 도구입니다.


메모리 관리의 중요성

메모리 관리는 프로그램의 성능과 안정성에 큰 영향을 미칩니다. 메모리를 효율적으로 관리하지 않으면, 프로그램이 비정상적으로 종료되거나, 메모리 누수와 같은 문제가 발생할 수 있습니다. 따라서, C언어에서는 malloc과 free 함수를 사용하여 동적 메모리를 할당하고 해제하는 것이 중요합니다.


malloc 함수의 사용법

malloc 함수는 메모리를 동적으로 할당하는 데 사용됩니다. 이 함수는 요청한 크기만큼의 메모리를 할당하고, 그 시작 주소를 반환합니다. 예를 들어, 다음과 같은 코드로 메모리를 할당할 수 있습니다.


int *arr;
arr = (int *)malloc(sizeof(int) * N);

위 코드에서 N은 배열의 크기를 나타내며, malloc 함수는 N개의 정수를 저장할 수 있는 메모리를 할당합니다. 메모리 할당이 성공하면, arr 포인터는 할당된 메모리의 시작 주소를 가리키게 됩니다.

 

free 함수의 사용법

할당한 메모리는 사용이 끝난 후 반드시 해제해야 합니다. 이를 위해 free 함수를 사용합니다. free 함수는 포인터가 가리키는 메모리를 해제하고, 해당 메모리를 다시 사용할 수 있도록 합니다. 예를 들어, 다음과 같이 사용할 수 있습니다.

c
free(arr);
arr = NULL; // 포인터를 NULL로 초기화

이렇게 하면 메모리 해제가 완료되고, 포인터를 NULL로 초기화하여 잘못된 메모리 접근을 방지할 수 있습니다.

메모리 해제를 하지 않았을 때의 문제
메모리 해제를 하지 않으면 메모리 누수가 발생할 수 있습니다. 메모리 누수란, 프로그램이 사용한 메모리를 해제하지 않아 점점 메모리가 부족해지는 현상입니다. 이로 인해 프로그램이 느려지거나, 심지어 시스템이 다운될 수도 있습니다.

 

메모리 누수 방지 방법

메모리 누수를 방지하기 위해서는 다음과 같은 방법을 사용할 수 있습니다.

동적 메모리 할당 후 즉시 해제하기
메모리 해제를 잊지 않기
메모리 사용이 끝난 후 포인터를 NULL로 초기화하기
이러한 방법을 통해 메모리 누수를 예방할 수 있습니다.

실제 코드 예제
아래는 학생의 점수를 입력받아 평균을 계산하는 간단한 프로그램입니다. 이 프로그램에서는 동적 메모리를 사용하여 점수를 저장합니다.

c
#include <stdio.h>
int main()

위 코드는 학생 수를 입력받고, 각 학생의 점수를 입력받아 총 점수와 평균 점수를 출력합니다.

 

마무리 및 추가 자료


C언어에서 포인터와 메모리 관리는 매우 중요한 개념입니다. malloc과 free를 적절히 사용하여 메모리를 관리하면, 프로그램의 성능을 높이고 안정성을 유지할 수 있습니다. 더 자세한 내용은 아래의 링크를 참고해 보세요.
https://dafher-diary.tistory.com/16

 

POCU C언어 정주행 14회차 - malloc과 free, 메모리 함수, 메모리 관리 기법

1. malloc과 free 이번 글은 강의에서 동적 할당에 대해 다룬 것들 중에서 내가 몰랐던 것들을 위주로 적어보려고 한다. 따라서 malloc, calloc, free함수의 사용법이나 어떤 역할을 하는지에 대한 내용은

dafher-diary.tistory.com

https://nunbu.tistory.com/157

 

[C언어] C언어 개발시 메모리 누수 오류의 원인과 방지 방법

[C언어] C언어 개발시 메모리 누수 오류의 원인과 방지 방법 C언어로 리눅스 코어뱅킹 은행 시스템을 개발하다 보면 메모리 누수 오류는 흔하게 발생하는 문제 중 하나입니다. 메모리 누수 오류

nunbu.tistory.com

 

https://velog.io/@mjung/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9malloc%EA%B3%BC-%ED%95%A0%EB%8B%B9%ED%95%B4%EC%A0%9Cfree-%EC%A0%95%EB%A6%AC

 

동적 메모리 할당(malloc)과 할당해제(free) 정리

ft_lstdelone 함수 main 부분을 짜던 중 자꾸 버그가 난다.해결을 위해 이것저것 만져보던 도중, 지금 직면한 문제와 시도하는 방법들이 정확히 모른 채 고치기에만 급급해 있다는 것을 느꼈다.현재

velog.io

 

이제 C언어의 포인터와 메모리 관리에 대해 좀 더 이해가 깊어지셨길 바랍니다. 포인터를 잘 활용하여 효율적인 프로그래밍을 해보세요!

반응형
LIST