코딩 학습/C와 C++

C++ 기초 - Casting, inline 키워드, static 키워드, 예외 처리

이개 2026. 4. 8. 12:52

암시적 캐스팅과 명시적 캐스팅

형변환

해당 자료형을 다른 자료형으로 변환하는 것

 

암시적 형변환

컴파일러가 자료형을 변환해준다. 형변환이 허용되면서, 프로그래머가 명시적으로 형변환을 안 할 경우에만 암시적 캐스팅이 일어난다.

int Num1 = 3;
long Num2 = Num1; 
	// 암시적 캐스팅
	// int 자료형에서 long 자료형으로 형변환이 허용됩니다. 
	// 프로그래머가 명시적으로 형변환하지 않았습니다.
	// 따라서 암시적 형변환이 일어납니다.

 

 

C++의 명시적 형변환 4가지

업 캐스팅(Up-Casting) vs 다운 캐스팅(Down-Casting)

  • 업 캐스팅: 자식 클래스에서 부모 클래스로의 형변환
  • 다운 캐스팅: 부모 클래스에서 자식 클래스로의 형변환

static_cast<>()

  • 컴파일 타임에 자료형을 검사하여 형변환
  • 컴파일 과정에서 얻을 수 있는 자료형 정보를 토대로 형변환 가능한지 검사
    • 형변환 할 수 있다면 컴파일 성공, 형변환 불가하면 컴파일 실패
  • 변수의 자료형으로만 판단하고, 실제 객체의 자료형(동적 할당 시 호출되는 생성자, 힙 메모리에 생성된 객체)은 검사하지 않음.
// Main.cpp

#include <iostream>

#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
#include "Cup.h"

int main(void)
{
	int IntegerNumber = 7;
	float FloatNumber = static_cast<float>(IntegerNumber);
	std::cout << IntegerNumber << ", " << FloatNumber << std::endl;

	Cat* MyCat01 = new Cat(10);

	Animal* MyPet01 = static_cast<Animal*>(MyCat01);
		// MyPet01의 자료형은 Animal*, MyCat01의 정적 자료형은 Cat*.
		// Animal 자료형과 Cat 자료형은 상속 관계이므로 컴파일 성공.

	Cat* MyClonedCat = static_cast<Cat*>(MyPet01);
		// MyClonedCat의 자료형은 Cat*, MyPet01의 정적 자료형은 Animal*.
		// Cat 자료형과 Animal 자료형은 상속 관계이므로 컴파일 성공.
		// 근데 생각해보면, MyPet01에게는 Animal 관련 메모리만 있고, Cat 관련 메모리가 없는데 형변환이 되버립니다.
		// static_cast는 "그런 디테일은 모르겠고, 일단 정적자료형을 봤을 때 상속 가능하면 컴파일 성공시킬게."입니다.

	//Cup* MyCupCat = static_cast<Cup*>(MyClonedCat);
		// MyCupCat의 자료형은 Cup*, MyClonedCat의 정적 자료형은 Cat*.
		// Cup 자료형과 Cat 자료형은 상속 관계가 아니므로 컴파일 에러.

	Animal* MyPet02 = new Cat(2);
	Cat* MyCat02 = static_cast<Cat*>(MyPet02);
		// MyCat02의 자료형은 Cat*, MyPet02의 정적 자료형은 Animal*.
		// Cat 자료형과 Animal 자료형은 상속 관계이므로 컴파일 성공.

	MyCat02->Flexibility = 999.0f;
	MyCat02->Climb();
		// 문제없이 가능.

	Dog* MyCatDog = static_cast<Dog*>(MyPet02);
		// MyCatDog의 자료형은 Dog*, MyPet02의 정적 자료형은 Animal*.
		// Dog 자료형과 Animal 자료형은 상속 관계이므로 컴파일 성공.
		
		// MyPet02의 정적 자료형은 Animal*이지만, 동적 자료형(실체)은 Cat*입니다.
		// 즉, 동적 자료형 Cat* 기준으로 Dog* 자료형으로의 형변환은 잘못된 상황이지만,
		// static_cast는 정적 자료형만 보고 형변환 가능한지 검사하므로, 컴파일 성공해버립니다.
		// 정리하자면 static_cast는 다운 캐스팅에 사용시 위험할 수 있습니다.
		
	MyCatDog->WagTail(); 
		// Cat 클래스의 멤버 변수를 읽어서 출력합니다. 괴기한 동작 발생.
		// 37번째 줄 MyCat02->Flexibility 변수의 값을 수정해보면 알 수 있습니다.

	delete MyCat01;
	MyCat01 = nullptr;
	
	delete MyPet02;
	MyPet02 = nullptr;

	return 0;
}

 

정리

  • 값 자료형(float, double, int, ...)의 형변환 시에만 사용하는 것이 좋다.
  • 포인터 자료형 간의 형변환 시에는 dynamic_cast만 사용한다.
  • 정적 자료형 기준으로 형변환 가능한지를 검사하므로, 다운 캐스팅 시에는 조심히 사용해야하는 경우가 있다.
  • 다운 캐스팅할 때는 dynamic_cast를 사용하는 것이 좋다.

 

dynamic_cast<>()

런타임에 자료형을 검사하여 형변환, 즉 실제 객체의 자료형을 검사하여 형변환.

동적 자료형(런타임 자료형)을 검사했을 때 형변환 불가하다면 nullptr 반환.

// Main.cpp

#include <iostream>

#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
#include "Cup.h"

int main(void)
{
	//int IntegerNumber = 7;
	//float FloatNumber = dynamic_cast<float>(IntegerNumber);
	//std::cout << IntegerNumber << ", " << FloatNumber << std::endl;
		// dynamic_cast는 값 자료형 간의 형변환은 지원하지 않습니다.

	Cat* MyCat01 = new Cat(10);
	Animal* MyPet01 = dynamic_cast<Animal*>(MyCat01);
		// MyPet01의 자료형은 Animal*, MyCat01의 동적 자료형은 Cat*.
		// Cat 자료형에서 Animal 자료형으로 형변환은 가능합니다.
		// Animal 자료형과 Cat 자료형은 상속 관계이므로 캐스팅 성공합니다.

	Animal* MyPet02 = new Animal(5);
	Cat* MyCat02 = dynamic_cast<Cat*>(MyPet02);
		// MyCat02의 자료형은 Cat*, MyPet02의 동적 자료형은 Animal*, 
		// Animal 자료형에서 Cat 자료형으로 형변환은 불가능합니다.
		// dynamic_cast는 캐스팅 실패시 nullptr을 반환해줍니다. 컴파일 에러는 안납니다.
		// 즉, 잘못된 다운 캐스팅에서 nullptr을 반환해주기에 안전합니다.

	//Cup* MyCupCat = static_cast<Cup*>(MyCat01);
		// 컴파일 에러.
	Cup* MyCupCat = dynamic_cast<Cup*>(MyCat01);
	if (nullptr == MyCupCat)
	{
		std::cout << "nullptr == MyCupCat" << std::endl;
	}
		// MyCupCat의 자료형은 Cup*, MyCat01의 동적 자료형은 Cat*.
		// Cup 자료형과 Cat 자료형은 상속 관계가 아니므로 캐스팅 실패합니다.
		// 즉, dynamic_cast 후 nullptr이 아니라면 작업에 사용하면 됩니다.

	Animal* MyPet03 = new Cat(2);
	Cat* MyCat03 = dynamic_cast<Cat*>(MyPet03);
		// MyCat03의 자료형은 Cat*, MyPet03의 동적 자료형은 Cat*.
		// Cat 자료형과 Cat 자료형은 캐스팅 성공합니다.
		// static_cast 기준으로는 Animal 자료형에서 Cat 자료형은 상속 관계라 컴파일 성공되었음.

	if (MyCat03 != nullptr)
	{
		MyCat03->Flexibility = 999.0f;
		MyCat03->Climb();
			// 문제없이 가능.
	}

	Dog* MyCatDog = dynamic_cast<Dog*>(MyPet03);
		// MyCatDog의 자료형은 Dog*, MyPet03의 동적 자료형은 Cat*.
		// Dog 자료형과 Cat 자료형은 상속 관계가 아니므로 캐스팅 실패합니다.
	if (MyCatDog != nullptr)
	{
		std::cout << "MyCatDog is valid." << std::endl;

		MyCatDog->WagTail();
			// static_cast와 다르게 dynamic_cast는 캐스팅 실패하므로
			// 기괴한 동작을 미연에 막을 수 있습니다.
	}

	delete MyCat01;
	MyCat01 = nullptr;

	delete MyPet02;
	MyPet02 = nullptr;

	delete MyPet03;
	MyPet03 = nullptr;

	return 0;
}

 

dynamic_cast 주의사항

// Main.cpp

#include <iostream>

#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
#include "Cup.h"

int main(void)
{
	Cat* MyCat01 = new Cat(10);

	Animal* MyPet01 = dynamic_cast<Animal*>(MyCat01);

	Cat* MyClonedCat = dynamic_cast<Cat*>(MyPet01);

	Cup* MyCupCat = dynamic_cast<Cup*>(MyClonedCat);
	if (nullptr == MyCupCat)
	{
		std::cout << "nullptr == MyCupCat" << std::endl;
	}

	delete MyCat01;
	MyCat01 = nullptr;

	return 0;
}

 

부모 클래스에 virtual 키워드가 단 하나도 없으면 가상함수 테이블이 만들어지지 않는다.

dynamic_cast는 가상 함수 테이블을 뒤져서 동적 자료형을 확인하는데, 가상 함수 테이블이 없으면 dynamic_cast도 불가능하다.

 

dynamic_cast와 참조

// Main.cpp

#include <iostream>

#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
#include "Cup.h"

int main(void)
{
	Cat MyCat01(10);
	Cat& MyCat01Ref = MyCat01;
	Animal& MyPet01Ref = dynamic_cast<Animal&>(MyCat01Ref);

	Animal MyPet02(5);
	Animal& MyPet02Ref = MyPet02;
	//Cat& MyCat02Ref = dynamic_cast<Cat&>(MyPet02Ref);
		// 참조형 캐스팅에 실패하면 예외가 발생합니다.

	//Cup& MyCupCat = dynamic_cast<Cup&>(MyCat01Ref);

	Animal MyPet03(2);
	Animal& MyPet03Ref = MyPet03;
	//Cat& MyCat03Ref = dynamic_cast<Cat&>(MyPet03Ref);
	//MyCat03Ref.Flexibility = 999.0f;
	//MyCat03Ref.Climb();

	//Dog& MyCatDog = dynamic_cast<Dog&>(MyPet03Ref);
	//MyCatDog.WagTail();

	return 0;
}

 

RTTI(Run-Time Type Information, 런타임 타입 정보)

dynamic_cast를 쓰려면 RTTI를 켜야 한다.

RTTI가 켜지면 각 객체마다 typeid를 얻어올 수 있으며, 이 typeid를 통해 어떤 클래스를 상속받는지도 알 수 있다.

RTTI가 꺼지면 dynamic_cast는 업 캐스팅만 가능하고, 다운 캐스팅 시에는 컴파일 에러가 난다.

'예'로 설정해줘야 dynamic_cast에서 다운 캐스팅을 할 수 있다.

 

reinterpret_cast<>()

데이터의 비트 패턴을 수정하지 않고, 강제로 다른 자료형으로 형변환한다.

즉, 연관 없는 두 자료형 사이의 형변환을 허용한다. 포인터와 포인터 아닌 자료형 사이의 변환도 허용한다.

가장 위험한 C++ 명시적 캐스팅이다.

Animal* MyPet = new Cat(2);
unsigned long long MyPetAddr = reinterpret_cast<unsigned long long>(MyPet);
	// MyPet이 포인터이므로, 저장된 주소값을 화면에 출력하고자 unsigned long long형으로 형변환하고 싶습니다.
std::cout << "Address: " << std::hex << MyPetAddr << std::endl;

int* APInt = new int(-10);
	// 비트패턴: 1111 1111 1111 1111 1111 1111 1111 0110
unsigned int* UAPInt = reinterpret_cast<unsigned int*>(APInt);
	// 비트 패턴은 그대로 두고, unsigned int 자료형으로 해석합니다.
	// 결과: 4294967286

 

const_cast<>()

const 또는 volatile attribute를 제거할 때 사용한다. 단, const_cast로 형변환은 불가능하다.

Animal* MyPet = new Cat(2);
const Animal* PtrToMyPet = MyPet;

// ... 수십 줄 ...
// 대부분의 프로그래머들은 const를 없애기 위해 일부러 형변환을 하진 않을 것입니다.
// 수십 줄 뒤라 const를 못보고 실수로 바꾸려고 하는 사례가 더 많습니다.

Animal* MyAnimal = const_cast<Animal*>(PtrToMyPet);
	// 컴파일 성공.
Cat* MyCat = const_cast<Cat*>(PtrToMyPet);
	// 컴파일 에러. 형변환 할거면 다른 캐스트를 사용하라고 함.

 

 

volatile

  • C++ 컴파일러에게 "이 변수는 모르는 사이에 언제든지 값이 변할 수 있으니, 함부로 최적화하지 마라"고 명령하는 지시어다.
  • 컴파일러는 보통 코드를 실행 속도가 빠르도록 최적화하는데, volatile은 그 최적화가 독이 되는 특수한 상황에서 사용한다
  • 변수 앞에 volatile을 붙이면 컴파일러에게 두 가지를 강제한다.
      1. 캐싱 금지: 변수의 값을 레지스터에 저장해두고 재사용하지 마라.
      2. 직접 참조: 값을 읽거나 쓸 때마다 반드시 실제 메모리 주소에 접근해서 확인하라.
  • 일반적인 애플리케이션 개발보다는 임베디드나 시스템 프로그래밍에서 주로 사용

 

C 스타일 캐스팅과 C++ 스타일 캐스팅

// C-Style Casting
int IntegerPI = (int)(3.141592f);

Dog* MyDog = new Dog();
Animal* Animal01 = (Animal*)(MyDog);
delete MyDog;
MyDog = nullptr;

const float ConstFloatPI = 3.141592f;
float FloatPI = (float)(ConstFloatPI); 
	// Primitive Type 간의 형변환에도 쓰이고, Class 간의 형변환에도 쓰이고, ...
	// 만능간장소스인가..? 연두?..
	// 내부적으로 어떤 일이 벌어질지는 우리도 알 수 없습니다.
// C++ Style Casting
int IntegerPI = static_cast<int>(3.141592f);

Dog* MyDog = new Dog();
Animal* Animal01 = dynamic_cast<Animal*>(MyDog);
delete MyDog;
MyDog = nullptr;

const float ConstFloatPI = 3.141592f;
const float* PtrToConstFloat = &ConstFloatPI;
float* PtrToFloat = const_cast<float*>(PtrToConstFloat);
	// C++에서는 C와 다르게 캐스팅을 나누고 명확하게 적어줄 수 있습니다.

 

 

인라인 함수

함수 호출 대신 컴파일 타임에 코드가 붙여넣기 된다. 매크로 함수와 유사하다.

// Animal.h

class Animal
{
public:
	Animal(int InAge);
    
	inline int GetAge() const 
	{ 
		return Age; 
	}
    
private:
	int Age;
	
};

 

함수 호출 오버헤드를 줄이기 위해 인라인 함수를 사용할 수 있다.

 

주의사항

  • inline 키워드를 단다고 무조건 해당 함수가 붙여넣기되지는 않는다.
  • 컴파일 타임에 붙여넣기 하려면 헤더파일에 선언 및 정의해두어야한다.
    • 컴파일 타임에는 링크가 되지 않아서 인클루드 되어있는 헤더 파일에 선언해야하는 것이다.
  • 동일 코드를 여러 번 복붙하기 때문에 실행 파일의 크기가 증가하기 쉽다. 

 

인라인 함수 vs 매크로 함수

매크로 함수는 디버깅하기 힘들다. call stack에 함수명이 표시되지 않고, break point도 설정 불가능하다. 또한 scope도 준수하지 않는다. 

따라서, 매크로를 쓸 이유가 따로 있지 않은 이상에는 인라인 함수를 쓰는 것이 낫다.

 

 

extern 키워드

해당 변수의 정의는 어딘가에 존재할 것이라고 컴파일러에게 선언하는 문법.

전방 선언과 유사하며, 전역 변수를 다른 파일에서도 접근할 수 있게 해준다.

// ExternTest.h

extern int GValue;
	// GValue 변수의 정의가 다른 파일에 존재할 것이라고 컴파일러에게 선언.
	// ExternTest 파일을 인클루드 하는 파일들에서는 GValue를 사용해도 컴파일 성공.

void IncreaseValue();
// ExternTest.cpp

#include "ExternTest.h"

int GValue = 2;
	// 진짜로 정의하는 곳.

void IncreaseValue()
{
    ++GValue;
}
// Main.cpp

#include <iostream>

#include "ExternTest.h" 
	// 이 부분에 extern int GValue; void IncreaseValue(); 가 적히게 됩니다. 
	// 즉, 어딘가에 GValue가 있을 거라는 심볼(빈칸). 링커 과정에서 채워집니다. 

int main()
{
    std::cout << GValue << std::endl;
    
    return 0;
}

 

 

static 키워드

변수를 파일/클래스 함수 범위로 접근 제한하는 키워드

 

파일 범위 static - 정적 전역 변수

// StaticTest.h

void IncreaseValue();
// StaticTest.cpp

#include "StaticTest.h"

static int GValue = 2;
	// GValue 변수는 StaticTest.cpp 파일 내에서만 접근 가능합니다.
	// 다른 .cpp 파일에서는 사용할 수 없습니다.

void IncreaseValue()
{
    ++GValue;
}

 

함수 범위 static - 정적 지역 변수

// Main.cpp

#include <iostream>

void CreateCanCoke(void)
{
    static int CreatedCanCokeCount = 0; 
    // CreatedCanCokeCount 변수는 CreateCanCoke() 함수 내에서만 접근 가능합니다.
		// 특이하게, CreatedCanCokeCount 변수는 스택 메모리가 아니라 데이터 섹션에 할당됩니다.
		// 그래서 CreateCanCoke() 함수가 종료돼도 CreatedCanCokeCount 변수 값이 날아가지 않습니다.
		// 그리고 선언과 동시에 초기화하면 CreateCanCoke() 함수가 다시 호출되었을 때 또 초기화 되지 않습니다.
		
    ++CreatedCanCokeCount;
    std::cout << "CreatedCanCokeCount: " << CreatedCanCokeCount << std::endl;
}

int main()
{
    CreateCanCoke();
    CreateCanCoke();
    
    return 0;
}

 

클래스 범위 static - 정적 멤버 변수

정적 멤버 변수도 스택 메모리가 아니라 데이터 섹션에 할당된다. 단, 정적 멤버 변수의 이름만 클래스에 소속된다.

따라서 객체 수에 따라 정적 멤버 변수의 개수가 증가하거나 감소하지 않는다.

 

// Cat.h
#pragma once

#include <string>

class Cat
{
public:
    Cat();
    
private:
    static float TailLength;
    
};
// Cat.cpp 

float Cat::TailLength = 5.0f; 
	// cpp 파일에서 초기화 해줘야함에 주의합시다.
	// header 파일에서 초기화할 경우 중복 정의 에러가 발생 할 수도 있습니다.

Cat::Cat()
{
}

 

 

  • 함수 안에서 정적 변수를 쓰기보다는 클래스 안에 정의한다.
  • 전역 변수 대신 public + 정적 멤버 변수 조합으로 대체하여 범위를 제한하자.

 

클래스 범위 static - 정적 멤버 함수

클래스 범위에 제한된 전역 함수. 멤버 함수와 달리 호출 시에 this 포인터가 필요하지 않아 객체가 없어도 정적 멤버 함수는 호출 가능. 

Math::Square(12);
	// 범위 지정 연산자로 어떤 클래스의 함수인지만 지정해주면 됩니다.

 

// MyMath.h

#pragma once

class MyMath
{
public:
	static int Ceil(float InValue);
	
	static int Floor(float InValue);
	
	static float Power(float InValue, float InPower);
	
	static int Round(float InValue);
	
	static float Square(float InValue);

private:
	MyMath() {};
	
public:
	static double PI;

};

 

// MyMath.cpp

#include <cmath>

#include "MyMath.h"

double MyMath::PI = 4.0 * std::atan(1.0);

int MyMath::Ceil(float InValue)
{
	int IntValue = static_cast<int>(InValue);

	if (InValue == static_cast<float>(IntValue))
	{
		return IntValue;
	}

	return IntValue + 1;
}

int MyMath::Floor(float InValue)
{
	return static_cast<int>(InValue);
}

float MyMath::Power(float InValue, float InPower)
{
	return std::pow(InValue, InPower);
}

int MyMath::Round(float InValue)
{
	return static_cast<int>(InValue + 0.5f);
}

float MyMath::Square(float InValue)
{
	return InValue * InValue;
}

 

// Main.cpp

#include <iostream>

#include "MyMath.h"

int main(void)
{
	std::cout << "PI: " << MyMath::PI << std::endl;
	std::cout << "Ceil(3.14): " << MyMath::Ceil(3.14f) << std::endl;
	std::cout << "Floor(3.14): " << MyMath::Floor(3.14f) << std::endl;
	std::cout << "Power(2.0, 3.0): " << MyMath::Power(2.0f, 3.0f) << std::endl;
	std::cout << "Round(3.14): " << MyMath::Round(3.14f) << std::endl;
	std::cout << "Square(3.14): " << MyMath::Square(3.14f) << std::endl;

	return 0;
}

 

static 함수는 헤더만 인클루드하면 범위 지정 연산자만 있어도 사용 가능!

 

static 키워드와 변수 접근

해당 변수 특정 여부가 관건.

멤버 함수의 멤버 변수 접근은 가능하다. 해당 객체(this)의 멤버 변수이므로 특정 가능.
멤버 함수의 정적 멤버 변수 접근은 가능하다. 정적 멤버 변수는 클래스 단위로 한 개만 존재하므로 특정 가능.
정적 멤버 함수의 멤버 변수 접근은 불가능하다. 어떤 객체의 멤버 변수인지 특정 불가.
정적 멤버 함수의 정적 멤버 변수 접근은 가능하다. 정적 멤버 변수는 클래스 단위로 한 개만 존재하므로 특정 가능.