티스토리 뷰

C++

[개념] C++의 형변환 연산자

다음김 2022. 7. 2. 22:55

 

해당 글은 '윤성우의 열혈 C++ 프로그래밍'을 참고하여 작성함. (이하 해당 도서라고 지칭)



C++에서는 C에서의 형변환 연산자보다 좀더 제한적인 형태의 형변환이 가능하다. C에서의 (변환 타입)의 형변환 연산자는 정말 말도 안되는 형변환도 오류 없이 실행이 된다. 제한이 없어 유용할 것 같지만 이는 예상치 못한 프로그램 실행 결과가 발생하는 원인이 되기도 한다. C++의 형변환에 대해 설명하기 앞서 왜 C의 형변환 연산자를 제한적으로 사용해야 하는지 먼저 짚고 넘어가고자 한다.

#include <iostream>
using namespace std;

class Basic {
private:
    int num;
public:
    Basic(int num) :num(num) {}
    void showInfo() {
        cout << "num = " << num << endl;
    }
};

class Derived : public Basic {
private:
    int num2;
public:
    Derived(int num, int num2) :Basic(num), num2(num2) {}
    void showInfo() {
        Basic::showInfo(); 
        cout << "num2 = " << num2 << endl;
    }
    void setNum2(int num2) {
        this->num2 = num2;
    }
};

int main() {
    Basic* ptr = new Basic(10);
    ptr->showInfo();
    cout << endl; 

    Derived* ptr2 = (Derived*) ptr;
    ptr2->showInfo();
    cout << endl;

    ptr2->setNum2(20);
    ptr2->showInfo();

    return 0;
}

실행결과

num = 10

num = 10
num2 = 350683

num = 10
num2 = 20

 

기초 클래스를 유도 클래스로 형변환하는 경우 실행결과의 3번째 줄처럼 의도하지 않은 값이 나올 수 있다. 유도 클래스로 형변환되는 과정에서 멤버 변수 num2가 정의되었지만 특정 값으로 초기화되지 않았기 때문에 쓰레기값이 들어있게 된다. (일부 컴파일러에서는 0으로 초기화) 물론 setter 함수를 호출하여 재초기화할 수 있지만 만약 해당 함수 호출을 생략한다면 프로그램에서 예상치 못한 오류가 발생할 수 있다.

 

유도 클래스를 기초 클래스로 형변환하는 것은 자연스러운 현상이며 이로 인해 예기치 못한 결과가 발생하지는 않는다. 즉 형변환 이후 객체는 유도 클래스에서 추가적으로 선언된 멤버 변수, 멤버 함수들을 제외한 기초 클래스에 선언되어 있는 멤버들만을 바라보게 된다. 그러나 기초 클래스를 유도 클래스로 형변환하면 기초 클래스에는 없는 유도 클래스의 추가적인 멤버들이 정의되며 만약 이들을 직접 초기화하지 않는다면 의도치 않은 결과가 발생할 수 있다.




이 예시 외에도 C의 형변환은 정말 말도안되는 (클래스 자료형 → 일반 자료형 → 포인터 자료형) 등의 형변환을 가능하게 하므로 되도록 사용하지 않는 것이 좋다고 한다. 이제 형변환의 케이스를 세부적으로 나누고 각 케이스마다 적절한 형변환 연산자를 사용하기 위해 4가지의 형변환 연산자를 정리해보려고 한다. 해당 도서에서 제시한 4가지 형변환 연산자는 다음과 같다.

dynamic_cast
static_cast
const_cast
reinterpret_cast

 

위 형변환 연산자들은 '형변환 연산자<변환 타입>(형변환 대상)' 방식으로 사용하면 된다.
예를 들어 아래 코드의 C의 형변환은 그 다음 코드와 같은 형태로 작성할 수 있다.

int i = 65;
char ch = (char) i;
int i = 65;
char ch = static_cast<char>(i);

 

이제부터 각 연산자를 어느 경우에 사용하면 되는지 정리해보자.




dynamic_cast

"상속관계에 놓여있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형변환하는 경우"

 

다시 말해 자식 클래스를 부모 클래스로 형변환하는 일반적인 경우를 말한다. 이 외의 경우에서 해당 연산자로 형변환 시도 시, 컴파일 에러가 발생한다.




static_cast

"dynamic_cast의 경우(유도 → 기초) + 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형변환하는 경우"

 

그러나 해당 연산자는 부모 클래스를 자식 클래스로 형변환하는 연산을 허용하기 때문에 글 첫부분에서 예시로 든 예기치 못한 프로그램 실행결과가 발생할 수 있다. 따라서 static_cast 연산자는 이로 인한 부작용을 핸들링할 수 있는 경우에 한해서 사용하는 것이 좋다.



이는 대부분의 형변환을 허용하는 C의 형변환 연산자와 다를바 없게 보이지만 static_cast 연산자는 '기본 자료형 데이터의 형변환', '상속관계에 있는 클래스의 포인터 및 참조형 데이터의 형변환'만을 허용하기 때문에 다음과 같은 형변환에서는 오류를 발생시킨다.

 

int main() {
    const int num = 20;
    int* ptr = (int*)&num;
    *ptr = 30;
    cout << *ptr << endl; //30

    float* adr = (float*)ptr;
    cout << *adr << endl; //4.2039e-44

    return 0;
}

C의 형변환 연산자를 이용한 경우에는 에러 없이 프로그램이 실행되지만 static_cast 연산자를 사용하는 경우에는 다음과 같이 오류가 발생한다.

 

int main() {
    const int num = 20;
    int* ptr = static_cast<int*>(&num);
    *ptr = 30;
    cout << *ptr << endl;

    float* adr = static_cast<float*>(ptr);
    cout << *adr << endl;

    return 0;
}

오류 메시지

static_cast from 'const int *' to 'int *' is not allowed
static_cast from 'int *' to 'float *' is not allowed




const_cast

"포인터와 참조자의 const 성향을 제거하는 형변환의 경우"

 

해당 연산자는 const 성향을 제거하는 것이 유의미한 경우에 대해서만 제한적으로 사용해야 하는데, 이 경우를 잘 보여주는 해당 도서의 예제는 다음과 같다.

#include <iostream>
using namespace std;

void showString(char* str) {
    cout << str << endl;
}

void showAddResult(int& n1, int& n2) {
    cout << n1 + n2 << endl;
}

int main(void) {
    const char* name = "Kim Da Eun";
    showString(const_cast<char*>(name));

    const int& num1 = 100;
    const int& num2 = 200;
    showAddResult(const_cast<int&>(num1), const_cast<int&>(num2));

    return 0;
}

위와 같이 일반변수로 선언된 함수의 인자로 const 변수 전달을 하기 위해 const_cast 연산자가 적절하게 사용될 수 있다. 물론 함수 내에서는 const 변수가 아니므로 매개변수를 통해 상수의 값을 변경할 수 있게 되어 const의 의미가 퇴색되므로 제한적으로 사용해야 한다.




reinterpret_cast

"전혀 상관이 없는 자료형으로 형변환하는 경우"

"포인터를 대상으로 하는, 포인터와 관련이 있는 모든 유형의 형변환"

 

상속관계가 없는, 즉 서로 전혀 관계가 없는 클래스 간 형변환 시 사용하는 연산자이다. 또한 포인터와 관련이 있다면 모든 유형의 형변환을 허용한다. 따라서 이 연산자 또한 매우 제한적으로 사용해야 한다.

int main(void) {
    int num = 72;
    int* ptr = &num;

    int adr = reinterpret_cast<int>(ptr); //주소값을 정수로 변환 
    cout << "Addr: " << adr << endl; //주소값 출력 

    int* rptr = reinterpret_cast<int*>(adr); //정수를 다시 주소값으로 변환 
    cout << "value: " << *rptr << endl; 

    return 0;
}

실행결과

Addr: 140731622664084
value: 72



앞의 dynamic_cast, static_cast, const_cast 연산자로는 일반 자료형 포인터로의 형변환이 불가능하다. 이 경우 포인터와 관련된 모든 형변환을 허용하는 reinterpret_cat 연산자를 사용하면 된다. (참고로 static_cast는 기본 자료형 간의 형변환만 가능하지 기본 자료형 포인터 간의 형변환은 불가능하다.)

int main(void) {
    int tmp = 65;
    int* i = &tmp;
    char* ch = reinterpret_cast<char*>(i); //int* -> char* 형변환
    cout << *ch << endl; //'A' 출력 

    return 0; 
}




부록) Polymorphic 클래스 기반의 형변환

상속관계에 놓여있는 두 클래스 사이에서 유도 클래스 → 기초 클래스 형변환 : dynamic_cast 연산자

상속관계에 놓여있는 두 클래스 사이에서 기초 클래스 → 유도 클래스 형변환 : static_cast 연산자

 

기초 클래스가 Polymorphic 클래스라면 dynamic_cast 연산자를 통해서도 상속관계에 놓여있는 두 클래스 사이의 기초 클래스 → 유도 클래스로의 형변환이 가능해진다. 여기서 Polymorphic 클래스란 '하나 이상의 가상함수를 지니는 클래스' 를 의미한다. 즉 기초 클래스에는 하나 이상의 virtual 함수가 정의되어 있어야 한다. 해당 도서에서 제시한 예제 코드는 다음과 같다.

#include <iostream>
using namespace std;

class SoSimple {
public:
    virtual void showSimpleInfo() {
        cout << "SoSimple Base Class" << endl;
    }
};

class SoComplex : public SoSimple {
public:
    void showSimpleInfo() {
        cout << "SoComplex Derived Class" << endl; 
    }
};

int main(void) {
    SoSimple* simPtr = new SoComplex();
    SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);
    comPtr->showSimpleInfo();

    return 0; 
}

 

원래 기초 → 유도 클래스로의 형변환은 static_cast 연산자를 통해서만 가능하지만, 기초 클래스가 Polymorphic 클래스이기 때문에 dynamic_cast 연산자를 통해서도 해당 형변환이 가능해진다. 만약 SoSimple 클래스에서 virtual 키워드를 삭제하면 'SoSimple is not polymorphic' 라는 오류 메시지가 출력된다.



이외에도 dynamic_cast에서 기초 → 유도 클래스로의 형변환이 가능한 조건이 하나 더 있는데 SoComplex형 포인터가 가리키는 실제 객체가 SoSimple이 아닌 SoComplex형이기 때문이다. 만약 다음과 같이 simPtr 포인터가 가리키는 실제 객체가 SoSimple형이라면 dynamic_cast 연산자는 SoComplex형의 변환된 포인터가 아닌 NULL 포인터를 반환한다. (여기서 포인터가 아닌 참조자를 형변환하는 경우, 참조자를 대상으로는 NULL을 반환할 수 없기 때문에 'bad_cast' 예외가 발생한다.)

int main(void) {
    SoSimple* simPtr = new SoSimple(); //**
    SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);
    comPtr->showSimpleInfo(); //오류 발생!

    return 0; 
}



반면 static_cast 연산자는 기초 클래스가 Polymorphic 클래스이지 않아도, 기초 클래스형 포인터가 가리키는 실제 객체가 유도 클래스의 객체가 아니더라도 기초 → 유도 클래스로의 형변환이 가능하다. 결론적으로 dynamic_cast 연산자가 static_cast 연산자에 비해 좀더 안정적인 형변환을 보장한다는 것을 알 수 있다. 이에 대해 해당 도서에서 소개하는 dynamic_caststatic_cast 연산자 간 동작방식의 차이는 다음과 같다.

 

dynamic_cast : "컴파일 시간이 아닌 실행시간에 안전성을 검사하도록 컴파일러가 바이너리 코드를 생성" → 실행 속도 느림
static_cast : "컴파일러는 무조건 형변환이 되도록 바이너리 코드를 생성 " → 실행 속도 빠름




C에 비해 프로그래머가 상황에 맞게 형변환 연산자를 선택할 수 있는 폭이 넓어졌지만 이에 따라 관련 내용이 좀더 복잡한 것 같다. 그러나 상황을 잘 파악하고 적절하게 형변환 연산자를 사용한다면 더 효율적이고 오류 발생 가능성이 적은 코드를 작성할 수 있을 것이다.




끝.




공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함