class에서 순수가상화 class를 사용하여, 인터페이스를 구축할 수 있다. 순수 가상화 선언 시 단독사용이 안되고, 필요할 시 자식class로 호출해야한다. class IErrorLog // 순수 가상함수로만 이루어진 인터페이스 이다. { public: virtual ~IErrorLog() = default; virtual bool ReportError(const char* const error) abstract; // 순수 가상화 선언 }; class FileErrorlog : public IErrorLog { public: virtual bool ReportError(const char* const error) override { cout
C++에서 일반적으로 함수를 사용하는 방법을 static binding이라고 한다. 이는, 함수를 정의해주는 바인딩이 컴파일 타임 때 호출될 함수가 정의된다는 의미이고 이렇게 컴파일 타임 때 정의되는 함수는 컴파일시간에 정의되기 떄문에, 실행시에 함수가 바뀌지 않는다. 그래서 실행시간에 바인딩을 검색할 필요가 없기 때문에, 실행속도가 동적 바인딩에 비해 빠르다. 동적바인딩은, 실행시간에 호출을 처리함으로써 객체의 타입이나 상태에 따라 실행 흐름을 자유롭게 제어할 수 있게된다. 이로써 객체지향 프로그래밍 언어에서의 다형성과 유연성을 확보할 수 있게된다. 그러나, 실행시간에 바인딩을 검색해야 하므로 정적 바인딩보다는 실행속도가 느릴 수 있다. 정적 바인딩 ( Static Binding ) 컴파일 시간에 변수..
C++ 의 상속은 이중상속이 가능하다. C++ 의 상속과정에서 상속자를 두개 넣으면 두개의 class의 public 과 protected에 대한 값을 사용할 수 있다. 다음은 이러한 이중상속 에 대한 코드이다. #include // 이중상속 에 대한 이야기 using namespace std; class USBDevice { int id; public: USBDevice() = default; USBDevice(int id) : id(id) {} auto GetId() const { return id; } void PlugAndPlay() {} }; class NetworkDevice { int id; public: NetworkDevice() = default; NetworkDevice(int id) ..
C++에서의 다형성인 오버로딩, 오버라이딩 중 오버 라이딩을 사용하는 부모자식 클레스에 대한 내용이다. C++ 에서는 부모자식 클래스에 대한 함수 오버라이딩과 privet을 제외한 함수 공유가 가능하다. 이는 부모클래스와 자식클래스로 이어지는 것을 이야기한다. 예를들어 책이라는 묶음에 책내용이라는 자식이 있는 격 이다. 부모클래스와 자식클래스의 관계에서는 부모클래스는 기본적인 특성과 동작을 정의하고, 자식클래스는 이러한 특성과 동작을 상속받아 추가적인 기능을 추가하거나 변경할 수 있다. ( 변경은 함수 오버라이딩을 의미한다 ) 이를 통해 코드의 확장성과 유연성을 증가시킬 수 있다. 다음 코드는 기본적인 부모자식클래스의 예시이다. 코드예시는 다음과 같다. class Mother { private: int ..
const Const는 상수를 선언하기 위해 사용하는 한정자이다. const를 사용하여 변수를 선언하면, 해당 변수의 값은 const선언한. 초기화 이후에 변경할 수 없게된다. 이를통해 프로그램의 의도적인 수정을 방지하고, 코드의 안정성과 가독성을 향상시킬 수 있다. const는 다음과 같이 사용할 수 있다. 읽을 때, 좌측에 있는것 을 상수화한다. 라고 읽으면 된다. // 변수 선언시 변수 상수화 const int MAX_VALUE1 = 100; int const Max_VALUE2 = 100; // 함수의 매개변수를 상수화 int Printvalue(const int value) { return value; } int Printvalue(int const value) { return value; } ..
프로그래밍에서 객체관계는, 각 구조체 간의 상호작용을 정의한다. 이는 클래스간의 관계 라고 도 이해할 수 있으며, 총 5가지 유형으로 분류할 수 있다. 연관(Association) Assocation은 두 클래스가 서로를 사용하는, 서로가 상속된 관계라고 할 수 있다. 이는 "사용한다" 또는 "알고있다"로 정의할 수 있는데, 서로가 서로의 값을 이용하거나 함수를 사용할 수 있는 관계라고 할 수 있다. 예를들어 '학생'클래스와 '교수'클래스가 있다면, '학생'은 '교수'가 누구인지, 어떠한 과목을 가르치는지 알고, '교수'는 학생이 누구인지 몃학년인지 등의 정보를 알 수 있을것이다. 이것은 두 클래스가 독립적으로 존재하면서도 서로를 참조할 수 있는 관계를 나타낸다. 집합(Aggregation) Aggreg..
class IErrorLog // 순수 가상함수로만 이루어진 인터페이스 이다.
{
public:
virtual ~IErrorLog() = default;
virtual bool ReportError(const char* const error) abstract; // 순수 가상화 선언
};
class FileErrorlog : public IErrorLog
{
public:
virtual bool ReportError(const char* const error) override
{
cout << "writting error to a file" << endl;
return true;
}
};
class ConsoleErrorlog : public IErrorLog
{
public:
virtual bool ReportError(const char* const error) override
{
cout << "Console error to a input" << endl;
return true;
}
};
void DoSomething(IErrorLog& log)
{
log.ReportError("Error");
}
int main()
{
//IErrorLog f;
FileErrorlog fileLog;
ConsoleErrorlog consoleLog;
DoSomething(fileLog);
DoSomething(consoleLog);
return 0;
}
위와같이, 원하는 error로 타입을 변환할 수 있다.
이러한 상속에서 다이아몬드 상속 문제가 발생할 수 있다.
다이아몬드 상속문제는, 상속 시 이중상속이 한번 일어나고, 바로 다음 상속이 그 두개의 상속을 받아올 때 발생하는데,
제일 윗 단계의 값을 가져올 때 모호성 문제가 발생하는걸 이야기한다.
다음 코드는 이러한 다이아몬드 상속문제를 다룬다.
#include <iostream>
using namespace std;
/*
다이아몬드 상속 문제
A
/ \
B C
\ /
D
class A {};
class B : public A {};
class C : public A {};
class D : pulbic B, public C {};
위와같이 상속이 되면, D는 B와 C중 어떤걸 사용해야 할 지 알수가 없어져서 모호성 문제가 발생함.
*/
class PoweredDevice
{
public:
PoweredDevice(int power) { cout << "PowerdDevice" << power << endl; }
int i;
};
class Scanner : virtual public PoweredDevice
{
public:
Scanner(int scanner, int power)
: PoweredDevice(power)
{
cout << "Scanner : " << scanner << endl;
}
};
class Printer : virtual public PoweredDevice
{
public:
Printer(int printer, int power)
: PoweredDevice(power)
{
cout << "Printer : " << printer << endl;
}
};
class Copier : public Scanner, public Printer
{
public:
Copier(int scanner, int printer, int power1, int power2, int power3)
: Scanner(scanner, power1)
, Printer(printer, power2)
, PoweredDevice(power3)
{}
};
int main()
{
Copier copier(1, 2, 3, 4, 5);
cout << &copier.Scanner::PoweredDevice::i << endl;
cout << &copier.Printer::PoweredDevice::i << endl;
copier.i; // abiguous 모호성 문제가 생겨난다.
// 이걸 막기 위해 virtual을 사용해서 가상상속을 해줄 수 있다.
return 0;
}
다운캐스팅 시, 값이 변동될 수 있다.
static _cast를 사용하면, 변수값이 원하지 않더라도 변경될 수 있다.
class에 저장된 데이터공간에 맞게 잘라서 박아버리기 때문에, int형 k 와 int 형 j 두개가 있을 경우, 두개의 변수 명이 다르더라도 그 변수위치에 삽입된다는 이야기이다.
다음은 그러한 코드의 예시이다.
#include <iostream>
using namespace std;
/*
dynamic cast
- 안전한 다운캐스팅 에 사용
*/
class Base
{
public:
int i = 0;
virtual void Print() { cout << "Base" << endl; }
};
class Derived1 : public Base
{
public:
int j = 1;
virtual void Print() override { cout << "Derived1" << endl; }
};
class Derived2 : public Base
{
public:
int k = 2;
virtual void Print() override { cout << "Derived2" << endl; }
};
void DoSomething(Base* b)
{
//Derived2* baseToD2 = static_cast<Derived2*>(b);
//baseToD2->k = 888;
// 위와같은 방식으로 진행하면 j 에 888이 들어가버린다.
Derived2* baseToD2 = dynamic_cast<Derived2*>(b);
baseToD2->k = 888;
// 런타임 상황에서 잡아준다.
}
int main()
{
Derived1 d1;
Base* base = &d1;
Derived1* baseToD1 = static_cast<Derived1*>(base);
baseToD1->Print();
DoSomething(&d1);
// j의 정보가 있는 d1을 d2로 바꾸어서 k값을 바꾸었다.
cout << d1.j << endl;
// 강제적인 형변환이 일어나기 때문에, 원하지 않는 정보의 변환이 이루어질 수 있다.
// 다이나믹 케스팅은 런타임 때 오류를 잡아준다. 그래서 조금 느릴 수 있다.
return 0;
}
이렇게 컴파일 타임 때 정의되는 함수는 컴파일시간에 정의되기 떄문에, 실행시에 함수가 바뀌지 않는다.
그래서 실행시간에 바인딩을 검색할 필요가 없기 때문에, 실행속도가 동적 바인딩에 비해 빠르다.
동적바인딩은, 실행시간에 호출을 처리함으로써 객체의 타입이나 상태에 따라 실행 흐름을 자유롭게 제어할 수 있게된다. 이로써 객체지향 프로그래밍 언어에서의 다형성과 유연성을 확보할 수 있게된다.
그러나, 실행시간에 바인딩을 검색해야 하므로 정적 바인딩보다는 실행속도가 느릴 수 있다.
정적 바인딩 ( 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는 단독으로 사용하지 못하도록 컴파일러에서 제한한다.
이는, 인터페이스의 구조 자체가 값을 가지지 않도록 프로그래밍 되는 경우가 많기 때문에, 단독사용되는 실수가 생길경우 컴파일러는 확인하지 못할 큰 오류가 나타날 수 있기 때문이다.
즉, 추상클래스를 단독으로 사용하게 되면, 구현되지 않은 메서드가 존재하게 됨 으로 실행할 수 없는 클래스가 되어버리기 때문이다.
C++ 의 상속과정에서 상속자를 두개 넣으면 두개의 class의 public 과 protected에 대한 값을 사용할 수 있다.
다음은 이러한 이중상속 에 대한 코드이다.
#include <iostream>
// 이중상속 에 대한 이야기
using namespace std;
class USBDevice
{
int id;
public:
USBDevice() = default;
USBDevice(int id) : id(id) {}
auto GetId() const { return id; }
void PlugAndPlay() {}
};
class NetworkDevice
{
int id;
public:
NetworkDevice() = default;
NetworkDevice(int id) : id(id) {}
auto GetId() const { return id; }
void Networking() {}
};
class USBNetworkDevice : public USBDevice, public NetworkDevice // 상속받는것 이 두가지로 이중상속을 한다.
{
public:
USBNetworkDevice(int usbId, int netId)
: USBDevice(usbId), NetworkDevice(netId)
{}
USBNetworkDevice(int id)
: USBDevice(id), NetworkDevice(id)
{}
};
int main()
{
USBNetworkDevice device(1, 2);
device.PlugAndPlay();
device.Networking();
device.USBDevice::GetId();
device.NetworkDevice::GetId();
return 0;
}
위와같이 class를 두개 상속받아 이중상속 할 수 있다.
C++ 에서 final을 통해 어떠한 클래스의 값을 상속시킬수 없게 하거나, virtual 키워드를 통해 함수 오버라이딩이 가능하다.
다음은 final과 virtual을 사용한 코드 예시이다.
#include <iostream>
using namespace std;
class A //final
// final = 클래스를 상속시킬 수 없게 제한하는 키워드 이다. 이걸 붙이면
// 나는 더이상 이 함수를 상속하지 않겠다. 라는 의미이다.
{
public:
virtual void Print(int x) { cout << "A" << endl; }
// virtual = 가상함수 라는 의미로, 하위클레스에서 이 함수를 재정의 해줄 수 있음을 명시해주는 것.
int i = 0;
};
class B /*final*/ : public A
// B옆에 붙이면, B를 이후에 상속시키지 않겠다. 라는 의미.
{
public:
void Print(int x) override /*final*/ { cout << "B" << endl; }
// override 는 이 함수가 부모의 함수를 덮어씌워즈는 역활 이라는 것 을 명시해주는것.
// 이때 인자값 과 반환형은 언제나 같아야 한다.
// 함수에 final을 붙이면, 함수 한정해서 상속을 시키지 않을 수 있다.
};
class C : public B
{
public:
void Print(int x) override { cout << "C" << endl; }
};
int main()
{
A a;
B b;
C c;
a.Print(1);
b.Print(2);
c.Print(3);
A* aptr = new B();
// B* bptr = aptr;
B* bb = new B();
A* aa = bb; // A포인터가 B를 가리킬때 A것이 출력되고 B가 출력되지 않는다.
// 이는 오버라이딩 기준이 호출자 기준으로 정해지기 때문이다.
return 0;
}
위와같이 Print를 통해 a와 b 그리고 c의 함수를 함수오버라이딩을 통해 출력했다.
원래라면, 상속받은 b와 c의 함수가 동일함으로 모호성문제가 발생하거나 함수출력에 다한 값을 지정해주어야 할 수 있지만,
virtual선언을 A class 에서 해주었음으로 A의 Print함수는 가상함수 라고 컴파일러가 이해하였기 때문에, override되어,
각 class에서는 자기가 가지고있는 Print를 사용하여 출력하게 되는것이다.
또한, final을 class에 붙이게되면, 해당 class의 값을 다음 상속자에게 넘겨주지 않겠다고 선언하는. 명시해주는 역활로,
final을 B에 붙이게되면, C에서는 컴파일 오류가 나타난다.
위와같이, final 클래스 형식을 상속받을 수 없음이 컴파일러 오류로 나타난다.
그러나, 함수 오버라이딩 을 통하여 함수를 다음함수로 넘겨주어 자식클래스의 함수를 사용하게 된다고 하더라도, 포인터로 접속시 부모클래스를 통해 자식클래스로 접속하여 출력하게 되면, override된 함수가 출력되지 않는다.
다음은 class의 포인터로 부모클래스에대한 포인터에 자식클래스를 넣었을 때 의 코드이다.
#include <iostream>
using namespace std;
// 자기자신을 가리키는 포인터를 출력할 때 는 상속자 기준으로 출력이 아니라 포인터 기준으로 출력된다.
class A
{
public:
virtual A* GetThisPointer() { cout << "A" << endl; return this; }
void print() { cout << "A" << endl; }
};
class B : public A
{
public:
B* GetThisPointer() override { cout << "B" << endl; return this; }
void print() { cout << "B" << endl; }
};
int main()
{
B b;
b.print();
cout << "Address : " << b.GetThisPointer() << endl;
cout << "typeid : " << typeid(b.GetThisPointer()).name() << endl;
A& ref = b;
ref.print(); // A가 참조되어서, override가 되지 않았기 때문에, A가 출력된다.
cout << "Adress : " << ref.GetThisPointer() << endl; // B를 출력하기 때문에 B가 출력되는것 이 맞다.
cout << "typeid : " << typeid(ref.GetThisPointer()).name() << endl; // 타입 자체는 A이기 때문에 A의 타입이 반환되는것 이다.
return 0;
}
위와같이, 자기자신의 포인터를 출력하였지만, Class의 포인터위치값은 같게 나타난다. 그러나, class의 타입이 A에서부터 B로 들어가게되면, A로 나타나는것 을 볼 수 있다.
다음은, viertual ( 가상함수 ) 를 활용한 코드의 예시이다.
#include <iostream>
// 가상함수 특징을 활용한 코드예시
using namespace std;
class Animal
{
string name;
public:
Animal(string name) : name(name) {}
virtual void Speak() const { cout << "??" << endl; }
};
class Cat : public Animal
{
public:
Cat(string name) : Animal(name) {}
void Speak() const override { cout << "야옹" << endl; }
};
class Dog : public Animal
{
public:
Dog(string name) : Animal(name) {}
void Speak() const override { cout << "멍멍" << endl; }
};
int main()
{
Cat cats[]{ Cat("cat1"),Cat("cat2"), Cat("cat3"), Cat("cat4"), Cat("cat5") };
Dog dogs[]{ Dog("dog1"),Dog("dog2"), Dog("dog3"), Dog("dog4"), Dog("dog5") };
for (const auto& cat : cats)
cat.Speak();
for (const auto& dog : dogs)
dog.Speak();
Animal* animals[]
{
&cats[0],
&cats[1],
&cats[2],
&cats[3],
&cats[4],
&dogs[0],
&dogs[1],
&dogs[2],
&dogs[3],
&dogs[4]
};
for (const auto& animal : animals)
animal->Speak();
return 0;
}
위와 같이, 부모자식클래스를 활용하여 어떠한 그룹을 만들어, 출력할 수 있는 코드를 작성할 수 있다.
위 코드에서는 Animal이라는 부모클래스를 구성하고, Cat과 Dog라는 class의 배열을 생성하여, 생성자로 이름을 구분지어주었다.
그후, for문을 통하여 배열 을 전체검출하고, 검출마다 해당 class의 함수를 출력하도록 만들었다.
또한, 이러한 Animal class의 함수가 virtual 로 선언되어있음으로, override되어, 정상적인 함수의 출력값이 나타나게 되는것이다.
다음은 가상함수에서 재정의되어 출력되는 코드예시이다.
#include <iostream>
// 가상함수 에서 재정의하여 출력하는 예시.
using namespace std;
class A
{
public:
void Print() { cout << "A" << endl; }
virtual void Print1() { cout << "A1" << endl; }
virtual void Print2() { cout << "A2" << endl; }
virtual void Print3() { cout << "A3" << endl; }
virtual void Print4() final { cout << "A4" << endl; }
};
class B : public A
{
public:
void Print() { cout << "B" << endl; }
virtual void Print1() { cout << "B1" << endl; }
virtual void Print2() { cout << "B2" << endl; }
virtual void Print3() final { cout << "B3" << endl; }
// virtual void Print4() final { cout << "A4" << endl; }
};
class C : public B
{
public:
void Print() { cout << "C" << endl; }
virtual void Print1() { cout << "C1" << endl; }
virtual void Print2() final { cout << "C2" << endl; }
// virtual void Print3() final { cout << "C3" << endl; }
// virtual void Print4() final { cout << "C4" << endl; }
};
class D : public C
{
public:
void Print() { cout << "D" << endl; }
virtual void Print1() final { cout << "D1" << endl; }
// virtual void Print2() final { cout << "D2" << endl; }
// virtual void Print3() final { cout << "D3" << endl; }
// virtual void Print4() final { cout << "D4" << endl; }
};
int main()
{
A a;
a.Print1();
B b;
b.Print1();
C c;
c.Print1();
D d;
d.Print1();
cout << endl;
A& refB = b;
A& refC = c;
A& refD = d;
cout << "######### A" << endl;
a.Print();
a.Print1();
a.Print2();
a.Print3();
a.Print4();
cout << "######### refB" << endl;
refB.Print();
refB.Print1();
refB.Print2();
refB.Print3();
refB.Print4();
cout << "######### refC" << endl;
refC.Print();
refC.Print1();
refC.Print2();
refC.Print3();
refC.Print4();
cout << "######### refD" << endl;
refD.Print();
refD.Print1();
refD.Print2();
refD.Print3();
refD.Print4();
D d1;
D d2;
// d1 과 d2의 주소는 같다.
// 가상함수 테이블은 함수 포인터 배열이라고 생각하면 된다. 가상함수의 배열의 순서로 접근하게 된다.
// 가상함수 테이블은 변수주소와는 다르다고 생각하면 된다.
// 선입선출 방식으로 덮어씌워지면서 생성이된다.
return 0;
}
위와같이, 상속, 상속, 상속하여. 계속해서 자신의 함수로 대체되는 과정을 반복하고, virtual되지 않을 함수를 지정하기 위해 명시적 선언자인 final을 사용하여 상속되지 않음을 표현하였다. [ 주석처리는 시각적으로 나타내기 위한 코드줄 ]
이때, virtual 선언된 함수가 대체될때 가상함수 테이블이 생성되어 d1과 d2의 주소가 같게 나타난다.
이는, 함수가 코드영역에 존재함으로써 나타나는 최적화 방식중 하나인데,
어떠한 class가 여러개 생성되더라도 함수는 언제나 같은 함수가 호출되기 때문에, 함수에 대한 저장영역을 여러개 만들어 줄 필요가 없기 때문이다.
그리하여 class가 여러개 생성되었을 때 각각 class는 자신의 가상함수 테이블이 생성되고, 해당되는 변수가 호출될 때 가상함수 테이블에서 지정된 가상함수테이블 의 함수를 가져오게 된다.
부모 자식 클래스간의 함수 override 가 다르기 때문에, 예를들어 부모클래스 A와 부모이자 자식인 B, B를 상속받는 C가 있다고 한다면,
C++에서의 다형성인 오버로딩, 오버라이딩 중 오버 라이딩을 사용하는 부모자식 클레스에 대한 내용이다.
C++ 에서는 부모자식 클래스에 대한 함수 오버라이딩과 privet을 제외한 함수 공유가 가능하다.
이는 부모클래스와 자식클래스로 이어지는 것을 이야기한다. 예를들어 책이라는 묶음에 책내용이라는 자식이 있는 격 이다.
부모클래스와 자식클래스의 관계에서는 부모클래스는 기본적인 특성과 동작을 정의하고, 자식클래스는 이러한 특성과 동작을 상속받아 추가적인 기능을 추가하거나 변경할 수 있다. ( 변경은 함수 오버라이딩을 의미한다 ) 이를 통해 코드의 확장성과 유연성을 증가시킬 수 있다.
다음 코드는 기본적인 부모자식클래스의 예시이다.
코드예시는 다음과 같다.
class Mother
{
private:
int a;
// 상속되어도 private은 물려받아 사용할 수 없게된다.
public:
auto GetA() const
{
cout << "Mother" << endl;
return a;
}
void SetA(const int& a) { this->a = a; }
protected:
int b;
int c;
// 여기서 만들어지는 애들은 자식과 내 클레스에서만 사용할 수 있다.
};
class Chiled : public Mother
{
int b;
// 자식과 부모가 겹친다면, 자식의 것이 먼저 우선시된다.
public:
auto GetA() const
{
c;
b; // chiled
Mother::b; // Mother
cout << "chiled" << endl;
return Mother::GetA();
}
};
int main()
{
Chiled c;
c.SetA(1);
c.GetA();
c.Mother::GetA();
// Mohter이 생성되지 않아도 Mother의 함수를 사용할 수 있다.
return 0;
}
위 코드에서의 설명은. 자식클래스와 부모클래스에 대한 기본적인 사용방법을 이야기한다.
private는 물려받아 사용할 수 없으나, 그 이외에 protected나 public은 물려받아 자식클래스에서 사용이 가능하다.
위에서 설명했듯, private는 물려받아 사용할 수 없으나, 그 이외에 protected나 public은 물려받아 자식클래스에서 사용이 가능하다. 그러나, 이러한 자식클래스 선언에서 부모클래스의 다른값들을 public이나 private으로 설정해주는 방법이 존재한다.
이는, 부모클래스의 값을 이어받아온 자식클래스가. 사용할 때는 자신이 설정한 값대로 변경하여 사용하나. 그곳에서 한번 더 자식클래스를 만들 경우. 그 자식클래스 에서는 변경한 형태 대로 따라오게 취급한다.
다음은. 그러한 권한변경에 대한 코드 예시이다.
#include <iostream>
using namespace std;
/*
상속 접근 지정자 부모 클래스 자식클래스
public public public
protected protected
private 접근 불가
protected public protected
protected protected
private 접근 불가
private public private
protected private
private 접근 불가
*/
class Base
{
private:
int privateDate;
protected:
int protectedData;
public:
int publicData;
void Test() {}
};
class Derived : private Base
{
public:
Derived()
{
Test();
publicData;
protectedData;
// privateDate;
}
};
class A : public Derived
{
public:
A()
{
// Test();
// pulbicData;
// protectedData;
// 접근하는것들을 이미 private으로 받았기 때문에 다시 상속시키면, 이미 private이기 때문에 접근할 수 없게된다.
}
};
int main()
{
A a;
//a. // 아무것도 뜨지 않는다.
return 0;
}
위 코드에서 볼 수 있듯. Derived에서 가져온 Base의 변수들에 따라 Derived에서는 Base의 변수를 사용할 수 있지만,
class A에서는 Derived에서 이미 Base의 값들을 전부 private으로 선언하였기 때문에, 사용할 수 없게된다.
부모자식클래스 에서는 Up casting과 Down casting이 존재한다.
UP casting
일반적으로 자식클래스가 더 크다고 생각할 수 있지만, 개념적으로 부모클래스 의 것을 물려받기 때문에, 부모클래스가 더 상위라고 볼 수 있다. 이는 약간 폴더 같은 개념이라고 이해하면 된다.
예를들어, 어떤 폴더에 게임파일이 존재한다고 해보자. 이때 게임파일은 폴더에 들어가기 전에는 접근할 수 있는 방법이 존재하지 않을것 이다. 폴더를 열어야만 게임파일에 들어갈 수 있기 때문에, 이는 폴더가 게임파일보다 위에 있는. 접근할때 폴더를 먼저 열어야하는. 폴더가 게임의 상위이다. 라고 할 수 있을것이다.
이때, 자식 -> 부모 쪽으로 이동하기위해 포인터 작업을 진행하는것 을 Up casting 이라고 한다.
Down casting
부모클래스의 포인터가 가리키는 객체를 자식클래스의 포인터로 가리키게 하는것이다.
부모클래스의 포인터가 부모의 객체를 가리킬 때 에는 자식클래스의 값을 가리킬 수 없게된다.
이는. 위에서 설명한 UP casting과는 정 반대되는 개념이라고 할 수 있다.
#include <iostream>
using namespace std;
class Snack {};
class Chitos : public Snack {};
class ChocoChip : public Snack {};
class Homerunball : public Snack {};
int main()
{
Snack* chocochip = new ChocoChip(); // 업케스팅(Up casting)
Snack* chitos = new Chitos();
Snack* sneck[2] = { chitos, chocochip }; // 업케스팅을 사용해서 배열로 사용이 가능하다. 접근이 가능하다. 관련이 있는것 끼리 묶어야한다.
Chitos* test = static_cast<Chitos*>(chitos); // 다운케스팅 많이 사용하지는 않는다.
// Chitos* cc = new Snack(); // error이 된다. 아래에서 위로만 케스팅이 된다.
return 0;
}
위와같이 배치하였을 때, UPcasting을 활용해서 과자종류 에 대한 배열을 유지하고 생성해줄 수 있게된다.
이때, 부모클래스와 자식클래스 간의 생성과 종료에 대한 순서를 살펴보자.
일반적으로, 부모클래스가 생성되고 나서 자식클래스가 생성되는 순서로 class가 생성된다.
이는, 자식클래스는 부모클래스가 생성되지 않으면 생성되어 값을 유지할 수 없기 때문이다.
그래서 일반적인 생성시 순소는 부모 -> 자식순으로 유지된다.
그렇다면, 종료시 소멸자는 어떤식으로 동작하게 될까?
대부분의 코드동작방식은 후입선출 방식으로 이루어진다.
그렇기 때문에 생성과 종료는 다음과 같은 순서로 이어지게 된다.
부모[생성] -> 자식[생성] -> 자식[종료] -> 부모[종료]
다음 코드 예시를 통해 살펴보자.
#include <iostream>
using namespace std;
class Parent
{
int a;
public:
Parent()
:a(10)
{
cout << "Base Constructor" << endl;
}
};
class Child : public Parent
{
double b;
public:
Child()
:/*Parent() , */b(10.0) // 부모클래스의 생성자가 먼저 호출 되고 자식클래스가 생성되면서, 자식클래스의 맴버 클래스 생성자가 만들어 지면서 만들어지게 된다.
{
cout << "Derved Constructor" << endl;
}
};
class A { public: A() { cout << "A constructor" << endl; } };
class B : public A { public: B() { cout << "B constructor" << endl; } };
class C : public B { public: C() { cout << "C constructor" << endl; } };
class D : public C { public: D() { cout << "D constructor" << endl; } };
int main()
{
Child child;
D d;
// D만 호출하더라도, 나머지의 class의 생성자가 전부 생성되어 나타나게 된다.
return 0;
}
위와 같은 코드를 통해 생성자와 종료자를 살펴보자.
위와 같은 방식대로 실행될것 이다.
위코드 에서 는 생성방식 에 대해 설명한다. class가 생성될 때 에는 부모가 먼저 생성되고 그 이후에 자식이 생성되는 순서로 이어진다.
// 소멸자 관련 이야기
#include <iostream>
using namespace std;
class A
{
int a;
// 4byte
public:
A() { cout << "A constructor" << endl; }
~A() { cout << "A destructor" << endl; }
};
class B : public A
{
double b;
// 16byte
public:
B() { cout << "B constructor" << endl; }
~B() { cout << "B destructor" << endl; }
};
int main()
{
B b;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
// 페딩바이트가 여러개 있을떄 cpu가 중복접근을 할 수 있기 때문에, 그걸 막기위해 페딩바이트를 추가해주어서 하는것.
return 0;
}
const를 사용하여 변수를 선언하면, 해당 변수의 값은 const선언한. 초기화 이후에 변경할 수 없게된다.
이를통해 프로그램의 의도적인 수정을 방지하고, 코드의 안정성과 가독성을 향상시킬 수 있다.
const는 다음과 같이 사용할 수 있다.
읽을 때, 좌측에 있는것 을 상수화한다. 라고 읽으면 된다.
// 변수 선언시 변수 상수화
const int MAX_VALUE1 = 100;
int const Max_VALUE2 = 100;
// 함수의 매개변수를 상수화
int Printvalue(const int value) { return value; }
int Printvalue(int const value) { return value; }
// 함수의 반환값을 상수화
const int Printvalue(int value) { return value; }
int const Printvalue(int value) { return value; }
// L_value 에 대한 상수화
const int& Printvalue(int value) { return value; }
int& const Printvalue(int value) { return value; }
// 다 끼얹어보기
const int& Printvalue (const int value) { return value; }
static
static은 함수, 메인문, 클래스 등 의 변수나 함수를 전역선언해주는것 을 의미한다.
메인문 밖에서 변수를 선언해주지 않아도 변수를 static선언해주는것 으로 전역변수 처럼 사용할 수 있다.
전역변수를 선언하게되면, 전에 선언된 것과 다르게 전에 선언된 변수를 따르게 됨으로 변수의 값이 이어지는 효과가 나타난다.
static의 코드사용은 다음과 같다.
// 지역에서 전역변수 선언하기
static int coutner = 0;
// 함수에서 전역변수 선언하기
void def() {
static int count = 0;
count++
// 이렇게 하면 def선언 할 때 마다 count가 1씩 증가한다.
}
// 정적 멤버 변수
class MyClass{
private:
static int count;
public:
void countUP() { this->count++; }
// 선언될 떄 마다count가 1씩 증가한다.
}
// 정적 멤버 함수
class MyClass {
static int count;
int count1;
public:
static void contUP() { this->count++; }
void countUP() { this->count1++; }
// 선언될 때 마다 count가 1씩 증가한다. 그러나, static함수는 static변수만 사용할 수 있다.
// 또한 static함수는 선언 전에도 class이름을 타고 사용이 가능하다.
// myClass::countUP(); 이런식으로
}
Assocation은 두 클래스가 서로를 사용하는, 서로가 상속된 관계라고 할 수 있다. 이는 "사용한다" 또는 "알고있다"로 정의할 수 있는데, 서로가 서로의 값을 이용하거나 함수를 사용할 수 있는 관계라고 할 수 있다.
예를들어 '학생'클래스와 '교수'클래스가 있다면, '학생'은 '교수'가 누구인지, 어떠한 과목을 가르치는지 알고, '교수'는 학생이 누구인지 몃학년인지 등의 정보를 알 수 있을것이다.
이것은 두 클래스가 독립적으로 존재하면서도 서로를 참조할 수 있는 관계를 나타낸다.
집합(Aggregation)
Aggregation은 "전체 - 부분" 관계를 표현하며, "소유한다", "포함한다" 라고 이해할 수 있다.
예를 들어 '대학'과 '학부' 가 있다면, 대학은 여러 개의 학부를 소유,포함 하고 학부는 특정 '대학' 에 속해있다. 그러나, 학부는 학부 자체로 나타낼 수 있으며, 이것은 '대학' 이 없어도 어떠한 '학부' 라고 표현할 수 있을것이다. 즉, 독립적으로 존재할 수 있다.
이것은 전체와 부분 사이의 관계를 나타내며, 부분은 전체 없이도 독립적으로 존재할 수 있다.
합성(Composition)
Composition은 Aggregation과 유사하게 "전체 - 부분" 관계를 표현하지만, 이 경우 부분은 전체 없이는 존재할 수 없다.
예를 들어 '사람' 과 '심장' 이 있다면, '사람' 은 '심장' 없이 존재할 수 없고, 마찬가지로 '심장' 은 '사람'없이 홀로 존재할 수 없을것이다.
이것은 전체와 부분 사이의 강력한 결합을 나타낸다.
상속(Inheritance)
Inheritatnce는 "일반 - 특수" 관계를 표현하며, '확장하다' '받는다' 라고 이해할 수 있다.
예를 들어 '동물' 과 '강아지' 가 있다면, '강아지' 는 '동물'의 어떠한 경우이고. '동물' 안에 포함된다고 할 수 있으나, '강아지' 는 동물의 특성을 받아온다 즉,'상속' 해 온다 라고 할수 있다.
이것은 일반적인 개념에서 보다 구체적인 개념으로 확장하는 관계를 나타낸다.
의존(Dependency)
Dependency 관계는 한 구조체, 클래스가 다른 클래스의 메서드 내에서 사용될때를 말하며, "사용한다"라고 표현한다.
예를 들어 '학생' 과 '교과서' 가 있다고 한다면, '학생' 이 '수업' 이라는 행동을 할 때 '교과서' 라는 것에 의존하게 되는것을 말한다. '학생' 이 '수업'에 '교과서' 가 없으면 '수업'을 수행할 수 없기 때문이다.
이것은 여러개의 관계에서 동작을 수행하기위한 최소조건을 다른 클래스에 '의존'하는 관계를 나타낸다.
다음은 연관 Association의 코드 예시이다.
#include <iostream>
#include <vector>
using namespace std;
// 특정 클래스가 다른 클래스의 맴버 를 선언한 경우.
// 서로 연계가 되는 관계. 의사와 환자 의 경우.
// 커플링 관계
class Patient
{
string name;
friend class Doctor;
vector<class Doctor*> doctors;
public:
Patient(const string& name)
:name(name)
{}
void AddDoctor(class Doctor* const doctor)
{
doctors.push_back(doctor);
}
void MeetDoctor();
};
class Doctor
{
string name;
friend class Patient;
vector<class Patient*> patients;
public:
Doctor(const string& name)
:name(name)
{}
void AddPatient(class Patient* const patient)
{
patients.push_back(patient);
}
void MeetPatient()
{
for (const auto& patient : patients)
cout << "Meet Patient : " << patient->name << endl;
}
};
void Patient::MeetDoctor()
{
for (const auto& doctor : doctors)
cout << "Meet Patient : " << doctor->name << endl;
}
int main()
{
Patient* p1 = new Patient("Kim");
Patient* p2 = new Patient("Lee");
Patient* p3 = new Patient("Park");
Doctor* d1 = new Doctor("Doc. Lee");
Doctor* d2 = new Doctor("Doc. Kim");
p1->AddDoctor(d1);
d1->AddPatient(p1);
p2->AddDoctor(d2);
d2->AddPatient(p2);
p3->AddDoctor(d2);
d2->AddPatient(p3);
// 서로가 서로에게 영향을 끼치기 때문에, 하나가 수정되면 오류가 폭팔할 수 있다. 그래서 별로 좋은 코드가 아니다.
delete d2;
delete d1;
delete p3;
delete p2;
delete p1;
// 선언된 클래스들은 후입선출 방식으로 해제해주어야 한다. 처음 선언된 클래스는 얼마나 엮여있을지 알 수 없기 때문.
return 0;
}
위 코드에서는, Doctor이라는 클래스와 Patient라는 클래스가 서로의 값을 사용하게 된다.
Doctor(의사)는 AddPatient로 입력되었던 Patient(환자)의 값이 들어있는 vector을,
Patient(환자)는 AddDoctor로 입력되었던 Doctor(의사)의 값이 들어있는 vector을
서로 사용하여 서로가 서로를 의존한다. 즉,연관되어있다.
각 클래스는 환자 또는 의사 둘중 한개만 사라져도. 동작하지 않는 위험한 코드가 되어버렸다.