형 변환

형 변환이란 프로그램 실행 중에 데이터의 타입을 다른 타입으로 변경하는 것을 말한다. 형 변환은 크게 자동적인(묵시적) 형 변환과 강제적인(명시적) 형 변환으로 나눌 수 있다.

image.png

형 변환 과정에서는 데이터의 손실이 발생할 수도 있고 자동 형 변환의 결과와 명시적 형 변환의 결과가 다를 수도 있다. 따라서 프로그래머는 주어진 상황에서 어느 쪽이 더 적절한지를 판단하여 형 변환 연산자를 사용해야 한다.

자동적인(묵시적) 형 변환

자동적인 형 변환은 컴파일러가 자동으로 수행하는 형 변환으로 대입 연산, 정수 연산, 수식 연산 등에 일어날 수 있다.

대입 연산자의 오른쪽에 있는 값은 왼쪽에 있는 변수의 자료형으로 자동 형 변환된다.

double f;
f = 10;        // 10이 double형으로 변환된 후 변수 f에 대입된다. (올림 변환)
int i;
i = 3.14;      // 3.14가 int형으로 변환된 후 변수 i에 3이 저장된다. (내림 변환)

다음의 수식에서 num1num2int 형 변수라면 수식의 결과도 int 형 일것이다.

num1 + num2    // int + int = int

이때 num1num2char 형인 경우에도 연산 결과는 int 형이 된다.

컴파일러가 정수형 연산시 여러 가지 자료형 형태로 수행하지 않고 하나의 자료형으로 통일하여 실행하기 위해 char 형이나, short 형을 일반적으로 CPU가 처리하기 가장 적합한 크기인 int 형으로 승격시키기 때문이다.

#include <iostream>
#include <typeinfo>

int main()
{
	char num1 = 10;
	char num2 = 20;

	std::cout << "num1 : " << typeid(num1).name() << std::endl;		// num1의 자료형
	std::cout << "num2 : " << typeid(num2).name() << std::endl;		// num2의 자료형
	std::cout << "num1 + num2 : " << typeid(num1 + num2).name() << std::endl;	// num1 + num2의 자료형

	return 0;
}
num1 : char
num2 : char
num1 + num2 : int

수식 연산시 서로 다른 자료형이 사용되면 모든 자료형은 데이터의 손실을 최소화하기 위해 그 중에서 가장 높은 등급의 자료형으로 변환된다.

#include <iostream>
#include <typeinfo>

int main()
{
	float  num1 = 1 + 0.12f;		// int + float -> float + float -> float
	double num2 = 0.12f + 1.003;	// float + double -> double + double -> double

	std::cout << "num1 = " << num1 << ", " << typeid(num1).name() << std::endl;
	std::cout << "num2 = " << num2 << ", " << typeid(num2).name() << std::endl;

	return 0;
}
num1 = 1.12, float
num2 = 1.123, double

강제적인(명시적) 형 변환

명시적 형 변환은 사용자가 형 변환 연산자를 사용하여 강제적으로 수행하는 형 변환을 말한다.

명시적 형 변환은 형 변환 연산자를 통해 할 수 잇다. 형 변환 연산자에는 static_cast, dynamic_cast, reinterpret_cast, const_cast 연산자가 있다.


static_cast

static_cast 는 기본 자료형의 형변환 및 기본 클래스에서 파생 클래스로의 포인터 변환 연산에 사용할 수 있다.

dynamic_cast 의 경우 클래스의 포인터 변환시 모호한 포인터에 대해서는 캐스트에 실패하지만, static_cast 는 아무 문제 없는 것처럼 반환된다. 즉, static_cast 는 런타임 타입 검사(RTTI)를 하지 않기 때문에 dynamic_cast 보다 위험하지만 빠르다. 따라서 주로 기본 자료형의 형 변환에 사용한다.

static_cast 의 특징을 요약하면 다음과 같다.

static_cast 의 형태는 다음과 같다.

static_cast<Type>(Target);

기본 데이터 타입간의 형 변환

#include <iostream>

enum class Enum
{
    Zero,
    One,
    Two
};

int main()
{
    char C = 'a';
    float NumF = 10.123f;

    int NumI1 = static_cast<int>(C);            // 문자형 -> 정수형
    int NumI2 = static_cast<int>(NumF);         // 실수형 -> 정수형
    int NumI3 = static_cast<int>(Enum::One);    // 열거형 -> 정수형

    std::cout << "C : " << C << std::endl;
    std::cout << "static_cast<int>(C) : " << NumI1 << std::endl;
    std::cout << std::endl;
                 
    std::cout << "NumF : " << NumF << std::endl;
    std::cout << "static_cast<int>(NumF) : " << NumI2 << std::endl;
    std::cout << std::endl;

    std::cout << "static_cast<int>(Enum::One) : " << NumI3 << std::endl;

    return 0;
}
C : a
static_cast<int>(C) : 97

NumF : 10.123
static_cast<int>(NumF) : 10

static_cast<int>(Enum::One) : 1

상속 관계에서의 형 변환

#include <iostream>

class A
{
};

class B : public A
{
};

int main()
{
    A* pA = new A;
    B* pB = new B;

    A* pA2 = static_cast<A*>(pB);   // B는 항상 A를 포함하고 있어 안전함
    B* pB2 = static_cast<B*>(pA);   // B가 A는 가지고 있지않은 멤버를 갖고있을 수 있어 안전하지않음

    delete pA;
    delete pB;

    return 0;
}

void 포인터 변환

#include <iostream>

int main()
{
    void* vptr = new int(10);
    int* iptr = static_cast<int*>(vptr);

    std::cout << *iptr << std::endl;

    return 0;
}
10

dynamic_cast

dynamic_cast클래스의 상속 관계에서 포인터나 참조자의 타입을 기본 클래스에서 파생 클래스로 다운 캐스팅하거나, 다중 상속에서 기본 클래스간의 안전한 형 변환에 사용할 수 있다.

dynamic_cast 의 경우 안전한 형 변환을 위해 런타임 타입 검사(RTTI)를 지원하기 때문에 느리다. RTTI 는 런타임에서 클래스의 타입정보를 보고 해당 클래스가 올바른 타입의 형태인지를 판단해준다.

기본 클래스에 virtual 함수가 없는 경우 파생 클래스에서 기본 클래스로의 변환만 가능하며, 기본 클래스에 virtual 함수가 있는 경우 기본 클래스에서 파생 클래스로의 변환(다운 캐스팅)까지 가능하다.

런타임 중에 다운 캐스팅에 실패하는 경우 nullptr 을 반환 한다.

dynamic_cast 의 특징을 요약하면 다음과 같다.

dynamic_cast 의 형태는 다음과 같다.

dynamic_cast<Type>(Target)

기봌 클래스에 virtual 함수가 없는 경우

#include <iostream>

class A
{
};

class B : public A
{
};

int main()
{
    A* pA = new A;
    B* pB = new B;

    A* pA2 = dynamic_cast<A*>(pB);  // 업 캐스팅 성공
    B* pB2 = dynamic_cast<B*>(pA);  // 오류

    delete pA;
    delete pB;

    return 0;
}

기본 클래스에 virtual 함수가 있는 경우

#include <iostream>

class A
{
    virtual void Func() {};
};

class B : public A
{
};

int main()
{
    A* pA = new A;
    B* pB = new B;

    A* pA2 = dynamic_cast<A*>(pB);  // 업 캐스팅 성공
    B* pB2 = dynamic_cast<B*>(pA);  // 다운 캐스팅 실패
    B* pB3 = dynamic_cast<B*>(pA2); // 다운 캐스팅 성공

    std::cout << pA2 << std::endl;
    std::cout << pB2 << std::endl;
    std::cout << pB3 << std::endl;

    delete pA;
    delete pB;

    return 0;
}
000001E030862820
0000000000000000
000001E030862820

reinterpret_cast

reinterpret_cast 는 포인터를 다른 포인터의 형식으로 변환하거나 정수 계열 형식과 포인터 형식간의 변환에 사용할 수 있다.

reinterpret_cast 는 형 변환이 이루어지면 해당 자료형의 비트 수에 맞게 들어간다.

reinterpret_cast 의 형태는 다음과 같다.

reinterpret_cast<Type>(Target);
#include <iostream>

int main()
{
    int* p = new int(100);
    char c = reinterpret_cast<char>(p);
    __int64 ui = reinterpret_cast<__int64>(p);
    
    int* temp1 = reinterpret_cast<int*>(c);
    int* temp2 = reinterpret_cast<int*>(ui);

    std::cout << p << std::endl;
    std::cout << c << std::endl;
    std::cout << temp1 << std::endl;
    std::cout << temp2 << std::endl;
    std::cout << *temp2 << std::endl;

    delete p;

    return 0;
}
00000284B6461960
`
0000000000000060
00000284B6461960
100

const_cast

const_cast 는 포인터와 참조자의 const 성향을 제거할 때 사용할 수 있다.

const_cast 에는 조건이 있다.