개발자 면접 공부/C-C++

NULL 포인터로 객체의 함수가 왜 호출되냐?

chogyujin 2024. 3. 26. 16:29
728x90

1. 개요

오늘은 신기한 포스터를 봐서 개시해볼려고 합니다.


2. 이게 왜 됨?

 

class Parent
{
public:
	int age;
	void run() 
	{
		std::cout << "자식 run" << std::endl;
	}

	virtual void run2() 
	{
		std::cout << "자식 run" << std::endl;
	}
};

int main(void)
{
	// case 1 non-virtual 함수 : 실행됨
	Parent* temp = NULL;
	temp->run();

	// case 2 virtual 함수 : segfualt
	Parent* temp2 = NULL;
	temp2->run2();

	// case3 멤버변수 : segfault
	Parent* temp3 = NULL;
	std::cout << temp3->age << std::endl;
	
	return 0;

}

클래스의 맴버 변수와 맴버 함수는 객체가 할당될 때 메모리에 올라가기 때문에, 할당 후 접근이 가능합니다. 이걸 보고 난뒤에는 예상을 못하겠습니다.
그래서인지 nullptr로 접근한 case 2 나 case 3는 어느정도 납득이 갑니다.
근데 왜 case1은 이렇게 될까요?

이를 확인하기 위해 클래스의 메모리 구조에 대한 이해가 필요합니다.(static, virtual, binding 등 키워드도 알아야합니다.)


3. 메모리 구조

 

출처 : https://stdbc.tistory.com/124

1. 클래스의 멤버

클래스에는 멤버변수와 멤버함수가 있고 각각 정적(static), 비정적(non-static)으로 선언이 가능하다.

또한 멤버함수는 가상(virtiual)으로 선언이 가능한데, virtual과 static은 동시 사용이 불가능하다. 

표로 정리하면 아래와 같다.

C++ 클래스 static non-static
멤버변수 static non-static
멤버함수 static non-static non-static + virtual

 

2. static, virtual, binding은 간단하게 정리

static 멤버를 사용하는 이유

  • static 멤버변수: 여러 인스턴스가 공유해서 사용할 수 있는 변수
  • static 멤버함수: 인스턴스가 생성되지 않더라도 호출한 필요한 함수

가상함수를 사용하는 이유

: 동적바인딩을 통한 다형성 - 인스턴스의 실제 클래스에 따라 오버라이딩된 함수 호출이 가능

3. 클래스의 메모리 구조

클래스 안에 멤버변수와 멤버함수가 있기 때문에 둘이 같은 메모리 공간에 위치한다고 생각할 수 있지만, 그렇지 않다

 

  • 인스턴스: 생성될 때마다 각각 다른 메모리 주소에 할당된다. (정적할당:스택 or 동적할당:힙)
  • non-static 변수: 인스턴스의 생성과 동시에 할당된다. (정적할당: 스택 or 동적할당: 힙)
  • static 변수:  인스턴스 생성 시가 아니라, 프로그램 시작 시 저장되고 프로그램이 종료되어야 소멸한다. (데이터영역) -> 여러 인스턴스가 공유한다.
    출처 : https://stdbc.tistory.com/124
    • 멤버함수: static, non-static 모두 프로그램 시작 시 코드영역에 저장된다. 멤버함수는 인스턴스마다 다르게 동작하는 것이 아니기 때문에 인스턴스마다 함수가 할당되면 비효율적일 것이다. 따라서 해당 클래스의 모든 인스턴스는 코드 영역에 있는 멤버함수를 공유하면서 사용한다. (코드영역) 
    • 함수를 공유할 때, 함수를 호출한 인스턴스의 구분은 어떻게 하는 것일까? (ex 함수 내부에서 멤버변수를 사용한다면, 해당 인스턴스 정보가 필요) 멤버 함수가 코드영역에 올라갈 때 객체의 포인터를 hidden 인자로 전달받도록 변경되기 때문에, 어느 인스턴스에서 호출한 함수인지 확인할 수 있다.
    인스턴스가 생성될 때 같이 생성되는 멤버는 밑줄친 non-static 멤버변수뿐이다. 따라서 클래스의 자체의 크기에도 non-static 멤버 변수만 영향을 미친다.여기서 추가로 생각해볼 부분이 바로 virtual 함수다.
    • 클래스에 가상함수를 선언하면 가상함수테이블이 생성되고(어디에? 아마 코드영역..??) 클래스 내부적으로 가상함수테이블을 가리키는 포인터를 갖는다. (4 or 8바이트)
    • 가상함수의 개수에 관계없이 포인터의 개수는 1개고, 클래스의 크기에 영향을 준다. 
    • 가상함수테이블은 클래스단위로 생성되고, 가상함수테이블 포인터는 인스턴스단위로 생성된다.

4. 요약

  1. 맴버 함수는 코드 영역에 저장이된다.
  2. 가상 함수 테이블은 코드 영역에 저장되지만 그걸 가르키는 가상함수 포인터가 인스턴스화를 안했기때문에 segfault가 뜬다

5. Ref

https://stdbc.tistory.com/124

 

[C++] NULL 포인터로 객체의 함수가 호출되는 이유

class Child { public: int age; void run() { std::cout

stdbc.tistory.com