기본 콘텐츠로 건너뛰기

방송대 C프로그래밍 10강: 배열 주소 계산과 이중 포인터

0-썸네일-요약노트-C프로그래밍-10강

방송대 C프로그래밍 10강: 배열 주소 계산과 이중 포인터

p[3], *(p + 3), a[1][0]은 언제 같은 위치를 가리킬까? 이번 강의는 포인터를 단순한 주소 기호가 아니라 배열을 따라 이동하는 계산 도구로 읽는다. 문자열의 끝, 2차원 배열의 행 너비, 포인터 배열의 저장 방식, 이중 포인터의 두 단계 간접 참조를 실제 식과 메모리 흐름으로 풀어 보자.

포인터 식은 기준 주소와 이동량으로 나누어 푼다

배열과 포인터가 함께 나오는 식은 기호만 보고 암기하면 쉽게 뒤섞인다. 먼저 포인터가 현재 어느 요소를 가리키는지 정하고, 그다음 괄호 안에서 몇 요소를 이동하는지 계산하고, 마지막으로 *가 있으면 그 위치의 값을 읽는다.

int a[5] = {10, 20, 30, 40, 50};
int *p = a;

int b = *p + *(p + 3);  /* 10 + 40 = 50 */
p++;                    /* 이제 p는 a[1]을 가리킴 */
int c = *p + *(p + 3);  /* 20 + 50 = 70 */
int d = *p + 3;         /* 20 + 3 = 23 */

*(p + 3)은 포인터를 세 요소 이동한 뒤 그곳의 값을 읽는다. 반면 *p + 3은 현재 값을 읽은 뒤 정수 3을 더한다. 괄호 하나가 주소 이동과 값 계산을 갈라놓는다.

식주소 단계값 단계결과
*p현재 위치 a[0]그 위치를 역참조10
*(p + 3)a[0]에서 3요소 이동a[3]을 역참조40
*p + 3현재 위치는 그대로10을 읽고 3을 더함13
p[3]p + 3과 같은 위치그 위치를 역참조40

계산 순서: 현재 기준 요소를 표시하고, +나 ++의 이동량을 요소 단위로 적용한 뒤, * 또는 []가 요구하는 값을 읽는다.

포인터 한 칸은 자료형 한 요소만큼 이동한다

p + 1은 주소 숫자에 무조건 1바이트를 더한다는 뜻이 아니다. p가 int *이면 다음 int 요소로, char *이면 다음 char 요소로 이동한다. 그래서 배열 요소를 순서대로 방문할 때 자료형의 실제 바이트 수를 식에 직접 곱하지 않아도 된다.

int values[4] = {7, 14, 21, 28};
int *p = values;

printf("%d\n", *p);       /* 7 */
printf("%d\n", *(p + 2)); /* 21 */
printf("%d\n", p[3]);     /* 28 */

첨자식 p[i]는 *(p + i)와 같은 방식으로 해석된다. 배열 이름 values도 대부분의 식에서 첫 요소를 가리키는 포인터 값으로 변환되므로 values[i]와 *(values + i)가 같은 요소를 읽는다. 단, 이 표현의 호환성이 배열 객체와 포인터 변수를 같은 것으로 만들지는 않는다.

문자열 순회는 널 문자를 사용하기 전에 검사한다

C 문자열은 마지막 보이는 문자 뒤에 널 문자 '\0'를 둔다. 포인터로 문자열을 순회할 때 종료 검사를 출력 뒤에 두면 널 문자 위치까지 한 번 처리한다. 널 문자를 %c로 출력하면 화면에는 보통 빈칸처럼 보여도 반복이 한 차례 더 수행된 사실은 남는다.

const char *cp = "COMPUTER";
int i = 0;

while (*(cp + i) != '\0') {
    printf("%d: %c\n", i, *(cp + i));
    i++;
}

이 반복문은 인덱스 0부터 7까지의 문자만 출력한다. do-while로 먼저 출력한 뒤 조건을 검사하면 인덱스 8의 널 문자도 출력 대상으로 삼는다. 종료 표지를 자료로 사용하기 전에 확인해야 하는 이유다.

문자 배열과 문자열 리터럴 포인터를 구분한다

선언실제 저장문자 수정이름 또는 변수 이동
char s[] = "SCIENCE";배열 안에 문자들을 복사해 저장s[0] = 's'; 가능s++ 불가
const char *p = "SCIENCE";문자열 리터럴을 가리키는 포인터*p = 's'; 불가p++ 가능

강의 예제의 char *cp = "computer";는 문자열 리터럴을 가리킨다. 리터럴의 내용을 바꾸려는 동작은 올바르지 않으므로 실제 코드에서는 const char *로 선언해 변경 금지를 자료형에 드러내는 편이 안전하다. 수정 가능한 문자열이 필요하면 char s[] = "computer";처럼 배열을 만든다.

오개념 교정: 포인터로 문자를 읽을 수 있다는 사실은 그 문자를 쓸 수도 있다는 뜻이 아니다. 가리키는 대상이 수정 가능한 배열인지 문자열 리터럴인지 먼저 확인한다.

1차원 배열식은 첨자를 포인터 이동으로 번역한다

배열 s의 시작을 포인터 cp에 대입하면 같은 요소를 두 문법으로 표현할 수 있다.

char s[] = "SCIENCE";
char *cp = s;

printf("%c %c\n", s[0], *cp);       /* S S */
printf("%c %c\n", s[2], *(cp + 2)); /* I I */
printf("%c %c\n", *(s + 5), cp[5]); /* C C */

s[i], *(s + i), cp[i], *(cp + i)는 cp == s인 동안 같은 요소를 읽는다. 그러나 cp++로 포인터를 옮기면 기준 주소가 달라지므로 이후의 cp[i]는 원래 s[i]보다 한 칸 뒤 요소를 가리킨다.

따라서 등가식을 사용할 때는 식 모양만 비교해서는 안 된다. 두 표현의 기준 주소가 같은지, 포인터가 그 사이에 이동하지 않았는지를 함께 확인해야 한다.

2차원 첨자는 행 너비를 곱하고 열을 더한다

int a[2][3]은 논리적으로 2행 3열이지만 메모리에는 첫 행의 세 요소 다음에 둘째 행의 세 요소가 이어지는 행 우선 순서로 놓인다. 첫 요소를 int *pt = &a[0][0];로 가리키면 강의의 주소 계산식은 다음과 같다.

a[i][j]의 선형 이동량 = i * 열 수 + j
열 수가 3이면          = i * 3 + j
강의의 포인터 표현     = *(pt + i * 3 + j)

예를 들어 a[1][2]는 1 * 3 + 2 = 5이므로 시작점에서 다섯 요소 뒤에 있다. 인덱스가 0부터 시작하므로 전체 여섯 요소 중 마지막 요소다.

요소계산선형 이동량포인터식
a[0][0]0 * 3 + 00*pt
a[0][2]0 * 3 + 22*(pt + 2)
a[1][0]1 * 3 + 03*(pt + 3)
a[1][2]1 * 3 + 25*(pt + 5)

중첩 반복문도 같은 식을 반복한다

int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
int *pt = &a[0][0];

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", *(pt + i * 3 + j));
    }
    printf("\n");
}

바깥 반복문의 i가 행을 고르고 i * 3이 그 행의 시작 위치를 만든다. 안쪽 반복문의 j가 행 안의 열을 이동한다. 열 수 3을 2나 행 수 2로 잘못 곱하면 둘째 행의 시작점부터 어긋난다.

센티널 반복은 종료값과 배열 경계를 함께 요구한다

강의에서는 2차원 배열의 값들을 연속해서 읽다가 -9를 만나면 멈추는 센티널 방식을 다룬다. 센티널은 자료와 겹치지 않는 특별한 종료값이다.

int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, -9}};
int *p = &a[0][0];
int i = 0;

while (i < 9 && *(p + i) != -9) {
    printf("%d ", *(p + i));
    i++;
}

강의의 핵심은 -9가 종료 기준이라는 점이다. 여기에 i < 9라는 실제 요소 수 조건을 함께 두면 센티널이 실수로 빠졌을 때도 배열 밖으로 나가지 않는다. 센티널만 믿는 반복문은 종료값이 없거나 바뀌었을 때 경계를 잃는다.

경계 검산: 문자열에는 널 문자, 센티널 배열에는 약속한 종료값이 필요하다. 어느 경우든 저장 공간의 실제 크기를 함께 검사하면 종료 표지가 손상된 상황까지 방어할 수 있다.

배열 이름과 포인터 변수는 표현이 겹쳐도 역할이 다르다

배열 이름은 대부분의 식에서 첫 요소의 주소로 변환되므로 포인터 연산에 사용할 수 있다. 하지만 배열 이름 자체는 다른 위치를 새로 저장하는 포인터 변수가 아니다.

int a[5] = {1, 2, 3, 4, 5};
int *p = a;

p++;  /* 가능: 포인터 변수 p의 저장값을 다음 요소 주소로 변경 */
/* a++; */  /* 오류: 배열 이름이 가리키는 시작 위치는 변경 불가 */

p++ 뒤에 *p를 읽으면 2가 나온다. 배열 a의 저장 위치나 내용은 바뀌지 않았고, 포인터 변수 p에 들어 있던 주소만 달라졌다. 이 차이를 놓치면 “배열 이름은 포인터다”라는 지나친 결론에 도달한다.

두 객체를 비교하는 세 질문

  1. 그 이름이 실제 배열 저장 공간을 선언했는가, 주소를 저장하는 포인터 변수를 선언했는가?
  2. []나 *로 같은 요소를 읽는 표현이 가능한가?
  3. ++나 대입으로 기준 주소 자체를 바꿀 수 있는가?

두 번째 질문에는 둘 다 “가능”일 수 있지만 첫 번째와 세 번째 답이 다르다. 표현의 호환성과 객체의 동일성을 분리해야 한다.

포인터 배열은 각 행의 시작 주소를 따로 저장한다

길이가 다른 여러 문자열을 저장할 때 char str[3][10]을 사용하면 모든 행이 10칸을 차지한다. 짧은 문자열의 남은 칸도 배열에 포함된다. 반면 포인터 배열은 각 원소가 문자열의 첫 문자 주소를 저장하므로 행마다 길이가 달라도 같은 폭의 문자 저장 공간을 강제하지 않는다.

구조선언행의 배치접근
2차원 문자 배열char str[3][10]문자 10칸짜리 행 3개가 연속 배치str[i][j]
문자 포인터 배열const char *str[3]문자열 시작 주소 3개를 배열에 저장str[i][j]
const char *str[3] = {"KNOU", "Computer", "123456789"};

for (int i = 0; i < 3; i++) {
    for (int j = 0; str[i][j] != '\0'; j++) {
        putchar(str[i][j]);
    }
    putchar('\n');
}

str[i]는 i번째 포인터, 즉 i번째 문자열의 시작 주소다. 여기에 다시 [j]를 적용하면 해당 문자열의 j번째 문자를 읽는다. 문자열 리터럴을 가리키므로 여기서도 const char *로 변경 금지를 표시했다.

선택 기준: 모든 행을 같은 크기의 수정 가능한 문자 공간으로 확보하려면 2차원 문자 배열이 알맞다. 길이가 다른 읽기 전용 문자열의 시작 주소들을 모으려면 포인터 배열이 공간 구조를 더 직접적으로 표현한다.

이중 포인터는 포인터를 거쳐 값에 도달한다

이중 포인터는 자료가 있는 곳을 바로 가리키지 않고, 그 자료를 가리키는 포인터 변수의 주소를 저장한다. 선언 int **pp;에서 pp는 int *를 가리키고, *pp는 그 안의 int * 값이며, **pp는 최종 int 자료다.

char a = 'A';
char *p = &a;
char **pp = &p;

printf("%c\n", **pp);  /* A */

주소 사슬은 pp -> p -> a다. 첫 번째 *는 pp가 가리키는 포인터 변수 p에 도달하고, 두 번째 *는 p가 가리키는 문자 a에 도달한다.

표현자료형뜻
ppchar **포인터 변수 p의 주소
*ppchar *p에 저장된 주소, 즉 &a
**ppchar최종 자료 'A'

**pp = 'B';라고 대입하면 주소 두 개를 바꾸는 것이 아니라 사슬 끝의 a가 'B'로 바뀐다. 별표의 개수는 “주소를 몇 번 따라가야 최종 자료가 나오는가”로 읽는다.

포인터 문제는 네 칸 검산표로 마무리한다

새로운 포인터 식을 만나면 다음 네 항목을 순서대로 적는다. 이 절차는 1차원 배열, 문자열, 2차원 배열, 포인터 배열, 이중 포인터를 같은 틀에서 판독하게 해 준다.

  1. 기준 주소: 포인터가 현재 어느 객체의 어느 요소를 가리키는가?
  2. 이동 단위: 포인터의 자료형은 무엇이며 몇 요소를 이동하는가?
  3. 종료 경계: 요소 수, 널 문자, 센티널 가운데 무엇으로 범위를 제한하는가?
  4. 변경 가능성: 포인터 변수의 주소, 배열의 내용, 문자열 리터럴 중 무엇을 바꾸려는가?

예를 들어 str[1][3]을 읽는다면 str[1]이 두 번째 문자열의 시작 주소이고, 문자 단위로 세 칸 이동하며, 널 문자 이전인지 확인하고, 리터럴을 가리키므로 읽기만 해야 한다. 복잡해 보이는 두 첨자도 네 질문으로 분해하면 한 단계씩 검산할 수 있다.

핵심 개념 정리

  • p[i]는 *(p + i)와 같은 요소를 읽으며, 포인터 덧셈의 단위는 그 포인터가 가리키는 자료형의 한 요소다.
  • *(p + 3)은 주소를 이동한 뒤 역참조하고, *p + 3은 현재 값을 읽은 뒤 3을 더한다.
  • 문자열은 널 문자를 사용하기 전에 검사하고, 문자열 리터럴을 가리키는 포인터는 const char *로 표현하는 편이 안전하다.
  • 2차원 배열의 선형 이동량은 행 인덱스 * 열 수 + 열 인덱스로 계산한다.
  • 센티널 반복에는 종료값뿐 아니라 실제 배열 크기 조건도 함께 두어야 경계 이탈을 막을 수 있다.
  • 배열 이름은 식에서 첫 요소 주소로 변환될 수 있지만, 다른 주소를 대입하거나 ++로 이동시키는 포인터 변수는 아니다.
  • 포인터 배열은 포인터들을 요소로 저장하고, 이중 포인터는 포인터의 주소를 저장해 두 번 역참조한다.

포인터 식을 풀 때는 먼저 주소 사슬을 그린다. 현재 기준 요소에서 몇 칸 이동하는지 계산하고, 행이 있으면 열 수를 곱하고, 별표 수만큼 주소를 따라간다. 마지막으로 널 문자·센티널·요소 수의 경계와 대상의 수정 가능성을 확인한다. 이 순서를 지키면 배열과 포인터의 비슷한 표현을 외우지 않고도 실제 위치와 값을 계산할 수 있다.

예상문제 10선

1. 포인터 p가 유효한 배열 요소들을 가리킬 때 p[3]과 같은 값을 읽는 식은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ③

  • ① 오답: 현재 요소의 값에 정수 3을 더할 뿐 주소를 세 요소 이동하지 않는다.
  • ② 오답: 시작점에서 두 요소 뒤인 p[2]를 읽는다.
  • ③ 정답: 첨자식 p[i]는 포인터식 *(p + i)와 같은 요소를 읽는다.
  • ④ 오답: 현재 요소의 값을 읽은 뒤 그 값을 증가시키는 변경 연산이다.

2. int a[5] = {12, 18, 25, 31, 40}; int *p = a + 1;일 때 *p + *(p + 2)의 값은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ①

  • ① 정답: p는 a[1]을 가리키므로 18 + a[3], 즉 18 + 31 = 49다.
  • ② 오답: a[1] + a[2]를 계산한 값으로, 두 칸 이동 뒤의 요소를 잘못 골랐다.
  • ③ 오답: 기준을 배열 시작으로 되돌리거나 이동량을 혼동한 결과다.
  • ④ 오답: 현재 값 18에 정수 2를 더하는 계산과도 일치하지 않는다.

3. 문자열 리터럴 "DATA"를 가리키고 문자는 수정하지 않을 포인터 선언으로 가장 적절한 것은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ④

  • ① 오답: char 변수 하나에는 문자열의 시작 주소를 저장할 수 없다.
  • ② 오답: 이중 포인터는 문자 포인터의 주소를 저장하는 형식이며 문자열 시작 주소의 직접 형식이 아니다.
  • ③ 오답: 문자열의 요소는 char이므로 int *와 자료형이 맞지 않는다.
  • ④ 정답: 문자 리터럴의 시작을 가리키되 포인터를 통한 문자 변경을 금지한다.

4. 널 문자를 출력 대상으로 삼지 않고 문자열의 보이는 문자만 순회하는 방법은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ②

  • ① 오답: 사용 후 검사하면 널 문자 위치도 한 번 출력 대상으로 처리한다.
  • ② 정답: while (*p != '\0')처럼 먼저 경계를 확인하면 보이는 문자만 출력한다.
  • ③ 오답: 종료 표지를 없애면 문자열의 끝을 판별할 수 없게 된다.
  • ④ 오답: 문자열의 앞 두 문자를 건너뛰므로 올바른 전체 순회가 아니다.

5. int a[2][4]; int *p = &a[0][0];에서 a[1][2]의 주소에 해당하는 포인터식은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ③

  • ① 오답: 첫 행의 마지막 요소에 해당하며 둘째 행으로 이동하지 않았다.
  • ② 오답: 1 * 4 + 1로 계산한 a[1][1]의 위치다.
  • ③ 정답: 열 수가 4이므로 이동량은 1 * 4 + 2 = 6이다.
  • ④ 오답: 전체 요소 수와 이동량을 혼동했으며 배열 끝 다음 위치다.

6. 센티널 -9만 믿고 배열을 순회하는 반복문에서 실제 배열에 -9가 없다면 가장 큰 위험은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ④

  • ① 오답: 센티널이 없으면 자동으로 첫 요소에서 멈추는 규칙이 없다.
  • ② 오답: 컴파일러는 프로그램이 약속한 센티널을 배열에 삽입하지 않는다.
  • ③ 오답: 읽기 반복이 배열 요소를 모두 0으로 바꾸지는 않는다.
  • ④ 정답: 종료값을 만나지 못하면 포인터가 배열 밖으로 이동하므로 요소 수 경계도 함께 검사해야 한다.

7. int a[5]; int *p = a;에서 p++는 가능하지만 a++는 불가능한 이유는?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ①

  • ① 정답: 포인터 변수에는 새 주소를 저장할 수 있지만 배열 객체의 이름이 나타내는 시작 위치는 변경 대상이 아니다.
  • ② 오답: 일반 int 배열의 요소는 a[0] = 3;처럼 수정할 수 있다.
  • ③ 오답: int *의 증가는 다음 int 요소로 이동하며 가능 여부의 이유도 아니다.
  • ④ 오답: 배열 이름은 대부분의 식에서 첫 요소 주소로 변환되어 포인터식에 쓰인다.

8. 길이가 서로 다른 읽기 전용 문자열 여러 개의 시작 주소를 하나의 배열로 관리하려 할 때 가장 직접적인 선언은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ③

  • ① 오답: 문자 하나만 저장하므로 여러 문자열의 주소를 관리할 수 없다.
  • ② 오답: 요소가 정수이며 문자 주소를 저장하는 포인터 배열이 아니다.
  • ③ 정답: 세 개의 const char * 요소가 각각 읽기 전용 문자열의 시작 주소를 저장한다.
  • ④ 오답: 이중 포인터의 2차원 배열은 요구보다 간접 단계와 차원이 과도하다.

9. int n = 6; int *p = &n; int **pp = &p; **pp += 4; 실행 뒤 n의 값은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ②

  • ① 오답: 두 번 역참조한 대상은 실제 변수 n이므로 값이 변경된다.
  • ② 정답: pp -> p -> n을 따라가 n의 값 6에 4를 더한다.
  • ③ 오답: 4는 새 값이 아니라 기존 값에 더하는 증가량이다.
  • ④ 오답: **pp는 주소가 아니라 사슬 끝의 int 자료를 나타낸다.

10. 포인터로 2차원 배열 요소를 안전하게 계산하고 순회하기 위한 점검 순서로 가장 타당한 것은?

정답입니다.

오답입니다. 답안을 다시 선택해 보세요.

정답 및 해설 보기

정답: ④

  • ① 오답: 배열 이름은 증가시킬 수 없고 열 수가 없으면 행의 시작 위치를 계산할 수 없다.
  • ② 오답: 포인터 이동은 가리키는 자료형의 요소 단위이므로 자료형을 무시할 수 없다.
  • ③ 오답: 센티널이 빠질 수 있으므로 실제 요소 수 경계도 함께 확인해야 한다.
  • ④ 정답: 기준·이동량·경계·변경 가능성을 차례로 확인해 위치 계산과 메모리 안전을 함께 검산한다.

참고 자료와 작성 기준

이 글은 해당 차시 강의자료를 바탕으로 학습 목적에 맞게 재구성한 비공식 학습자료입니다. 기준 주소와 이동량을 분리하는 풀이법, 2차원 배열의 선형 위치 계산표, 널 문자와 센티널의 경계 비교, 포인터 배열 선택 기준, 이중 포인터 주소 사슬 추적은 학습자가 식을 직접 검산하도록 새로 구성했습니다.

  • 작성·편집: 올에이클래스 학습연구팀
  • 주요 근거: 한국방송통신대학교 컴퓨터과학과, 「C프로그래밍」 10강 배열과 포인터(3) 강의자료, 2025
  • 보충 자료: 외부 자료를 사용하지 않음
  • 편집 원칙: 올에이클래스 편집 정책
  • 최종 내용 검토: 2026-09-07

댓글