[C++] 상속
REF : 윤성우의 열혈 C++
상속 기초
- C++의 상속은 단어 그대로 물려받는다는 의미가 강하지만, 단순히 기존 클래스를 재활용하는 목적의 기능이 아니다.
-
상속을 적절히 이용하면 프로그램의 유연성과 확장성을 확보할 수 있다.
- A라는 클래스가 B라는 클래스를 상속한다.
- A는 B클래스의 모든 멤버를 가진 상태로, 자신의 멤버도 가지게된다.
- 따라서
B클래스의 생성자
로 B클래스 멤버를 초기화해야한다.
class B
{
private:
int num;
public:
B(int num) : num(num)
{}
};
class A : public B
{
private:
int A_Num;
public:
A(int a, int b) : B(b), A_Num(a)
{}
};
유도(자식) 클래스의 객체생성과정
- 유도클래스의 객체 생성과정에서 기초 클래스의 생성자도 호출된다.
- 유도클래스의 생성자가 호출.
- 기초 클래스의 생성자 호출을 위해 이니셜라이저를 확인.
- 이때, 이니셜라이저에 기초클래스의 생성자를 명시하지 않으면 void 생성자가 호출된다.
- 있으면 해당 기초클래스 생성자 호출
- 유도클래스의 멤버 초기화 후, 유도클래스 생성자 실행 완료.
유도(자식) 클래스의 객체소멸과정
- 생성순서와 소멸순서는 반대이다.
- 소멸과정에서도 똑같이 유도클래스의 소멸자와 기초클래스의 소멸자 모두 호출된다.
- 유도클래스의 소멸자 호출, 실행.
- 기초 클래스의 소멸자 호출, 실행.
- 따라서, 각 클래스의 생성자에서 동적할당한 메모리는 각 클래스의 소멸자에서 해제해야한다.
protected선언과 세가지 형태의 상속
protect로 선언된 멤버
접근제어 지시자 중, public
, private
는 계속 써왔는데, protected
는 이제야 공부한다.
- 이 세 가지 접근제어 지시자가 허용하는 접근범위는
public > protected > private
- protected 멤버변수는
상속
시에 private와 차이를 나타낸다.- protected 멤버변수:
해당 클래스를 상속하는 유도 클래스에서 접근 가능
- private는 클래스 외부에선 접근이 불가능하나,
- protected는 유도클래스에게만 제한적으로 접근을 허용한다.
- protected 멤버변수:
상속의 3가지 형태
- protected 상속
- protected보다 접근 허용 범위가 넓은 멤버(public)는 protected로 변환해 상속해
- 클래스 외부에서 접근하지 못하도록 한다.
class A { private: int n1; protected: int n2; public: int n3; }; class B : protected A { int n1 <- A의 private이므로 B에서 접근불가. int n2 <- protected int n3 <- protected }; int main(void) { B b; b.n3 //<-에러 발생 }
- private 상속
- private 접근 허용 범위가 넓은 멤버(public, protected)는 private로 변환해 상속해
- 클래스 외부에서 접근하지 못하도록 한다.
class A { private: int n1; protected: int n2; public: int n3; }; class B : private A { int n1 <- A의 private이므로 B에서 접근불가. int n2 <- private int n3 <- private }; int main(void) { B b; b.n3 //<-에러 발생 }
- private 상속을 한 클래스를 다시 private상속하면, 모든 멤버변수가 접근불가 상태이므로 의미없는 상속이된다.
- public 상속
- private를 제외한 나머지를 그대로 상속한다.
상속조건
- 상속으로 클래스의 관계를 구성하기 위해서는 조건이 필요하다.
- IS_A 관계
- 유도클래스는 기초클래스가 지니는 모든 것을 지니고, 유도클래스만의 추가적인 특성이 더해진다.
- 예시
- 사람 - 학생 : 학생은 is a 사람.
- 사람 - 남자 : 남자 is a 사람.
- HAS_A 관계
- 소유관계도 상속으로 표현이 가능하다.
- 하지만 이런 소유 관계는
- 소유하지 않을때를 표현하는게 어렵고
- 다른 클래스도 소유하기 시작하면 표현하기 어려워진다.(다중상속을 할 수 있지만 어려움)
- 따라서, 소유관계는 상속으로 표현하기에 적합하지 않다.
다형성
객체 포인터의 참조관계
- 객체 포인터 변수 : 객체의 주소값을 저장하는 포인터 변수
-
선언시의 클래스의 객체 뿐만아니라, 이를 상속하는
유도 클래스의 객체
도 가리킬 수 있다.class A {}; class B : public A {}; A *ptr = new A(); A *ptr2 = new B();
-
즉, cpp는 부모클래스의 포인터 변수는 부모 클래스를 직접적으로 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.
-
함수 오버라이딩
-
함수 오버로딩
은 동일한 이름의 여러 함수가 다양한 매개변수로 선언되어 있을때,
매개변수에 따라서 함수를 호출하는 것을 의미한다. -
반면,
함수 오버라이딩
은 기초클래스의 함수를 유도클래스에서 다시 선언해기초클래스의 함수를 가리고
,유도클래스에서 함수를 덮어씌우는 것
을 의미한다. -
그리고 유도클래스에서 기초클래스의 이름공간을 명시하면 기초클래스에서 오버라이딩되어 가려진 함수를 호출할 수 있다.
가상함수
기초 클래스의 포인터로 객체 참조
class A
{
public:
void basefunc()
{}
};
class B : public A
{
public:
void derivedfunc()
{}
};
int main(void)
{
A *ptr1 = new A();
A *ptr2 = new B();
return 0;
}
- 앞서 이와같은 코드가 오류를 발생시키지 않는다는 것을 공부했다.
- 컴파일러는 포인터변수를 바라볼 때, 포인터의 자료형을 위주로 보지, 내부는 들여다보지 않는다.
- 하지만, 아래와같은 코드는 오류를 발생시킨다.
int main(void) { A *ptr1 = new A(); A *ptr2 = new B(); ptr1->derivedfunc(); //오류 return 0; }
- 실제로 가리키는 대상은 B이므로, B의 메서드가 실행될 수 있다고 생각하지만,
컴파일러는 포인터의 자료형으로 연산가능성을 확인
하므로, 컴파일 에러를 일으킨다.
유도클래스의 포인터에 기초클래스의 객체 대입 연산
도 컴파일 에러를 일으킨다.int main(void) { A *ptr1 = new B(); B *ptr2 = ptr1; //에러 }
- 컴파일러 입장에서는
ptr1
에 담긴 객체가기초클래스
인지유도클래스
인지 구분할 수 없으므로, 에러를 일으킨다.
- 컴파일러 입장에서는
- 마찬가지로,
컴파일러는 포인터의 자료형으로 연산가능성을 확인
하므로, 클래스에 정의된 멤버에만 접근이 가능하다.
가상함수의 필요성
- 실제로 가리키는 객체가 아닌, 포인터형으로만 호출할 함수를 결정하지말고,
실제 가리키는 객체의 자료형에따라 멤버를 호출하고 싶다면,
virtual
키워드를 사용해야한다.- 가상함수가 선언되고나면, 이 함수를 오버라이딩하는 함수도 가상함수가 된다.
포인터 변수가 실제로 가리키는 객체를 참조해 호출 대상을 결정
한다.
순수 가상함수와 추상 클래스
- 기초클래스의 의미만 가지고, 객체 생성 목적이 없는 클래스가 존재한다.
- 이때,
가상함수를 순수가상함수로 선언
하면 문제가 해결된다.class A { public: virtual void basefunc() const = 0; //순수 가상함수 };
- 이때,
순수 가상함수
란 함수의 몸체가 정의되지 않은 함수를 의미한다.- 명시적으로 몸체를 정의하지 않겠다는 것을 컴파일러에 알린다.
- 순수 가상함수를 가진 클래스는 객체를 생성하지 못한다.
- 이렇게 순수 가상함수를 1개 이상 가진 클래스를
추상 클래스
라고 부른다.- 객체 생성이 불가능한 클래스라고 한다.
다형성
- 객체 지향을 이해하는데에 있어서 매우 중요한 요소.
- 다형성 == 동질이상 == 모습은 같은데, 형태가 다르다 ==
문장은 같은데 결과가 다르다.
- 포인터의 자료형이 아닌, 실제 가리키는 객체의 자료형에 따라 다른 함수를 호출하는 것.
- 이것이 다형성의 예시
가상소멸자와 참조자의 참조가능성
- virtual 선언은 소멸자에게도 올 수 있다.
- 이를 가리켜 가리켜
가상 소멸자
라고 한다.
class A
{
private :
char *member1;
public :
A(void)
{
member1 = new char[20];
}
~A(void)
{
delete []member1;
}
};
class B : public A
{
private :
char *member2;
public:
B(void) : A()
{
member2 = new char[20];
}
~B(void)
{
delete []member2;
}
};
int main(void)
{
A *ptr1 = new B();
delete ptr1; //소멸자 호출
return 0;
}
- 위 코드의 문제점은, 소멸자를 호출하면 컴파일러는 포인터형만 보고
A의 소멸자만 호출
하는 것이다. - 결과적으로, B의 멤버변수에서 leak이 발생하게된다.
포인터 변수의 자료형에 상관없이 모든 소멸자가 호출되어야 한다.
- 기초 클래스의 소멸자에 virtual을 작성해주어야 유도클래스의 소멸자들도 가상소멸자가 되어 가장 밑의 유도클래스부터 기초클래스까지 순차적으로 소멸자가 호출된다.
참조자의 참조 가능성
- 컴파일러가 포인터를 선언하면, 해당 포인터는
선언된 자료형의 클래스 객체 뿐만아니라, 간접적으로 상속하는 모든 객체
를 가리킬 수 있다. - 이러한 특성은
참조자에게도 적용
된다.
class A
{
public:
virtual void simplefunc()
{
std::cout<<"A simplefunc"<<std::endl;
}
};
class B : public class A
{
public:
virtual void simplefunc()
{
std::cout<<"B simplefunc"<<std::endl;
}
};
int main(void)
{
B obj();
A &ref = obj;
B &ref = obj;
A.simplefunc(); //B simplefunc
B.simplefunc(); //B simplefunc
return 0;
}
- 참조자또한 이런 특성을 가지는데, 가상함수또한 같은 원리로 호출되어 실제 자료형으로 찾아가 멤버함수를 호출한다.
댓글남기기