준비하는 대학생

[Cpp] 다형성 본문

Programming/C++

[Cpp] 다형성

Bangii 2023. 3. 10. 23:13

C++에서 다형성(Polymorphism)은 객체 지향 프로그래밍(OOP)의 핵심 개념 중 하나이다.
다형성은 같은 이름을 갖는 여러 형태의 함수를 클래스별로 만들 수 있게 해주는 기능을 말하며 다른 클래스나 객체들을 동일한 인터페이스로 다룰 수 있도록 한다. 다형성을 잘 활용하면 코드의 유지 보수성과 재사용성이 증가할 수 있다.

가상 함수(Virtual Function)

C++에서 가상 함수(Virtual Function)를 이용한 다형성은 가상 함수를 이용하여 런타임 다형성(Runtime Polymorphism)을 구현한다. 가상 함수는 베이스 클래스(Base Class)에서 정의되며 파생 클래스(Derived Class)에서 재정의될 수 있다. 이때 파생 클래스에서 재정의된 가상 함수를 호출하면 컴파일러는 객체의 실제 타입에 따라 해당 함수를 호출한다. 이를 가상 함수 호출(Virtual Function Call)이라고 한다.

가상 함수를 선언할 때는 함수의 선언 앞에 virtual 한정자를 붙인다. 

아래는 가상함수를 이용해 다형성을 활용한 예시 코드이다.

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() {
        cout << "Drawing Shape..." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() {
        cout << "Drawing Circle..." << endl;
    }
};

class Square : public Shape {
public:
    void draw() {
        cout << "Drawing Square..." << endl;
    }
};

int main() {
    Shape* shapePtr;
    Circle circle;
    Square square;

    shapePtr = &circle;
    shapePtr->draw();

    shapePtr = &square;
    shapePtr->draw();

    return 0;
}

 

위 코드에서 Shape 클래스는 추상적인 개념의 도형을 나타내며, draw() 함수가 가상 함수로 선언되어 있다.
Circle 클래스와 Square 클래스는 Shape 클래스를 상속받아서 도형의 구체적인 종류를 나타낸다. 각 클래스에서는 draw() 함수를 오버라이딩하여 도형을 그리는 방식을 구현한다.
main 함수에서는 Shape 포인터 변수 shapePtr을 선언하고, Circle 객체와 Square 객체를 각각 생성한다. 이후 shapePtr에 Circle객체의 주소를 할당하여 draw() 함수를 호출하면 "Drawing Circle..."가 출력되고, 이후에 shapePtr에 Square 객체의 주소를 할당하여 draw() 함수를 호출하면 "Drawing Square..."가 출력된다.
이처럼 Shape 포인터 변수를 이용하여 Circle 객체와 Square 객체를 동일한 인터페이스로 다룰 수 있으며, 실제 객체의 타입에 상관없이 동일한 인터페이스를 사용할 수 있다.

가상 함수를 이용한 다형성의 가장 큰 장점은 런타임 다형성을 구현할 수 있다는 것이다. 런타임 다형성은 프로그램이 실행되는 동안 객체의 타입을 동적으로 결정할 수 있게 한다. 이를 통해 객체의 실제 타입에 상관없이 동일한 인터페이스를 사용할 수 있다.

가상 생성자

생성자와 소멸자도 멤버함수이다. 하지만 생성자와 소멸자는 다른 멤버함수와 달리 클래스와 이름이 같은 특징이 있다.

가상 생성자는 생성자의 이름은 베이스 클래스와 파생 클래스가 무조건 다르기 때문에 가상 함수가 될 수 없다.

가상 소멸자

가상 소멸자도 가상 생성자와 같이 베이스 클래스와 파생 클래스가 무조건 다르다.

하지만, 가상 소멸자는 다형성을 사용하는 경우에 중요한 역할을 한다. 만약 파생 클래스에서 메모리를 동적으로 할당하고, 이를 해제하기 위해 delete 연산자를 사용한다면, 베이스 클래스의 포인터로 파생 클래스를 가리키는 경우에는 파생 클래스의 소멸자가 호출되지 않을 수 있다.

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() {
        cout << "Base deleted"  << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived deleted"  << endl;
    }
};

int main() {
    Base* b = new Derived;
    delete b; // 소멸자 호출
    return 0;
}
출력
Base deleted

위 예시가 가상 소멸자를 사용하지 않고 소멸자를 호출했을 때 Derived 소멸자가 호출되지 않는 것을 알 수 있다. 이 경우, Derived에서 동적할당한 변수들이 존재하는 경우, delete를 하지 못하기 때문에 메모리 누수현상이 일어날 수 있다.

이 문제를 해결하기 위해서는, 베이스 클래스에서 가상 소멸자를 선언하고, 파생 클래스에서 재정의해야 한다. 이렇게 하면, 객체의 파괴 시 파생 클래스에서 정의한 소멸자가 호출되므로, 메모리 누수(Memory Leak)를 방지할 수 있다.

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {
        cout << "Base deleted"  << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived deleted"  << endl;
    }
};

int main() {
    Base* b = new Derived;
    delete b; // Derived 소멸자 호출됨
    return 0;
}
출력
Derived deleted
Base deleted

위 예시와 같이 virtual 한정사를 사용하여 가상 소멸자를 사용하는 경우, Derived 클래스의 소멸자도 같이 호출되는 것을 확인할 수 있다. 이처럼 다형성을 사용할 때에는 가상 소멸자를 사용해 메모리 누수를 방지해주어야 한다.

 

 

 

Comments