C언어 포인터와 메모리 구조, 면접에서 막히는 지점

C언어 포인터가 면접에서 막히는 지점은 문법 기호가 아니라 주소와 대상, 배열이 포인터로 바뀌는 변환, 구조체 멤버 접근이 서로 다른 타입이라는 점입니다. 이 글은 C 표준과 cppreference를 기준으로 그 지점을 정리합니다.

이 글은 C 언어의 포인터와 배열·구조체 관계를 면접 준비 관점에서 정리한 일반 설명이며, 컴파일러와 아키텍처에 따라 객체 표현과 호출 규약은 달라질 수 있습니다.

C언어 포인터는 메모리에서 무엇을 가리키나?

한 줄 답: 포인터는 함수나 다른 객체를 가리키거나 아무것도 가리키지 않는 객체이며, &는 포인터를 얻고 단항 *는 가리키는 객체의 lvalue를 얻습니다.

포인터를 설명할 때는 포인터 값과 그 포인터가 가리키는 대상의 타입을 함께 구분해야 합니다. int *p에서 pint 객체를 가리키며, p라는 포인터 객체와 가리킴을 받는 int 객체는 서로 다른 대상입니다.

주소 연산자 &는 피연산자의 포인터를 얻으며, 단항 *는 포인터가 가리키는 객체를 나타내는 lvalue를 얻습니다. 다음처럼 &value로 포인터를 초기화한 뒤 *p에 대입하면 포인터 자체가 아니라 value에 대입합니다.

int value = 7;
int *p = &value;

*p = 9;

이 관계는 cppreference Pointer declaration의 포인터 선언 설명과 연결되며, 포인터 선언은 C23 6.7.6.1과 C17 6.7.6.1에서 다룹니다. 주소 연산자와 간접 참조 연산자는 C23/C17 6.5.3.2와 관련됩니다. NULL은 포인터가 어떤 객체나 함수도 가리키지 않는 상태를 나타내는 맥락에서 사용합니다.

이중 포인터는 언제 필요한가?

한 줄 답: 이중 포인터는 포인터를 가리키는 타입으로, 호출한 쪽의 포인터 값을 바꿔야 하는 out-parameter, 포인터 배열, 포인터로 연결한 구조에서 사용합니다.

T **T *의 주소를 받는 타입입니다. 따라서 pp를 한 번 역참조한 *ppT * 포인터 객체를 나타내고, 한 단계 더 따라가면 T 대상에 접근합니다.

int *p = NULL;
int **pp = &p;

이 예에서 ppp의 주소를 가리키므로, *pp에 새 포인터 값을 저장하면 호출한 쪽의 p를 갱신하는 관계가 됩니다. 동적 저장 공간을 재할당하여 주소가 바뀔 수 있는 작업을 호출한 쪽에 반영할 때 이런 out-parameter가 필요할 수 있습니다.

argv와 같은 문자열 포인터 배열에서는 char **가 포인터 배열을 표현하는 형태로 나타납니다. 노드가 다음 노드의 포인터를 멤버로 가지는 연결 구조에서도 링크 포인터를 바꾸기 위해 이중 포인터가 사용될 수 있습니다.

배열이 포인터로 바뀌는 array decay는 무엇인가?

한 줄 답: 배열 lvalue는 대부분의 식에서 첫 원소를 가리키는 포인터로 변환되지만, &, sizeof, C23의 typeof, 배열 초기화에 쓰이는 문자열 리터럴에서는 예외가 있습니다.

int a[2]와 같은 배열 객체가 일반 식에서 사용되면 배열 lvalue가 첫 원소를 가리키는 포인터로 변환됩니다. 이 변환을 array decay라고 부르지만, 배열 이름이 포인터 객체로 바뀌어 저장되는 것은 아닙니다.

변환은 모든 문맥에서 일어나지 않으며, 대표적인 예외는 다음과 같습니다.

문맥배열-포인터 변환
&a변환하지 않고 배열을 대상으로 주소 연산을 적용합니다.
sizeof a변환하지 않고 배열 자체의 크기를 계산합니다.
typeof(a) (C23)배열-포인터 변환의 예외에 해당합니다.
배열 초기화에 사용되는 문자열 리터럴배열 초기화 문맥이므로 변환 예외에 해당합니다.
int a[2] = {1, 2};
int *p = a;

함수 매개변수에서 int a[2]라는 배열 표기는 int *a와 같은 매개변수 타입으로 조정됩니다. 이때 [2]의 크기는 매개변수 타입의 일부가 아니므로, 원소 수가 필요하면 별도로 전달해야 합니다.

int a[2][3]에서 바깥 배열이 식에 사용되면 첫 원소의 타입은 int[3]이고, 변환 결과는 int (*)[3]입니다. 이는 int **가 아니며, int **int *를 가리키는 타입이기 때문입니다.

int a[2][3];
int (*row)[3] = a;

int *pint를 가리키고 int (*p)[N]은 N개 int 배열 한 행을 가리키므로 서로 다른 타입입니다. 배열 선언과 array-to-pointer conversion, 함수 매개변수 조정은 cppreference Array declaration에서 확인할 수 있으며 C23/C17 6.7.6.2, 6.3.2.1, 6.7.6.3과 연결됩니다.

구조체 포인터와 ->는 무엇을 하나?

한 줄 답: p->n은 구조체 포인터를 통해 멤버에 접근하는 표기이며 (*p).n과 같고, 구조체 자체는 배열처럼 포인터로 변환되지 않습니다.

.는 구조체 객체에서 멤버를 선택하는 연산자이고, ->는 구조체 포인터를 통해 멤버를 선택하는 연산자입니다. 따라서 p->np가 가리키는 구조체의 n 멤버에 접근하는 표기이며 (*p).n과 같습니다.

struct Item { int n; };
struct Item item = {0};
struct Item *p = &item;

p->n = 1;
(*p).n = 1;

구조체 자체는 배열처럼 식에서 첫 원소를 가리키는 포인터로 변환되지 않습니다. 다만 구조체 안의 배열 멤버가 식에서 사용되면 그 멤버는 배열이므로 해당 배열의 변환 규칙이 적용될 수 있습니다. 구조체 선언과 멤버 접근은 cppreference Struct declaration 및 C23/C17 6.5.2.3과 관련됩니다.

면접에서 자주 섞는 포인터 오해는 무엇인가?

한 줄 답: 배열 이름을 대입 가능한 포인터 객체로 보거나, 배열과 포인터의 sizeof, 2차원 배열의 타입을 같게 취급하는 오해를 타입으로 풀어야 합니다.

아래 표는 특정 시험의 기출이나 실제 출제 항목을 제시하는 것이 아니라, 선언과 식의 타입을 구분하기 위한 일반 설명입니다. 배열, 포인터, 포인터가 가리키는 대상의 관계를 각각 확인하면 오해를 줄일 수 있습니다.

섞기 쉬운 지점타입 기준의 구분
배열 이름을 포인터 객체로 취급배열 이름은 대입 가능한 포인터 객체가 아니며, 배열 자체에 다른 주소를 대입할 수 없습니다.
배열과 포인터의 sizeofsizeof가 배열 자체를 피연산자로 받으면 배열 전체 크기를 계산하고, 포인터를 피연산자로 받으면 포인터 객체의 크기를 계산합니다.
2차원 배열과 int **int a[2][3]의 배열-포인터 변환 결과는 int (*)[3]이며 int **가 아닙니다.
int *pint (*p)[N]int *pint를 가리키고 int (*p)[N]은 N개 int 배열 한 행을 가리키므로 서로 다른 타입입니다.

특히 함수 매개변수에서는 배열 표기가 포인터 타입으로 조정되므로 선언 위치를 확인해야 합니다. 반대로 일반 식의 sizeof&에서는 배열 변환 예외가 적용되므로, 같은 이름이 문맥마다 같은 타입으로 취급된다고 단정하면 안 됩니다.

FAQ에서 포인터는 무엇을 확인하나?

한 줄 답: 포인터의 정의, 이중 포인터의 용도, array decay 예외, 구조체 포인터, malloc·NULL의 최소 확인 항목을 점검합니다.

다음 표는 앞서 설명한 개념을 짧게 확인하는 일반 설명입니다. 특정 출제 항목, 문제 원문, 손코딩 문항을 전제로 하지 않습니다.

확인 항목짧은 답
포인터 정의포인터는 함수나 다른 객체를 가리키거나 아무것도 가리키지 않는 객체입니다. &는 포인터를 얻고 단항 *는 가리키는 객체의 lvalue를 얻습니다.
이중 포인터T **T *를 가리키며, 호출한 쪽 포인터를 갱신하는 out-parameter, 포인터 배열, 포인터로 연결한 구조에서 사용합니다.
array decay 예외(sizeof, &)배열 lvalue는 sizeof&의 피연산자일 때 첫 원소 포인터로 변환되지 않습니다. C23의 typeof와 배열 초기화에 쓰이는 문자열 리터럴도 예외입니다.
구조체 포인터p->n(*p).n과 같으며, .는 구조체 객체, ->는 구조체 포인터를 통한 멤버 접근에 사용합니다. 구조체 자체는 배열처럼 포인터로 변환되지 않습니다.
malloc·NULLmalloc은 필요한 크기의 저장 공간을 할당하고 실패하면 NULL을 반환합니다. 성공한 결과는 void *로 다룰 수 있으며, 동적 할당 저장 공간은 사용이 끝난 뒤 free로 해제합니다. cppreference malloc

관련 글은 무엇을 보면 되나?

한 줄 답: 관련 글은 C와 C++의 범위 차이, C-only 준비 범위, 포인터 손코딩이 언급되는 맥락을 보완하며 이 글의 타입 설명을 되풀이하지 않습니다.