티스토리 뷰
class Child
{
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 함수 : 실행됨
Child *temp = NULL;
temp->run();
// case 2 virtual 함수 : segfualt
Child *temp2 = NULL;
temp2->run2();
// case3 멤버변수 : segfault
Child *temp3 = NULL;
std::cout << temp3->age << std::endl;
}
"클래스의 멤버변수와 멤버함수는 객체가 할당될 때 메모리에 올라가기 때문에, 할당 후에 접근이 가능하다." 고 생각했었다.
그래서 null pointer로 접근한 case 2나 case 3에서 에러가 발생하는 건 납득했지만, case 1의 경우 출력이 되는 이유를 알지 못했다.
이를 확인하기 위해선 '클래스의 메모리 구조'에 대한 이해가 필요하다 (static, virtual, binding 등 키워드도 알아야 함)
1. 기본적인 메모리 구조에 대해서는 아래 글을 참고하자
2. 클래스의 멤버
클래스에는 멤버변수와 멤버함수가 있고 각각 정적(static), 비정적(non-static)으로 선언이 가능하다.
또한 멤버함수는 가상(virtiual)으로 선언이 가능한데, virtual과 static은 동시 사용이 불가능하다.
표로 정리하면 아래와 같다.
C++ 클래스 | static | non-static | |
멤버변수 | static | non-static | |
멤버함수 | static | non-static | non-static + virtual |
3. static, virtual, binding은 간단하게 정리
static 멤버를 사용하는 이유
- static 멤버변수: 여러 인스턴스가 공유해서 사용할 수 있는 변수
- static 멤버함수: 인스턴스가 생성되지 않더라도 호출한 필요한 함수
가상함수를 사용하는 이유
: 동적바인딩을 통한 다형성 - 인스턴스의 실제 클래스에 따라 오버라이딩된 함수 호출이 가능
4. 클래스의 메모리 구조
클래스 안에 멤버변수와 멤버함수가 있기 때문에 둘이 같은 메모리 공간에 위치한다고 생각할 수 있지만, 그렇지 않다
- 인스턴스: 생성될 때마다 각각 다른 메모리 주소에 할당된다. (정적할당:스택 or 동적할당:힙)
- non-static 변수: 인스턴스의 생성과 동시에 할당된다. (정적할당: 스택 or 동적할당: 힙)
- static 변수: 인스턴스 생성 시가 아니라, 프로그램 시작 시 저장되고 프로그램이 종료되어야 소멸한다. (데이터영역) -> 여러 인스턴스가 공유한다.
- 멤버함수: static, non-static 모두 프로그램 시작 시 코드영역에 저장된다. 멤버함수는 인스턴스마다 다르게 동작하는 것이 아니기 때문에 인스턴스마다 함수가 할당되면 비효율적일 것이다. 따라서 해당 클래스의 모든 인스턴스는 코드 영역에 있는 멤버함수를 공유하면서 사용한다. (코드영역)
- 함수를 공유할 때, 함수를 호출한 인스턴스의 구분은 어떻게 하는 것일까? (ex 함수 내부에서 멤버변수를 사용한다면, 해당 인스턴스 정보가 필요) 멤버 함수가 코드영역에 올라갈 때 객체의 포인터를 hidden 인자로 전달받도록 변경되기 때문에, 어느 인스턴스에서 호출한 함수인지 확인할 수 있다.
인스턴스가 생성될 때 같이 생성되는 멤버는 밑줄친 non-static 멤버변수뿐이다. 따라서 클래스의 자체의 크기에도 non-static 멤버 변수만 영향을 미친다.
여기서 추가로 생각해볼 부분이 바로 virtual 함수다.
- 클래스에 가상함수를 선언하면 가상함수테이블이 생성되고(어디에? 아마 코드영역..??) 클래스 내부적으로 가상함수테이블을 가리키는 포인터를 갖는다. (4 or 8바이트)
- 가상함수의 개수에 관계없이 포인터의 개수는 1개고, 클래스의 크기에 영향을 준다.
- 가상함수테이블은 클래스단위로 생성되고, 가상함수테이블 포인터는 인스턴스단위로 생성된다.
정리해보면...
인스턴스가 할당될 때 같이 생성되는, 즉 인스턴스를 통해 접근이 가능한 멤버는 non-static 멤버변수와 가상함수(테이블포인터)다.
따라서 얘네들은 null pointer를 통해 접근하면 바로 에러가 발생한다.
static은 본래 인스턴스 없이 클래스이름을 통해 접근이 가능하다.
non-static, non-virtual 멤버함수는 코드영역에 생성되어서 null포인터를 통해 접근은 가능하다.
다만, 해당 멤버함수 내부에서 객체에 접근(non-static, non-virtual 멤버함수 외의 모든 멤버들)할 때 에러가 발생한다.
다시 한 번 위의 코드를 살펴보자 (static case 추가)
class Child
{
public:
int age;
void run() {
std::cout << "자식 run" << std::endl;
}
virtual void run2() {
std::cout << "자식 run" << std::endl;
}
static void run3() {
std::cout << "자식 run" << std::endl;
}
};
int main(void)
{
// case 1 non-virtual 함수 : 실행됨
Child *temp = NULL;
temp->run();
// case 2 virtual 함수 : segfualt
Child *temp2 = NULL;
temp2->run2();
// case 3 멤버변수 : segfault
Child *temp3 = NULL;
std::cout << temp3->age << std::endl;
// case 4 static 함수 : 실행됨
Child *temp4 = NULL;
temp4->run3();
}
case 1 :
출력이 가능한 이유는 run() 함수가 객체에 접근하고 있지 않기 때문이다.
이 멤버함수 내부에 객체에 접근하는 코드를 추가한다면 case 2, 3과 동일하게 segfault가 발생하게 된다.
case 4:
출력이 가능한 이유는 run3() 함수가 static이기 때문에 인스턴스와 상관없기 때문이다.
이 멤버함수 내부에서 static 멤버 변수에 접근하는 코드를 추가해도 정상적으로 실행이 된다. (당연히 non-static은 컴파일조차 안됨)
표로 마지막 정리
NULL 포인터로 호출가능? | static | non-static | |
멤버변수 | O | X | |
멤버함수 | O | non-static (객체와 관계 없는 함수만 O) |
non-static + virtual (X) |
하지만 null pointer를 통한 모든 접근은 undefined behavior이기 때문에 이렇게 사용해서는 안됨! (자바에서는 전부 NullPointerException 발생함)
왜 얘는 에러고 쟤는 아닌지 궁금해서 정리한 것일 뿐!
static 호출은 클래스 이름을 통해서, non-static 호출은 인스턴스를 통해서 하자!
참조
- ★null 포인터로 함수 호출? https://stackoverflow.com/questions/24528886/calling-function-through-pointer-without-allocating-memory-from-object
- sizeof 클래스 with 변수, 함수, 가상함수 in C++? https://stackoverflow.com/questions/9439240/sizeof-class-with-int-function-virtual-function-in-c
- 클래스의 크기 https://blog.naver.com/tipsware/221090063784
- 가상함수(virtual function)에 대하여 https://42place.innovationacademy.kr/archives/8728
- C++ 가상소멸자, 가상함수 테이블, 순수 가상함수, 추상 클래스 https://blog.naver.com/vjhh0712v/221550613198
추가 정리가 필요한 부분
1. 가상함수테이블은 코드영역에 생성되는 것이 맞는지
2. null->(non-virtual, non-static 함수)에서 객체에 접근하면 에러가 발생하는데.. 이 객체의 범위에 static이 포함되는 이유는??
클래스이름::변수 를 사용하면 인스턴스가 필요 없으니까 접근 가능하다고 생각했는데 아니던데..
'42 > cpp' 카테고리의 다른 글
생성자, 복사생성자, 복사대입연산자, 소멸자 (0) | 2021.05.27 |
---|---|
참조자 (0) | 2021.05.26 |
메모리구조, 정적할당과 동적할당 (0) | 2021.05.25 |
객체지향, 접근지정자 (0) | 2021.05.25 |
네임스페이스, 표준입출력 (0) | 2021.05.18 |