C# 인덱서 (Indexer)


인덱서는 인덱스를 이용해서 객체 내의 데이터에

접근하게 해주는 프로퍼티라고 생각하면 된다.

객체를 마치 배열처럼 사용할 수 있게 해준다.


한정자 인덱서 형식 this [형식 Index]{

get{ ... }

set{ ... }

}



C# 인덱서 예제




foreach가 가능하게 만들어보자.

foreach 문은 IEnumerable과 IEnumerator를

상속하는 형식만 지원한다. 따라서 IEnumerable과

IEnumerator를 상속하기만 하면 foreach를 이용할 수 있다.




IEnumerable의 메소드

1. IEnumerable GetEnumerator()


IEnumerator 인터페이스를 상속하는

클래스의 객체를 반환하면 된다.

그렇다면 IEnumerator 인터페이스는 무엇일까.




IEnumerator 메소드 및 프로퍼티

1. boolean MoveNext()

2. void Reset()

3. Object Current { get; }


자 그럼 위의 예제를 foreach가 가능하도록 만들어 보자.




IEnumerator 멤버




IEnuemerable 멤버




메인




C#의 인덱서 (Indexer)에 대해 알아보았다.

C++이랑 용어만 다르지 아직까진 비슷한게 더 많은듯




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

절대로 재정의하지 말자.


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

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




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#] 상속 관계에 있는 클래스의 형변환

is 연산자와 as 연산자




C++의 다운 캐스팅과 연관된 연산자인 듯 하다.

예전 dynamic_cast<>를 공부할 때 비슷한 내용을

다뤘던 것 같다. C#에서는 좀 더 세련된(?) 형변환을

위해 멋진 연산자 두 개를 제공한다. 바로 is와 as




class Mammal{

public void Nurse() { ... }

}


class Dog : Mammal {

public void Bark() { ... }

}


class Cat : Mammal {

public void Meow() { ... }

}


익히 알고 있듯이 포유류는 포유류 이고

개도 고양이도 모두 포유류이다.


Mamal m = new Mammla();

m.Nurse();


m = new Dog();

m.Nurse();


Dog dog = (Dog)m;     // 다운 캐스팅이다.

dog.Nurse();                // 하지만 m이 가리키고 있는 것이

dog.Bark();                  // Dog이기 때문에 가능하다.


m = new Cat();

m.Nurse();


Cat cat = (Cat)m;        // 역시 다운 캐스팅.

cat.Nurse();                // 하지만 m이 가리키고 있는 것이

cat.Meow();                // Cat이기 때문에 가능하다.




위의 코드를 is 와 as 연산자를 사용해서 바꾸어보자.

먼저 is와 as를 간단하게 설명하자면


is

객체가 해당 형식에 해당하는지 검사하여

그 결과를 bool 값으로 반환한다.


as

형식 변환 연산자와 같은 역할을 한다.

다만 형변환 연산자가 변환에 실패하는 경우에

예외를 던지는 반면, as 연산자는 객체 참조를

null로 만들어 버린다. (dynamic_cast와 같은 성질)




Mammal m = new Dog();

Dog dog;


if( m is Dog ){                    // 말그대로  m 이 Dog를 가리키고 있니?

dog = (Dog)m;

dog.Bark();

}


Mammal m2 = new Cat();

Cat cat = m2 as Cat;         // m2를 Cat으로 형변환하라.

if( cat != null ){                 // 다운 캐스팅을 하라는 말인데

cat.Meow();             // 이 경우 m2는 Cat를 가리키고 있기 때문에 가능하다.

}                                      // 만약 m2 = new Mammal() 이었다면 불가능.

 // 파생 클래스는 기본 클래스보다 더 많은 정보를 담고 있는데

 // 기본 클래스에 없는 부분을 호출하려 한다면 문제가 발생.

// 때문에 이 경우 cat는 null이 된다.




C++의 시점에서 설명이 된 듯한데

확실히 C++의 dynamic_cast<> 보다는

훨씬 사용법이 간편한 것 같다.




일반적으로 형식 변환 연산자 대신 as 연산자를

사용하는 쪽이 더 권장 되는데, 형식 변환에 실패하더라도

예외가 일어나 갑자기 코드의 실행이 갑자기 점프하는 일이

없으므로 코드 관리가 더 수월해지기 때문이란다.


주의해야할 점은 as 연산자는 참조 형식에 대해서만

사용이 가능하므로 값 형식의 객체는 기존의 형식

변환 연산자를 사용해야 한다.





[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!!

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

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






01. 모바일 2D 디펜스 게임

'Wings Defense'


포트폴리오 링크 :

https://www.youtube.com/watch?v=bB1_zYog1o0


디펜스 게임의 긴장감과 RPG 게임의 성장에

초점을 맞추어 제작한 디펜스 게임입니다.

다양한 스킬 업그레이드과 캐릭터의 업그레이드 중

어느 쪽에 적절한 투자를 하느냐가 게임의 핵심입니다.


제작 기간 : 16.06.23 ~ 16.08.05 (약 5주)





02. 모바일 스릴러 TPS 게임

'Red Sniper'


포트폴리오 링크 :

https://www.youtube.com/watch?v=OiL2Yv6L73Q


스릴러 장르에 저격이라는 요소를 넣어

제작한 모바일 TPS 게임입니다.

저격수답게 적에게 들키지 않아야하며

신중하게 조준하여 적을 처치해야 합니다.


제작 기간 : 16.08.16 ~ 16.09.13 (약 4주)







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!!

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

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


다이나믹 (동적 계획법) 알고리즘 : 동전 교환 문제 (정올)


다이나믹 알고리즘의 가장 대표적인 문제가 아닌가 한다.

차곡차곡 저장된 데이터를 기반으로 답을 찾아간다는

단순한 논리이지만 수학문제를 푸는 것 같아 굉장히 어렵다..


이번 문제는 1시간 이상 걸렸고, 데이터를 차곡차곡

저장하는 방법과, 저장된 데이터를 빼서 사용하는

부분에서 상당히 애를 먹었다. ( 그냥 다 어려웠다는 말 )







01. 다이나믹(동적 계획법)을 수행할 클래스 선언





02. 초기화




03. 다이나믹 (동적 계획법) 알고리즘 수행

나름 자세하게 설명해 보았다. 다이나믹을 처음 접했을 때

상당히 난해했던 기억이 난다. 혹시나 나와 같은 사람이 있을까

최대한 설명을 해보지만.. 더 헷갈릴 수도 있겠다는 생각이 든다.




04. 메인 함수




멤버 함수를 하나하나 부르기 보다 편의 함수를

정의해서 함수 호출을 했다. 알고리즘 공부를 하면서

C++도 공부할 수 있으면 괜찮겠다 생각해서

클래스로 만들고 편의 함수도 만들어 보았다.




+ Recent posts