객체 지향 프로그래밍
객체 지향 프로그래밍은 4가지 특징이 있다
상속, 다형성, 추상화, 캡슐화이다
이 특징들에서 공부하다보니
가상함수에 대해서 자세히 공부해야겠다는 생각을 했다
가상 함수
가상함수란?
부모 클래스를 상속받을 클래스에서 재정의할 것으로 기대하고 정의해놓은 함수다
virtual 키워드로 함수 앞에 붙여서 가상함수를 나타내고
재정의는 override키워드를 사용하여 할 수 있다
class Parent
{
public:
virtual void Func1();
}
class Child :public Parent
{
public:
virtual void Func1() override;
}
바인딩이란 함수나 변수의 주소가 결정되는 것을 말한다
Early binding( = static binding) : 컴파일 타임에 함수나 변수의 주소가 결정되는 것
Late binding( =Dynamic binding) : 런타임에 결정되는 것
가상함수는 Late binding( =Dynamic binding)이다
가상함수는 프로그램 실행 시에 객체를 결정하기 때문에
가상함수의 호출은 컴파일러가 어떤 함수를 호출해야하는지 미리 알 수 없어서 Late binding을 하게 된다
class Parent
{
public:
void Func1()
{
cout << "Parent_Func1" << endl;
}
};
class Child : public Parent
{
public:
void Func1()
{
cout << "Child Func1" << endl;
}
};
void main()
{
Parent* p = new Parent;
Child* c = new Child;
p->Func1();
p = c;
p->Func1();
}
이러한 상황에서는 Func1()은 가상함수가 아닌 일반 함수로 만들어져있다
따라서 Early binding이 이루어지고
main함수에서 p에 c의 주소를 넣어주었음에도 불구하고 p->Func1()을 호출하면
이미 p의 Func1()주소로 바인딩 되어있기 때문에 p의 Func1()이 호출된다
class Parent
{
public:
virtual void Virtual_Func1()
{
cout << "Parent_Func1" << endl;
}
};
class Child : public Parent
{
public:
virtual void Virtual_Func1() override
{
cout << "Child Func1" << endl;
}
};
void main()
{
Parent* p = new Parent;
Child* c = new Child;
p->Virtual_Func1();
p = c;
p->Virtual_Func1();
}
반면에 이처럼 가상함수로 Func1()을 선언한다면
Late binding( = Dynamic binding)이 이루어지기 때문에 p에 c의 주소값을 넣어준 뒤에 p->Virtual_Func1()을 호출해주면 Child클래스에 있는 Func1()이 호출되는 것이다.
순수 가상 함수
순수 가상 함수는 부모 클래스에서 구현하지 않고 자식 클래스에서 반드시 재정의해야하는 가상함수를 말한다
class Parent
{
public:
virtual void Func1() = 0;
}
순수 가상 함수는 가상함수에 = 0 을 붙여서 선언하며
반드시 자식 클래스에서 재정의해야한다
추상클래스와 인터페이스
추상클래스란 순수 가상 함수가 하나라도 포함되어 있는 클래스를 말하고
인터페이스는 순수 가상 함수만으로 이루어져 있는 클래스를 말한다
추상 클래스의 특징
- Abstract 키워드 사용
- 인스턴스화 할 수 없다 (=객체로 생성할 수 없다)
- 상속으로 기능을 확장시키기 위해서 사용한다
인터페이스의 특징
- 생성자, 소멸자, 연산자를 포함할 수 없다
- 추상 클래스와 마찬가지로 인스턴스화 할 수 없다
- 필드 및 정적 멤버는 허용되지 않는다
- 인터페이스를 통해 약한 결합으로 만들 수 있다 (loosely coupling)
- 메서드를 반드시 재정의해줘야하 하므로 구현 객체가 같은 동작을 한다는 것을 보장한다
상속에서의 초기화 과정
자식 클래스의 객체를 생성할 때, 먼저 부모 클래스의 객체가 생성된다
자식 클래스의 이니셜라이져를 통해 부모 클래스 생성자에게 부모 클래스 정보를 제공한다
자식 클래스 생성자는 자식 클래스에 새로 추가된 데이터 멤버들을 초기화한다
상속받은 멤버들을 초기화하는 일은 부모 클래스의 생성자에서 담당한다
이니셜라이저를 사용하여 어떤 부모 클래스의 생성자를 사용할 것인지 나타낼 수 있다
이니셜라이저를 생략하면 디폴트 부모 클래스의 생성자가 사용된다
#include <iostream>
using namespace std;
class Table
{
public:
Table(string n, int a)
:name(n), age(a)
{
cout << "Parent 생성자" << endl;
}
public:
void Print()
{
cout << name << ", " << age << endl;
}
private:
string name;
int age;
};
class Info : public Table
{
public:
Info(string c, string n, int a)
:Table(n, a)
{
country = c;
cout << "Info 생성자" << endl;
}
Info(string c, Table& t)
:Table(t), country(c)
{
cout << "Info 생성자2" << endl;
}
public:
string Country() { return country; }
private:
string country;
};
void main()
{
Table t("아무개", 30);
Info info("대한민국", "홍길동", 25);
cout << "국적 : " << info.Country() << " / ";
info.Print();
Info info2("미국", t);
cout << "국적 : " << info2.Country() << " / ";
info2.Print();
}
main함수를 보면
첫 번째 Print에서는 info의 생성자에 이니셜라이저를 통해서 정보를 넘겨주었고
자식클래스인 Info클래스가 부모 클래스인 Table 클래스의 생성자에 정보를 넘겨주어 해당 정보가 출력되는 것을 알 수 있다
두 번째 Print에서는 Table에 정보를 먼저 넣어주고 그 Table객체를 넘겨주는 방식을 사용해보았다
그리고 생성자의 호출 순서를 보았을때 생성자는 부모클래스가 먼저 생성된다는 것을 알 수 있다
또한 초기화는 자식 클래스에서 초기화되고 그 값이 부모클래스로 넘어가는 것도 확인할 수 있다
가상 함수 테이블
가상 함수 테이블은 가상 함수가 하나라도 포함되어 있는 클래스가 있을 때 컴파일 시에 생성되는 테이블이다
클래스마다 하나씩 생성되며 가상 함수로 선언된 멤버 함수의 주소들이 배열의 형태로 이루어져 있다
클래스는 이 가상 함수 테이블을 가르키는 포인터를 가진다
#include <iostream>
using namespace std;
class Parent
{
public:
virtual void Virtual_Func1()
{
cout << "Parent_Func1" << endl;
}
virtual void Virtual_Func2()
{
cout << "Parent_Func2" << endl;
}
void Func3()
{
cout << "Parent_Func3" << endl;
}
};
class Child : public Parent
{
public:
virtual void Virtual_Func1()
{
cout << "Child Func1" << endl;
}
};
void main()
{
Parent* p = new Parent;
Child* c = new Child;
p->Virtual_Func1();
c->Virtual_Func1();
p->Virtual_Func2();
c->Virtual_Func2();
p->Func3();
c->Func3();
}
컴파일 후에 조사식을 걸어서 보면
p와 c클래스 각각에 __vfptr이라는 이름으로 가상함수 테이블이 만들어져있는 것을 확인할 수 있다
보이다시피 [0] [1]로 배열형태로 만들어져 있으며 가상 함수에 대해서만 만들어져 있다
각 배열의 값을 살펴보면
먼저 p클래스에서는 [0] : Func1()과 [1] : Func2()의 주소가 들어가있고
c클래스에서는 p클래스의 가상함수를 상속받아 재정의한 [0] : Func()1과 부모클래스(p)의 [1] : Func2()의 주소가 들어있다
주소값을 보면 Func2()의 주소값은 Parent클래스와 Child클래스의 값이 같지만
Child클래스에서 재정의된 Func1()은 주소값이 다르게 들어가 있는 것을 확인할 수 있다
생성자에서 가상 함수를 사용한다면?
기본적으로 생성자에서 가상함수를 선언할 수 없다
그 이유는 자식 클래스에서 객체의 생성은 부모 클래스 생성자가 아니라 자식 클래스 생성자를 호출한다
그러고 나서 자식 클래스 생성자가 부모 클래스 생성자를 사용한다
따라서 자식 클래스는 부모 클래스 생성자를 상속하지 않기 때문에 가상으로 만들 이유가 없다
소멸자에서 virtual
부모 클래스라면 소멸자는 virtual로 선언해야한다
virtual로 선언하지 않으면
class Parent
{
public:
Parent()
{
cout << "Parent_생성자" << endl;
}
~Parent()
{
cout << "Parent_소멸자" << endl;
}
};
class Child : public Parent
{
public:
Child()
{
cout << "Child_생성자" << endl;
}
~Child()
{
cout << "Child_소멸자" << endl;
}
};
void main()
{
Parent* p = new Child;
delete p;
}
이처럼 Parent * p = new Child; 로 업캐스팅 되었을 경우에
Child의 소멸자는 호출되지 않아 메모리 누수(Memory Leak)가 발생할 수 있다
class Parent
{
public:
Parent()
{
cout << "Parent_생성자" << endl;
}
virtual ~Parent()
{
cout << "Parent_소멸자" << endl;
}
};
class Child : public Parent
{
public:
Child()
{
cout << "Child_생성자" << endl;
}
~Child()
{
cout << "Child_소멸자" << endl;
}
};
void main()
{
Parent* p = new Child;
delete p;
}
'> 개념' 카테고리의 다른 글
[게임 수학] 벡터의 내적과 외적 활용 (0) | 2024.01.06 |
---|---|
[C++] 생성자 (0) | 2023.12.01 |
[C++] 변수와 메모리 공간 (0) | 2023.11.24 |
[C++] #pragma pack (0) | 2023.06.24 |
[게임 수학] 사원수 (Quaternion) (0) | 2023.06.04 |