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 |