C++ 입문 3일차 - 클래스, 생성자, 소멸자, 상속, 다형성, 추상 클래스, 인터페이스

하는·4일 전
1

C++ 입문 챌린지

목록 보기
3/5

클래스 Class

class: 클래스를 정의할 때 사용하는 키워드.
public: 외부에서 접근 가능한 멤버 지정.
- 클래스 내부에서만 사용할 멤버는 private으로 설정할 수 있다.

멤버 함수: 클래스 내부에 포함된 함수. 데이터를 처리하는 로직을 포함

#include <iostream>

class Person {
public:
    std::string name;
    int age;
    char gender;

    // 멤버 함수
    void introduce() {
        std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
    }
};

int main() {
    // 클래스 객체 선언
    Person p1;

    // 객체 멤버 변수에 값 대입
    p1.name = "Alice";
    p1.age = 30;
    p1.gender = 'F';

    // 멤버 함수 호출
    p1.introduce();

    return 0;
}

연습문제

  1. 구조체를 사용해 학생 정보를 저장하고 출력하는 프로그램을 만들어보자. 학생의 이름, 나이, 학년을 저장하도록 하자.
  2. 클래스를 사용해 간단한 Car 클래스를 만들어보자. Car 클래스는 모델명, 연식, 그리고 출력을 담당하는 멤버 함수를 가지도록 해보자.
// 1.
#include <iostream>
#include <string>  // 문자열을 사용하기 위해 포함

struct Student {
    std::string name;  // str -> std::string
    int age;
    int grade;
};

int main() {
    Student s1;
    s1.name = "Alice";
    s1.age = 16;
    s1.grade = 10;

    std::cout << "Name: " << s1.name << std::endl;
    std::cout << "Age: " << s1.age << std::endl;
    std::cout << "Grade: " << s1.grade << std::endl;

    return 0;
}

// 2.
#include <iostream>
#include <string>

class Car {
public:
    std::string name;  // 모델명
    int age;  // 연식
    
    // 멤버 함수
    void introduce() {
        std::cout << "This car's name is " << name << " and it is " << age << " years old." << std::endl;
    }
};

int main() {
    Car car1;
    car1.name = "Toyota";
    car1.age = 5;

    car1.introduce();

    return 0;
}

생성자 (Constructor)

객체가 생성될 때 자동으로 호출되는 함수.
객체의 멤버 변수를 초기화하는 역할. 클래스 이름과 동일한 이름을 가지고 반환형이 없다.

  • 파이썬에서 __init__과 같은 역할
class Person {
public:
    std::string name;
    int age;

    // 생성자
    Person() {
        name = "Unknown";
        age = 0;
    }

    void introduce() {
        std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
    }
};

int main() {
    Person p1;  // 생성자가 자동으로 호출되어 name과 age가 초기화됨
    p1.introduce();
    return 0;
}

매개변수를 받는 생성자

매개변수: 객체의 멤버 변수를 초기화하기 위한 값.

class Person {
public:
    std::string name;
    int age;

    // 매개변수 생성자
    Person(std::string n, int a) {
        name = n;
        age = a;
    }

    void introduce() {
        std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
    }
};

같은 기능 파이썬에서 구현:

class Person:
    # 매개변수 생성자
    def __init__(self, name, age):
        self.name = name  # 매개변수 name을 멤버 변수 self.name에 저장
        self.age = age    # 매개변수 age를 멤버 변수 self.age에 저장

    def introduce(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

소멸자 (Destructor)

  • 객체가 삭제될 때 자동으로 호출되는 함수
  • 동적으로 할당된 메모리를 해제할 때 사용
  • 클래스 이름 앞에 ~를 붙여서 정의
  • 반환형x, 매개변수x
class Person {
public:
    std::string name;

    // 생성자
    Person(std::string n) {
        name = n;
        std::cout << name << " is created." << std::endl;
    }

    // 소멸자
    ~Person() {
        std::cout << name << " is destroyed." << std::endl;
    }
};

int main() {
    Person p1("Alice");
    // 프로그램이 끝날 때 p1의 소멸자가 자동으로 호출됨
    return 0;
}

연습 문제

  1. 매개변수 생성자를 사용해 Student 클래스를 만들어보자. Student는 이름과 학년을 가지며, 학생의 정보를 출력하는 멤버 함수를 포함해야 해.
  2. 소멸자를 사용해 Car 클래스에 소멸자가 호출될 때 "Car is being destroyed"라는 메시지를 출력하도록 만들어보자.
#include <iostream>

class Student {
public:
    std::string name;
    int grade;

    // 매개변수 생성자
    Student(std::string n, int g) {
        name = n;
        grade = g;
    }

    // 멤버 함수
    void introduce() {
        std::cout << "Name: " << name << ", Grade: " << grade << std::endl;
    }

    // 소멸자
    ~Student() {
        std::cout << name << " is being destroyed." << std::endl;
    }
};

int main() {
    // 객체 생성
    Student s1("Alice", 26);

    // 객체의 멤버 함수 호출
    s1.introduce();

    return 0;
}

상속

부모 클래스의 기능을 자식 클래스가 물려받아 확장하거나 수정할 수 있는 기능

상속의 기본 구조

class Parent {
public:
    void speak() {
        std::cout << "I am the parent." << std::endl;
    }
};

class Child : public Parent {
public:
    void introduce() {
        std::cout << "I am the child." << std::endl;
    }
};

int main() {
    Child c;
    c.speak();  // 부모 클래스의 함수 호출
    c.introduce();  // 자식 클래스의 함수 호출
    return 0;
}

다형성(Polymorphism)

  • 같은 함수를 여러 가지 방식으로 구현하는 능력
    부모 클래스에서 정의된 함수를 자식 클래스가 재정의(override)할 수 있다.

Virtual: 함수 재정의

  • 부모 클래스 앞에 virtual 키워드를 붙이면, 자식 클래스에서 그 함수를 재정의할 수 있다.
  • 재정의된 함수는 부모 클래스의 포인터나 참조를 통해 호출되더라도 자식 클래스의 동작을 따르게 된다.
class Parent {
public:  // 외부에서 클래스의 멤버 변수나 함수에 접근하고 싶을 때 사용
    virtual void speak() {  // 가상 함수
        std::cout << "I am the parent." << std::endl;
    }
};

class Child : public Parent {
public:
    void speak() override {  // 함수 재정의
        std::cout << "I am the child." << std::endl;
    }
};

int main() {
    Parent* p = new Child();  // 부모 클래스 포인터가 자식 클래스를 가리킴.
    p->speak();  // 자식 클래스의 speak 함수가 호출됨

    delete p;  // 동적 메모리 해제
    return 0;
}

virtual과 override

만약 Child 클래스에서 함수를 재정의할 때 virtual과 override가 없다면?
부모 클래스의 포인터나 참조로 자식 객체를 가리킬 때 부모 클래스의 함수가 호출된다. 즉, 부모 클래스의 speak() 함수가 호출되고, 자식 클래스의 speak() 함수는 무시된다.

포인터 사용 이유

다형성을 사용하려면 부모 클래스의 포인터 또는 참조를 통해 자식 클래스 객체에 접근해야 한다.
포인터를 사용하지 않으면 정적 바인딩이 이뤄진다.
즉, 컴파일 타임에 어느 함수가 호출될지 결정된다. = 해당 클래스의 함수만 사용할 수 있고 재정의된 함수는 호출되지 않는다.

동적 메모리 해제

  • new로 동적 할당한 메모리는 반드시 delete로 해제해야 함.

delete p;가 없으면: 동적으로 할당된 메모리가 해제되지 않아 메모리 누수(memory leak) 가 발생 -> 프로그램의 메모리 사용량이 계속 증가하여 시스템 성능이 저하

연습 문제

  1. 상속을 사용해 Animal 클래스를 만들고, Dog와 Cat 클래스를 상속받아 각각 다른 소리를 내는 speak 함수를 구현해보자.
  2. 다형성을 사용해 Animal 클래스의 speak 함수를 Dog와 Cat에서 재정의하고, 부모 클래스 포인터를 통해 각각의 speak 함수가 호출되는 프로그램을 만들어보자.
#include <iostream>

class Animal {
public:
    virtual void sound() {  // 가상 함수
        std::cout << "sound sample" << std::endl;
    }
};

class Dog: public Animal {
public:
    void sound() override {  // 함수 재정의
        std::cout << "Bark" << std::endl;
    }
};

class Cat: public Animal {
public:
    void sound() override {  // 함수 재정의
        std::cout << "Meow" << std::endl;
    }
};

int main() {
    Animal* p1 = new Dog;
    Animal* p2 = new Cat;

    // 함수 호출
    p1->sound();  // Dog의 sound 함수 호출
    p2->sound();  // Cat의 sound 함수 호출

    // 동적 메모리 해제
    delete p1;
    delete p2;

    return 0;
}

추상 클래스 (Abstract Class)

  • 한 개 이상의 순수 가상 함수를 포함하는 클래스
  • 여러 클래스를 그룹화 하기 위해 생성한다.

순수 가상 함수

  • 함수 자체에 구현이 없는 가상 함수
class Animal {
public:
    virtual void sound() = 0;  // 순수 가상 함수
};

class Dog : public Animal {
public:
    void sound() override {
        std::cout << "Bark" << std::endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
        std::cout << "Meow" << std::endl;
    }
};

int main() {
    Animal* p1 = new Dog;
    Animal* p2 = new Cat;

    p1->sound();  // Dog의 sound 호출
    p2->sound();  // Cat의 sound 호출

    delete p1;
    delete p2;

    return 0;
}

인터페이스 (Interface)

인터페이스(추상 클래스)란?

공통된 기능을 정의하지만, 그 기능의 구체적인 구현은 포함하지 않는 클래스

파이썬에서 추상 클래스를 사용한 인터페이스 예시:

class Shape:
    def area(self):
        pass  # 공통 인터페이스 역할만, 실제 구현 없음

# Circle 클래스 (Shape 상속)
class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return 3.14 * self.r * self.r

# Rectangle 클래스 (Shape 상속)
class Rectangle(Shape):
    def __init__(self, w, h):
        self.w = w
        self.h = h

    def area(self):
        return self.w * self.h

# 메인 함수
def main():
    # Rectangle 객체 생성
    rec = Rectangle(3, 4)
    print("Area of rectangle:", rec.area())

    # Circle 객체 생성
    circ = Circle(5)
    print("Area of circle:", circ.area())

if __name__ == "__main__":
    main()

추상 메서드란?

  • 인터페이스에서 선언된 메서드
  • 구현이 없이 선언만 되어 있는 메서드
  • 파이썬에서는 pass를 사용

위 코드에서 Shape 클래스는 추상 클래스입니다. area 메소드는 구현되어 있지 않고, 추상 메소드로 선언되어 있습니다. 따라서, 이 클래스를 직접 인스턴스화 할 수 없으며, 이 클래스를 상속받아 구현된 클래스를 사용해야 합니다.
Circle 클래스와 Rectangle 클래스는 Shape 클래스를 상속받아서 만들어진 구체적인 도형 클래스입니다. 이들 클래스는 Shape 클래스에서 정의한 move 메소드를 사용할 수 있고, area 메소드를 구체적으로 구현하여 각 도형의 면적을 계산할 수 있습니다.
이러한 추상화를 통해, 각 도형의 공통된 속성과 기능을 효율적으로 관리할 수 있고, 필요에 따라 도형 클래스를 상속받아서 새로운 도형을 추가할 수 있습니다.
C++에서는 인터페이스라는 개념이 별도로 존재하지는 않지만, 이를 구현하는 방법은 순수 가상 함수만을 포함하는 추상 클래스를 정의하는 방식이다. 인터페이스는 오직 함수의 선언만 포함하고, 구현은 자식 클래스에서 이루어지게끔 만든다.

cpp:

class IShape {
public:
    virtual void draw() = 0;
    virtual double area() = 0;
};

class Circle : public IShape {
public:
    void draw() override {
        std::cout << "Drawing Circle" << std::endl;
    }

    double area() override {
        return 3.14 * radius * radius;
    }

private:
    double radius = 5.0;
};

class Rectangle : public IShape {
public:
    void draw() override {
        std::cout << "Drawing Rectangle" << std::endl;
    }

    double area() override {
        return width * height;
    }

private:
    double width = 4.0;
    double height = 6.0;
};

int main() {
    IShape* shape1 = new Circle;
    IShape* shape2 = new Rectangle;

    shape1->draw();
    std::cout << "Area: " << shape1->area() << std::endl;

    shape2->draw();
    std::cout << "Area: " << shape2->area() << std::endl;

    delete shape1;
    delete shape2;

    return 0;
}

추상 클래스를 직접 객체로 생성하면 에러가 뜬다.

#include <iostream>
class IShape {
public:
    virtual void draw() = 0;  // 순수 가상 함수
};

int main() {
    IShape shape;  // 에러 발생: 추상 클래스의 객체는 생성 불가능
    return 0;
}
// 에러 메세지:
main.cpp: In function ‘int main()’:
main.cpp:16:12: error: cannot declare variable ‘shape’ to be of abstract type ‘IShape’
   16 |     IShape shape;  // 에러 발생: 추상 클래스의 객체는 생성 불가능
      |            ^~~~~

연습 문제

  1. 추상 클래스를 사용해 Vehicle 클래스를 만들고, 이를 상속받는 Car와 Bike 클래스를 정의해보자. 각 클래스는 move() 함수를 구현해야 해.
  2. 인터페이스를 사용해 Shape 인터페이스를 만들고, 이를 구현하는 Square와 Triangle 클래스를 만들어 각각의 넓이를 구하는 함수를 구현해보자.
// 1번
#include <iostream>

class Vehicle {
public:
    virtual void move() = 0;
};

class Bike : public Vehicle {
public:
    void move() override { 
        std::cout << "beep beep" << std::endl;
    }
};

class Car : public Vehicle {
public:
    void move() override { 
        std::cout << "ding-ding" << std::endl;
    }
};

int main() {
    Vehicle* veh1 = new Car;
    Vehicle* veh2 = new Bike;

    std::cout << "Car sound: ";
    veh1->move();  // Car의 move 함수 호출
    
    std::cout << "Bike sound: ";
    veh2->move();  // Bike의 move 함수 호출

    delete veh1;
    delete veh2;

    return 0;
}

// 2번
#include <iostream>

// 추상 클래스 Shape
class Shape {
public:
    virtual double area(double l) = 0;  // 순수 가상 함수
};

// Square 클래스, Shape 상속
class Square : public Shape {
public:
    double area(double l) override {  // 함수 재정의
        return l * l;
    }
};

// Triangle 클래스, Shape 상속
class Triangle : public Shape {
public:
    double area(double l) override {  // 함수 재정의
        return (l * l) / 2;
    }
};

int main() {
    Shape* shape1 = new Square;
    Shape* shape2 = new Triangle;

    double l = 4.0;
    
    // Square의 area 함수 호출
    std::cout << "Square area: " << shape1->area(l) << std::endl;
    
    // Triangle의 area 함수 호출
    std::cout << "Triangle area: " << shape2->area(l) << std::endl;

    delete shape1;
    delete shape2;

    return 0;
}
profile
천천히 꾸준히 취미처럼 냐미😋

0개의 댓글