배열은 한 타입의 값을 여럿 담습니다. struct는 정반대입니다. 서로 다른 타입의 값 몇 개를 담고 그것을 하나로 다룹니다. 점은 x와 y입니다. 직원은 이름과 사번과 급여입니다. 구조체가 없다면 그것들을 흩어진 변수 셋으로 들고 다니면서 누구의 급여인지 뒤섞이지 않기를 바라야 합니다.
구조체 타입 선언하기
구조체 선언은 태그에 이름을 붙이고 중괄호 안에 멤버를 나열합니다.
struct Point {
int x;
int y;
};
닫는 중괄호 뒤의 세미콜론은 필수입니다. 빠뜨리는 것은 C 입문자가 만나는 가장 헷갈리는 오류 메시지 중 하나인데, 컴파일러가 다음 줄을 탓하기 때문입니다.
이것은 변수가 아니라 타입을 선언합니다. C에서 그 타입의 완전한 이름은 키워드를 포함한 struct Point입니다.
멤버는 점 연산자 p.x로 읽고 씁니다. 구조체 변수는 평범한 변수입니다. 스택에 살고, 같은 타입의 다른 구조체에 =로 대입할 수 있으며, 스코프가 끝나면 사라집니다.
어디서나 struct Point라고 쓰는 것은 지겨운 일이고, 그래서 실제 C 코드 대부분이 선언을 typedef로 감싸 타입을 그냥 Point로 만듭니다.
구조체 초기화하기
선언하는 그 자리에서 구조체를 채울 수 있습니다. 위치 형태는 선언 순서대로 값을 나열합니다.
struct Point p = {3, 4}; // x = 3, y = 4
지정 초기화자는 대신 각 멤버의 이름을 밝히는데, 더 명확하고 안전합니다.
눈여겨볼 것이 셋 있습니다.
- 지정 형태는 멤버를 어떤 순서로든 나열할 수 있으므로, 나중에 멤버를 추가하거나 순서를 바꿔도 값이 조용히 엉뚱한 자리로 밀려나지 않습니다.
- 빠뜨린 멤버는 0으로 초기화됩니다.
{0}은 구조체 전체를 0으로 만드는 관용적인 방법입니다. char name[32]멤버는 구조체 안의 진짜 배열이므로, 문자열은 다른 어딘가가 아니라 구조체 자체에 저장됩니다. 그래서 나중에a.name = "Ada";라고 하는 것은 불법이고,strcpy가 필요합니다(문자열 함수 참고).
초기화자가 없는 구조체는 초기화되지 않은 상태이고, 멤버에 쓰기 전에 읽는 것은 미정의 동작입니다. = {0}은 비용이 들지 않으면서 이 부류의 버그를 통째로 없앱니다.
대입과 비교
구조체 대입은 모든 멤버를 복사합니다.
a는 그대로입니다. b가 자기 사본을 가졌습니다. 하지만 C가 주지 않는 것을 보세요. 구조체를 위한 ==는 없습니다. if (a == b)는 컴파일되지 않습니다. 비교한다는 것은 멤버를 비교한다는 뜻입니다.
if (a.x == b.x && a.y == b.y) { /* 같음 */ }
지름길로 memcmp에 손대지 마세요. 컴파일러는 정렬을 위해 멤버 사이에 눈에 보이지 않는 패딩 바이트를 넣고, 그 바이트에는 쓰레기 값이 들어 있을 수 있어서, 멤버가 똑같은 구조체 둘이 바이트 단위로는 다르게 비교될 수 있습니다.
구조체 배열
구조체도 다른 것과 같은 타입이므로 그것들의 배열을 만들 수 있습니다. 레코드 표를 담는 표준적인 방법입니다.
staff[i].salary는 "i번째 원소, 그다음 그것의 salary 멤버"로 읽힙니다. 인덱싱이 점보다 강하게 묶이므로 괄호가 필요 없습니다.
구조체는 함수로 복사되어 들어간다
사람들을 걸어 넘어뜨리는 규칙이 이것입니다. 구조체를 함수에 넘기면 C가 그것을 복사합니다. 함수는 자기만의 사본을 가지고 일합니다.
p는 여전히 (3, 4)입니다. distance_from_origin처럼 구조체를 값으로 읽는 것은 전혀 문제가 없고, 작은 구조체에는 종종 가장 명확한 선택입니다. 하지만 함수가 호출자의 구조체를 바꿔야 하거나, 구조체가 복사가 아까울 만큼 크다면 대신 포인터를 넘기세요. 구조체와 포인터의 주제입니다.
구조체를 값으로 반환하는 것도 같은 방식으로 동작하고 완전히 합법입니다.
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
중첩 구조체
구조체의 멤버가 그 자체로 구조체일 수 있습니다. 점을 이어 안쪽으로 들어갑니다.
b.published.year는 한 단계씩 안으로 걸어 들어갑니다. 중첩은 C에서 실제 데이터 모델을 세우는 방법이고, 안쪽 구조체는 가리켜지는 것이 아니라 바깥쪽 안에 저장되므로 Book 전체가 연속된 메모리 덩어리 하나입니다.
크기, 패딩, 멤버 순서
sizeof는 구조체가 얼마나 큰지 알려 주는데, 답이 멤버의 합보다 클 때가 많습니다.
보통의 기계에서 Wasteful은 12바이트이고 Tidy는 8바이트입니다. 둘 다 같은 멤버 셋을 담는데도 말이죠. 컴파일러는 각 멤버가 자기 타입이 좋아하는 주소에 앉도록 패딩을 넣습니다. int는 보통 4의 배수를 원합니다. 큰 멤버를 작은 멤버보다 앞에 모으면 대체로 더 빽빽하게 들어갑니다.
이것이 중요한 경우는 드물고(백만 개짜리 배열, 네트워크 패킷 배치) 여러분 코드의 정확성에는 절대 영향을 주지 않습니다. 다만 sizeof가 왜 놀라운 답을 주는지, 그리고 구조체에 memcmp를 쓰는 것이 왜 나쁜 생각인지를 설명해 줍니다.
자주 묻는 질문
C에서 구조체는 어떻게 선언하나요?
struct 뒤에 태그 이름과 중괄호로 감싼 멤버 목록을 씁니다. struct Point { int x; int y; };처럼요. 이것은 변수가 아니라 타입을 선언합니다. 변수를 만들려면 struct Point p;라고 써야 합니다. C에서는 struct라는 낱말이 타입 이름의 일부이고, 그래서 대부분의 코드가 typedef와 짝지어 씁니다.
구조체에서 .와 ->의 차이는 무엇인가요?
.와 ->의 차이는 무엇인가요?구조체 자체를 가지고 있을 때는 .(p.x)를, 그것을 가리키는 포인터를 가지고 있을 때는 ->(ptr->x)를 씁니다. 화살표는 (*ptr).x의 축약입니다. 자세한 내용은 구조체와 포인터를 보세요.
C에서 구조체는 값으로 전달되나요, 참조로 전달되나요?
항상 값으로 전달됩니다. 구조체를 함수에 넘기면 모든 멤버가 복사되므로, 함수 안의 변경은 호출자의 변수에 영향을 주지 않습니다. 원본을 바꾸려면 - 또는 큰 구조체의 복사를 피하려면 - 대신 포인터를 넘기세요.
C에서 구조체를 어떻게 초기화하나요?
위치로 struct Point p = {3, 4};라고 쓰거나, 지정 초기화자로 struct Point p = {.y = 4, .x = 3};라고 씁니다. 지정 형태는 각 멤버의 이름을 밝히므로 나중에 누군가 구조체 순서를 바꿔도 살아남고, 이름을 대지 않은 멤버는 0으로 둡니다.