본문 바로가기

형변환

대입과 산술, 함수 호출에서 컴파일러가 스스로 하는 암시적 변환과 그 과정에서 잃는 값, 부호 있는 정수와 부호 없는 정수를 섞을 때의 함정을 살펴보고, static_cast를 비롯한 네 가지 이름 있는 캐스트가 허용하는 변환과 나눗셈에서 변환하는 시점이 결과를 바꾸는 모습을 확인합니다.

정수와 실수를 함께 계산하거나, 실수 값을 정수 변수에 담거나, 문자를 그 코드 값으로 읽을 때는 한 타입의 값을 다른 타입으로 바꾸는 형변환(type conversion)이 일어납니다. 정수 5와 실수 5.0은 같은 수처럼 보이지만 메모리에 저장되는 비트의 모양이 전혀 달라서, 둘을 함께 계산하려면 먼저 같은 타입으로 맞춰야 합니다.

형변환에는 컴파일러가 스스로 하는 암시적 변환(implicit conversion)과, 프로그래머가 직접 적는 명시적 변환(explicit conversion)이 있습니다. 이 글은 두 변환이 언제 일어나고 어떤 값을 잃을 수 있는지 보고, 변환하는 시점이 계산의 결과를 어떻게 바꾸는지 확인합니다.

C++ · TYPE AND SCOPE

암시적 허용과 값 보존은 별도로 확인한다

대상 타입을 정하는 문맥과 실제 값의 범위·정밀도를 함께 확인합니다.

암시적으로 허용되는지와 값을 보존하는지는 별개의 질문이므로, 변환 문맥이 정한 대상 타입과 실제 값의 범위를 함께 확인한다.
변환 문맥 대상 타입을 정하는 것 대표 예 수락 전 확인
초기화 · 대입 · 호출 · 반환 객체·매개변수·반환 타입 double d = 10; 값이 대상 범위와 정밀도에서 보존되는가? 경고를 표준 보장으로 오해하지 않는가?
일반 산술 변환 연산자가 만드는 공통 타입 10 + 3.5 → double signed/unsigned 조합과 정수·부동 소수점 정밀도 변화가 의도와 맞는가?
정수 · 부동 소수점 승격 정수 변환 rank와 부동 소수점 승격 규칙 bool → int, float → double 승격과 임의의 산술 변환을 같은 말로 부르지 않았는가?
값이 달라질 수 있는 변환 문맥의 대상 타입 3.14 → int(3), 큰 정수 → double 소수부 폐기, 정밀도 손실, 범위 밖 값의 동작을 검토했는가?
초기화 · 대입 · 호출 · 반환
대상 객체·매개변수·반환 타입이 정합니다.
예 double d = 10;에서 값 10은 정확히 보존됩니다.
확인 다른 값도 범위와 정밀도에서 보존되는지 따로 봅니다. 경고 여부는 도구 설정에 달려 있습니다.
일반 산술 변환
대상 연산자가 피연산자의 공통 타입을 만듭니다.
예 10 + 3.5의 공통 타입과 결과 타입은 double입니다.
확인 signed/unsigned와 정밀도 변화가 의도와 맞는지 봅니다.
정수 · 부동 소수점 승격
대상 정수 rank와 부동 소수점 승격 규칙이 정합니다.
예 bool → int, float → double
확인 모든 산술 변환을 승격이라고 부르지 않습니다.
값이 달라질 수 있는 변환
대상 문맥의 대상 타입이 정합니다.
예 3.14 → int(3), 큰 정수 → double
확인 소수부 폐기, 정밀도 손실, 범위 밖 값의 동작을 검토합니다.

리스트 초기화는 표준이 정한 narrowing을 오류로 만들지만 모든 의미 손실을 증명하지는 않습니다. 명시적 캐스트도 값 검증을 대신하지 않습니다.

그림의 요점은 컴파일러가 변환을 허용한다는 것과 값이 그대로 보존된다는 것이 별개의 질문이라는 점입니다. 변환은 초기화와 대입, 함수 호출, 반환, 산술 연산처럼 대상 타입이 정해지는 자리에서 일어나며, 그때마다 그 값이 대상 타입의 범위와 정밀도 안에 드는지를 따로 확인해야 합니다.

암시적 변환

암시적 변환은 대상 타입이 정해진 문맥에서 컴파일러가 알아서 하는 변환입니다. 코드는 짧아지지만, 허용된다고 해서 값을 잃지 않는다는 뜻은 아닙니다.

대입과 초기화에서는 오른쪽 값이 왼쪽 변수의 타입으로 바뀝니다. int 값 10을 double에 담으면 10.0이 되어 값이 보존되지만, 실수를 정수에 담으면 소수점 아래를 버립니다. 버린 뒤의 값이 그 정수 타입의 범위를 넘으면 정의되지 않은 동작이며, 일반 대입에서 경고를 낼지는 컴파일러의 옵션에 달려 있습니다.

int num_int = 10;
double num_double = num_int;

double pi = 3.141592;
int integer_pi = pi;

num_double은 10.0이고, integer_pi는 소수점 아래를 버린 3입니다. 정수를 실수로 바꿀 때도 모든 값이 보존되는 것은 아니어서, 64비트 long long의 큰 값은 double의 정밀도 53비트로 담을 수 없어 가장 가까운 값으로 반올림됩니다.

산술 연산에서는 두 피연산자를 하나의 공통 타입으로 맞춘 뒤 계산하는데, 이 규칙을 일반 산술 변환(usual arithmetic conversions)이라고 합니다. int와 double을 더하면 int가 double로 바뀌어 double로 계산하고, char나 short, bool처럼 int보다 작은 정수는 계산하기 전에 먼저 int로 바뀝니다. 작은 타입을 이렇게 값을 잃지 않고 넓히는 것을 특히 승격(promotion)이라고 부르며, bool의 false는 0, true는 1이 됩니다.

implicit_conversion.cpp
#include <iostream>

void printNumber(double num) {
    std::cout << "received: " << num << "\n";
}

int main() {
    int a = 10;
    double b = 3.5;
    std::cout << "a + b: " << a + b << "\n";

    printNumber(123);

    char ch = 'A';
    int code = ch;
    bool flag = true;
    int flagValue = flag;
    std::cout << "code: " << code << ", flag: " << flagValue << "\n";

    std::cout << "-1 < 1u: " << (-1 < 1u) << "\n";
}
a + b: 13.5
received: 123
code: 65, flag: 1
-1 < 1u: 0

함수를 부를 때도 인자가 매개변수의 타입으로 바뀌어, 정수 123이 double 매개변수에 123.0으로 들어갑니다. char의 'A'는 GCC가 쓰는 UTF-8에서 65로 승격되고, true는 1이 됩니다.

마지막 줄은 부호 있는 정수와 부호 없는 정수를 섞을 때의 함정입니다. -1은 int이고 1u는 unsigned int이므로 일반 산술 변환이 -1을 unsigned int로 바꾸는데, 그 값은 232−12^{32}-1인 4294967295여서 1u보다 큽니다. 그래서 -1 < 1u가 거짓인 0이 되며, 이런 비교를 변수끼리 하면 GCC는 -Wall에서 부호가 다른 정수의 비교라는 경고를 냅니다. 음수가 될 수 있는 값을 부호 없는 값과 비교할 때는 이 변환을 먼저 생각합니다.

명시적 변환과 이름 있는 캐스트

명시적 변환은 프로그래머가 어떤 값을 어느 타입으로 바꾸라고 직접 적는 것으로, 캐스트(cast)라고도 부릅니다. 값을 잃을 수 있는 변환을 일부러 한다는 뜻을 드러내거나, 암시적 변환으로는 원하는 계산이 되지 않을 때 씁니다.

C에서 쓰던 (double)numerator 같은 괄호 캐스트는 C++에서도 동작하지만, 문맥에 따라 아래의 여러 캐스트 가운데 하나나 그 조합으로 해석되어 어떤 위험을 받아들이는지가 표기에 드러나지 않습니다. 그래서 C++은 하는 일에 따라 이름이 다른 네 가지 캐스트 연산자를 두고, 새 코드에서는 이쪽을 씁니다.

C++ · TYPE AND SCOPE

캐스트 이름은 허용 범위와 검증 책임을 나눈다

변환 형식의 허용, 실패 신호, 값·수명·실제 타입의 전제를 나누어 읽습니다.

static_cast
경계 숫자·열거형·명시적으로 허용된 변환과 일부 상속 관계
신호 허용 형식은 컴파일 시점에 결정됩니다.
검증 숫자 범위·정밀도와 수동 다운캐스트의 실제 객체 타입
dynamic_cast
경계 다형적 계층의 런타임 다운·교차 캐스트
신호 실패한 포인터는 nullptr, 참조는 std::bad_cast
검증 객체 수명, public·유일한 관계, 실패 처리
const_cast
경계 비슷한 포인터·참조의 cv 한정 조정
신호 결과는 같은 객체를 가리킵니다.
검증 원래부터 const인 객체를 결과를 통해 쓰지 않습니다.
reinterpret_cast
경계 포인터·정수·함수 포인터의 저수준 매핑
신호 결과가 구현 정의·미지정·조건부 지원일 수 있습니다.
검증 정렬·수명·접근·호출 호환성과 외부 ABI

C-스타일 캐스트는 여러 변환 종류나 조합을 한 표기로 숨길 수 있습니다. 이름 있는 캐스트를 선택하고도 각 행의 전제를 증명할 수 없다면 설계를 다시 검토합니다.

static_cast<T>(식)은 가장 흔히 쓰는 캐스트로, 숫자와 열거형 사이의 변환, 클래스가 허용한 변환, 상속 관계에서의 일부 변환처럼 표준이 정한 변환을 합니다. 그 변환이 허용되는지는 컴파일할 때 검사하지만, 값이 범위 안에 드는지나 객체의 실제 타입이 맞는지는 프로그래머가 확인해야 합니다.

static_cast.cpp
#include <iostream>

int main() {
    double d_val = 3.14;
    int i_val = 10;

    std::cout << static_cast<int>(d_val) << "\n";
    std::cout << static_cast<double>(i_val) / 4 << "\n";
    std::cout << static_cast<char>(65) << "\n";
}
3
2.5
A

static_cast<int>(3.14)는 소수점 아래를 버린 3이고, 10을 먼저 double로 바꾸어 4로 나누면 2.5가 됩니다. 65를 char로 바꾸면 UTF-8에서 A인 코드 단위가 되어 글자로 출력됩니다.

나머지 세 캐스트는 쓰임이 좁습니다. dynamic_cast는 가상 함수가 있는 클래스의 상속 관계에서, 기반 클래스를 가리키는 포인터가 실제로 어떤 파생 클래스의 객체를 가리키는지 실행 중에 검사하며 변환합니다. 실패하면 포인터는 nullptr이 되고 참조는 std::bad_cast 예외를 던지며, 이 캐스트가 필요한 상속 구조의 예는 다중 상속과 가상 상속에서 다시 나옵니다.

const_cast는 포인터나 참조에서 const를 붙이거나 떼는 데만 쓰며, const를 받지 않는 오래된 함수에 값을 넘기는 것처럼 드문 경우에 씁니다. 원래 const로 만든 객체를 const_cast로 떼어 낸 뒤 고치는 것은 정의되지 않은 동작입니다. reinterpret_cast는 포인터와 정수, 서로 관계없는 포인터 타입 사이의 저수준 변환을 하는데, 결과가 구현에 따라 정해지고 그 포인터로 객체에 접근해도 되는지는 정렬과 수명 같은 별도의 조건에 달려 있어, 장치나 운영체제의 인터페이스처럼 꼭 필요한 곳에만 씁니다.

더 깊이: 비트를 그대로 다른 타입으로 읽으려면

float 값의 비트를 정수로 보려고 *reinterpret_cast<std::uint32_t*>(&f)처럼 쓰는 코드를 흔히 볼 수 있지만, 이것은 타입 별칭 규칙(type aliasing rule)을 어기는 정의되지 않은 동작입니다. 이 규칙에 따르면 객체는 자기 타입, 부호만 다른 짝 타입, 그리고 char, unsigned char, std::byte를 통해서만 읽고 쓸 수 있습니다. 컴파일러는 이 규칙을 믿고 다른 타입으로 쓴 값이 이 타입의 값을 바꾸지 않는다고 가정해 최적화하며, GCC에서는 -O2부터 켜지는 -fstrict-aliasing이 이 가정을 씁니다. 그래서 최적화를 끈 빌드에서 맞게 보이던 코드가 -O2에서 다른 값을 읽기도 합니다.

C++20의 <bit>에 있는 std::bit_cast<To>(from)은 같은 일을 정의된 방법으로 합니다. 두 타입의 sizeof가 같고 둘 다 바이트를 그대로 복사해도 되는 trivially copyable 타입이면, from과 같은 비트를 가진 To 값을 새로 만들어 돌려줍니다. 포인터와 멤버 포인터, 공용체, volatile 타입, 참조 멤버가 어느 부분에도 들어 있지 않은 타입끼리라면 constexpr 문맥에서도 쓸 수 있습니다. C++20 이전에는 std::memcpy로 바이트를 복사하는 방법을 썼으며, cppreference의 std::bit_cast 설명에도 이 방식으로 만든 구현 예가 있습니다.

중괄호 초기화와 축소 변환

변수 선언과 초기화에서 본 것처럼, 중괄호 초기화는 값을 잃을 수 있는 축소 변환이 필요하면 컴파일 오류를 냅니다. 아래 세 줄은 같은 3.14를 int에 담는 세 방법으로, 중괄호 초기화와 복사 초기화, 명시적 캐스트가 같은 변환을 어떻게 다르게 다루는지 보여 줍니다.

int x{3.14};
int y = 3.14;
int z = static_cast<int>(3.14);

첫 줄은 double에서 int로의 축소 변환이어서 컴파일되지 않습니다. 둘째 줄은 표준이 허용해 3이 되고 경고는 옵션에 달려 있으며, 셋째 줄은 같은 3을 일부러 고른다는 뜻을 캐스트로 드러냅니다. 다만 중괄호 초기화가 모든 손실을 잡아 주는 것은 아니며, 표준이 축소 변환으로 정한 경우만 막습니다.

변환하는 시점과 나눗셈

변환이 계산의 어느 시점에 일어나는지가 결과를 바꾸는 대표적인 예가 나눗셈입니다. int끼리 나누면 정수 나눗셈이 되어 몫만 남고 소수점 아래는 버려집니다.

C++ 나눗셈에서 변환 시점이 결과 타입을 정하는 흐름

100/3의 int 나눗셈 뒤 double 변환 33.0과 먼저 double로 변환해 나누는 약 33.333을 독립된 두 계산의 입력/계산/저장 단계로 비교할 수 있다.

C++ · TYPE AND SCOPE

나눗셈 전에 변환해야 소수부를 얻는다

같은 입력 total=100, count=3으로 변환 시점만 바꾸어 비교합니다.

나눗셈 전후 변환 시점 비교 total과 count가 모두 int인 상태에서 나눗셈을 먼저 하면 33이 생긴 뒤 double 33.0으로 저장됩니다. total을 먼저 double로 바꾸면 count도 공통 타입 double로 변환되어 약 33.333의 실수 결과가 생깁니다. 같은 정수 입력 total = 100 · count = 3 연산 뒤에 변환 연산 전에 변환 표현식을 먼저 평가 int / int → 33 피연산자를 먼저 변환 double / int → 공통 double double 객체를 초기화 33 → 33.0 실수 나눗셈을 실행 100.0 / 3.0 → 약 33.333… 소수부는 초기화 전에 버려짐 공통 타입을 연산 전에 결정
같은 입력의 두 변환 시점을 비교한다같은 정수 입력에서 두 경로로 갈라진다. 나눗셈 뒤 변환 경로는 정수 몫33을 double33.0으로 저장한다. 나눗셈 전 변환 경로는 total을double로 바꾸고 실수 몫을 저장한다. 두 경로를 순서대로 모두 수행하는 단계도로 읽지 않는다.같은 정수 입력total100 · count3연산 뒤 변환int / int먼저 몫33double에 저장33 → 33.0연산 전에 변환total → double공통 double로 계산실수 몫을 저장약33.333…경로1: 먼저 정수 연산경로2: 먼저 타입 변환변환 시점이 결과를 바꾼다
same input total = 100 · count = 3
연산 뒤에 변환
표현식 double avg = total / count;
연산 int / int → 33
저장 이미 생긴 33이 33.0으로 바뀝니다.
연산 전에 변환
표현식 static_cast<double>(total) / count
연산 일반 산술 변환 뒤 double / double
결과 100.0 / 3.0 → 약 33.333…

결과 객체의 타입은 이미 끝난 연산의 규칙을 되돌리지 못합니다. 원하는 공통 타입은 연산이 시작되기 전에 피연산자에서 만들어야 합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명같은 정수 입력에서 두 경로로 갈라진다. 나눗셈 뒤 변환 경로는 정수 몫 33을 double 33.0으로 저장한다. 나눗셈 전 변환 경로는 total을 double로 바꾸고 실수 몫을 저장한다. 두 경로를 순서대로 모두 수행하는 단계도로 읽지 않는다.
노드·연결선 라벨먼저 몫 33
노드·연결선 라벨약 33.333…
노드·연결선 라벨경로 1: 먼저 정수 연산
노드·연결선 라벨경로 2: 먼저 타입 변환
예의 값과 변화
관찰·상황값·보충 정보변화·이유
같은 입력의 두 경로

100과 3을 서로 독립된 계산에 넣는다.

  • 경로 A: int 100 / int 3
  • 경로 B: double 100 / int 3
  • 변환을 나눗셈 전후 어느 시점에 하는지가 다르다.
나눗셈 결과

피연산자 타입에 따라 계산 결과를 얻는다.

  • A 계산: 정수 33
  • B 계산: 약 33.333
  • 정수 나눗셈의 소수부 손실은 후속 double 변환으로 되돌릴 수 없다.
double 저장

각 결과를 double로 저장한다.

  • A 저장: 33.0
  • B 저장: 약 33.333
  • A의 변환은 이미 계산된 정수 33만 표현한다.

C++ 나눗셈에서 변환 시점이 결과 타입을 정하는 흐름

100/3의 int 나눗셈 뒤 double 변환 33.0과 먼저 double로 변환해 나누는 약 33.333을 독립된 두 계산의 입력/계산/저장 단계로 비교할 수 있다.

C++ · TYPE AND SCOPE

나눗셈 전에 변환해야 소수부를 얻는다

같은 입력 total=100, count=3으로 변환 시점만 바꾸어 비교합니다.

나눗셈 전후 변환 시점 비교 total과 count가 모두 int인 상태에서 나눗셈을 먼저 하면 33이 생긴 뒤 double 33.0으로 저장됩니다. total을 먼저 double로 바꾸면 count도 공통 타입 double로 변환되어 약 33.333의 실수 결과가 생깁니다. 같은 정수 입력 total = 100 · count = 3 연산 뒤에 변환 연산 전에 변환 표현식을 먼저 평가 int / int → 33 피연산자를 먼저 변환 double / int → 공통 double double 객체를 초기화 33 → 33.0 실수 나눗셈을 실행 100.0 / 3.0 → 약 33.333… 소수부는 초기화 전에 버려짐 공통 타입을 연산 전에 결정
같은 입력의 두 변환 시점을 비교한다같은 정수 입력에서 두 경로로 갈라진다. 나눗셈 뒤 변환 경로는 정수 몫33을 double33.0으로 저장한다. 나눗셈 전 변환 경로는 total을double로 바꾸고 실수 몫을 저장한다. 두 경로를 순서대로 모두 수행하는 단계도로 읽지 않는다.같은 정수 입력total100 · count3연산 뒤 변환int / int먼저 몫33double에 저장33 → 33.0연산 전에 변환total → double공통 double로 계산실수 몫을 저장약33.333…경로1: 먼저 정수 연산경로2: 먼저 타입 변환변환 시점이 결과를 바꾼다
same input total = 100 · count = 3
연산 뒤에 변환
표현식 double avg = total / count;
연산 int / int → 33
저장 이미 생긴 33이 33.0으로 바뀝니다.
연산 전에 변환
표현식 static_cast<double>(total) / count
연산 일반 산술 변환 뒤 double / double
결과 100.0 / 3.0 → 약 33.333…

결과 객체의 타입은 이미 끝난 연산의 규칙을 되돌리지 못합니다. 원하는 공통 타입은 연산이 시작되기 전에 피연산자에서 만들어야 합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명같은 정수 입력에서 두 경로로 갈라진다. 나눗셈 뒤 변환 경로는 정수 몫 33을 double 33.0으로 저장한다. 나눗셈 전 변환 경로는 total을 double로 바꾸고 실수 몫을 저장한다. 두 경로를 순서대로 모두 수행하는 단계도로 읽지 않는다.
노드·연결선 라벨먼저 몫 33
노드·연결선 라벨약 33.333…
노드·연결선 라벨경로 1: 먼저 정수 연산
노드·연결선 라벨경로 2: 먼저 타입 변환
예의 값과 변화
관찰·상황값·보충 정보변화·이유
같은 입력의 두 경로

100과 3을 서로 독립된 계산에 넣는다.

  • 경로 A: int 100 / int 3
  • 경로 B: double 100 / int 3
  • 변환을 나눗셈 전후 어느 시점에 하는지가 다르다.
나눗셈 결과

피연산자 타입에 따라 계산 결과를 얻는다.

  • A 계산: 정수 33
  • B 계산: 약 33.333
  • 정수 나눗셈의 소수부 손실은 후속 double 변환으로 되돌릴 수 없다.
double 저장

각 결과를 double로 저장한다.

  • A 저장: 33.0
  • B 저장: 약 33.333
  • A의 변환은 이미 계산된 정수 33만 표현한다.
divide_timing.cpp
#include <iomanip>
#include <iostream>

int main() {
    int total{100};
    int count{3};
    double after = total / count;
    double alsoAfter = static_cast<double>(total / count);
    double before = static_cast<double>(total) / count;
    std::cout << std::fixed << std::setprecision(3)
              << after << ' ' << alsoAfter << ' ' << before << '\n';
}
33.000 33.000 33.333

그림처럼 앞의 두 식은 int / int가 먼저 정수 몫 33을 만들고, 그 33을 double로 바꾸므로 버려진 소수점 아래가 돌아오지 않습니다. 셋째 식은 total을 먼저 double로 바꾸므로, 일반 산술 변환이 count도 double로 맞춰 100.0 / 3.0을 계산합니다. std::setprecision(3)은 소수점 아래 세 자리로 보여 주는 형식일 뿐, 저장된 값을 세 자리로 바꾼 것은 아닙니다. 소수부를 얻으려면 나누기 전에 피연산자를 바꿔야 합니다.

정리

  • 형변환은 대입과 초기화, 산술, 함수 호출, 반환처럼 대상 타입이 정해지는 자리에서 일어나며, 허용되는 것과 값이 보존되는 것은 별개입니다.
  • 실수를 정수로 바꾸면 소수점 아래를 버리고, 범위를 넘으면 정의되지 않은 동작이며, 큰 정수를 실수로 바꾸면 정밀도를 잃을 수 있습니다.
  • 산술 연산은 일반 산술 변환으로 공통 타입을 만들며, 부호 있는 음수와 부호 없는 값을 섞으면 음수가 큰 양수로 바뀝니다.
  • C++의 캐스트는 쓰임에 따라 static_cast, dynamic_cast, const_cast, reinterpret_cast로 나뉘며, 괄호 캐스트보다 뜻이 분명합니다.
  • 중괄호 초기화는 표준이 정한 축소 변환을 컴파일 오류로 막습니다.
  • 나눗셈에서 소수부를 얻으려면 나누기 전에 피연산자를 실수로 바꿔야 하며, 결과를 나중에 바꿔도 버려진 값은 돌아오지 않습니다.

연습 문제

1. 입력을 바꾼 평균

divide_timing.cpp에서 total을 101로, count를 4로 바꾸면 세 값은 어떻게 출력되나요?

해설 보기

25.000 25.000 25.250이 출력됩니다. 앞의 두 식은 정수 몫 25를 나중에 double로 바꾸므로 0.25를 잃고, 셋째 식만 101.0 / 4.0 = 25.25를 계산합니다.

2. 음수의 소수점 버리기

static_cast<int>(-3.9)는 -3인가요, -4인가요?

해설 보기

-3입니다. 실수를 정수로 바꿀 때는 0 쪽으로 소수점 아래를 버리므로, 음수에서는 더 작은 수로 내림하는 것이 아니라 -3이 됩니다. 내림이 필요하면 std::floor를, 반올림이 필요하면 std::round를 쓴 뒤 바꿉니다.

3. 부호가 다른 비교

std::vector<int> v;의 크기 v.size()는 부호 없는 정수입니다. int i = -1;일 때 i < v.size()는 비어 있는 v에서 참인가요?

해설 보기

거짓입니다. 비교하기 전에 i가 부호 없는 타입으로 바뀌어 아주 큰 수가 되므로, 크기 0보다 작지 않습니다. 이런 비교는 GCC가 -Wall에서 경고하며, 음수가 될 수 있는 값은 먼저 0 이상인지 확인하거나, C++20의 std::cmp_less처럼 부호를 고려해 비교하는 함수를 씁니다.

다음 글 네임스페이스에서는 std::가 무엇을 뜻하는지, 이름이 서로 부딪히지 않도록 이름 공간을 나누는 방법과 using 선언의 범위를 다룹니다.