티스토리 뷰

형변환

C에서 부터 캐스트 연산자는 변수 타입을 바꾸는 방면에서 편리하고 유연한 코드 작성을 하도록 도와준다. 하지만 그 결과 실제 데이터의 크기가 변환되는 과정에서 데이터가 손상되거나 메모리 공간이 부족할 경우 할당할 수 없게 될 수 있다.

 

또한 클래스 간 형변환에서 가장 중요한 사실은 "컴파일러는 다운캐스팅이 올바르게 일어났는지 모른다."는 것이다. 하여 개발자는 형변환이 가능한지 항상 먼저 확인해야하고 정말 확인을 못하는 경우 C++에서 지원하는 형변환 연산자들을 통해 변환이 가능한 상황을 확인하고 진행해야한다.

 

C++는 형변환을 좀 더 안전한 변환을 위해 4개의 형변환 연산자를 제공한다. 형변환에 대한 글은 이전 글에도 있으니 잠시 살펴보자.

 


형변환 연산자의 필요성

타입 안전성 및 명확성

C의 형변환 연산자는 너무 느슨한 경향이 있다. 이에 C++는 변수나 객체의 타입을 명시하여 프로그램의 안정성을 높이고 코드 명확성을 유지한다. 하지만 때로는 C++도 완벽히 다른 타입으로 변환을 해야하는 경우가 있을 수 있다.

 

느슨한 데이터형 변환
#include <stdio.h>

typedef struct Data {
	double data[200];
} Data;

typedef struct Junk {
	int junk[100];
} Junk;


int main() {
    Data d = {2.1, 3.1, 4.1};
    
    char *pch = (char*) (&d) ; // 데이터형 변환 #1 - 문자열로 변환
    char ch = (char) (&d) ; // 데이터형 변환 #2 - 주소를 문자로 변환
    Junk *pj = (Junk*) (&d) ; // 데이터형 변환 #3 - Junk
    
    printf("%s\n", pch);
    printf("%c\n", ch);
    printf("%p\n", pj);
    
    return 0;
}
main.c: In function ‘main’:
main.c:16:15: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   16 |     char ch = (char) (&d) ; // 데이터형 변환 #2 - 주소를 문자로 변환
      |               ^
������

0x7ffe91132c00

 

위 코드에서 뭔가 이상한 방향으로 진행되는 것 같은 형변환들이 3가지 데이터 변환이 일어난다. 주소를 문자나 문자열로 바꾸고 심지어는 객체로 전환도 가능하다. 이는 주소 값을 이루는 bit들은 다른 자료형들도 그대로 사용하기 때문에 문제가 없을 뿐 제대로된 형변환이 아니다.

 

프로그램의 요구사항

C++도 버전이 업데이트되며 더욱 다양한 데이터 타입을 사용할 수 있는 환경이 구축되고 있다. 이때, 데이터 간 변환을 효율적으로 처리해야 한다. 

 

포인터와 참조 간의 변환

포인터나 참조를 통한 변수 작업에 왜 캐스트 연산자가 유용하다. 포인터 변수의 타입을 변경하고자 한다면 비트 수준에서 재해석을 해서 다른 포인터로 변환이 필요하다. 이 과정에서 사용가능한 비트 주소를 확보하거나 불필요한 비트를 제거하는 과정이 필요하다.

int intValue = 42;
int* intPointer = &intValue;
double* doublePointer = reinterpret_cast<double*>(intPointer);

 

다형성 지원

다형성 = 상속 관계의 구조에서 클래스간 형변환, 다운캐스트와 업캐스트가 일어날 때 클래스 객체 자체의 확장과 축소가발생할 수 있기 때문에 캐스트 연산자가 사용된다.

class Base {
    // ...
};

class Derived : public Base {
    // ...
};

Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

 

상수성 조작

포인터나 참조를 통해 상수값의 주소를 받아 상수 변수를 상수가 아닌 변수로 캐스팅하거나, 상수가 아닌 변수를 상수로 변환할 수 있다.

const int constValue = 42;

// constValue를 const에서 제거하고자 함
int* nonConstPointer = const_cast<int*>(&constValue);

// 변수를 상수로 만들고자 함
int variable = 100;
const int* constPointer = &variable;

 

 


RTTI (Runtime type identification)

RTTI는 C++98부터 추가된 기능으로 프로그램 런타임 중 객체의 타입을 결정/확인하는 기능을 제공한다. RTTI를 사용하는 것은 종종 코드의 유연성을 제공하지만, 너무 자주 사용하거나 오용할 경우 코드의 복잡성을 증가시키고 성능에 영향을 미칠 수 있다.

 

RTTI의 목적

RTTI는 런타임 중 '다형적 클래스'(polymorphic class)의 형변환에 사용된다.

📌 다형적 클래스

다형적 클래스란 가상 소멸자 혹은 가상 함수가 하나라도 포함된 클래스이다.

 

하나의 부모 클래스를 상속 받은 다수의 자식 클래스가 있다. 이 클래스 계층에 속한 자식 클래스들은 부모 클래스 인스턴스 포인터에  되도록 지시할 수 있다. 이러한 다운캐스팅을 할 경우, RTTI는 사용할 타입을 확인하고 안전한 다운캐스팅을 검사할 수 있다. 실행 시간에 객체의 타입 정보를 얻어서 로깅, 디버깅 또는 다른 동적인 상황에서도 활용할 수 있다.

 

일반적 다운캐스팅
class Parent { /* ... */ };
class Child1 : public Parent { /* ... */ };
class Child2 : public Parent { /* ... */ };

Parent *b = new Child1; // 다형적 클래스 Parent 객체 선언

 

 

RTTI의 동작 방식

동작 설명
dynamic_cast 부모 클래스 포인터에 자식 클래스의 포인터를 생성, 자식 클래스 생성이 불가능하면 NULL(=0) 반환
typeid 반환할 객체의 정확한 데이터형을 식별하는 값 반환
type_info 특정된 데이터형에 대한 정보를 저장하는 구조체

 

RTTI는 가상 함수들을 가지고 있는 클래스 계층에 대해서만 사용할 수 있다. 그 이유는, 자식 객체들의 주소를 부모 클래스 포인터들에 대입해야 하는 클래스 계층이기 때문이다.

 

 


dynamic_cast

dynamic_cast는 RTTI에서 가장 많이 사용되는 요소이며 런타임 중 안전한 형변환을 보장다. 반드시 다형성 클래스에게만 사용이 가능하기에 RTTI가 필수이다.

 

dynamic_cast는 L-value에 R-value 자료가 들어갈 수 있는지 확인한다. 만약 변환이 가능하다면 해당 값으로 변환한 값을 할당하고 그렇지 않는다면, NULL을 반환한다. 하여 포인터에 입력되는 객체형의 자료형을 알려주지 않지만 포인터에 안전하게 대입할 수 있는지 알려 주는 것이다.

 

dynamic_cast는 주로 다운캐스팅에 사용된다. (업캐스팅은 애초에 안전하기에 필요하지 않다.) 혹은 다운캐스팅할 수 있는 객체인지 확인할 때 사용할 수도 있다.

 

다운캐스팅
class Base {
    virtual void foo() {}
};

class Derived : public Base {
    // ...
};

int main() {
    Base* basePtr = new Derived;

    // dynamic_cast를 사용하여 다운캐스팅
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

    if (derivedPtr != nullptr) {
        // 다운캐스팅이 성공한 경우 실행
        // ...
    } else {
        // 다운캐스팅이 실패한 경우(nullptr 반환) 실행
        // ...
    }

    delete basePtr;
    return 0;
}

 

위 코드는 dynamic_cast를 통해 다운캐스팅을 진행한 코드이다. 부모 클래스 포인터나 참조를 하위 클래스 포인터나 참조로 변환이 가능하다.

 

다형적 객체 검사
class Base {
public:
    virtual ~Base() {} // 다형성을 위해 가상 소멸자 필요
};

class Derived : public Base {
    // ...
};

void checkObjectType(Base* basePtr) {
    if (dynamic_cast<Derived*>(basePtr) != nullptr) {
        // Derived 타입의 객체
        // ...
    } else {
        // Derived 타입이 아닌 객체
        // ...
    }
}

 

위 코드는 객체가 특정 타입인지 확인하는 것이다. 타입 변환이 불가능하다면 반드시 NULL이 반환될 것이기에 이를 통해 예외처리나 다른 조건을 걸 수 있다.

 

 


typeid 연산자 & type_info 클래스

typeid 연산자특정 객체나 클래스의 타입을 반환한다. type_info 클래스는 이러한 타입 정보를 가지고 두 객체 타입이 같은지 결정하는 비교 연산자를 포함한 클래스이다. (해당 클래스와 연산자는 typeinfo 헤더에 포함되어 있다.)

 

typeid를 사용하면 어떤 객체 혹은 표현식의 정적 타입을 확인할 수 있다. 주로 RTTI와 함께 사용되며, 다형성 객체의 경우 실제 타입 확인에 사용된다.

 

typeid 연산자 타입 확인
#include <typeinfo>

// 객체 또는 표현식의 타입 정보 가져오기
const std::type_info& type_info_object = typeid(expression);
typeid 연산자 비교 연산
#include <typeinfo>
#include <iostream>

using namespace std;

class class1 {};
class class2 {};

int main(void) {
	class1 c1;
    class2 c2;
    
	if (typeid(c1) == typeid(c2)){
    	cout << "Match" << endl;
    } else {
    	cout << "Not Match" << endl;
    }
	return 0;
}
Not Match

 

위 두 코드는 모두 typeid를 활용하여 객체의 타입을 받고 type_info 클래스에 정의된 비교 연산자를 활용하여 두 객체가 동일한 타입을 가지는지 확인하는 예제이다.  (※ 만약 typeid가 받은 객체 포인터가 NULL이라면 bad_typeid 예외가 발생한다.)

 

type_info 클래스는 구현 업체마다 내부 구조가 조금씩 다르다. 하지만 클래스 이름 값을 반환하는 name() 멤버 함수가 포함되며 다음과 같은 구문이 가능해진다.

 

name 함수
cout << "지금 " << typeid(*pg).name() << "형을 처리하고 있습니다.\n";

 

 


static_cast

static_cast는 컴파일 타임에 안전한 형변환을 수행하도록 도와준다. 다른 말로 컴파일러에게 명시적인 형변환을 지시하는 역할을 한다.

 

static_cast 자체가 런타임에 안정성 검사가 이루어지지 않기 때문에, 개발자 스스로가 실제로 안전한 형변환이라고 판단되는 경우에 사용되어야 한다. 만약 안정성이 보장되지 않은 형변환이 필요한 경우 dynamic_cast나 다른 적절한 연산자를 사용해야한다.

 

물론 이말만 들으면 모든 것을 dynamic_cast로 처리하면 되지 않을까 싶지만 기본적으로 두 연산자는 동작하는 시간과 조건이 다르기 때문에 극명한 차이가 있다.

 

비교 static_cast dynamic_cast
동작 시간 컴파일 시간에 형변환 수행 런타임에 형변환 수행
속도 속도가 빠름, 실행 시간 비용이 거의 발생하지 않음 런타임에 실행되어 확인 절차가 존재하여 상대적 느림
사용 범위 거의 모든 자료형에 가용 가능 다형성 클래스 계층 구조에만 사용

 

위 차이로 인해 성능이 중요하다면 static_cast를, 안정성이 필요하다면 dynamic_cast를 사용하는 적절한 센스가 필요하다.

 

static_cast 형변환은 기본 자료형에서 클래스, 구조체까지 다양하게 사용이 가능하다.

 

기본 자료형 변환
int intValue = 42;
double doubleValue = static_cast<double>(intValue);

 

열거형 변환
enum class Color { Red, Green, Blue };
int intValue = static_cast<int>(Color::Green);
상속 관계 다운캐스팅
Base* basePtr = new Derived;
Derived* derivedPtr = static_cast<Derived*>(basePtr);

 

위의 경우 static_cast로 다운캐스팅이 되는 과정이며 컴파일러는 오류를 감지하지 못한다. 즉, 유효성 검사 없이 그러려니하고 넘어가는 것이다.

 

상속 관계 업캐스팅
class Base { /* ... */ };
class Derived : public Base { /* ... */ };

Derived derivedObj;
Base* basePtr = static_cast<Base*>(&derivedObj);

 

위 경우 static_cast를 통해 업캐스팅이 수행된다. 업캐스팅 자체가 안전한 변환이기 때문에 큰 문제는 발생하지 않는다.

 

 


const_cast

const_cast는 상수성(const-ness)을 추가/제거하는 연산자로 포인터 또는 참조를 통해 변수에 상수 속성을 조작한다.

 

애초에 상속 값이라는게 정말 특별한 목적으로 사용되는 경우가 많기에 상속 속성을 부여하는 것은 어느정도 이해가 가더라도 상수 속성을 제거하는 것은 위험하다. 하여 const_cast는 신중하게 사용되어야 한다.

 

const_cast는 포인터 간 사용이 가능하다. 애초에 상수 값이기에 새로운 변수를 할당할 수 없기에 포인터가 아닌 상태로 변환이 불가능하다.

 

const_cast
int nonConstVariable = 15;
const int constValue = 42;

// const_cast를 사용하여 const 속성 제거
int* nonConstPtr = const_cast<int*>(&constValue);

cout << &constValue << ' ' << nonConstPtr << endl;

*nonConstPtr = 10;

cout << constValue << endl;
cout << *nonConstPtr << endl;

// const_cast를 사용하여 const 속성 추가
const int* constPtr = const_cast<const int*>(&nonConstVariable);
0x7ffeeb811444 0x7ffeeb811444
42
10

 

위 코드는 const_cast를 통해 상수 값을 추가/제거하는 예제이다. 그런데 결과를 확인하면 상수 값과 일반 포인터가 동일한 주소를 가리키지만 서로 다른 값을 출력하는 것을 볼 수 있다. 이는 다음과 같은 동작이 일어나기에 발생하는 현상이다.

 

  1. const_cast 포인터 변수에 값을 할당하면 원본 상수 값의 값도 변경된다.
  2. 원본 상수 값을 호출하면 데이터 영역의 원본 상수 값을 출력한다.
    • 원본 상수 값을 가리키는 주소값과 별개로 상수 변수 본연의 값을 유지하는 형태의 값이 존재한다.
    • 가상 함수 테이블 마냥 따로 정리해주는 것이 있는 것 같다.

 

위 이류를 근거로 const_cast로 다른 포인터 변수를 생성하고 다른 값을 할당할 경우, 변경된 값에 대한 정보는 다른 상수 값 포인터를 통해 확인할 수 있게 된다.

 

const int constValue = 42;

// const_cast를 사용하여 const 속성 제거
int* nonConstPtr = const_cast<int*>(&constValue);

*nonConstPtr = 10;  // constValue를 간접적으로 수정

const int* changedValue = &constValue;  // constValue의 주소를 가리키는 포인터

cout << "Address of constValue: " << &constValue << endl;
cout << "Address of nonConstPtr: " << nonConstPtr << endl;
cout << "Address of changedValue: " << changedValue << endl;

cout << "constValue: " << constValue << endl;
cout << "*nonConstPtr: " << *nonConstPtr << endl;
cout << "*changedValue: " << *changedValue << endl;
Address of constValue: 0x7fffbc5d4f14
Address of nonConstPtr: 0x7fffbc5d4f14
Address of changedValue: 0x7fffbc5d4f14
constValue: 42
*nonConstPtr: 10
*changedValue: 10

 

 


reinterpret_cast

reinterpret_cast는 포인터 형식 사이의 포인터 혹은 다른 데이터 형식의 형변환을 수행한다. 이 연산자는 다른 형식 간의 안전하지 않은 형변환을 허용하고, 컴파일러에게 특정 바이트 패턴을 다른 형식으로 해석하도록 지시할 수 있다.

 

포인터 간 형변환
int* intValuePtr = new int(42);
double* doubleValuePtr = reinterpret_cast<double*>(intValuePtr);
포인터와 정 간 형변환
int intValue = 42;
int* intValuePtr = &intValue;
uintptr_t intValueAsUInt = reinterpret_cast<uintptr_t>(intValuePtr);
객체의 비트 패턴 복사
A a;
A b;
a.x = 42;
a.y = 3.14;

// memcpy를 통한 비트 패턴 복사
//memcpy(&b, &a, sizeof(A));

// reinterpret_cast를 사용한 비트 패턴 복사
b = *reinterpret_cast<A*>(&a);

cout << a.x << ", " << a.y << endl;
cout << b.x << ", " << b.y << endl;

 

위 예제에서 reinterpret_cast를 통해 A 구조체의 비트를 다른 A 구조체 변수에 복사하고 있다. memcpy를 통해서도 가능한 비트 복사지만 안전한 방법으로 복사되는 것이 아니기에 주의가 필요하다.

 

이러한 위험한 형변환을 지원하는 연산자도 모든 것을 허용하지는 않는다. 예를 들어, 포인터형을 포인터 표기를 충분히 저장할 수 있는 큰 정수형으로 캐스트할 수 있지만, 포인터형 보다 작은 형태로 캐스트할 수 없다. 또한 함수 포인터와 데이터 포인터 간 캐스트할 수 없다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함