추상클래스 복습과 포함, 상속에 대한 문제 해결방법에 대한 수업을 들었다.
1. 추상클래스 복습
1.1 추상 클래스
- 추상 클래스는 직접 인스턴스화할 수 없고, 주로 다른 클래스가 상속받아 구현을 확장하도록 사용한다.
- C++에서는 추상 클래스가 하나 이상의 순수 가상 함수를 포함할 때 추상 클래스가 된다.
- 가상 소멸자를 선언해야 메모리 관리가 안전하다. 그렇지 않으면 다형성을 사용할 때 자식 클래스 소멸자가 호출되지 않아 메모리 누수가 발생한.
class C_PARENT abstract
{
public:
C_PARENT() = default;
virtual ~C_PARENT() = default;
C_PARENT(const C_PARENT&) = delete;
C_PARENT& operator=(const C_PARENT&) = delete;
};
주요 특징
- 상속 전용이므로 abstract 키워드 사용 (컴파일러에 의해 강제되진 않는다)
- 객체 관리를 위해서 추상 클래스를 사용하는 경우가 많으며, 직접 인스턴스를 생성하지 않는다.
1.2 인터페이스
- 인터페이스는 구현 없이 함수 선언만을 포함하여 순수 가상 함수로 구성된 클래스이다.
- 인터페이스는 순수하게 기능을 정의하고, 이를 여러 클래스가 다르게 구현하도록 강제하는 데 목적이 있다.
- C++에서는 __interface 구문을 사용해 표준 인터페이스처럼 작성할 수 있지만, 이 구문은 비표준 구문이니 알아두자
class C_PARENT_INTERFACE {
public:
virtual void test() = 0; // 순수 가상 함수
};
__interface I_PARENT {
void test();
void run();
};
1.3 상속 관계 구성과 사용법 예제
#pragma once
#include <stdio.h>
//C언어 방식
class C_PARENT_INTERFACE
{
public:
virtual void test() abstract;
};
// C++ 방식
__interface I_PARENT // 관리
{
void test();
void run();
};
class C_PARENT abstract : public I_PARENT // 상속
{
public:
C_PARENT() = default;
virtual ~C_PARENT() = default;
C_PARENT(const C_PARENT&) = delete;
C_PARENT& operator=(const C_PARENT&) = delete;
};
[header.cpp]
#pragma once
#include "parent.h"
class C_CHILD : public C_PARENT
{
public:
virtual void test() override;
virtual void run() override;
};
[main.cpp]
#include <iostream>
#include"child.h"
int main()
{
C_PARENT* p{};
p = new C_CHILD{};
p->test();
p->run();
}
1.4 추상 클래스와 인터페이스의 사용 목적과 주의사항
- 추상 클래스는 클래스의 관리 목적이나 특정 기능을 상속받아 확장하기 위해 정의된다.
- 인터페이는 순수 가상 함수로만 이우어진 클래스이며, 특정 기능의 집합을 명시하여 이를 자식 클래스가 구현하도록 강제한다.
- 상속 시 가상 소멸자를 포함하여 메모리 누수를 막아야 한다.
- 다중 상속이 가능하지만, 복잡성을 피하기 위해 다중 상속은 사용하지 않도록 하자
1.5 정리된 사용 지침
- virtual 키워드를 사용하여 가삼 함수로 정의하고, 오버라이딩 시 override를 붙이는 것이 명시적으로 보기 좋다.
- 순수 가상 함수는 인터페이스의 기능을 저의 하는 데 사용하고, 이를 통해 자식 클래스는 반드시 구현해야 한다.
2. 포함에 대한 문제 해결 방법
[header.h]
// data.h
#pragma once
#include <stdio.h>
class C_DATA
{
private:
int m_nData;
public:
C_DATA(int nData);
void print();
};
// composite.h
#pragma once
#include "data.h"
class C_COMPOSITE // C_DATA 포함관계
{
private:
C_DATA m_cData;
public:
C_COMPOSITE(int nData);
C_COMPOSITE();
C_DATA* getData();
};
[header.cpp]
// data.cpp
#include "data.h"
C_DATA::C_DATA(int nData) :
m_nData{}
{
m_nData = nData;
}
void C_DATA::print()
{
printf("%d\n", m_nData);
}
// composite.cpp
#include "composite.h"
C_COMPOSITE::C_COMPOSITE(int nData) :
m_cData(nData)
{
}
C_DATA* C_COMPOSITE::getData()
{
return &m_cData;
}
[main.cpp]
#include <iostream>
#include "composite.h"
int main()
{
C_COMPOSITE cComp{};
C_COMPOSITE cComp(5);
cComp.getData()->print();
}
2.1 문제 원인
- C_COMPOSITE 클래스는 C_DATA 객체 (m_cData)를 멤버로 포함하고 있는 상태이다.
- C_COMPOSITE 클래스의 기본 생성자에서 C_DATA 객체를 초기화하지 않기 때문에 기본 생성자가 정의되지 않는 경우 컴파일러에서 에러가 발생한다
- C_DATA 클래스는 매개변수를 받는 생성자만 정의되어 있기에 기본 생성자가 자동으로 생성되지 않는다. 그렇기에 C_DATA클래스는 인스턴스를 생성할 때 반드시 매개변수를 제공해야 하는 것이다.
2.2 해결 방법 : 포인터와 동적 할당 사용
C_DATA 객체를 포인터로 선언하고 동적 할당을 통해 객체를 초기화하는 방법이 있다.
이 방법은 C_COMPOSITE 생성 시 C_DATA를 동적으로 생성하여 메모리 관리가 가능하도록 한다.
// composite.h
#pragma once
#include "data.h"
class C_COMPOSITE // C_DATA 포함관계
{
private:
C_DATA *m_pData;
public:
C_COMPOSITE() = default;
void init(int nData);
void release();
C_DATA* getData();
};
// composite.cpp
#include "composite.h"
void C_COMPOSITE::init(int nData)
{
m_pData = new C_DATA(nData);
}
void C_COMPOSITE::release()
{
delete m_pData;
m_pData = nullptr;
}
C_DATA* C_COMPOSITE::getData()
{
return m_pData;
}
C_COMPOSITE의 기본 생성자와 매개변수를 받는 생성자 모두 m_cData를 정상적으로 초기화하며, 동적 할당된 메모리를 소멸자에서 해제해 준다.
3. 상속에 대한 문제 해결 방법
3.1 문제 원인
- C_INHERITANCE 클래스는 C_DATA 클래스를 상속받고 있으며, C_DATA의 생성자는 매개변수를 받도록 정의되어 있다.
- C++에서는 상속 시 자식 클래스의 생성자에서 부모 클래스의 생성자를 명시적으로 호출해야 부모 클래스의 멤버들이 올바르게 호출이 된다.
- C_INHERITANCE의 생성자를 호출하면 C_DATA 생성자가 먼저 호출되어야 하는데 C_DATA의 기본 생성자가 없으므로 초기화를 해줘야 한다.
3.2 해결 방법 : 생성자 초기화 리스트 사용
- 상속된 부모 클래스의 생성자는 자식 클래스의 생성자의 초기화 리스트에서 호출이 가능하다.
- 이 방식은 상속 관계에서 부모 클래스의 초기화가 명확하게 이루어지도록 보장해야 한다.
// inheritance.h
#pragma once
#include "data.h"
class C_INHERITANCE : public C_DATA
{
public:
C_INHERITANCE(int nData);
};
// inheritance.cpp
#include "inheritance.h"
C_INHERITANCE::C_INHERITANCE(int nData) :
C_DATA(nData) // 이거는 방법 없음 이것뿐이며, 상속이라 분리가 안됨
{
}
// main.cpp
#include <iostream>
#include "inheritance.h"
int main()
{
C_INHERITANCE cInheritance(5);
cInheritance.print();
}
이와 같이 초기화 리스트를 사용하여 C_DATA의 생성자를 호출하면 C_INHERITANCE객체 생성 시 C_DATA의 멤버가 올바르게 초기화된다.
3.3 개념 정리
- 상속 관계에서 부모 클래스의 생성자는 자식 클래스의 생성자 초기화 리스트에서 호출하여 부모 클래스 멤버 변수를 초기화한다.
- 포함 관계와 달리, 상속 관계에서는 자식 생성자가 호출될 때 부모 클래스가 먼저 초기화되어야 하므로 부모 생성자를 명시적으로 호출해야 한다.
- 포함 관계의 경우는 멤버 변수로 구성되어 있으므로 생성자 없이 기본 값으로 초기화가 가능하지만 상속은 부모 클래스의 생성자 호출이 필수이므로 초기화 리스트를 사용해야 한다.
'C++' 카테고리의 다른 글
[강의] 11월 14일 수업정리 (8) | 2024.11.15 |
---|---|
[강의] 11월 13일 수업정리 (2) | 2024.11.14 |
[강의] 11월 8일 수업정리 (1) | 2024.11.10 |
[강의] 11월 7일 수업정리 (1) | 2024.11.08 |
[강의] 11월 6일 수업정리 (0) | 2024.11.07 |