본문 바로가기
C & C++/C++

[C++] 형변환

by 거북이 코딩 2024. 8. 7.

Ch 04. 형변환

형변환 규칙

형변환이 일어나는 다양한 경우

// 변수에 대입할 때
float f = 10; // int 에서 float으로 형 변환이 일어남

// 함수에 파라미터로 전달할 때
void func(float f){ ... }
func(10); // int -> float 형 변환

// 연산할 때
cout << 11.1f + 10; // int -> float 형 변환

promotion, 승급, widening, 확장 변환

// 범위가 좁은 타입이 더 넓은 타입으로 변환 될 때
char c = 'a';
int i = c; // char -> int 로 형 변환

demotion, 강등, narrowing, 축소변환, coercion

// 범위가 넓은 타입이 더 큰 타입으로 변환 될 때
char c = 1999;

// 서로 다른 표현 방법을 사용할 때
int i = 1.1f;

// 데이터 손실이 일어난다, 주의가 필요함

uniform initialization

// 축소 변환이 일어날 수 있다면 중괄호 초기화를 사용한다.
int i = { 1.1f }; // 축소변환이 일어나서 컴파일 에러가 발생

int 보다 범위가 좁은 char, short 등은 산술연산을 할 때 자동으로 int로 변환된다.

unsigned short s = 40000;
cout<<s+s; // short의 최대값을 벗어났지만 오버플로가 나지 않는다.

int 범위 이상의 타입들은 변수 중에 가장 범위가 넓은 타입으로 변환된다.

unsigned int i = 4000000000;
long long ll = 4000000000;
cout<<i+ll; // long long 으로 변환되어 오버플로가 나지 않음

signed 와 unsigned의 연산은 unsigned로 변환된다.

int i = -100;
unsigned int ui = 100;
cout << i + ui; // unsigned로 변환되면서 언더플로우가 발생한다. 주의해야 함

부동소수점끼리의 연산은 long double > double > float의 순으로 큰 타입으로 변환된다.

double d = 100.0;
float f = 10.0f;
cout<<d+f; // double

부동 소수점과 정수를 연산하면 부동소수점 형 으로 바뀐다.

float f = 1.1f;
int i = 1;
cout << f + i; // float

contextual conversion, bool이 들어갈 자리에서는 bool로 변환된다.

ex) if, while, for 과 !, &&, ||, 논리연산자, ? : 삼항 연산자

explicit을 하더라도 변환이 가능하다.

if(bool), while(bool), for( ;bool; ), !bool, bool? : 
// explicit overloading을 하더라도 변환이 일어남

암시적 형변환

int a = 10;
const int& b = a;
const int* c = &a;

상속관계에서의 암시적 형 변환

class Parent
{

};
class Child : public Parent
{

};

Child c;
Parent& p = c;
Parent* ptr = &c;

void func(Parent* parent){ }
func(&c);

부모클래스를 private으로 상속하면 암시적 형 변환이 일어날 수 없다.

class Parent
{

};
class Child : private Parent
{

};

Child c;
Parent& p = c; // 컴파일 에러
Parent* ptr = &c; // 컴파일 에러

private 또는 protected 상속을 하면 외부에서 부모 클래스에 접근할 수 없기 때문에 암시적 변환이 금지되는 것이다.

static_cast

static_cast<타입>(대상)
// 대상을 타입으로 형 변환 한다.
#include <iostream>

using namespace std;

class ClassA
{

};

class ClassB
{

};

int main()
{
	ClassA a;
	ClassB* b = (ClassB*)&a;
	// ClassB* b = static_cast<ClassB*>&a; 적절하지 않은 변환은 오류를 발생시킨다.
	return 0;
}

위 코드는 ClassA를 ClassB로 변환하고 있다. 이러한 안전성이 보장되지 않는 형 변환은 위험하기 때문에 컴파일 타임 내에 변환 가능 여부를 검사해 주는 static_cast를 사용한다

int main()
{
	float f = 1.1f;
	int* i = (int*)&f; // 위험함, 오류가 나타나지 않음
	// int* i1 = static_cast<int*>(&f); // 오류가 발생한다.
	return 0;
}

함수 스타일 형 변환과 static_cast 형 변환

// functional style
bool(num);

// static_cast
static_cast<bool>(num);

상속 관계에서의 캐스팅

#include <iostream>

using namespace std;

class Parent
{

};

class Child : public Parent
{
public:
	int num = 10;
};

int main()
{
	Parent p;
	Child& c = static_cast<Child&>(p); // 오류가 일어나지 않음
	cout << c.num;
	return 0;
}

부모를 자식으로 캐스팅 하는것을 다운 캐스팅이라고 하는데 이러한 다운캐스팅에서는 static_cast도 문제가 발생한다. 사용에 주의해야 한다. 일반적으로는 대부분 static_cast로 해결할 수 있다.

const_cast

포인터 또는 참조형 변수에서 const와 volatile이라는 것을 제거하는 캐스팅이다.

volatile이란 최적화를 하지 않겠다는 의미이다.

volatile int i = 0;
i++; // volatile이 붙지 않으면 최적화 하여 한번에 i에 3을 더한다
i++; // 하지만 volatile이 붙으면 최적화를 하지 않고 3번의 연산을 한다
i++;
return i;

cv(const와 volatile)을 제거하는 캐스팅이 바로 const_cast이다.

원본 데이터에 const가 붙어 있지 않았을 때만 사용해야 한다.

#include <iostream>

using namespace std;

void func(const int& num)
{
	int& ref = const_cast<int&>(num); // const를 제거
	ref = 10; // 변경이 가능해짐
}

int main()
{
	int num = 0; // 원본에 const 없음
	func(num);
	cout << num;
	return 0;
}

원본에 const가 있는 경우 undefined behavior가 되기 때문에 사용하면 안 된다.

const_cast를 의미 있기 사용하는 예시

#pragma warning(disable: 4996)

#include <iostream>
#include <cstring>

class String
{
private:
    char* _chars;

public:
    String(const char* chars) // 생성자
        : _chars(new char[strlen(chars) + 1])
    {
        strcpy(_chars, chars);
    }

    // 기존 구현(중복 코드)
    //char& operator[](int index) // []연산자 오버로딩
    //{
    //    return _chars[index];
    //}

    //const char& operator[](int index) const
    //{
    //    return _chars[index];
    //}

    // const_cast를 이용하여 중복 삭제
    char& operator[](int index) // const가 없는 함수만 호출 할 수 있음
    {
        const String& s = *this; // const [] 를 호출하기 위해
        const char& c = s[index];
        return const_cast<char&>(c); // 리턴할 때 const를 제거
    }

    const char& operator[](int index) const
    {
        return _chars[index];
    }
};

void stringFunc()
{
    String s0("abc");
    std::cout << s0[0] << std::endl; // a

    const String& s1 = s0;
    std::cout << s1[0] << std::endl; // a
}

dynamic_cast

포인터나 참조형 변수들에 대해서 상속관계에서 전환하는 캐스트를 다이나믹 캐스트라고 한다.

dynamic_cast<타입>(대상);
// 대상을 타입을 변환하고, 만약 실제로 가르키는 객체가 타입이 아니라면 nullptr을 반환한다.
// 실제 객체를 확인하고 변환하기 때문에 에러를 줄일 수 있다.

Child c;
Parent* p = &c
dynamic_cast<Child*>(p); // Child를 가르키는 p를 Child*로 변환
#include <iostream>

using std::cout;
using std::endl;

class Parent
{
public:
    virtual ~Parent() {} // 실제 소멸자를 호출하기 위해 virtual 사용
};

class Child : public Parent
{
public:
    void func()
    {
        cout << "func" << endl;
    }
};

void func0(Parent* p)
{
    // 상속 관계에 대한 변환 지원, virtual 함수가 한 개라도 있어야 dynamic_cast가 사용 가능
    // p가 실제로 Parent이면 정의되지 않은 행동
    //Child* child = dynamic_cast<Child*>(p); 
    //child->func();

    Child* child = dynamic_cast<Child*>(p);  // downcasting
    if (child != nullptr)
    {
        child->func();
    }

    // if 내 선언 활용
    if (Child* child = dynamic_cast<Child*>(p)) // downcasting
        child->func();
}

void func1(Parent* p)
{
    // p가 Child가 아닌 경우 예외가 발생, 추후 try catch 활용
    Child& child = dynamic_cast<Child&>(*p); //downcasting
    child.func(); 
}

int main()
{
    Parent* p = new Parent;
    func0(p);
    func1(p);

    // upcasting, 자식으로 부모로 변환 (암시적)
    // downcasting, 부모를 자식으로 변환 (명시적, 위험할 수 있음)
}

cross-cast 같은 부모를 둔 형제 사이의 캐스트

class A
{

};

class B0 : public A
{

};

class B1 : public A
{

};

class C : public B0, public B1
{

};

int main()
{
    C c;
    B0* b0 = &c;
    B1* b1 = dynamic_cast<B1*>(b0); // cross cast
    return 0;
}

reinterpret_cast

비트 배열을 재해석하는 캐스트가 reinterpret_cast이다. undefined behavior를 유발하는 경우가 잦기 때문에 사용 시 주의해야 한다.

#include <iostream>
#include <cfloat>

using std::cout;
using std::endl;

union ID
{
    char chars[10];
    int integer;
};

int main()
{
    ID id;
    id.integer = 10;

    // 해당 비트 배열을 int로 인식하겠다는 의미
    // 사용에 주의
    int* p = reinterpret_cast<int*>(&id);
    cout << *p << endl;

    // 특수한 경우 메모리의 특정 주소에 있는 값을 Device로 다루는 경우가 있을 수 있음
    //Device* p0 = reinterpret_cast<Device*>(0xabcd);

    
    // 0000,0000,0000,0000,0000,0000,0000,0001
    // int, 1의 비트 배열을 flaot 형태로 해석하면 float의 최소 값이 나옴
    int i = 1;
    float* a = reinterpret_cast<float*>(&i);
    cout << *a << endl;
    cout << FLT_TRUE_MIN << endl;

    // 일반 형변환은 단순히 1이 나온다
    float b = i;
    cout << b << endl;

}

C 스타일 함수 스타일 변환

C 스타일 변환과 함수 스타일 변환은 코드를 읽었을 때 어떤 의도의 캐스팅인지 알기 어렵고 하나의 캐스팅 방법이 세 개의 캐스팅을 실행하고 있어서 모호하다는 단점이 있다.

#include <iostream>

using std::cout;
using std::endl;

enum class Type
{
    A, B, C
};

int main()
{
    // C 스타일 캐스팅
    int num0 = (int)Type::A;

    // 함수형 스타일 캐스팅
    int num1 = int(Type::A);
    
    // C, 함수형 스타틸 캐스팅은 아래 세 개의 캐스팅을 시행한다
		int i = 10;
		float& f = (float&)i; // reinterpret_cast

		const int& j = i;
		int& k = (int&)j; // const_cast

		i = (int)Type::A; // static_cast
    
    return 0;
}

클래스의 관점에서 생성과 변환은 일정 부분 호환이 된다.

#include <iostream>

using std::cout;
using std::endl;

class Parent
{
public:
    Parent() {} // 생성자
    explicit Parent(int i) {} // 명시적 변환 생성자
};

int main()
{
    Parent p;

    // 아래는 생성자 호출인가 변환인가?, 둘 다 맞음
    p = Parent(10);
    p = (Parent)10;
}

'C & C++ > C++' 카테고리의 다른 글

[C++] 템플릿, 타입  (0) 2024.08.16
[C++] 예외 처리  (0) 2024.08.08
[C++] 상속  (0) 2024.08.05
[C++] 연산자 오버로딩  (4) 2024.07.16
[C++] 클래스  (0) 2024.07.13