프로그래밍(c++)/인프런 강의 정리(C++)

강의 내용 정리(Section5 / 동적 할당 및 캐스팅 등)

정체불명의 모모 2024. 5. 29. 00:35

강의 중 배운 내용과 추가적으로 배워야 할 부분들을 정리한 메모장 입니다.

강의 : (인프런) 게임 프로그래머 입문 올인원 


 

전방 선언
: 전방 선언은 실제로 식별자(함수, 변수, 객체) 등을 정의하기 전에 식별자의 존재를 컴파일러에게 알려주는 것이다.

 

  • 특정 클래스에서 다른 클래스를 포인터로 사용할 때, 미리 해당 클래스가 필요하다는 것을 알려준다.
  • 함수의 프로토타입을 전방 선언하여 미리 알려주는것 처럼, 사용할 클래스의 이름을 전방 선언
  • 특정 클래스의 헤더 파일에서 다른 클래스의 헤더 파일을 'include' 하지 않아도 된다.
  • 클래스 멤버 함수가 정의된 cpp 파일에서 다른 클래스 헤더 파일이 필요한 경우 'include' 수행

다만 상속을 할때는 전방선언이 아니라 'include'해서 클래스를 가져와야 한다.

그리고 헤더(header)파일에서 전방 선언된 클래스의 멤버 함수, 변수를 사용 할 수 없다.

#include <iostream>
using namespace std;

class Cat;
class Dog
{
	public :
          Dog();
          ~ Dog();
          
    public:
         Cat* cat;
 }
 
 int main()
 {
     Dog* dog = new Dog;
  }

 

ㅁ 전방 선언을 하는 이유

  • 클래스 헤더 파일에서 다른 클래스 헤더 파일을 include 하지 않아도 된다.
    : 헤더 파일을 추가하는것은 많은 데이터를 복사 붙여 넣기를 하는 것이기 때문에, 헤더 파일을 많이 선언하게 되면
      파일이 무거워진다.
  • 두 헤더 파일이 각가의 헤더 파일을 필요로 하게되는 현상이 발생하는 것을 방지 한다.
    : 서로의 클래스가 필요한 상황이 와서 각각의 클래스의 헤더에 include 하게 되면은 객체간에 무한하게 사이클이 
      돌아가 문제가 발생한다.(컴파일 오류 발생)

얕은 복사 & 깊은 복사

 

ㅁ 객체 복사(Object Copy)

:  기존의 객체의 사본을 생성하는 것.

    가장 흔한 것으로 복사 생성자 등을 통해서 Person B = A; 와 같이 B에 A를 복사해 줄 수 있다.

     

    하지만, 문제점이 있는데 참조 값을 복사하는 경우 원하는 동작 결과가 나오지 않게 된다.

 

  • 얕은 복사란?
    : 얕은 복사는 원본 객체와 복사된 객체가 같은 메모리를 공유하는 방법 입니다.

  • 깊은 복사란?
    : 깊은 복사는 원본 객체와 완전히 독립적인 새로운 객체를 생성하여 값들을 복사하는 것을 말합니다.

ㅁ 깊은 복사와 얕은 복사의 차이점

: 깊은 복사와 얕은 복사는 복사된 객체의 변경이 원본 객체에 영향을 미치는지 여부로 구분됩니다.

   깊은 복사는 복사된 객체가 원본 객체와 독립적으로 동작하며, 얕은 복사는 복사된 객체와 원본 객체가

    메모리를 공유하여 변경 할 경우 서로 영향을 미칩니다.

 

▽ 깊은 복사 예제 코드

#include <iostream>
#include <string>
using namespace std;

class Pet
{
public:
	Pet() {}
	~Pet() {}
public:
	int _age;
	int _name;
};

class Person
{
public:
	Person(): _age(0), _name("")
	{
		_pet = new Pet;
		cout << "Person 얕은 복사" << endl;
	}
	Person(const Person& other)
	{
		_age = other._age;
		_name = other._name;
		_pet = new Pet;

		cout << "Person 깊은 복사" << endl;
	}
	~Person() 
	{
		cout << "Person 소멸자" << endl;
	}

public:
	int _age;
	string _name;
	Pet* _pet;
};

int main()
{
	Person p1 = Person(); 
	Person p2 = Person(p1);	// 깊은 복사
}

_pet의 주소가 다른 것을 확인 할 수 있다.

 

▽ 얕은 복사 예제 코드

#include <string>
using namespace std;

class Pet
{
public:
	Pet() {}
	~Pet() {}
public:
	int _age;
	int _name;
};

class Person
{
public:
	Person(): _age(0), _name("")
	{
		_pet = new Pet;
		cout << "Person 얕은 복사" << endl;
	}

	~Person() 
	{
		cout << "Person 소멸자" << endl;
	}

public:
	int _age;
	string _name;
	Pet* _pet;
};

int main()
{
	Person p1 = Person(); 
	Person p2 = p1;
}

_pet의 주소가 같은 곳을 가르키는 것을 확인 할 수 있다.

 

ㅁ 얕은 복사의 장단점

  • 참조값을 그대로 복사 하기 때문에 다른 객체에서 참조값을 가지고 있는 변수를 메모리 해제(delete) 할 경우
    다른 객체에서도 같은 주소값을 공유하고 있는 변수가 삭제되 오류가 발생하게 된다.(단점)
  • 한 군데서 수정을 하면 다른 객체에서의 참조값을 갖고 있는 변수의 데이터가 다 같이 변경 된다.(단점)
  • 얕은 복사는 메모리 사용량이 적다.(장점)

ㅁ 깊은 복사의 장단점

  • 깊은 복사는 안전하고 독립적인 복사를 제공 한다.(장점)
  • 메모리 사용량이 많다.(단점)

 

ㅁ 깊은 복사와 얕은 복사를 선택하는 기준

  • 객체가 동적으로 할당된 자원을 소유하는 경우 --> 깊은 복사
  • 객체가 동적으로 할당된 자원을 소유하지 않고, 단순한 값들의 집합인 경우 --> 얕은 복사
  • 객체에 소유권 개념이 필요한 경우 --> 깊은 복사

Casting 4 종류
: static_cast , dynamic_cast, const_cast, reinterpret_cast

 

  1. static_cast : 기본 자료형의 형변환 및 기본(base) 클래스에서 파생(derived) 클래스로의 포인터 변환 
    연산에 사용 할 수 있다. 
    형 변환 시점이 컴파일 시점이기 때문에 static 이라는 명칭이 붙는다.
    ( 정적 캐스팅 / 논리적인 캐스팅 / 묵시적 형변환과 같다.)

   static_cast<바꾸려는 타입>(대상)   

     ㅁ static_cast를 사용하는 이유

          - 실수형과 정수형, 정수형과 열거형등의 기본 데이터 타입 간의 변환

          - 상속관계의 클래스 계층 간의 변환

          - void 포인터를 다른 타입의 포인터로 변환

          - 서로 다른 타입의 포인터 간의 타입 변환은 못함

 

     ㅁ static_cast의 제약 사항

          - 런타임 타입 검사를 하지 않음

          - 다형성이 없어도 변환가능(virtual)

          - 다중 상속에서 기본 클래스 간의 타입 변환은 못함

        

    ※ static_cast는  변환이 안전하고 잘 정의되어 있다고 확신할 때 유형 변환을 수행하는데 사용 됩니다.

        그러나 런타임에 변환이 실제로 안전한지 확인하지 않으며 그렇지 않은 경우 컴파일 오류를 생성 합니다.

 

 위의 컴파일 실행 결과와 같이 static_cast를 이용하여 다운캐스팅을 해주었는데, 'testCat'에 'testDog'의 데이터가

       들어가 있는 것을 확인해 볼 수 있다.

그래서 변환이 안전하다고 확실할 때 static_cast를 유용하게 사용해 주면 될 것 같다.


 

2. dynamic_cast : dynamic_cast는 런타임에 유형 변환을 수행하는 C++ 연산자 입니다. 컴파일 타임에 원래 

    값의 유형을 알 수 없거나 변환이 안전하지 않고 잘 정의되지 않은 경우 값을 한 유형에서 다른 유형으로 변환하는데

    사용 됩니다.

 

     dynamic_cast는 상속 관계 안에서 포인터나 참조자의 타입을 기본 클래스에서 파생 클래스로의 다운 캐스팅과

    다중 상속에서 기본 클래스간의 안전한 타입 캐스팅에 사용 됩니다.

 

  • 부모 클래스에 virtual 함수가 없는 경우
    : 자식 클래스에서 부모클래스로만 변환이 가능하며, 자식 클래스에서 부모 클래스로 변환하기 위해서
       부모 클래스를 public 상속 받아야 합니다.

  • 부모 클래스에 virtual 함수가 있는 경우
    : 부모 클래스에서 자식 클래스로 형변환 하는 것 까지 가능하되, 런타임 중에 부모클래스 포인터에서 자식
      클래스 포인터로 형변환이 실패하는 경우 nullptr 반환 합니다.

   dynamic_cast<바꾸려는 타입>(대상)   

ㅁ dynamic_cast의 제약 사항

   - 상속 관계 안에서만 사용 할 수 있다.

   - 하나 이상의 가상 함수를 가지고 있어야 한다.

   - 컴파일러의 RTTI 설정이 켜져 있어야 한다.

 

 

 위의 컴파일 실행 결과와 같이 dynamic_cast를 이용하여 다운캐스팅을 해주었는데

testDog에만 정상적으로 데이터가 들어가 있고,

testCat에는 nullptr의 값을 반환 하고 있다.

그렇기 때문에 어떠한 타입으로 변경 될지 모를 경우 dynamic_cast를 사용하여 보다 안전하게

타입 변경을 해주면 좋을 것 같다.


 

▽ static_ cast 일반 변수의 형변환 예제

#include <string>
using namespace std;

int main()
{
	char a = 'a';
	int charInt = static_cast<int>(a);
}

 

상속 관계에서 형변환(static_cast, dynamic_cast) 예제  & 업 캐스팅  & 다운 캐스팅

#include<iostream>
#include "string"
using namespace std;


class Animal
{
public:
	Animal(): age(0), name("난다용~?") { /*cout << "Animal()" << endl;*/ }
	virtual ~Animal() {/* cout << "~Animal()" << endl;*/ }

	virtual void PrintShout() { cout << "=============== 동물 소개 ================" << endl; }

	void PrintOnly() { cout << "Animal Only : 뭐라고 짖어야 하지?" << endl; }
public:
	int age;
	string name;
};

class Dog : public Animal
{
public:
	Dog() { /*cout << "Dog()" << endl;*/ }
	virtual ~Dog() { /*cout << "~Dog()" << endl;*/ }

 	virtual void PrintShout() override 
	{
		// 부모의 PrintShou 함수 호출
		Animal::PrintShout();

		cout << "멍멍!!!멍멍!!" << endl;
		cout << "나이 : " << age << endl;
		cout << "이름 : " << name << endl;
	}

	void PrintDogOnly() { cout << "왈!! 왈!! 왈!!" << endl; }
};

class Cat : public Animal
{
public : 
	Cat() { /*cout << "Cat()" << endl; */}
	virtual ~Cat() {/* cout << "~Cat()" << endl;*/ }

	virtual void PrintShout() override
	{
		cout << "미야옹~~ 미야옹~~" << endl;
		cout << "나이 : " << age << endl;
		cout << "이름 : " << name << endl;
	}

	void PrintCatOnly() { cout << "키야옹!!!!! 키야옹!!!" << endl; }
};

int main()
{
	Animal * animal = new Animal;
	
	Dog* dog = new Dog();
	dog->age = 3;
	dog->name = "행복이";

	Cat* cat = new Cat();
	cat->age = 10;
	cat->name = "나비";

	Animal* aniArr[2];

	// ---------------------- 업캐스팅 ---------------------------
	aniArr[0] = static_cast<Animal*>(dog);
	aniArr[1] = static_cast<Animal*>(cat);

	cout << "  ----------------------------------------" << endl;
	for (int i = 0; i < 2; i++)
	{
		// 자식의 함수 호출 불가
		aniArr[i]->PrintShout();	
		aniArr[i]->PrintOnly();	
	}
	cout << " ----------------------------------------" << endl;
	// ---------------------- 업캐스팅 ---------------------------

	// ---------------------- 다운 캐스팅 ---------------------------
	Dog* testDog = new Dog();
	testDog = nullptr;

	Cat* testCat = new Cat();
	testCat = nullptr;

	// ---------------------- 다운 캐스팅 ---------------------------
	for (int i = 0; i < 2; i++)
	{
		testDog = dynamic_cast<Dog*>(aniArr[i]);
		testCat = dynamic_cast<Cat*>(aniArr[i]);

		if (testDog != nullptr)
		{
			testDog->PrintShout();
			testDog->PrintDogOnly();
			testDog->PrintOnly(); // 부모의 함수 호출 가능
		}
			

		if (testCat != nullptr)
		{
			testCat->PrintShout();
			testCat->PrintCatOnly();
			testDog->PrintOnly();	// 부모의 함수 호출 가능
		}
			
	}
	// ---------------------- 다운 캐스팅 ---------------------------
	cout << " ----------------------------------------" << endl;
}

제가 테스트 코드를 짠것이라 문제가 있다면 댓글 부탁 드립니다.

 

 

ㅁ static_cast와 dynamic_cast의 차이점

 : static_cast와 dynamic_cast의 주요 차이점은 static_cast는 컴파일 타임에 수행 되는 반면, dynamic_cast는 런타임에

   수행 된다는것 입니다.

 

   이는 static_cast가 dynamic_cast보다 빠르다는 것을 의미하지만 static_cast가 컴파일 타임에 안전한 변환만 수행 할 

     수 있음을 의미하기도 합니다.

   static_cast는 사용 시 변환이 올바르게 이뤄지지 않는다면 컴파일 타임 오류가 발생 합니다.

     반면 dynamic_cast는 컴파일 시간에 안전하지 않은 변환을 수행할 수 있지만, 런타임에 검사를 수행하므로 static_cast

    보다 속도가 느립니다.


가상함수 테이블(virtual table)

: 부모 클래스에서 가상 함수를 선언하면 클래스에 포함된 가상 함수를 관리하는 가상 함수 테이블 'vfable'을 생성하고,

   가상 함수 테이블의 주소를 가리키는 가상 함수 테이블 포인터 'vfptr'를 객체에 할당한다. 

 

   아래의 데이터를 보다 싶이 '_vfptr'의 주소가 aniArr[0] , testDog , dog 가 다 같은 것을 알 수 있습니다.

   그 중 'printShout( )' 함수는 virtual 키워드를 사용한 함수 이고, 이 함수의 주소를 담고 있습니다.

 

  그리고 'printShout( )' 함수가 호출 되엇을 때 'vfptr'로 가상 함수 테이블을 찾아가 호출 할 가상 함수의 주소를 추적 합니다.

 

   각 객체는 고유한 가상 함수 테이블을 가지고, 파생 클래스는 자신이 오버라이딩한 가상 함수에 한하여 테이블 정보를

    업데이트 합니다.