C++ 기초플러스 (13. 클래스의 상속)
클래스 파생시키기
기존 클래스를 활용해서 새로운 클래스를 만들어내는 것
● 파생 클래스형의 객체 안에는 기초 클래스형의 데이터 멤버들이 저장된다.(파생 클래스는 기초 클래스의 구현들을 상속받는다.)
● 파생 클래스형의 객체는 기초 클래스형의 메서드들을 사용할 수 있다.(파생 클래스는 기초 클래스의 인터페이스를 상속받는다.)
● 파생 클래스는 자기 자신의 생성자를 필요로 한다.
● 파생 클래스는 부가적인 데이터 멤버들과 멤버 함수들을 필요한 만큼 추가할 수 있다.
● 파생 클래스는 기초 클래스의 private 멤버에 직접 접근할 수 없다. 기초 클래스의 메서드들을 통해서 접근해야 한다.
** 기초클래스의 private 멤버에 접근하려면, 기초클래스의 public 메서드를 사용해야한다. 특히, 파생 클래스의 생성자는 기초 클래스의 생성자를 사용해야 한다.
class 자식클래스 : 접근지정자 부모클래스
{
// 자식 클래스의 멤버
};
[파생 클래스의 생성자]
자식 클래스(파생 클래스)의 객체를 생성할 때는 부모 클래스(기초 클래스)의 멤버 변수들도 초기화되어야 하므로 부모 클래스의 생성자도 함께 호출된다. 자식 클래스의 생성자는 부모 클래스의 생성자를 호출하여 부모 클래스의 멤버들을 먼저 초기화하고, 그 다음에 자식 클래스 자신의 멤버들을 초기화 한다.
자식 클래스의 생성자에서는 멤버 초기화 리스트를 사용하여 부모 클래스의 생성자를 명시적으로 호출할 수 있다. 멤버 초기화 리스트는 생성자의 몸체 부분 앞에 콜론(:)을 붙이고, 부모 클래스의 생성자와 초기화할 멤버 변수들을 나열하는 방식으로 작성한다.
class Animal {
public:
Animal(int age) : age(age) {}
int age;
};
class Dog : public Animal {
public:
Dog(int age, string name) : Animal(age), name(name) {}
string name;
};
int main() {
Dog myDog(3, "Buddy");
return 0;
}
=> 이 코드에서 Dog 클래스의 생성자는 멤버 초기화 리스트 : Animal(age), name(name)를 사용하여 부모 클래스 Animal의 생성자를 호출하고, name 멤버 변수를 초기화한다.
=> myDog 객체가 생성될 때, 먼저 Animal의 생성자가 호출되어 age가 3으로 초기화되고, 그 다음에 Dog의 생성자가 호출되어 name이 "Buddy"로 초기화된다.
=> 파생 클래스의 객체가 수명이 다했을 때, 프로그램은 먼저 자식 클래스(파생 클래스) 파괴자를 호출하고, 그 다음에 부모 클래스(기초 클래스) 파괴자를 호출한다.
// tabtenn1.h -- 탁구 기초 수업
#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
// 간단한 기초 클래스
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer (const string & fn = "none",
const string & ln = "none", bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
// 간단한 파생 클래스 (자식 클래스: 접근제어자 부모클래스)
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating; // 자식 클래스에서 만든 멤버변수
public:
RatedPlayer (unsigned int r = 0, const string & fn = "none",
const string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const { return rating; }
void ResetRating (unsigned int r) {rating = r;}
};
#endif
//tabtenn1.cpp -- 간단한 기초 클래스 메서드
#include "tabtenn1.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer (const string & fn,
const string & ln, bool ht) : firstname(fn),
lastname(ln), hasTable(ht) {}
void TableTennisPlayer::Name() const
{
std::cout << lastname << ", " << firstname;
}
// RatedPlayer 메서드
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}
// usett1.cpp -- 기초 클래스와 파생 클래스를 사용한다
#include <iostream>
#include "tabtenn1.h"
int main ( void )
{
using std::cout;
using std::endl;
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
rplayer1.Name(); // 자식객체(파생객체)가 부모메서드(기초메서드)를 사용한다.
if (rplayer1.HasTable())
cout << ": 탁구대가 있다.\n";
else
cout << ": 탁구대가 없다.\n";
player1.Name(); // 기초객체(부모객체)가 기초메서드를 사용한다
if (player1.HasTable())
cout << ": 탁구대가 있다.";
else
cout << ": 탁구대가 없다.\n";
cout << "이름: ";
rplayer1.Name();
cout << "; 랭킹: " << rplayer1.Rating() << endl;
// TableTennisPlayer 객체를 사용하여 RatePlayer를 초기화한다
RatedPlayer rplayer2(1212, player1);
cout << "이름: ";
rplayer2.Name();
cout << "; 랭킹: " << rplayer2.Rating() << endl;
// std::cin.get();
return 0;
}
상속 : is-a 관계 / has-a관계
▶▶▶ is-a 관계:
정의:
"is-a" 관계는 한 클래스가 다른 클래스의 특수한 형태임을 나타냅니다.
이는 상속을 통해 구현됩니다.
특징:
부모 클래스의 모든 특성을 자식 클래스가 상속받습니다.
자식 클래스는 부모 클래스의 확장된 버전이라고 볼 수 있습니다.
class Animal {
// Animal 클래스의 속성과 메서드
};
class Dog : public Animal {
// Dog는 Animal의 특수한 형태입니다.
};
// "Dog is an Animal"이라고 말할 수 있습니다.
사용 시기:
두 개체 간에 명확한 계층 관계가 있을 때 사용합니다.
공통된 특성을 여러 클래스에서 재사용하고자 할 때 적합합니다.
▶▶▶ has-a 관계:
정의:
"has-a" 관계는 한 클래스가 다른 클래스의 객체를 포함하고 있음을 나타냅니다.
이는 컴포지션(composition) 또는 집합(aggregation)을 통해 구현됩니다.
특징:
한 클래스가 다른 클래스의 인스턴스를 멤버 변수로 가집니다.
두 클래스 간의 관계가 "소유" 또는 "사용"의 관계입니다.
class Engine {
// Engine 클래스의 속성과 메서드
};
class Car {
private:
Engine engine; // Car has an Engine
};
// Car has an Engine 이라고 말할 수 있다.
사용 시기:
한 클래스가 다른 클래스의 기능을 사용해야 할 때 적합합니다.
클래스 간의 결합도를 낮추고 유연성을 높이고자 할 때 사용합니다.
▶▶▶ 주요 차이점
관계의 성격:
is-a: 계층적, 분류학적 관계
has-a: 소유 또는 사용 관계
구현 방식:
is-a: 상속을 통해 구현
has-a: 컴포지션 또는 집합을 통해 구현
코드 재사용:
is-a: 부모 클래스의 코드를 직접 재사용
has-a: 포함된 객체의 기능을 활용하여 간접적으로 재사용
유연성:
is-a: 상대적으로 덜 유연함 (상속은 컴파일 시간에 결정됨)
has-a: 더 유연함 (런타임에 객체 구성을 변경할 수 있음)
결합도:
is-a: 높은 결합도 (자식 클래스가 부모 클래스에 강하게 의존)
has-a: 낮은 결합도 (객체 간 독립성이 더 높음)
적절한 관계 선택은 설계의 유연성, 재사용성, 그리고 유지보수성에 큰 영향을 미칩니다. 따라서 각 상황에 맞는 관계를 신중히 선택해야 한다.
class 자식클래스 : 접근지정자 부모클래스
{
// 자식 클래스의 멤버
};
▶▶▶ 접근 지정자의 역할:
1. public 상속: 부모 클래스의 public 멤버는 자식 클래스에서도 public으로 유지됩니다.
2. protected 상속: 부모 클래스의 public 멤버가 자식 클래스에서 protected로 변경됩니다.
3. private 상속: 부모 클래스의 모든 멤버가 자식 클래스에서 private로 변경됩니다.
▶▶▶ 생성자와 소멸자:
자식 클래스의 생성자는 부모 클래스의 생성자를 먼저 호출한 후 실행됩니다.
소멸자는 자식 클래스의 소멸자가 먼저 실행된 후 부모 클래스의 소멸자가 실행됩니다.
▶▶▶ 멤버 접근:
자식 클래스는 부모 클래스의 public과 protected 멤버에 접근할 수 있습니다.
private 멤버는 직접 접근할 수 없으며, public 메서드를 통해 간접적으로 접근해야 합니다.
▶▶▶ 다중 상속:
C++에서는 한 클래스가 여러 클래스를 동시에 상속받을 수 있습니다.
▶▶▶ 상속의 종류:
1. 단일 상속: 한 클래스가 하나의 클래스만 상속받는 경우
2. 다중 상속: 한 클래스가 여러 클래스를 상속받는 경우
3. 계층적 상속: 여러 클래스가 하나의 기본 클래스를 상속받는 경우
▶▶▶ 가상 함수와 다형성:
부모 클래스에서 virtual 키워드를 사용하여 가상 함수를 선언하면, 자식 클래스에서 이를 재정의할 수 있습니다.
이를 통해 런타임 다형성을 구현할 수 있습니다.
상속은 코드 재사용성을 높이고 관계를 표현하는 강력한 도구이지만, 과도한 사용은 복잡성을 증가시킬 수 있으므로 신중하게 사용해야 한다.
public의 다형 상속
정의:
public 다형 상속은 기본 클래스의 public 및 protected 멤버를 그대로 유지하면서 상속하는 방식입니다.
이를 통해 기본 클래스의 인터페이스를 파생 클래스에서 그대로 사용할 수 있습니다.
[public 다형상속을 구현하는 두가지 방법]
1. 기초 클래스(부모 클래스) 메서드를 파생 클래스(자식 클래스)에서 다시 정의한다.
2. 가상 메서드를 사용한다.
class 파생클래스 : public 기본클래스
{
// 파생 클래스의 멤버
};
접근 지정자 유지:
기본 클래스의 public 멤버는 파생 클래스에서도 public으로 유지됩니다.
protected 멤버도 파생 클래스에서 protected로 유지됩니다.
다형성 지원:
기본 클래스 포인터나 참조를 통해 파생 클래스 객체를 다룰 수 있습니다.
이를 통해 런타임 다형성을 구현할 수 있습니다.
가상 함수 사용:
기본 클래스에서 virtual 키워드를 사용하여 가상 함수를 선언합니다.
파생 클래스에서 이 함수를 재정의(override)할 수 있습니다.
동적 바인딩:
가상 함수를 사용하면 런타임에 실제 객체의 타입에 따라 적절한 함수가 호출됩니다.
#include<iostream>
class Animal {
public:
virtual void makeSound() { // 가상 메서드 (추상 클래스)
std::cout << "The animal makes a sound" << std::endl;
}
virtual ~Animal() {} // 가상 소멸자
};
class Dog : public Animal {
public:
void makeSound() override { // 부모 클래스 메서드 재정의
std::cout << "The dog barks" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "The cat meows" << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // 출력: The dog barks
animal2->makeSound(); // 출력: The cat meows
delete animal1;
delete animal2;
return 0;
}
▶ 가상 소멸자:
다형 상속을 사용할 때는 기본 클래스의 소멸자를 가상으로 선언해야 한다.
이는 메모리 누수를 방지하고 올바른 소멸자 호출 순서를 보장한다.
** 소멸자(파괴자)들이 가상이 아니라면, 포인터형에 해당하는 소멸자(파괴자)만 호출될 것이다.
▶ 오버라이딩 vs 오버로딩:
오버라이딩은 기본 클래스의 가상 함수를 파생 클래스에서 재정의하는 것이다.
오버로딩은 같은 이름의 함수를 다른 매개변수로 정의하는 것으로, 다형성과는 관련이 없다.
▶ 순수 가상 함수:
기본 클래스에서 순수 가상 함수를 선언하면 추상 클래스가 된다.
추상 클래스는 인스턴스화할 수 없으며, 파생 클래스에서 모든 순수 가상 함수를 구현해야 한다.
** public 다형 상속은 코드의 재사용성과 확장성을 높이는 강력한 도구이다. 하지만 과도한 사용은 복잡성을 증가시킬 수 있으므로, 적절한 설계와 사용이 중요하다.
기초클래스(부모 클래스)의 생성자와 파괴자(소멸자)
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 생성자 호출\n"; }
~Base() { std::cout << "Base 소멸자 호출\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 생성자 호출\n"; }
~Derived() { std::cout << "Derived 소멸자 호출\n"; }
};
int main() {
Derived obj;
return 0;
}
=> main( )실행 Derived클래스 객체 obj 생성-> 부모클래스(기초클래스) 생성자 호출 -> 자식클래스(파생클래스) 생성자 호출 -> 자식클래스 소멸자 호출
-> 부모클래스 소멸자 호출 -> main( ) 종료
정적 결합(Static Binding) 과 동적 결합(Dynamic Binding)
▶▶▶ 정적결합
정의:
컴파일 시간에 함수 호출과 함수 정의가 연결되는 방식이다.
조기 결합(early binding)이라고도 한다.
특징:
컴파일러가 컴파일 시간에 어떤 함수를 호출할지 결정한다.
실행 속도가 빠르다.
함수 오버로딩, 연산자 오버로딩 등에 사용된다.
#include <iostream>
using namespace std;
class Base {
public:
void show() { cout << "In Base\n"; }
};
class Derived : public Base {
public:
void show() { cout << "In Derived\n"; }
};
int main() {
Base* bp = new Derived;
bp->show();
}
▶▶▶ 동적결합
정의:
런타임에 함수 호출과 함수 정의가 연결되는 방식입니다.
지연 결합(late binding)이라고도 합니다.
특징:
실행 시간에 객체의 실제 타입에 따라 호출할 함수가 결정됩니다.
가상 함수(virtual function)를 사용하여 구현됩니다.
다형성을 구현하는 데 사용됩니다.
#include<iostream>
using namespace std;
class Base { // 기초 클래스
public:
virtual void show() { cout << "In Base\n"; } // 기초클래스의 가상함수
};
class Derived : public Base { // 파생클래스 (자식 클래스에서 부모클래스 호출)
public:
void show() override { cout << "In Derived\n"; } // 기초클래스 메서드 재정의
};
int main() {
Base* bp = new Derived;
bp->show(); // 출력: "In Derived"
}
▶▶▶ 주요 차이점
결정 시점:
정적 결합: 컴파일 시간
동적 결합: 런타임
성능:
정적 결합: 더 빠름
동적 결합: 상대적으로 느림
유연성:
정적 결합: 덜 유연함
동적 결합: 더 유연함, 다형성 지원
구현 방식:
정적 결합: 일반 함수 호출, 함수 오버로딩
동적 결합: 가상 함수 사용
** 동적 결합은 객체 지향 프로그래밍의 핵심 개념인 다형성을 구현하는 데 중요한 역할을 합니다. 이를 통해 코드의 유연성과 확장성을 높일 수 있습니다.