C++에서 일반적으로 함수를 사용하는 방법을 static binding이라고 한다.
이는, 함수를 정의해주는 바인딩이 컴파일 타임 때 호출될 함수가 정의된다는 의미이고
이렇게 컴파일 타임 때 정의되는 함수는 컴파일시간에 정의되기 떄문에, 실행시에 함수가 바뀌지 않는다.
그래서 실행시간에 바인딩을 검색할 필요가 없기 때문에, 실행속도가 동적 바인딩에 비해 빠르다.
동적바인딩은, 실행시간에 호출을 처리함으로써 객체의 타입이나 상태에 따라 실행 흐름을 자유롭게 제어할 수 있게된다. 이로써 객체지향 프로그래밍 언어에서의 다형성과 유연성을 확보할 수 있게된다.
그러나, 실행시간에 바인딩을 검색해야 하므로 정적 바인딩보다는 실행속도가 느릴 수 있다.
- 정적 바인딩 ( Static Binding )
- 컴파일 시간에 변수 또는 함수호출을 처리하는 방식
- 변수의 타입 또는 함수의 식별자와 해당 식별자에 대한 메모리 위치를 직접 연결
- 컴파일 시간에 바인딩이 결정, 실행시간에 바인딩이 변경되지 않음.
- 컴파일 언어에서 주로 사용되며, 실행속도가 빠르고 예층가능
- 정적 바인딩은 실행시간에 객체의 탕비이나, 상태에 따라 실행흐름을 제어하는데 에는 제한적일 수 있음.
- 동적 바인딩 ( Dynamic Binding )
- 실행시간에 변수 또는 함수호출 을 처리하는 방식
- 변수의 타입 또는 함수의 식별자와 메모리 위치는 실행시간에 결정됨
- 객체의 타입이나 상태에 따라 실행흐름을 제어할 수 있음
- 객체 지향 프로그래밍 언어에서 주로 사용되며, 다형성과 유연성을 확보할 수 있음.
- 실행시간에 바인딩을 검색해야 함 으로 정적 바인딩 보다는 실행 속도가 느릴 수 있음.
다음은, 바인딩 에 대한 코드 예시이다.
#include <iostream>
// static binding(Early binding)
// binding 이 실행 이후에 결정됨 전역바인딩 [ 컴파일 타임 때 호출할 함수가 정해짐 ]
// Dynamic binding(late binding)
// binding 이 실행 도중에 결정됨 도중바인딩 [ 런타임 도중 중 호출할 함수가 정해짐 ]
// 함수 포인터로 정하는느낌
using namespace std;
int add(int x, int y) { return x + y; }
int subract(int x, int y) { return x - y; }
int multiply(int x, int y) { return x * y; }
int main()
{
int x, y;
cin >> x >> y;
int op;
cout << "0 : add, 1 : subtract, 2 : multiply" << endl;
cin >> op;
// 정적 바인딩
int result;
switch (op)
{
case 0: result = add(x, y); break;
case 1: result = subract(x, y); break;
case 2: result = multiply(x, y); break;
}
cout << result << endl;
// 동적 바인딩
// 포인터 형태로 조금더 객체지향적으로 나눌 때 사용하게 된다.
int(*funcPtr)(int, int) = nullptr;
switch (op)
{
case 0: funcPtr = add; break;
case 1: funcPtr = subract; break;
case 2: funcPtr = multiply; break;
}
cout << result << endl;
return 0;
}
위와 같이, 동적과 정적 바인딩의 차이는 switch를 통하여 원하는 함수를 실행시켜줄 때 찾아볼 수 있다.
정적 은, 해당되는 switch부분에서 함수를 실행시켜주는 역활을 하고,
동적 은, 해당되는 switch부분에서 함수를 지정해주는 역활을 한다.
C++에서 기본 객체 class의 선언시에는 byte단위로 오버라이딩을 통해 함수를 상속해주어 동작시킬 수 있다.
이때, 상속하여 덮어씌워지는 함수는 가상화 함수 로서, virtual 선언하여, 가상함수 임을 알려주어야 한다.
그러나, 가상함수를 사용할 때, 오버라이딩 하지 않는 함수 또한 상속받는 자식class에서 재선언됨으로 하지않아도 되는 함수의 재선언을 막아주어야 할 필요가 있다.
다음은 그러한 코드 예시이다.
#include <iostream>
using namespace std;
// c++ 기본의 객체 class 선언.
class A
{
int a; // 4byte;
public:
void func();
};
/*
class Base Base my_base;
*__vfptr ---------------------------> -Base virtual-table
virtual func1() <--------------------------- -func1()
virtual func2() <--------------------------- -func2()
^
|
\
\________________________________
\
class Derived : public Base \ Derived my_derived;
*__vfptr (inherited) --------------------------->\ -Derived virtual_table
virtual func1() <----------------------------\--func1() // 오버라이딩 하지 않을 키워드는 virtual을 넣어주지 않는게 좋다.
\-func2() // 이유없이 생성이 될 수 있기 때문.
*/
class Base
{
public:
virtual void func1() { cout << "func1" << endl; }
virtual void func2() { cout << "func2" << endl; }
};
class Derived : public Base
{
public:
virtual void func1() { cout << "Derived::func1" << endl; }
void func3() { cout << "func3" << endl; }
};
int main()
{
Base* bptr = new Base();
bptr->func1();
Derived* dptr = new Derived();
dptr->func1();
dptr->func2();
dptr->func3();
return 0;
}
이러한, virtual선언을 통하여 함수를 상속시키는 개념에서, 주의해야할 것 이 있는데,
업케스팅 부분에서의 소멸자에 주의하여야 한다.
일반적인 함수에서의 소멸자는. 함수의 종료 이후에 사용됨으로 부모클래스를 상속받은 자식클래스를 생성해줄 때
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A 생성" << endl; }
~A() { cout << "A 종료" << endl; }
}
class B : public A
{
public:
B() { cout << "A 생성" << endl; }
~B() { cout << "B 종료" << endl; }
}
위와같이 선언하였을 때, B를 생성하고 종료하게되면, 출력은 다음과 같이 나타날 것 이다.
A 생성
B생성
B종료
A종료
이 순서대로 나타나고 종료될 것 이다.
그러나, 이러한 부모 자식클래스를 다음과 같이 업스케일로 실행하면 어떤식으로 나타날까.
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A 생성" << endl; }
~A() { cout << "A 종료" << endl; }
}
class B : public A
{
public:
B() { cout << "A 생성" << endl; }
~B() { cout << "B 종료" << endl; }
}
int main()
{
A* def = new B;
delete def;
return 0;
}
만일 이렇게 생성하였다면, 출력은 다음과 같이 나타난다.
A 생성
B 생성
A 종료
A의 소멸자만 사용되고 B의 소멸자는 사용되지 않은것 이다.
A로 업케스팅 된다는것 은, B에있는 함수를 virtual과 변동되는 변수를 제외한 나머지 값을 A로 쓰겠다는 의미 이기 때문에, B의 소멸자가 자연스럽게 사용되지 않는다.
그렇기 때문에, 우리는 소멸자에 virtual 키워드를 붙여야 한다.
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A 생성" << endl; }
virtual ~A() { cout << "A 종료" << endl; }
// 업케스팅 하게되면 자식클래스의 소멸자가 발생하지 않는다.
};
class B : public A
{
int* arr = nullptr;
public:
B() { cout << "B 생성" << endl; }
~B()
{
//TODO : 동적해제
cout << "B 종료" << endl;
}
};
int main()
{
A* def = new B; // 업케스팅 해줌.
// 업케스팅 하게되면 자식클래스의 소멸자가 발생하지 않음.
// 그걸 방지하기 위해 소멸자에 vitrual을 넣어주는것 이 더 안전한 코딩이다.
delete def;
return 0;
}
이러한 방법을 통해, virtual 함수선언 처럼 순수 가상화 클래스,
추상클래스를 만들어 줄 수 있다.
이 추상클래스는, 대부분 명시적으로 알리기 위해 I 를 붙여서 i_def, i_list 이런식으로 인터페이스 임을 알려주어야 한다.
이러한 추상클래스는 단독으로 사용할 수 없으며, abstract를 붙여야만 사용이 가능하다.
다음은, 추상클래스를 사용하는 코드 예시이다.
#include <iostream>
// pure_virtual_function
// abstract_class 순수가상화 class
// Interface 기능을 구현할 것을 약속한 추상의 형식
// 명시적으로 알리기 위해 대부분 i를 붙여서 사용하는 경우가 많다.
using namespace std;
class animal abstract // 추상클래스로 만들때를 가정 abstract 붙인다.
{
string name;
public:
animal(const string& name) : name(name) {}
auto getname()const { return name; }
virtual void sleep() const final
{
cout << "sleep" << endl;
} // 공통된 특성이기 떄문에 궂이 vitrual로 써줄 필요가 없지만, 모두가 공통되게 sleep할것 이기 때문에, final로 막아준다.
virtual void speak() const abstract = 0;
};
class cat : public animal
{
public:
cat(string name) : animal(name) {}
void speak() const { cout << "야옹" << endl; }
};
class dog : public animal
{
public:
dog(string name) : animal(name) {}
void speak() const { cout << "멍멍" << endl; }
};
class fox : public animal
{
public:
fox(string name) : animal(name) {}
};
int main()
{
//animal ani("asdfa"); // 가상함수는 실체화 불가 오류가 나타난다.
cat c("cc");
dog d("dd");
//fox f("ff"); // 가상화함수를 인스턴스화 해주지 않았기 때문에 사용할 수 없다.
c.sleep();
d.sleep();
c.speak();
d.speak();
return 0;
}
위와같이, 가상함수를 사용하여, 코드의 대략적인 조건과 예시를 만들어줄 수 있다.
그러나, abstract선언된 class는 단독으로 사용하지 못하도록 컴파일러에서 제한한다.
이는, 인터페이스의 구조 자체가 값을 가지지 않도록 프로그래밍 되는 경우가 많기 때문에, 단독사용되는 실수가 생길경우 컴파일러는 확인하지 못할 큰 오류가 나타날 수 있기 때문이다.
즉, 추상클래스를 단독으로 사용하게 되면, 구현되지 않은 메서드가 존재하게 됨 으로 실행할 수 없는 클래스가 되어버리기 때문이다.