화면 밝기
9주차 · 배열과 포인터(2)
지난 시간에는 배열을 배웠습니다.
이번 시간에는 많은 학생들이 가장 어렵다고 느끼는 포인터(pointer)를 배웁니다.
처음에는 &, *, 주소 같은 기호가 한꺼번에 나와서 복잡해 보일 수 있습니다. 하지만 핵심은 아주 단순합니다.
일반 변수는 값을 저장합니다.
포인터 변수는 값이 저장된 위치, 즉 주소를 저장합니다.
포인터 변수는 값이 저장된 위치, 즉 주소를 저장합니다.
오늘의 목표
오늘 수업이 끝났을 때 아래 내용을 설명할 수 있으면 충분합니다.
- 메모리 주소가 무엇인지
&a가a의 주소라는 것- 포인터 변수를 선언하는 방법
p와*p의 차이- 포인터를 통해 원래 변수의 값을 바꿀 수 있다는 것
NULL이 아무 곳도 가리키지 않는 상태라는 것
1. 포인터를 배우기 전에 "주소"부터 이해합시다
지금까지 우리는 다음처럼 변수를 만들었습니다.
c
int a = 10;우리는 이것을 보통 이렇게 이해했습니다.
a라는 이름의 상자에 10을 넣는다.
이 설명은 맞습니다.
하지만 컴퓨터 안에서는 이 상자가 실제 메모리(memory) 어딘가에 놓여 있습니다.
메모리의 각 위치에는 위치를 구분할 수 있는 번호가 있습니다.
그 번호를 주소(address)라고 합니다.
쉽게 생각하면 집과 비슷합니다.
text
사람 이름 → 변수 이름
집 안의 물건 → 변수에 저장된 값
집 주소 → 메모리 주소2. 변수의 값과 주소를 그림으로 봅시다
그림에서 0x1000은 이해를 돕기 위한 예시 주소입니다.
실제 프로그램을 실행하면 주소는 훨씬 길고 복잡한 숫자로 보일 수 있습니다.
주소값 자체를 외울 필요는 없습니다
중요한 것은 숫자가 몇 번인지가 아니라 "변수에도 저장된 위치가 있다"는 사실입니다.
3. 실제로 주소를 출력해 봅시다
c
#include <stdio.h>
int main(void)
{
int a = 10;
printf("a의 값: %d\n", a);
printf("a의 주소: %p\n", (void *)&a);
return 0;
}실행 결과는 컴퓨터마다 다릅니다.
예를 들면 다음처럼 보일 수 있습니다.
text
a의 값: 10
a의 주소: 0000007FF8A3F814주소를 출력할 때는 %p를 사용합니다.
c
printf("%p\n", (void *)&a);(void *) 부분은 주소를 %p로 출력할 때 사용하는 안전한 형태라고 생각하면 됩니다.
지금은 자세한 형 변환 원리까지 외우지 않아도 됩니다.
4. a와 &a는 전혀 다릅니다
다음 두 표현을 비교해 봅시다.
c
a
&aa는 변수 안에 들어 있는 값입니다.
&a는 그 변수가 놓여 있는 주소입니다.
text
a → 10
&a → a가 있는 위치여기서 &는 주소 연산자(address operator)라고 부릅니다.
&a = a의 주소
5. scanf()에서 이미 주소를 사용하고 있었습니다
우리는 3주차부터 이런 코드를 계속 사용했습니다.
c
int age;
scanf("%d", &age);그때는 우선 이렇게 외웠습니다.
scanf()로 숫자를 입력받을 때 변수 앞에&를 붙인다.
이제 이유를 설명할 수 있습니다.
scanf()는 입력한 값을 변수 안에 저장해야 합니다.
그러려면 단순히 현재 값만 알아서는 안 되고 어디에 값을 써야 하는지 알아야 합니다.
그래서 변수의 주소를 전달합니다.
c
&age쉽게 읽으면:
입력한 숫자를 저장할 곳은
age가 있는 위치입니다.
라고 볼 수 있습니다.
6. 주소를 저장하는 변수도 만들 수 있습니다
여기서 한 가지 질문을 해 봅시다.
&a도 하나의 주소값이라면, 그 주소를 변수에 저장할 수 있을까요?
할 수 있습니다.
주소를 저장하는 특별한 변수를 포인터 변수(pointer variable)라고 합니다.
가장 기본적인 선언은 다음과 같습니다.
c
int *p;이 문장은 이렇게 읽으면 됩니다.
p는int형 값을 가리킬 수 있는 포인터 변수이다.
처음에는 이 정도만 이해하면 충분합니다.
7. 포인터에 변수의 주소를 넣어 봅시다
c
int a = 10;
int *p;
p = &a;마지막 줄을 아주 천천히 읽어 보겠습니다.
c
p = &a;오른쪽 &a는 a의 주소입니다.
왼쪽 p는 주소를 저장하는 포인터 변수입니다.
따라서 이 문장은:
p에 a의 주소를 저장한다.
라는 뜻입니다.
처음부터 한 줄로 쓸 수도 있습니다.
c
int a = 10;
int *p = &a;8. p = &a를 그림으로 봅시다
여기서 아주 중요한 점이 있습니다.
포인터 p도 하나의 변수입니다.
즉 p 자신도 메모리 어딘가에 존재합니다.
다만 p 안에 들어 있는 값이 일반 숫자가 아니라 다른 변수의 주소라는 것이 다릅니다.
text
일반 변수 a → 10을 저장
포인터 p → a의 주소를 저장9. 이제 가장 중요한 *p를 배웁니다
다음 상태라고 합시다.
c
int a = 10;
int *p = &a;p에는 a의 주소가 들어 있습니다.
그런데 우리는 주소 자체보다 그 주소에 저장된 실제 값이 필요할 때가 많습니다.
그럴 때 다음처럼 씁니다.
c
*p*p는 이렇게 읽으면 됩니다.
p가 가리키는 곳에 들어 있는 값
현재 p는 a를 가리키므로:
text
*p = a의 값 = 10이 됩니다.
이때 *를 간접 참조 연산자 또는 역참조 연산자라고 부릅니다.
처음에는 용어보다 뜻이 더 중요합니다.
*p = p가 가리키는 곳의 값
10. 직접 실행해 봅시다
c
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
printf("a의 값: %d\n", a);
printf("*p의 값: %d\n", *p);
return 0;
}실행 결과:
text
a의 값: 10
*p의 값: 10둘 다 10이 나오는 이유는 p가 a를 가리키고 있기 때문입니다.
11. 포인터에서 가장 중요한 네 가지를 비교합니다
이 네 가지는 반드시 구분할 수 있어야 합니다.
| 표현 | 뜻 | 예시 |
|---|---|---|
a | a에 저장된 값 | 10 |
&a | a의 주소 | 0x1000 |
p | p에 저장된 주소 | 0x1000 |
*p | p가 가리키는 곳의 값 | 10 |
따라서 현재 상태에서는:
text
a 와 *p → 같은 값
&a 와 p → 같은 주소가 됩니다.
12. 선언할 때의 *와 사용할 때의 *가 헷갈립니다
초보자가 가장 많이 헷갈리는 부분입니다.
먼저 선언을 봅시다.
c
int *p;여기에서 *는:
p는 포인터 변수입니다.
라는 뜻을 나타냅니다.
이번에는 선언한 뒤 사용합니다.
c
printf("%d\n", *p);여기에서 *p는:
p가 가리키는 곳의 값을 가져옵니다.
라는 뜻입니다.
같은 * 모양이지만 사용된 위치에 따라 읽는 방법이 달라집니다.
text
int *p; → 포인터 p를 선언
*p → p가 가리키는 값13. &, *, &&를 한 번에 구분합시다
앞에서 비슷한 기호를 이미 여러 번 봤습니다.
| 표현 | 사용 예 | 뜻 |
|---|---|---|
&a | p = &a; | a의 주소 |
int *p | int *p; | 포인터 변수 선언 |
*p | printf("%d", *p); | p가 가리키는 값 |
&& | if (a > 0 && b > 0) | 논리 AND |
& | a & b | 비트 AND |
모양이 비슷해도 문맥이 다르면 역할이 완전히 달라집니다.
외우는 방법
기호 하나만 보지 말고 코드 한 줄 전체를 보고 뜻을 판단하세요.
14. 포인터를 사용하면 원래 값을 바꿀 수도 있습니다
다음 코드를 봅시다.
c
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
*p = 100;
printf("a = %d\n", a);
printf("*p = %d\n", *p);
return 0;
}실행 결과:
text
a = 100
*p = 100우리는 a = 100;이라고 직접 쓰지 않았습니다.
대신:
c
*p = 100;이라고 썼습니다.
그런데 p가 a를 가리키고 있으므로 결국 a가 있는 곳의 값을 100으로 바꾼 것입니다.
15. 포인터를 통한 값 변경을 그림으로 봅시다
중요한 것은 p가 저장한 주소가 바뀐 것이 아니라는 점입니다.
text
p → 계속 a를 가리킴그 상태에서:
c
*p = 100;을 실행했기 때문에 가리키는 대상인 a의 값이 바뀝니다.
16. 직접 수정 실습 · 값을 3번 바꿔 봅시다
아래 코드를 그대로 실행해 보세요.
c
#include <stdio.h>
int main(void)
{
int score = 50;
int *p = &score;
printf("1. score = %d\n", score);
*p = 70;
printf("2. score = %d\n", score);
*p = 90;
printf("3. score = %d\n", score);
return 0;
}실행 결과:
text
1. score = 50
2. score = 70
3. score = 90직접 바꿔 보기
다음 부분만 바꾸어 다시 실행해 보세요.
c
*p = 70;
*p = 90;자신이 원하는 점수로 바꿔 보세요.
17. 주소를 출력해서 정말 같은지 확인해 봅시다
c
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
printf("&a = %p\n", (void *)&a);
printf("p = %p\n", (void *)p);
return 0;
}두 주소는 같게 나옵니다.
왜냐하면:
c
p = &a;라고 했기 때문입니다.
p에 &a를 저장했으므로 p와 &a는 같은 주소를 나타냅니다.
18. 포인터도 자료형이 중요합니다
일반 변수도 자료형이 있었습니다.
c
int age;
double height;
char grade;포인터도 어떤 종류의 값을 가리키는지 자료형을 적습니다.
c
int *pInt;
double *pDouble;
char *pChar;쉽게 읽으면:
text
int *pInt → int 값을 가리키는 포인터
double *pDouble → double 값을 가리키는 포인터
char *pChar → char 값을 가리키는 포인터처음에는 가리키는 대상의 자료형과 포인터 선언의 자료형을 맞춘다고 기억하면 됩니다.
19. 여러 포인터 변수를 선언할 때 주의합니다
다음 코드를 봅시다.
c
int *p1, *p2;p1과 p2 모두 포인터입니다.
그런데 다음은 다릅니다.
c
int *p1, p2;여기서는:
text
p1 → int 포인터
p2 → 일반 int 변수입니다.
그래서 처음 배우는 동안에는 한 줄에 하나씩 쓰는 방법도 좋습니다.
c
int *p1;
int *p2;코드가 조금 길어져도 실수를 줄일 수 있습니다.
20. 초기화하지 않은 포인터는 사용하면 안 됩니다
다음 코드는 위험합니다.
c
int *p;
printf("%d\n", *p);p가 어느 곳을 가리키는지 정하지 않았기 때문입니다.
어디를 가리키는지 모르는 포인터를 바로 역참조하면 프로그램이 잘못 동작하거나 종료될 수 있습니다.
따라서 처음에는 다음처럼 선언과 동시에 올바른 주소를 넣는 습관을 들이는 것이 좋습니다.
c
int a = 10;
int *p = &a;21. 아직 가리킬 대상이 없다면 NULL
포인터를 만들어야 하지만 아직 어떤 변수도 가리키지 않게 하고 싶을 수 있습니다.
그때 NULL을 사용합니다.
c
#include <stdio.h>
int main(void)
{
int *p = NULL;
if (p == NULL)
{
printf("p는 아직 아무 것도 가리키지 않습니다.\n");
}
return 0;
}실행 결과:
text
p는 아직 아무 것도 가리키지 않습니다.22. NULL을 그림으로 이해해 봅시다
NULL은 쉽게:
포인터가 지금은 아무 곳도 가리키지 않는다.
라고 이해하면 됩니다.
매우 중요합니다
p == NULL인 상태에서 다음처럼 사용하면 안 됩니다.
c
*p = 10;가리키는 대상이 없기 때문입니다.
23. 포인터와 함수가 연결되면 무엇이 달라질까요?
7주차에서 C언어의 함수 인수는 기본적으로 값이 복사되어 전달된다고 배웠습니다.
c
void change(int number)
{
number = 100;
}이 함수에서 number를 바꿔도 main()의 원래 변수는 바뀌지 않았습니다.
그런데 이제 주소를 알고 있으므로 원본 위치를 함수에게 알려주는 방법을 생각할 수 있습니다.
다음 코드는 우선 눈으로만 봅시다.
c
void change(int *p)
{
*p = 100;
}그리고 호출할 때:
c
change(&a);처럼 주소를 전달할 수 있습니다.
이 원리는 앞으로 배열과 포인터, 함수가 연결될 때 계속 등장합니다.
지금 완벽하게 외우지 않아도 됩니다
이번 주 핵심은 &a, p, *p의 관계입니다. 함수와 포인터를 함께 쓰는 방법은 뒤에서 다시 만나게 됩니다.
24. 실습 · 함수가 원래 값을 바꾸는지 확인해 봅시다
c
#include <stdio.h>
void change(int *p)
{
*p = 100;
}
int main(void)
{
int a = 10;
printf("변경 전: %d\n", a);
change(&a);
printf("변경 후: %d\n", a);
return 0;
}실행 결과:
text
변경 전: 10
변경 후: 100흐름을 말로 읽어 봅시다.
text
① a의 값은 10
② &a로 a의 주소를 구함
③ change 함수에 a의 주소를 전달
④ p가 a를 가리킴
⑤ *p = 100으로 a의 실제 값을 변경25. 일부러 틀려 봅시다 · 값과 주소 혼동
다음 코드는 문제가 있습니다.
c
int a = 10;
int *p;
p = a;무엇이 잘못되었을까요?
정답 보기
p는 주소를 저장해야 하는데 a는 값 10을 나타냅니다.
올바른 코드는 다음과 같습니다.
c
p = &a;26. 일부러 틀려 봅시다 · p와 *p 혼동
c
int a = 10;
int *p = &a;
printf("%d\n", p);무엇이 문제일까요?
정답 보기
p에는 주소가 들어 있습니다. 주소를 %d로 출력하려는 것은 맞지 않습니다.
주소는 다음처럼 출력할 수 있습니다.
c
printf("%p\n", (void *)p);가리키는 실제 정수값을 출력하려면:
c
printf("%d\n", *p);를 사용합니다.
27. 일부러 틀려 봅시다 · 초기화하지 않은 포인터
c
int *p;
*p = 50;왜 위험할까요?
정답 보기
p가 아직 어디를 가리킬지 정해지지 않았습니다.
먼저 올바른 주소를 넣어야 합니다.
c
int a = 0;
int *p = &a;
*p = 50;28. 오늘 배운 내용 확인하기
문제 1
다음 중 a의 주소를 뜻하는 것은 무엇인가요?
a*a&ap
정답 보기
정답은 3번 &a입니다.
문제 2
다음 코드에서 *p의 값은 무엇인가요?
c
int a = 30;
int *p = &a;정답 보기
정답은 30입니다.
p가 a를 가리키므로 *p는 a의 값입니다.
문제 3
다음 코드를 실행한 뒤 a의 값은 무엇인가요?
c
int a = 10;
int *p = &a;
*p = 80;정답 보기
정답은 80입니다.
*p = 80;은 p가 가리키는 a의 실제 값을 바꿉니다.
문제 4
p == NULL은 어떤 뜻인가요?
정답 보기
p가 현재 아무 곳도 가리키지 않는다는 뜻입니다.
문제 5
다음 중 포인터 변수 선언은 무엇인가요?
int p;int *p;*int p;int &p;
정답 보기
정답은 2번 int *p;입니다.
29. 이번 주 핵심 요약
포인터에서 가장 중요한 네 가지
a = 변수의 값&a = 변수의 주소p = 주소를 저장하는 포인터 변수*p = p가 가리키는 곳의 값 또한 다음 관계를 기억하세요.
c
int a = 10;
int *p = &a;이 상태에서는:
text
p → a의 주소
*p → a의 값입니다.
그리고:
c
*p = 100;처럼 사용하면 포인터를 통해 원래 변수의 값을 바꿀 수 있습니다.
이번 주에 모든 것을 완벽히 외울 필요는 없습니다.
다음 네 표현을 보고 말로 설명할 수 있다면 충분합니다.
c
a
&a
p
*p30. 다음 시간 예고
다음 10주차에서는 이번 시간에 배운 포인터를 8주차의 배열과 연결합니다.
다음 내용을 배웁니다.
text
포인터와 배열
포인터 배열
이중 포인터이번 주의 p와 *p가 확실히 이해되면 다음 주 내용이 훨씬 쉬워집니다.