Menu

C 형변환: 암묵적 변환과 명시적 캐스트

C는 끊임없이 타입을 변환합니다. 캐스트로 직접 요청한 경우도 있지만, 대부분은 알아서 일어납니다. 승격 규칙, 명시적 캐스트 문법, 그리고 조용히 데이터를 잃는 변환들을 정리했습니다.

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

C는 끊임없이 값의 타입을 변환합니다. 그중 일부는 캐스트로 직접 쓰는 것이지만, 대부분은 여러분이 고르지 않은 규칙에 따라 컴파일러가 조용히 수행합니다. 어느 쪽인지 아는 것이 "왜 평균이 항상 3이지?"와 코드가 말한 대로 동작하는 것의 차이를 만듭니다.

암묵적 변환

어떤 타입의 값이 다른 타입을 기대하는 자리를 만나면, C는 언제든 변환합니다:

정보를 잃을 수 없는 변환(int에서 double로, char에서 int로, short에서 long으로)은 **확장(widening)**이며 언제나 안전합니다. 반대 방향의 변환은 **축소(narrowing)**이고 데이터를 잃을 수 있습니다. 위의 3.9는 아무 경고 없이 3이 되었습니다. 경고를 보려면 -Wconversion을 요청해야 합니다.

일반 산술 변환

이항 연산자의 피연산자 타입이 서로 다르면, C는 작업을 하기 전에 둘을 공통 타입으로 변환합니다. 아래에서 위로 올라가는 사다리는 이렇습니다:

  1. int보다 작은 것(char, short, _Bool)은 모두 int로 승격됩니다. 이것이 정수 승격이며, 언제나 가장 먼저 일어납니다.
  2. 한쪽이 long double이면 다른 쪽도 long double이 됩니다.
  3. 그렇지 않고 한쪽이 double이면 다른 쪽도 double이 됩니다.
  4. 그렇지 않고 한쪽이 float이면 다른 쪽도 float이 됩니다.
  5. 그렇지 않으면 정수 타입 중 등급(rank)이 높은 쪽이 이기고, 등급이 같으면 부호 없는 쪽이 이깁니다.

마지막 규칙이 진짜 버그를 만드는 규칙입니다. 나머지는 직관적입니다.

정수 승격은 char 산술이 예상과 달리 넘치지 않는 이유이자, 결과를 다시 char에 저장하면 넘치는 이유입니다.

명시적 캐스트

캐스트는 괄호로 감싼 목표 타입입니다:

(double)x
(int)3.9
(char)65
(unsigned int)n

바로 뒤에 오는 식에 적용되며, 결합력이 매우 강합니다. *, /, +보다 강합니다.

첫 줄은 정수로 나눈 뒤(3이 됨) 3을 3.0으로 변환합니다. 너무 늦었습니다. 두 번째 줄은 나눗셈 전에 total을 부동소수점으로 변환하므로, / 연산자는 doubleint를 보고 int 쪽을 승격시킨 뒤 부동소수점 나눗셈을 수행합니다.

피연산자 하나만 캐스트하면 충분합니다. 나머지는 일반 산술 변환이 알아서 처리합니다.

정수 나눗셈 고치기

C에서 캐스트를 쓰는 가장 흔한 이유가 바로 이것입니다:

백분율 줄이 교훈적입니다. passed / n3 / 5이고 정수로는 0이며, 0 * 1000입니다. 나누기 전에 곱하면(100 * passed / n) 캐스트 없이도 고쳐집니다. 300 / 5가 딱 떨어지기 때문입니다. 하지만 그건 숫자가 협조해줄 때만 통합니다. 믿을 만한 해법은 캐스트입니다.

반올림이 아니라 절삭

부동소수점 값을 정수로 캐스트하면 소수부가 버려집니다. 0 방향으로 절삭될 뿐, 반올림하지 않습니다:

이 코드를 직접 컴파일한다면, 리눅스에서는 math.h를 쓸 때 링크 단계에 -lm이 필요하다는 점을 기억하세요.

위험이 하나 더 있습니다. 정수 타입에 담기에 너무 큰 부동소수점 값을 변환하는 것은 되돌아 감기(wrap-around)가 아니라 미정의 동작입니다. (int)1e20은 무엇이든 만들어낼 수 있습니다. 값이 여러분의 통제 밖에 있다면 캐스트 전에 범위를 확인하세요.

char와 int

C의 char는 문자 코드를 담은 작은 정수 그 자체입니다. 둘 사이를 오가는 변환은 일상적인 작업입니다:

digit - '0'은 숫자 문자를 값으로 바꾸는 표준 관용구이며, 열 개의 숫자 문자가 연속이라는 것이 보장되기 때문에 동작합니다. 알파벳에는 + 32 산술보다 ctype.htoupper()tolower()를 쓰세요. 그 간격은 아스키의 사실일 뿐 C의 보장이 아닙니다.

관련된 함정 하나: isdigit, toupper 같은 ctype.h의 함수는 EOF이거나 unsigned char로 표현 가능한 int를 받아야 합니다. 음수인 순수 char를 넘기는 것은(순수 char는 부호 있을 수 있으므로 가능합니다) 미정의 동작입니다. 캐스트하세요: isdigit((unsigned char)c).

부호 있음/없음의 함정

변환 사다리의 5단계, 즉 동점이면 부호 없는 쪽이 이긴다는 규칙이 C에서 가장 놀라운 비교를 만들어냅니다:

-1unsigned int로 변환되면서 그 비트 패턴이 4,294,967,295로 재해석됩니다. 1보다 크므로 비교는 거짓입니다.

같은 변환이 반복문을 영원히 돌게 만듭니다:

/* BUG: i is unsigned, so i >= 0 is always true. When i is 0, i-- wraps. */
for (size_t i = n - 1; i >= 0; i--) { ... }

그리고 길이 검사를 실패하게 만듭니다:

/* BUG: strlen returns size_t (unsigned). If the string is shorter
   than 5, len - 5 wraps to a huge number and the test passes. */
if (strlen(s) - 5 > 0) { ... }

if (strlen(s) > 5)로 다시 쓰면 뺄셈 자체가 일어나지 않습니다.

방어책은 이렇습니다. 개수와 인덱스는 처음부터 끝까지 부호 체계를 하나로 유지하고, -Wsign-compare(-Wextra에 포함됨)로 컴파일하며, 꼭 섞어야 한다면 값이 음수가 될 수 없음을 확인한 뒤 명시적으로 캐스트하세요.

포인터 캐스트

캐스트는 포인터 타입 사이의 변환에도 쓰이는데, 여기서는 실질적인 위험을 동반합니다. 바이트 자체가 아니라 메모리를 해석하는 방식을 바꾸기 때문입니다.

리틀 엔디언 머신에서는 01 00 00 00이 출력됩니다. unsigned char *를 통해 객체 표현을 들여다보는 것은 표준이 명시적으로 허락한 몇 안 되는 포인터 캐스트 중 하나입니다.

다른 대부분의 포인터 캐스트는 허락되지 않습니다. float *를 통해 int를 읽는 것은 엄격한 앨리어싱 규칙을 어기는 일이며, 컴파일이 되더라도 미정의 동작입니다. 바이트를 재해석하려면 memcpy를 쓰세요.

알아둘 만한 관례가 둘 있습니다. C에서 void *캐스트 없이 어떤 객체 포인터 타입으로도 오갈 수 있습니다. 그래서 malloc의 결과를 캐스트하면 안 됩니다:

int *arr = malloc(n * sizeof *arr);        /* correct C */
int *arr = (int *)malloc(n * sizeof *arr); /* needless; hides a missing header */

C++에서는 캐스트가 필수이며, 그래서 그렇게 많은 코드가 캐스트를 달고 있습니다. C에서는 <stdlib.h>를 빠뜨린 실수를 감춰버릴 수 있습니다.

그리고 printf("%p", ...)void *를 기대하므로, 여기 넘기는 포인터 인자는 정말로 캐스트가 필요합니다: printf("%p", (void *)p).

캐스트가 틀린 답일 때

캐스트는 컴파일러의 입을 막습니다. 때로는 컴파일러가 옳았습니다.

long big = 5000000000L;
int  small = (int)big;    /* the cast hides real data loss */

값이 정말로 들어간다면 캐스트는 여러분이 확인했다는 문서 역할을 합니다. 들어가지 않을 수도 있다면, 캐스트는 경고를 조용한 오답으로 바꿔놓은 것입니다. 캐스트를 쓰기 전에, 해법이 변수의 타입을 바꾸는 쪽은 아닌지 먼저 물어보세요. int 대신 double, int 대신 size_t, long 대신 long long 같은 식으로요. 캐스트는 (double)sum / count처럼 올바른 두 타입이 한 연산에서 만나야 할 때 주로 알맞은 도구입니다.

자주 묻는 질문

C에서 캐스트는 어떻게 하나요?

값 앞에 괄호로 목표 타입을 적습니다: (double)x, (int)3.9, (char)65. 캐스트는 바로 뒤에 오는 식에 적용되므로, (double)a / ba를 먼저 변환한 뒤 나누고, (double)(a / b)는 정수로 나눈 다음 그 결과를 변환합니다.

C에서 int를 float으로 어떻게 변환하나요?

대입만 해도 암묵적으로 변환됩니다. double d = 5;는 5.0을 저장합니다. 식 안에서는 명시적 캐스트가 필요한 경우가 많습니다. (double)total / count는 정수 나눗셈 대신 부동소수점 나눗셈을 강제합니다.

C에서 float을 int로 캐스트하면 어떻게 되나요?

소수부가 버려집니다. 반올림이 아니라 0 방향으로 절삭됩니다. (int)3.9는 3이고 (int)-3.9는 -3입니다. 반올림하려면 양수에 한해 캐스트 전에 0.5를 더하거나, math.hround(), floor(), ceil()을 쓰세요.

부호 있는 int와 부호 없는 int를 비교하면 왜 엉뚱한 답이 나오나요?

C의 일반 산술 변환은 부호 있는 값을 부호 없는 값으로 바꿉니다. 그래서 -1 < 1u는 거짓입니다. -1이 거대한 양수가 되기 때문입니다. 개수와 크기는 부호 체계를 하나로 통일하거나, 값이 음수가 될 수 없음을 확인한 뒤 명시적으로 캐스트하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기