암시적 변환
암시적 변환 (Implicit conversions)
표현식이 쓰인 문맥이 다른 타입의 값을 기대할 때, C는 알아서 값을 변환(conversion) 해 주는 경우가 많아요. 이걸 한마디로 "컴파일러가 자연스럽게 타입을 맞춰 준다"고만 알고 넘어가면 금방 어긋나요. 이 페이지에서 언제 어떤 변환이 일어나고, 그 규칙이 정확히 무엇인지 차근차근 살펴볼게요.
출처: cppreference
본문
표현식이 다른 타입의 값을 기대하는 문맥에서 쓰일 때 변환(conversion) 이 일어날 수 있어요.
int n = 1L; // 표현식 1L은 long 타입, int가 기대됨
n = 2.1; // 표현식 2.1은 double 타입, int가 기대됨
char* p = malloc(10); // 표현식 malloc(10)은 void* 타입, char*가 기대됨
변환은 다음 상황에서 일어나요.
대입에 의한 것과 같은 변환 (Conversion as if by assignment)
- 대입 연산자에서 오른쪽 피연산자의 값은 왼쪽 피연산자의 한정되지 않은 타입으로 변환돼요.
- 스칼라 초기화에서 초기화자 표현식의 값은 초기화되는 객체의 한정되지 않은 타입으로 변환돼요.
- 원형(prototype)을 가진 함수에 대한 함수 호출 표현식에서 각 인자 표현식의 값은 해당 매개변수의 한정되지 않은 선언 타입으로 변환돼요.
- return 문에서
return의 피연산자 값은 함수의 반환 타입을 가진 객체로 변환돼요.
실제 대입은 변환 외에도 부동소수점 타입에서 여분의 범위와 정밀도를 제거하고 겹침을 금지한다는 점에 주의하세요. 이런 특성은 "대입에 의한 것과 같은" 변환에는 적용되지 않아요.
기본 인자 승격 (Default argument promotions)
함수 호출 표현식에서 호출이 다음에 대해 이루어질 때: 정수 타입의 각 인자는 정수 승격을 겪고, float 타입의 각 인자는 double 타입으로 암시적으로 변환돼요.
int add_nums(int count, ...);
int sum = add_nums(2, 'c', true); // add_nums는 세 개의 int로 호출됨: (2, 99, 1)
| float complex와 float imaginary는 이 문맥에서 double complex와 double imaginary로 승격되지 않는다는 점에 주의하세요. | (since C99) |
통상적인 산술 변환 (Usual arithmetic conversions)
다음 산술 연산자의 인자는 공통 실 타입(common real type) 을 얻기 위해 암시적 변환을 겪어요. 그 공통 실 타입이 계산이 수행되는 타입이에요.
1) 한 피연산자가 십진 부동 타입이면, 다른 피연산자는 표준 부동·복소수·허수 타입을 가져서는 안 돼요. 먼저 어느 피연산자의 타입이 _Decimal128이면 다른 피연산자가 _Decimal128로 변환돼요. 그렇지 않고 어느 피연산자의 타입이 _Decimal64이면 다른 피연산자가 _Decimal64로 변환돼요. 그렇지 않고 어느 피연산자의 타입이 _Decimal32이면 다른 피연산자가 _Decimal32로 변환돼요. |
(since C23) |
long double, long double complex, long double imaginary
| complex 타입은 long double complex로, imaginary 타입은 long double imaginary로 | (since C99) |
double, double complex, double imaginary
| complex 타입은 double complex로, imaginary 타입은 double imaginary로 | (since C99) |
float, float complex, float imaginary
| complex 타입은 float complex로 유지, imaginary 타입은 float imaginary로 유지 | (since C99) |
- ↑ 순위 규칙은 아래 정수 승격을 참조하세요.
- ↑ 아래 "암시적 변환 의미론"의 "정수 변환"을 참조하세요.
1.f + 20000001; // int는 float로 변환되어 20000000.00이 됨
// 더한 뒤 float로 반올림하여 20000000.00이 됨
(char)'a' + 1L; // 먼저 char 'a'(값 97)는 int로 승격
// 다른 타입: int와 long
// 같은 부호: 둘 다 signed
// 다른 순위: long이 int보다 순위가 높음
// 따라서 int 97은 long 97로 변환
// 결과는 signed long 타입의 97 + 1 = 98
2u - 10; // 다른 타입: unsigned int와 signed int
// 다른 부호
// 같은 순위
// 따라서 signed int 10은 unsigned int 10으로 변환
// 산술 연산이 unsigned 정수에 대해 수행되므로
// (참고: "Arithmetic operators" 항목), 수행되는 계산은
// (2 - 10) modulo (2의 n승), 여기서 n은 unsigned int의 값 비트 수
// unsigned int가 32비트이고 객체 표현에 패딩 비트가 없으면,
// 결과는 (-8) modulo (2의 32승) = 4294967288, unsigned int 타입
5UL - 2ULL; // 다른 타입: unsigned long과 unsigned long long
// 같은 부호
// 다른 순위: unsigned long long의 순위가 더 높음
// 따라서 unsigned long 5는 unsigned long long 5로 변환
// 산술 연산이 unsigned 정수에 대해 수행되므로
// (참고: "Arithmetic operators" 항목),
// unsigned long long이 64비트라면,
// 결과는 (5 - 2) modulo (2의 64승) = 3, unsigned long long 타입
0UL - 1LL; // 다른 타입: unsigned long과 signed long long
// 다른 부호
// 다른 순위: signed long long의 순위가 더 높음
// ULONG_MAX > LLONG_MAX라면, signed long long은 모든
// unsigned long을 표현할 수 없으므로, 이 경우는 마지막 경우:
// 두 피연산자가 모두 unsigned long long으로 변환됨
// unsigned long 0은 unsigned long long 0으로 변환
// long long 1은 unsigned long long 1로 변환
// 산술 연산이 unsigned 정수에 대해 수행되므로
// (참고: "Arithmetic operators" 항목),
// unsigned long long이 64비트라면,
// 계산은 (0 - 1) modulo (2의 64승)
// 따라서 결과는 18446744073709551615 (ULLONG_MAX), unsigned long long 타입
결과 타입은 다음과 같이 결정돼요: 두 피연산자가 모두 complex면 결과 타입은 complex, 둘 다 imaginary면 imaginary, 둘 다 real이면 real. 두 부동소수점 피연산자가 서로 다른 타입 영역(complex vs real, complex vs imaginary, imaginary vs real)을 가지면 결과 타입은 complex예요. double complex z = 1 + 2 * I; double f = 3.0; z + f; // z는 그대로, f는 double로 변환, 결과는 double complex |
(since C99) |
언제나 그렇듯, 부동소수점 연산자의 결과는 그 타입이 나타내는 것보다 더 큰 범위와 정밀도를 가질 수 있어요(FLT_EVAL_METHOD 참조).
참고: 실(real)과 허수(imaginary) 피연산자는 추가 계산이 필요하고 무한·NaN·부호 있는 0을 포함하는 특정 경우에 바람직하지 않은 결과를 낳기 때문에 complex로 암시적으로 변환되지 않아요. 예를 들어 실수를 complex로 변환하면 2.0×(3.0+i∞)는 올바른 6.0+i∞ 대신 (2.0+i0.0)×(3.0+i∞) ⇒ (2.0×3.0–0.0×∞) + i(2.0×∞+0.0×3.0) ⇒ NaN+i∞로 평가돼요. 허수를 complex로 변환하면 i2.0×(∞+i3.0)은 -6.0 + i∞ 대신 (0.0+i2.0) × (∞+i3.0) ⇒ (0.0×∞ – 2.0×3.0) + i(0.0×3.0 + 2.0×∞) ⇒ NaN + i∞로 평가돼요. |
(since C99) |
참고: 통상적인 산술 변환과 무관하게, 계산은 as-if 규칙에 따라 항상 이 규칙들이 지정하는 것보다 좁은 타입으로 수행될 수 있어요.
값 변환 (Value transformations)
lvalue 변환 (Lvalue conversion)
비-배열 타입의 어떤 lvalue 표현식도, 다음을 제외한 어떤 문맥에서 쓰일 때:
- 주소 연산자의 피연산자(허용될 때)로 쓰일 때
- 전위/후위 증가·감소 연산자의 피연산자로 쓰일 때
- 멤버 접근(점) 연산자의 왼쪽 피연산자로 쓰일 때
- 대입 및 복합 대입 연산자의 왼쪽 피연산자로 쓰일 때
- sizeof의 피연산자로 쓰일 때
lvalue 변환 을 겪어요: 타입은 같게 유지되지만 const/volatile/restrict 한정자와 원자(atomic) 속성(있다면)을 잃어요. 값은 같게 유지되지만 lvalue 속성은 잃어요(주소를 더는 취할 수 없어요).
lvalue가 불완전 타입이면 동작은 정의되지 않아요.
lvalue가 주소를 한 번도 취하지 않은 자동 저장 기간 객체를 지정하고, 그 객체가 초기화되지 않았으며(초기화자로 선언되지 않고 사용 전 대입도 수행되지 않음) 사용된다면 동작은 정의되지 않아요.
이 변환은 객체의 위치에서 그 값의 메모리 로드를 모델링해요.
volatile int n = 1;
int x = n; // n에 대한 lvalue 변환은 n의 값을 읽음
volatile int* p = &n; // lvalue 변환 없음: n의 값을 읽지 않음
배열-포인터 변환 (Array to pointer conversion)
배열 타입의 어떤 lvalue 표현식(until C99) 표현식(since C99)도, 다음을 제외한 어떤 문맥에서 쓰일 때:
- 주소 연산자의 피연산자로 쓰일 때
- sizeof의 피연산자로 쓰일 때
- typeof·typeof_unqual(since C23)의 피연산자로 쓰일 때
- 배열 초기화에 쓰이는 문자열 리터럴로 쓰일 때
그 첫 요소를 가리키는 비-lvalue 포인터로의 변환을 겪어요.
배열이 register로 선언됐다면 동작은 정의되지 않아요.
비-lvalue 배열이나 그 요소는 접근할 수 없고(until C99), 임시 수명을 가져요(since C99).
int a[3], b[3][4];
int* p = a; /* &a[0]으로의 변환 */
int (*q)[4] = b; /* &b[0]으로의 변환 */
struct S
{
int a[1];
};
struct S f(void)
{
struct S result = {{0}}; /* C99부터 {0} */
return result;
}
void g(void)
{
int* p = f().a; /* C99까지 오류; C99부터 OK */
int n = f().a[0]; /* C99까지 오류; C99부터 OK */
f().a[0] = 13; /* C99까지 오류; C99부터 UB */
(void)p, (void)n;
}
int main(void) { return 0; }
함수-포인터 변환 (Function to pointer conversion)
어떤 함수 지정자 표현식도, 다음을 제외한 어떤 문맥에서 쓰일 때:
- 주소 연산자의 피연산자로 쓰일 때
- sizeof의 피연산자로 쓰일 때
- typeof·typeof_unqual(since C23)의 피연산자로 쓰일 때
그 표현식이 지정하는 함수를 가리키는 비-lvalue 포인터로의 변환을 겪어요.
int f(int);
int (*p)(int) = f; // &f로의 변환
(***p)(1); // f로의 반복 역참조와 &f로의 재변환
암시적 변환 의미론 (Implicit conversion semantics)
암시적 변환은 대입에 의한 것과 같은 것인지 통상적인 산술 변환 인지와 무관하게 두 단계로 구성돼요.
호환 타입 (Compatible types)
어떤 타입의 값을 호환 타입으로 변환하는 것은 항상 no-op이고 표현을 바꾸지 않아요.
uint8_t (*a)[10]; // uint8_t가 unsigned char의 typedef라면
unsigned char (*b)[] = a; // 이 포인터 타입들은 호환
정수 승격 (Integer promotions)
정수 승격은 int보다 작거나 같은 순위(rank) 를 가진 어떤 정수 타입, 또는 _Bool(until C23) / bool(since C23), int, signed int, unsigned int 타입의 비트 필드의 값을 int 또는 unsigned int 타입의 값으로 변환하는 암시적 변환이에요.
int가 원래 타입의 전체 값 범위(또는 원래 비트 필드의 값 범위)를 표현할 수 있으면 그 값은 int 타입으로 변환돼요. 그렇지 않으면 unsigned int로 변환돼요.
| 비트 정밀 정수 타입의 비트 필드의 값은 그에 대응하는 비트 정밀 정수 타입으로 변환돼요. 그 외에는 비트 정밀 정수 타입은 정수 승격 규칙에서 제외돼요. | (since C23) |
정수 승격은 부호를 포함해 값을 보존해요.
int main(void)
{
void f(); // 옛 스타일 함수 선언
// C23부터 void f(...)는 승격에 대해 같은 동작
char x = 'a'; // int에서 char로의 정수 변환
f(x); // char에서 int로의 정수 승격
}
void f(x) int x; {} // 함수는 int를 기대
위의 순위(rank) 는 모든 정수 타입의 속성이에요. 순위는 타입의 폭(더 넓은 타입이 더 높은 순위)에 따라 대략적으로: signed char < short < int < long int < long long int 순이고, char·signed char·unsigned char는 같은 순위예요(확장 정수 타입은 폭에 따라 삽입돼요).
| 8) 비트 정밀 부호 있는 정수 타입의 순위는 더 작은 폭의 어떤 표준 정수 타입이나 더 작은 폭의 어떤 비트 정밀 정수 타입의 순위보다 커야 해요. 9) 어떤 비트 정밀 정수 타입의, 같은 폭의 확장 정수 타입에 대한 상대적 순위는 구현 정의예요. | (since C23) |
참고: 정수 승격은 다음에서만 적용돼요.
- 통상적인 산술 변환의 일부로 (위 참조)
- 기본 인자 승격의 일부로 (위 참조)
- 단항 산술 연산자
+와-의 피연산자에 - 단항 비트 연산자
~의 피연산자에 - 시프트 연산자
<<와>>의 두 피연산자에
불리언 변환(Boolean conversion): 어떤 스칼라 타입의 값도 _Bool(until C23) / bool(since C23)로 암시적으로 변환될 수 있어요. 값 0의 정수 상수 표현식과 같게 비교되는 값들(until C23) — 산술 타입의 경우 0, 포인터 타입의 경우 null(since C99), nullptr_t 타입(since C23) — 은 0(until C23) / false(since C23)로 변환되고, 그 외 모든 값은 1(until C23) / true(since C23)로 변환돼요. bool b1 = 0.5; // b1 == 1 (0.5를 int로 변환하면 0임) bool b2 = 2.0 * _Imaginary_I; // b2 == 1 (하지만 int로 변환하면 0임) bool b3 = 0.0 + 3.0 * I; // b3 == 1 (하지만 int로 변환하면 0임) bool b4 = 0.0 / 0.0; // b4 == 1 (NaN은 0과 같게 비교되지 않음) bool b5 = nullptr; // b5 == 0 (C23부터) |
(since C99) |
정수 변환 (Integer conversions)
어떤 정수 타입의 값도 다른 어떤 정수 타입으로 암시적으로 변환될 수 있어요. 위의 승격과 불리언 변환이 다루는 경우를 제외하면 규칙은 다음과 같아요.
- 대상 타입이 그 값을 표현할 수 있으면 값은 바뀌지 않아요.
- 그렇지 않고 대상 타입이 unsigned면, 값
2^b(여기서b는 대상 타입의 값 비트 수)를 원본 값에서 결과가 대상 타입에 들어맞을 때까지 반복해서 빼거나 더해요. 다시 말해 unsigned 정수는 모듈로 산술을 구현해요. - 그렇지 않고 대상 타입이 signed면, 동작은 구현 정의예요(신호를 발생시키는 것을 포함할 수 있음).
char x = 'a'; // int -> char, 결과는 그대로
unsigned char n = -123456; // 대상이 unsigned, 결과는 192 (-123456+483*256)
signed char m = 123456; // 대상이 signed, 결과는 구현 정의
assert(sizeof(int) > -1); // assert 실패:
// 연산자 >는 -1의 size_t로의 변환을 요구,
// 대상이 unsigned, 결과는 SIZE_MAX
실 부동-정수 변환 (Real floating-integer conversions)
어떤 실 부동 타입의 유한값도 어떤 정수 타입으로 암시적으로 변환될 수 있어요. 위의 불리언 변환으로 다뤄지는 경우를 제외하면 규칙은 다음과 같아요.
- 소수 부분은 버려져요(0 방향으로 절삭).
int n = 3.14; // n == 3
int x = 1e10; // 32비트 int에서는 정의되지 않은 동작
어떤 정수 타입의 값도 어떤 실 부동 타입으로 암시적으로 변환될 수 있어요.
- 그 값을 대상 타입이 정확히 표현할 수 있으면 그대로예요.
- 그 값을 표현할 수는 있지만 정확히 표현할 수 없으면, 결과는 가장 가까운 높은 값 또는 가장 가까운 낮은 값 중 구현 정의의 선택이에요(IEEE 산술이 지원된다면 가장 가까운 값으로 반올림). 이 경우
FE_INEXACT가 발생하는지는 불특정이에요. - 그 값을 표현할 수 없으면 동작은 정의되지 않아요. 단 IEEE 산술이 지원된다면
FE_INVALID가 발생하고 결과 값은 불특정이에요.
이 변환의 결과는 그 대상 타입이 나타내는 것보다 더 큰 범위와 정밀도를 가질 수 있어요(FLT_EVAL_METHOD 참조).
부동-정수 변환에서 FE_INEXACT를 제어할 필요가 있다면 rint와 nearbyint를 쓸 수 있어요.
double d = 10; // d = 10.00
float f = 20000001; // f = 20000000.00 (FE_INEXACT)
float x = 1 + (long long)FLT_MAX; // 정의되지 않은 동작
실 부동소수점 변환 (Real floating-point conversions)
어떤 실 부동 타입의 값도 다른 어떤 실 부동 타입으로 암시적으로 변환될 수 있어요.
- 대상 타입이 그 값을 정확히 표현할 수 있으면 그대로예요.
- 그 값을 표현할 수는 있지만 정확히 표현할 수 없으면, 결과는 가장 가까운 높은 값 또는 가장 가까운 낮은 값이에요(다시 말해 반올림 방향은 구현 정의). 단 IEEE 산술이 지원된다면 가장 가까운 값으로 반올림돼요.
- 그 값을 표현할 수 없으면 동작은 정의되지 않아요.
이 변환의 결과는 그 대상 타입이 나타내는 것보다 더 큰 범위와 정밀도를 가질 수 있어요(FLT_EVAL_METHOD 참조).
double d = 0.1; // d = 0.1000000000000000055511151231257827021181583404541015625
float f = d; // f = 0.100000001490116119384765625
float x = 2 * (double)FLT_MAX; // 정의되지 않음
복소 타입 변환(Complex type conversions): 어떤 복소 타입의 값도 다른 어떤 복소 타입으로 암시적으로 변환될 수 있어요. 실수 부분과 허수 부분은 각각 실 부동 타입의 변환 규칙을 따르고, 결과의 허수 부분은 양의 0이에요(또는 비-IEEE 시스템에서는 부호 없는 0). double complex d = 0.1 + 0.1 * I; float complex f = d; // f는 (0.100000001490116119384765625, 0.100000001490116119384765625) 허수 타입 변환(Imaginary type conversions): 어떤 허수 타입의 값도 다른 어떤 허수 타입으로 암시적으로 변환될 수 있어요. 허수 부분은 실 부동 타입의 변환 규칙을 따라요. double imaginary d = 0.1 * _Imaginary_I; float imaginary f = d; // f는 0.100000001490116119384765625*I 실-복소 변환(Real-complex conversions): 어떤 실 부동 타입의 값도 어떤 복소 타입으로 암시적으로 변환될 수 있어요. 결과의 실수 부분은 실 부동 타입의 변환 규칙으로 결정돼요. 결과의 허수 부분은 양의 0이에요. 어떤 복소 타입의 값도 어떤 실 부동 타입으로 암시적으로 변환될 수 있어요. 실수 부분은 실 부동 타입의 규칙에 따라 변환돼요. 허수 부분은 버려져요. 참고: 복소-실 변환에서 허수 부분의 NaN은 실 결과로 전파되지 않아요. double complex z = 0.5 + 3 * I; float f = z; // 허수 부분은 버려지고, f는 0.5로 설정 z = f; // z를 0.5 + 0*I로 설정 실-허수 변환(Real-imaginary conversions): 어떤 허수 타입의 값도 어떤 실 타입(정수 또는 부동소수점)으로 암시적으로 변환될 수 있어요. 결과는 항상 양의(또는 부호 없는) 0이에요. 단 대상 타입이 _Bool(until C23) / bool(since C23)일 때는 불리언 변환 규칙이 적용돼요. 어떤 실 타입의 값도 어떤 허수 타입으로 암시적으로 변환될 수 있어요. 결과는 항상 양의 허수 0이에요. double imaginary z = 3*I; bool b = z; // 불리언 변환: b를 true로 설정 float f = z; // 실-허수 변환: f를 0.0으로 설정 z = 3.14; // 허수-실 변환: z를 0*_Imaginary_I로 설정 복소-허수 변환(Complex-imaginary conversions): 어떤 허수 타입의 값도 어떤 복소 타입으로 암시적으로 변환될 수 있어요. 결과의 실수 부분은 양의 0이고, 허수 부분은 대응하는 실 타입의 변환 규칙을 따라요. 어떤 복소 타입의 값도 어떤 허수 타입으로 암시적으로 변환될 수 있어요. 실수 부분은 버려지고, 허수 부분은 대응하는 실 타입의 변환 규칙을 따라요. double imaginary z = I * (3*I); // 복소 결과 -3.0+0i가 실수 부분을 잃음 // z를 0*_Imaginary_I로 설정 |
(since C99) |
포인터 변환 (Pointer conversions)
객체 타입에 대한 포인터와 void에 대한 포인터는 다음 의미론으로 서로 암시적으로 변환될 수 있어요.
- 객체 포인터가
void포인터로 변환된 뒤 다시 되돌아오면, 그 값은 원래 포인터와 같게 비교돼요. - 그 외의 보장은 없어요.
int* p = malloc(10 * sizeof(int)); // malloc은 void*를 반환
한정되지 않은 타입의 포인터는 그 타입의 한정된 버전의 포인터로 암시적으로 변환될 수 있어요(다시 말해 const, volatile, restrict 한정자를 추가할 수 있어요). 원래 포인터와 결과는 같게 비교돼요.
int n;
const int* p = &n; // &n은 int* 타입
값 0의 어떤 정수 상수 표현식과, 값 0의 정수 포인터 표현식을 void* 타입으로 캐스트한 것은 어떤 포인터 타입으로도 암시적으로 변환될 수 있어요(객체 포인터와 함수 포인터 모두). 결과는 그 타입의 널 포인터 값이며, 그 타입의 어떤 비-널 포인터 값과도 같지 않게 비교되는 것이 보장돼요. 이 정수 또는 void* 표현식을 널 포인터 상수(null pointer constant) 라고 부르고, 표준 라이브러리는 이 상수의 정의 하나를 매크로 NULL로 제공해요.
int* p = 0;
double* q = NULL;
주의 (Notes)
어떤 산술 연산자에서 signed 정수 오버플로가 정의되지 않은 동작이지만, 정수 변환에서 signed 정수 타입을 오버플로하는 것은 단지 불특정 동작이에요.
반면, 어떤 산술 연산자(그리고 정수 변환)의 unsigned 정수 오버플로는 잘 정의된 연산이고 모듈로 산술의 규칙을 따르지만, 부동-정수 변환에서 unsigned 정수를 오버플로하는 것은 정의되지 않은 동작이에요. unsigned 정수로 변환될 수 있는 실 부동 타입의 값은 열린 구간 (-1, Unnn_MAX + 1)의 값이에요.
unsigned int n = -1.0; // 정의되지 않은 동작
포인터와 정수 사이의 변환(포인터에서 _Bool(until C23) / bool(since C23)로, 그리고 값 0의 정수 상수 표현식에서 포인터로의 변환(since C99)은 제외), 객체 포인터들 사이의 변환(둘 중 하나가 void에 대한 포인터인 경우 제외), 그리고 함수 포인터들 사이의 변환(함수들이 호환 타입을 가질 때 제외)은 결코 암시적이지 않으며 캐스트 연산자가 필요해요.
함수에 대한 포인터와 객체에 대한 포인터(void* 포함) 또는 정수 사이에는 (암시적이든 명시적이든) 변환이 없어요.
더 알아보기
- 캐스트 연산자 (cast operators) — 명시적 변환
- 통상적인 산술 변환 · 값 범주 (value category) — 변환이 일어나는 문맥
- 상수 표현식 (constant expressions) — 널 포인터 상수와의 관계
- C++ documentation for Implicit conversions