정체불명의 모모
강의 내용 정리(Section5 / 동적 할당 및 캐스팅 등) 본문
강의 중 배운 내용과 추가적으로 배워야 할 부분들을 정리한 메모장 입니다.
강의 : (인프런) 게임 프로그래머 입문 올인원
전방 선언
: 전방 선언은 실제로 식별자(함수, 변수, 객체) 등을 정의하기 전에 식별자의 존재를 컴파일러에게 알려주는 것이다.
- 특정 클래스에서 다른 클래스를 포인터로 사용할 때, 미리 해당 클래스가 필요하다는 것을 알려준다.
- 함수의 프로토타입을 전방 선언하여 미리 알려주는것 처럼, 사용할 클래스의 이름을 전방 선언
- 특정 클래스의 헤더 파일에서 다른 클래스의 헤더 파일을 '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); // 깊은 복사
}
▽ 얕은 복사 예제 코드
#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;
}
ㅁ 얕은 복사의 장단점
- 참조값을 그대로 복사 하기 때문에 다른 객체에서 참조값을 가지고 있는 변수를 메모리 해제(delete) 할 경우
다른 객체에서도 같은 주소값을 공유하고 있는 변수가 삭제되 오류가 발생하게 된다.(단점) - 한 군데서 수정을 하면 다른 객체에서의 참조값을 갖고 있는 변수의 데이터가 다 같이 변경 된다.(단점)
- 얕은 복사는 메모리 사용량이 적다.(장점)
ㅁ 깊은 복사의 장단점
- 깊은 복사는 안전하고 독립적인 복사를 제공 한다.(장점)
- 메모리 사용량이 많다.(단점)
ㅁ 깊은 복사와 얕은 복사를 선택하는 기준
- 객체가 동적으로 할당된 자원을 소유하는 경우 --> 깊은 복사
- 객체가 동적으로 할당된 자원을 소유하지 않고, 단순한 값들의 집합인 경우 --> 얕은 복사
- 객체에 소유권 개념이 필요한 경우 --> 깊은 복사
Casting 4 종류
: static_cast , dynamic_cast, const_cast, reinterpret_cast
- 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'로 가상 함수 테이블을 찾아가 호출 할 가상 함수의 주소를 추적 합니다.
각 객체는 고유한 가상 함수 테이블을 가지고, 파생 클래스는 자신이 오버라이딩한 가상 함수에 한하여 테이블 정보를
업데이트 합니다.
'프로그래밍(c++) > 인프런 강의 정리(C++)' 카테고리의 다른 글
강의 내용 정리(Section5 / 버그 유형) (0) | 2024.05.29 |
---|---|
강의 내용 정리(Section3_8/달팽이 문제) (0) | 2024.05.17 |
강의 내용 정리(Section_3/포인터, const, 참조 등) (1) | 2024.05.17 |
강의 내용 정리(Section3_5/문자열 예제 코드 분석) (0) | 2024.05.17 |
강의 내용 정리(섹션2/함수 기초, 스택 메모리와 스택 프레임) (0) | 2024.05.13 |