Skip to Content
독학사독학사 2단계C프로그래밍11. 포인터 기초: 주소·역참조·포인터와 배열의 관계

이번 문서의 목표: 포인터 변수가 저장하는 것이 정확히 무엇인지, &* 연산자가 각각 무슨 일을 하는지 메모리 주소 그림으로 이해하고, 배열 이름이 포인터처럼 동작하는 원리와 그 예외를 구분해 포인터·배열 혼합 문제를 정확히 풀 수 있게 된다.

왜 포인터가 필요한가

10편에서 C 함수는 인자를 값으로 전달하기 때문에, 함수 안에서 매개변수를 바꿔도 호출한 쪽의 원본 변수는 바뀌지 않는다는 것을 확인했습니다. 그런데 만약 함수가 호출한 쪽의 변수를 직접 바꿔야 한다면 어떻게 해야 할까요? 값이 아니라 그 변수가 저장된 위치(주소)를 넘겨주면 됩니다. 이 “메모리 주소를 저장하는 변수”가 바로 포인터(pointer, 포인터)입니다.

쉽게 말하면: 일반 변수가 “값 자체를 담는 상자”라면, 포인터는 “다른 상자가 어디 있는지 적어 놓은 쪽지”입니다.

포인터는 독학사 C프로그래밍에서 가장 어렵게 느껴지는 동시에 출제 비중이 가장 높은 주제입니다. 배열, 함수 인자, 문자열, 구조체, 동적 메모리(13~17편)가 모두 포인터 개념 위에 세워져 있으므로, 이번 편에서 기초를 확실히 다져야 합니다.

1. 메모리 주소와 변수

컴퓨터의 메모리는 한 칸(보통 1바이트)마다 고유한 번호가 매겨진 거대한 서랍장과 같습니다. 이 번호를 주소(address, 주소)라고 부릅니다. 변수를 선언하면 컴퓨터는 그 자료형의 크기만큼 메모리를 확보하고, 그 확보된 공간의 시작 주소를 변수 이름과 연결합니다.

int num = 10; printf("%d\n", num); /* num에 저장된 값 */ printf("%p\n", (void *)&num); /* num이 저장된 주소 */
10 0x7ffee4b1c9ac

%p는 주소 값을 출력하는 서식입니다(실제 출력되는 주소 값은 실행할 때마다 달라집니다). &(앰퍼샌드) 연산자를 변수 앞에 붙이면 그 변수가 저장된 메모리 주소를 얻습니다. 이를 주소 연산자(address-of operator, 주소 연산자)라고 부릅니다.

개념num (일반 변수)&num (num의 주소)
저장하는 것정수 값 10num이 위치한 메모리 주소(예: 0x7ffee4b1c9ac)
자료형intint *(int를 가리키는 포인터형)

2. 포인터 변수 선언·초기화와 역참조

포인터 변수는 다른 변수의 주소를 저장하는 변수입니다. 선언 문법은 자료형 뒤에 *를 붙입니다.

int num = 10; int *p = # /* p는 num의 주소를 저장하는 int형 포인터 */ printf("%d\n", num); /* 10 */ printf("%d\n", *p); /* 10: p가 가리키는 곳의 값 */ printf("%p\n", (void *)p); /* num의 주소와 같은 값 */
10 10 0x7ffee4b1c9ac
변수저장하는 값의미
num10정수 값 자체
pnum의 주소 (예: 0x7ffee4b1c9ac)num이 어디 있는지 가리키는 화살표
*p10p가 가리키는 곳(num의 위치)에 실제로 들어 있는 값

*(에스터리스크)는 두 곳에서 다른 의미로 쓰여 헷갈리기 쉽습니다.

  • 선언할 때: int *p;*는 “p는 포인터 변수다”라는 자료형의 일부입니다.
  • 선언 이후 식에서: *p*는 “p가 가리키는 곳으로 가서 그 안의 값을 꺼내라”는 역참조 연산자(dereference operator, 역참조 연산자)입니다.

쉽게 말하면: &는 “이 변수의 주소를 알려줘”이고, *(역참조)는 “이 주소에 가서 그 안의 값을 보여줘”입니다. 서로 정반대 방향으로 움직입니다.

포인터를 통해 원본 값 바꾸기

void addOne(int *n) { *n = *n + 1; } int main(void) { int x = 5; addOne(&x); printf("%d\n", x); return 0; }
6

해석: 10편의 addOne(int n)과 달리, 이번에는 x주소(&x)를 넘겼습니다. 함수 안의 nx의 주소를 저장한 포인터이므로, *n = *n + 1;은 “n이 가리키는 곳(바로 x가 있는 자리)의 값을 1 늘려라”는 뜻입니다. 이번에는 x 자신의 메모리 공간을 직접 수정했으므로, 함수가 끝난 뒤에도 x는 6으로 바뀌어 있습니다. 자주 틀리는 점: “포인터를 함수에 넘기면 자동으로 원본이 바뀐다”가 아니라, 함수 안에서 역참조(*)로 그 주소에 실제로 접근해 값을 써야만 원본이 바뀝니다. 포인터 자체를 그냥 읽기만 하면 원본은 바뀌지 않습니다.

3. 배열 이름과 포인터의 관계

11편에서 배열은 메모리에 연속으로 배치된다고 배웠습니다. 이 성질 때문에 C에서는 배열 이름이 대부분의 상황에서 그 배열의 첫 번째 요소를 가리키는 포인터처럼 동작합니다.

int arr[3] = {10, 20, 30}; printf("%p\n", (void *)arr); /* 배열의 시작 주소 */ printf("%p\n", (void *)&arr[0]); /* arr[0]의 주소 */
0x7ffee4b1c990 0x7ffee4b1c990

두 출력이 완전히 같습니다. 즉 arr이라는 이름 자체가 &arr[0]같은 값(주소)을 나타냅니다.

포인터 산술 — 주소 이동은 자료형 크기만큼

int arr[3] = {10, 20, 30}; int *p = arr; /* 배열 이름 arr을 포인터에 대입 */ printf("%d\n", *p); /* 10 */ printf("%d\n", *(p + 1)); /* 20 */ printf("%d\n", *(p + 2)); /* 30 */
10 20 30
의미실제 주소 계산(int가 4바이트일 때)가리키는 값
parr의 시작 주소예: 1000arr[0] = 10
p + 1p보다 int 한 칸(4바이트) 뒤 주소1000 + 1×4 = 1004arr[1] = 20
p + 2p보다 int 두 칸(8바이트) 뒤 주소1000 + 2×4 = 1008arr[2] = 30

자주 틀리는 점: p + 1은 “주소값에 숫자 1을 더한 것”이 아니라 “p가 가리키는 자료형의 크기만큼 다음 요소로 이동”한 것입니다. int *에서 p + 1은 실제 주소가 4(또는 컴파일러의 int 크기)만큼 증가하고, double *이었다면 8만큼 증가합니다. 이를 포인터 산술(pointer arithmetic, 포인터 산술)이라고 합니다. 이 규칙 덕분에 *(p + i)arr[i]완전히 같은 값을 가리키며, 실제로 C 컴파일러는 arr[i]라는 표기를 내부적으로 *(arr + i)로 처리합니다.

배열 이름과 포인터 변수의 결정적 차이

배열 이름이 포인터처럼 동작한다고 해서 배열 이름이 곧 포인터 변수인 것은 아닙니다. 결정적인 차이가 하나 있습니다.

int arr[3] = {10, 20, 30}; int *p = arr; p = p + 1; /* 정상: 포인터 변수는 다른 주소를 다시 담을 수 있다 */ arr = arr + 1; /* 오류! 배열 이름은 상수 취급되어 재대입 불가능 */

자주 틀리는 점: p는 포인터 변수이므로 값을 계속 바꿔 다른 곳을 가리키게 할 수 있지만, arr은 컴파일 시점에 “이 배열이 시작하는 주소”로 고정된 상수와 같은 성격을 가져 arr = arr + 1;처럼 재대입할 수 없습니다. 배열 이름은 “포인터처럼 값을 읽을 수 있지만, 포인터처럼 다시 대입받을 수는 없는” 특수한 존재입니다. 시험에서는 이 둘을 같은 것으로 착각하게 만드는 오류 찾기 문제가 자주 나옵니다.

4. 포인터로 문자열 접근하기

11편에서 문자열은 널 종료 char 배열이라고 배웠습니다. 문자열은 char 배열이므로, 배열 이름-포인터 관계가 문자열에도 그대로 적용됩니다.

char str[] = "Hi!"; char *p = str; while (*p != '\0') { printf("%c", *p); p++; } printf("\n");
Hi!
반복p가 가리키는 문자*p 값*p != ‘\0’?동작
1str[0]H출력 후 p++
2str[1]i출력 후 p++
3str[2]!출력 후 p++
4str[3]\0거짓반복 종료

해석: p는 처음에 str[0](문자 H)을 가리키다가, p++가 실행될 때마다 char 한 칸(1바이트)씩 다음 문자로 이동합니다. *p가 널 문자 '\0'이 되는 순간 조건이 거짓이 되어 반복이 멈춥니다. 이것이 바로 strlen, printf%s가 내부적으로 문자열의 끝을 찾는 방식과 같은 원리입니다.

char 배열과 char 포인터의 차이

char a[] = "Hi!"; /* 배열: 문자들을 담을 새 메모리를 직접 확보 */ char *b = "Hi!"; /* 포인터: 문자열 리터럴이 저장된 곳의 주소만 저장 */ a[0] = 'X'; /* 정상: a는 직접 확보한 메모리라 내용을 바꿀 수 있다 */ b[0] = 'X'; /* 위험: 문자열 리터럴은 읽기 전용 영역이라 수정이 정의되지 않은 동작 */

자주 틀리는 점: char a[] = "Hi!";"Hi!"의 내용을 복사해 새 메모리 공간(배열)에 담지만, char *b = "Hi!";"Hi!"가 프로그램의 읽기 전용 영역에 저장된 뒤 b가 그 주소만 가리킵니다. 겉모습이 비슷해 보여도, b가 가리키는 내용을 수정하려 하면 정의되지 않은 동작(많은 환경에서 프로그램이 비정상 종료)이 발생합니다. 반면 a는 자기 소유의 메모리이므로 자유롭게 내용을 바꿀 수 있습니다.

핵심 정리

  • &변수는 그 변수의 메모리 주소를, *포인터(역참조)는 그 포인터가 가리키는 위치의 값을 얻는다.
  • 포인터를 함수에 넘길 때 원본을 바꾸려면 함수 안에서 반드시 역참조로 그 주소에 값을 써야 한다.
  • 배열 이름은 대부분의 상황에서 첫 요소의 주소(&arr[0])와 같은 값으로 동작한다.
  • 포인터에 정수를 더하면(포인터 산술) 그 자료형의 크기만큼 주소가 이동하며, arr[i]*(arr + i)와 같다.
  • 배열 이름은 포인터처럼 읽을 수 있지만, 포인터 변수와 달리 재대입은 불가능하다.
  • char 배열 = "문자열"은 내용을 복사해 수정 가능한 메모리를 만들지만, char 포인터 = "문자열"은 읽기 전용 리터럴의 주소만 가리켜 수정하면 위험하다.

마무리 복습

문제 14지선다
다음 중 & 연산자와 * 연산자(역참조)의 관계를 옳게 설명한 것은?
문제 24지선다
함수 안에서 포인터 매개변수를 통해 호출한 쪽의 원본 변수 값을 실제로 바꾸려면 무엇이 필요한가?
문제 34지선다
int arr[3] = {10, 20, 30}; 일 때 arr과 &arr[0]의 관계로 옳은 것은?
문제 44지선다
int *p = arr; (arr은 int 배열)일 때, p + 1이 실제로 이동하는 주소 거리는? (int는 4바이트로 가정)
문제 54지선다
배열 이름과 포인터 변수의 차이에 대한 설명으로 옳은 것은?
문제 64지선다
char a[] = 'Hi'로 배열에 문자열을 담은 경우와 char b에 문자열 리터럴의 주소만 저장한 포인터의 차이로 옳은 것은?
문제 74지선다
다음 코드처럼 반복할 때, 반복이 멈추는 시점은?
char str[] = 'Hi!';
char *p = str;
while (*p != '\0') {
  p++;
}

참고 자료

Last updated on