[C++] Errors : cannot allocate an object of abstract type '???'

Programming/C++ Language 2010. 6. 14. 19:58
이 에러의 경우, 해당 class 를 생성( new ) 하는 부분에서 뜬다.

해당 클래스의 부모 클래스를 보면, 순수가상함수가 있을 텐데, 이를 구현하지 않았을 경우

이러한 메세지를 띄워준다. 순수가상함수는 객체 전체의 메모리의 일부분을 차지하게 되고, 이를

컴파일러에서는 생각하고 있을 텐데, 이를 자식이 구현을 하지 않았을 경우에는, 해당 부분이 없으

므로, 메모리를 할당해야되는 부분을 구현하지 않았으므로, 추상타입의 객체를 할당 할 수 없다고 나

오는 것이다.

'Programming > C++ Language' 카테고리의 다른 글

[C++] 클래스 I  (0) 2010.07.21
[C++] C++상에서 발생하는 name mangling에 관한 내용  (0) 2010.07.16
[C++] 복사 생성자  (0) 2010.05.31
[C++] Construct method  (0) 2010.05.27
[C++] static_cast  (0) 2010.03.22

설정

트랙백

댓글

[C++] 복사 생성자

Programming/C++ Language 2010. 5. 31. 11:34

복사 생성자는 지금까지의 평이한 내용에 비해 약간 난이도가 있는 내용이므로 정신을 집중해서 읽을 필요가 있다. 변수를 선언할 때 = 구분자 다음에 상수로 초기값을 지정할 수 있으며 이미 생성되어 있는 같은 타입의 다른 변수로도 초기화할 수 있다. 다음은 가장 간단한 타입인 정수형의 예이다.

int a=3;
int b=a; 

정수형 변수 a는 선언됨과 동시에 3으로 초기화되었다. 그리고 동일한 타입의 정수형 변수 b는 선언과 동시에 a로 초기화되었다. 결국 두 변수는 모두 3의 값을 가지게 될 것이다. 너무 너무 상식적인 코드이며 이런 초기화는 실수형이나 문자형, 구조체 등에 대해서도 똑같이 허용된다. 클래스가 int와 동일한 자격을 가지는 타입이 되기 위해서는 이미 생성되어 있는 같은 타입의 객체로부터 초기화될 수 있어야 한다. 객체에 대해서도 과연 이런 초기화가 성립할 수 있는지 Position 객체로 테스트해 보기 위해 Constructor 예제에 다음 코드를 작성해 보자. 

Position Here(30,10,'A');
Position There=Here;
There.OutPosition(); 

Here 객체가 먼저 (30,10) 위치의 문자 'A'를 가리키도록 초기화되었으며 이어서 There객체는 선언과 동시에 Here객체로 초기화되었다. 이때 멤버별 복사에 의해 There는 Here의 모든 멤버값을 그대로 복사받으며 두 객체는 완전히 동일한 값을 가지게 된다. Position 객체가 내부에 모든 정보를 포함하고 있기 때문에 이런 초기화는 전혀 문제가 없다. 그렇다면 모든 객체에 대해 이런 초기화가 가능한지 Person 객체로도 테스트해 보자. Person1 예제의 main 함수에 다음 테스트 코드를 작성한다. 

void main()
{
     Person Boy("강감찬",22);
     Person Young=Boy;
     Young.OutPerson();
} 

이 코드는 정상적으로 컴파일되며 실행도 되지만 종료할 때 파괴자에서 실행중 에러가 발생하는데 왜 그런지 보자. Young 객체가 Boy객체로 초기화될 때 멤버별 복사가 발생하며 Young의 Name멤버가 Boy의 Name과 동일한 번지를 가리키고 있다. 정수형인 Age끼리 값이 복사되는 것은 아무 문제가 없지만 포인터끼리의 복사는 문제가 된다. Young이 초기화된 직후의 메모리 상황을 그림으로 그려보면 다음과 같으며 두 객체가 힙에 동적 할당된 메모리를 공유하고 있는 모양이다.

이런 상태에서 Young.OutPerson이나 Boy.OutPerson 함수 호출은 아주 정상적으로 실행된다. 그러나 두 객체가 같은 메모리를 공유하고 있기 때문에 한쪽에서 Name을 변경하면 다른 쪽도 영향을 받게 되어 서로 독립적이지 못하다. 이 객체들이 파괴될 때 문제가 발생하는데 각 객체의 파괴자가 Name 번지를 따로 해제하기 때문이다. new는 Boy의 생성자에서 한 번만 했고 delete는 각 객체의 파괴자에서 두 번 실행하기 때문에 이미 해제된 메모리를 다시 해제하려고 시도하므로 실행중 에러가 된다. 정수형은 어떤지 보자.

int a=3;
int b=a;
b=5; 

b가 생성될 때 a의 값으로 초기화되어 a와 b는 같은 값을 가진다. 그러나 이는 어디까지나 초기화될 때 잠시만 같을 뿐이지 두 변수는 이후 완전히 독립적으로 동작한다. b에 5를 대입한다고 해서 a가 이 대입의 영향을 받지 않으며 a에 무슨 짓을 하더라도 b를 어찌할 수는 없다. 정수형의 복사 생성이 이처럼 독립적이므로 사용자 정의형도 이와 똑같이 복사 생성을 할 수 있어야 한다.

Person Young=Boy; 선언문에 의해 Young은 Boy의 멤버값을 복사받지만 이 때의 복사는 포인터를 그대로 복사하는 얕은 복사이다. 따라서 Young은 일시적으로 Boy와 같은 값을 가지지만 Boy의 Name을 빌려서 정보를 표현하는 불완전한 객체이며 독립적이지 못하다. 이 문제를 해결하려면 초기화할 때 얕은 복사를 해서는 안되며 깊은 복사를 해야 하는데 이때 복사 생성자가 필요하다. 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 복사 생성자를 만들어 해결할 수 있다. 다음 예제는 Person1예제를 수정하여 Person 클래스에 복사 생성자를 추가한 것이다.  

: Person2

#include <Turboc.h> 

class Person
{
private:
     char *Name;
     int Age; 

public:
     Person(const char *aName, int aAge) {
         Name=new char[strlen(aName)+1];
         strcpy(Name,aName);
         Age=aAge;
     }

    Person(const Person &Other)   {
        Name=new char[strlen(Other.Name)+1];
        strcpy(Name,Other.Name);
        Age=Other.Age;
    }

     ~Person() {
          delete [] Name;
     }

     void OutPerson() {
          printf("이름 : %s 나이 : %d\n",Name,Age);
     }
};

 

void main()
{
     Person Boy("강감찬",22);
     Person Young=Boy;
     Young.OutPerson();
}

복사 생성자는 자신과 같은 타입의 다른 객체에 대한 레퍼런스를 전달받아 이 레퍼런스로부터 자신을 초기화한다. Person복사 생성자는 동일한 타입의 Other를 인수로 전달받아 자신의 Name에 Other.Name의 길이만큼 버퍼를 새로 할당하여 복사한다. 새로 메모리를 할당해서 내용을 복사했으므로 이 메모리는 완전한 자기 것이며 안전하게 따로 관리할 수 있다. Age는 물론 단순 변수이므로 값만 대입받으면 된다.

컴파일러는 Person Young=Boy; 구문을 Person Young=Person(Boy);로 해석하는데 이 원형에 맞는 생성자인 복사 생성자를 호출한다. 실인수 Boy가 Person 객체이므로 Person을 인수로 받아들이는 생성자 함수를 호출할 것이다. 복사 생성자에 의해 Young은 깊은 복사를 하며 메모리에 다음과 같이 완전한 사본을 작성한다.

이제 Young과 Boy는 타입만 같을 뿐 완전히 다른 객체이고 메모리도 따로 소유하므로 각자의 Name을 마음대로 바꿀 수 있고 파괴자에서 메모리를 해제해도 문제가 없다. 복사 생성자에 의해 두 객체가 완전한 독립성을 얻은 것이다.

복사 생성자의 임무는 새로 생성되는 객체가 원본과 똑같으면서 완전한 독립성을 가지도록 하는 것이다. 만약 객체가 데이터 베이스를 사용한다면 이 클래스의 복사 생성자는 새 객체를 위한 별도의 데이터 베이스 연결을 해야 하며 독점적인 자원을 필요로 한다면 마찬가지로 별도의 자원을 할당해야 한다. 그래야 Class A=B; 선언문에 의해 A가 B에 대해 독립적으로 초기화된다.

객체가 인수로 전달될 때

같은 종류의 다른 객체로 새 객체를 선언하는 경우는 그리 흔하지 않다. 그러나 다음과 같이 함수의 인수로 객체를 넘기는 경우는 아주 흔한데 이때도 복사 생성자가 호출된다. 

 void PrintAbout(Person AnyBody)
{
     AnyBody.OutPerson();
} 

void main()
{
     Person Boy("강감찬",22);
     PrintAbout(Boy);
}

 함수 호출 과정에서 형식 인수가 실인수로 전달되는 것은 일종의 복사생성이다. 함수 내부에서 새로 생성되는 형식인수 AnyBody가 실인수 Boy를 대입받으면서 초기화되는데 이때 복사 생성자가 없다면 AnyBody가 Boy를 얕은 복사하며 두 객체가 동적 버퍼를 공유하는 상황이 된다. AnyBody는 지역변수이므로 PrintAbout 함수가 리턴될 때 AnyBody의 파괴자가 호출되고 이때 동적 할당된 메모리가 해제된다. 이후 Boy가 메모리를 정리할 때는 이미 해제된 메모리를 참조하고 있으므로 에러가 발생할 것이다.

복사 생성자가 정의되어 있으면 AnyBody가 Boy를 깊은 복사하므로 아무런 문제가 없다. 객체가 인수로 전달될 때 뿐만 아니라 리턴값으로 돌려질 때도 복사 생성자가 호출된다. 위 테스트 코드를 Person2 예제에 작성해 놓고 실행하면 정상적으로 실행된다. 그러나 복사 생성자를 주석으로 묶어 버리면 다운된다. 함수의 인수로 사용되거나 리턴값으로 사용되는 객체는 반드시 복사 생성자를 제대로 정의해야 한다.

복사 생성자의 인수

복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없다. 만약 다음과 같이 Person형의 객체를 인수로 받아들인다고 해 보자.

 Person(const Person Other)
{
     Name=new char[strlen(Other.Name)+1];
     strcpy(Name,Other.Name);
     Age=Other.Age;
}

 복사 생성자 자신도 함수이므로 실인수를 전달할 때 값의 복사가 발생할 것이다. 객체 자체를 인수로 전달하면 복사 생성자로 인수를 넘기는 과정에서 다시 복사 생성자가 호출될 것이고 이 복사 생성자는 인수를 받기 위해 또 다시 복사 생성자를 호출한다. 결국 자기가 자신을 종료조건없이 호출해대는 무한 재귀 호출이 발생할 것이며 컴파일러는 이런 상황을 방관하지 않고 에러로 처리한다.

이런 이유로 복사 생성자의 인수로 객체를 전달할 수는 없다. 그렇다면 포인터의 경우는 어떨까? 포인터는 어디까지나 객체를 가리키는 번지값이므로 한 번만 복사되며 무한 호출되지 않는다. 또한 객체가 아무리 거대해도 단 4바이트만 전달되므로 속도도 빠르다. 복사 생성자가 객체의 포인터를 전달받도록 다음과 같이 수정해 보자.

 Person(const Person *Other) {
     Name=new char[strlen(Other->Name)+1];
     strcpy(Name,Other->Name);
     Age=Other->Age;
}

 Other의 타입이 Person *로 바뀌었고 본체에서 Other의 멤버를 참조할 때 . 연산자 대신 -> 연산자를 사용하면 된다. 그러나 이렇게 하면 Person Young=Boy; 선언문이 암시적으로 호출하는 생성자인 Person(Boy)와 원형이 맞지 않다. 사실 포인터를 취하는 생성자는 복사 생성자로 인정되지도 않는다. 꼭 포인터로 객체를 복사하려면 main의 객체 선언문이 Person Young=&Boy;가 되어야 하는데 그래야 Person 복사 생성자로 Boy의 번지가 전달된다. main 함수까지 같이 수정하면 정상적으로 잘 동작한다.

그러나 이는 일반적인 변수 선언문과 형식이 일치하지 않는다. 기본 타입의 복사 생성문을 보면 int i=j; 라고 하지 int i=&j;라고 선언하지는 않는다. 즉 포인터를 통한 객체 복사 구문은 C 프로그래머가 알고 있는 상식적인 변수 선언문과는 틀리다. 클래스가 기본형과 완전히 같은 자격의 타입이 되려면 int i=j; 식으로 선언할 수 있어야 한다.

그래서 객체 이름에 대해 자동으로 &를 붙이고 함수 내부에서는 전달받은 포인터에 암시적으로 *연산자를 적용하는 레퍼런스라는 것이 필요해졌다. 복사 생성자가 객체의 레퍼런스를 받으면 Young=Boy라고 써도 실제로는 포인터인 &Boy가 전달되어 속도 저하나 무한 호출없이 기본 타입과 똑같은 형식의 선언이 가능하다. 이후 공부하게 될 연산자 오버로딩에도 똑같은 이유로 레퍼런스가 활용된다. C에서는 꼭 필요치 않았던 레퍼런스라는 개념이 C++에서는 필요해진 이유가 객체의 선언문, 연산문을 기본 타입과 완전히 일치시키기 위해서이다.

복사 생성자로 전달되는 인수는 상수일 수도 있고 아닐 수도 있는데 내부에서 읽기만 하므로 개념적으로 상수 속성을 주는 것이 옳다. int i=j; 연산 후 j의 값이 그대로 유지되어야 한다. 결론만 요약하자면 Class 클래스의 복사 생성자 원형은 Class(const Class &)여야 한다.

디폴트 복사 생성자

클래스가 복사 생성자를 정의하지 않으면 컴파일러가 디폴트 복사 생성자를 만든다. 컴파일러가 만드는 디폴트 복사 생성자는 멤버끼리 1:1로 복사함으로써 원본과 완전히 같은 사본을 만들기만 할 뿐 깊은 복사를 하지는 않는다. 만약 디폴트 복사 생성자만으로 충분하다면(Position 클래스의 경우) 굳이 복사 생성자를 따로 정의할 필요는 없다. 이때 만들어지는 디폴트 복사 생성자는 다음과 같을 것이다.

 Position(const Position &Other) {
     x=Other.x;
     y=Other.y;
     ch=Other.ch;
} 

대응되는 멤버끼리 그대로 대입하는데 전부 단순 타입이라 대입만 하면 잘 복사된다. 이런 디폴트 복사 생성자가 있기 때문에 별도의 조치가 없어도 Position There=Here가 잘 동작하는 것이다.

또한 Class A=B; 식의 선언을 하지 않거나 객체를 함수의 인수로 사용할 일이 전혀 없다는 것이 확실하다면 이때도 복사 생성자가 필요없다. 그러나 이런 가정은 무척 위험할 수 있다. 왜냐하면 클래스의 사용자는 클래스가 일반 타입과 동등하므로 int, double에서 가능한 일들은 클래스에 대해서도 모두 가능하다고 기대하며 실제로 그런 코드를 작성하기 때문이다. 이 기대에 부응하기 위해 클래스는 모든 면에서 기본 타입과 완전히 같아야 한다.

Person2 예제에서 복사 생성자를 정의함으로써 Person 클래스는 이미 생성된 객체로부터 새로운 객체를 선언할 수 있게 되었다. Person 클래스가 점점 기본 타입과 같아지고 있지만 이 클래스는 아직까지도 불완전하다. Person 클래스가 완전한 타입이 되려면 대입 연산자를 재정의해야 하는데 이 실습은 다음에 다시 해 보도록 하자.

출처 : www.winapi.co.kr

설정

트랙백

댓글

[C++] Construct method

Programming/C++ Language 2010. 5. 27. 19:38
Resource leak 을 방지하기 위해, 생성자에서 할당(Allocation) 로직을 빼고, 이를 Construct 함수에 넣자. 즉, 생성자에서는 변수들을 초기화를 하고, Construct 함수를 만들어서 Construct 함수에서 할당 로직을 작성하도록 하자는 것이다. 생성자에 할당(Allocation) 로직을 넣었을 경우, 만에 하나 생성자가 실행 중일 때, 프로그램이 죽을 경우 소멸자가 호출되지 않음으로써 Resource leak 이 발생 할 수 있다.

TestClass::TestClass()
{
__abc = 0;
__pStr = "Test";
}

TestClass::Construct()
{
__ptr = new char( 10 );
}

int main()
{
TestClass Test = new TestClass();
Test.Construct();
return 0;
}


 

'Programming > C++ Language' 카테고리의 다른 글

[C++] Errors : cannot allocate an object of abstract type '???'  (0) 2010.06.14
[C++] 복사 생성자  (0) 2010.05.31
[C++] static_cast  (0) 2010.03.22
[C++] const  (0) 2010.03.19
[C++] 다양한 생성자 초기화 방법  (0) 2010.03.19

설정

트랙백

댓글

[C++] static_cast

Programming/C++ Language 2010. 3. 22. 11:03
캐스팅은 기존 C에서도 지원하는 기능이며, 형변환을 할 때 마구잡이로 사용하던 기술이기도 했다. 상식적으로 불가능한것만 아니면 웬만한 변환은 다 지원해버렸기 때문에 마구마구 썼었지.. 별도의 키워드조차 필요치 않고 타입에다 괄호만 사용해주면 되니 사용법도 무지 간단하다.  C++을 배우고 나서도 C++을 위한 별도의 캐스팅 연산자가 있는줄도 몰랐으니 말 다했지 뭐. 

그런데 이건 어떨까. 

class CClass1
{
public:
int m_intTest;
};

class CClass2
{
public:
int m_intTestTmp;
int m_intAdditional;
};


void main()
{
CClass1 cl1;
CClass2* pcl2 = (CClass2*)&cl1;

pcl2->m_intAdditional = 0;
}

이 코드의 경우 컴파일타임에는 전혀 에러가 나지 않는것이 당연하다. C의 캐스팅은 적당한 캐스팅연산자만 써주면 그냥 만사 다 OK거든.. 심지어는 char szTest = (char)"어버버"; 이런거나 int *pintTest = (int*)'a'; 이런 괴상한 코드도 경고 없이 다 OK해버린다. 

암튼 위 코드를 실행하면, 그 결과는 당연히 크래시..... CClass2의 인스턴스가 갖게 될 메모리 사이즈가 CClass1보다 더 클테고, 따라서 CClass2의 포인터로 CClass1의 인스턴스를 액세스하게 되면 메모리크기의 차이때문에 fault가 생기게 되는 것이다. 이것이 C캐스팅의 한계라고 할 수 있다. 물론 미리 코드를 잘 검사해서 저런 코딩을 할 확률을 낮춰줄 수 있으면 좋은데, 그게 어디 쉬운가.. 슬픈 것은 상당수의 C++소스가 이런식으로 되어 있어서 디버깅이 무지 어렵다는 것이다. 물론 나도 단지 귀차니즘을 이유로 저런식의 코딩을 참 많이 했는데.. 일단 지금부터라도 가급적 C++전용의 캐스팅 연산자만 써보려고 한다. 코딩할 때 약간 귀찮은 것이, 디버깅하느라 밤새는 것보다는 훨씬 이익이니까. 

그럼, 포인터끼리 캐스팅을 할 때 가장 문제가 되는 상황이 어떤 것일까? 내 생각에는 대충 두 가지인 것 같다. 

첫째, 아예 다른 클래스끼리의 캐스팅.
둘째, 상속/피상속 관계에 있는 클래스에서, 자식클래스의 포인터가 부모클래스의 인스턴스를 가리키도록 하는 캐스팅. (또는 그 역)

첫째는 웬만하면 그냥 무조건 막으면 되니까 이해하기 쉬우나 둘째의 경우는 좀 더 세밀한 판단(상속/피상속 관계 확인)이 요구됨을 예상할 수 있을 것이다. 

아무튼 첫번째 상황을 체크하기 위해서 main 코드 부분을 다음과 같이 바꿔봤다. 

CClass1 cl1;
CClass2* pcl2 = static_cast<CClass2 *>(&cl1);

pcl2->m_intAdditional = 0;

아까와는 달리, 컴파일 타임에 에러가 발생한다. 

error C2440: 'static_cast' : cannot convert from 'CClass1 *__w64 ' to 'CClass2 *'
        Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

대충 보면, 두 클래스간 관계가 없어서 캐스팅에 실패했다고 한다. C스타일 캐스팅을 사용하든지, reinterpret_cast를 사용하라고 되어 있다. C스타일 캐스트는 말 그대로 C의 캐스팅이니 앞서 본 소스처럼 코딩을 하라는 것이고, reinterpret_cast는 C++에 존재하는 기존 C식 캐스팅이라고 생각하면 될것 같다. (다만 이녀석은 기본타입간 변환은 불가하고, 포인터와 관계된 변환만 제공하므로 분명한 차이가 있기는 하다.) 함수식 캐스팅은.. 음 기회가 되면 다음에.. 

단지 캐스팅 연산자만 바꿨을 뿐인데도, 전혀 다른 클래스를 실수로 캐스팅에 우겨넣는 실수는 하지 않게 되었다. Olleh!

여기까지는 이해하기가 쉬운데, 만약 두 클래스가 부모/자식 관계라면 어떨까? (위에서 살펴 본 두 번째 상황에 해당) 어쨌든 서로 다른 클래스이므로 캐스팅을 거부할 수도 있고, 연관성이 있으니 캐스팅을 허용할 수도 있을 것이다. 백문이 불여일견! 고고싱 

class CParent
{
public:
int m_intTest;
};

class CChild : public CParent
{
public:
int m_intTest2;
int m_intAdditional;
};

void main()
{
CParent clParent;
CChild clChild;

CParent *pclParent; 
CChild *pclChild;

pclParent = &clChild; // OK
pclChild = &clParent; // ERROR
pclChild = static_cast<CChild*>(&clParent); // OK
}

1. 부모클래스의 포인터에서 자식클래스의 인스턴스를 받을 때 : 캐스팅 안해도 OK (당연하지.. 소멸자 문제만 빼면)
2. 자식클래스의 포인터에서 부모클래스의 인스턴스를 받을 때 : 캐스팅 안하면 에러 (역시 당연함.. 자식클래스 고유의 멤버가 사용될 우려가 있음) 
3. 자식클래스의 포인터에서 부모클래스의 인스턴스를 받으면서 static_cast를 사용한경우 : OK 

즉, static_cast의 경우는 자식클래스 포인터에서 부모의 인스턴스를 받아도 경고없이 그냥 허용한다는거다. 아예 다른 타입의 경우 변환을 불허하지만, 상속관계에 있는 클래스의 경우에는 C스타일의 캐스트처럼 그냥 다 허용한다는 것. 결국 부모클래스의 인스턴스로 차일드 클래스의 멤버를 호출하면 문제가 발생하게 되는 것이다. 

그럼 static_cast를 아예 안쓰면 안전하지 않냐고? 인스턴스 관리에 있어서 항상 최하위 클래스의 포인터로만 작업하기 어렵다는 것은 경험상 알고 있을 것이다. (사과, 복숭아, 오이, 마늘 등을 모아놓고, 한큐에 "껍질까기"메서드를 호출하고 싶을 때처럼..) 

과거에 상속이 얽히고 설킨 상황에서 포인터로 멤버 함수를 호출하고자 했을 때 함수 선언을 virtual로 하여 런타임에 인스턴스 종류를 파악하게 하여 메서드 호출에서의 폴트를 피했던 것처럼, 캐스팅과 관련해서도 유사한 방법이 필요하다. 캐스팅 연산자에서는 dynamic_cast가 그런 역할을 하고 있어서, 상속관계에 있는 클래스의 포인터간에 안전한 타입변환을 해주고, 문제의 소지가 있는 형변환에 대해서는 null을 전달하여 오동작을 방지하게 해준다. dynamic_cast에 대해서는 다음 시간에..

아 그럼 여기서 또 질문이 나올 수 있는데. dynamic_cast로 안전하게 상속 클래스간 포인터변환을 할 수 있다면, static_cast가 굳이 상속관계 클래스 포인터간 캐스팅을 지원하는 이유가 뭐냐? 그냥 지원 안해버리면 되는데.... 맞는 말이다. 

이것은 virtual 함수의 특성과 비슷한 답변을 하면 될 것 같다. virtual 함수를 사용하게 되면, 실시간 타입검사(RTTI)가 들어가기 때문에 아무래도 속도상 손해가 있다. 결국 퍼포먼스가 무지막지하게 중요한 소프트웨어라서 RTTI를 빼버리고 컴파일하고 싶거나, 컴파일러가 특이해서 RTTI옵션을 아예 지원하지 않는다면 virtual 함수를 쓸 수 없듯이, dynamic_cast역시 RTTI를 쓸 수 없는 환경에서는 사용할 수가 없다. 이것은 dynamic_cast를 소개할 때 한 번 더 언급하게 될 것 같다. 아무튼 이런 상황에서 만능열쇠와도 같은 (=위험하기 짝이 없는) C스타일 캐스팅보다는 그나마 static_cast를 사용해주는게 차선책이 될 수 있음을 이해할 수 있을 것이다. 이쯤되면 왜 한놈은 스태틱이고 한놈은 다이내믹이라는 이름을 갖고 있는지도 충분히 이해가 될 듯. 

한 가지 재미있는 것은, static_cast의 경우 기본적으로 상속관계가 아닌 클래스끼리는 포인터변환을 할 수 없지만, 중간에 void* 로 한 번 변환을 해두면 자유롭게 캐스팅이 가능하다는 것. 

class CClass1
{
public:
int m_intTest;
};

class CClass2
{
public:
int m_intTest2;
int m_intAdditional;
};

void main()
{
CClass1 *pcl1; 
CClass2 *pcl2;
void *ptemp;

pcl1 = static_cast<CClass1*>(pcl2); // ERROR

ptemp = static_cast<void*>(pcl2); // OK
pcl1 = static_cast<CClass1*>(ptemp); // OK
}

나의 미천한 실력으로 정확한 이유는 알 수 없으나, 모든 클래스를 void를 상속한 걸로 간주하기 때문에 그런것이 아닐까 추측한다. 아니면, 파일 처리나 통신 등에서 serialize를 지원하게 하기 위해서일지도 모르고.. 또는, malloc등의 함수를 통해 고정된 타입으로 메모리주소를 넘겨받는 경우가 있어서 그런지도 모르겠다.  

암튼 이렇게까지 할거면 걍 C캐스팅 쓰는거나 무슨 차이냐 싶을 수도 있지만, 명시적으로 강제 캐스팅이 불가피하다는 것을 보여주기 위해선 나쁘지 않을것 같기도 하다. 

사실 여기서 소개한 것 외에도 몇 가지 특성이 더 있기는 한데.. 나중에 생각나면 추가해 보도록 하겠다.
다음 시간에는 dynamic_cast에 대해서 살펴볼 것이다. 

그리고 끝으로 언급해야 될 것이 한 가지 있다면.. C++에는 오늘 살펴본 static_cast처럼 C++특유의 네 가지 정도의 캐스팅 연산자가 있으며, 앞서 대강 살펴봤듯이 각각의 역할이 뚜렷이 구분되어 있다. 이것은 프로그래머에게 실수를 방지하도록 돕는 것과 동시에, 타인이 소스를 읽어봤을 때 프로그래머의 의중을 쉽게 파악할 수 있도록 하는 역할도 겸한다. 그런데 이런 상황에서 갑자기 중간에 C스타일의 캐스팅이 툭 튀어나온다면? (워3 용어로, 갑툭튀라고 할 수 있겠다) 그동안 쌓아놓은 무결성과 가독성이 와르르 무너지게 되는 것이다. 일반 타입 변환에 static_cast를 쓰라고까지는 못하겠지만(그러나 실제로 이렇게 프로그래밍하는 사람도 많음), 클래스 또는 포인터 변환에서만이라도 C++ 캐스팅 키워드를 반드시 사용해보면 어떨까 싶다. 그리고 이 캐스팅 기법들은 포인터뿐만 아니고 레퍼런스에도 동일한 방식으로 지원을 하고 있으니 레퍼런스 변환에도 적극적으로 사용해보도록 하자. 

출처 : http://poorteam.tistory.com/25

'Programming > C++ Language' 카테고리의 다른 글

[C++] 복사 생성자  (0) 2010.05.31
[C++] Construct method  (0) 2010.05.27
[C++] const  (0) 2010.03.19
[C++] 다양한 생성자 초기화 방법  (0) 2010.03.19
static 클래스 멤버  (0) 2010.03.19

설정

트랙백

댓글

[C++] const

Programming/C++ Language 2010. 3. 19. 14:46
const 함수를 선언/정의 할 때는 아래와 같이 한다.

int getHour() const;
int Time::getHour() const
{
}

아래는 상수 함수와 비상수 함수에서의 상수/비상수 멤버함수를 호출하는 예를 보여준다.

Time wakeUp();
const Time noon();

wakeUp.setHour( 10 );
noon.setHour( 10 );

wekeUp.getHour();
noon.getHour();

wakeUp.printStandard();
noon.printStandard();

위에서 set/print 함수는 비상수 함수이고, get 함수는 상수 함수임을 먼저 알린다.
빨간 부분으로 되어 있는 부분에 컴파일 에러가 생기게 된다. 이유는, 비상수 객체에서는 상수/비상수 함수를 모두 호출 시킬수 있는 반면에, 상수 객체에서는 비상수 함수를 일체 사용할 수 없기 때문에 에러가 뜨고, 실제로 printStandard 함수에서 값을 변경하지 않더라도 확실하게 값을 변화시키지 않는다고 보장을 하지 못하기 때문에 컴파일 에러를 발생한다.


class Increment{
private:
int count;
const int increment;
}

Increment::Increment( int c, int i ) : count( c ), increment( i )
{
}

const 데이터 멤버와 참조되는 데이터 멤버는 멤버 초기값 구문을 사용하여 초기화 하여야 한다. const 변수를 Increment 생성자 스코프 안에서는 초기화를 할 수 없다. 위와 같이 초기화를 하면 생성자 본체 시작전에 선언과 동시에 초기화를 하게 된다.
그리고, Increment 의 부모 클래스가 있는 경우, 부모 클래스의 생성자가 수행되기 전에 멤버 초기값 구문이 실행된다.

'Programming > C++ Language' 카테고리의 다른 글

[C++] Construct method  (0) 2010.05.27
[C++] static_cast  (0) 2010.03.22
[C++] 다양한 생성자 초기화 방법  (0) 2010.03.19
static 클래스 멤버  (0) 2010.03.19
namespace  (0) 2010.03.19

설정

트랙백

댓글

[C++] 다양한 생성자 초기화 방법

Programming/C++ Language 2010. 3. 19. 14:36
디폴트 인수를 사용하는 생성자

class Time{
Time( int = 0, int = 0, int = 0 );
}

Time::Time( int hr, int min, int sec )
{
SetTimer( hr, min, sec );
}

인자가 없는 디폴트 생성자를 호출 하더라도, 일괄된 값으로 초기화되는 것을 보장해 준다.

1. Time t1;
2. Time t2( 2 );
3. Time t3( 21, 34 );
4. Time t4( 12, 25, 42 );

위와 같은 경우에는 t1은 모든 인수가 default 값으로 초기화가 된다. t2는 hr 이 2가 되고, min, sec 는 default 로 사용되고, t3는 sec 만 default 로 사용을 하게 된다. t4 는 모든 값이 지정된 값으로 초기화를 하게 된다.( 여기서 주의할 점은, SetTimer 가 없어도 기본값으로 초기화 되는 것이 아니라, 생성자의 인자에 값이 없더라도 인자값을 초기값으로 넣어주는 것이다. )

아래는 생성자를 또 다른 방법으로 사용할 수 있는 예로, 상수를 초기화 할 때 주로 사용을 한다.

class TIme
{
private:
int count;
const int increment;
}

Time::Time( int c, int i ) : count( c ), increment( i )
{
// Empty body
}


인자로 넘어온 c, i 값을 이용하여, 비상수 멤버와 상수 멤버를 초기화하는 것이다. count( c ), increment( i ) 는 선언과 동시에 초기화가 되는 것으로 Time 생성자 본체가 시작되기 전에 수행이 되게 된다.( 부모 class 가 있는 경우, 부모 class 의 생성자가 호출 되기 전에 값이 초기화 된다. 값 초기화 > 부모 class 생성자 호출 > 자신 생성자 호출 ) 상수인 increment 는 Time 본체 안에서는 초기화 될 수 없고, 위와 같은 방법으로 초기화 될 수 있다.

'Programming > C++ Language' 카테고리의 다른 글

[C++] static_cast  (0) 2010.03.22
[C++] const  (0) 2010.03.19
static 클래스 멤버  (0) 2010.03.19
namespace  (0) 2010.03.19
연산자 오버로딩  (0) 2009.09.30

설정

트랙백

댓글

static 클래스 멤버

Programming/C++ Language 2010. 3. 19. 14:21
클래스의 각 객체는 자신만의 클래스 데이터 멤버의 복사본을 갖는다. static 클래스 변수는 한 변수에 대해 하나의 복사본만 만들어져 클래스의 모든 객체가 공유한다. 즉, static 클래스 변수는 클래스전체 에 사용되는 정보를 나타낸다. 클래스 스코프를 갖는다고 말할수 있다.
스타크래프트를 예로 들었을 때, Terran Unit 이라는 상위 클래스가 있고, 자식 클래스가 Marine, Madic 이 있다고 했을 때, 테란의 전체 유닛의 수를 알기 위해서는 Terran Unit 에 static int nUnitNum; 이 있으면, 테란의 Marine 이나 Madic 이 생성/소멸 때 마다 소멸자에서 nUnitNum 을 증가/감소 시켜주면 테란의 총 유닛의 수를 알수 있다. 또는 Supply 라는 클래스에 int static nSuplNum 을 두고, Supply 가 생성/파괴 될 때 마다, 이 값을 증가/감소 시켜주면, 총 Supply 개수를 알수 있게 된다.
이와 같이, 데이터가 하나의 복사본만으로 충분하다면, static 데이터 멤버를 사용하여 메모리 공간을 절약할 수 있다.

static 멤버는 public, protected, private 로 선언될 수 있으며, 해당 클래스 정의의 몸체가 아닌 파일 스코프에서 반드시 단 한 번만 초기화되어야 한다. 클래스의 private / protected static 멤버는 그 클래스의 public 멤버 함수나 friend 를 통해서 접근되어야 한다. 클래스의 객체가 없을 때에도 접근이 가능한데 이는 클래스이름::변수이름 의 형식으로 접근 가능하다.

-- .h --
class Employee{
public:
static int getCount();
private:
static int count();
}

-- .cpp --
#include .h
int Employee::count = 0; // static 멤버의 정의와 초기화

int Employee::getCount()
{
return count;
}

'Programming > C++ Language' 카테고리의 다른 글

[C++] const  (0) 2010.03.19
[C++] 다양한 생성자 초기화 방법  (0) 2010.03.19
namespace  (0) 2010.03.19
연산자 오버로딩  (0) 2009.09.30
순수 가상 함수  (0) 2009.09.26

설정

트랙백

댓글

namespace

Programming/C++ Language 2010. 3. 19. 13:35
큰 프로젝트를 진행하다 보면, 함수나 변수의 이름의 충돌이 일어나는 경우가 많다. 경우에 따라서 prefix를 추가하여 충돌이 발생하지 않는 노력을 할 수는 있지만, 확실하게 방지할 수는 없다. 그래서 namespace 를 사용하여 명칭의 충돌을 막는다.

#include <Turboc.h>

 int i;                  // 전역 네임 스페이스 소속

namespace A {
     int i;             // A 소속
}

 

void func()
{
     int i;
     i=1;              // 지역변수 i
     ::i=2;            // 전역 네임 스페이스의 i
     A::i=3;          // A 네임 스페이스의 i

}

void main()
{
     func();
}


위와 같이 사용을 하면 한 namespace 안에서 같은 이름의 변수가 있더라도, 각 영역의 변수를 가져와서 사용을 해서 중복을 막 을 수 있다.

namespace 의 규칙
1. 네임 스페이스는 반드시 전역 영역에 선언해야 한다. 함수안에 선언할 수 없다는 뜻이며 다음과 같은 지역 네임 스페이스는 허가되지 않는다.
2. 네임 스페이스끼리 중첩 가능하다.
3. 네임 스페이스는 항상 개방되어 있다. 그래서 같은 네임 스페이스를 여러 번 나누어 명칭을 선언할 수 있다. 꼭 한꺼번에 몰아서 네임 스페이스내의 모든 명칭을 일괄 선언해야 하는 것은 아니다.

 namespace A {
     double i;
}

namespace B {
     int i;
}

namespace A {
     char name[32];
}

4. 여러 개의 모듈로 나누어진 프로젝트를 개발할 때는 보통 헤더 파일과 구현 파일을 따로 작성한다. 네임 스페이스안에 함수를 정의할 때 헤더 파일에 원형만 선언하고 구현 파일에 함수의 본체를 작성한다.


using 지시자
- namespace에 있는 변수나 함수를 ㅁ사용 할 때, 매번 namespace 를 쓰고, 변수나 함수이름을 써야되서 번거로움이 있는데, using 지시자를 써서 이를 생략 할 수 있다.
- using 지시자가 영향을 미치는 범위는 이 지시자가 있는 영역에 국한된다. 특정 함수나 블록 안에 using 지시자를 사용하면 이 블록에서만 지정한 명칭을 바로 사용할 수 있으며 그외의 영역에서는 여전히 소속 지정이 필요하다. 다음 코드를 보자.

'Programming > C++ Language' 카테고리의 다른 글

[C++] 다양한 생성자 초기화 방법  (0) 2010.03.19
static 클래스 멤버  (0) 2010.03.19
연산자 오버로딩  (0) 2009.09.30
순수 가상 함수  (0) 2009.09.26
가상 함수의 활용  (0) 2009.09.25

설정

트랙백

댓글

연산자 오버로딩

Programming/C++ Language 2009. 9. 30. 18:29
연산자 오버로딩을 할 때는, 연산자 오버로딩 함수가 멤버함수 나 비멤버 함수가 될 수 있다.
멤버 함수 라면, 가장 왼쪽의 피연산자( 혹은 하나뿐인 ) 가 반드시 연산자 클래스의 객체( 또는 개체의 참조 ) 가 되어야 한다.
비멤버 함수 일 경우에는, 왼쪽의 피연산자가 반드시 내장 타입이거나 다른 클래스의 객체가 되어야 한다. 그러나 이 함수가 private 나 protected 멤버에 직접 접근한다면 비멤버함수는 friend 함수로 구현 되어야 한다. 비멤버 함수를 사용하는 다른 이유는, long int 타입과 huge integer 타입 클래스를 더한 다고 했을 때, 이 때 사용할 임시 huge integer 객체를 생성한다. 이 처럼 덧셈 연산자의 피연산항을 교환할 수 있도록 만들 필요가 있다.
get , set 함수가 클래스의 public 인터페이스에 있다면, 비멤버 함수가 반드시 friend가 될 필요는 없다.( 대신, 함수 호출에 따른 부하가 있다 )

const test operator + ( const test& ) const;

const test : 리턴 타입
opertor + : 함수이름
const test& : 인수 = 피연산자
const : 상수 함수


ex 1 ) <<, >> 연산자 오버로딩
class PhoneNumber {
friend ostream &operator<<( ostream&, const PhoneNumber &);
friend istream &operator>>( istream&, PhoneNumber & );
}

cin >> phone;
cout << phone << endl;

연산자 오버로딩 정의 및 구현시에 문법은 위와 같다. cin >> phone; 를 컴파일러는 operator >> ( cin, phone ); 와 같은 비멤버 함수 호출을 생성한다. 반환값을 ostream&, istream&로 하는 이유는, 두번째 cout 구문을 보면, 먼저 operator <<( cout, phone ) 을 호출하고 난 뒤에, ostream 이 반환되서 다시 operator << ( cout, endl ) 이 호출되어 << 와 >> 을 연속적으로 사용 가능 하게 해준다.


ex 2 ) 단항 연산자 오버로딩
class String {
friend bool operator!( const String & );
}

class String {
public:
    bool operator! () const;
}

( s 는 String 객체 )
멤버 함수가 선언 되어 있을 때는, !s 는 s.operator!(); 호출이 된다.
friend 함수로 선언 되어 있을 때, !s 는 operator!(s) 가 된다.


ex 3 ) 이항 연산자 오버로딩

이항 연산자 오버로딩은 1. 하나의 인수를 갖고, static이 아닌 멤버함수 2. 두 개의 인수( 단, 하나는 반드시 클래스 객체 or 클래스 객체의 참조 )를 갖는 비멤버 함수로 오버로딩 될 수 있다.

1 번 의 경우
public String {
public:
     const String &operator+=( const String & );
}

y += z 는 y.operator( z ) 가 된다.

2 번의 경우
class String {
     friend const String &operator+=( String &, const String & );
}

y += z 는 operator+=( y, z ) 가 된다.


ex )
const Array& Array::operator=( const Array& right )
{
   return *this; // x = y = z; 반복적인 호출 가능하도록
}

int& Array::operator[]( int subscript )
const int& Array::operator[]( int subscript ) const

여기서 위의 [] 연산자는 참조를 반환해서 lvalue 를 생성 하는 것이고,
아래의 [] 연산자는 상수 참조 반환으로 rvalue 를 생성한다. 여기서 주목 할 것은 붉은색 const 이다.
이는 상수 함수를 뜻하는 것으로, 단일 연산자일 경우 a[1] 라고 코딩 했을때 이는 a.operator[]( 1 ) 이 될 것인데 여기서 a 는 상수 이기 때문에 상수 함수로 취할 필요가 있다. 물론 이항 연산자일 경우에는 필요 없을 것이다.

'Programming > C++ Language' 카테고리의 다른 글

static 클래스 멤버  (0) 2010.03.19
namespace  (0) 2010.03.19
순수 가상 함수  (0) 2009.09.26
가상 함수의 활용  (0) 2009.09.25
가상 함수에 대한 이해  (0) 2009.09.25

설정

트랙백

댓글

순수 가상 함수

Programming/C++ Language 2009. 9. 26. 00:12
순수 가상 함수 는 자바의 Interface와 같은 것이다. 문법은 간단히 =0 만 붙여 주면된다. virtual void Draw()=0; 여기서 함수의 동작을 구현해주고 싶으면 헤더파일에서 virtual voidi Draw()=0{ test(); } 로 구현을 해주면 된다. 이 때, 동작은 Interface를 상속받아 재정의를 한 Draw를 호출할 경우, 항상 먼저, Interface에 구현 되어 있는 test() 를 먼저 호출하고 난 뒤에 재정의된 Draw() 함수 코드가 수행 된다는 것이다.
그리고 순수 가상 함수를 포함하는 클래스는 추상 클래스 이므로 인스턴스를 생성 할 수 없다. 단, 포인터형 변수는 선언 할 수 있다.

'Programming > C++ Language' 카테고리의 다른 글

namespace  (0) 2010.03.19
연산자 오버로딩  (0) 2009.09.30
가상 함수의 활용  (0) 2009.09.25
가상 함수에 대한 이해  (0) 2009.09.25
Static Member  (0) 2009.05.31

설정

트랙백

댓글