일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- virtual destructor
- c++ basic practice
- base from member
- operator overloading
- std::endl
- virtual function
- std::ostream
- c++ multi chatting room
- this call
- discord bot
- 더 지니어스 양면포커
- std::vector
- constructor
- virtual inheritance
- vector capacity
- new&delete
- delete function
- vector size
- C++
- diamond inheritance
- member function pointer
- suffix return type
- dynamic_cast
- conversion constructor
- pointer to member data
- std::cout
- placement new
- virtual function table
- increment operator
- return by reference
- Today
- Total
I'm FanJae.
[C++] Upcasting, Virtual Function 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
1. UpCasting
#include <string>
class Animal
{
std::string name;
int age;
public:
void cry() { }
};
class Dog : public Animal
{
int color;
public:
void setColor(int c) {
color = c;
}
};
int main()
{
Dog dog;
Dog* p1 = &dog; // ok
// int* p2 = &dog; // Error
Animal* p3 = &dog;
}
- UpCasting이란, 기반 클래스 포인터로 파생 클래스 객체를 가리키는 행위를 의미한다.
- 메모리 레이아웃 차원에서 보면, 파생 클래스(Derived Class)인 Dog가 기반 클래스(Aniaml)의 객체를 포함하므로, 객체의 메모리 공간의 처음 부분은 Animal의 메모리 공간이다. 따라서, 이와 같은 작업이 가능하다.
- 기반 클래스의 포인터로는 기반 클래스의 멤버에만 접근이 가능하다.
- 만약 파생 클래스의 고유 멤버에 접근하고 싶다면, 명시적으로 캐스팅(static_cast)가 필요하다.
static_cast<Dog*>(p3)->setColor(5);
- 위와같이 변환하여 접근이 가능하다.
1-1. Upcasting의 활용 예시
class Animal
{
public:
int age = 0;
};
class Dog : public Animal {};
class Cat : public Animal {};
void NewYear(Animal *p)
{
++(p->age);
}
int main()
{
Dog dog;
NewYear(&dog);
Cat cat;
NewYear(&cat);
std::vector<Dog*> v1; // Dog 만 보관하는 vector
std::vector<Animal*> v2; // 모든 동물을 보관
}
- Upcasting의 활용
① 동종(동일 기반 클래스 부터 파생된 클래스)를 처리하는 함수
② 동종 보관하는 컨테이너
2. 함수 오버라이드(Function override)
#include <iostream>
class Animal
{
int age;
public:
void cry() { std::cout << "Animal cry" << std::endl;} // 1
};
class Dog : public Animal
{
public:
// override
void cry() { std::cout << "Dog cry" << std::endl;} // 2
};
int main()
{
Animal a; a.cry() // 1
Dog d; d.cry() // 2
Animal *p = &d;
p->cry(); // 1 ? 2
}
- 이와 같이 기반 클래스의 있는 멤버 함수를 파생 클래스에 다시 구현 하는것을 Function Override라고 한다.
- 기존 배웠던 Function Overloading 과 완전히 다른 개념임에 유의 해야한다.
Animal a; a.cry(); // 1
Dog d; d.cry(); // 2
Animal *p = &d; // ?
p->cry();
- 맨 위의 2개에 대해서는 쉽게 이해할 수 있다..
- 하지만, Animap *p = &d; 부분은 과연 Animal의 cry()를 호출할까 아니면 Dog의 cry()를 호출할까?
- 결과와 별개로 논리적으로 생각해보면 객체가 실제 가리키고 있는건 Dog이기 때문에 Dog의 Cry()를 호출해야 한다.
- 하지만 이는 Animal의 cry() 를 호출한다. Java, Python등에서는 Dog의 cry()를 호출한다.
- 이와 같이 처리 되는 이유와 C++ 에서 Dog의 cry()를 호출하게 할 방법이 존재한다.
- 우선, 이를 결정하는 함수 바인딩(Function Binding)이라는 개념에 대해 알아보려 한다.
3. 함수 바인딩(Function Binding)
#include <iostream>
class Animal
{
int age;
public:
void cry() { std::cout << "Animal cry" << std::endl;} // 1
};
class Dog : public Animal
{
public:
// override
void cry() { std::cout << "Dog cry" << std::endl;}
};
int main()
{
Animal a;
Dog d;
Animal *p = &d;
p->cry();
}
- 이와 같은 상황에 p->cry() 표현식을 어느 함수와 연결할지를 결정하는 과정을 함수 바인딩이라고 한다.
- Function Binding에는 Static binding 과 Dynamic Binding 2가지가 존재한다.
if ( 사용자 입력 == 1 ) p = &a;
p -> cry();
- p -> cry(); 이전에 이와 같은 코드가 삽입되어 있었다면, 이를 컴파일러가 이를 알아내는 것은 어렵다.
- 따라서 일반적으로는 컴파일 시간에 포인터 타입만 보고 결정하는 것이다.
① Static binding(early binding)
- 컴파일러가 컴파일 시간에 함수 호출을 결정한다.
- 컴파일러는 p가 실제로 어느 객체를 가리키는지 컴파일 시간에 알 수 없다.
- 즉, 포인터 타입만을 가지고 함수 호출을 결정하여, Animal cry를 호출한다.
- 빠르지만 논리적이지 않다. C++, C#의 Non-Virtual Function등이 대부분 이 방식으로 작동한다.
② Dynamic binding(late binding)
- 컴파일 시간에는 p가 가리키는 곳을 조사하는 기계어 코드를 생성한다.
- 실행 시간에 p가 가리키는 곳을 조사한 이후, 실제 메모리에 있는 객체에 따라 함수 호출을 결정한다.
- 느리지만 논리적이다. C++, C#의 Virtual Function이 이러한 방식으로 작동한다.
③ virtual function vs non-virtual function
③-① Virtual Function
- 함수 선언부에 virtual 붙은 멤버 함수
- dynamic binding
③-② Non-virtual function
- virtual을 붙이지 않은 멤버 함수
- static binding
#include <iostream>
class Animal
{
int age;
public:
// static binding
void cry() { std::cout << "Animal cry" << std::endl;} // 1
// dynamic binding
virtual void cry2() { std::cout << "Animal cry2" << std::endl;} // 1
};
class Dog : public Animal
{
public:
void cry() { std::cout << "Dog cry" << std::endl;} // 2
virtual void cry2() { std::cout << "Animal cry2" << std::endl;} // 2
};
int main()
{
Animal a;
Dog d;
Animal *p = &d;
p->cry1(); // Animal
p->cry2(); // Dog
}
- 이와 같이 Virtual Function을 사용하면 dynamic binding이 일어남을 확인 가능하다.
4. Virtual Function Example
- 우선, 가상함수를 언제, 어떻게 사용하는가?를 알아보자.
- Powerpoint와 같이 다양한 도형을 관리하는 프로그램을 만든다고 생각해보자.
#include <iostream>
#include <vector>
class Rect
{
public:
void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle
{
public:
void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
std::vector<Rect*> v1;
std::vector<Circle*> v2;
}
- 위와 같이 Rect와 Circle을 별개로 관리하면, 서로 겹치는 도형등의 대한 관리가 어려워진다.
- 따라서, 공통의 기반 클래스를 설계해서 한번에 관리하는 것이 좋다.
4-1. 기반 클래스 설계
- 모든 도형의 공통의 기반 클래스가 있다면 모든 종류의 도형을 하나의 컨테이너에 보관할 수 있다.
#include <iostream>
#include <vector>
class Shape
{
}
class Rect : public Shape
{
public:
void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle : public Shape
{
public:
void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
std::vector<Shape*> v;
while(1)
{
int cmd;
std::cin >> cmd;
if ( cmd == 1 ) // 사각형을 그림.
{
// Rect rc;
Shape* p = new Rect;
v.push_back(p);
}
else if ( cmd == 2 ) // 원을 그림
{
v.push_back( new Circle );
}
else if ( cmd == 9 )
{
for ( auto p : v)
p->draw();
}
}
}
- 이를 컴파일 해보면, Draw()에 대해서 오류가 발생한다.
- 이는 객체는 Rect 또는 Circle 이지만 Vector에 보관시에는 Shape* 로 보관되므로, draw()를 호출할 수 없다.
① 해결방법 1
- Shape *를 Rect *나 Circle *로 캐스팅한다.
- 다만 문제는, 코드를 작성할 때 어떤 도형인지 알 수 있을까?
p->draw();
- 이것을 캐스팅 하여 도형의 정보를 알아내는 것은 불가능하다.
② 해결방법 2
- Shape 안에 draw() 함수가 있어야 한다.
4-2. 공통의 특징은 기반 클래스에도 있어야 한다.
- 모든 파생클래스(Rect, Circle)의 공통의 특징은 반드시 기반 클래스(Shape)에도 있어야 한다.
- 그래야, 기반 클래스 포인터 타입으로 객체를 관리할 때 해당 특징을 사용할 수 있다.
※ Shape 안에 반드시 draw() 함수가 있어야 한다.
#include <iostream>
#include <vector>
class Shape
{
public:
void draw() { std::cout << "draw shape" << std::endl;}
}
class Rect : public Shape
{
public:
void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle : public Shape
{
public:
void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
std::vector<Shape*> v;
while(1)
{
int cmd;
std::cin >> cmd;
if ( cmd == 1 ) // 사각형을 그림.
{
// Rect rc;
Shape* p = new Rect;
v.push_back(p);
}
else if ( cmd == 2 ) // 원을 그림
{
v.push_back( new Circle );
}
else if ( cmd == 9 )
{
for ( auto p : v) // p는 Shape*
p->draw();
}
}
}
- 이를 실행해보면 p->draw(); 실행시 Draw Shape로만 출력된다.
- 즉, 기반 클래스 함수를 호출하는 문제가 발생한다.
- 여기서도 알 수 있듯, 기반 클래스의 함수를 재정의 할때는 일반 함수를 사용해서는 안된다.
4-3. 가상함수 도입
- 기반 클래스 함수 중 파생클래스가 재정의하게 되는 함수는 반드시 가상함수로 만들어야 한다.
#include <iostream>
#include <vector>
class Shape
{
public:
virtual void draw() { std::cout << "draw shape" << std::endl;}
}
class Rect : public Shape
{
public:
virtual void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle : public Shape
{
public:
virtual void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
std::vector<Shape*> v;
while(1)
{
int cmd;
std::cin >> cmd;
if ( cmd == 1 ) // 사각형을 그림.
{
// Rect rc;
Shape* p = new Rect;
v.push_back(p);
}
else if ( cmd == 2 ) // 원을 그림
{
v.push_back( new Circle );
}
else if ( cmd == 9 )
{
for ( auto p : v) // p는 Shape*
p->draw();
}
}
}
- 이와 같이 구현하면, draw()에 대해서 Shape가 아닌 재정의 된 함수를 부르는 것을 확인 가능하다.
5. Virtual Function 문법
- 가상함수를 만들 때는 함수 앞에 virtual을 붙인다.
- 단, 가상함수를 재정의 할 때는 virtual을 붙여도 되고 붙이지 않아도 되지만, 가독성을 위해서는 붙이는 것이 좋다.
- 가상 함수 재정의시 함수 이름 등에 오타가 있어도 에러가 발생하지 않는다.
- 컴파일러는 새로운 함수를 추가 했다고 생각하지 않는다.
virtual void Dra()
{
}
- 예를들어 Draw()를 쓰려다가 오타를 냈다고 하더라도.. 컴파일러가 이를 새로운 함수를 추가했다고 생각한 것이다.
5-1. overrride // C++11
virtual void Draw() override {}
- 가상함수 재정의시 override를 붙이는 것이 안전하다.
- 이를 붙이면, 컴파일러가 이를 새로운 함수가 아닌 재정의한 함수라고 판단하여 오탈자 등을 지적해준다.
5-2. Virtual Desturctor
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base()" << endl;}
virtual ~Base() { cout << "~Base()" << endl;}
};
class Derived : public Base
{
public:
Derived() { cout << "Derived()" << endl;}
virtual ~Derived() { cout << "~Derived()" << endl;}
};
int main()
{
Base* p = new Derived; // Derived 객체 생성
// 컴파일러는 p가 Base 라고만 생각.
delete p; // 1. p는 Base*이다.
// 2. 소멸자는 가상함수가 아니다.
}
- 이 예제의 문제점은 Base, Derived에 대한 생성은 정상적으로 이뤄졌지만, p를 Base *로 인식한 것이다.
- 즉, Derived 부분의 자원 회수가 이뤄지지 않았다.
- 아래와 같은 사항이다.
- 해결 방법은 소멸자 호출시 포인터 타입이 아닌 p가 가리키는 메모리를 조사 후 호출해야한다.
- 즉, 기반 클래스(Base)의 소멸자를 가상함수로 한다.
※ 기반 클래스의 소멸자는 반드시 가상함수가 되어야 한다.
※ 의도적으로 가상함수로 하지 않는 경우도 있지만, 이는 고급 기법이다.
'C++ > Basic' 카테고리의 다른 글
[C++] Virtual Function Table (0) | 2024.08.22 |
---|---|
[C++] Abstract class, Interface (0) | 2024.08.21 |
[C++] Inheritance(상속) (0) | 2024.08.20 |
[C++] Copy Constructor (0) | 2024.08.19 |
[C++] this, Reference return (0) | 2024.08.18 |