레이블이 CPP인 게시물을 표시합니다. 모든 게시물 표시
레이블이 CPP인 게시물을 표시합니다. 모든 게시물 표시

2016년 3월 22일 화요일

[c++] 순수가상클래스와 인터페이스의 차이점

오랜만에 C++ 코딩을 하느라 정신이 없습니다. 그런 와중에도 한가지 적을만한 내용이 있어서 적어봅니다.

C++ 에는 아시다 시피 interface 라는 것이 컴파일러 차원에서 재공되지 않습니다. Java 에서는 interface 라는 키워드도 있고
interface 구현 할때는 내부 구현을 못하도록 강제로 막아 주기도 하지요. 디자인 적인 차원에서 매우 훌륭한 특징이라고 생각합니다.

대신에
C++ 에서는 Abstract class 라는 것을 지원하는데 구현 함수중의 적어도 한개를 virtual 로 선언하고 구현을 Null 값으로 할당해서 구현을 비워두면 자동으로 추상 클래스가 됩니다. Java 의 interface 와의 차이점은, Java 에서는 내부 구현을 전혀 못하도록 강제하는 반면에  C++ 에서는 일부 구현을 허용한다는 점이 다릅니다.

그러면 C++ 의 추상 클래스를 사용해서 Java 의 interface 와 완전히 똑같은 것을 사용하려면 어떻게 해야하는가? 하는 것이 사실 이 글의 요점입니다.
컴파일러의 도움없이 수작업으로 모든 함수들을 공란으로 비워두면 interface 와 비슷해지지만, 완전히 같아지지는 않습니다. 문제는 C++ 에서는 interface 같은 형태로 사용하려면 다중 상속의 개념이 적용되는데 C++ 에서 상속을 할때에는 반듯이 소멸자가 virtual 로 선언되어야 합니다. 그런데 소멸자는 선언이 되면 반듯이 구현도 같이 해줘야 하는 문제가 덩달아 생겨납니다.

그래서 아무리 비슷하게 하려고 해도 아래와 같이 소멸자는 구현이 되어야 한다는 것입니다.

class EventListener {
public:
    virtual ~EventListener() {} // 이 부분이 구현이 된 것입니다.

    virtual void handleEvent(EventPtr evt) = 0; // 함수를 virtual 로 선언하고 구현을 비워둡니다.
};
그래서 결론은 비슷하게 사용할수 있지만 interface 인 녀석이 구현을 담고 있다는 것이 여전히 꺼림직 합니다.

소멸자는 순수 가상함수로 선언할수 없습니다. 

추가로(참고 : http://pacs.tistory.com/35)
class EventListener {

public:
   virtual ~EventListener() = 0; // 순수 가상 소멸자의 선언. 이렇게 수행할 수 없는게 아니다.

};
 EventListener::~ EventListener() {} //순수 가상 소멸자의 선언. 다만 순수 가상소멸자는 반드시 정의를 해 두어야 하기에 위의 예제와같이 작성한다.

2016년 3월 17일 목요일

[c++] 상수 멤버 함수

상수 멤버 함수

상수 멤버 함수는 멤버값을 변경할 수 없는 함수이다. 멤버값을 단순히 읽기만 한다면 이 함수는 객체의 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 지정하는 것이 좋다. 클래스 선언문의 함수 원형 뒤쪽에 const 키워드를 붙이면 상수 멤버 함수가 된다. 함수의 앞쪽에서는 리턴값의 타입을 지정하기 때문에 const를 함수 뒤에 붙이는 좀 별난 표기법을 사용한다.

class Some
{
private:
     int Value;

public:
     int SetValue(int aValue);        // 비상수 멤버 함수
     int GetValue() const;             // 상수 멤버 함수
};

정수형의 Value 변수가 비공개 영역에 선언되어 있고 이 멤버값을 읽고 쓰는 Get/Set 액세스 함수들은 공개 영역에 선언되어 있다. Value를 외부에서 변경하고 싶다면 SetValue 함수를 호출하고 Value를 읽고 싶을 때는 GetValue 함수를 호출한다. 이때 GetValue는 객체의 어떠한 멤버값도 변경하지 않으므로 상수 멤버 함수이며 이 함수 원형 뒤에 const를 붙여 GetValue는 값을 읽기만 한다는 것을 컴파일러에게 확실하게 알려 준다.
상수로 선언된 객체에 대해서는 상수 멤버 함수만 호출할 수 있으며 비상수 멤버 함수는 호출할 수 없다. 왜냐하면 상수 객체는 읽기 전용이므로 어떤 멤버의 값도 변경되어서는 안되기 때문이다. 다음 예제를 보자.

  : ConstFunc
#include <Turboc.h>

class Position
{
private:
     int x,y;
     char ch;

public:
     Position(int ax, int ay, char ach) { x=ax;y=ay;ch=ach; }
     void OutPosition() const { gotoxy(x, y);putch(ch); }
     void MoveTo(int ax, int ay) { x=ax; y=ay; }
};

void main()
{
     Position Here(1,2,'A');
     Here.MoveTo(20,5);
     Here.OutPosition();

     const Position There(3,4,'B');
     There.MoveTo(40,10);           // 에러 발생
     There.OutPosition();
}

문자를 출력하는 OutPosition함수는 값을 읽기만 하므로 const로 선언되어 있고 MoveTo함수는 위치를 옮기기 위해 x, y 멤버의 값을 변경하므로 const가 아니다. 만약 MoveTo를 const로 지정하면 상수를 변경할 수 없다는 에러로 처리된다. 객체의 값을 조금이라도 변경하는 함수는 상수 멤버 함수로 지정하지 말아야 한다. const로 선언된 OutPosition에 x++따위의 코드를 작성하면 상수 멤버 함수가 객체의 상태를 변경하려고 했으므로 역시 에러로 처리될 것이다. 단, 상수 멤버 함수라도 정적 멤버 변수의 값은 변경할 수 있는데 정적 멤버는 객체의 소속이 아니며 객체의 상태를 나타내지도 않기 때문이다.
main의 테스트 코드를 보자. Here는 비상수 객체로 선언되었므로 OutPosition으로 문자를 출력함은 물론 MoveTo로 위치를 옮길 수도 있다. 그러나 There는 상수 객체로 선언되었므로 상수 멤버 함수인 OutPosition만 호출할 수 있으며 MoveTo 호출문은 에러로 처리된다. 이 문장이 에러로 처리되는 이유는 다음 문장이 에러로 처리되는 이유와 동일하다.

const int i=5;
i=8;                   // 에러

상수에 어떤 값을 대입하여 변경할 수 없는 것과 마찬가지로 상수 객체의 상태를 변경하는 함수를 호출하는 것도 불가능하다. 비상수 멤버 함수가 받는 객체 포인터 this는 Position * const 형이며 this 자체는 상수이지만 this가 가리키는 대상은 상수가 아니다. 반면 상수 멤버 함수가 받는 객체 포인터 this는 const Position * const 형이며 this도 상수이고 this가 가리키는 대상도 상수이다. 결국 상수 멤버 함수의 제일 끝에 붙는 const는 이 함수로 전달되는 숨겨진 인수 this의 상수성을 지정한다.
컴파일러는 멤버 함수의 코드를 읽어보고 멤버값을 변경하는지 아닌지를 정확하게 판단할 수 없다. 멤버의 값을 변경하는 방법에는 직접적인 대입만 있는 것이 아니라 포인터를 통한 간접 변경, 함수 호출을 통한 변경 등 여러 가지 변칙적인 방법들이 많기 때문에 함수의 내용만으로 상수성을 정확하게 판단하는 것은 불가능하다. 그래서 상수 멤버 함수인지 아닌지는 개발자가 판단해서 지정해야 한다. 만약 OutPosition의 원형에 const를 빼 버리면 There.OutPosition() 호출조차도 에러로 처리된다. 왜냐하면 컴파일러는 OutPosition함수가 멤버값을 변경할 수도 있다고 생각하기 때문이다.
어떤 멤버 함수가 값을 읽기만 하고 바꾸지는 않는다면 const를 붙이는 것이 원칙이며 이 원칙대로 클래스를 작성해야 한다. 그러나 이 책의 예제들은 예제로서의 간략함을 위해 종종 이 원칙을 무시하고 있는데 절대로 본받지 말아야 한다. 예제는 다루고 있는 주제의 핵심을 보여야 하기 때문에 불가피하게 모든 원칙을 준수하기 어렵다. 만약 원칙을 어기면 상수 객체에 대해서 비상수 멤버 함수를 호출할 수 없게 된다. 다음과 같은 함수의 경우를 보자.

void func(const Position *Pos);

이 함수로 전달되는 Pos는 상수 지시 포인터이므로 *Pos는 func 함수 안에서 상수 객체이다. 따라서 Pos 객체에 대해서는 상수 멤버 함수만 호출할 수 있다. MoveTo로 위치를 옮길 수 없으며 OutPosition 이 상수 멤버 함수로 지정되어 있지 않다면 문자를 출력하는 것도 불가능해진다. 이런 경우에도 잘 동작하려면 원칙대로 멤버값을 바꾸지 않는 함수는 상수 멤버 함수로 지정해야 한다.
함수의 상수성은 함수 원형의 일부로 포함된다. 그래서 이름과 인수 목록이 같더라도 const가 있는 함수와 그렇지 않은 함수를 오버로딩할 수 있다. 즉, 다음 두 함수는 이름과 취하는 인수가 같더라도 다른 함수로 인식된다.

void func(int a, double b, char c) const;
void func(int a, double b, char c);

사실 이는 지극히 당연한 규칙인데 인수의 상수성이 오버로딩 조건이 되므로 const인 this와 그렇지 않은 this를 받는 함수도 당연히 중복 정의할 수 있다. 다음 두 함수가 중복 정의 가능한 것과 같은 이유라고 이해하면 된다.

void func(const char *p);
void func(char *p);

컴파일러는 상수 객체에 대해서는 위쪽의 상수 멤버 함수를 호출할 것이고 그렇지 않은 경우는 아래쪽의 비상수 멤버 함수를 호출할 것이다. 객체가 상수일 때와 그렇지 않을 때의 처리를 다르게 하고 싶다면 두 타입의 함수를 따로 제공하는 것도 가능하다. const와 비슷한 지정자인 volatile 도 마찬가지로 함수 원형의 일부이다.

나름 결론 : 
1. 함수의 const 도 오버로딩 된다. 
2. 상수 멤버 함수는 상수 객체나 비상수 객체에서 부를 수 있지만, 상수 객체는 비상수 멤버 함수는 부를 수 없다. 
3. 값에 의한 전달 보다는 상수 객체 참조자에 의한 전달 방식이 낫다. ( 상수 객체를 사용하는데, 상수 멤버 함수만 호출 할 수 있기 때문에, 상수 멤버 함수를 사용하는 듯 ) 



[c++] inline 함수

※요약
인라인 함수는 프로그램의 실행 속도를 높이기 위해 추가된 기능이며 C언어의 매크로 함수와 비교된다.

(개발자 입장에서)일반 함수와 인라인 함수의 가장 큰 차이점은 함수의 호출 방식이다.
일반 함수의 호출 방법은 프로그램 실행 중 함수를 실행해야하면 해당 함수의 주소로 점프시켰다가,  
함수의 처리가 종결되면 다시 원래의 자리로 돌아오는 것이다.
이렇게 앞뒤로 점프를 수행하고, 점프할 위치를 기억하려면 함수를 사용하는데 시간이 많이 걸린다.

인라인 함수는 컴파일된 함수 코드가 프로그램의 코드 안에 직접 삽입되어진다.
이 말은 컴파일러가 함수를 호출하는 대신, 그에 대응하는 함수 코드로 대체한다는 것을 의미하며 
함수 호출없이 삽입된 함수 코드를 그 자리에서 처리하므로 해당 함수를 수행하기 위해 
프로그램이 다른 주소로 점프했다가 되돌아 올 필요가 없어 속도면에서 유리하다.


※특징
 - 인라인 함수를 사용하려면 함수 선언 앞에 inline이라는 키워드를 붙이거나 함수 정의 앞에 inline이라는 키워드를 붙인다.
 - 클래스 멤버 함수가 inline을 사용하려면, 함수 정의의 위치가 *.h에 있어야 한다. 안 그러면 확인할 수 없는 외부 참조라고 뜬다.
 - 프로그래머가 inline 선언을 해도 컴파일러가 인라인화를 거부할 수 있다.
 - 프로그래머가 inline 선언을 안 해도 컴파일러가 인라인화를 할 수 있다.
 - 함수의 덩치가 크거나 재귀호출이면 inline 요구를 거절하는 컴파일러도 있다.
 - 함수 코드의 수행 시간이 짧고 빈번하게 호출되는 함수가 아니라면, 인라인 함수로 인한 절대적인 시간 절약은 그다지 크지 않다.


※장점
 - 함수가 인라인화 되어 성능의 향상으로 이어질 수 있다.


※단점
 - 메모리 사용 측면에서는 인라인 함수가 일반 함수보다 불리하다.
   이유는 어떤 프로그램에서 인라인 함수를 열 번 호출한다면, 
   프로그램은 그 함수의 사본을 프로그램의 코드 안에 열 번이나 삽입해야 하기 때문이다.
 - 매크로 함수와 달리 자료형에 독립적이지 못 하다. 단, 템플릿을 이용하면 자료형에 독립적으로 사용할 수 있다.
인라인 + 템플릿 = 매크로 함수 


※예제
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
inline void Test( int nNum1 );
int main( )
{
    Test( 2 );
    return 0;
}
void Test( int nNum1 )
{
    int nResult = nNum1;
}

2016년 3월 16일 수요일

[c++] RTTI ( Real-Time Type Information ) 와 dynamic_cast

RTTI 
 RTTI (Runtime Type Information)실행시간에 객체의 타입에 대한 정보를 얻을 수 있는 기능을 말한다. C++은 클래스의 객체만 가지고선 어떤 클래스의 객체인지 알수 있는 방법이 원래 없기 때문이다. 형변환 중 dynamic_cast를 할려면 RTTI가 필요 한데, 우리가 많이 쓰는 Visual Studio는 기본적으로 RTTI 기능을 사용하지 않게 설정되어 있다. 왜냐하면 RTTI의 특성상 객체를 생성할때마다 그 객체 내부에 타입 정보와 상속 정보를 넣어두기 때문에 속도(퍼포먼스)의 저하가 일어나기 때문이다. 
그래서 RTTI 기능 및 dynamic_cast를 사용하기 위해서는 비주얼 스튜디오의 프로젝트 설정을 변경 시켜 줘야 한다. 프로젝트의 Properties에 들어가 C++ -> Language -> Enable Run-Tie Type Information을 Yes(/GR)로 바꿔주면 된다.

  그럼 이 RTTI와 dynamic_cast는 어떻게 작동을 하는 것일까? 
컴파일러는 RTTI 와 객체를 연결하기 위해서 가상함수 포인터 테이블을 이용을 한다. 원래 C++ 언어의 가상함수 포인터 테이블은 순수한 가상함수에 대한 함수 포인터 배열이다. RTTI 와 객체의 연결을 위해 C++ 언어는 가상함수 포인터 테이블 앞에 4 byte 를 만들고 이것을 RTTI 와의 연결 고리로 사용한다. 
 프로그램이 dynamic_cast 를 이용하여 캐스트를 한 경우 실행 코드는 dynamic_cast 의 표현식에 기술된 객체를 이용하여 RTTI 포인터 테이블을 검색하고, 만약 RTTI 포인터 테이블 상에 일치하는 RTTI 가 존재 한다면 표현식에 기술된 객체의 타입을 변환하여 반환하고, RTTI 포인터 테이블 상에 일치하는 RTTI 가 존재 하지 않는다면 dynamic_cast 는 NULL(0) 을 반환을 할 것이다. 

[c++] 임시 객체



임시 객체 

  1. int a = 3 + 4;  
 위와 같이 선언된 문장이 있다. 우리는 보통 생각하기에 이 문장은 int 형 데이터가 4Byte 이므로 메모리 공간에 4Byte의 메모리 공간만 할당한다고 생각하겠지만, 이 경우에 메모리 공간에 3과 4라는 상수도 CPU에 의해서 연산이 이루어 져야 하므로 메모리 공간에 올라간다. 
 그럼 이렇게 상수도 메모리 공간에 올라가니까 다음 라인에서 이 숫자를 참조가 가능할까? 불가능하다. 물론 변수 a는 참조가 가능하다. 무슨 차이가 있을까? 바로 상수3과 4는 변수 a와 같이 이름이 없기 때문이다.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class AAA{  
  5.     char name[20];  
  6. public:  
  7.     AAA(char* _name){  
  8.         strcpy(name, _name);  
  9.         cout<<name<<" 객체 생성"<<endl;  
  10.     }  
  11.     ~AAA(){  
  12.         cout<<name<<" 객체 소멸"<<endl;  
  13.     }  
  14. };  
  15.   
  16. int main(void)  
  17. {  
  18.     AAA aaa("aaa Obj");  
  19.     cout<<"--------임시 객체 생성 전---------"<<endl;  
  20.     AAA("Temp Obj");  
  21.     cout<<"--------임시 객체 생성 후---------"<<endl;  
  22.     return 0;  
  23. }  
 위 소스 코드는 임시 객체 생성 코드 이다. 여기에서 AAA("Temp Obj");는  AAA aaa("aaa Obj"); 와 비교해보면 무엇이 틀린지 금방 알 수 있다. 바로 AAA("Temp Obj")는 이름이 없다. 이 문장은 AAA라는 객체 클래스를 생성하는데 "Temp Obj"라는 문자열을 받는 함수를 호출하라는 의미이다. 물론 객체는 생성하지만, 이름이 없어서 해당 라인을 벗어나면 바로 소멸 된다. 이렇게 만들어지는 순간은 존재했다가 해당 라인에서 벗어나면 소멸하는 객체를 바로 임시 객체 라고 한다. 임시 객체의 생성과 소멸은 결과를 보면 충분히 이해 할 수 있을 것이다. (해당 라인에서 생성, 벗어나자마자 소멸을 cout으로 출력해주는것을 볼 수 있다.)

 이 임시객체라는 것은 경우에 따라서는 컴파일러에 따라 최적화가 되기 때문에 가급적이면 사용하는 것이 좋다. 위의 예제를 임시객체를 이용해 다음과 같이 바꿀 수도 있을 것이다. 


[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...