Skip to content

9주차 · 배열과 포인터(2)

지난 시간에는 배열을 배웠습니다.

이번 시간에는 많은 학생들이 가장 어렵다고 느끼는 포인터(pointer)를 배웁니다.

처음에는 &, *, 주소 같은 기호가 한꺼번에 나와서 복잡해 보일 수 있습니다. 하지만 핵심은 아주 단순합니다.

일반 변수는 값을 저장합니다.

포인터 변수는 값이 저장된 위치, 즉 주소를 저장합니다.

오늘의 목표

오늘 수업이 끝났을 때 아래 내용을 설명할 수 있으면 충분합니다.

  1. 메모리 주소가 무엇인지
  2. &aa의 주소라는 것
  3. 포인터 변수를 선언하는 방법
  4. p*p의 차이
  5. 포인터를 통해 원래 변수의 값을 바꿀 수 있다는 것
  6. NULL이 아무 곳도 가리키지 않는 상태라는 것

1. 포인터를 배우기 전에 "주소"부터 이해합시다

지금까지 우리는 다음처럼 변수를 만들었습니다.

c
int a = 10;

우리는 이것을 보통 이렇게 이해했습니다.

a라는 이름의 상자에 10을 넣는다.

이 설명은 맞습니다.

하지만 컴퓨터 안에서는 이 상자가 실제 메모리(memory) 어딘가에 놓여 있습니다.

메모리의 각 위치에는 위치를 구분할 수 있는 번호가 있습니다.

그 번호를 주소(address)라고 합니다.

쉽게 생각하면 집과 비슷합니다.

text
사람 이름     → 변수 이름
집 안의 물건  → 변수에 저장된 값
집 주소       → 메모리 주소

2. 변수의 값과 주소를 그림으로 봅시다

변수 a의 값 10과 예시 메모리 주소 0x1000을 비교한 그림

그림에서 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
&a

a는 변수 안에 들어 있는 입니다.

&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;

이 문장은 이렇게 읽으면 됩니다.

pint형 값을 가리킬 수 있는 포인터 변수이다.

처음에는 이 정도만 이해하면 충분합니다.


7. 포인터에 변수의 주소를 넣어 봅시다

c
int a = 10;
int *p;

p = &a;

마지막 줄을 아주 천천히 읽어 보겠습니다.

c
p = &a;

오른쪽 &aa의 주소입니다.

왼쪽 p주소를 저장하는 포인터 변수입니다.

따라서 이 문장은:

p에 a의 주소를 저장한다.

라는 뜻입니다.

처음부터 한 줄로 쓸 수도 있습니다.

c
int a = 10;
int *p = &a;

8. p = &a를 그림으로 봅시다

포인터 p가 변수 a의 주소를 저장하고 a를 가리키는 그림

여기서 아주 중요한 점이 있습니다.

포인터 p도 하나의 변수입니다.

p 자신도 메모리 어딘가에 존재합니다.

다만 p 안에 들어 있는 값이 일반 숫자가 아니라 다른 변수의 주소라는 것이 다릅니다.

text
일반 변수 a → 10을 저장
포인터 p     → a의 주소를 저장

9. 이제 가장 중요한 *p를 배웁니다

다음 상태라고 합시다.

c
int a = 10;
int *p = &a;

p에는 a의 주소가 들어 있습니다.

그런데 우리는 주소 자체보다 그 주소에 저장된 실제 값이 필요할 때가 많습니다.

그럴 때 다음처럼 씁니다.

c
*p

*p는 이렇게 읽으면 됩니다.

p가 가리키는 곳에 들어 있는 값

현재 pa를 가리키므로:

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이 나오는 이유는 pa를 가리키고 있기 때문입니다.


11. 포인터에서 가장 중요한 네 가지를 비교합니다

a, &a, p, *p의 차이를 비교한 그림

이 네 가지는 반드시 구분할 수 있어야 합니다.

표현예시
aa에 저장된 값10
&aa의 주소0x1000
pp에 저장된 주소0x1000
*pp가 가리키는 곳의 값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. &, *, &&를 한 번에 구분합시다

앞에서 비슷한 기호를 이미 여러 번 봤습니다.

표현사용 예
&ap = &a;a의 주소
int *pint *p;포인터 변수 선언
*pprintf("%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;

이라고 썼습니다.

그런데 pa를 가리키고 있으므로 결국 a가 있는 곳의 값을 100으로 바꾼 것입니다.


15. 포인터를 통한 값 변경을 그림으로 봅시다

포인터를 통해 a의 값이 10에서 100으로 변경되는 그림

중요한 것은 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;

p1p2 모두 포인터입니다.

그런데 다음은 다릅니다.

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을 그림으로 이해해 봅시다

포인터 p가 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의 주소를 뜻하는 것은 무엇인가요?

  1. a
  2. *a
  3. &a
  4. p
정답 보기

정답은 3번 &a입니다.

문제 2

다음 코드에서 *p의 값은 무엇인가요?

c
int a = 30;
int *p = &a;
정답 보기

정답은 30입니다.

pa를 가리키므로 *pa의 값입니다.

문제 3

다음 코드를 실행한 뒤 a의 값은 무엇인가요?

c
int a = 10;
int *p = &a;
*p = 80;
정답 보기

정답은 80입니다.

*p = 80;은 p가 가리키는 a의 실제 값을 바꿉니다.

문제 4

p == NULL은 어떤 뜻인가요?

정답 보기

p가 현재 아무 곳도 가리키지 않는다는 뜻입니다.

문제 5

다음 중 포인터 변수 선언은 무엇인가요?

  1. int p;
  2. int *p;
  3. *int p;
  4. 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
*p

30. 다음 시간 예고

다음 10주차에서는 이번 시간에 배운 포인터를 8주차의 배열과 연결합니다.

다음 내용을 배웁니다.

text
포인터와 배열
포인터 배열
이중 포인터

이번 주의 p*p가 확실히 이해되면 다음 주 내용이 훨씬 쉬워집니다.