윤 홍 란 hryun@sookmyung.ac.kr 다형성과 가상 함수 윤 홍 란 hryun@sookmyung.ac.kr
Shape 클래스와 자식 클래스들(1) Shape클래스 // 일반적인 '도형'을 상징하는 클래스 class Shape { public: void Move(double x, double y); void Draw() const; Shape(); Shape(double x, double y); protected: double _x, _y; }; void Shape::Draw() const cout << "[Shape] Position = ( " << _x << ", " << _y << ")\n"; } 도형 원 사각형 삼각형
Shape 클래스와 자식 클래스들(2) Rectangle클래스 // 사각형을 상징하는 클래스 class Rectangle : public Shape { public: void Draw() const; void Resize(double width, double height); Rectangle(); Rectangle(double x, double y, double width, double height); protected: double _width; double _height; }; void Rectangle::Draw() const cout << "[Rectangle] Position = ( " << _x << ", " << _y << ") " "Size = ( " << _width << ", " << _height << ")\n"; }
Shape 클래스와 자식 클래스들(3) Circle 클래스 // 원을 상징하는 클래스 class Circle : public Shape { public: void Draw() const; void SetRadius(double radius); Circle(); Circle(double x, double y, double radius); protected: double _radius; }; void Circle::Draw() const cout << "[Circle] Position = ( " << _x << ", " << _y << ") " << "Radius = " << _radius << "\n"; }
Shape 클래스와 자식 클래스들(4) 도형객체 생성 및 그리기 int main() { // 도형 객체 생성 및 그리기 Shape s; s.Move(100, 100); s.Draw(); // 사각형 객체 생성 및 그리기 Rectangle r; r.Move( 200, 100); r.Resize( 50, 50); r.Draw(); // 원 객체 생성 및 그리기 Circle c; c.Move( 300, 100); c.SetRadius( 30); c.Draw(); return 0; }
Shape 클래스와 자식 클래스들(5) 도형 클래스들의 상속 계층도 [23-3] 뒤로
보관한 객체를 사용하기 및 문제점 앞의 예에서 shape의 하위클래스인 circle이나 Rectangle클래스를 이용하여 여러 개의 객체를 생성해야 하는 경우, 객체배열을 이용할 수 있다. Circle과 Rectangle을 위한 객체배열을 따로 선언해 주어야 한다. -> 비 효율적 상위클래스의 객체배열로 선언하고 사용할 수 있다. (지난 상속에서, 자식클래스의 포인터는 부모클래스의 포인터로 변환이 가능하다.)
다양한 클래스의 객체를 배열에 담기(1) 도형 클래스의 객체들을 배열에 담아서 사용하는 예 실행 결과 – 항상 Shape::Draw() 가 호출 Shape* shapes[5] = {NULL}; shapes[0] = new Circle( 100, 100, 50); shapes[1] = new Rectangle( 300, 300, 100, 100); shapes[2] = new Rectangle( 200, 100, 50, 150); shapes[3] = new Circle(100, 300, 150); shapes[4] = new Rectangle( 200, 200, 200, 200); for (int i = 0; i < 5; ++i) shapes[i]->Draw(); for (i = 0; i < 5; ++i) { delete shapes[i]; shapes[i] = NULL; }
다양한 클래스의 객체를 배열에 담기(2) 배열과 객체들의 메모리 구조 [23-5]
가상 함수로 문제점 해결하기 Draw() 함수를 가상 함수로 만들면 문제 해결 class Shape { public: void Move(double x, double y); virtual void Draw() const; Shape(); Shape(double x, double y); protected: double _x, _y; }; 객체의 타입에 맞는 Draw()함수가 호출된다. Shape::Draw()함수를 가상함수로 만들면 Circle::Draw()와 Rectangle::Draw()함수 역시 자동적으로 가상함수가 됨 virtual키워드는 클래스의 정의 안쪽에서만 한 번 붙여주면 된다. [23-6]
가상함수 클래스 내에 virtual 키워드로 선언된 함수로, C++ 언어에서는 이를 통해 상속을 이용한 다형성(polymorphism)을 제공 가상 함수의 선언 virtual 함수_선언; 가상 함수는 가상 메커니즘을 구동 시킴 가상 메커니즘 가상 함수로 선언된 함수를 하위 클래스에서 오버라이딩하고, 가상 함수를 호출하면 실제 호출되는 함수는 가상으로 선언된 함수가 아니라, 실제 인스턴스의 타입에 따라 호출 가상 함수는 다형성(polymorphism)을 제공하는 중요한 도구
다형성(Polymorphism)과 가상함수(1) 다른 분야에서의 다형성의 의미 객체지향 프로그래밍에서의 다형성이란 타입에 관계 없이 동일한 방법으로 다룰 수 있는 능력을 말한다. 예) Circle이나 Rectangle 객체들을 각각의 타입에 상관 없이 Shape 객체처럼 다룰 수 있는 능력 [표 23-1]
다형성(Polymorphism)과 가상함수(2) 다형성은 객체간의 결합(Coupling)을 약하게 만들어서, 객체 간의 연결을 유연하게 해준다. // 도형을 원점으로 이동하는 함수 void Controller::MoveToOrigin( Shape* p ) { p->Move( 0, 0 ); p->Draw(); } [23-7]
순수 가상 함수(Pure Virtual Functions) 하위 클래스에서 반드시 오버라이딩해야 할 때 사용 가상 함수의 선언부 끝에 ‘=0’이라는 순수 지시자(Pure Specifier)를 붙인 함수 가상 함수를 선언한 클래스에서는 이 함수를 정의해서는 안 되며, 상속한 클래스에서는 반드시 오버라이딩해야만 한다. 순수 가상 함수를 선언한 기본 클래스에서는 인스턴스를 생성할 수 없다. 순수 가상 함수에 해당하는 멤버 함수는 정의부가 없어 클래스의 인스턴스 생성을 위한 메모리 지도를 만들 수 없기 때문 Shape::Draw() 함수를 순수 가상 함수로 만들어보자. class Shape { public: void Move(double x, double y); virtual void Draw() const = 0; // 중간 생략 /* 함수의 정의를 지워도 컴파일 오류가 발생하지 않는다. void Shape::Draw() const cout << "[Shape] Position = ( " << _x << ", " << _y << ")\n"; } */
순수 가상 함수의 의미 하나 이상의 순수 가상 함수를 포함하고 있는 클래스를 추상 클래스(Abstract Class)라고 부른다. 추상 클래스 타입의 의 객체를 생성하는 것은 불가능하고, 오로지 부모 클래스로서만 사용할 수 있다. Shape::Draw() 함수를 순수 가상 함수로 만드는 것의 효과 Shape 클래스를 추상 클래스로 만들기 때문에 Shape 클래스의 객체를 만들 수 없다. 컴파일러나 다른 개발자들에게 Shape 클래스는 오로지 부모 클래스로만 사용할 것이라는 점을 알리는 역할을 한다. Shape::Draw() 함수를 구현하지 않아도 컴파일 오류가 발생하지 않는다. 컴파일러나 다른 개발자들에게 Shape::Draw() 함수가 자식 클래스에 의해서 오버라이딩 될 것이며, 다형성을 통해서만 호출할 것이라는 점을 알리는 역할을 한다.
다양한 종류의 멤버 함수 상속과 관련해서 지금까지 살펴본 멤버 함수의 종류 일반적인 멤버 함수 가상 함수 순수 가상 함수 어떤 종류의 멤버 함수를 사용할 지에 대한 가이드 라인 처음엔 그냥 멤버 함수로 만든다. 다형성을 이용해야 하는 경우라면 가상 함수로 만든다. 다형성을 위해서 함수의 원형만 필요한 경우라면 순수 가상 함수로 만든다.
오버로딩과 오버라이딩 부모 클래스에서 오버로드된 함수 중에서 어느 것 하나라도 오버라이드 하면 나머지 다른 함수들도 모두 사용할 수 없다. class Pet{ public: void Eat(); void Eat(const string& it); string name; }; class Dog : public Pet { int main() { // 강아지 생성 Dog dog1; dog1.name = "Patrasche"; // 두 가지 Eat() 함수를 호출한다. dog1.Eat(); dog1.Eat( "milk" ); // Error!! return 0; } [23-11]
1. 범용 함수 - 함수 템플릿 2. 범용 클래스 – 클래스 템플릿
함수 템플릿 int, double, ... 형 변수 2개에 대한 swap() 함수 구현하라. #include <iostream> using namespace std; void swap(int &x, int &y); void swap(double &x, double &y); void main(void) { int a = 3, b = 5; double x = 1.1, y = 2.2; cout << "a = " << a << ", " << "b = " << b << endl; cout << "x = " << x << ", " << "y = " << y << endl; swap(a, b); swap(x, y); } void swap(int &x, int &y) { int temp = x; x = y; y = temp; } void swap(double &x, double &y) double temp = x; 함수 템플릿을 사용하면 하나의 함수만으로 모두 수용 가능! 다음 페이지
함수 템플릿 함수 템플릿 : 관련된 함수들의 틀 여러 가지 타입에 대해 공통적으로 적용 가능 사용할 자료형을 매개변수로 전달받음 #include <iostream> using namespace std; template <typename T> void myswap(T &x, T &y); void main(void) { int a = 3, b = 5; double x = 1.1, y = 2.2; cout << "a = " << a << ", " << "b = " << b << endl; cout << "x = " << x << ", " << "y = " << y << endl; myswap(a, b); myswap(x, y); } 아래와 같이 두 줄에 걸쳐 써도 됨 template <typename T> void myswap(T &x, T &y) typename 대신 class 사용 가능 template <typename T> void myswap(T &x, T &y) { T temp = x; x = y; y = temp; } 템플릿 하나로 해결! T Type의 대표 이름
함수 템플릿 함수 템플릿의 동작 원리 여러 개의 범용 자료형을 갖는 예 컴컴파일 시 함수 템플릿으로부터 해당 타입의 함수가 만들어짐 (Instantiation) swap(T, T) swap(int, int), swap(double, double) 컴파일 주의 : 함수 템플릿 또는 클래스 템플릿 구현 시 프로토타입을 header 파일로, 구현 부분을 cpp 파일로 분리하여 구현하면 안됨 (Link Error) – 템플릿은 그 자체로 컴파일되는 것이 아님 VC++에서 지원하지 않음? 둘 다 header 파일 또는 둘 다 사용하는 파일 여러 개의 범용 자료형을 갖는 예 #include <iostream> using namespace std; template <typename T1, typename T2> void Print(T1 x, T2 y) { cout << x << "\t" << y << endl; } void main(void) { Print(10, "Hi"); Print(0.23, 15); Print("Hi", 4.54);
함수 템플릿 : 템플릿 함수의 오버로딩 연관된 함수들 : 같은 이름을 가진 하나의 함수 템플릿 사용 컴파일러는 해당 타입에 맞는 함수를 동작하기 위해 오버로딩 처리 함수 템플릿 : 명시적으로 오버로딩 가능 template <typename T> void swap(T &x, T &y); template <typename T> void swap(T &x, T &y, int option); non-template 함수 역시 다른 매개변수로 같은 이름 사용 가능 void swap(int &x, int &y) 함수 호출 순서 1. 이름, 매개 변수가 정확히 일치하는 함수 2. 해당 함수를 만들어 낼 수 있는 함수 템플릿을 사용하여 함수 생성 3. 없다면 에러
클래스 템플릿 문제 int형 값 5개를 배열로 저장하는 클래스를 만들어 보라. (IntArray) double형 값 5개를 배열로 저장하는 클래스를 만들어 보라. (DoubleArray) char형 값 5개를 배열로 저장하는 클래스를 만들어 보라. (CharArray) void main(void) { IntArray IntA; DoubleArray DoubleA; CharArray CharA; int i; for (i = 0; i < 5; i++) IntA.SetX(i, i * i); DoubleA.SetX(i, 3.14 * i * i); CharA.SetX(i, i + 'A'); cout << IntA.GetX(i) << "\t"; cout << endl; for (i = 0; i < 5; i++) cout << DoubleA.GetX(i) << "\t"; cout << endl; cout << CharA.GetX(i) << "\t"; }
클래스 템플릿 문제 - 계속 class IntArray { private : int x[5]; public: void SetX(int index, int value) { x[index] = value; } int GetX(int index) { return x[index]; } }; 문제 - 계속 type을 제외하고는 모두 동일하다 class DoubleArray { private : double x[5]; public: void SetX(int index, double value) { x[index] = value; } double GetX(int index) { return x[index]; } }; 하나의 클래스로 만드는 방법은? 클래스 템플릿 : type의 매개변수화! class CharArray { private : char x[5]; public: void SetX(int index, char value) { x[index] = value; } char GetX(int index) { return x[index]; } };
클래스 템플릿 void main(void) { int형 Array 객체 생성 Array<int> IntA; Array<double> DoubleA; Array<char> CharA; int i; for (i = 0; i < 5; i++) IntA.SetX(i, i * i); DoubleA.SetX(i, 3.14 * i * i); CharA.SetX(i, i + 'A'); cout << IntA.GetX(i) << "\t"; cout << endl; cout << DoubleA.GetX(i) << "\t"; cout << CharA.GetX(i) << "\t"; } int형 Array 객체 생성 #include <iostream> using namespace std; template <typename T> class Array { private : T x[5]; public: void SetX(int index, T value) { x[index] = value; } T GetX(int index) { return x[index]; } }; 추가 : T : type의 매개변수화 필요한 type을 T로 변경 // 멤버함수를 외부에 구현할 때의 구문 template <typename T> T Array<T>(int index) { return x[index]; }
클래스 템플릿 : 예제 - 2개 이상의 type을 매개변수로 받기 #include <iostream> using namespace std; template <typename T1, typename T2> class myclass { private : T1 i; T2 j; public: myclass(T1 a, T2 b) { i = a; j = b; } void show(void) { cout << i << "\t" << j << endl; } }; void main(void) { myclass<int, double> ob1(10, 0.23); myclass<char, char *> ob2('X', "This is a test"); ob1.show(); ob2.show(); }
클래스 템플릿 유용한 클래스들을 템플릿으로 만들어 라이브러리로 준비해 두면 어떨까? C++ 표준 템플릿 라이브러리 (STL) 많이 사용되는 자료구조와 알고리즘을 클래스 템플릿으로 구현 stack, queue, list, set, vector(동적배열), ... search, sort, swap, ...
STL 컨테이너(1) list 클래스를 사용하는 예 #include <list> #include <iostream> int main() { // int 타입을 담을링크드 리스트 생성 std::list<int> intList; // 1 ~ 10까지 링크드 리스트에 넣는다. for (int i = 0; i < 10; ++i) intList.push_back( i); // 5를 찾아서 제거한다. intList.remove( 5); // 링크드 리스트의 내용을 출력한다. std::list<int>::iterator it; for (it = intList.begin(); it != intList.end(); ++it) std::cout << *it << "\n"; return 0; }
STL 컨테이너(2) list 클래스의 탐색 [29-2]
STL 컨테이너(3) 자주 사용하는 STL의 컨테이너 클래스 [표 29-1]
STL 알고리즘(1) sort() 함수를 사용하는 예 #include <algorithm> #include <vector> #include <iostream> int main() { // 동적 배열을 생성해서 임의의 영문자를 추가한다. std::vector<char> vec; vec.push_back( 'e'); vec.push_back( 'b'); vec.push_back( 'a'); vec.push_back( 'd'); vec.push_back( 'c'); std::cout << "\nvector 정렬 전\n"; std::vector<char>::iterator it1; for (it1 = vec.begin(); it1 != vec.end(); ++it1) std::cout << *it1; // sort() 함수를 사용해서 정렬한다. std::sort( vec.begin(), vec.end() ); // 정렬 후 상태를 출력한다. std::cout << "\nvector 정렬 후\n"; std::vector<char>::iterator it2; for (it2 = vec.begin(); it2 != vec.end(); ++it2) std::cout << *it2; std::cout<< std::endl; return 0; }
STL 알고리즘(1) 자주 사용하는 STL의 알고리즘 함수 [표 29-2]