타입

타입 (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
    • 부동소수점 타입
      • 실수 부동소수점 타입: 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 표현식 타입은 접근하는 객체 타입과 호환되어야 해요.

타입 TU는 다음 중 하나라도 해당하면 호환돼요.

  • 같은 타입일 때 (같은 이름, 또는 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++에는 없다는 차이가 특히 흥미로워요.