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 |
Byte Order (0) | 2011.08.13 |
ASCII Code Table (0) | 2011.08.13 |
ACE 란 무엇인가? (0) | 2011.08.13 |