본문 바로가기

💻 내 소개 안녕하세요 엄청짱 프로그래머 손다빈 입니다.
  • 나이 : 96년생
  • 특이사항 : MZ세대, INFJ, 오른손잡이, 아이폰 유저
  • 좋아하는 음식 : 햄버거피자치킨솥뚜껑삼겹살떡볶이오튀김밥
  • 취미 : 개발, Programming, 코딩, 프로그래밍, Coding

🥷기술
Unity
Godot
Cpp
Javascript
D3
Vue

🐱 우리집 고양이 소개
츄르 먹은 후 츄르 먹기 전
  • 이름 : 콜라
  • 나이 : 8살
  • 종 : Nado moreum

📱 개인 프로젝트
🏢 참여한 프로젝트
빌런즈 Life is Pair 도씨어부키우기 직장상사혼내주기 서바이벌빙고 SlitherCoin

🌱 내 잔디밭

C++ 형변환 연산자 완벽 정리 본문

글 묶음/지옥에서 온 C++

C++ 형변환 연산자 완벽 정리

초긍정 개발자 다빈맨 2019. 4. 21. 22:58

| C++ 형변환 연산자 (casting operators)

 

 

 

혹시 C++에서 형 변환을 할 때 항상 이렇게 코드를 작성하시나요?

double d = 3.1415; 
int i1 = (int)d; // 형 변환 
int i2 = int(d); // 혹은 이렇게

위 코드처럼 형변환 하는 방식을 C++에서는 C 스타일 형변환 혹은 오래된 형변환 이라고 부릅니다. 이렇게 말하니 벌써 뭔가 구시대의 잔재물처럼 느껴집니다. 이런 형 변환 방식이 아직까지 사용되고는 있지만 공식적으로 C++ 형변환을 위한 연산자가 따로 제공됩니다. 무려 네 가지나!

 

- static_cast

- dynamic_cast

- reinterpret_cast

- const_cast

 

이 네 가지 연산자를 처음 보신다면 이거 없이 잘 살아온 자신에게 시련이 내려진 기분일겁니다. 일단 코드를 봅시다.

double d = 3.1415;
int i1 = static_cast<int>(d); // 형 변환

솔직히 첫인상은 별로입니다. 코드도 길어지고 이름도 뭔가 어렵거든요. 하지만 이 네 가지 연산자를 사용하는걸 권장할만한 타당한 이유가 있습니다. 기존 C 스타일 형변환의 경우에는 아무래도 소괄호"()" 를 이용해서 사용하다 보니 다른 코드와 구분하기 어렵고 의도하지 않은 형변환의 경우에도 가차없이 진행하는 경우가 많기 때문에 이렇게 발생된 에러는 개발자가 발견하기도 어렵습니다.

 

여러분도 그냥 C++의 법칙에 따르는 수 밖에 없습니다. 이 열받는 네 가지 연산자도 놀랍게도 익숙해지면 좀 더 안정적인 코드를 작성하고 있음을 느끼게 됩니다. 

 


 

 

| static_cast 연산자

 

이름을 잘 살펴봅시다. static(정적) 캐스트 연산자네요. 이름에서 알 수 있듯이 이 연산자를 통해 형 변환을 하게되면 컴파일(정적) 타임에 형변환이 가능한지 검사합니다. 이게 얼마나 큰 장점을 가지는지는 예제로 확인할 수 있습니다.

double d = 4.24; 
int* i1 = (int*)&d; // 컴파일 성공! 하지만.. 언젠가 에러를 유발시키지 않을까? 
int* i2 = static_cast<int*>(&d); //컴파일 실패!

논리적으로 &로 얻은 주소값은 항상 정수이기 때문에 int 형 포인터에 담을 수 있습니다. 하지만 i1 변수를 나중에 프로그래머가 접근하게되면 런타임 에러가 발생할 확률이 다분합니다. 반면 마지막 라인에서 static_cast 를 사용한 코드는 컴파일 단계에서 형 변환이 적합한지 검사해주고, 그 과정에서 미리 에러를 발생 시켜줍니다.

 

그리고 한가지 또 특징이 있다면 dynamic_cast 와 달리 static_cast 연산자는 사용자가 정의한 클래스가 아닌 C++ 내부에 이미 정의된 기본 자료형간의 형변환을 하고싶을 때 사용할 수 있습니다. 

double d = 4.14; 
int i1 = static_cast<int>(d);  
int i2 = dynamic_cast<int>(d); // 컴파일 에러! 기본 자료형은 안됩니다.

dynamic_cast 연산자를 이용해서는 기본 자료형간의 형변환은 불가능 하기 때문에 아무래도 사용빈도가 static_cast가 높을 수 밖에 없습니다.

기본 자료형에 한해서는 선택지가 정해져 있습니다. 그냥 static_cast를 사용하면 되겠네요. 이제 여러분은 dynamic_cast 와 static_cast 중에서 뭘 사용할지는 부모-자식 클래스간의 포인터/참조 형식의 형변환에 대해서만 고민하면 됩니다. 여기서는 static_cast의 경우를 먼저 봅니다.

 

" 부모 클래스의 참조/포인터 형식에서 자식 클래스의 참조/포인터 형식으로 형변환을 허용합니다. 그리고

자식 클래스의 참조/포인터 형식에서 부모 클래스의 참조/포인터 형식으로의 형변환도 허용합니다. "

 

일단 부모-자식 관계를 가진 클래스를 정의하겠습니다.

class Water  
{ 
private:     
	int mL; //물의 밀도 
public:     
	Water(int mL) : mL(mL) { }     
    
    void showInfo()      
    {         
        std::cout << mL << "mL" << std::endl;     
    }
};  

class SparklingWater : public Water  
{ 
private:     
    int pH; //수소 이온 농도 
public:     
    SparklingWater(int mL, int pH) : Water(mL), pH(pH) { }     
    
    void showInfo()     
    {         
        Water::showInfo();         
        std::cout << "pH" << pH << std::endl;     
    } 
};

물과 탄산수 클래스를 정의했습니다. 탄산수 클래스의 경우 물을 상속하고 있고 추가로 수소 이온 농도(pH) 데이터를 가지고 있습니다.

Water* water1 = new SparklingWater(150, 15); 

SparklingWater* sparklingWater1 = static_cast<SparklingWater*>(water1); // 컴파일 성공! 충분이 의도할 수 있는 상황 이군요. 
sparklingWater1->showInfo();      

Water* water2 = new Water(150); 
SparklingWater* sparklingWater2 = static_cast<SparklingWater*>(water2); // 컴파일 성공! 근데 의도가 뭐지? sparklingWater2->showInfo();

그리고 위에서 정의한 두 클래스를 사용하는 코드를 작성했습니다. 첫번째로 형변환 후 showInfo 함수를 호출하는건 어느정도 납득이 가는 상황입니다. 하지만 두번째로 형변환을 진행한 코드를 보면 납득하기 힘든 상황이네요. 출력 결과를 살펴봅시다.

150mL pH15 150mL pH32732

마지막 출력라인을 보면 예측하지 못한 쓰레기 값이 출력되었음을 확인할 수 있습니다. 이렇게 문제가 발생될만한 상황인데도 문제없이 컴파일을 진행한 이유는 위에서도 설명했듯이 부모(물)-자식(탄산수) 포인터 간의 형변환도 허용하고, 그 반대인 자식(탄산수)-부모(물) 포인터 간의 형변환도 허용하기 때문에 둘 다 컴파일은 성공합니다. 즉, static_cast는 상속관계에서의 형변환이 그다지 안전하지 못합니다. 

 

| dynamic_cast 연산자

 

static_cast 연산자와 달리 dynamic_cast 는 컴파일 시점이 아닌 런타임 중에 안정성 검사를 진행하고 static_cast가 해결하지 못한 상속관계에서의 형변환을 보다 안전하게 처리할 수 있습니다.

Water* water1 = new SparklingWater(150, 15); 
SparklingWater* sparklingWater1 = dynamic_cast<SparklingWater*>(water1); // 에러! 
sparklingWater1->showInfo();        

Water* water2 = new Water(150); 
SparklingWater* sparklingWater2 = dynamic_cast<SparklingWater*>(water2); // 에러! 
sparklingWater2->showInfo();

static_cast 연산자를 설명할 때 사용했던 코드를 다시 가져왔습니다. static_cast 대신 dynamic_cast 로 바꿨을 뿐인데 두 형변환 모두 에러가 발생합니다. dynamic_cast 가 가지는 정의는 다음과 같습니다.

 

" 자식 클래스의 참조/포인터 형식에서 부모 클래스의 참조/포인터 형식으로 형변환을 허용합니다."

 

위 코드에서 sparklingWater1, sparklingWater2 변수들은 둘 다 Water(부모) 클래스 포인터 타입이기 때문에 자식 클래스 타입인 SparklingWater* 로 형변환 시 둘다 실패하게 됩니다. 그렇기 때문에 다음과 같은 코드는 허용됩니다.

SparklingWater* sparkingWater3 = new SparklingWater(150, 20); 
Water* water3 = dynamic_cast<water*>(sparkingWater3); // 컴파일 성공! 
water3->showInfo();

좋아요. 다 좋은데 이쯤되면 너무 꽉막히다보니 불편한 상황이 생깁니다. 다음 코드를 다시 살펴봅시다.

Water* water1 = new SparklingWater(150, 15); 
SparklingWater* sparklingWater1 = dynamic_cast<SparklingWater*>(water1); // 에러! 
sparklingWater1->showInfo();

객체의 다형성을 이용하다보면 위 코드는 충분히 납득이 갈만한 상황이 분명히 생깁니다. 그래서 static_cast 를 사용했을때도 그에 대한 결과에 문제가 없었고요. 그래서 dynamic_cast 연산자는 한가지 예외적인 상황이 더 붙습니다.

 

" 하나 이상의 가상함수를 가진 다형성 클래스에 한해서는 부모 클래스의 참조/포인터 형식에서 자식 클래스의 참조/포인터로 형변환을 허용합니다 "

 

물과 탄산수 클래스가 제대로된 다형성(Polymorphic)을 가질 수 있도록 수정해야 합니다. C++ 에서 다형성 객체란 하나 이상의 가상함수를 가진 클래스를 의미합니다.

class Water  
{ 
private:     
	int mL; //물의 밀도 
    
public:     
	Water(int mL) : mL(mL) { }     
    
    virtual void showInfo()     
    {         
    	std::cout << mL << "mL" << std::endl;     
    } 
};  

class SparklingWater : public Water  
{ 
private:     
	int pH; //수소 이온 농도 
    
public:     
	SparklingWater(int mL, int pH) : Water(mL), pH(pH) { }     
    
    virtual void showInfo() override     
    {         
    	Water::showInfo();         
        std::cout << "pH" << pH << std::endl;     
    } 
};

showInfo 함수를 가상함수로 수정하고 탄산수 클래스에서 오버라이드 했습니다. 

Water* water1 = new SparklingWater(150, 15); 
SparklingWater* sparklingWater1 = dynamic_cast<sparklingwater*>(water1); // 컴파일 성공! 
sparklingWater1->showInfo();        

/* 
Water* water2 = new Water(150); SparklingWater* 
sparklingWater2 = dynamic_cast<sparklingwater*>(water2); // 컴파일 에러! 
sparklingWater2->showInfo(); 
*/     

SparklingWater* sparklingWater3 = new SparklingWater(150, 20);
Water* water3 = dynamic_cast<water*>(sparklingWater3); // 컴파일 성공! 
water3->showInfo();

드디어 의도한대로 에러 없이 실행됩니다.

 


 

 

 

| reinterpret_cast 연산자

 

reinterpret_cast 는 포인터/참조와 관련된 형변환만 지원합니다. 하지만 위에서 봤던 static_cast 와 dynamic_cast 보다 훨씬 쎈 연산자 입니다. 거의 C 스타일의 형변환 수준으로 막무가내 형변환이 가능하죠. 말 그대로 타입을 재해석(reinterpret) 한다고 볼 수 있네요.

Person* person = new  Person(); 
int* i = reinterpret_cast<int*>(person); //컴파일 성공..!?

위 코드는 에러를 띄우지 않습니다. 포인터간의 형변환 이라면 무조건 진행해버리는 위험한 연산자거든요. 도대체 이 위험한 연산자는 언제 써먹을 수 있을까요? 

int num = 0x040204; 
char* ptr = reinterpret_cast<char*>(&num); 
std::cout << static_cast<int>(*(ptr+1)) << std::endl; // 2가 출력

재밌는 코드입니다. int 형 타입의 변수의 메모리를 바이트 단위로 접근하기 위해 char* 으로 주소를 받아서 + n 으로 접근할 수 있습니다.

 

현재 CPU가 리틀엔디안 방식인지 빅엔디안 방식인지 확인하는 응용도 있더군요.

int num = 0x01020304;
char* ptr = reinterpret_cast<char*>(&num); 

if(ptr[0] == '\x04')     
	std::cout << "little-endian\n"; // 리틀 엔디안 데이터 저장된 방식 : [04] [03] [02] [01] 
else     
	std::cout << "big-endian\n";    // 빅 엔디안 데이터 저장된 방식 : [01] [02] [03] [04]

※ 리틀 엔디안, 빅 엔디안은 CPU가 메모리상에 데이터를 기록하는 방식을 의미합니다. 간단히 말하면 CPU 바이트 단위로 데이터가 메모리상에 배열되는 순서가 역순일 수 있습니다. 

 

이런식으로 reinterpret_cast 연산자는 형변환을 강력하게 강제하다보니 안전하지 않아서 자주 사용되지는 않습니다.

 


 

 

 

 

 

| const_cast 연산자

 

드디어 마지막 const_cast 입니다. 제일 간단하고 이해하기 쉬운 형변환 연산자 입니다. 그래서 마지막에 넣어놨습니다. 이 연산자는 const 성질을 제거하고 싶을 때 사용합니다.

const char* str = "Hello"; 
char* str2 = const_cast<char*>(str); //const 성향을 제거했다! 
cout << str2 << endl;

char* 를 매개변수로 받는 함수를 정의하는 경우가 종종 있는데 그렇게 되면 const char* 타입의 리터럴 문자열은 전달하지 못하게 됩니다. 이 때 const_cast 가 나름 유용하게 사용될 수 있겠네요. 좋아보이긴 하지만 방심하지는 마세요.

const char* str = "Hello"; 
char* str2 = const_cast<char*>(str); //const 성향을 제거했다! 
str2[2] = 'a'; // 에러! cout << str2 << endl;

const 성향을 제거했지만 실제 데이터가 가지는 메모리 배열이 바뀌거나 하는게 아니기 때문에 위와같이 처음부터 읽기전용으로 만들어진 메모리를 억지로 접근해서 바꾸려고 하면 에러가 발생합니다.

 

 


 

 

| 정리

 

- 기본(built-in) 자료형의 형변환에는 static_cast 를 사용하면 됩니다.

- 상속관계에서 안정적인 형변환을 원한다면 dynamic_cast 를 사용하면 됩니다.

- 상속관계에서 때로는 형변환을 강제해야 하는 상황이라면 static_cast 를 사용하면 됩니다.

- 포인터/참조 타입에 상관없이 무조건 형변환을 강제하고 싶다면 reinterpret_cast 를 사용하면 됩니다.

- const 성향을 없애고 싶다면 const_cast 를 사용하면 됩니다.