Menu

C 언어 구조체 포인터: 화살표 연산자와 힙 구조체

C에서 구조체를 가리키는 법 - 화살표 연산자, (*p).x에 괄호가 필요한 이유, 변경하거나 복사를 피하려고 포인터로 구조체 넘기기, malloc으로 구조체 할당하기, 그리고 연결 리스트 노드 만들기.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

함수에 넘긴 구조체는 복사됩니다. int 둘짜리 점에는 괜찮지만 200바이트짜리 레코드에는 낭비이고, 게다가 수정이 불가능해집니다. 함수는 자기 사본만 보니까요. 구조체를 가리키면 두 문제가 한꺼번에 풀리고, 커져 가는 구조 - 리스트, 트리, 그래프 - 를 만드는 유일한 방법이기도 합니다.

구조체의 포인터 얻기

특별한 것은 없습니다. &가 주소를 주고, 타입은 "구조체를 가리키는 포인터"입니다.

셋 다 3을 출력합니다. 뒤의 둘은 같은 연산을 두 가지로 쓴 것입니다.

(*ptr).x에 괄호가 필요한 이유

괄호를 빼고 *ptr.x라고 쓰고 싶을 수 있습니다. 그것은 꽤 다른 것으로 컴파일되거나, 더 자주는 아예 컴파일을 거부합니다. 멤버 연산자 .이 역참조 *보다 우선순위가 높기 때문입니다.

*ptr.x        // *(ptr.x)로 파싱된다 -- 틀림
(*ptr).x      // 먼저 역참조하고, 그다음 멤버를 취한다 -- 맞음
ptr->x        // 같은 것을, 짧게 말한 것

ptr.x포인터의 멤버를 요구하는데, 포인터에는 멤버가 없습니다. 컴파일러의 불평("구조체나 공용체가 아닌 것에서 멤버 'x'를 요청함")은 변장한 우선순위 버그입니다.

괄호 친 형태는 보기 흉하고 틀리기도 쉬워서, C는 ->를 제공합니다. 실제로 진짜 코드에서 (*p).x를 보는 일은 거의 없습니다. p->x에 손을 뻗고, 긴 형태는 화살표가 무엇을 뜻하는지에 대한 설명으로만 기억하세요.

포인터로 구조체 넘기기

포인터 매개변수는 함수에 호출자의 실제 구조체를 주므로, 변경이 남습니다.

여기서 들일 만한 습관이 둘 있습니다.

  • 읽기 전용 포인터는 const로 표시하세요. const struct Point *p는 함수가 p가 가리키는 것을 바꾸지 않겠다고 약속합니다. 나중에 누군가 안에 p->x = 0을 넣으면, 버그가 출하되는 대신 컴파일이 실패합니다. 호출 지점에서 의도를 문서화하기도 합니다.
  • 큰 구조체는 읽을 때도 포인터로 넘기세요. 64바이트 이름 버퍼가 있는 구조체를 값으로 넘기면 호출마다 64바이트를 복사하지만, 포인터는 8바이트입니다.

작은 구조체는 값으로 넘겨도 괜찮습니다. 8바이트짜리 struct Point는 포인터만큼 싸게 복사되고, 값 형태는 NULL이 될 수 없어서 실패 경로 하나가 통째로 사라집니다.

구조체 배열 안의 포인터

포인터 산술은 int 배열에서와 똑같이 구조체 배열에도 동작합니다. p + 1은 패딩을 포함해 구조체 하나만큼 전진합니다.

배열 이름은 첫 원소를 가리키는 포인터로 붕괴하므로, 호출 시점에 staff는 이미 struct Employee *입니다. 함수가 n을 따로 받아야 하는 이유도 그것입니다. 함수 안의 sizeof는 배열이 아니라 포인터를 잽니다.

힙에 구조체 할당하기

스택 구조체는 스코프가 끝나면 죽습니다. 자신을 만든 함수보다 오래 사는 구조체를 만들려면 - 또는 입력이 요구하는 만큼 많이 만들려면 - malloc으로 할당하세요.

이 작은 함수 안의 네 가지 세부 사항은 모두 하중을 견디고 있습니다.

  • sizeof(struct Employee)가 아니라 sizeof *e. "e가 가리키는 것의 크기"로 읽히므로, 타입이 바뀌면 할당도 자동으로 따라옵니다. 둘이 어긋날 방법이 없습니다.
  • NULL을 확인하세요. malloc은 요청을 들어줄 수 없으면 NULL을 반환합니다. 널 포인터로 e->id에 쓰는 것은 세그멘테이션 폴트입니다.
  • 모든 멤버를 초기화하세요. malloc은 메모리를 0으로 만들지 않습니다. 구조체는 쓰레기 값으로 가득 찬 채 도착합니다. (calloc(1, sizeof *e)가 대신 0으로 채워 줍니다.)
  • 누군가는 free 해야 합니다. 할당된 포인터를 반환하는 것은 그 의무를 호출자에게 넘기는 일입니다. 주석에 그렇다고 적어 두세요. 불분명한 소유권 규칙이 메모리 누수가 시작되는 방식입니다.

보상: 연결 리스트 노드

포인터 없이는 구조체가 할 수 없는 일이 여기 있습니다. 구조체는 자기 자신을 담을 수 없습니다. 무한히 커질 테니까요. 하지만 자기와 같은 종류를 가리키는 포인터는 담을 수 있고, 그 요령 하나가 C의 모든 연결 자료구조를 만듭니다.

struct Node *next;struct Node가 아직 선언되는 중에 struct Node를 가리킨다는 점을 보세요. 포인터는 무엇을 가리키든 크기가 알려져 있기 때문에 이것이 합법입니다. 컴파일러는 아직 완전한 정의가 필요 없습니다. typedef를 함께 썼더라도 struct Node 태그를 반드시 써야 하는 유일한 자리이기도 합니다. typedef 이름은 자기 정의 안에서는 아직 존재하지 않으니까요.

free_list 반복문은 free(head)를 호출하기 전에 head->next를 저장합니다. 해제 뒤에 head->next를 읽는 것은 해제 후 사용입니다. 그 메모리는 더 이상 여러분 것이 아니고, 읽는 값은 할당자가 거기에 넣어 둔 무엇입니다.

흔한 실수

  • 초기화되지 않았거나 NULL인 포인터에 p->x. 구조체를 많이 쓰는 코드에서 크래시의 가장 흔한 원인입니다. 포인터를 NULL로 초기화하고 역참조 전에 확인하세요.
  • 지역 구조체의 포인터 반환. struct Point *bad(void) { struct Point p = {1,2}; return &p; }는 함수가 반환하는 순간 존재하기를 멈추는 메모리의 주소를 반환합니다. 구조체를 값으로 반환하거나 할당하세요.
  • 두 번 해제하거나 해제를 잊음.malloc은 정확히 하나의 free와 짝을 이룹니다. 해제한 뒤에는 포인터를 NULL로 두어, 나중에 실수로 쓰는 일이 메모리를 조용히 망가뜨리는 대신 요란하게 죽게 하세요.
  • 포인터를 담은 구조체를 복사. 구조체 대입은 포인터가 가리키는 대상이 아니라 포인터 을 복사합니다. 이제 두 구조체가 같은 버퍼를 참조하고, 먼저 해제한 쪽이 다른 쪽을 댕글링 상태로 남깁니다.

자주 묻는 질문

C에서 -> 연산자는 무슨 일을 하나요?

p->x는 포인터 p를 통해 멤버 x에 접근합니다. (*p).x와 정확히 같습니다. 포인터를 역참조한 뒤 멤버를 취하는 것이죠. 구조체 포인터가 워낙 흔해서 긴 형태가 코드를 어지럽히기 때문에 화살표가 존재합니다.

구조체 포인터에 *p.x가 왜 안 되나요?

.*보다 강하게 묶이기 때문입니다. *p.x*(p.x)로 파싱됩니다. 구조체가 아니라 포인터인 p의 멤버 x를 취해 그것을 역참조하라는 뜻이죠. (*p).x라고 쓰거나, 그냥 p->x라고 쓰세요.

C에서 구조체를 값으로 넘겨야 하나요, 포인터로 넘겨야 하나요?

함수가 호출자의 구조체를 수정해야 하거나 구조체가 커서 복사가 아까울 때는 포인터로 넘기세요. 복사가 싸고 코드가 더 단순한 작은 읽기 전용 구조체는 값으로 넘기세요. 읽기 전용 포인터 매개변수는 const struct T *로 표시해 컴파일러가 약속을 강제하게 하세요.

C에서 malloc으로 구조체를 어떻게 할당하나요?

struct Node *n = malloc(sizeof *n);입니다. sizeof *n 형태는 n이 가리키는 것의 크기를 묻기 때문에, 나중에 타입을 바꿔도 여전히 올바릅니다. 결과를 NULL과 비교해 확인하고, 모든 멤버를 초기화하고, 다 쓰면 free(n) 하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기