C++

[강의] 11월 12일 수업정리

k-codestudy 2024. 11. 13. 03:34

추상클래스 복습과 포함, 상속에 대한 문제 해결방법에 대한 수업을 들었다.

 

 

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