목차

C++ 스터디 #12: 클래스(2)

시간 2021년 8월 2일 화요일 20:00 ~ 22:00
장소 ZOOM
참가자 -

1. 소멸자

생성된 객체가 범위를 벗어날때 소멸자를 호출한다. 소멸자는 ~ + 클래스 이름 으로 만든다.
소멸자는 파일을 닫거나 메모리를 반환하는 작업과 같이 프로그램을 종료하기 전 자원을 반납하는데 사용된다.



class MyString
{
private: 
    char* s;
    int size;

public:
    MyString(char* c)
    {
        size = strlen(c) + 1;
        s = new char[size];
        strcpy(s, c);
    }
    ~MyString()
    {
        delte[]s;
    }
};
여기서 ~MyString이 소멸자이다.

2. 객체와 함수

2.1. 객체를 함수로 전달하기

#include<iostream>
using namespace std;

class circle
{
public:
    circle(int s) : size(s) {}
    int size;
};

void makedouble(circle c)
{
    c.size *= 2;
}

int main()
{
    circle circle1(10);
    makedouble(circle1);
    cout << circle1.size << endl;

    return 0;
}

결과 : 10

이 결과를 보면 c.size *= 2; 를 했음에도 size는 변경되지 않았다. 위의 예에서 main()의 객체인 circle1은 makedouble() 함수의 매개 변수 c로 복사되었다. 복사된 c의 size는 변경되었지만 main() 안의 circle1 객체는 변경되지 않았다.
이때 makedouble()의 매개 변수 c는 생성될 때 다른 객체의 내용을 복사하여서 생성된다. 따라서 일반적인 생성자가 호출되는 것이 아니라 복사 생성자 라는 특수한 생성자가 호출된다. 이 방법은 함수에서 매개 변수를 변경하더라도 원본 객체에 영향이 없어 안전성이 높다는 장점이 있다.

2.2. 참조자 매개 변수 사용하기

#include<iostream>
using namespace std;

class circle
{
public:
    circle(int s) : size(s) {}
    int size;
};

void makedouble(circle& c)
{
    c.size *= 2;
}

int main()
{
    circle circle1(10);
    makedouble(circle1);
    cout << circle1.size << endl;

    return 0;
}
결과 : 20
반면에 참조자를 통하여 객체를 변경하면 원래의 객체를 변경하는 것이다. 따라서 20이라는 결과가 나온다.
일반적으로 객체의 크기가 큰 경우 객체를 복사하는 시간이 오래 걸리기 때문에 객체의 참조자를 전달하는 편이 효율적이다.

2.3. 객체의 주소를 함수로 전달하기

#include<iostream>
using namespace std;

class circle
{
public:
    circle(int s) : size(s) {}
    int size;
};

void makedouble(circle *c)
{
    c->size *= 2;
}

int main()
{
    circle circle1(10);
    makedouble(&circle1);
    cout << circle1.size << endl;

    return 0;
}
결과 : 20
이 방법은 2.2.와 동일한 효과를 가진다.

2.4. 함수가 객체 반환하기

#include<iostream>
using namespace std;

class Circle
{
public:
    Circle(int s) : size(s) { }
    int size;
};

Circle createCircle()
{
    Circle c(10);
    return c;
}

int main()
{
    Circle Circle1 = createCircle();
    cout << Circle1.size << endl;

    return 0;
}
결과 : 10
함수가 객체를 반환할 때도 객체의 내용이 복사될 뿐 원본은 전달되지 않는다. 이 경우 'createCircle()'은 함수 안에서 정의된 객체 변수 c를 메인함수의 circle1 로 복사한다.

복사 생성자

이렇게 객체를 복사하여 객체를 생성할 때 사용하는 생성자인 복사생성자에 대해 알아보자.
1. 같은 종류의 객체로 초기화하는 경우

Myclass obj(obj2); // 여기서 복사 생성자 호출
2. 객체를 함수에 전달하는 경우
Myclass func(Myclass obj) // 여기서 복사 생성자 호출
{....
}
3. 함수가 객체를 반환하는 경우
Myclass func(myclass obj)
{
    Myclass tmp;
    ...
    return tmp; // 여기서 복사 생성자 호출
}
이해하기 쉽도록 다음 예시를 살펴보자.
#include<iostream>
using namespace std;

class Person
{
public : 
    int age;
    Person(int a) : age(a) { }
};

int main()
{
    Person A(21);
    Person clone(A); // 복사 생성자 호출

    cout << "A의 나이 : " << A.age << ", clone의 나이 : " << clone.age << endl;

    A.age = 25;

    cout << "A의 나이 : " << A.age << ", clone의 나이 : " << clone.age << endl;

    return 0;
}

이 예시에서 A의 나이를 25로 바꿔도 clone은 A를 복사한 것이기 때문에 변화가 없다.

예제

Circle 클래스의 반지름을 교환하는 swap 함수를 작성해보자. 원본 객체를 받아서 실제로 객체의 내용이 교환되어야 한다.

메인함수

int main()
{
    Circle circle1(5);
    Circle circle2(3);
    
    cout << "circle1 넓이 : " << getArea(circle1) << ", circle2 넓이 : " << getArea(circle2) << endl;

    swap(circle1, circle2);

    cout << "circle1 넓이 : " << getArea(circle1) << ", circle2 넓이 : " << getArea(circle2) << endl;

    return 0;
} 
출력 예시 :
circle1 넓이 : 78.5, circle2 넓이 : 28.26
circle1 넓이 : 28.26, circle2 넓이 : 78.5

3. 클래스 안에 객체 포함하기

객체 지향 프로그래밍에서는 하나의 객체 안에 다른 객체가 포함될 수 있다. 다음 예를 살펴보자.

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

class Date
{
    int year, month, day;
public : 
    Date(int y, int m, int d) : year(y), month(m), day(d) { }
    void print()
    {
        cout << year << "." << month << "." << day << endl;
    }
};

class Person
{
    string name;
    Date birth;
public : 
    Person(string n, Date d) : name(n), birth(d) { }
    void print()
    {
        cout << name << " : ";
        birth.print();
        cout << endl;
    }
};

int main()
{
    Date d(2001, 6, 25);
    Person p("윤호연", d);
    p.print();
    return 0;
}
이 예시에서 Person 클래스 안에 Date 클래스를 포함하고 있는 것을 볼 수 있다.

4. 정적 변수

정적 변수는 클래스안에서만 사용되는 전역 변수라고 생각하면 된다. 정적 변수는 앞에 static을 붙여서 선언한다. 정적 변수의 초기화는 전역 변수와 비슷하게 클래스 외부에서 int Circle :: count = 0; 으로 수행한다.

class Circle
{
    int x, y;
    int radius;
    static int count;

public : 
    Circle() : x(0), y(0), radius(0)
    {
        count++;
    }
    Circle(int x, int y, int r) : x(x), y(y), radius(r)
    {
        count++;
    }
};
int Circle :: count = 0;

5. 프렌드 함수와 프렌드 클래스

프렌드라는 메커니즘을 사용하면 외부의 클래스나 함수가 자신의 내부 데이터를 사용하도록 허가할 수 있다.

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

class A
{
public : 
    friend class B;
    A(string s = "") : secret(s) { }

private : 
    string secret;
};

class B
{
public : 
    B() { }
    void print(A obj)
    {
        cout << obj.secret << endl;
    }
};

int main()
{
    A a("기밀 정보");
    B b;
    b.print(a);

    return 0;
}
결과 : 기밀 정보
이처럼 A의 friend 클래스 B는 A의 private 멤버에도 접근 할 수 있다.

6. 상속

6.1. 상속이란?

상속이란 기존에 존재하는 클래스로부터 모든 멤버를 이어받아 새로운 클래스를 만드는 것이다.
이미 존재하던 클래스를 기초 클래스 혹은 부모 클래스, 상위 클래스라고 하고 상속받는 클래스를 파생 클래스 또는 자식 클래스, 하위 클래스라고 한다.
다음은 상속에 사용되는 구문이다.

class <자식 클래스> : <접근지정자> <부모 클래스> {
    · · ·    // 추가된 멤버 변수와 멤버 함수
}
대개 부모 클래스는 추상적이고 자식 클래스는 구체적이다.

부모 클래스 자식 클래스
Animal Dog, Cat, Tiger
Vehicle Car, Bus, Truck, Boat, Bicycle
Shape Rectangle, Triangle, Circle


부모 클래스의 멤버가 자식 클래스로 상속되어 자식 클래스는 부모 클래스의 멤버 변수와 멤버 함수를 자유롭게 사용할 수 있고, 필요하다면 자식 클래스만의 변수와 함수를 추가시킬 수도 있으며 이미 존재하는 멤부를 재정의할 수도 있다.
상속의 강점은 부모 클래스로부터 상속된 특징을 자식 클래스에서 추가, 교체, 상세화시킬 수 있다는 점이다.

6.2. 필요성

상속을 사용하면 중복되는 코드를 줄일 수 있다.
Car, Bicycle 클래스는 공통적으로 speed, setSpeed(), getSpeed() 등의 멤버 변수와 멤버 함수를 갖는다. 이 클래스들을 개별적으로 작성하면 다음과 같다.

class Car {
    int speed;
public:
    void setSpeed(int s) { speed = s; }
    int getSpeed() { return speed; }
};

class Bicycle {
    int speed;
public:
    void setSpeed(int s) { speed = s; }
    int getSpeed() { return speed; }
};
Vehicle이라는 부모 클래스를 작성하여 상속을 사용하면 다음과 같다.
class Vehicle {
    int speed;
public:
    void setSpeed(int s) { speed = s; }
    int getSpeed() { return speed; }
};

class Car : public Vehicle { };
class Bicycle : public Vehicle { };
코드가 보다 간결해진 것을 볼 수 있다. 상속의 장점은 코드가 길어질수록 더 발휘된다.
중복되는 코드를 부모 클래스에 모으면 하나로 정리되어 관리하기나 유지 보수 및 변경이 수월해진다.

예제

아래 코드가 올바르게 실행되도록 Shape 클래스를 상속받아 Rectangle 클래스 작성하기

int main() {
    Rectangle r;
    r.x = 3;
    r.y = 6;
    r.width = 2;
    r.height = 8;
    r.printLocation();
    r.printArea();

    return 0;
}

Location: (3, 6)
Area: 16

예시 답안

#include <iostream>
using namespace std;

class Shape {
public:
    int x, y;
    void printLocation() {
        cout << "Location: (" << x << ", " << y << ")" << endl;
    }
};

class Rectangle : public Shape {
public:
    int width, height;
    void printArea() {
        cout << "Area: " << width * height << endl;
    }
};

int main() {
    Rectangle r;
    r.x = 3;
    r.y = 6;
    r.width = 2;
    r.height = 8;
    r.printLocation();
    r.printArea();

    return 0;
}

6.3. 상속과 생성자/소멸자

상속된 자식 클래스의 객체가 생성될 때에는 부모 클래스의 생성자가 먼저 호출된 후 자식 클래스의 생성자가 호출된다.
특별히 지정하지 않으면 부모 클래스의 기본 생성자가 호출된다.
반대로 객체가 소멸될 때에는 자식 클래스의 소멸자가 먼저 호출된 뒤 부모 클래스의 소멸자가 호출된다.
다음 코드를 실행하여 생성자와 소멸자의 호출 순서를 확인해보자.

#include <iostream>
using namespace std;

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

class Circle : public Shape {
public:
    Circle() { cout << "Circle()" << endl; }
    ~Circle() { cout << "~Circle()" << endl; }
};

int main()
{
    Circle c;
    return 0;
}

Shape()
Circle()
~Circle()
~Shape()


부모 클래스의 생성자를 지정하지 않으면 기본 생성자가 호출된다.
매개 변수가 있는 생성자를 호출하려면 자식 클래스의 생성자 헤더 뒤에 콜론을 추가하여 원하는 부모 클래스의 생성자를 적어주면 된다.

<자식 클래스의 생성자>() : <부모 클래스의 생성자>() {
    · · ·
}

#include <iostream>
using namespace std;

class Shape {
    int x = 0, y = 0;
public:
    Shape() { cout << "Shape()" << endl; }
    Shape(int xloc, int yloc) : x(xloc), y(yloc) {
        cout << "Shape(" << xloc << ", " << yloc << ")" << endl;
    }
    ~Shape() { cout << "~Shape()" << endl; }
};

class Circle : public Shape {
    int radius;
public:
    Circle(int x, int y, int r) : Shape(x, y), radius(r) {
        cout << "Circle(" << x << ", " << y << ", " << r << ")" << endl;
    }
    ~Circle() { cout << "~Circle()" << endl; }
};

int main() {
    Circle c(0, 0, 1);
    return 0;
}

Shape(0, 0)
Circle(0, 0, 1)
~Circle()
~Shape()


예제

Shape 클래스를 상속받아 Rectangle 클래스 작성하기

int main() {
    Rectangle(2, -4, 3, 5)
    return 0;
}

Shape(2, -4)
Rectangle(2, -4, 3, 5)
~Rectangle()
~Shape()

예시 답안

#include <iostream>
using namespace std;

class Shape {
private:
    int x, y;
public:
    Shape(int locX, int locY) : x(locX), y(locY) {
        cout << "Shape(" << x << ", " << y << ")" << endl;
    }
    ~Shape() { cout << "~Shape()" << endl; }
};

class Rectangle : public Shape {
private:
    int width, height;
public:
    Rectangle(int locX, int locY, int w, int h) : Shape(locX, locY), width(w), height(h) {
        cout << "Rectangle(" << locX << ", " << locY << ", " << width << ", " << height << ")" << endl;
    }
    ~Rectangle () { cout << "~Rectangle()" << endl; }
};

int main() {
    Rectangle r(2, -4, 3, 5);
    return 0;
}

6.4. 접근 지정자

클래스에서 멤버 변수들은 보통 private으로 지정되어 외부의 접근을 막는다. 하지만 private으로 지정되면 자식 클래스에서도 접근할 수 없다. 그렇다고 public으로 지정하면 외부에서 접근할 수 있어 객체 지향에 어긋나게 된다.
이때 사용하는 접근 지정자가 바로 protected이다.

접근 지정자 자기 클래스 자식 클래스 외부
private
protected
public


6.5. 멤버 함수 재정의

멤버 함수 재정의란 멤버 함수의 헤더는 그대로 두고 몸체만을 교체하는 것이다.
멤버 함수의 이름, 반환형, 매개 변수의 개수와 자료형이 모두 일치해야 재정의가 일어난다.

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "동물 소리" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "멍멍" << endl;
    }
};

int main() {
    Dog d;
    d.speak();
    return 0;
}

멍멍


위 코드에서 d.speak();을 실행했을 때 Animal 클래스의 speak() 함수가 아니라 Dog 클래스의 speak() 함수가 실행되었다.
Dog 객체를 통해 접근하면 Dog 클래스의 멤버 함수에 우선권이 있어 재정의된 함수가 실행된 것이다.
재정의된 함수가 아니라 부모 클래스의 함수를 실행하고 싶다면 범위 연산자 ::를 사용해 부모 클래스의 함수를 사용하겠다고 밝혀주면 된다.

int main() {
    Dog d;
    d.Animal::speak();
    return 0;
}

동물 소리


중복 정의(overloading)와 재정의(overriding)의 차이

중복 정의는 같은 이름의 함수를 여러 개 정의하는 것이고 재정의는 상속받은 멤버 함수의 내용을 변경하는 것이다.

6.6. 상속 접근 지정자

앞서 수행했던 상속은 모두 public을 통해 이루어졌다. 하지만 접근 지정자에 세 가지가 있는 것처럼, public 외의 다른 두 가지 접근 지정자를 사용하여 상속할 수 있다.

상속 접근 지정자 public protected private
부모 클래스의 public 멤버 → public protected private
부모 클래스의 protected 멤버 → protected protected private
부모 클래스의 private 멤버 → 접근 불가 접근 불가 접근 불가

상속된 클래스의 멤버의 접근 수준이 상속 접근 지정자의 접근 수준보다 낮으면 해당 상속 접근 지정자로 덮어 씌워진다고 생각하면 된다.
상속 접근 지정자를 따로 지정하지 않을 경우 클래스에서 접근 지정자를 특별히 표기하지 않았을 시 private으로 정해지는 것과 마찬가지로 상속에서도 기본값인 private로 정해진다.

#include <iostream>
using namespace;

class Base {
public: int a;
protected: int b;
private: int c;
};

class Derived : Base {
    // a는 사용 가능, private
    // b는 사용 가능, private
    // c는 사용 불가
}

int main() {
    Base baseObj;
    Derived derivedObj;

    cout << baseObj.a;  // 접근 가능
    cout << baseObj.b;  // 접근 불가
    cout << baseObj.c;  // 접근 불가
    cout << derivedObj.a;  // 접근 불가
    cout << derivedObj.b;  // 접근 불가
    cout << derivedObj.c;  // 접근 불가

6.7. 다중 상속

다중 상속이란 말 그대로 두 개 이상의 부모 클래스로부터 상속받는 것을 뜻한다.

class Sub : public Sup1, public Sup2 {
    · · ·
};

다중 상속을 사용할 때의 문제점이 하나 있는데, 바로 상속해주는 서로 다른 클래스에 똑같은 이름을 가진 멤버가 있을 경우 일반적인 접근으로는 해당 멤버를 사용할 수 없다는 것이다.

#include <iostream>
using namespace std;

class Sup1 { public: int x = 1; };
class Sup2 { public: int x = 2; };
class Sub : public Sup1, public Sup2 { };

int main() {
    Sub s;
    cout << s.x;  // 'Sub::x'가 모호합니다. 'x' 액세스가 모호합니다.
    return 0;
}

이를 해결하려면 가리키는 대상이 모호하지 않게 해주면 된다.

int main() {
    Sub s;
    cout << s.Sup1::x;  // 1
    return 0;
}

7. 다형성

7.1. 다형성이란?

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "동물 소리" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "멍멍" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() {
        cout << "야옹" << endl;
    }
};

int main() {
    Dog d;
    Cat c;
    d.speak();
    c.speak();
    return 0;
}

멍멍
야옹

위 코드를 보면 개와 고양이 객체에서 똑같은 speak() 함수를 실행시켰는데 서로 다른 결과를 얻었다.
이처럼 동일한 코드를 사용하지만 객체의 타입이 다르면 서로 다른 결과를 얻는 것이 다형성이다.
위 코드는 다형성의 개념을 설명하기 위한 예시일 뿐 실제 다형성이 아니다. 다형성은 포인터를 사용하여 수행된다.

7.2. 상향 형변환

다형성은 객체 포인터로 수행된다. Animal 클래스에서 상속받은 Dog 클래스를 생각해보자.

Animal* p = new Dog();

Animal 타입의 포인터로 Dog 타입의 객체를 가리키는 이 코드는 놀랍게도 정상적으로 기능한다.
자식 객체는 부모 객체를 포함하고 있기 때문에 부모 포인터로 자식 객체를 가리킬 수 있는 것이다.
이를 상향 형변환이라고 한다.
다만, 부모 포인터로 가리키고 있기 때문에 이 방법으로는 자식 클래스 중에서 부모에게 상속받은 부분만을 사용할 수 있다.

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "동물 소리" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "멍멍" << endl;
    }
};

int main() {
    Animal* p = new Dog();
    p->speak();
    return 0;
}

동물 소리

7.3. 가상 함수

위의 코드에서 Animal 포인터로 접근하더라도 객체의 종류에 따라 speak() 함수가 다르게 실행된다면 좋을 것이다. 이는 부모 클래스의 멤버 함수를 virtual 키워드를 통해 '가상 함수'로 정의하여 수행할 수 있다.

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "동물 소리" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "멍멍" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() {
        cout << "야옹" << endl;
    }
};

int main() {
    Animal* p = new Dog();
    p->speak();
    p = new Cat();
    p->speak();
    return 0;
}

멍멍
야옹

Animal 형의 포인터로 서로 다른 형의 객체를 가리켰고, 똑같이 speak() 함수가 실행되었지만 서로 다른 결과를 얻었다. 이를 다형성이라 한다.

7.4. 참조자와 가상 함수

참조자를 통해서도 다형성을 수행할 수 있다.
포인터를 사용하여 다형성을 수행할 때처럼 부모 클래스의 참조자로 자식 클래스를 참조할 수 있고 가상 함수 또한 기능한다.

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "동물 소리" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "멍멍" << endl;
    }
};

int main() {
    Dog d;
    Animal &a = d;
    a.speak();
    return 0;
}

멍멍


7.5. 가상 소멸자

포인터로 다형성을 수행하고 나서 객체를 삭제해보자.

#include <iostream>
using namespace std;

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

class Sub : public Sup {
public:
    ~Sub() { cout << "~Sub()" << endl; }
};

int main() {
    Sup* p = new Sub();
    delete p;
    return 0;
}

~Sup()

자식 객체를 생성하여 부모 포인터로 가리킨 후 객체를 삭제하면 자식 소멸자가 호출되지 않고 부모 소멸자만 호출된다.
자식 소멸자도 호출되게 하려면 부모 클래스의 소멸자를 가상 함수로 선언하면 된다.

#include <iostream>
using namespace std;

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

class Sub : public Sup {
public:
    ~Sub() { cout << "~Sub()" << endl; }
};

int main() {
    Sup* p = new Sub();
    delete p;
    return 0;
}

~Sub()
~Sup()


7.6. 순수 가상 함수

순수 가상 함수는 함수의 정의가 없고 선언만 된 함수이다.

virtual <반환형> <함수명>(매개 변수) = 0;

순수 가상 함수를 갖고 있는 클래스를 추상 클래스라고 한다.
추상 클래스는 추상적인 개념을 나타내거나 클래스 간 인터페이스를 나타내는 용도로 사용된다.
추상 클래스로는 객체를 만들 수 없고 상속으로만 사용된다.
또, 함수 정의가 없기에 상속받은 자식 클래스는 순수 가상 함수를 재정의해야 한다.

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
public:
    void speak() { cout << "멍멍" << endl; }
};

class Cat : public Animal {
public:
    void speak() { cout << "야옹" << endl; }
};

int main() {
    Animal* p = new Dog();
    p->speak();
    p = new Cat();
    p->speak();
    return 0;
}

멍멍
야옹

추상 클래스를 사용하지 않고 각 클래스별로 멤버 함수를 정의하여 사용할 수도 있다.
하지만 그럴 경우 데이터가 커지게 되면 몇 가지 공통 요소를 빠트릴 수 있다.
추상 클래스를 사용하게 되면 함수를 꼭 재정의해야 하기 때문에 이러한 실수를 미연에 방지할 수 있다.
또, 추상 클래스를 정의할 때에는 멤버 변수 없이 온전히 순수 가상 함수로만 이뤄져야 좋다.

예제

Shape 클래스를 상속받아 Circle, Rectangle, Triangle 클래스 작성하여 다형성 수행하기

int main() {
    Shape* s = new Circle(0, 4, 5);
    cout << "Circle : " << s->getArea() << endl;
    s = new Rectangle(3, -2, 6, 4);
    cout << "Rectangle : " << s->getArea() << endl;
    s = new Triangle(2, 5, 7, 3);
    cout << "Triangle : " << s->getArea() << endl;

    return 0;
}

Circle : 78.5398
Rectangle : 24
Triangle : 10.5

예시 답안

#define _USE_MATH_DEFINES
#include <iostream>
#include <cmath>
using namespace std;

class Shape {
private:
    int x, y;
public:
    Shape(int locX, int locY) : x(locX), y(locY) {}
    virtual double getArea() = 0;
};

class Circle : public Shape {
private:
    int radius;
public:
    Circle(int locX, int locY, int r) : Shape(locX, locY), radius(r) {}
    double getArea() { return radius * radius * M_PI; }
};

class Rectangle : public Shape {
private:
    int width, height;
public:
    Rectangle(int locX, int locY, int w, int h) : Shape(locX, locY), width(w), height(h) {}
    double getArea() { return width * height; }
};

class Triangle : public Rectangle {
public:
    Triangle(int locX, int locY, int w, int h) : Rectangle(locX, locY, w, h) {}
    double getArea() { return Rectangle::getArea() / 2; }
};

int main() {
    Shape* s = new Circle(0, 4, 5);
    cout << "Circle : " << s->getArea() << endl;
    s = new Rectangle(3, -2, 6, 4);
    cout << "Rectangle : " << s->getArea() << endl;
    s = new Triangle(2, 5, 7, 3);
    cout << "Triangle : " << s->getArea() << endl;

    return 0;
}