포인터를 손으로 따라가는 네 문제다. 모두 “코드를 읽고 결과를 예측한다”는 같은 일을 한다. 답을 펴기 전에 종이에 칸을 그리고 화살표를 그어 본다1.

검증: 네 문제와 변형 문제의 답을 C로 컴파일해 실행(ASan·UBSan)해 확인했다 — 05_pointer-address-practice_p4.c

p1 · 포인터를 따라가며 값 추적하기

다음 코드가 찍는 값은?

int a = 10, b = 20;
int *p = &a;
int **q = &p;

*p = *p + b;       /* 1 */
p = &b;            /* 2 */
**q = **q + 1;     /* 3 */
*q = &a;           /* 4 */
*p = *p - b;       /* 5 */
printf("%d %d %d\n", a, b, **q);

사용 개념: 포인터의 &, *, 이중 포인터.

풀이 전략: 변수마다 칸을 하나씩 그린다. 포인터 칸에서는 가리키는 칸으로 화살표를 긋는다. 줄마다 “왼쪽은 어느 칸인가”를 먼저 정한 뒤 값을 넣는다. 별표가 하나면 한 번, 둘이면 두 번 따라간다.

풀이
  1. 칸 그리기: a 10, b 20. p는 a를 가리키고 q는 p를 가리킨다.
  2. 한 줄씩 따라가기:
줄 왼쪽이 가리키는 칸 a b p가 가리키는 곳
처음   10 20 a
1 p를 따라간 a 30 20 a
2 p 자신 30 20 b
3 q → p → b 30 21 b
4 q를 한 번 따라간 p 30 21 a
5 p를 따라간 a 9 21 a
  1. 찍는 값 정하기: **q는 q → p → a라서 9다. 출력은 9 21 9다.

변형. 4번 줄을 지우면 무엇이 찍히는가?

답

30 0 0. 5번 줄에서 p가 여전히 b를 가리키므로 b가 \(21 - 21 = 0\)이 된다. a는 30 그대로이고 **q는 b의 값 0이다.

p2 · 칸의 주소 계산하기

칸의 크기는 char 1바이트, short 2바이트, int 4바이트, double 8바이트로 한다.

  • (가) int arr[10]의 arr[0]이 100번지에 있다. arr[3]의 주소는?
  • (나) double d[10]의 d[0]이 400번지에 있다. d[2]의 주소는?
  • (다) short s[10]의 s[0]이 2000번지에 있다. s[7]의 주소는?
  • (라) char c[10]의 c[0]이 50번지에 있다. c[9]의 주소는?
  • (마) (가)의 배열에서 &arr[4] - &arr[1]의 값은? 두 주소는 몇 바이트 떨어져 있는가?
  • (바) int *p = arr + 2;라고 하면 p[1]은 arr의 몇 번 칸인가?

사용 개념: 배열과 포인터의 주소 식 “시작 + 번호 × 칸 크기”.

풀이 전략: 번호에 칸 크기를 곱해 시작 주소에 더한다. 포인터끼리의 덧셈과 뺄셈은 바이트가 아니라 칸을 센다는 것만 따로 챙긴다.

풀이
  1. 식에 넣기: (가) \(100 + 3 \times 4 = 112\). “arr[0]의 주소가 100이면 arr[3]은 왜 112인가”의 답이 이것이다. (나) \(400 + 2 \times 8 = 416\). (다) \(2000 + 7 \times 2 = 2014\). (라) \(50 + 9 \times 1 = 59\).
  2. 포인터끼리 빼기: (마) 3이다. 포인터의 뺄셈은 사이에 있는 칸 수를 준다. 바이트로는 \(3 \times 4 = 12\)바이트 떨어져 있다.
  3. 포인터에서 번호 붙이기: (바) p는 arr[2]를 가리킨다. p[1]은 거기서 한 칸 뒤인 arr[3]이다.

변형. (가)에서 int 대신 원소 하나가 12바이트인 구조체 배열이라면 3번 칸의 주소는?

답

\(100 + 3 \times 12 = 136\). 식은 같고 칸 크기만 바뀐다.

p3 · 값으로 받는 인자와 주소로 받는 인자

다음 코드가 찍는 값은?

void f(int x, int *y)
{
    x = x + 1;
    *y = *y + x;
}

int main(void)
{
    int a = 1, b = 2;
    f(a, &b);
    f(b, &a);
    printf("%d %d\n", a, b);
    return 0;
}

사용 개념: 포인터 인자와 값 전달.

풀이 전략: 함수를 부를 때마다 인자 칸을 새로 그린다. x에는 값을 베껴 넣고, y에는 화살표를 긋는다. x를 고치는 줄은 부른 쪽에 아무 일도 하지 않는다는 것을 놓치지 않는다.

풀이
  1. 첫 호출 f(a, &b): x는 1을 베껴 받고 y는 b를 가리킨다. x가 2가 되고, b는 \(2 + 2 = 4\)가 된다. a는 1 그대로다.
  2. 둘째 호출 f(b, &a): x는 4를 베껴 받고 y는 a를 가리킨다. x가 5가 되고, a는 \(1 + 5 = 6\)이 된다. b는 4 그대로다.
  3. 찍는 값 정하기: 6 4.

변형. 첫 인자도 주소로 받게 고친다. void g(int *x, int *y) { *x = *x + 1; *y = *y + *x; }로 바꾸고 g(&a, &b); g(&b, &a);를 부르면?

답

7 5. 첫 호출에서 a가 2, b가 \(2 + 2 = 4\)가 된다. 둘째 호출에서 b가 5, a가 \(2 + 5 = 7\)이 된다. 이번에는 1을 더한 것이 부른 쪽에 남는다.

p4 · 문자열 함수

  • (가) 문자열의 길이를 돌려주는 함수 length를 포인터로 써라. 번호 i를 쓰지 않는다.
  • (나) 아래의 포인터 판 비교 함수 compare에 ("data", "date"), ("list", "lis"), ("heap", "heap")을 넣으면 각각 무엇을 돌려주는가? 글자의 번호는 'a'가 97, 'e'가 101, 't'가 116이다.
  • (다) 아래의 포인터 판 복사 함수 copy로 "tree"를 베끼면 글자를 몇 번 베끼고 포인터를 몇 번 옮기는가?
int compare(const char *s, const char *t)
{
    for (; *s == *t; s++, t++)
        if (*s == '\0')
            return 0;
    return *s - *t;
}

void copy(char *s, const char *t)
{
    while ((*s = *t) != '\0') {
        s++;
        t++;
    }
}

사용 개념: 배열과 포인터의 문자 포인터와 '\0'.

풀이 전략: 문자열은 '\0'에서 끝난다는 것 하나로 푼다. 포인터를 한 칸씩 옮기다가 '\0'을 만나는 순간에 무엇을 하는지만 따져 본다.

풀이
  1. (가) 끝까지 옮기고 거리 재기: 포인터 하나를 '\0'까지 옮긴 뒤, 처음 주소와의 차를 돌려준다. 포인터끼리 빼면 칸 수가 나오고 char는 한 칸이 글자 하나다.
int length(const char *s)
{
    const char *p = s;
    while (*p != '\0')
        p++;
    return (int)(p - s);
}

length("tree")는 4, length("")는 0이다.

  1. (나) 처음 갈리는 글자 찾기: "data"와 "date"는 넷째 글자에서 갈린다. 'a' - 'e'는 \(97 - 101 = -4\)다. "list"와 "lis"는 넷째 자리에서 't'와 '\0'이 만난다. \(116 - 0 = 116\)이다. "heap"과 "heap"은 같은 채로 '\0'에 닿아 0이다.
  2. (다) 바퀴 세기: 't', 'r', 'e', 'e'를 베낄 때마다 계속하고, 다섯 번째로 '\0'을 베낀 뒤 멈춘다. 글자를 5번 베끼고 포인터를 4번 옮긴다. 글자가 \(k\)개면 \(k + 1\)번 베끼고 \(k\)번 옮긴다.

변형. copy의 조건을 while (*t != '\0') { *s = *t; s++; t++; }로 바꾸면 무엇이 잘못되는가?

답

끝 표시 '\0'을 베끼지 않는다. '\0'을 보는 순간 반복을 빠져나가기 때문이다. 받은 쪽 배열에 끝 표시가 없으면 그 문자열을 읽는 함수가 어디서 멈출지 모른다. 반복 뒤에 *s = '\0';을 한 줄 더 써야 한다.

  1. 답은 변형 문제까지 문제 코드로 확인했다. ↩