c++에서 아주 중요한 class에 대해서 공부하고 있군요..
질문자는 class가 내부적으로 어떻게 돌아가는지 잘 모르시는 것 같습니다..
사실 c++이라는게 워낙에 프로그래머가 모르는 일을 많이 해서 직관적으로 다가오지 않는게
사실입니다..
저 역시 class를 공부했을때 질문자처럼 답답함을 느꼈죠..
이 내용은 저에게 많은 가르침을 주셨던 분에게 배웠던 내용입니다..
어설프게 설명하여 그 분의 이름에 먹칠이나 하지 않을까 걱정이지만 그래도 설명을 해보겠습니다..^^
(이해를 못했다면 제가 부족한탓임..^^;;)

 

흔희들 c++를 객체지향 c언어를 절차적 언어라고 하는데요..
아이러니하게도 c++의 클래스는 c언어로 표현할 때 직관적으로 이해할 수 있습니다..
(질문자가 c언어를 모를수도 있기때문에 쉽게 구조체로 표현한다고 생각하세요..
 또한 printf를 모를수도 있기때문에 cout으로 표현하겠습니다..)

 

1.생성자와 소멸자..
아래는 클래스로 표현한 간단한 생성자와 소멸자 예제입니다..

 

class CTest
{
public:
 CTest()
 {
  cout << "생성자" << endl;
 }
 ~CTest()
 {
  cout << "소멸자" << endl;
 }
 
 int i;
};

void main()
{
 CTest *test;

 test = new CTest;

 delete test;
}

 

출력내용은 "생성자" "소멸자" 입니다..
위의 예제로 보면 new를 할때 생성자가 호출되고 delete를 할 때 소멸자가 호출된다는것은 알겠지만
직관적이지 못합니다..
저 내용을 c언어로 표현하면 아래와 같습니다..

 

//*
typedef struct CTest
{
 int i;
}CTest; //*/

 

void CTest_Init(CTest* test)
{
 cout << "생성자" << endl;
}
void CTest_Clear(CTest* test)
{
 cout << "소멸자" << endl;
}

 

void main()
{
 CTest *test;

 test = (CTest*)malloc(sizeof(CTest));
 CTest_Init(test);

 CTest_Clear(test);
 free(test);

 getch();
}

 

보시면 알겠지만 c언어로 표현을 하니까 생성자와 소멸자가 무엇인지 직관적으로 보입니다..
c++에서 new를 한다는것은 malloc 후에 생성자 함수를 호출하는것이고 delete를 한다는것은
소멸자함수를 호출한후에 free를 한다는것을 알 수 있습니다..
new,delete와 c언어와의 관계를 보기쉽게 아래와같이 표현했습니다..

new  ==  malloc() -> Init();호출
delete == Clear();호출 -> free()

즉 c++에서의 객체생성은 저와같은 과정을 생략하고 있었던 것입니다..


2. 상속
아래는 클래스를 이용한 간단한 상속 예제입니다..

 

class A
{
public:
 A()
 {
  cout << "어미 생성됐다.." << endl;
 }
 ~A()
 {
  cout << "어미 소멸됐다.." << endl;
 }
 
 int i;
};

 

 

class B : public A
{
public:
 B()
 {
  cout << "자식 생성됐다.." << endl;
 }
 ~B()
 {
  cout << "자식 소멸됐다.." << endl;
 }

 char j;
};


void main()
{
 B *b;

 b = new B;
 delete b;

 getch();
}


실행을 해보면 생성자와 소멸자의 호출순서가 흥미롭습니다..
어미->자식->자식->어미 이런식으로 생성자와 소멸자가 호출됨을 알 수 있습니다..
하지만 이러한 내용은 new와 delete에서 내부적으로 지가 알아서 해버리기때문에
프로그래머한테 직관적으로 다가오지 않습니다..
c언어로 표현을 해보죠..


typedef struct A
{
 int i;
}A;

 

void A_Init(A* a)
{
 cout << "어미 생성" << endl;
}
void A_Clear(A* a)
{
 cout << "어미 소멸" << endl;
}

 

 

typedef struct B
{
 A parent;        // 이게 상속의 정체다..
 char j;
}B;

 

void B_Init(B* b)
{
 cout << "자식 생성" << endl;
}
void B_Clear(B* b)
{
 cout << "자식 소멸" << endl;
}

 

void main()
{
 B *b;

 b = (B*)malloc(sizeof(B));
 
A_Init(&b->parent);
 B_Init(b);


 B_Clear(b);
 A_Clear(&b->parent);
 free(b);

 getch();
}


B구조체에서 상속을 어떻게 표현했는지 잘 보세요..
바로 A객체를 포함함으로써 상속이 표현됐음을 알 수 있습니다.. 이게 바로 c++에서 상속의 정체죠..
또한 B객체를 생성했을때 생성자와 소멸자의 호출 역시 c언어에서 더욱더 직관적으로 알 수
있게 되었습니다..
상속된 객체를 new나 delete를 했을경우 c++내부에서는 아래와같이 진행이 된다는 것이죠..

 

new == malloc() -> 어미생성자함수호출 -> 자식생성자함수호출
delete ==  자식소멸자함수호출 -> 어미소멸자함수호출 -> free()

 

자 여기에서 질문자의 마지막 질문의 의문이 풀렸습니다..

 

class A
class B :public A 일때

A *a = new B;

 

당연히 되겠죠?? B구조체에서 봤던것처럼 B는 내부적으로 낮은주소에 A객체를 포함하고 있기때문에
A포인터형으로 B를 가리키게 된다고 해도 유효한 영역이므로 가능한것입니다..

 

A포인터 -->  [A]|j
B포인터 -->  [A]|j

 

둘다 상관없다는 말입니다.. 그런데 아래와같이 B구조체를 만들었다고해봅시다..

 

tyepdef struct B
{
   char j;
   A parent;
}

 

이건 상속이 아니고 위임입니다.. 상속은 항상 상속의 대상이되는 객체가 구조체의 맨위에 위치해야합니다.

 

A포인터 -->  j|[A]
B포인터 -->  j|[A]

 

위와같이 A포인터가 j영역을 가리키고있기때문에 성립이 안되는거죠..
이제 확실히 상속이 무엇인지 알겠죠??


3. 다중상속의 사기성..
A,B,C클래스가 있고 C클래스가 A,B를 상속받았다고 합시다..
그런데 이것을 구조체로 표현하면 아래와 같을것입니다..

 

tyepdef struct C
{
   A parent1;
   B parent2;
   .
   .
   .
}C;

 

메모리 배치는 아래와같이 되겠죠..


[A][B]...

 

그런데 이상하죠..?? 분명 C는 A,B를 둘다 상속했으므로 A,B포인터로 다 표현가능해야합니다..
그런데 c언어에서 보니까 아래와같은 문제가 생기게됩니다..

 

A포인터 --> [A][B]...
B포인터 --> [A][B]...

 

A포인터로 C를 가리키면 문제가 안되지만 B포인터로 C를 가리키면 문제가됩니다..
왜냐하면 B포인터는 B영역이 아닌 A영역을 가리킬테니까여..
그래서 c++에서는 프로그래머가 예상치못한 과부하가 걸리는 어떤 조작을 내부적으로 해버립니다..
아래와같이 말이죠..

 

A포인터 --> [A][B];
B포인터 --> c++내부조작 --> [B][A];

 

c++을 처음공부하는 프로그래머라면은 이런현상을 알 수 없습니다..
왜냐하면 내부적으로 프로그래머 모르게 처리해버리니까요..
즉 어설프게 c++를 했다가는 끊임없이 프로그래머를 속이는 컴파일러에 당할수 밖에 없다는 거죠..

다른예를 들어보도록하죠.. 다이아몬드형태의 상속도 문제가 됩니다..
아래와같은 상속이 있다고 해보죠..

 

A <--  B   <--
  <--  C   <-- D

 

위의 상속계통도는 B와 C는 A를 상속받았고 D는 다시 B,C 모두에게 다중상속을 받았습니다..
그러면 이것을 c언어로 표현한다면 아래와같이 되겠죠..

 

struct B
{
   A parent;
   int b
};

struct C
{
   A parent;
   int c
};

struct D
{
   B parent1;
   C parent2;
   int d
};

 

왜 이것이 문제가 될까요?? 이것을 메모리배치로 표현을 하면 아래와 같습니다..

 

D포인터  -->  [[A][b]] [[A][c]] [d]

 

앞에A는 B의 A이고 뒤에 A는 C의 A입니다.. 그런데 만약 상속받은 A객체의 멤버에 접근한다면
어느 A멤버에 접근해야 하죠?? 앞에 A?? 뒤에 A??
이건 큰 문제죠.. 그래서 저런 다이아몬드 상속의 문제점을 해결하기 위해 c++에서는 상속에 virtual 키워드를
사용하게 만들어놓았습니다.. 아래와같이 말이죠..

 

class B : virtual public A
{
};

class C : virtual public A
{
};

class D : public B, public C
{
};

 

아무리 virtual키워드를 사용하여 상속을 한다고 하지만 c++이 내부적으로 과부하를 일으킨다는것은
너무도 뻔합니다..
결과적으로 다중상속은 "별로좋지않다" 라는것입니다.. 다중상속을 많이 할 수록 문제점은 기하급수적으로 늘어만 가죠..
때문에 자바와 c#에서는 문제가 많은 다중상속을 지원하지 않습니다..
대신 인터페이스라는 개념을 도입하여 다중상속의 문제점을 해결하고 있죠..(인터페이스는 c++에서 안나오므로 설명생략..)
하지만 c++에서는 그러한 개념이 없기때문에 조심해서 다중상속을 사용하면 됩니다.. ㅡㅡ;

 

4. 가상함수..
이제 질문자가 궁금해하고 있는 가상함수부분에 대해서 설명하도록 하죠..
아래는 간단한 가상함수의 예입니다..

class A
{
public:
 A()
 {
  i = 100;
 }
 ~A()
 {
 }

 virtual void Print()
 {
  cout << i << endl;
 }

 int i;
};

 

 

class B : public A
{
public:
 B()
 {
  ch = 'a';
 }
 ~B()
 {
 }

 virtual void Print()
 {
  cout << ch << endl;
 }

 char ch;
};

 

 

void main()
{
 A *a = new A;
 A *b = new B;

 a->Print();
 b->Print();

 delete a;
 delete b;

 getch();
}

 

실행해보면 알겠지만 a->Print()와 b->Print()는 서로 다른 메소드입니다..
도대체 virtual이 뭐길래 실행도중에 저렇게 Print가 바뀌는것일까요???
c언어로 가상함수를 구현해보면 쉽게 알 수 있습니다..
c언어에서는 함수포인터를 사용하여 가상함수를 구현합니다..
위의 예제를 c언어로 구현해보면 아래와같습니다..


struct A;
typedef void (*Print)(A* a);        // 함수포인터..

 

 

 

typedef struct A
{
 Print print;
 int i;
}A;

 

void A_Init(A* a)
{
 a->i = 100;
}
void A_Clear(A* a)
{
}
void A_Print(A* a)
{
 cout << a->i << endl;
}

 

 


typedef struct B
{
 A parent;
 char ch;
}B;

 

void B_Init(B* a)
{
 a->ch = 'a';
}
void B_Clear(B* a)
{
}
void B_Print(A* a)
{
 cout << ((B*)a)->ch << endl;
}

 

 

void main()
{
 A *a,*b;

 

 a = (A*) malloc (sizeof(A));
 a->print = A_Print;           // 이것이 가상함수의 정체다..
 A_Init(a);

 

 b = (A*) malloc (sizeof(B));
 b->print = B_Print;           // 가상함수세팅..
 A_Init(a);                    // 상속받았으므로 부모부터 생성자호출..
 B_Init((B*)b);               
// 자식생성자 호출..

 

 a->print(a);
 b->print((A*)b);

 

 B_Clear((B*)b);
 A_Clear(a);
 free(b);

 

 A_Clear(a);
 free(a);

 getch();
}

결과는 클래스로 구현했을때와 같습니다.. 즉 각각의 함수는 메모리가 할당되는 순간
배치되어 실시간적으로 Print메소드가 서로 다르게 구현되었던 것이었습니다..
c++에서는 가상함수테이블이 라는것이 있어서 A_Print,B_Print의 함수포인터를 테이블에 저장해놓습니다..
그리고 클래스의 메모리가 생성될때 함수포인터를 테이블에서 읽어와 위에처럼 세팅하죠..
new 를 사용하다보면 뒤에 클래스명 적어주죠?? 바로 그 클래스명을 해쉬코드값으로 하여 해당 테이블을 찾아
함수포인터를 세팅하는것입니다..
아래와 같이 말이죠..

 

new == malloc실행 -> 구조체명을 해쉬코드로 하여 함수포인터를 얻어와세팅  -> 부모생성자함수호출 -> 자식생성자함수호출
delete == 자식소멸자함수호출 -> 부모소멸자함수호출 -> free실행

 

c++에서는 위의 과정이 new명령어의 내부적으로 실행되기때문에 프로그래머는 알 수 없습니다..
그러고보면 new나 delete가 상당히 많은 일을 합니다..^^

 

5. 순수가상함수..
위의 예제에서 A클래스가 아래와 같이 추상클래스라고 해봅시다..


class A
{
public:
 A()
 {
 }
 ~A()
 {
 }

 virtual void Print() = 0;        // 순수가상함수..
};

 

이때 A를 new로 메모리생성하게 되면 함수포인터를 얻어와 세팅되는 부분이 생략됩니다..
c언어 입장에서 보면 malloc 후에 a->print = NULL; 로 세팅한다는 얘기죠..
때문에 a->print(a); 를 실행할 수가 없습니다.. (NULL은 실행못하니까여..)

 

typedef struct A
{
 Print print;
}A;

 

위와같이 Print 의 포인터함수 선언만 있을뿐 실제로 print에 아무런 할당을 안하기때문에
추상클래스는 인스턴스를 생성할 수 없는것입니다..
물론 A_Print함수도 만들필요가 없기때문에 가상함수테이블에 저장되지도 않겠죠..

 


위의 5가지만 알아도 클래스에 대해서 많은 부분을 안것입니다..
c언어로 클래스를 표현하면 클래스의 여러가지 개념에 대해 쉽게 알수 있다는사실을 알았을것입니다..
때문에 c++을 먼저공부하는것보다는 c언어 공부후 c++를 하라고 권장하는것입니다..
c언어와 c++의 차이는 불편함과 편리함의 차이지 c언어 역시 객체지향적으로 얼마든지 표현 가능하거든요..

 

클래스에 대해서 자신감이 붙었다면 디자인패턴에 대해서 공부하세요.. 디자인패턴은 클래스설계에 관한 내용이거든요..
디자인패턴을 공부해야 비로소 클래스의 진가를 알게 됩니다..

 

출처 : 네이버 지식인

http://kin.naver.com/browse/db_detail.php?d1id=1&dir_id=10104&eid=GN9zYf%2F9tkD3UgcTC2aRj4z3LGiyQnB6

'Development > C/C++' 카테고리의 다른 글

CDatabase 을 사용한 엑셀 데이터 수정  (0) 2011.08.13
cast 연산자  (0) 2011.08.13
c++ 추상 기초클래스  (0) 2011.08.13
Byte Order  (0) 2011.08.13
ASCII Code Table  (0) 2011.08.13
ACE 란 무엇인가?  (0) 2011.08.13
안정적인 DNS서비스 DNSEver DNS server, DNS service
Posted by 키르히

댓글을 달아 주세요