[C++ Primer Plus] Chapter 13. 클래스의 상속
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
서론 #
-
C의 전통적인 함수 라이브러리는
- 그 라이브러리의 소스 코드를 제공하지 않는다면, 사용자의 특정 요구에 맞게 그 함수를 확장하거나 수정할 수 없다.
-
C++의 클래스 라이브러리는
- 클래스 상속을 사용한다. 이것은 기초 클래스(base class)로 부터 멤버 변수, 함수들을 상속받아서 새로운 파생 클래스(derived class)를 만드는 것이다.
- 기초 클래스의 소스 코드에 접근할 필요 없이, 상속으로 새로운 기능이나 데이터를 추가하거나, 함수의 동작 방식을 변경할 수 있다.
기초 클래스를 상속 받는 파생 클래스 #
-
파생 클래스는 기초 클래스의 구현들을 상속받는다. (데이터 멤버)
-
파생 클래스는 기초 클래스의 인터페이스를 상속받는다. (메서드)
-
파생 클래스는 자기 자신의 생성자를 필요로 한다.
-
파생 클래스는 부가적인 데이터 멤버들과 멤버 함수들을 필요한 만큼 추가할 수 있다.
class BaseClass
{
private:
int baseClassNumber;
public:
BaseClass()
{
baseClassNumber = 0;
};
BaseClass(int n)
{
baseClassNumber = n;
}
int GetBaseClassNumber() const
{
return baseClassNumber;
}
};
class DerivedClass : public BaseClass
{
private:
int derivedClassNumber;
public:
// 기초 클래스의 생성자를 멤버 초기자 리스트 문법을 사용해 먼저 호출한다.
DerivedClass(int base, int derived) : BaseClass(base)
{
derivedClassNumber = derived;
}
int Sum()
{
// 기초 클래스의 private 멤버는 public 멤버로 접근할 수 있다.
return derivedClassNumber + GetBaseClassNumber();
}
};
- 생성자
- 파생 클래스의 생성자는 기초 클래스의 생성자를 사용해야 한다.
- 프로그램은 기초 클래스의 객체를 생성 한 후, 파생 클래스의 객체를 생성한다.
- 파생 클래스 생성자의 몸체 안으로 들어가기 전에 기초 클래스 객체를 생성하기 위해 멤버 초기자 리스트 문법을 사용할 수 있다.
DerivedClass::DerivedClass(int base, int derived) : BaseClass(base)
{
derivedClassNumber = derived;
}
- 멤버 초기자 리스트를 생략하면?
- 디폴트 기초 클래스 생성자를 사용한다.
DerivedClass::DerivedClass(int base, int derived) // 이것은 : BaseClass()와 같다.
{
derivedClassNumber = derived;
}
- 원한다면, 파생 클래스 멤버들에도 멤버 초기자 리스트 문법을 사용할 수 있다.
DerivedClass::DerivedClass(int base, int derived)
: BaseClass(base), derivedClassNumber(derived);
{
}
- 파괴자는 반대 순서로 일어난다.
- 파생 클래스 파괴자의 먼저 호출되고, 기초 클래스의 파괴자라 호출된다.
- 파생 클래스는
private
이 아니면 기초 클래스의 메서드들을 사용할 수 있다.
class DerivedClass : public BaseClass
{
private:
int derivedClassNumber;
public:
// ...
int Sum()
{
// 기초 클래스의 private 멤버는 public 멤버로 접근할 수 있다.
return derivedClassNumber + GetBaseClassNumber();
}
};
- 기초 클래스 포인터, 참조는 명시적 데이터형 변환 없이도 파생 클래스의 객체를 지시, 참조할 수 있다.
- 반대는 안 된다.
DerivedClass derivedClass(5, 6);
// 기초 클래스로 파생 클래스를 지시, 참조할 수 있다.
BaseClass * basePtr = &derivedClass;
BaseClass & baseRef = derivedClass;
// 기초 클래스의 메서드를 사용할 수 있다.
basePtr->GetBaseClassNumber();
baseRef.GetBaseClassNumber();
// 파생 클래스의 메서드는 사용할 수 없다.
basePtr->Sum(); // (X)
baseRef.Sum(); // (X)
BaseClass baseClass(5);
// 반대로
// 파생 클래스로 기초 클래스를 지시, 참조할 수 없다.
DerivedClass * derivedPtr = &baseClass; // (X)
DerivedClass & derivedRef = baseClass; // (X)
- 따라서 기초 클래스 포인터, 참조를 매개변수를 사용하는 함수는 파생 클래스 객체에도 사용할 수 있다.
void Show(BaseClass & baseRef)
{
cout << baseRef.GetBaseClassNumber() << endl;
}
int main()
{
BaseClass baseClass(5);
DerivedClass derivedClass(5, 6);
Show(baseClass);
Show(derivedClass); // (O)
}
- 파생 클래스의 객체로 기초 클래스 객체를 초기화하는 것도 간접적으로나마 허용한다.
- 초기화하는 생성자의 원형은
BaseClass(const DerivedClass &);
일 것이다. - 이러한 생성자를 만들지 않았으므로 암시적인 복사 생성자가 그 역할을 대신한다.
BaseClass(const BaseClass &);
- 기초 클래스의 참조는 파생 클래스를 참조할 수 있으므로, 파생 클래스의 멤버들을 복사한다.
DerivedClass derivedClass(5, 6);
BaseClass baseClass(derivedClass); // (O)
// 이러한 복사 생성자가 암시적으로 호출되었을 것이다.
BaseClass::BaseClass(const BaseClass& b)
{
baseClassNumber = b.baseClassNumber;
}
- 대입도 마찬가지이다.
BaseClass & operator=(const BaseClass &)
과 같은 암시적인 오버로딩 대입 연산자를 대입에 사용한다.
DerivedClass derivedClass(5, 6);
BaseClass baseClass;
baseClass = derivedClass; // (O)
is-a 관계 #
- is-a관계는 파생 클래스와 기초 클래스의 특별한 관계를 나타낸다.
- 바나나와 과일
- 바나나는 과일이다. 바나나 is a 과일.
- is-a 관계는 파생 클래스가 기초 클래스이기도 하다는 것을 뜻한다.
- 바나나(파생 클래스)는 과일(기초 클래스)이다.
- 점심과 과일
- 점심은 과일를 가진다. 점심 has a 과일. 점심이 과일인 것은 아니다.
- 이것은 has-a 관계이다.
- 점심 클래스는 데이터 멤버로 과일 객체를 가진다.
public 다형 상속 #
-
호출하는 객체가 어떤 것인가에 따라서 메서드의 행동이 달라질 수 있다.
-
메서드가 여러 가지 다른 행동을 할 수 있기 때문에 그러한 행동을 다형이라고 부른다.
-
방법
- 기초 클래스 메서드를 파생 클래스에서 다시 정의한다.
- 그리고
virtual
키워드를 사용해서 가상 메서드로 만든다.
-
virtual
키워드virtual
키워드를 사용한 메서드를 가상 메서드라고 한다.virtual
키워드를 사용하면 각각의 클래스에서 서로 다른 행동을 하는 메서드을 만들 수 있다.- 만약 같은 행동을 한다면 기초 클래스에 단 한 번만 선언된다.
virtual
키워드를 사용하지 않을 경우, 프로그램은 참조형이나 포인터형에 기초하여 메서드를 선택한다.virtual
키워드를 사용할 경우, 프로그램은 참조형이나 포인터형이 지시하는 객체가 무엇인가에 기초하여 메서드를 선택한다.
class Animal
{
public:
virtual void WhatIsThis()
{
cout << "이것은 Animal 객체입니다." << endl;
}
};
class Dog : public Animal
{
public:
virtual void WhatIsThis()
{
cout << "이것은 Dog 객체입니다." << endl;
}
};
int main()
{
Animal animal;
Dog dog;
Animal * aniPtr = &animal;
Animal * dogPtr = &dog;
aniPtr->WhatIsThis(); // Animal의 WhatIsThis() 호출
dogPtr->WhatIsThis(); // Dog의 WhatIsThis() 호출
}
- 출력 결과
- 포인터가 실제 어떤 객체를 가리지는 지 판단해서 메서드를 선택한다.
이것은 Animal 객체입니다.
이것은 Dog 객체입니다.
- 만약
virtual
키워드를 사용하지 않으면 출력 결과는 다음과 같다.- 포인터가 실제 어떤 객체를 가리키는 지에 상관없이 포인터형으로만 메서드를 선택한다.
이것은 Animal 객체입니다.
이것은 Animal 객체입니다.
virtual
파생 클래스 메서드에서 기초 클래스의 메서드를 부르는 방법- 사용 범위 결정 연산자를 사용한다.
class Dog : public Animal
{
public:
virtual void WhatIsThis()
{
Animal::WhatIsThis(); // Animal 기초 클래스의 메서드를 호출한다.
WhatIsThis(); // 이것은 자기 자신을 부르는 재귀호출이다.
cout << "이것은 Dog 객체입니다." << endl;
}
};
- 가상 파괴자의 필요성
- 파괴자가 가상이 아니면, 포인터형에 대한 파괴자만 호출될 것이다.
class Animal
{
public:
~Animal()
{
cout << "Animal 파괴자 호출됨" << endl;
};
};
class Dog : public Animal
{
public:
~Dog()
{
cout << "Dog 파괴자 호출됨" << endl;
}
};
// 예시 1
Animal * ptr = new Animal;
delete ptr; // Animal 파괴자 호출
// 예시 2
Animal * ptr = new Dog;
delete ptr; // Animal 파괴자 호출
// 예시 3
Dog * ptr = new Dog;
delete ptr; // Dog 파괴자 호출 후, Animal 파괴자 호출
Dog
객체를 지시하는 경우에도Animal
객체의 파괴자만 호출한다. (예시 2번)- 하지만, 파괴자를 가상으로 만들면 실제로 지시하는
Dog
객체의 파괴자를 호출한 후Animal
객체의 파괴자를 호출한다.
class Animal
{
public:
virtual ~Animal() // 가상 소멸자
{
cout << "Animal 파괴자 호출됨" << endl;
};
};
class Dog : public Animal
{
public:
~Dog()
{
cout << "Dog 파괴자 호출됨" << endl;
}
};
// 예시 2
Animal * ptr = new Dog;
delete ptr; // Dog 파괴자 호출 후, Animal 파괴자 호출
일반적으로, 파괴자가 필요 없는 기초 클래스라 하더라도 가상 파괴자를 제공해야 한다.
정적 바인딩과 동적 바인딩 #
-
바인딩(binding; 결합)
- 함수가 호출되었을 때 어떤 블록을 실행할지 결합하는 것을 바인딩이라고 한다.
-
정적 바인딩(static binding) = 초기 바인딩(early binding)
- 컴파일 동안 일어나는 바인딩.
-
동적 바인딩(dynamic binding) = 말기 바인딩(lately binding)
- 프로그램 실행 시에 올바른 가상 메서드가 선택되도록 하는 바인딩.
- C++은 일반적으로 한 데이터형의 주소를 다른 데이터형 포인터에 대입하는 것을 허용하지 않는다.
- 참조하는 것도 허용하지 않는다.
double d = 1.5;
int * iPtr = &d; // (X)
int & iRef = d; // (X)
- 그러나 앞서 보았듯이, 상속하는 클래스는 허용된다.
DerivedClass d;
BaseClass * bPtr = &d; // (O) 업캐스팅
BaseClass & bRef = d; // (O)
- 업캐스팅(upcasting)
- 파생 클래스의 참조/포인터를 기초 클래스의 참조/포인터로 변환하는 것을 업캐스팅이라고 한다.
- public 상속에서는 명시적인 데이터형 변환이 없어도 업캐스팅이 허용된다.
- 업캐스팅은 전이된다.
- 예를 들어,
DerivedClass
를 상속받는DerivedDerivedClass
가 있다면, 이 클래스 또한BaseClass
에 의해 참조될 수 있다.
- 예를 들어,
- 다운캐스팅(downcasting)
- 반대로 기초 클래스의 참조/포인터를 파생 클래스의 참조/포인터로 변환하는 것을 다운캐스팅이라고 한다.
- 다운캐스팅은 명시적인 데이터형 변환 없이는 허용되지 않는다.
- 왜냐하면
is-a
관계는 일반적으로 대칭적이지 않기 때문이다. 예를 들어, 사과 is a 과일이지만, 과일 is a 사과는 아니다.
- 왜냐하면
- 암시적 업캐스팅 때문에 동적 바인딩이 필요한 것이다.
virtual
멤버 함수는 이러한 필요성 때문에 만들어 졌다.- 컴파일러는
virtual
이 아닌 멤버 함수는 정적 바인딩을 사용한다. - 반면
virtual
인 멤버 함수는 프로그램이 실행되는 동안에 결정되는 객체형에 따라서 맞는 함수를 바인딩한다. 즉, 동적으로 바인딩한다.
- 컴파일러는
- 왜 두 종류의 바인딩이 필요한가?
- 효율성
- 프로그램이 무언가를 실행 시간에 결정하려면, 기초 클래스 참조/포인터가 지시하는 객체가 무엇인지 추적하는 방법이 필요하다.
- 이것은 가외의 처리 부담(다음 차례에 설명되어 있다)이 생긴다.
- 따라서 동적 바인딩이 필요 없는 경우, 정적 바인딩이 좀 더 효율적이며,
- C++에서는 정적 바인딩이 디폴트로 되어있다.
- 개념 모델
- 파생 클래스에서 다시 정의되는 것을 원하지 않는 멤버 함수는 가상이 아닌 함수로 만듦으로써,
- 다시 정의되면 안 된다는 의도를 드러낸다.
- 효율성
- 가상 함수는 어떻게 동작하는가?
- 일반적으로 컴파일러는 각각의 객체에 숨겨진 멤버를 하나씩 추가한다.
- 그 숨겨진 멤버는 어떤 배열을 지시하는 포인터이다.
- 배열에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어 있다.
- 이 배열을 가상 함수 테이블(virtual function table; vtbl) 이라고 한다.
- 파생 클래스가 가상 함수를 다시 정의하지 않으면, vtbl은 그 함수의 오리지널 버전의 주소를 저장한다.
- 반면, 파생 클래스가 새로운 함수를 정의하면, 그 주소가 vtbl에 저장된다.
- 따라서 가상 함수를 호출하면, 프로그램은 객체에 있는 vtbl에 접근한다.
- 사용하는 함수가 첫 번째 가상 함수라면, 프로그램은 vtbl의 첫 번째 주소를 사용한다.
- 이렇듯 가상 함수를 사용하면 메모리, 실행 속도 면에서 약간의 부담이 따른다.
- 각 객체의 크기가 커진다. (vtbl을 가리키는 주소를 저장하므로)
- 컴파일러는 테이블을 만들어야 한다.
- 함수 호출 시 테이블에 접근하는 가외의 단계가 더 필요하다.
- 일반적으로 컴파일러는 각각의 객체에 숨겨진 멤버를 하나씩 추가한다.
- 생성자는 가상으로 선언할 수 없다.
- 파생 클래스의 객체 생성
- (1) 파생 클래스의 생성자를 호출한다.
- (2) 파생 클래스의 생성자가 기초 클래스의 생성자를 호출한다.
- 이 시퀀스는 상속 매커니즘과 다르다. 그래서, 파생 클래스는 기초 클래스 생성자를 상속하지 않는다.
- 따라서 가상으로 만들 이유가 전혀 없다.
- 파생 클래스의 객체 생성
- 파생 클래스가 가상 함수를 다시 정의 하지 않으면?
- 기초 클래스의 버전을 사용한다.
- 만약, 파생 클래스가 길게 이어진 파생 사슬의 일부라면?
- 가장 최근에 정의된 버전을 사용한다.
- 다만, 기초 클래스의 버전이 은닉되어 있는 경우는 예외이다.
- 가상 함수를 다시 정의하면, 메서드가 은닉된다.
class BaseClass
{
public:
virtual void Show(int n) const; // 매개변수 존재
};
class DerivedClass : public BaseClass
{
public:
virtual void Show() const; // 매개변수 없음.
};
int main()
{
DerivedClass derived;
derived.Show(); // (O)
derived.Show(1); // (X) 매개변수 없는 버전에 의해 가려졌다.
}
-
매개변수가 없는 새로운 정의는 매개변수가 있는 기초 클래스 버전을 가린다.
- 다시 말하면, 상속된 메서드를 다시 정의하는 것은 오버로딩과는 다르다.
- 매개변수 시그내처와는 상관 없이 같은 이름을 가진 모든 기초 클래스 메서드들을 가린다.
-
따라서 이것 때문에 두 가지 규칙이 성립된다.
- (1) 재정의 시 오리지널 원형과 정확히 일치시켜야 한다.
- 예외: 리턴형의 공변(covariance)
- 리턴형이 기초 클래스에 대한 참조/포인터인 경우 파생 클래스에 대한 참조/포인터로 대체 될 수 있다.
- 예외: 리턴형의 공변(covariance)
- (2) 기초 클래스 선언이 오버로딩되어 있다면, 파생 클래스에서 모두 재정의해야 한다.
- 만약 한 가지 버전만 제공하면, 그 한 가지가 나머지를 모두 가린다.
- 변경이 필요 없다면 다음과 같이 재정의 할 수 있겠다.
- (1) 재정의 시 오리지널 원형과 정확히 일치시켜야 한다.
class BaseClass
{
public:
virtual BaseClass & ReturnTest();
virtual void Show() const;
virtual void Show(int n) const;
};
class DerivedClass : public BaseClass
{
public:
virtual DerivedClass & ReturnTest(); // (1번 설명 예시) 동일한 함수 시그내처이다.
virtual void Show() const;
virtual void Show(int n) const
{
BaseClass::Show(n); // (2번 설명 예시) 필요 없다면, 이렇게 하면 된다.
}
};
접근 제어: protected #
-
클래스 접근 제어를 위한 키워드 세 가지
private
protected
public
-
protected
- 바깥 세계에서 바라보면
private
처럼 행동하지만, - 파생 클래스 입장에서는
public
멤버와 같이 행동한다.
- 바깥 세계에서 바라보면
가능하다면 protected 접근 제어보다 private 접근 제어를 사용해서 메서드를 통해 안전하게 기초 클래스의 데이터에 접근하도록 해야한다.
추상화 기초 클래스 #
- 원과 타원 클래스를 만들고자 한다.
- 원을 타원의 한 종류로 보아 타원을 상속받아서 원 클래스를 만들 수 있겠다.
- 하지만 타원에 속한 것들 중 대다수는(반장경 변수, 반당경 변수, 회전시키기 함수 등)은 원에는 필요 없는 것들이다.
- 그냥 따로 각각 클래스를 만드는 게 낫겠다.
- 아니면 공통적인 것들만 뽑아서 기초 클래스를 만들어보던지?
- 추상화 기초 클래스(abstract basic class; ABC)
- 두 클래스의 공통적인 부분만 따로 뽑아서 만든 기초 클래스이다.
class BaseEllipse
{
private:
double x, y;
public:
BaseEllipse(double x0 = 0, double y0 = 0) : x(x0), y(y0) {}
virtual ~BaseEllipse() {}
void Move(int nx, int ny)
{
x = nx;
y = ny;
}
virtual double Area() const = 0; // 순수 가상 함수
};
Area()
는 원과 타원 각각 다르게 구현해야 하므로 순수 가상 함수(pure virual function) 로 만들었다.- 순수 가상 함수는 함수 선언 뒤에
=0
을 가진다. - 순수 가상 함수는 반드시 정의를 할 필요는 없다. 해도 되고, 안 해도 된다.
- 순수 가상 함수가 들어 있는 클래스는 자신의 객체를 생성할 수 없다.
- 왜냐하면 순수 가상 함수는 기초 클래스의 역할을 하기 위해서만 존재하기 때문이다.
- 어떤 클래스가 진짜 ABC가 되려면 순수 가상 함수가 적어도 하나 이상 있어야 한다.
- 순수 가상 함수는 함수 선언 뒤에
상속과 동적 메모리 대입 #
1. 파생 클래스가 new
를 사용하지 않는 경우.
-
기초 클래스는 동적 메모리 대입을 사용한다. 그래서 파괴자, 복사 생성자, 대입 연산자를 가지고 있다.
-
하지만 파생 클래스는 동적 메모리 대입을 사용하지 않는다.
-
이 때, 파생 클래스에도 명시적 파괴자, 복사 생성자, 대입 연산자를 정의해야 할까?
- 정답은 아니오이다.
-
파괴자
- 파생 클래스의 디폴트 파괴자는 기초 클래스의 파괴자를 항상 호출한다.
- 따라서 디폴트 파괴자로 충분하다.
-
복사 생성자
- 파생 클래스의 디폴트 복사 생성자는 가지고 있는 기초 클래스의 성분을 복사하기 위해, 명시적인 기초 클래스의 복사 생성자를 사용한다.
- 따라서 파생 클래스의 디폴트 복사 생성자로도 괜찮다.
-
대입 연산자
- 마찬가지로, 기초 클래스의 대입 연산자를 사용하므로 파생 클래스는 명시적으로 대입 연산자를 정의하지 않아도 된다.
2. 파생 클래스가 new
를 사용하는 경우
-
당연히 명시적 파괴자, 복사 생성자, 대입 연산자를 정의해야 한다.
-
파괴자
- 파생 클래스에서 사용한 메모리를 해제하는 명시적 파괴자를 만든다.
-
복사 생성자
- 파생 클래스의 복사 생성자는 자신의 데이터에만 접근할 수 있으므로 기초 클래스의 복사 생성자를 호출한다.
DerivedClass::DerivedClass(const DerivedClass & d)
: BaseClass(d) // 기초 클래스의 복사 생성자를 호출한다.
{
// new로 메모리 할당 후 복사
}
- 대입 연산자
- 마찬가지로, 기초 클래스의 대입 연산자를 호출해준다.
DerivedClass & DerivedClass::operator=(const DerivedClass & d)
{
// ...
// 기초 클래스의 대입 연산자를 호출한다.
// *this = d; 와 같은 효과이다.
BaseClass::operator=(d);
//...
}
- 기초 클래스의
friend
에 접근하는 법- 프렌드는 멤버 함수가 아니기 때문에 사용 범위 결정 연산자로 접근할 수 없다.
- 해결 방법
- 강제 데이터형 변환을 사용해서 접근한다.
- 나중에 소개할
dynamic_cast<>
연산자를 사용할 수도 있다.
class BaseClass
{
public:
friend ostream & operator<<(ostream & os, const BaseClass & b)
{
os << "기초 클래스";
}
};
class DerivedClass : public BaseClass
{
public:
friend ostream & operator<<(ostream & os, const DerivedClass & d)
{
// 방법 1
os << (const BaseClass &)d << " 그리고, 파생 클래스";
// 방법 2
os << dynamic_cast<const BaseClass &>(d) << " 그리고, 파생 클래스";
}
};
그 외의 사항들 #
- 상속되지 않는 것
- 생성자
- 파괴자
- 대입 연산자
- 파생 클래스 객체를 기초 클래스 객체에 대입하면 어떻게 될까?
- 왼쪽에 있는 객체에 의해 호출되므로, 기초 클래스의 대입 연산자가 호출된다.
- 따라서 파생 클래스가 가지는 값들은 모두 무시된다.
BaseClass base;
DerivedClass derived;
base = derived; // BaseClass의 대입 연산자가 호출된다.
- 반대로 기초 클래스 객체를 파생 클래스에 대입하면 어떻게 될까?
- 파생 클래스의 대입 연산자가 호출된다.
- 파생 클래스의 대입 연산자의 매개변수는 파생 클래스이다.
- 하지만 파생 클래스는 자동으로 기초 클래스를 참조할 수 없다.
- 대안
- 변환 생성자를 만든다.
- 기초 클래스를 대입하기 위한 또다른 대입 연산자를 만든다.
class DerivedClass
{
public:
// 변환 생성자 (방법 1)
DerivedClass(const BaseClass & b)
{
//...
}
// 대입 연산자
DerivedClass & operator=(const DerivedClass & d)
{
//...
}
// 기초 클래스를 대입하기 위한 또다른 대입 연산자 (방법 2)
DerivedClass & operator=(const BaseClass & b)
{
//...
}
};
int main()
{
BaseClass base;
DerivedClass derived;
derived = base; // DerivedClass의 대입 연산자가 호출된다.
}
- 부적절한 코드는 동적 결합을 불가능하게 만들 수 있으므로 주의하자.
class BaseClass
{
public:
virtual void View() const;
};
class DerivedClass : public BaseClass
{
public:
virtual void View() const;
};
void CallByRefernce(const BaseClass & b)
{
b.View();
}
void CallByValue(BaseClass b)
{
b.View();
}
int main()
{
DerivedClass derived;
// 실제 참조하는 객체인 DerivedClass의 View()를 호출된다.
CallByRefernce(derived);
// BaseClass(const BaseClass &)생성자로 만든 BaseClass의 View()를 호출한다.
CallByValue(derived);
}
- 멤버 함수의 특성
함수 | 상속 | 멤버 또는 프렌드 | 디폴트로 생성 | 가상으로 선언 | 리턴형 |
---|---|---|---|---|---|
생성자 | X | 멤버 | O | X | X |
파괴자 | X | 멤버 | O | O | X |
= |
X | 멤버 | O | O | O |
& |
O | 둘 중 하나 | O | O | O |
변환 | O | 멤버 | X | O | X |
() |
O | 멤버 | X | O | O |
[] |
O | 멤버 | X | O | O |
-> |
O | 멤버 | X | O | O |
op= |
O | 둘 중 하나 | X | O | O |
new |
O | static 멤버 |
X | X | void * |
delete |
O | static 멤버 |
X | X | void |
기타 연산자 | O | 둘 중 하나 | X | O | O |
기타 멤버 | O | 멤버 | X | O | O |
프렌드 | X | 프렌드 | X | X | O |