크래프톤 정글 8기 - 32일차 TIL (포인터)
TIL - 2025.04.11 (금요일)
📝 오늘 배운 것 (c - 포인터)
포인터란?
포인터는 "주소값을 저장하는 변수"이다.
즉, 어떤 데이터가 메모리 어딘가에 저장되어 있을 텐데, 포인터는 그 "메모리 주소"를 기억하는 변수.
왜 포인터가 필요할까?
그보다 c를 처음 입문할 때 들었던 생각이 "왜 주소라는 것을 직접 다뤄야 할까?"였다. 그도 그럴 것이 파이썬으로 주로 코딩에 입문을 하고는 하는데 파이썬은 그냥 하고 싶은 대로 하면 되었다. 하지만 c를 배우며 포인터를 만나면 그 개념부터 혼동이 된다. 나 또한 지금도 포인터가 혼동된다.
이 어지러운 포인터를 이해를 위해서 그나마 주소로 설명하는 것이 유용하다.
포인터는 집 주소?
한번 생각해보자
- 변수 : 집 안에 살고 있는 사람
- 포인터 : 그 집의 주소
int a = 10;
int *p; // int형 변수를 가리키는 포인터 선언
p = &a; // a의 주소를 p에 저장
printf("%d\n", *p); // *p는 a의 값을 출력 (10)
변수 a라는 집에 10이라는 사람이 살고 있다. *p는 택배 기사이다. 그런데 이 택배 기사 p에게 a라는 집에 택배를 배송하라고 택배가 쥐어졌는데 택배기사의 손에 a가 살고 있는 주소가 적힌 송장 &a 가 주어진 것이다.
만약 당신이 택배 기사이며 a 라는 사람에게 택배를 주기 위해 a라는 사람을 일일이 찾아가는 것보다는 주소를 알고 그 주소에 직접 찾아가 택배를 던져주고 오는 것이 빠르지 않을까?
물론 이런 예시가 포인터에 대한 근본적인 필요성을 대변하지는 않는다.
그래서 왜 필요한데?
1. 메모리에 직접 접근하기 위해
- c 언어를 하드웨어와 매우 가까운 언어 즉 저수준 언어라고 표현하곤 한다. 물론 어셈블리어라는 진짜 저수준의 무언가가 있지만 그 어셈블리어도 c처럼 메모리를 다루어야 한다. 즉 저수준 언어인 c에서는 특정 메모리 주소에 접근해야 하는 필요성이 있기 때문이다.
- 예를 들어, 특정 하드웨어 레지스터나, 운영체제에서 제공하는 특정 메모리 주소로 접근해야 할때 포인터가 필수적으로 사용되기 때문이다.
- 추가적인 예시로 당신은 레스토랑의 손님인데 주방장(c)에게 어떠한 요리를 주문했다. 그럼 이 주방장은(c) 요리를 하기 위해 창고(메모리)에 가서 재료들을 꺼내오거나 저장을 해야 할 것이다.
- 그리고 정글에서 배웠던 파이썬도 사실 근본적으로 Cpython이라고 c로 구성이 되어 있다.
2. 큰 데이터를 복사 없이 다루기 위해(효율성)
- 큰 배열, 구조체, 문자열 등을 다른 함수에 넘길 때, 그 값들을 전부 복사해서 전달하는 것은 메모리와 시간을 많이 소모할 것이다. 지금은 모르지만 과거 컴퓨터들은 메모리가 매우 제한적이었는데 그런 상황에서 이런 값들을 전부 복사하기보다는 그냥 그 값이 있는 메모리 주소만 넘겨줘서 필요하면 직접 찾아가도록 한 것이 효율적이었을 것이다.
- 이번에도 예시를 들어 보겠다. 당신이 만약 부모님께 차를 선물하려고 한다. 하지만 당신은 현재 매우 바쁘고 고향에서 멀리 떨어져 있다. 물론 부모님께 드리는 선물이니 직접 가지고 가야하는 것이 좋지만 당신은 현재 매우 중요한 프로젝트로 그럴 시간이나 여유가 없다. 이럴 때 기쁨과 보람은 조금 떨어지겠지만 부모님께 차가 있는 주소를 보내드려 직접 타고 가시게 하면 되지 않을까?
3. 함수에서 참조(Reference) 전달을 위해
- c 언어는 함수로 인자를 전달할 때 기본적으로 값이 복사된다. 이를 "Call by Value" 라고 한다.
- 만약 함수 내부에서 원본 데이터를 직접 수정해야 한다면, "복사된 값"이 아니라 "실제 주소"를 전달해야 한다.
- 이때 포인터를 함수 인자로 전달하면 함수 내부에서도 원본 데이터를 수정할. 수 있다.
- 비유하자면 당신이 매우 중요한 프레젠테이션을 앞두고 있다. 이 프레젠 테이션(원본)을 준비하는데 팀원들과 회의를 하며 회의실(함수)에서 같이 ppt(원본)을 제작하고 있다. 그렇게 완성된 ppt자료(원본)를 대 회의실(함수)에서 발표를 한다 이때 임원들에게 설명을 위해 ppt 발표 자료의 사본을 출력해서 자리에 올려두었다.(값 복사)
포인터는 변수다!
int a, b;
int *p;
p = &a;
*p = 2; // a = 2 와 동일한 의미
p = &b;
*p = 4; // b = 4 와 동일한 의미
즉, 포인터도 값을 바꿀 수 있고, 다른 변수를 가리키도록 할 수도 있다.
포인터에 타입이 왜 필요할까?
포인터도 형이 필요하다.
예를 들어, int *p
는 int형 변수를 가리키는 포인터고, char *p
는 char형 변수를 가리킨다.
왜냐하면, 포인터가 어떤 타입의 데이터를 가리키는지 알아야 얼마만큼의 메모리를 읽을지 결정할 수 있기 때문
심화
이중 포인터 개념
이중 포인터는 포인터를 가리키는 포인터이다. 즉, int *p;
는 int
형 변수의 주소를 저장하는 포인터고,int **pp;
는 포인터 p의 주소를 저장하는 포인터이다.
int a = 10;
int *p = &a; // a를 가리킴
int **pp = &p; // p를 가리킴
a : 10 (실제 값)
p : 주소(&a) -> 가리키는 값은 10
pp : 주소(&p) -> 가리키는 값은 주소(&a)
예시 코드
#include <stdio.h>
int main() {
int a = 5;
int *p = &a;
int **pp = &p;
printf("a = %d\n", a); // 5
printf("*p = %d\n", *p); // 5
printf("**pp = %d\n", **pp); // 5
**pp = 20;
printf("a = %d\n", a); // 20 (이중 포인터를 통해 값 변경)
return 0;
}
왜 이중 포인터를 쓸까?
- 함수 안에서 포인터 자체를 변경하고 싶을 때
- 다차원 배열이나 포인터 배열을 다룰 때
- 문자열 배열이나 동적 메모리 할당에서 유용
예시 : 함수에서 포인터를 바꾸고 싶을 때
#include <stdio.h>
void changePointer(int **pp) {
static int b = 99;
*pp = &b;
}
int main() {
int a = 10;
int *p = &a;
changePointer(&p); // p 자체의 주소를 넘김
printf("*p = %d\n", *p); // 99
return 0;
}
👉 만약 단순히 p만 넘기면 함수 내부에서만 바뀌고 원래 포인터는 변하지 않는다. 그래서 이중 포인터가 필요한 것이다.
실습 퀴즈
1. 아래 코드에서 출력 결과는?
int a = 5;
int *p = &a;
*p = 10;
printf("%d\n", a);
- 정답 : 10
*p
는 포인터 변수이고 &a
는 a
변수의 주소값을 포인터 변수 p
에 대입한다는 것이다.
*p
에 10을 전달하면 포인터 변수 내부에 있던 a
의 주소값을 이용해 a
의 값을 10으로 바꾸는 것이다.
2. 다음 코드를 작성하고, a
, b
, *p
의 값을 각각 출력하라.
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int *p;
p = &a;
*p = *p + 5;
p = &b;
*p = *p + 10;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("*p = %d\n", *p);
return 0;
}
a = 15
b = 30
*p = 30
*p = *p + 5
는 a
의 값에 5를 더한 것이다.
그다음 p
가 b
를 가리키고 있으니 b
에 10을 더한 것이다.
3. 아래 코드의 출력 결과를 예상하라.
#include <stdio.h>
int main() {
int nums[3] = {100, 200, 300};
int *p = nums;
for(int i = 0; i < 3; i++) {
printf("%d ", *(p + i));
}
return 0;
}
- 정답 : 100 200 300
배열 이름 nums
는 배열의 첫 번째 요소의 주소와 같다.
*(p + i)
는 포인터 연산을 이용한 배열 접근이다.
4. 다음 코드를 실행하면 어떤 결과가 나오는가?
#include <stdio.h>
int main() {
int a = 42;
int *p = &a;
int **pp = &p;
**pp = **pp + 8;
printf("a = %d\n", a);
return 0;
}
- 정답 : a = 50
**pp
는 p
가 가리키는 주소를 통해 a
에 접근하는 것
**pp = **pp + 8
은 결국 a = a + 8
과 같음
5. 다음 코드에서 어떤 문장이 출력되는가?
#include <stdio.h>
int main() {
char *str = "Hello, World!";
printf("%c\n", *(str + 7));
return 0;
}
- 정답 : W
str
은 문자열의 시작 주소를 가리키는 포인터
*(str + 7)
은 7번째 문자 'W'
를 가리킴 (0부터 시작)
6. malloc을 이용해서 사용자가 입력한 숫자만큼 정수를 저장하고, 합을 구하는 프로그램을 작성하라.
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("입력할 숫자의 개수를 입력하세요: ");
scanf("%d", &n);
int *arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
printf("메모리 할당 실패\n");
return 1;
}
printf("숫자 %d개를 입력하세요:\n", n);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
printf("총합: %d\n", sum);
free(arr); // 꼭 메모리 해제!
return 0;
}
malloc()
은 힙(heap)에 메모리를 할당한다.
사용 후에는 반드시 free()
로 해제해야 한다.