[C++강좌40] 소멸자의 개념...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/07 12:07:38
수 정 일 :
크 기 : 9.4K
조회횟수 : 16
[C++ 강좌40] -> 소멸자의 개념
< 98.02.07 >
9. 직접 소멸자를 호출한다.
- 직접 소멸자를 호출하는 방법에는 두 가지가 있다. 한 가
지는 호출하고자 하는 소멸자의 한정된 이름을 사용하는
경우이고 다른 한 가지는 delete연산자를 사용 하는 것이
다.
class A
{
public :
~A();
};
class B : public A;
void func( void )
{
B b;
B * bP = new B;
b.A::~A(); // A 클래스의 소멸자 호출
delete bP; // (*bP) 의 소멸자 호출
} // bP 가 소멸된다.
// b 오브젝트의 소멸자 호출
b.A::~A() 가 뜻하는 것은 b 오브젝트가 계승한 A 클래스
의 소멸자를 b 오브젝트 내의 A 클래스를 대상으로 호출
한다는 뜻이고 이때 A 클래스의 소멸자가 받는 this 포인
터는 b 클래스의 lay-out 에서 A 클래스 부분의 시작 주
소이다. delete bP; 가 의미하는 것은 new로 받은 B 클래
스 오브젝트의 주소를 대상으로 B클래스의 소멸자를 호출
한다는 뜻이고 연산자 new 와 delete 를 공부할 때에 좀
더 자세히 다루겠다.
함수 func 의 스코프가 끝나는 곳에 그 스코프에서 정의
된 오브젝트들의 소멸자가 호출되게 된다. 우선 bP 는 포
인터이고 함수 스코프가 끝나는 곳에서 소멸된다. 그런데
b 오브젝트의 경우, 위에서 b 오브젝트 내 A 클래스의 소
멸자를 호출 했지만 다시 이 곳에서 B 클래스와 상위 클
래스의 소멸자들이 정해진 순서대로 호출되게 되며 A::~A
() 또한 호출되게 된다. 주의할 점은 한번 한 오브젝트의
소멸자를 직접 호출했다 하더라도 그 오브젝트가 스코프
를 벗어나는 지점에서 다시 컴파일러에 의해 호출된다는
점이다. 흔히 있는 경우는 아니나 만일 소멸자가 두 번
호출되면 엉뚱한 동작을 하게되는 식의 소멸자는 만들지
말자. 생성자의 경우에는 어떤 오브젝트든지 오직 한 번
만이 호출되게 되지만 소멸자는 꼭 그렇질 않고 몇 번이
던 호출될 수도 있다. 예를 들어 화일 핸들을 내부에 가
지고 있고 소멸자가 호출되면 내부 버퍼의 내용을 화일로
출력하고 그 화일을 닫는 소멸자가 있을 때에 만일 직접
소멸자가 불러 화일을 갱신하고 닫은 후에 다른 화일들을
열었다면 아마도 방금전에 닫은 화일 핸들이 돌아올 것이
다. 이때 이미 직접 소멸시킨 소멸자가 스코프가 끝남에
따라 컴파일러에 의해 다시 호출되게 되어 다른 오브젝트
가 사용하는 핸들에 쓰레기가 된 버퍼의 값을 출력하고
닫아 버리는 사태가 일어날 수도 있는 것이고 프로그램에
따라 더 미묘한 경우가 발생할 수도 있다. 그런 경우를
대피한 즉 포인터나 화일 핸들 등 다른 오브젝트와 공유
될 수 있는 자원을 가진 클래스의 소멸자는 여러번 호출
되어도 '튼튼하게' 대처하는 소멸자가 필요하다.
- 예제 #3 을 실행시켜 보자.
( 예제 #3 )
#include
#include
unsigned Stack; // 오브젝트가 스택에 형성되는 지를 본다.
class String
{
public :
char * string;
int length;
String() : string(0), length(0) {}; // 디폴트 생성자
String( char * );
String( const String & ); // 카피 생성자
~String(); // 소멸자
friend ostream & operator <<( ostream & os, String & str )
{
os << str.string;
return os;
}
};
String::String( char * str )
{
cout << "constructor called [ this = " << this
<< " ] with " << str << endl;
length = strlen( str );
string = new char[ length + 1 ];
strcpy( string, str );
}
String::String( const String & ost )
{
if( (unsigned)this > Stack ) // 스택에서 형성된 오브젝트이면
cout << "[STACK] copy [this = " << this << " ] from ["
<< (void *)&ost << " ]" << endl;
else
cout << "copy-constr called [ this = " << this
<< " ] with " << ost.string << endl;
length = ost.length;
string = new char[ length + 1];
strcpy( string, ost.string );
}
String::~String()
{
if( (unsigned)this > Stack )
cout << "[STACK] destory [this = " << this << " ] " << endl;
else
cout << "destructor called [ this = " << this
<< " ] with " << string << endl;
if( string ) delete string;
string = 0;
}
String dos2unix( String str )
{
for( int i = 0; str.string[i]; i++ )
if( str.string[i] == '\\\\' ) str.string[i] = '/';
return str;
}
void beforeEntry() // 전역 오브젝트가 생성되기 전에
{
cout << endl << "#Before creating global objects" << endl << endl;
}
#pragma startup beforeEntry 31
void afterEntry() // 전역 오브젝트가 생성된 후에
{
cout << endl << "#After creating global objects" << endl;
}
#pragma startup afterEntry 33
String dos("c:\\\\lang\\\\bc\\\\bin");
String not("declared next to dos object"); // 아무일도 않는다.
void main()
{
extern unsigned int _stklen; // <dos.h> 에 선언되어있다.
Stack = _SP - _stklen; // 대충 스택임을 알 수가 있게
cout << '{' << endl;
{
String unix = dos2unix( dos );
unix.String::~String(); // 직접 소멸자 호출
} // unix 오브젝트가 소멸된다.
cout << '}' << endl;
cout << "unix path = " << dos2unix( dos ) << endl;
}
void beforeExit() // 전역 오브젝트가 소멸되기 전에
{
cout << endl << "#Before destroy global objects" << endl << endl;
}
#pragma exit beforeExit 33
void afterExit() // 전역 오브젝트가 소멸된 후에
{
cout << endl << "#After destroy global objects";
}
#pragma exit afterExit 31
예제는 일반적으로 소멸자와 생성자가 호출되는 몇 가지
규칙을 잘 보여 주고 있다. 전역 오브젝트의 경우 정의된
순서에 의해서 호출되고, 그 역순으로 소멸된다는 사실을
나타낸다. #pragma startup 과 #pragma exit 함수들에 준
우선 순위 31, 33 이 오브젝트들이 생성되는 전후와 소멸
되는 전후에 각각 4 개의 함수가 호출되도록 프로그래밍
되었다. 전역 오브젝트의 생성과 소멸의 우선 순위는 32
이고 #pragma startup 의 경우는 낮은 번호의 우선 순위
에서 높은 번호로, #pragma exit 의 경우는 그 역순으로
스타트 업 코드와 종료 코드에 의해서 호출되는 것을 예
제로 정확히 확인할 수 있다. 만일 컴파일러가 틀려서 메
시지 순서가 틀리게 출력된다면 우선 순위를 조정하면서
자신이 사용하는 컴파일러에서의 전역 오브젝트 생성과
소멸의 우선 순위를 알아두도록 하자.
스타트 업 코드와 exit 코드에 익숙하지 않다면 LIB\\STAR
TUP 디렉토리의 c0.asm 을 분석해 두는 것도 좋겠다.
10. 임시 오브젝트와 소멸자
- 카피 콘스트럭터를 생성자 강좌에서 공부할 때에 pass by
value 에 의해서 인자를 받고 리턴 할 때에 두 번의 임시
오브젝트가 생성된다는 것을 배웠다. 그때는 소멸자에 대
한 언급이 없었으나 실제로 생성자가 호출된 임시 오브젝
트가 소멸할 때에도 반드시 소멸자가 컴파일러에 의해 호
출되게 된다.
- 예제 #3에서 한 화면 가득하게 문자열들이 출력되게 된다.
생성자가 호출되고 소멸자가 호출되는 관계가 나열될 것
이다. 그 예제에는 여러 가지 중요한 소멸자 호출 법칙이
들어 있다. 우선 pass by value 로 오브젝트를 인자로 받
을 때에 컴파일러가 임시 영역으로 오브젝트를 카피 콘스
트럭터를 이용하여 카피하고 임시 오브젝트를 함수로 넘
겨주며 그 임시 오브젝트는 인자를 받은 함수가 종료할
때에 소멸되게 된다. 또 return by value 로 오브젝트를
리턴 할 때에는 리턴될 주소를 컴파일러가 함수가 호출될
때에 미리 넘겨주면 함수는 종료할 때에 카피 콘스트럭터
를 이용하여 미리 받은 주소로 리턴 될 오브젝트를 카피
하게 된다. 이때 예제 #3 의 예에서
{
String unix = dos2unix( dos );
unix.String::~String();
}
dos2unix 가 내부적으로 전달 받는 리턴 할 곳의 String
오브젝트의 주소는 unix 오브젝트의 주소이다. 결국 위의
{} 문안에서는
카피 콘스트럭터 ( dos -> pass by value 의 임시 영역 )
카피 콘스트럭터 ( 임시 영역 -> unix )
소멸자 ( 임시 오브젝트 )
소멸자 ( unix , 직접 호출 )
소멸자 ( unix , 컴파일러 호출 )
의 순서로 String 클래스의 생성자와 소멸자가 호출되게
된다. 생성자 강좌에서 말했듯이 unix 오브젝트를 생성시
키는 데에 많은 함수가 호출되게 된다. OOP 의 단점인 수
행 속도가 떨어지고 코드가 커지는 점이 확연히 나타나는
곳이 바로 생성자와 소멸자가 호출되는 부분이다. 값에
의한 호출과 리턴은 꼭 필요한 경우가 아니면 참조에 의
한 호출과 리턴으로 대체하자. 이때 예제 #3 의 예에서
cout << "unix path = " << dos2unix( dos ) << endl;
dos2unix( dos ); 가 호출되면 카피 콘스트럭터 2번 소멸
자 2 번이 인자를 받을 때와 값을 리턴 할 때에 불린다.
이 때에 리턴되는 카피된 오브젝트는 임시 오브젝트이다.
unix = dos2unix( dos ) 의 경우 unix 오브젝트의 주소가
dos2unix 에 전달되었지만 위의 경우에서는 실제 순수한
임시 오브젝트의 주소가 dos2unix에 전달이 된다. 보통은
스택에서 할당된 오브젝트가 되겠다.
예제 #3 을 실행시키면 여러 메시지가 나온다. 하나하나
의 의미와 순서를 파악해 두자. 그리고 그런 관계를 꼭
기억해 두자. 계속되는 강좌에서 생성자와 소멸자가 호출
되는 관계를 나타내는 메시지가 종종 나타나게 될 것이고
점점 여러분을 혼란으로 몰아넣게 될 듯하다. 예제를 수
정하면서 생성자와 소멸자가 값에 의한 호출시에 어떻게
호출되는지 this 포인터의 주소를 관찰하면서 꼭 익혀 두
기 바란다.
C++ 강좌40 끝...
다음 강좌 예고 : 소멸자의 개념
^-^
[C++강좌41] 소멸자의 개념...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/07 12:08:17
수 정 일 :
크 기 : 7.7K
조회횟수 : 11
[C++ 강좌41] -> 소멸자의 개념
< 98.02.07 >
11. 소멸자가 호출되지 않을 때
- 소멸자가 호출되지 않는 경우는 컴파일러가 소멸자를 호
출하는 부분을 뛰어넘는 경우에 일어난다. 생성자가 호출
하는 코드를 goto 문이 넘을 수가 없다고 배웠다. 그런데
goto 문은 소멸자가 호출되는 곳을 넘을 수가 있다.
void func()
{
AClass A;
{
AClass B;
int i;
...
if( i == 0 ) goto next; // B 의 소멸자가 호출된다.
...
} // B 의 소멸자가 호출된다.
next:;
} // A 의 소멸자가 호출된다.
AClass B 의 소멸자는 goto next이 있는 곳에서 소멸자가
호출된다. 그러나 goto next 가 asm jmp next 라면 B 의
소멸자는 호출되지 않는다. 만일 내부의 포인터에 동적
메모리가 할당되어 있거나 한다면 그 메모리는 해제되지
않은 상태로 계속 남아 있게 될 것이다. 만일 그럴 경우
라면 goto next 부분을 아래처럼 바꾸어 줄 수 있겠다.
if( i == 0 )
{
B.AClass::~AClass(); // 소멸자를 직접 호출
asm jmp next;
}
컴파일러가 소멸자를 자동으로 호출하지 않게 될 때에 소
멸자를 직접 호출할 수 있는 예이기도 하다. goto문은 그
goto문이 있는 스코프내에 생성된 오브젝트들의 소멸자를
호출하고 점프한다. 그러므로 최소 두번 소멸자가 호출되
는 코드가 소스에 들어가게 되어 쓸데없이 코드가 커지게
될 것이다. 어떤 경우 프로그램의 속도와 크기 문제로 go
to 문 대신에 asm jmp 문을 쓰면서 소멸자를 호출할 오브
젝트를 선택하여 호출하는 경우도 있을 것이다.
또다른 예는 abort 함수 등을 사용할 경우이다. abort 함
수가 사용되면 "비정상적인 프로그램의 종료"라는 문자열
을 화면에 출력하고 그 상태에서 도스로 종료된다. 또 프
로그램 어디선가
asm mov ax, 4C03h
asm int 21h
와 같이 도스로 종료하는 도스 서비스를 직접 호출했다면
abort와 마찬가지로 그 즉시 도스로 종료해 버리게 된다.
이럴 경우에 앞서 살펴 본 전역 오브젝트들의 소멸자를
호출하는 exit코드는 사용되지 않게 될 것이고 결국 전역
오브젝트들의 소멸자는 단 하나도 호출되지 않게 된다.
물론 abort 와 같은 함수를 사용할 때에는 그만한 이유가
있을 것이다. 하지만 중요한 전역 오브젝트의 소멸자들
을 호출하고 종료해야 한다면 abort 함수 등과 같이 도스
로 직접 종료해서는 안된다.
지난 강좌에서 goto, asm jmp, abort 등과 소멸자의 관계
를 살펴보았다. goto, asm jmp, abort 등은 일종의 점프
이다. 점프란 한코드 부분에서 다른 코드 부분으로 직접
분기함을 뜻한다. if, else, do {} while, while 등이 조
건 분기라면 goto 는 강제 분기가 된다. 이들은 함수내에
서 원하는 곳으로 분기하는 명령어들이다. C에는 한 함수
에서 다른 함수로 직접 분기하는 명령은 제공되고 있지
않지만 기본 라이브러리를 보면 다른 함수 내로 점프할
수 있는 실제로는 하위 루틴에서 상위 루틴으로 점프할
수 있는 함수와 구조체가 있다. 를 보면 long
jmp, setjmp 함수와 jmp_buf 구조체가 선언이 되어 있고,
이들이 C 의 exeptional handler 의 역할을 담당한다.
Exeptional handling은 간단히 살펴보자면 한 프로그램에
서 연속적인 또는 중복적인 하위 루틴으로 상위 또는 하
위 루틴에서 알수 없는 정도의 복잡한 호출이 이루어진다
고 생각해 보자. 이때 만일 하위 루틴에서는 복구할 수
없는 고수준의 에러가 발생했을 때에 하위 루틴에서는 그
에러를 직접 처리할 수가 없게 되고 더구나 현재 자신의
함수 호출 관계에서의 위치를 알 수가 없으므로 상당히
난처한 경우에 처하게 된다. 주로 되부름으로 몇 개의 함
수를 부르는 경우에 발생하며 예를 들자면 수식을 계산할
때에
((((((1*3)/4)>>4)%7)*6)/3)|0x0F)
와 같은 수식을 해석하기 위해서 term, expression이라는
함수 두개를 번갈아 되부름으로 부른다면 위의 수식의 에
러는 term 이 7번 되부름 되어야 ')'가 하나더 찍혀 있다
는 에러를 찾게 된다. 하위 루틴에서는 다시 처음부터 입
력 받는 등의 에러 처리를 할 수가 없다. 이런 경우 수식
이 잘 못되었음을 직접 최상위 함수로 리턴 하여 상위 함
수에서 에러를 처리하도록 하는 등의 필요에 사용되는 기
법을 exeptional handling 이라 한다. 즉 위의 수식을 입
력 받고 해석하기 전에는 프로그램의 상태가 에러 없는
상태이었음을 알고 하위 루틴에서 다시 그 에러 없는 상
태로 복귀하는 과정이다. 다음 과정을 보자.
void claculator()
{
clean: // 에러가 없는 깨끗한 상태
setjmp( Jumper) // 이곳에서 현재의 상태를 저장
...
expression();
...
}
void expression()
{
.... // 에러 발생
longjmp( 1, Jumper ) // claculator 의 clean 으로 복귀
}
expression 에서 에러가 발생하였고, 이때 claculator 의
에러 없이 깨끗한 상태로 직접 복귀하고 싶다면 setjmp로
상태를 기억해 놓고는 longjmp로 직접 복귀하게 된다. 예
를 들어 사용자에게 수식을 입력 받는 프로그램이 있고
만일 지금 입력된 수식의 문법이 잘못되었을 때에 더 이
상 수식을 해석하고 계산하지 않고 직접 입력 받기 전의
상태로 복귀한 후에 "수식이 틀렸다"라는 메시지를 출력
하고자 한다면 setjmp, longjmp 함수들을 사용하게 된다.
이 처럼 상위 함수의 어느 부분의 상태를 기억해 놓고는
복잡한 하위 루틴의 호출 중에 에러가 발생하면 직접 상
위 함수에 기억해 놓은 곳으로 복귀하는 기능을 exeption
al handling 즉 저수준의 하위 함수에서 다룰 수 없는 고
수준의 에러가 발생했을 때에 그 에러를 처리할 수 있는
고수준의 상위 함수로 직접 복귀하는 것을 말한다. 과장
해서 말하자면 어떤 하위 루틴에서 심각한 에러가 발생하
면 직접 프로그램의 시작 지점으로 복귀해 다시 프로그램
을 처음부터 시작할 수도 있음을 뜻한다.
그러나 그렇게 상위 함수로 직접 복귀했을 때에 몇 가지
문제가 발생하게 된다. 만일 하위 루틴이 수행 중에 mall
oc 등으로 동적인 메모리를 할당했으면 상위 루틴으로 복
귀해도 그 메모리는 그대로 있게 될 것이다. 그런 경우
파스칼의 mark, release와 같이 동적이 메모리를 상위 루
틴에서 강제로 해제해 버리는 기능이 없는 C와 C++에서는
별다른 대책이 없고 그런 일이 발생하지 않도록 구현하는
수 밖에는 없다. 동적인 메모리를 예를 들었지만, C++ 의
오브젝트와 longjmp함수간의 문제는 좀더 복잡해진다. 우
선 상위 함수로 복귀하게 되면 하위 루틴들에서 생성된
오브젝트들 중에 많은 부분이 소멸자를 거치지 않고 그대
로 쓰레기 값으로 변해 버리고 만다. 또 정적인 오브젝트
의 경우에는 성격가 바뀌어 있을 수도 있다. 그런 경우 l
ongjmp 만으로는 겨우 점프만 했을 뿐 주위 환경의 여러
변화는 그대로 있게 되어 버린다. 그러나 longjmp 외에
C++가 가진 exeptional handling 기법은 아직 없다. 다른
OOPL 언어들의 대부분도 그렇다. 앞으로 C++가 어떻게 변
할 지는 모르나 현재 같으면 앞으로도 지원되기 힘들 것
이다. 결국 프로그래머가 exeptional handling 이 필요한
부분에서는 정적인 오브젝트보다 스택에 생성되는 auto오
브젝트만을 사용하며 동적 메모리나 화일 입출력 등 시스
템의 자원을 변화시키지 않도록 주의하여 작성할 밖에 도
리가 없게 된다.
회원 중 몇 분은 하위 루틴에서 상위로 일반적인 종료 과
정을 통하여 상위 함수에게 에러 발생을 알리면 되지 않
는 가라는 의문을 가지고 있을 듯하다. 실제로 함수의 호
출간에 하위 루틴에서 에러가 발생했음을 정상적인 리턴
과정을 통하여 상위로 전달할 수 없는 경우는 없을 것이
나 간단히 int 라 리턴되던 상황이 구조체가 리턴되고 또
구조체가 인자로 전달되는 아주 비효율적인 코드가 나타
나게 된다. 덧붙이자면 longjmp와 같은 함수는 에러가 발
생했을 때도 물론이지만 불필요한 함수 리턴 과정을 생략
하고 직접 상위 함수로 복귀할 때 등 코드의 효율을 위해
사용될 수도 있다.
C++ 강좌41 끝...
다음 강좌 예고 : 소멸자의 개념
^-^
[C++강좌42] 소멸자의 개념...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/07 21:07:06
수 정 일 :
크 기 : 4.7K
조회횟수 : 11
[C++ 강좌42] -> 소멸자의 개념
< 98.02.07 >
12. Virtual 소멸자
- 기본적으로 virtual 멤버 함수와 그 용법이나 사용 용도
가 같다. 상위 클래스의 주소로 접근되고 있는 오브젝트
의 실제 소멸자를 호출하게 하는 기법이다. 예를 들어보
면
class Aclass
{
public :
~Aclass() { printf("Aclass destructor\\n"); }
};
class Bclass
{
public :
~Bclass() { printf("Bclass destructor\\n"); }
};
class Cclass
{
public :
~Cclass() { printf("Cclass destructor\\n"); }
};
void func()
{
Aclass * aP = (Aclass *)new Cclass();
delete aP; // 오브젝트의 소멸자를 호출한다.
}
위의 결과는 Aclass 의 소멸자만이 호출되게 된다. 당연
하게도 aP 는 Aclass 의 포인터이니 Aclass 의 소멸자가
호출되게 되며 실제 aP 가 가리키고 있는 Cclass 와 Ccla
ss 가 계승한 Bclass의 소멸자는 호출되지 않게 된다. 이
럴 경우 Cclass 의 소멸자가 호출되게 하려면 Aclass 에
서의 소멸자 선언을
virtual ~Aclass();
와 같이 virtual 함수로 변형해 주면 된다. Virtual 멤버
함수의 경우 상위 클래스에서 같은 이름에 같은 인자를
받는 함수가 virtual 이면 하위 클래스에서 자동적으로 v
irtual 멤버 함수가 되었듯이 소멸자의 경우에도 상위 클
래스의 소멸자가 virtual 이면 하위 클래스의 소멸자도 v
irtual 함수가 된다. 물론 이 때 소멸자는 그 클래스의
이름에 '~' 문자를 앞에 붙인다는 점 때문에 상위 클래스
와 하위 클래스간의 virtual소멸자의 이름은 다르게 된다.
그러므로 상위 클래스에서 virtual 로 소멸자를 선언하게
되면 그 클래스에서 계승된 모든 클래스의 소멸자가 virt
ual 이 되게 된다. 또, virtual 소멸자의 virtual functi
on table 에서의 위치는 일반 virtual멤버 함수의 경우와
같다.
다중 계승된 경우를 살펴보자.
Aclass
|
Bclass Dclass
| |
+------+--------+
|
Cclass --- class C : public Bclass, pubulic Dclass
로 계승 관계를 가질 때에 Dclass 의 소멸자가 virtual이
고 Aclass 의 소멸자가 virtual 이라면 문제는 간단하다.
상위 클래스의 어떤 클래스의 포인터나 레퍼런스로 소멸
자를 호출하여도 모든 계승된 클래스의 소멸자가 호출되
게 된다. 그러나 Aclass 의 소멸자가 virtual 이고 Dclas
s 가 virtual 이 아니라면 Dclass 의 소멸자만 호출되게
될 것이다.
소멸자는 함수이고 한 클래스의 소멸자에서는 그 클래스
가 직접 계승한 클래스들의 소멸자를 호출하게 된다. Ccl
ass 의 소멸자에서는 종료 직전에 Bclass 의 소멸자를 호
출하고 Dclass 의 소멸자를 호출하게 된다. Bclass 의 소
멸자에서는 종료 전에 Aclass 의 소멸자를 호출하게 된다.
순서는 Cclass - Bclass - Aclass - Dclass 의 순서로 호
출되게 된다. 만일 Dclass의 소멸자가 virtual 이 아니고
Dclass * dP = (Dclass *)new Cclass;
라고 하였다면 dP 에 들어가는 주소는 Cclass 오브젝트
메모리 lay-out 내의 Dclass 시작 주소이다. 이 때 delet
e dP; 를 하게되면 new Cclass; 로부터 받은 주소와 틀리
기 때문에 실패하고 예상치 못한 결과를 낳을 수가 있다.
만일 Dclass의 소멸자가 virtual 이라면 virtual 멤버 함
수에서 설명한 것과 마찬가지로 dP 포인터의 틀린 오프셋
은 virutual 함수 테이블에 있는 오프셋 조정 루틴을 거
쳐 수정된 뒤에 Cclass 의 소멸자가 호출되게 된다. 만일
위 처럼 virtual 소멸자가 아닌 상위 클래스의 포인터를
사용했을 경우에는 반드시
delete (Cclass *)dP;
와 같이 실제 dP 가 가리키는 Cclass의 시작 주소로 포인
터를 형변환 해주어야만 한다. new 와 delete 연산자와
생성자 그리고 소멸자와의 관계는 new 와 delete 연산자
를 강좌 할 때에 좀 더 자세히 살펴보자.
Virtual 멤버 함수와 virtual 소멸자는 OOP 의 중요 개념
인 데이터의 추상화에 결정적인 역할을 한다. 상위 클래
스는 하위 클래스보다 일반적인 성격을 가지고 있다. 즉
상위 클래스가 사람이고 하위 클래스가 학생 이라면 보다
보편적 개념인 사람으로 학생 오브젝트를 다루게 된다면,
사람 이란 보편적 개념이 포괄하는 주부, 회사원, 어린이,
늙은이 등의 모든 사람 클래스의 하위 클래스 오브젝트들
과 함께 다루어질 수가 있게 되고 나아가서 사람 이란 클
래스를 다루는 코드는 그대로 학생, 주부, 회사원, 어린
이 등의 클래스를 수정 없이 포용할 수가 있게 된다. 이
처럼 상위 클래스의 성격과 행동이 실제로는 하위 클래스
의 성격과 행동으로 다루어질 수 있게 하기 위해서 virtu
al 함수와 virtual 소멸자가 반드시 존재해야만 한다. 소
멸자를 예로 들자면 '사람' 이라고 일반적 성격으로 다
루어진 오브젝트를 소멸시킬 때에 사람이 아닌 실제 '학
생'이 소멸되는 관계를 이룬다.
C++ 강좌42 끝...
다음 강좌 예고 : ???
^-^
[C++강좌43] 기타...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/08 23:03:25
수 정 일 :
크 기 : 4.4K
조회횟수 : 7
[C++ 강좌43] -> inline 함수
< 98.02.08 >
>>> inline 함수
- 일반적으로 C에서 인라인 어셈블리를 사용한 함수와 혼동
되는 경우가 많읍니다만 인라인 함수와 인라인 어셈블리
코드로 정확히 구분하여 사용해야 되겠읍니다.
BC++ 의 헤더 화일 중에 에는 다음과 같은 메크
로가 있다.
_MK_FP( SEG, OFS )
// 세그먼트와 오프셋으로 화 포인터를 합성
이와 같은 메크로가 하는 일은 매번 반복되는 코드를 축
소해놓아 프로그래머가 키를 적게 치게하고, 코드의 변형
을 쉽게할 수가 있으며, 코드를 읽기쉽게 하여주고, 에러
의 가능성을 줄여주면서 함수가 불리울때에 소모되는 오
버헤드을 제거하여 코드의 효율성을 높여준다. 이런한 메
크로의 장점에 반해 가장 큰 단점은 메크로는 '진짜 함수'
가 아닌 '함수 비슷한' 코드의 집합이므로 보통의 함수의
코드에 적용되는 컴파일러의 코드 에러 검출이 불가능해
진다.
C++ 는 크기가 작은 함수를 자주 사용해야만 한다. 또 어
떤 때는 그렇게 해야된 되는 경우도 많다. 거의 대부분의
C++ 클래스에는 Mutator 라고 불리우는 아주 작은 함수들
이 있으며 이들의 일반 형태는 다음과 같다.
int get10() { return 10; }
위에서 class 의 부럭안에 들어있는 getInt 함수가 하는
일이라고는 return i; 라는 아주 작은 코드이며 정수 하
나를 씨의 리턴 방식에 따라 _AX 레지스터에 넣는 코드로
아주 작다. 이런 작은 함수는 함수 내부의 코드보다도 오
히려 어셈으로 함수를 호출하고 스택을 재정리하고 리턴
하는 코드가 훨씬 더 큰 가분수 형태를 취하고 있다. 이
런 함수를 메크로와 같이 함수가 호출된 곳에 직접 함수
내부의 코드( return i; ) 부분만을 넣는다면 함수를 호
출하는 데에 소모되는 오버헤드와 불필요한 코드를 줄일
수가 있게되는 것이다. 이러한 필요에 의해서 C++에는 인
라인 함수를 도입했으며, 그 특징은 일반 함수와 같이 컴
파일러가 코드를 검사하며, 메크로처럼 함수의 오버헤드
를 없애는 장점을 모두 가진 새로운 형태의 코드 작성법
이다. 그 선언 형태는 inline이란 키워드 함수 선언 앞에
넣는 것으로 다음과 같다.
inline int get10() { return 10; }
C++ 컴파일러가 인라인 함수에 동작하는 방식은 컴파일러
가 inline 이라는 선언을 만나면 일반 함수와 같은 방식
이 아니라, 인라인 함수의 내부 코드를 모두 기억해 놓고
는, 그 인라인 함수가 사용된 곳을 만날 때마다, 기억된
인라인 함수를 정확히 사용했는가를 검사한 후에, 기억된
내부 코드를 그 곳에 직접 집어넣게 되는 것이다. 이렇게
함으로서 메크로의 효율성과 함수의 에러 검출 능력을 모
두 갖게 되는 것이다.
위와 같은 방식의 컴파일을 하려면 항상 인라인 함수는
선언된 자리에서 함수를 결정해놓아야만 한다.. 즉
inline int get10();
과 같은 형 선언은 아무런 의미가 없다. 왜냐하면 컴파일
러는 인라인 함수의 내부 코드를 기억할 수가 없기 때문
에 선언된 인라인 함수가 상용된 곳에 인라인 함수의 내
부 코드를 직접 넣을 수 없으므로 에러가 발생하게 된다.
그러므로 인라인 함수가 선언되었으면 반드시 그 인라인
함수가 첫 번째로 사용되기 전에 함수의 본체가 결정되어
있어야만 하는 것이다. 다시 말하면 코드의 본체가 결정
되어 있더라도 그 인라인 함수가 사용되지 않으면 인라인
함수의 크기는 실행화일에 포함되지 않게되는 것이다.
인라인 함수는 잘 쓰면 코드의 크기도 줄이고, 코드의 속
도도 빠르게 만든다. 그러나 이러한 인라인 함수에는 몇
가지 단점이 있다. 우선 그 첫째는 인라인 함수를 남용하
는 문제이다. 인라인 함수는 인라인 함수가 사용된 곳마
다 내부의 코드가 삽입된다. 다시 말해서 큰 인라인 함수
는 필요이상으로 실행화일의 크기를 증가시킬 수가 있다.
둘째는 인라인 함수를 컴파일러가 기억하려면 그만큼의
메모리가 소모된다. 즉 인라인 함수가 많은 화일을 컴파
일하려면 더 큰 메모리를 요구 하게 되며, 시스템의 메모
리의 크기가 작다면 메모리가 부족해 질 수가 있다. 셋째
로 디버깅의 어려움이다. 인라인 함수는 코드 속으로 삽
입되어 버리고, 함수의 형태는 사라져버린다. 그러므로
소스 코드에서는 함수의 호출로 되어있지만, 기게어 코드
에는 나타나지 않고, 전혀 다른 코드가 있게 되는 것이다.
그러므로 인라인 함수가 사용된 코드는 소스 차원에서의
디버깅이 되지 않는다. 이 문제는 BC++ 의 경우에 'Out o
f inline function' 이라는 옵션을 켜면 인라인 함수가
보통의 함수와 같이 컴파일되고 소스 차원에서의 디버깅
이 가능해진다.
C++ 강좌43 끝...
다음 강좌 예고 : ???
^-^
[C++강좌44] 기타...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/08 23:04:48
수 정 일 :
크 기 : 8.4K
조회횟수 : 5
[C++ 강좌44] -> 함수의 오버로딩과 네임 멩글링
< 98.02.08 >
>>> 함수의 오버로딩과 네임 멩글링
1. 기본 인자
- C++ 는 기본 인자라는 것을 도입했다. 이의 사용은 다음
과 같다.
int func( int = 100 ); 또는 int func( int i = 100 );
위와 같이 선언되어있으면 func();라고 아무런 인자도 주
지않고 부르게 되면 컴파일러가 자동7막 func 함수에 100
이라는 값을 가진 int 형을 주게되는 것이다. 이런 기능
은 아주 편리하다. 특히 유용한 부분으로는 어떤 함수가
보통은 거의 똑 같은 일을 하고 가끔 특별한 경우에만 다
른 기능도 한다고 했을 때에 아주 편한 코딩을 하게 해준
다. 단 여러개의 인자가 있을 때에는 기본 인자는 함수의
인자 나열의 마지막 부분이 되어야만 한다.
int func( int A, int B, int C = 1, int D = 0 );
위에서 처럼 뒷 부분에 기본 인자가 있음으로 컴파일러가
정확한 컴파일을 할 수가 있게된다. 위의 예에서는 앞의
두 인자는 반드시 들어와야 되며, 뒤의 두 인자에서는 D
하나만 생략할 수는 있으나 C 만 생략할 수는 없다. 물론
C 와 D 모두 생략할 수 있다.
2. 함수 오버로딩
C++는 함수의 오버로딩이라는 새로운 개념을 소개하고 있
다. 이것이 의미하는 것은 프로그래머는 C++ 에서 하나의
같은 이름으로 인자의 종류와 수에 따라 여러 개의 함수
로 나누어서 작석할 수가 있음을 뜻 한다. 예를 들어 pri
nt라는 함수를 인자에 따라 오버로딩 시킨다면 다음과 같
을 것이다.
print( char * ); --> print( "함수의 중복선언" );
print( int ); --> print( 10 );
print( float ); --> print( 12.10 );
위에서 나타내는 것은 print 라는 하나의 함수 이름으로
여러 가지의 인자를 받아드리는 예이다. 위의 세가지 함
수는 전혀 상관이 없는 별개의 함수이다. 다른 예를 보자
[1] int sum( int, int );
[2] long sum( int, int, int );
[3] float sum( float, int, int );
이번의 예는 함수 인자의 갯수와 성격이 혼합되어있는 예
이다. 함수의 오버로딩에는 인자의 갯수도 성격과 마찬가
지로 중요하다. 그럼 다음의 사용예에서 위의 세가지 sum
함수 중에 어떤 것이 불리울 것인가를 연구해보자.
int i = 1;
long L = 1;
float f = 1.0 ;
(1) i = sum( 10, 20 );
(2) L = sum( i, i, i );
(3) f = sum( f, L, i );
(4) i = sum( L, i, i );
(5) i = (int)sum( (int)f, L, i );
(1)(2)(3) 번의 예는 모두 대응되는 [1][2][3] 의 함수가
불리우게 된다..
(4) 번을 보면 함수의 인자에 int 형이 아닌 long 형인 L
이 들어 있고 리턴을 int 형인 i 가 받고있다. 씨를 하신
분은 금방 아시겠지만, long 형이 int 형으로 컴파일러에
의해 변형되어서 불리우게 된다. 좀 더 정확히 사용하려
면, i = (int) sum( (int)L, i, i ); 가 되어야 한다.
(5) 번은 (4) 번과 비슷하다, 역시 int 형으로 형변형된
인자가 사용되므로 [2] 번이 사용되게 된다.
그러면 여기서 몇가지 함수 오버로딩이 안되는 예를 들어
보자. 우선 리턴 형만 틀린 오버로딩은 금지되어있다.
int sum( int, int ); long sum( int, int );
위의 두 함수는 리턴 형만 틀리므로 컴파일러의 에러메세
지가 출력되게 될 것이다. long 이 리턴되는 함수를 int
형이 받을 수가 있고, int 가 리턴되는 함수의 리턴을 lo
ng 형이 받을 수도 있으므로 단지 리턴 형만이 틀린 함수
의 오버로딩은 금지되어있다.
일립시스( ellipsis, 말 줄임표, "..." ) 가 함수 오버로
딩에 사용될 경우는 일립시스가 여러개의 함수 인자로 대
체되는 관계로 함수의 오버로딩에 제한이 생긴다.
int sum( int, int ); int sum( int, ... );
위의 오버로딩은 불가능하다. 뒤의 sum 은 인자를 몇 개
던지 더 받을 수가 있다. 그러므로 sum( 10, 20 ); 이라
고 불리웠을 때에 위의 두 함수 중에 어떤 함수가 불리워
야 되는지를 알 수있는 방법이 없기 때문이다. 그러나 다
음은 가능하다.
int sum( float, int ); int sum( int, ... );
그리고 또 한가지는 기본 인자와 상관이 있다. 이 관계는
바로 위의 일립시스를 사용한 함수의 오버로딩과 비슷하다.
int sum( int, int ); int sum( int, int, int=0, 7int=0 );
위와 같은 오버로딩은 안된다. 인자가 두개로서 sum( 10,
10) 과 같이 불리웠을 때에 어떤 함수를 불러야 될지 알
수가 없기 때문이다.
함수의 오버로딩은 연산자의 오버로딩과 분리되면서도 거
의 같은 개념이다. 연산자 중복 사용에 대해서는 다음에
클래스의 멤버를 강좌할 때에 두세번의 강좌로 나누어서
자세히 할 계획이다.
3. Name mangling
네임 멩글링을 문자대로 해석한다면 '이름 토막내기' 이다.
C++ 는 이름 섞어 만들기를 통하여 함수의 오버로딩을 구
현한다. 우선 일반적인 C 의 어셈 코드를 살펴보자.
int func( int );
위의 함수가 일반 C 의 코드로 어셈블리 코드로 만들어지
면, _func 라는 이름이 사용된다. C 는 함수의 이름 앞에
항상 언더 스코어( '_' ) 를 붙인다. 그런 이유로 어셈에
서 C 에서 만들어진 변수나 함수를 사용하려면 선언된 이
름의 앞에 꼭 언더스코어를 넣어야만 조회가 가능해지며,
그 역으로 어셈에서는 변수나 함수의 이름앞에 언더스코
어를 붙여야만 C 에서 불러서 쓸 수가 있다. 이런 기존의
C 함수 이름을 어셈블리 이름으로 만드는 방법으로는 함
수의 오버로딩은 불가능하다. 우선 어셈에서는 함수의 인
자가 무엇이든 몇 개이든 그저 함수의 이름 문자열 밖에
는 구별하지 않기 때문이다. 즉
int func( int ); int func( char *, float, int, long );
위와 같은 두 함수가 있다면, C 에서는 분명히 에러가 난
다. 두 함수의 이름이 _func 로 똑 같기 때문에 링커의
차원에서는 두 함수를 구별할 수 있는 방법이 전혀 없기
때문이다. C++ 도 오브젝트 파일( .OBJ ) 을 만들어내며,
링커의 도움으로 실행화일을 만들어내기 때문에 함수의
오버로딩을 구현하려면, C 형태의 함수 이름 만들기로는
불가능한 것이다. 이런 이유로 C++ 는 함수의 이름으로
그 함수가 받아들이는 인자의 수와 성격을 구별할 수있는
이름 만드는 방법을 고안했고, 이것이 바로 name manggli
ng 인 것이다. 즉 위의 func 두 함수를 예를 들자면
int func( int ); -----------------------> @func$qi
int func( char *, float, long, int ); ---> @func$qpzcfli
와 같이 오브젝트 화일이 생성된다. 자세히 보시면 아시
겠지만 @ 마크는 C++ 함수인것을 나타내고, 그 뒤에 함수
의 이름, $ 표시는 함수 이름의 끝을 가리키며, q 는 인
자들의 시작을 나타내고 뒤에 int - i, float - f, long
- l, char near * - pzc 라고 되어있음을 알 수가 있읍니
다. 위의 예는 BC++ 의 예입니다. 재미있는 것은 함수의
이름 중간에 C 에서 사용 못하는 달러마크($) 가 들어가
있어서, 일반 C에서는 위의 함수를 조회할 수도, 선언 할
수도 없게 되었다는 점입니다.
위와 같이 이름에 전달받는 독치 이름을 적어놓아 함수의
오버로딩을 해결할 수가 있게됩니다. 이런 넴임 멩글링은
오버로딩된 함수뿐 아니라 전체 C++ 함수에 적용됩니다.
그러므로 일반 C 로 작성된 이름이 사용되어야 할 때는,
그 함수가 일반 C 의 이름으로 사용된다는 선언을 다음과
같이 해주어야 됩니다.
extern "C" { int func( int ); } -----> _func 로 사용된다.
extern "PASCAL" { int func( int ); } -> FUNC 로 사용된다.
이와 같이 C++ 의 이름 바꾸는 방법이 사용되지 않은 함
수나 외부의 함수를 조회하려면 C, PASCAL, FORTRAN 등으
로 이름 만드는 방법을 C++컴파일러에게 가리켜 주어야만
합니다.
4. 기본 인자와 함수의 오버로딩은 Object-Oriented 인가??
OOP 는 프로그래밍의 세세한 구현의 짐을 인간에게서 컴
퓨터로 이전하는 긴 프로그래밍 발전사의 또 하나의 과정
이다. C++ 도 이러한 프로그램 세계의 발전 과정의 하나
이며 C++ 가 만들어진 중요한 동기의 하나가 "프로그래머
에게 전체적인 디자인과 모델링, 분석에 시간을 더 주고,
컴퓨터에게는 세세한 구현에 더 많은 시간을 쓰게하자"
라는 취지인 것이다. 이런 쪽에서 보면 기본 인자나 함수
의 오버로딩은 OOP 의 취지에 적합한 것이다. 그 러나 OO
P 는 그 기원이 smalltalk 와 같은 인터프레터 환경에서
시작되었다. 그리고 컴파일 타임에 구성되는 코드는 OOP
적이 아니라는 의견이 지배적인 것이다. 런타임에 오브젝
트등이 생성되고 결합되는 것이 OOP 라고 본다면 기본 인
자와 함수의 오버로딩은 컴파일러에 의해서 컴파일 시간
에 고 정적인 코드를 만들어내므로 OOP 가 아니라고 생각
할 수도 있다. 그러나, 그런 관점에서 본다면 "C++ 가 진
짜 OOP 인가?" 라는 물음이 먼저 와닿게 된다. 만일 OOP
를 프로그래머를 잡다한 함수의 이름을 정하는 부담에서
해방시키고 좀 더 차원높은 디자인, 모델링, 분석으로 이
끄는 시도라고 본다면, 분명 기본 인자와 오버로딩은 OOP
인 것이다.
C++ 강좌44 끝...
다음 강좌 예고 : ???
^-^
[C++강좌45] 기타...
--------------------------------------------------------------------------------
게 시 자 : shachah(오승용)
게 시 일 : 98/02/08 23:05:20
수 정 일 :
크 기 : 2.9K
조회횟수 : 4
[C++ 강좌45] -> 즉시 변수 정의와 스코프 연산자
< 98.02.08 >
>>> 즉시 변수 정의와 스코프 연산자
1. 즉시 변수 정의
- C++ 도 어떤 변수가 사용되기 전에 항상 그 변수의 형태
가 정의 되어있어야한다. 그러나 C 의 경우는 항상 어떤
변수가 전역이 아닌 지역에서 사용되기 위해서는 그 지역
이 시작되는 첫 부분에 정의 되어있어야만 함에 반하여
C++ 는 변수가 사용되기 전이라면 그 지역의 어떤 곳이라
도 가능하게 되었다. 이는 프로그래머가 좀 더 자유롭게
프로그램을 작성할 수 있게하고 소스를 읽기 쉽게하여준
다. 예를 들자면
void main( void )
{
// 새로운 지역이 선언되고...
int i; // 일반 C 처럼 변수를 정의
i = 11;
for( int j = 0; ... // 필요한 곳에서 변수를 정의..
...
}
위와 같이 j라는 변수가 필요한 곳에서 자유롭게 선언 될
수가 있다.
2. 스코프 연산자, '::'
- 구조적 언어에는 데이터에 대한 스코프라는 개념이 절대
적인 영향을 소스에 미친다. 실제로 잘 설게 되지못한 전
역변수와 지역 변수간에 충돌도 일어나는 것이다. 기존 C
의 스코프 법칙은 간단하다. 예를 들자면
int i = 10, j;
{
int i = 5;
j = i;
}
printf( "%d", j );
위의 소스로 출력되는 j 의 값은 당연히 5 가된다. C에서
의 스코프 경계선은 '{', '}' 문자로 안에서 밖으로 중복
된 경계를 범위를 넓혀가면서 변수의 조회를 해결한다.
그로벌 스코프, 즉 프로그램 전체의 스코프로도 해결 할
수가 없으면 컴파일에러가 나게 되는 것이다.
앞서의 강좌에서 언급한데로 C++ 에는 함수 오버로딩등으
로 같은 이름의 함수가 여러개 있을 수가 있다. 이럴 경
우에 현재 작성되고 있는 라인의 스코프의 밖에 있는 같
은 이름의 다른 함수나 변수를 조회할 수 있는 방법이 필
요하게 되었다. 나중에 다루어지겠지만 특히 클래스의 멤
버 함수의 경우가 그렇다. 현재의 스코프를 벗어나 전역
함수나 변수를 조회하려고 할 때에 쓰이는 연산자가 '::'
이다. 이 스코프 연산자가 쓰이게 되는 예를 살펴보자.
struct Scope
{
int i;
struct scope
{
char i;
};
};
int i;