E D R , A S I H C RSS

Cpp에서의멤버함수구현메커니즘 (rev. 1.1)

Cpp에서의멤버함수구현메커니즘


1. 문제 제기를 담은 설명 코드

  • 다음 소스에서는 die메소드에서 자신을 삭제해줍니다. 그런데. 삭제되고도 뻔뻔스럽게 자신의 메소드를 호출하네요. 어떻게 된것일까요? 알아봅시다.

~cpp 

#include <iostream>
using namespace std;

class Foo{
public:
	int id;
	Foo(){
		static idSequance = 0;
		id = idSequance++;
		cout << "Create! id = " << id << endl;
	}
	void die(){
		cout << "I suicide. Id is " << id << endl;
		delete this;	// 자신 삭제
	}
	void sayMyId(){
		cout << "My Id is no " << id << endl;
	}
	void sayHello(){
		cout << "say Hello" << endl;
	}	
};


int main(int argc, char** argv)
{	
	cout << endl << ":::::: Case 1 - 동적할당 " << endl;
	Foo* foo1 = new Foo();// Create! 를 출력한다.
	foo1->sayHello();		// Hello 출력
	foo1->sayMyId();		// id 출력
	foo1->die();		// 객체 삭제
	foo1->sayHello();		// 동작 가능
	foo1->sayMyId();		// 동작 가능
	

	// id 확인차
	cout << endl << ":::::: Case 2 - id 확인" << endl;
	Foo* foo2 = new Foo();// Create! 를 출력한다.
	foo2->sayHello();		// Hello World 출력
	foo2->sayMyId();		// id 출력
	foo2->die();			
	foo2->sayHello();
	foo2->sayMyId();		
	
	cout << endl << ":::::: Case 3 - 포인터 NULL로 하고 메소드 호출하기 " << endl;
	Foo* foo3 = NULL;		//  자세한 설명보세요.		
	foo3->sayHello();		// Hello 를 출력한다.
	//foo3->sayMyId();		// debug, release 모두 동작할 수 없다. 
	//foo3->die();		// debug, release 모두 동작할 수 없다.	

	cout << endl << ":::::: Case3 - 지역변수로 선언" << endl;
	Foo foo4;
	foo4.sayHello();		// Hello World 출력
	foo4.sayMyId();		// id 출력
	foo4.die();		// debug mode에서 assertion error
	foo4.sayHello();		// release 에서 동작 가능
	foo4.sayMyId();		// release 에서 동작 가능
	cout << endl;

	return 0;
}
결과는 다음과 같이 출력됩니다. 비교해보면서, 생각해 보고 이해가지 않는다면 자세한 설명을 보세요.
~cpp 
:::::: Case 1 - 동적할당
Create! id = 0
say Hello
My Id is no 0
I suicide. Id is 0
say Hello
My Id is no 3604872

:::::: Case 2 - id 확인
Create! id = 1
say Hello
My Id is no 1
I suicide. Id is 1
say Hello
My Id is no 3604872

:::::: Case 3 - 포인터 NULL로 하고 메소드 호출하기
say Hello

:::::: Case3 - 지역변수로 선언
Create! id = 2
say Hello
My Id is no 2
I suicide. Id is 2
say Hello
My Id is no 2

Press any key to continue

2. 각각의 경우에 따른 테스트

2.1. Case1 - 동적할당

~cpp 
	Foo* foo1 = new Foo();	// Create! 를 출력한다.
	foo1->sayHello();		// Hello World 출력
	foo1->sayMyId();		// id 출력
	foo1->die();		// 죽였다.	
	foo1->sayHello();		// 호출이 된다?
	foo1->sayMyId();		// 이것도..--;

2.2. Case2 - 포인터 NULL로 하고 메소드 호출하기

~cpp 
	Foo* foo2 = NULL;		// 자세한 설명보세요.		
	foo2->sayHello();		// Hello 를 출력한다.(음--;)
	foo2->sayMyId();		// debug, release 모두 동작할 수 없다. 
	foo2->die();		// debug, release 모두 동작할 수 없다.	

2.3. Case3 - 지역변수로 선언

~cpp 
	Foo foo3;
	foo3.sayHello();		// Hello World 출력
	foo3.sayMyId();		// id 출력
	foo3.die();		// debug mode에서 assertion error
	foo3.sayHello();		// release 에서 동작 가능
	foo3.sayMyId();		// release 에서 동작 가능

DeleteMe 이렇게 보이는 것도 좋지만, 실행할수 있는 완전한 소스 형태를 제시하는 편이 더 좋을것 같습니다. --NeoCoin

3. 자세한 설명

위의 "간단한 설명=코드"를 인용하면서 설명합니다.

C++의 목표는 기존 C의 성능을 해하지 않으면서 OOP를 섞는 것입니다. 필연적으로 OOP적 사고에서 용납하기 어려운 코드를 실행할 수 있습니다. OOP를 C의 구현 위에서 해야 됩니다.

이 이상하게 보이는 코드를 이해하기 위해서는 C++의 원론적인 배경지식이 필요합니다.
  • 첫째로, C++의 클래스와 인스턴스 생성을 알아봅시다.
  • 둘째로, 인스턴스에 귀속된 멤버 함수들을 실행하는 것을 생각해 보겠습니다.
  • 마지막으로, 위의 문제제기에 대해 분석해 봅시다.

3.1. C++에서 클래스의 선언과 인스턴스의 생성입니다.

자신이 컴파일러가 되었다고 가정해 봅시다. 우리가 class를 선언하고 컴파일하려면 프로그램의 영역에 class 의 Data 들을 저장할 수 있는 "class 틀"의 정보를 담아 놓을 곳이 필요합니다.
~cpp 
class Foo{
public:
    int id;  // <- 이 부분의 정보 입니다.
};
C++은 Strong typed language 이므로 컴파일 시간에 모든 형이 선언되고, 사용되는 것을 검증할 수 있습니다. 하지만 이는 다음과 같은 지역 변수에 국한합니다.
~cpp 
Foo foo4; 
new 키워드로 할당시에는 runtime 에 class 의 instance 를 찍어 낼수 있어야 합니다. 이를 위해 프로그램 안에는 위의 id가 int 라는 정보를 담는 class의 "class 틀" 정보를 담는 곳이 필요합니다.

여기까지가, class 와 struct 키워드가 하는 동일한 작업입니다. 그리고, class 에는 몇가지 더 생각해야 하는데, 그중 하나가 foo 를 이용해서 어떠한 member 함수를 호출할 수 있는가 입니다.

그러나, 컴파일러인 우리는 이 정보를 지역 변수이든, new 로 할당이든 컴파일 시간에 인자의 type을 보고 함수 호출 유효성을 확인하고, 적절한 함수 포인터를 함수 호출하는 부분에 넣어 줄수 있습니다. 그리고 실행할 수 있는데 이 과정을 두번째에서 설명합니다.

그외 class와 instance의 생성시 vpt와, 상속 관계에 대한 pointer 정보가 더 들어 가야 합니다. 그러나 여기에서는 생각하지 않습니다. 둘째로 넘어갑니다

3.2. instance 에 귀속된 멤버 함수들을 실행해 봅시다.

자 계속 우리는 컴파일러 입니다. 컴파일러인 우리는 member 함수 호출 부분을 함수의 실행코드를 가리키는 함수 포인터로 모두 교체하였습니다. 그리고 인간으로 돌아옵시다.

C++ 표준안에서 전역에서 함수 호출과, instance에 귀속된 멤버 함수들의 호출을 가리지 않습니다. 함수 선언과 멤버 함수 선언의 함수 실행 코드는 모두 동일 방법으로 선언되고, 모두 동일한 메커니즘의 함수 포인터를 이용해서 호출합니다.

이러한 전제라면, 한가지 의문이 생깁니다.
전역 함수와 동일한 함수 선언의 형태라면 각각의 instance에 어떻게 접근하는가?

무슨 소리냐 하면,
~cpp 
class Foo{
public: 
	int id;
	void sayMyId(){
		cout << "My Id is no " << id << endl;
	}
};
라는 함수는 각 instance의 id 라는 인자에 접근합니다.
그러나 ~cpp Foo::sayMyId() 같은 아무런 인자 없는 함수의 실행코드가 함수 선언 영역에 세팅된다면, id 라는 인자에 접근할수 없습니다.

C++ 에서는 이런 한계를 class 에 귀속된 함수들의 처음 인자로 해당 class 의 포인터를 묵시적으로 선언해서 해결하고 있습니다. 즉,
~cpp 
Foo:sayMyId()
라는 함수 실행 코드가 함수 영역에 선언될때 컴파일러가
~cpp 
Foo:sayMyId(Foo* x)
라는 형태의 함수로 선언하고, 실행할수 있도록 만듭니다. 그리고, 호출한다면 ~cpp Foo* 부분에
~cpp 
foo1->sayMyId(foo1);
형태로 함수 포인터를 세팅해서 함수 코드를 실행합니다.

C++은 이러한 다소 황당한 수로 C와의 컴파일시 호환성을 지키면서 OOP를 구현합니다.

사족. 이러한 사연이 class내에서 static 멤버 함수를 선언하고 instance에서 호출할때 instance 의 멤버 변수에 접근하지 못하는 이유가 됩니다. static 함수로 선언 하면 묵시적으로 pointer 를 세팅하지 않고 함수를 호출합니다.

이런 배경 지식을 바탕으로 다음으로 넘어갑니다.

3.3. 위의 문제제기에 대해 분석해 봅시다.

다음과 같은 코드를 실행하면,
~cpp 
	Foo* foo1 = new Foo();	// Create! 를 출력한다.
	foo1->sayHello();		// Hello 출력
	foo1->sayMyId();		// id 출력
	foo1->die();		// 객체 삭제
	foo1->sayHello();		// 동작 가능
	foo1->sayMyId();		// 동작 가능

이렇게 나옵니다.(윈도우즈 플랫폼에서)

~cpp 
Create! id = 0
say Hello
My Id is no 0
I suicide. Id is 0
say Hello                    // 객체 삭제(delete this)후 실행 코드
My Id is no 3604872          // 객체 삭제(delete this)후 실행 코드
이렇게 나옵니다. (C++ 주석 빼고) 위에서 문제시 되는 부분은, 후반의 두가지 ~cpp sayHello() 와 sayMyId() 일겁니다. 둘째 설명의 member 함수를 호출하는 메커니즘을 이해했다면
~cpp 
sayHello() -> sayHello(Foo*)
sayMyId() -> sayMyId(Foo*) 
형태로 호출된다는 것을 짐작할 수 있을 겁니다. 하지만 두 함수는 다른 점이 있습니다.

sayHello()는 instance variable에 접근하지 않는다는 것이고, ~cpp sayMyId() 는 접근한다는 점이지요.

따라서, 삭제후에 메모리를 청소(?) 해버리고 난후 해당 instance 부분을 실행해 보리니 id가 다음과 같이 엉뚱하게 나오지요.
~cpp 
say Hello                    객체 삭제(delete this)후 실행 코드
My Id is no 3604872          객체 삭제(delete this)후 실행 코드
instance에 사용되었던 메모리는, 해당 process의 가용 메모리로 돌아가지, 접근 금지 영역으로 세팅되지 않습니다. 이 부분은 delete this 시 해당 instance 영역의 값을 어떻게 "청소"하느냐에 따라서, 플랫폼 별로 다르게 나옵니다.
그리고
~cpp 
delete x
라는 코드는 x의 값을 변화시키지 않습니다. 변화시킬수 없습니다. 이유는 call by value 로 넘어온 x의 값을 NULL로 변경시켜봤자, 영향 받지 않는 코드가 경우가 있기 때문에, 변화시킬 필요성이 없습니다.

만약 구현 컴파일러가, 메모리를 system으로 반환하고 접근 금지 영역으로 설정한다면, 다음 강제 접근시 access 에러가 나겠지요. 이러한 비효율적 구현 플랫폼 없을 겁니다.

foo2~3 이러한 배경 지식을 이용해서 결과를 이해할수 있는 보충 설명입니다.

그 중 foo2는 C++에서 볼수 있는 웃긴 예제지요. (foo3도 웃기지만)
~cpp 
	Foo* foo2 = NULL;		//  자세한 설명보세요.		
	foo2->sayHello();		// Hello 를 출력한다.
이 코드의 결과는
~cpp 
say Hello
과 같이 잘 동작합니다. 호출 형태가
~cpp 
Foo::sayHello(foo3)  -> Foo::sayHello(NULL)
이 된것이고, sayHello 내에는 pointer 값 NULL 을 이용한 접근이 없으므로 아무런 에러가 발생하지 않습니다.

OOP적인 사고로 실행할 instance가 없으므로 불가능한 코드지만, 잘 실행됩니다. C++에서 함수 실행시 해당 instance의 존재 유무를 검사하지 않습니다. 검사 할 수도 없겠지요. NULL 조차 0 이라는 pointer 값에 해당하니까요.

4. 정리

  • C++ 에서 class 의 멤버 함수를 호출할때 멤버 함수의 첫인자를 해당 class 의 instance pointer 로 묵시적으로 선언되고 호출된다.
  • 위의 호출시에 pointer 의 유효성은 확인하지 않는다.

5. 덧붙여 Java에서 비슷한 코드를 봅시다.

instance 유무를 검사하는 Java 코드를 봅시다.
~cpp 
// Main.java file
class Foo{
	public int a;
	void sayHello(){
		System.out.println("Hello");
	}
}
public class Main {
	public static void main(String[] args) {
		Foo foo= null;
		foo.sayHello();
	}
}
이를 실행하면, 다음과 같은 exception을 출력합니다. 이는 Java Language Specification 2nd (3rd가 아직 안나왔군요.) 와 jvm specification을 참고하세요.
(실행시점에 null 값인지 검사하고, 필요시 instance pool에서 instance를 pointer를 이용해서 접근하는 것으로 기억합니다. )
~cpp 
java.lang.NullPointerException
	at Main.main(Main.java:19)
Exception in thread "main" 

C++에서는 NULL 이 키워드로 존재하지 않고, 0 이라고 확실히 약속되어 있지 않기 때문에 검사자체가 불가능합니다.그러나 vm상의 언어들은 그래서 모두 null에 해당하는 키워드가 존재하고 검사가 가능합니다.

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:02
Processing time 0.0382 sec