Program Languege/C/C++

C++에서 형변환 종류

Frozen0113 2013. 2. 20. 01:40

C++에서 형변환 종류

 

static_cast<>()

 

 

논리적으로 변환 가능한 타입만 변환 가능.(컴파일에서 허용하는 한도내에서 형변환 가능.)

내부적으로 에러가 없는지는 검사 안함.(dynamic_cast에서 추가 설명.)

실수형을 정수형으로 캐스팅하거나 반대로 캐스팅하는 것은 허용되며 상호 호환되는 열거형과 정수형, double과 float의 변환 등도 허용되지만 포인터의 경우 다른 것으로 변환하는 것은 허용되지 않는다.(컴파일 시 에러처리) 실수형 포인터를 정수형 포인터로 형변환을 하거나 반대의 경우 등이 이에 포함.

 

위험한 형변환 연산을 컴파일 중에 알려주면서 실수를 방지할 수 있다. 이에 비해 C의 형변환은 너무 친절..?해서 언제나 OK를 해주다가 프로그램이 언제 KO당할지 모른다고 한다.

 

포인터끼리 타입을 변환할 때는 상속 관계에 있는 포인터끼리만 변환이 허용되며 상속 관계가 아닌 경우 변환이 안된다.

 

Ex>   static_cast<변환할 타입>(변환할 대상)

 

- 소스 코드 예시

 

int a = 10;

double b = 15.5;

 

char str[] = "korea";

int *pi;

 

1) b = a;

2) a = b;

3) pi = static_cast(int *>(str);

4) pi = (int *)str;        // C의 형변환.

 

1) b  = a; // 이 경우는 업캐스팅이라 하며 암묵적(묵시적)형변환이 일어난다. 기본 자료형 변수에서는 작은 자료형에서 큰 자료형으로

              // 넘어가는 경우를 말한다. 

 

2) a  = b; // 이 경우는 다운캐스팅이라고 하며 명시적인 형변환을 해주어야한다. 그렇지 않으면 경고나 오류가 발생한다.

              // 위와 반대의 경우로 큰 자료형에서 작은 자료형으로 값이 넘어가면서 값이 손실되는 경우를 말한다.

 

    a = static_cast<int>(d); // 이와 같이 수정해주면 경고가 사라진다.

 

3) pi = static_cast(int *>(str); // 위의 설명대로 포인터의 타입의 변환이기 때문에 컴파일 오류가 발생한다.

 

4) pi = (int *)str; // C의 형변환은 너무나 친절해서 가능하다. 오류는 없음.

                        // cout으로 출력도 되지만 정수형으로 값은 변해있다.

// cout << pi << endl; 의 경우 주소 값. cout << *pi << endl; 의 경우 아래와 같이 정수형 값.

// 결과적으로 코드만 봐서는 결과를 알 수도 없고 의도를 알 수도 없기 때문에 하지 말자.

 

 

- 소스 코드 예시

 

class Parent{};

class Child : public Parent {};

 

int main()

{

Parent P, *pP;

Child C, *pC;

int i = 1;

 

1) pP = static_cast<Parent *>(&C);

2) pC = static_cast<Child *>(&P);

3) pP = static_cast<Parent *>(&i);

4) pC = static_cast<Child *>(&i);

 

return 0;

}

 

1) pP = static_cast<Parent *>(&C);  // 자식 객체(부모 클래스로부터 상속받은 자식 클래스)의 번지를 부모형의 포인터로 업 캐스팅한

                                                   // 다. 상속 관계에서 위쪽으로 변환하는 것(자식 -> 부모로 변환하는 것)을 업 캐스팅이라고 한다.

                                                   // 업캐스팅이기 때문에 묵시적 암묵적 형변환이 되며 언제나 안전하다. pP로 가리킬 수 있는 멤버

                                                   // 변수나 함수는 C에 항상 존재하기 때문이다.

 

2) pC = static_cast<Child *>(&P);   // 위와 반대로 다운 캐스팅이다. 상속 관계가 아래쪽으로 변환(부모 -> 자식으로 변환하는 것)

                                                   // 하기 때문에 다운 캐스팅이라고 한다. 형변환 연산자의 도움 없이는 허가되지 않는다.

                                                   // 하지만 부모 객체가 자식 클래스의 모든 멤버를 가지고 있지 않기때문에 이는 매우 위험한

                                                   // 변환이다. static_cast는 실행 중 타입 체크를 하지 않기 때문에 일단 허용하고 넘어가기 때문에

                                                   // 위험하다. (컴파일에서 에러를 알 수 없음) 부모에게 없는 멤버 함수를 호출할 경우 어떻게 될지

                                                   // 예측할 수가 없다. 이를 막아줄 수 있는 변환이 밑에 나올 dynamic_cast다.

 

3) pP = static_cast<Parent *>(&i);   // Parent 클래스와 상속관계에 있지 않기 때문에 컴파일 에러가 발생한다.

 

4) pC = static_cast<Child *>(&i);    // Child 클래스와 상속관계에 있지 않기 때문에 컴파일 에러가 발생한다.

 

 

 

 

dynamic_cast<>()

 

 

 포인터끼리나 레퍼런스끼리의 형변환에 사용된다.(포인터에서 레퍼런스나 레퍼런스에서 포인터로 변환하는 것은 불가능하다, 상식으로도 필요하지 않다고 함.) 포인터끼리 변환할 때도 반드시 상속 계층에 속한 클래스끼리만 변환할 수 있다.

위에서 설명한 static_cast에서 클래스간 다운 캐스팅의 경우 일어날 수 있는 문제점을 해결할 수 있다. dynamic_cast의 경우 다운 캐스팅을 할 때 무조건 변환을 허용하지 않고 안전하다고 판단될 때만 허용한다.(런타임때 형변환이 가능한지 검사) 형변환이 위험한경우 NULL 반환. NULL을 반환하면 형변환에 실패한 것이기 때문에 멤버에 접근하다 메모리를 침범하는 에러를 막을 수 있다. (업 캐스팅의 경우에는 문제가 발생되지 않음)

 

Ex>    dynamic_cast<변환할 타입>(변환할 대상)

 

안전한 경우 - 부모 클래스 포인터지만 자식 클래스 객체를 가르키고 있을 때 이것을 자식 클래스 형 포인터로 다운 캐스팅하는 경우.

위험한 경우 - 부모 클래스 포인터이며 부모 클래스 객체를 가르키고 있을 때 이것을 자식 클래스 형 포인터로 다운 캐스팅하는 경우.

 

- 소스 코드 예시

 

class Parent

{

public:

 

virtual void PrintMe()

{

cout << "I am Parent" << endl;

}

};

 

class Child : public Parent

{

private:

 

int num;

 

public:

 

Child(int anum = 1234)

{

num = anum;

}

 

virtual void PrintMe()

{

cout << "i am Child" << endl;

}

 

void PrintNum()

{

cout << "Hello Child = " << num << endl;

}

};

 

int main()

{

Parent P, *pP1, *pP2;

Child C, *pC1, *pC2;

 

pP1 = &P;

pC1 = &C;

 

1) pP2 = dynamic_cast<Parent *>(pC1);

2) pC2 = dynamic_cast<Child *>(pP2);

3) cout << "pC2 = " << pC2 << endl;

4) pC2 = dynamic_cast<Child *>(pP1);

5) cout << "pC2 = " << pC2 << endl;

 

return 0;

}

 

1) pP2 = dynamic_cast<Parent *>(pC1); // 업 캐스팅으로 항상 안전하다.

 

2) pC2 = dynamic_cast<Child *>(pP2);  // 다운 캐스팅으로 Parent 포인터 타입에서 Child 포인터 타입으로 바로 대입할 수 없고

                                                        // Child 포인터로 다운 캐스팅해서 대입해야한다. 이때 pP2가 가르키는 대상은 실제 Child 클

                                                        // 래스 객체인 C이기 때문에 캐스팅하고자 하는 타입이 서로 일치한다.그렇기 때문에 성공하

                                                        // 고 pC2가 C를 가르킨다.

 

3) cout << "pC2 = " << pC2 << endl;    // 주소 값이 제대로 출력 됨.

 

4) pC2 = dynamic_cast<Child *>(pP1); // 이번에도 다운 캐스팅의 경우지만 위와는 경우가 다르다. Child 포인터로 다운 캐스팅을 하

                                                       // 지만 실제로 가르키고 있는 대상은 Parent 클래스 객체인 P이기 때문에 다운 캐스팅이 허가

                                                       // 되지 않고 dynamic_cast에서 NULL을 리턴하여 잘못된 캐스팅이라는 것을 알린다.

 

5) cout << "pC2 = " << pC2 << endl;   // 위에서 NULL이 리턴 되었기 때문에 00000000이 출력된다.

 

 

static_cast와 dynamic_cast의 차이점을 알겠는가? 상속 관계에 있는 클래스들끼리 캐스팅을 하는 것은 동일 하지만 static_cast의 경우 무조건 허가해주지만(컴파일 에러 없음. 실행시 오류 발생 가능.) dynamic_cast의 경우에는 안전한 경우에만 허가를 한다는 점이 다르다.(컴파일 에러 없음. 실행시 형변환에 문제가 있는 경우 NULL을 반환함.)

 

dynamic_cast가 변환 가능성을 판단하기 위해서는 실행중 객체의 실제 타입을 판별할 수 있어야 한다. 그래서 RTTI 옵션이 켜져 있어야 하며 변환 대상 타입들끼리는 상속 관계에 있어야 하고 최소한 하나 이상의 가상 함수를 가져야한다. 가상 함수가 없는 클래스 계층이라면 부모 클래스 포인터에 자식 클래스 객체의 번지를 대입할 일이 없을 것이고 캐스팅도 불필요할 것이다.(가상 함수가 없는 경우 굳이 이렇게 쓸 일이 없다는 말)

 

dynamic_cast를 쓰면서 할 수 있는 것? NULL을 반환 받는다는 것은 무슨 의미일까?

 

변환 후에 대입된 값이 NULL이라면 실패 NULL이 아니라면 성공. 이를 사용해서 다음과 같은 응용이 가능하다.

 

void func(Parent *p)

{

p->PrintMe();

Child *c = dynamic_cast<Child *>(p); // 이때 p가 Parent 클래스지만 함수 호출시 Child 클래스를 매개변수로 넘겨받은 경우.

                                                     // 성공적인 다운 캐스팅이 실행된다. Parent 클래스의 매개변수를 넘겨받은 경우는 형변환

                                                     // 과정에서 NULL을 반환해서 c에는 NULL 값이 들어간다.

 

if(C) // c가 NULL이 아닌 경우.

{

c->PrintNum();

}

else // c가 NULL인 경우. 변환에 실패했기때문에 잘못된 접근을 막을 수 있다.

{

cout << "이 객체는 num을 가지고 있지 않습니다." << endl;

}

}

 

레퍼런스에 대해서도 캐스팅할 수 있다고 했는데 레퍼런스는 에러에 해당하는 NULL을 리턴할 수 없으므로 대신 bad_cast 예외를 던진다. 따라서 레퍼런스를 변환할 때는 반드시 캐스팅 코드를 try 블록에 작성하고 bad_cast 예외를 잡아서 처리해야 한다.(try-catch문)

 

 

 

 

const_cast<>()

 

포인터의 상수성만 변경하고 싶을 때 사용한다. 상수 지시 포인터를 비상수 지시 포인터로 잠시 바꾸고 싶을 때 사용한다.

포인터의 const속성을 넣거나 빼거나 할 수 있으며 잘 사용되지 않는 비슷한 성격의 지정자인 volatile속성과 __unaligned속성에 대해서도 변경할 수 있다. 이외에 다른 캐스트 연산자는 포인터의 상수성을 변경할 수 없다.(C 형변환자 제외)

 

Ex>      contst_cast<변환할 타입>(변환할 대상)

 

 

- 소스 코드 예시

 

char str[] = "string";

const char *c1 = str; // 비상수 지시 포인터(str)을 상수 지시 포인터(c1)에 넣을 수 있다. 이때 c1은 읽기 전용이다.

char *c2;

 

1) c2 = const_cast<char *>(c1);

2) c2[0] = 'a';

 

3) cout << c2 << endl;

 

 

1) c2 = const_cast<char *>(c1); // 상수 지시 포인터를 비상수 지시 포인터에 바로 대입이 불가능하다. 상수성이 서로 다른데 읽기 전

                                              // 용인 값을 c2에서 부주의하게 바꿔 버릴 위험이 있기 때문에 const_cast를 사용해서 잠시 상수성을

                                              // 제거하고 대입을 한다. 최초에 대입 받은 str이 변경 가능한 대상이라는 것을 알기때문에 사용한다.

                                              // str이 char *로 선언되어 있다면 이때 str은 실행 파일의 일부분을 가리키고 있으므로 변경할 수 없

                                              // 다. char *로 변경하면 컴파일은 되지만 디버깅과정에서 아래와 같은 오류가 발생한다.

                                              // char *로 선언하면 "string" 값은 상수로 저장이되고 이를 가르키는 포인터가 되기 때문에 상수를

                                              // 변경하려고 했기때문에 오류가 발생한다.

 

 

2) c2[0] = 'a';                          // "string"에서 첫글자인 's'가 'a'로 변경된다.

 

3) cout << c2 << endl;              // "atring" 출력.

 

 

Ex> int *pi = const_cast<int *>(c1);

      d = const_cast<double>(i);

 

정수를 실수형 타입으로 변환하는 것은 업캐스팅이므로 당연히 가능하지만 const_cast의 경우에는 이를 허용하지 않는다. const_cast는 오로지 포인터의 상수성만을 변경할 수 있다. 그래서 상수성을 변경할 때 const_cast를 사용하면 다른 엉뚱한 변환을 피할 수 있어 안전하며 코드를 읽는 사람도 어떤 의도로 캐스트 연산자를 사용했는지 쉽게 파악할 수 있다.(가독성 증가)

 

다음 예시를 보면 C 형변환 연산자의 문제점을 알 수 있다.

 

Ex>   const char *c1;

   char *c2;

   c2 = (char *)c1;

 

c1의 상수성을 잠시 없애서 c2에 대입하기 위해 형변환 연산자를 사용한 경우. 어떤 이유로 c1을 const double *로 변경했다고 가정하자. c1이 가르키는 대상이 char에서 double로 변경된 것이다. 그렇다면 상수성을 없애기 위해 사용한 (char *)가 의미가 완전 바뀌어서 타입을 변경하라는 명령이 된다. 하지만 컴파일러는 아무런 문제없이 넘어간다. c1이 const double *로 변경되었다면 c2도 당연히 변경되어야하는데 컴파일에서 오류가 없으니 못보고 지나칠 수 있는 가능성은 얼마든지 있다. 문제를 알게되더라도 소스를 일일이 다 뒤져 타입 변경하는 것은 무척 번거롭고 일부 수정되지 않고 누락되는 부분이 생길 수 있어 위험하다.

 

다음과 같이 사용한 경우를 보자.

 

Ex>    c2 = const_cast<char *>(c1);

 

이렇게 하면 상수성 변경만을 하기 때문에 c1의 타입이 변경되는 경우 컴파일러에서 에러로 처리를 해주어 위험을 제거할 수 있다. C의 형변환 연산자는 변환의 범위가 너무 넓은데 비해 C++의 형변환 연산자는 기능이 제한적이다.

 

 

 

 

 

reinterpret_cast<>()

 

임의의 포인터 타입끼리 변환을 허용하는 상당히 위험한 형변환 연산자다. 심지어 정수형과 포인터간의 변환도 허용한다. 정수형 값을 포인터 타입으로 바꾸어 절대 번지를 가르키도록 한다거나 할 때 이 연산자를 사용한다.

 

Ex>    reinterpret_cast<변환할 타입>(변환할 대상)

 

 

- 소스 코드 예시

 

int *pi;

char *pc;

 

1)pi = reinterpret_cast<int *>(12345678);

2)pc = reinterpret_cast<char *>(pi);

 

1)pi = reinterpret_cast<int *>(12345678); // 12345678이라는 정수값을 정수형 포인터로 바꾸어 pi에 대입할 수 있고 이 값을 다시 문자

2)pc = reinterpret_cast<char *>(pi);      //  형 포인터로 변경하여 pc에 대입할 수도 있다. 상속 관계에 있지 않은 포인터끼리도 변환

                                                        // 가능하다. 대입을 허가하기는 하지만 이렇게 대입한 후 pi와 pc 포인터를 사용하면서 발생

                                                        // 하는 문제는 개발자가 책임져야한다. 일종의 강제 변환이므로 안전하지도 않고 이식성도

                                                        // 없다. 실행결과 문제 없이 넘어갔지만, 대입 이외에 값을 출력해보기위해 cout을 사용하자

                                                        // 에러가 발생했다.

 

 

기본 타입끼리의 변환에는 사용할 수 없다. 정수형을 실수형으로 바꾸거나 실수형을 정수형으로 바꾸는 것은 안된다.(컴파일 에러)

 

 

 

 

변수의 타입을 변경하는 형변환 연산은 어떤 경우라도 항상 주의해서 사용해야 한다.

 

- 출처 : 열혈강의 C++ 언어본색

 

혼자 연구하는 c/c++

http://www.winapi.co.kr/clec/cpp3/33-2-2.htm - static_cast

http://www.winapi.co.kr/clec/cpp3/33-2-3.htm - dynamic_cast

http://www.winapi.co.kr/clec/cpp3/33-2-4.htm - const_cast

http://www.winapi.co.kr/clec/cpp3/33-2-5.htm - reinterpret_Cast

 

 

※ 공부한 내용을 정리하였기때문에 출처와 내용이 다를 수 있습니다.