타입
타입 (Type)
C 프로그램에서 객체, 함수, 표현식은 모두 **타입(type)**이라는 속성을 가져요. 타입은 객체에 저장된 값이나 표현식이 평가한 값의 이진 표현을 어떻게 해석할지를 정해요. 같은 바이트라도 타입에 따라 정수로 읽힐 수도, 부동소수점으로 읽힐 수도 있죠.
출처: cppreference
본문
타입 분류
C의 타입 시스템은 다음 타입들로 구성돼요.
void타입- 기본(basic) 타입
char타입- 부호 있는 정수 타입
- 표준:
signed char,short,int,long(C99부터long long) - (C23) 비트 정밀:
_BitInt(N).N은 타입을 표현하는 데 쓰는 비트 수(부호 비트 포함)를 지정하는 정수 상수 표현식이에요.N값마다 구별되는 타입을 지정해요. - (C99) 확장: 구현 정의, 예를 들어
__int128
- 표준:
- 부호 없는 정수 타입
- 표준: (C99부터
_Bool,)unsigned char,unsigned short,unsigned int,unsigned long(C99부터unsigned long long) - (C23) 비트 정밀:
unsigned _BitInt(N).N은 타입을 표현하는 데 쓰는 비트 수를 지정하는 정수 상수 표현식이에요.N값마다 구별되는 타입을 지정해요. 이 범주에는 대응하는 부호 있는 비트 정밀 정수 타입이 없는unsigned _BitInt(1)도 포함돼요. - (C99) 확장: 구현 정의, 예를 들어
__uint128
- 표준: (C99부터
- 부동소수점 타입
- 실수 부동소수점 타입:
float,double,long double - (C23) 십진 실수 부동소수점 타입:
_Decimal32,_Decimal64,_Decimal128 - (C99) 복소수 타입:
float _Complex,double _Complex,long double _Complex - (C99) 허수 타입:
float _Imaginary,double _Imaginary,long double _Imaginary
- 실수 부동소수점 타입:
- 열거형(enum) 타입
- 파생(derived) 타입
위 타입마다, 한정자가 허용하는 의미에 따라 const, volatile, restrict 한정자 중 하나·둘·셋을 조합한 여러 한정 버전이 존재할 수 있어요.
타입 그룹(Type groups)
- 객체 타입(object types): 함수 타입이 아닌 모든 타입
- 문자 타입(character types):
char,signed char,unsigned char - 정수 타입(integer types):
char, 부호 있는 정수 타입, 부호 없는 정수 타입, 열거형 타입 - 실수 타입(real types): 정수 타입과 실수 부동소수점 타입
- 산술 타입(arithmetic types): 정수 타입과 부동소수점 타입
- 스칼라 타입(scalar types): 산술 타입, 포인터 타입 (C23부터
nullptr_t) - 집합 타입(aggregate types): 배열 타입과 구조체 타입
- 파생 선언자 타입(derived declarator types): 배열 타입, 함수 타입, 포인터 타입
객체 표현의 바이트 수가 size_t 타입(sizeof 연산자의 결과 타입)으로 표현할 수 없는 완전 객체 타입을 만드는 것 — (C99부터) 런타임에 그런 VLA 타입을 형성하는 것까지 포함 — 은 정의되지 않은 동작이에요.
호환 타입 (Compatible types)
C 프로그램에서 **서로 다른 번역 단위(translation unit)**가 같은 객체나 함수를 가리키는 선언은 같은 타입을 쓸 필요가 없어요. 충분히 비슷한 타입 — 공식적으로 **호환 타입(compatible types)**이라 부르는 — 만 쓰면 돼요. 이는 함수 호출과 lvalue 접근에도 그대로 적용돼요. 인자 타입은 매개변수 타입과 호환되어야 하고, lvalue 표현식 타입은 접근하는 객체 타입과 호환되어야 해요.
타입 T와 U는 다음 중 하나라도 해당하면 호환돼요.
- 같은 타입일 때 (같은 이름, 또는
typedef가 도입한 같은 별칭) - 호환되는 무한정 타입의 동일한 cvr-한정 버전일 때
- 호환되는 타입을 가리키는 포인터 타입일 때
- 배열 타입이고
- 요소 타입이 호환되고,
- 둘 다 일정한 크기를 가지면 그 크기가 같을 때. 참고: 크기를 모르는 배열은 호환되는 요소 타입의 어떤 배열과도 호환돼요. (C99부터 VLA는 호환되는 요소 타입의 어떤 배열과도 호환돼요.)
- 둘 다 구조체/공용체/열거형 타입이고
- (C99) 하나가 태그로 선언되면 다른 것도 같은 태그로 선언돼야 해요.
- 둘 다 완성 타입이면, 그 멤버들이 개수에서 정확히 일치하고, 호환되는 타입으로 선언되며, 이름이 같아야 해요.
- 추가로 열거형이라면 대응하는 멤버의 값도 같아야 해요.
- 추가로 구조체나 공용체라면
- 대응하는 멤버가 같은 순서로 선언돼야 해요 (구조체만)
- 대응하는 비트 필드의 폭이 같아야 해요.
- 하나가 열거형 타입이고 다른 하나가 그 열거형의 기반 타입일 때
- 함수 타입이고
- 반환 타입이 호환되고,
- 둘 다 매개변수 목록을 쓰면, 매개변수 개수(생략 기호 사용 포함)가 같고, 배열-포인터·함수-포인터 타입 조정과 최상위 한정 제거를 적용한 뒤 대응하는 매개변수 타입이 호환될 때
- (C23 이전) 하나는 옛 스타일(매개변수 없는) 정의이고 다른 하나는 매개변수 목록을 가지며, 그 목록이 생략 기호를 쓰지 않고 각 매개변수가 (함수 매개변수 타입 조정 후) 기본 인자 승격 후의 대응하는 옛 스타일 매개변수와 호환될 때
- (C23 이전) 하나는 옛 스타일(매개변수 없는) 선언이고 다른 하나는 매개변수 목록을 가지며, 그 목록이 생략 기호를 쓰지 않고 모든 매개변수(함수 매개변수 타입 조정 후)가 기본 인자 승격의 영향을 받지 않을 때
char 타입은 signed char와도 호환되지 않고 unsigned char와도 호환되지 않아요.
두 선언이 같은 객체나 함수를 가리키는데 호환되지 않는 타입을 쓰면 프로그램의 동작은 정의되지 않아요.
// Translation Unit 1
struct S { int a; };
extern struct S *x; // compatible with TU2's x, but not with TU3's x
// Translation Unit 2
struct S;
extern struct S *x; // compatible with both x's
// Translation Unit 3
struct S { float a; };
extern struct S *x; // compatible with TU2's x, but not with TU1's x
// the behavior is undefined
// Translation Unit 1
#include <stdio.h>
struct s { int i; }; // compatible with TU3's s, but not TU2's
extern struct s x = {0}; // compatible with TU3's x
extern void f(void); // compatible with TU2's f
int main()
{
f();
return x.i;
}
// Translation Unit 2
struct s { float f; }; // compatible with TU4's s, but not TU1's s
extern struct s y = {3.14}; // compatible with TU4's y
void f() // compatible with TU1's f
{
return;
}
// Translation Unit 3
struct s { int i; }; // compatible with TU1's s, but not TU2's s
extern struct s x; // compatible with TU1's x
// Translation Unit 4
struct s { float f; }; // compatible with TU2's s, but not TU1's s
extern struct s y; // compatible with TU2's y
// the behavior is well-defined: only multiple declarations
// of objects and functions must have compatible types, not the types themselves
참고: C++에는 호환 타입이라는 개념이 없어요. 서로 다른 번역 단위에서 호환되지만 동일하지는 않은 두 타입을 선언하는 C 프로그램은 유효한 C++ 프로그램이 아니에요.
복합 타입 (Composite types)
호환되는 두 타입에서 **복합 타입(composite type)**을 만들 수 있어요. 복합 타입은 두 타입 모두와 호환되고 다음 조건을 만족하는 타입이에요.
- 둘 다 배열 타입이면 다음 규칙이 적용돼요.
- 한 타입이 알려진 일정한 크기의 배열이면, 복합 타입은 그 크기의 배열이에요.
- (C99부터) 그 외에, 한 타입이 평가되지 않는 표현식으로 크기가 지정된 VLA라면, 두 타입의 복합 타입이 필요한 프로그램은 정의되지 않은 동작이에요.
- (C99부터) 그 외에, 한 타입이 크기가 지정된 VLA이면, 복합 타입은 그 크기의 VLA예요.
- (C99부터) 그 외에, 한 타입이 크기가 미지정인 VLA이면, 복합 타입은 크기가 미지정인 VLA예요.
- 그 외에는, 둘 다 크기를 모르는 배열이고 복합 타입도 크기를 모르는 배열이에요.
- 복합 타입의 요소 타입은 두 요소 타입의 복합 타입이에요.
- (C23 이전) 한 타입만 매개변수 타입 목록이 있는 함수 타입(함수 프로토타입)이면, 복합 타입은 그 매개변수 타입 목록을 가진 함수 프로토타입이에요.
- 둘 다 매개변수 타입 목록이 있는 함수 타입이면, 복합 매개변수 타입 목록의 각 매개변수 타입은 대응하는 매개변수의 복합 타입이에요.
이 규칙은 두 타입이 파생된 타입들에 재귀적으로 적용돼요.
// Given the following two file scope declarations:
int f(int (*)(), double (*)[3]);
int f(int (*)(char *), double (*)[]); // C23: Error: conflicting types for 'f'
// The resulting composite type for the function is:
int f(int (*)(char *), double (*)[3]);
내부 또는 외부 링키지를 가진 식별자가, 그 식별자의 이전 선언이 보이는 스코프에서 선언될 때, 이전 선언이 내부·외부 링키지를 지정한다면 나중 선언에서 그 식별자의 타입은 복합 타입이 돼요.
불완전 타입 (Incomplete types)
**불완전 타입(incomplete type)**은 그 타입 객체의 크기를 결정하기에 충분한 정보가 없는 객체 타입이에요. 불완전 타입은 번역 단위의 어느 시점에 완성될 수 있어요.
다음 타입들은 불완전해요.
-
void타입. 이 타입은 완성될 수 없어요. -
크기를 모르는 배열 타입. 크기를 지정하는 나중 선언으로 완성될 수 있어요.
extern char a[]; // the type of a is incomplete (this typically appears in a header) char a[10]; // the type of a is now complete (this typically appears in a source file) -
내용을 모르는 구조체·공용체 타입. 같은 스코프에서 나중에 내용을 정의하는 같은 구조체·공용체의 선언으로 완성될 수 있어요.
struct node { struct node* next; // struct node is incomplete at this point }; // struct node is complete at this point
타입 이름 (Type names)
선언이 아닌 문맥에서도 타입에 이름을 붙여야 할 때가 있어요. 그럴 때 **타입 이름(type name)**을 써요. 문법적으로 이는 이 타입의 객체나 함수 하나를 선언할 때 쓰는 것과 똑같은 '타입 지정자'·'타입 한정자' 목록 뒤에 '선언자'가 오는 형태인데, 식별자만 빠진 거예요.
int n; // declaration of an int
sizeof(int); // use of type name
int *a[3]; // declaration of an array of 3 pointers to int
sizeof(int *[3]); // use of type name
int (*p)[3]; // declaration of a pointer to array of 3 int
sizeof(int (*)[3]); // use of type name
int (*a)[*] // declaration of pointer to VLA (in a function parameter)
sizeof(int (*)[*]) // use of type name (in a function parameter)
int *f(void); // declaration of function
sizeof(int *(void)); // use of type name
int (*p)(void); // declaration of pointer to function
sizeof(int (*)(void)); // use of type name
int (*const a[])(unsigned int, ...) = {0}; // array of pointers to functions
sizeof(int (*const [])(unsigned int, ...)); // use of type name
다만 식별자 주변의 중복 괄호는 타입 이름에서 의미가 있어요. "매개변수 지정이 없는 함수"를 나타내요.
int (n); // declares n of type int
sizeof(int ()); // uses type "function returning int"
타입 이름은 다음 상황에서 쓰여요.
- 캐스트(
cast) sizeof- (C99) 복합 리터럴(compound literal)
- (C11) 일반 선택(generic selection)
- (C11)
_Alignof(C23부터alignof) - (C11)
_Alignas(C23부터alignas) - (C11)
_Atomic(타입 지정자로 쓰일 때)
타입 이름은 새 타입을 도입할 수도 있어요.
void* p = (void*)(struct X { int i; } *)0;
// type name "struct X {int i;}*" used in the cast expression
// introduces the new type "struct X"
struct X x = {1}; // struct X is now in scope
더 알아보기
- 대부분의 내장 타입에 대한 세부 사항은 산술 타입(arithmetic types) 문서에서 다뤄요.
- C 라이브러리가 제공하는 타입 관련 유틸리티 목록은 c/types를 보세요.
- C++의 type 문서와 비교하면, "호환 타입"이라는 개념이 C++에는 없다는 차이가 특히 흥미로워요.