객체지향_부록 #1: extends, implements, interface, 순수 가상함수, 추상 클래스, 클래스 객체, 포인터, 다형성, 상속과 생성자

기타(임시) · 2020. 7. 30. 17:45

참조: https://thrillfighter.tistory.com/172

 

하나씩 찾아가면서 모르는거 또 찾고 추가한거라

 

역순으로 보시는게 편하실 수도 있습니다....ㅎㅎㅎㅎ

 

1. extends와 implements

2. interface

3. 순수 가상함수와 추상 클래스

4. 클래스 객체와 포인터 그리고 다형성

5. 상속과 생성자

 

1. extends와 implements

 

상속: 다른 것으로부터 기능을 빌려다 쓰는 것

 

상속에는 아래와 같은 2가지 형태가 있다.

 

- extends는 일반 클래스와 abstract 클래스 상속에 사용된다.

+ 단 하나만 상속 받을 수 있따.

+ 한번 상속 받은 클래스를 계속해서 사용할 수 있다.

+ abstract 안에 추상 클래스로 정의된 것은 무조건 오버라이드 해야한다.

+ 상속 받은 클래스를 다시 상속 받으면 최상위 abstract 클래스의 메소드를 사용할 수 있다.

 

- implements는 interface 상속에 사용된다.
+ 다중 상속이 가능하다.

+ 인터페이스를 상속받기 때문에 인터페이스에 정의된 모든 메소드를 오버라이드 해야한다.

 

첫번째는 순수 상속으로 부모 클래스로부터 모든 권한과 기능을 가져온다.

 

두번째는 구현 상속으로 interface만 얻어 오는 것이다.

-> 상속을 받아올 때 상속 내용은 비어 있고, 비어 있는 내부는 내가 채워서 써야한다.

 

"사용법 추가 필요"

 

2. interface

 

자바에서는 일반적인 클래스와 인터페이스가 확실히 구분된다.

 

구현 방법도 extends와 implements로 구분된다.

 

자바에서는 언어적인 차원으로 다중 상속을 하지 못하게 했고,

 

인터페이스 구현을 통해 다중 상속 부분을 해결한다고 한다.

 

반면에 다중 상속이 자유로운 C++의 경우는 인터페이스라는 용어를 굳이 구분해서 설명하지 않는다.

 

- interface: 접속기, 접접 (두 시스템이 만나는 공유영역, 경계영역)

 

컴퓨터에 메모리 슬롯에 램 카드를 삽입할 때, 같은 규격의 램 카드라면 삼성이나 하이닉스나 제조회사 상관없이 삽입할 수 있다.

 

이때 DDR3를 삽입할 수 있는 메모리 소켓을 만들었다면, DDR3 규격에 맞는 메모리와 DDR3를 구분하여 받을 수 있는 interface가 필요하다.

 

즉, 서로 다른 제조회사에서 DDR3 규격에 맞는 메모리를 생산했을 때 DDR3 interface만 맞다면 모두 같은 소켓에 삽입할 수 있다.

 

메모리 크기, 용량, 프로토콜 등등 DDR3 메모리가 가지는 공통 사항을 interface에 만들고, 이를 각각의 DDR3 메모리가 상속받아 구현하도록 한다.

 

이렇게 하면 interface에 구현된 기능 목록들을 구현하지 않을 경우 에러가 나게 되어, 필요한 기능을 꼭 구현하도록 강제할 수 있다.

 

 

3. 순수 가상함수와 추상 클래스
https://thrillfighter.tistory.com/46

 

- 가상함수 virtual 키워드

 

클래스 타입의 포인터로 멤버함수를 호출할 때 virtual 키워드의 유무에 따라 동작이 달라지게 된다.

 

ex1)

class A{

public:

 void test() { 

  코드1

 }

};

 

class B: public A{

public:

 void test(){

  코드2

 }

};

 

A *a = new B();

 

a->test(); // 동작1 -> 코드1이 실행

 

동작2 -> A 클래스의 멤버함수인 test()를 virtual로 선언하면 코드2가 실행

 

virtual이 붙으면 동적바인딩이 된다.

 

동적바인딩: 클래스 타입 포인터가 가르키는 대상에 따라

-> A 클래스 포인터 타입인 a로 가르켰지만 가르키는 대상에 구현된 메소드가 실행된다.
정적바인딩: 클래스 타입 포인터에 따라

-> A 클래스 포인터 타입인 a로 가르켰기 때문에 가르키는 대상에 관계 없이 A 클래스에 구현된 메소드가 실행된다.

 

 

4. 클래스 객체와 포인터 그리고 다형성

 

상속의 개념을 간략화 하면 다음과 같다.

 

(클래스)    (멤버 변수, 함수)

  A  -->               a             : 탈것

  B  -->               a | b         : 비행기

  C -->                a | b | c     : 전투기

 

색깔 별로 특성들이 상속되는 의미라고 한다.

 

상속 되어지는 클래스의 멤버 변수와 함수가 상속 하는 클래스의 멤버 변수와 함수로 활용된다.

+ 상속 하는 클래스 자체의 멤버 변수와 함수를 가질 수 있다.

 

객체와 포인터에 대해 알아보자.

 

멤버 변수와 함수는 재정의 (overriding) 될 수도 있고, 그냥 상속만 되어질 수도 있다.

 

재정의나 상속이나 함수의 경우 같은 기능 or 비슷한 기능을 하는 함수의 이름이 바뀌는 경우는 드물다.

 

만약, B 클래스의 객체 b를 생성했다면 b가 가리키는 a는어떤 클래스의 a인가?

-> 당연히 B 클래스의 것이라고 한다.

 

다시, (a와 색칠한 a는 다른 것이라고 봅시다....)

 

ex2)

A *a = new C();

a->a;

 

위와 같은 선언은 가능한가?

-> 가능하다.

 

A 클래스 객체 포인터가 C 객체를 가리킨다.

 

하지만, C++을 공부하지 않은 사람이라면 의미가 명확하지 않아 이해하기 어렵다.

 

이렇게 한눈에 이해하기 어려운 방식을 사용하는 이유는 뭘까?

 

C++은 논리적으로 짜여진 OOP 개념이 적용되었기 때문이다.

 

기본 법칙은 다음과 같다.

 

1) 상위 클래스 타입의 포인터 객체는 하위 타입의 클래스 객체를 가리킬 수 있다.

-> 하위 타이의 객체를 상위 클래스 타입의 객체에 대입할 수 없다. (is a 관계 // "B는 A이다"가 성립한다.)

2) 하위 클래스 객체는 상위 타입의 멤버 함수를 호출할 수 있다.

-> 당연히 그 멤버 함수는 public이나 protected로 선언 되어야 한다.

 

ex2에서 호출되는 a 도 A 클래스의 것이다.

 

그럼 왜 굳이 C에성 A의 멤버를 호출할까?

 

하위 클래스들에서 상위 클래스의 멤버를 사용할 때 일괄적으로 다루기 편리하기 때문,,,

 

하지만 A 클래스의 함수만 호출된다고 했다.

 

이러한 문제를 해결하기 위해 해당 함수를 virtual로 선언하면 각 클래스에 맞는 함수가 호출된다.

 

 

5. 상속과 생성자

 

상속을 하면 자식 클래스의 생성자는 어떻게 될까?

 

부모의 생성자가 상속되지는 않는다. (생성자의 이름이 다르기 때문)

-> 기능적인 상속조차 안 된다.

 

대신 자식 클래스의 객체를 생성하게 되면 부모 클래스로 거슬러 올라가며 생성자가 차례로 호출된다.

 

인수를 받아서 초기화 하는 생성자의 경우는 member initializer를 사용하면 된다.

 

- 상속 (ingeritance)

 

상속은 추상화, 캡슐화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나이다.

 

상속은 사용자에게 높은 수준의 코드 재활용성을 제공하며, 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다.

 

'클래스의 상속 (class inheritance)'이란 기존에 정의되어 있는 클래스의 모든 멤버 변수와 멤버 함수를 상속 받아 새로운 클래스를 작성하는 것을 말한다.

 

base, parent, super  <-->  derived, child, sub

 

상속의 장점

1) 기존에 작성된 클래스를 재활용할 수 있다.

2) 공통적인 부분은 기초 클래스에 미리 작성하여, 파생 클래스에서 중복되는 부분을 제거할 수 있다.

 

'파생 클래스 (derived class)'란 기초 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 말한다.

 

C++에서 파생 클래스는 다음과 같은 문법을 통해 선언한다.

 

class 파생클래스이름 : 접근제어지시자 기초클래스이름[, 접근제어지시자 기초클래스이름, ...]

{

  //파생클래스 멤버리스트

}

 

접근 제어 지시자는 파생 클래스가 기초 클래스의 멤버를 사용할 수 있는 권한을 설정한다.

-> 접근 제어 지시자를 생략했을 때, 파생 클래스의 접근 제어 권한은 private로 기본 설정한다.

 

쉼표(,)를 사용하여 상속받을 기초 클래스를 여러 개 명시할 수 있다.

 

이때 파생 클래스가 상속받는 기초 클래스 하나이면 단일 상속(single ingeritance), 두개 이상이면 다중 상속(multiple inheritance)라고 한다.

 

파생 클래스는 다음과 같은 특징을 갖고 있다.

 

1) 반드시 자신만의 생성자를 작성해야 한다.

2) 기초 클래스의 접근할 수 있는 모든 멤버 변수가 저장된다.

3) 기초 클래스의 접근할 수 있는 모든 멤버 함수를 사용할 수 있다.

4) 필요한 만크 멤버를 추가할 수 있다.

 

ex)

다음과 같은 Person 클래스가 미리 작성되어 있다고 가정하자.

 

class Person

{

private:

    string name_;

    int age_;

public:

    Person(const string& name, int age); // 기초 클래스 생성자의 선언

    void ShowPersonInfo();

};

...

Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의

{

    name_ = name;

    age_ = age;

}

 

다음은 기존에 작성된 Person 클래스를 상속받는 Student 클래스를 작성하는 예제이다.

 

class Student : public Person {

private:

    int student_id_;

public:

    Student(int sid, const string& name, int age); // 파생 클래스 생성자의 선언

};

...

Student::Student(int sid, const string& name, int age) : Person(name, age) // 파생 클래스 생성자의 선언

{

    student_id_ = sid;

}  

 

예제에서 보듯이 파생 클래스의 생성자는 기초 클래스의 생성자를 사용하고 있다.

-> 파생 클래스의 생성자가 기초 클래스의 priveate 멤버에 접근할 수 없기 때문에!

 

따라서 기초 클래스의 생성자를 사용해야만 기초 클래스의 private 멤버를 사용할 수 있다. (name_과 age_를 말하는건가?)

 

이때 기초 클래스의 생성자를 명시하지 않으면, 기초 클래스의 디폴트 생성자를 사용하게 된다.

 

ex)

#include <iostream>
using namespace std;

class Person
{
private:
     string name_;
     int age_;
public:
     Person(const string& name, int age); // 기초 클래스 생성자의 선언 
     void ShowPersonInfo();
};

class Student : public Person
{
private:
     int student_id_;
public:
     Student(int sid, const string& name, int age); // 파생 클래스 생성자의 선언 
};

int main(void)
{
     Student hong(123456789, "길동", 29);
     hong.ShowPersonInfo();

     return 0;
}

Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의 
{
     name_ = name;
     age_ = age;
}

void Person::ShowPersonInfo()
{
     cout << name_ << "의 나이는 " << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string& name, int age) : Person(name, age) // 파생 클래스 생성자의 정의 
{
     student_id_ = sid;
}

 

기초 클래스의 생성자를 함께 정의하지 않으면 기초 생성자의 private 멤버 변수인 student_id를 사용할 수 없다.

 

 

- 파생 클래스의 객체 생성 순서

 

다음 그림은 파생 클래스의 생성자가 호출되면 수행되는 동작을 설명한다.

 

C++에서 파생 클래스의 객체가 생성되는 순서는 다음과 같습니다.

 

1. 파생 클래스의 객체를 생성하면, 프로그램은 제일 먼저 기초 클래스의 생성자를 호출합니다.

    이때 기초 클래스 생성자는 상속받은 멤버 변수의 초기화를 진행합니다.

2. 그리고서 파생 클래스의 생성자가 호출됩니다.

3. 반대로 파생 클래스의 수명이 다하면, 먼저 파생 클래스의 소멸자가 호출되고, 그 후에 기초 클래스의 소멸자가 호출됩니다.

 

 

 

 

 

 

 

반응형