public 상속은 반드시 is-a 관계를 따르도록 하자.


사람 <------ 학생

위의 표현은 C++ 기본서에서

public 상속을 설명할 때 가장

빈번히 활용되는 예이다.




학생은 사람이라서 사람이 할 수 있는

(먹기, 앉기, 눕기 등등)을 할 수 있지만

모든 사람은 학생이 아니기 때문에

모든 사람은 학교를 다닌다가 참이 될 수 없다.




즉, Base클래스 (사람) 은 일반적인 개념을 타나내며

Drived클래스 (학생) 은 좀 더 특수화된 개념을 나타낸다.


public 상속과 is-a 관계가 똑같은 뜻이라는 이야기는

꽤 직관적이고 간단하긴 하지만, 그 직관 때문에 잘못된

판단을 하는 경우가 있다. 아래의 예제를 보도록 하자.

펭귄은 새의 일종이고, 새는 날 수있다는 것을 알고있다.




class Bird{

public:

virtual void fly();            // 새는 날 수 있다.

...

};

class Penguin: public Bird{    // 펭귄은 새이다.

...

};


새는 날 수 있다는 사실을 바탕으로 클래스를 디자인 했는데

펭귄이라는 새는 날지 못한다. 모든 새는 날지 못한다는 점도 

구분하여 좀 더 현실에 가까운 클래스 계통구조를 만들어야 한다.




class Bird {

...                                            // fly 함수가 없다.

}


class FlyingBird : public Bird{

public:

virtual void fly();

...

};


class Penguin: public Bird {

...                                            // fly 함수가 없다.

};




좀 더 현실에 가까운 클래스 구조가 되었다.

하지만 어떠한 소프트웨어 시스템이냐에 따라

새의 비행능력을 고려하지 않아도 되는 경우가 있다.

만약 새의 서식지와 먹이에 대한 응용프로그램이라면

처음 디자인한 클래스가 더욱 만족스러울 것이다.

즉, 프로그램의 요구 사항에 맞게 디자인 하는 것이 중요하다는 것이다.




이번에는 위의 펭귄문제를 런타임 에러를 통해 해결해보자.


void error(const std::string& msg);


class Penguin : public Bird {

public:

virtual void fly() { error ("Attempt to make a penguin fly!"); }

...

};


이 경우는 "펭귄은 날 수 없다." 가 아닌

"펭귄은 날 수 있다. 하지만 날려고 하면 에러가 난다" 이다.

"펭귄은 날 수 없다" 라는 것은 컴파일러가 판단할 수 있지만,

"날려고 하면 에러가 난다" 는 런타임에만 발견할 수 있다.




자 그럼 이번에는 컴파일 타임에서 이 문제를 해결해 보도록 하자.


class Bird{

...                                    // fly 없음

};


class Penguin : public Bird {

...                                    // fly 없음

};


Penguin p;

p.fly();                                    // 에러


이제 펭귄을 날려 보려고 하면, 컴파일 단계에서 문제가 생기게 된다.

런타임 에러를 내주는 것은 p.fly를 호출하는 것에 대해 문제가 없다.

런타임, 컴파일 타임 과연 어떤 해결책이 더 좋은 것일까?




유효하지 않은 코드를 컴파일 단게에서 막아 주는 인터페이스가

좋은 인터페이스이다. 즉 , 펭귄의 비행을 컴파일 타임에 거부하는

설계가 그것을 런타임에 뒤늦게 알아내는 것보다 훨씬 좋다는 것이다.




새와 펭귄 문제는 이쯤하고, 사각형과 정사각형을 클래스의

상속 계통으로 만드는 문제로 넘어가 보자. 초등학교 수학시간에

배운 기억을 바탕으로 우리는 "정사각형은 직사각형이다."라는

사실을 알고 있을 것이다. 때문에 정사각형은 직사각형을

상속받도록 디자인 해야한다고 생각하는 것도 당연할 것이다.




하지만 직사각형은 넓이와 높이 모두 자유자재로 바뀌어도

직사각형이라는 것을 만족한다. 하지만 이를 상속받는

정사각형은 가로 새로가 항상 일치해야 한다는 조건이 있다.

때문에 직사각형에 가로 혹은 세로를 마음대로 조정하는

함수가 있다면, 정사각형에는 적용할 수 없는 함수이다.

때문에 이 케이스틑 is-a관계가 적용될 수가 없다.




여기서 중요한 부분은 public 상속은 기본 클래스 객체가

가진 모든 것들이 파생 클래스 객체에도 그대로 적용되는 상속이다.

때문에 직사각형과 정사각형은  public 상속으로 표현한다면

틀린 것이고, 컴파일 수준에서 무사히 통과과 되었다 해도

제대로 동작할 것이라는 보장은 없다.




코드가 컴파일된다는 것이 제대로 동작한다는 의미는 아니다.

이런 점 때문에 프로그래머는 계속 배움의 길을 개척해 가야한다.


Point!!

public 상속의 의미는 is-a 이다.

기본 클래스에 적용되는 모든 것들이

파생 클래스에 그대로 적용되어야 한다.

모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문이다.




[C++] 타입 변환이 모든 매개변수에 대해 적용되어야

한다면 비멤버 함수를 선언하자.


유리수를 나타내는 클래스를 만들고 있다.

이 클래스를 디자인할 때 정수에서 유리수로

암시적 변환은 허용하자고 판단을 했다.

int -> double 변환과 별반 다르지 않기 때문에.




class Rational {

public:

Rational ( int numerator = 0 , int denominator = 1 );

// explicit으로 선언하지 않았다.


int numerator() const;    // 분자 접근 함수

int denominator() const;    // 분모 접근 함수


private:

...

}



유리수를 나타내는 클래스이기 때문에 덧셈이나

곱셈 등의 수치연산은 기본으로 지원해야 할 것이다.


멤버 함수로 연산자 오버로딩을 해보자.

const Rational operator* (const Rational& rhs) const;

이로써 곱셈 연산이 가능해진다.


Rational oneEighth (1 , 8);    // 가능

Rational oneHalf(1, 2);    // 가능


Rational result = oneHalf * oneEighth;    // 가능

result = result * oneEighth;    // 가능




이제 혼합형 수치 연산을 해보자.

유리수를 int(정수)와 곱셈을 가능하게

하는 것도 어떻게 보면 당연한 일이다.


result = oneHalf * 2;    // 가능

result = oneHalf.operator(2);    // 가능


result = 2 * ontHalf;    // 에러

result = 2.operator(oneHalf);    // 에러




첫 번쨰 줄에서 oneHalf 객체는 operator* 함수를 멤버로

가지고 있는 클래스의 인스턴스이기 때문에, 컴파일러는

이 함수를 호출한다. 하지만 두 번째 줄에서 정수 2에는

클래스 같은 것이 연관되어 있지 않기 때문에 operator*

멤버 함수도 있을리가 없다. 따라서 컴파일러는 비멤버

버전의 operator*를 찾아본다.


result = operator*(2,oneHalf);


비멤버 버전의 operator*가 없기 때문에 실패.




여기서 위의 성공 케이스를 다시 보도록 하자.

매개변수가 정수 2인데 Rational 객체를 받도록

되어 있는 operator*의 호출을 가능하게 한다.

이게 어떻게 가능한 것일까?


바로 암시적 타입 변환에 의해 가능하게 되는 것이다.

컴파일러는 이 int를 Rational 클래스의 생성자에 주어

호출하면 Rational로 둔갑시킬 수 있다는 사실도 알고 있다.




따라서

const Rational temp(2);

result = oneHalf * temp;

이런식으로 동작을 하게 된다.


물론 생성자를 명시호출 (explicit)로 선언되었다면 모두 불가능.



자 다시 돌아와서 둘 다 비명시호출을 했는데도 하나는 되고

하나는 왜 되지 않는지 고민해 보도록 하자.


result = oneHalf * 2; --> 비명시호출 생성자

result = 2 * ontHalf; --> 비명시호출 생성자인데도 안됨




결론

암시적 타입 변환이 먹혀들려면 매개변수 리스트에 들어있어야만 한다.

전자의 경우 매개변수 리스트에 있는 객체가 쓰이고 있지만

후자는 그렇지 않다. operator*의 구현부를 살짝 보자면

this * rhs 두 개의 매개 변수로 이루어져 있을 것인데

this란 놈은 매개변수 리스트에 없고 rhs란 놈만 있다.

따라서 매개변수 리스트에 있는 rhs란 놈만 암시적 타입변환이 일어난다.

암시적 타입 변환이 모든 매개변수에 대해서 이루어 지지 않고 있다.


그렇다면 모든 인자에 대해 암시적 타입변환을 수행하도록 하려면

어떻게 해야할까? operator*를 비멤버 함수로 만들어버리면 된다.




const Rational operator* (const Rational& rhs, const Rational& rhs){

return Rational (lhs.numerator() * rhs.numerator(),

   lhs.denominator() * rhs.denomiator());

}


Rational oneForth (1, 4);

Rational result;


result = oneFourth * 2;    // 가능

result = 2 * oneFourth;    // 가능




위의 함수는 비멤버이지만 프렌스 함수로 선언해야

하는 것 아닌가 의아해 할 수도 있다. 하지만 위의 

operator*는 클래스의 private 부분을 하나도

건드리지 않고 있다. 오로지 public 부분만을

사용하기 때문에 프렌드 선언은 적절하지 않다.


--> 프렌드 함수는 피할 수 있으면 피할 것




POINT!!

어떤 함수에 들어가는 모든 매개변수(this도 포함)에 대해

타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 한다.

어떤 함수에 대해서도 상속받은 기본 매개변수 값은

절대로 재정의하지 말자.


가상 함수는 동적으로 바인딩되지만,

기본 매개변수 값은 정적으로 바인딩된다.




class AAA{

public:

enum num { first , second , third };

virtual void show(num n = first);

}


class BBB : public AAA{

public:

virtual void show(num n = second);

}


class CCC : public AAA{

public:

virtual void show(inum n);

}


AAA *pa;

AAA *pb = new BBB;

AAA *pc = new CCC;



여기서 pb, pc는 모두 AAA에 대한 포인터로

선언되었기 때문에, 각각의 정적 타입도 모두 이 타입니다.

객체의 동적 타입은 현재 그 객체가 진짜로 무엇이냐에

따라 결정되는 타입니다. 다시 말해, '이 객체가 어떻게

동작할 것이냐'를 가리키는 타입이 동적 타입이다.




따라서 pb의 동적타입은 BBB*

pc의 동적 타입은 CCC*

pa는 동적 타입이 없다.




pb->show();

문제는 이 가상함수를 호출했을 때 발생한다.

파생 클래스에 정의된 가상 함수를 호출하면서

기본 클래스에 정의된 기본 매개변수 값을

사용해 버릴 수 있다는 것이다.


pb의 동적타입은 BBB*이기 때문에

함수는 BBB::show가 호출되지만

기본 매개변수 값은 AAA에서 가져온다.




이러한 동작방식은 런타임 효율이라는 요소가 숨어있다.

프로그램 실행 중에 가상 함수의 기본 매개변수 값을

결정할 방법을 컴파일러 쪽에서 마련해 주어야 하는데,

이 방법은 컴파일 과정에서 결정하는 현재의 메커니즘보다

느리고 복잡하다. 때문에 속도와 구현 간편성에 무게를

더 두어야 했고, 그 덕에 효율 좋은 실행 동작을 누릴 수 있게 되었다.




그렇다면 원하는 대로 가상함수가 동작하도록 만들어보자.

그 전 포스팅에서도 언급했던 NVI 관용구를 써보자.


class AAA{

public:

enum num { first , second , third };


void show ( num n = first ) const {

doShow(n);

}


private:

virtual void doShow(num n) const = 0;

}


class BBB : public AAA{

public:

...

private:

virtual void doShow(num n) const;

};




비가상 함수는 파생 클래스에서 오버라이드되면 안 되기 떄문에

위와 같이 설계한다면, 기본 매개변수에 대한 문제를 해결할 수 있다.


POINT!!

상속받은 기본 매개변수 값은 절대로 재정의해서는 안된다.

기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는

동적으로 바인딩되기 때문이다.


[C++] 상속받은 비가상 함수의 재정의는 절대 금물!!


앞선 포스팅에서 언급했듯이

비가상 함수의 상속은 파생에 관계없는

불변동작을 정해두는 것이다.

인터페이스와 필수 구현을 물려주는 것

그 것이 바로 비가상 함수의 상속인 것이다.




양쪽에서 x의 객체로 부터 mf 함수를 호출한다.

함수도 똑같고 객체도 똑같으니, 동작 또한 같다.

하지만 D클래스가 mf 함수를 또 정의하면 어떻게 될까.





이렇게 두 개의 다른 동작이 나오는 이유는

비가상 함수는 정적 바인딩이기 때문이다.

pb는 B에 대한 포인터 타입으로 선언되었기 때문에

pb를 통해 호출되는 비가상 함수는 항상 B클래스에

정의되어 있을 것이라고 결정해 버린다.


마찬가지로 B에서 파생된 객체를 pb가

가리키고 있다 해도 결과는 마찬가지이다.




반면, 가상 함수는 동적바인딩으로 묶이는데

mf가 가상 함수 였다면, mf가 어디서

호출 되든 D::mf가 호출된다.

pb, pd가 진짜로 가리키는 대상은 D 타입 객체이기 때문이다.




즉, 비가상 함수의 상속에서는

B냐 D냐를 결정하는 요인이 해당 객체가 아니라

그 객체를 가리키는 포인터 타입이라는 것이다.




이제는 비가상 함수의 재정의를 하면 안되는

이론적인 이유에 대해서 알아보도록 하자.


비가상 멤버 함수는 클래스 파생에 관계없는

불변동작을 정해두는 것이다.

is-a 관계 상속(public)의 관점에서 보자면




1. B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용된다.

2. B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받는다.


D에서 mf를 재정의 하는 순간 이러한 설계는 모순이 생겨 버린다.

B의 mf 구현을 모든 파생 클래스에서 사용하도록 정한 것인데

mf를 D에서 재정의 하는 순간 모든 D는 B의 일종이라는

명제는 사라져 버린다. D에서 정말 mf함수를 다르게

구현해야 한다면 비가상 함수가 아닌 가상 함수여야 한다.



POINT!!

상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자!!



[C++] 오버로딩 된 함수의 상속, using 선언


기본 클래스와 똑같은 이름의 멤버 함수를

파생 클래스에서 정의할 경우(비가상으로)

바깥쪽 유효범위에 있는 이름을 가리는

특성으로 인해 기본 클래스의 함수가 숨겨져 버린다.

( 참고로 비가상 함수를 파생클래스에서 재정의

하는 일은 절대로 해서는 안된다. )




하지만 때때로 가상 함수의 이름도 가려져 버리는

경우가 있는데, 기본 클래스로부터 오버로드

버전을 상속시키는 경우이다. 컴파일러는

기본 클래스의 오버로드 버전 상속을 막고 있는데,

public 상속을 쓰면서 이러한 상속을 하지 못하는 것은

엄연히 is-a 관계 위반이라고 할 수있다. 방법이 없을까?




비가상 함수인지 가상 함수인지 여부에

상관없이 이름이 가려진다.





파생 클래스 내에 using 선은을 해줌으로써

가려진 이름을 끄집어 냈다.


기본 클래스로부터 상속을 받으려고 하는데,

오버로드된 함수가 그 클래스에 들어 있고

이 함수 중 몇개만 재정의하고 싶다면,

각 이름에 대해 using 선언을 붙여 주어야 한다.


이렇게 하지 않으면 이름이 가려져 버린다.




POINT!!

가려진 이름을 다시 볼 수 있게 하는 방법으로,

using 선언을 쓸 수 있다.


[C++] 인터페이스 상속과 구현 상속(3)


인터페이스와 더불어 그 함수의

필수적인 구현을 물려받게 하는 비가상 함수




1. 멤버 함수의 인터페이스 만을 상속받고 싶은 경우

2. 함수의 인터페이스 및 구현을 모두 상속받고 그 상속받은

구현이 오버라이드가 가능하게 만들었으면 할 경우

3. 또한 인터페이스와 구현을 상속받되 어떤 것도

오버라이드할 수 없도록 막고 싶은 경우




class Shape{

public:

virtual void draw const = 0;                ----- 1번

virtual void error (const string& msg);----- 2번

int objectId() const;                            ----- 3번


}


인터페이스와 그 함수의 필수적인 구현을 물려받게 하는 것

즉, 위의 예시에서 3번에 해당하는 경우가 되겠다.




int objectId() const; ----- 3번

객체의 식별자를 내어주는 함수인데

객체 식별자를 계산하는 방법은 항상 똑같다는

전재하에 선언된 함수라 할 수 있다.


실제 계산 방법은 Shape::objectID의 정의에서

결정되고, 파생 클래스에서는 이를 바꿀 수 없다.




매우 간단하기 때문에 더 이상 설명이 필요없지만,

인터페이스 상속과 구현 상속을 하는데 있어

자주 일어나는 실수에 대해 알아보도록 하자.




1. 모든 멤버 함수를 비가상 함수로 선언하지 말자.

이러한 경우 파생 클래스를 만들어도 자신만의 특별한

동작을 만들 여지가 없어진다. 기본 클래스로 사용할

용도로 디자인 한 것이라면 대부분 가상 함수를 갖고 있게 된다.




2. 모든 멤버 함수를 가상 함수로 선언하지 말자.

이러한 부분이 맞는 경우도 있겠지만, 분명히 파생 클래스에서

재정의가 안 되어야 하는 함수도 분명히 있을 것이다.

이러한 경우는 비가상 함수로 만들어 입장을 확실히 하자.




POINT!!

비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속을 뜻한다.

[C++] 인터페이스 상속과 구현 상속(2)


인터페이스만을 물려주는 상속에 이어

인터페이스뿐만 아니라 그 함수의 기본 구현도

물려받는 상속에 대해 알아보도록 하자.


1. 멤버 함수의 인터페이스 만을 상속받고 싶은 경우

2. 함수의 인터페이스 및 구현을 모두 상속받고 그 상속받은

구현이 오버라이드가 가능하게 만들었으면 할 경우

3. 또한 인터페이스와 구현을 상속받되 어떤 것도

오버라이드할 수 없도록 막고 싶은 경우




class Shape{

public:

virtual void draw const = 0;                ----- 1번

virtual void error (const string& msg);----- 2번

int objectId() const;                            ----- 3번


}


인터페이스뿐만 아니라 그 함수의 기본 구현을 상속 받는것

즉, 위의 예시에서 2번에 해당하는 경우가 되겠다.


단순 가상 함수로 선이 되어 있는데, 순수 가상 함수와

비교했을 때 몇가지 다른 면을 가지고 있다. 파생 클래스가

인터페이스를 상속받는 것은 똑같지만, 파생 클래스 쪽에서

오버라이드 할 수 있는 함수 구현부도 제공한다는 점이다.




virtual void error (const string& msg); ----- 2번

이 함수가 뜻하는 것은 실행 중에 에러가 났을 때

이 함수를 제공하는 것은 모든 파생 클래스가 해야 하는

 일이지만, 그렇다고 각 파생 클래스마다 똑같은 방법으로

에러를 처리할 필요는 없다는 것이다. (재정의 가능)


또한 Shape 클래스에서 제공하는 기본 에러 처리

(기본 구현)를 사용해도 된다는 의미도 있다. ( 구현 상속 )




하지만 단순 가상 함수에서 함수 인터페이스와 기본 구현을

한꺼번에 지정하도록 내버려 두는 것은 위험할 수도 있다.

아래의 예제를 보도록 하자.




모든 비행기는 fly함수를 상속 받아

각각의 비행 방식을 설정할 수 있으며

기본 동작으로 움직일 수도 있다.


위의 두 비행기는 비행 동작이 같기 때문에

코드 중복을 피하기 위해 기본 비행 원리를

Airplane::fly 함수의 본문으로 제공했다.


여기까지는 객체 지향 기술의 장점을

모두 빼다박은 훌륭한 코드이다.




하지만 여기서 비행 방식이 완전히 다른

ModelC라는 비행기가 들어왔다.

때문에 fly 함수를 재정의 해야 하지만

만약 이부분을 깜빡했다면 큰 문제로 이어질 수 있다.


여기서 문제는 Airplane::fly 함수가 기본 동작을

구현해서 가지고 있다는 점이 아니라, ModelC가

기본 동작을 물려받는 데 아무런 제약이 없다는 것이다.

이 문제를 해결하기 위해서는 크게 두가지 방법이 있다.




01. 

가상 함수의 인터페이스와 그 가상 함수의

기본 구현을 잇는 연결관계를 끊어 버리는 방법




이제 fly함수가 순수 가상 함수로 선언되어 있기 때문에

파생 클래스에서는 무조건 이 함수를 재정의 해야한다.

ModelA , ModelB는 비가상 함수인 ( 구현만을 물려 받는 )

deflultFly를 fly함수 내에서 호출하면 된다.


ModelC는 fly를 재정의하지 않으면 안되기 때문에

앞서 보았던 재정의를 깜빡하는 일은 이제 할 수 없다.




02.

순수 가상 함수의 구현을 통해

함수의 선언부, 정의부를 나누는 방법




순수 가상 함수가 들어왔다는 것 말고는

이전 설계와 원리 모두 똑같다. 순수 가상 함수가

구체 파생 클래스에서 재선언되어야 한다는

사실을 활용하되, 자체적으로 순수 가상

함수의 구현을 구비해 두는 것이다.




하지만 이 방법은 01 방법과 다르게

함수의 각기 다른 보호 수준을 부여할 수 있는

융통성이 날아가 버린다. protected 영역에 있던

defaultfly가 순수 가상 함수의 영역으로 들어왔기 때문


저번 포스팅에서 순수 가상 함수의 구현을 주는 일은

단순 가상 함수에 대한 기본 구현을 보다 안전하게

제공하는 메커니즘으로 활용할 수 있다고 했는데

바로 이 부분이라 할 수 있겠다.




얘기가 길어졌지만, 이번 포스팅의 주제는

단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금

함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도

물려받게 하자는 것이다.



다음 포스팅에서는 인터페이스와 더불어 그 함수의

필수 구현을 물려받게하는 경우, 3번에 해당하는

내용을 다루도록 하겠다.


Point!!

단순 가상 함수는 인터페이스 상속과 더불어

기본 구현의 상속도 가능하도록 지정합니다.






C++의 인터페이스 상속과 구현 상속(1)

인터페이스만을 물려주자 : 순수 가상 함수




클래스 설계자 입장에서 보면, 

1. 멤버 함수의 인터페이스 만을 상속받고 싶은 경우

2. 함수의 인터페이스 및 구현을 모두 상속받고 그 상속받은

 구현이 오버라이드가 가능하게 만들었으면 할 경우

3. 또한 인터페이스와 구현을 상속받되 어떤 것도

 오버라이드할 수 없도록 막고 싶은 경우가 있다.


오늘은 인터페이스만을 상속받고 싶은 경우에 대해서 알아보자.




class Shape{

public:

virtual void draw const = 0;                ----- 1번

virtual void error (const string& msg);----- 2번

int objectId() const;                            ----- 3번


}



1번은 순수 가상함수로 선언되어 있다. 그말인 즉슨

Shape는 추상 클래스가 되고 인스턴스를 만들 수 없다.


순수 가상 함수의 가장 두드러진 특징이라면

1. 어떤 순수 가상 함수를 물려받은 구체 클래스가 해당

순수 가상 함수를 다시 선언해야 한다.

2. 순수 가상 함수는 전형적으로 추상 클래스 안에서

정의를 갖지 않는다.




따라서 순수 가상 함수를 선언하는 목적은

파생 클래스에서 함수의 인터페이스만을 물려주는 것이다.




하지만 순수 가상 함수에도 정의를 제공할 수 있다.

구현을 제공할 수 있다는 말이다. 단, 구현이 붙은

순수 가상 함수를 호출하려면 반드시 클래스의

이름을 한정자로 붙여 주어야만 한다.


Shape *ps = new Rec;

ps -> Shape::draw();




이는 단순 가상 함수에 대한 기본 구현을

안전하게 제공하는 메커니즘으로 활용할 수 있다.

이 부분에 대해서는 인터페이스와 기본 구현도

함께 물려받는 경우, 즉 단순 가상 함수에 대해

포스팅을 할 때, 같이 진행하도록 하겠다.




Point!!

순수 가상 함수를 선언하는 목적은 파생 클래스에게

함수의 인터페이스만을 물려주자는 것이다.


내부에서 사용하는 객체에 대한 핸들을

반환하는 코드는 되도록 피하자.


사각형의 좌측 상단, 우측 하단의 꼭짓점을

추상화한 Rec 클래스가 있다.  꼭짓점을

Rec클래스에 넣지 않고 별도의 구조체에

넣은 후 Rec가 이 구조체를 가리키도록 만들었다.




class point{

int x,y;

...

};


struct RecData{

point ulhc;

point lrhc;

};


class Rec{

tr1::shared_ptr<RecData> pData;

...

};




Rec 클래스의 사용자는 꼭짓점의 정보를

얻을 수 있어야하므로 멤버함수를 정의한다.

class Rec{

public:

point& upper() const (return pData->ulhc;}

point& lower() const (return pData->lrhc;}

}




함수들이 모두 const로 선언되어 있으나

함수안에서 리턴되는 참조자에 의해

값이 변경될 수 있다. ulhc, lrhc는 Rec의

멤버가 아니기 때문에 충분히 가능하다.


1. 클래스 데이터 멤버는 아무리 숨겨봤자 그 멤버의

참조자를 반환하는 함수들의 최대 접근도에 따라

캡슐화 정도가 정해진다.


2. 어떤 객체에서 호출한 상수 멤버 함수의 참조자

반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면

이 함수의 호출부에서 그 데이터의 수정이 가능하다.




참조자 뿐만 아니라 포인터, 반복자는 모두 핸들이다.

외부 공개가 차단된 멤버 변수 뿐만 아니라 멤버 함수에

대해서도 이들의 핸들을 반환하는 멤버 함수를 만들지 않아야 한다.


const point& upper() const (return pData->ulhc;}

const point& lower() const (return pData->lrhc;}

위의 문제는 이와 같이 const를 붙여주면 해결된다.




무효참조 핸들

객체를 참조자로 반환 했을 때

그 객체가 함수 내에서 선언된

지역 객체라면 함수가 종료됨과 동시에

없어지게 된다. 따라서 반환값을 받는

변수는 없어진 객체를 받게 된다.


핸들을 반환하는 것은 const를 붙였다해도

바깥으로 떨어져 나간 핸들은 그 핸들이 참조하는

객체보다 오래 살 위험이 있다. 따라서 핸들을

리턴하는 것은 되도록이면 피하도록 하자.




POINT!!

어떤 객체의 내부요소에 대한 해들을 반환하는 것은 되도록 피하자.

변수의 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자.


비밀번호가 충분히 길 경우에 해당 비밀번호를

암호화하여 반환하는 함수가 있다.




string encryptPassword ( const string& password){


string encrypted;  // 1번

if(password.length() < MinimumPasswordLength) {

throw logic_error ("Password is .... ");

}

//2번 string encryped

...  // 암호화 진행부분

return encrypted;

}


1번의 경우 encryped 객체는 길이 점검에서 예외가 던져지게 되면

안 쓰여질 확율이 있다. 따라서 불필요한 생성자 소멸자

비용을 지불하게 된다. 2번의 경우는 위와 같은 불필요한

비용을 지불하진 않지만 기본생성자가 호출된 후에

또 다시 대입을 해야하는 구조이기 때문에 여전히

비용에 대해 자유로울 수 없다.




void encrypt(string &s);

위 함수에서 암호화를 진행한다고 할 때


string encryptPassword ( const string& password){


if(password.length() < MinimumPasswordLength) {

throw logic_error ("Password is .... ");

}


string encrypted(password);

encrypt(encrypted);

return encrypted;

}




encrypted가 생성될 때 매개변수로 들어온

password로 초기화가 되고 있다.

변수의 정의를 늦줘서 기본생성자 호출

그리고 대입에 대한 불필요한 비용을 

최소화 하고 있다.


루프에 대해선 어떻게 해야 할까?


1번.

widget w;

for( ... ){

w = i;

}

2번.

for( ... ){

widget w(i);

}


1번의 경우 생성자와 소별자가 1번씩 호출되며

대입이 여러번 일어나고 있다. 2번의 경우에는

생성자와 소멸자의 호출이 여러번 일어나고 있다.




대입에 들어가는 비용이 생성자 소멸자 쌍보다

적게나오는 경우 1번 방법이 좋다. 하지만 A는

w의 유효범위가 넓어지기 때문에 프로그램의

이해도와 유지보수성이 역으로 안좋아질 수 있다.




POINT!!

변수 정의는 늦출 수 있을 때까지 늦추자.

+ Recent posts