본문 바로가기

개발 공부/C++

따라하며 배우는 C++ 12. 가상 함수들

따라하며 배우는 C++ 12. 가상 함수들

 

12.1 다형성의 기본 개념

 

#include <iostream>
using namespace std;
class Animal
{
protected:
	string m_name;
public:
	Animal(std::string name)
		: m_name(name)
	{}
public:
	string getName() { return m_name; }
	void speak() const
	{
		cout << m_name << " ??? " << endl;
	}
};

class Cat : public Animal
{
public:
	Cat(string name)
		:Animal(name)
	{}

	void speak() const
	{
		cout << m_name << "Meow " << endl;
	}
};

class Dog : public Animal
{
public:
	Dog(string name)
		:Animal(name)
	{}

	void speak() const
	{
		cout << m_name << "Woof" << endl;
	}
};

int main() 
{
	Animal animal("my animal");
	Cat cat("my cat");
	Dog dog("my dog");

	animal.speak();
	cat.speak();
	dog.speak();

	Animal* ptr_animal1 = &cat;
	Animal* ptr_animal2 = &dog;

	ptr_animal1->speak();
	ptr_animal2->speak();
	//자식 클래스를 부모 클래스의 pointer로 호출하면
	//부모 클래스처럼 작동한다.
	return 0;
}

 

 

 

#include <iostream>
using namespace std;
class Animal
{
protected:
	string m_name;
public:
	Animal(std::string name)
		: m_name(name)
	{}
public:
	string getName() { return m_name; }
	virtual void speak() const
	{
		cout << m_name << " ??? " << endl;
	}
};

class Cat : public Animal
{
public:
	Cat(string name)
		:Animal(name)
	{}

	void speak() const
	{
		cout << m_name << " Meow " << endl;
	}
};

class Dog : public Animal
{
public:
	Dog(string name)
		:Animal(name)
	{}

	void speak() const
	{
		cout << m_name << " Woof" << endl;
	}
};

int main() 
{
	Cat cats[] = { Cat("cat1"), Cat("cat2"), Cat("cat3"), Cat("cat4"), Cat("cat5") };
	Dog dogs[] = { Dog("dog1"), Dog("Dog2") };

	for (int i = 0; i < 5; ++i)
		cats[i].speak();

	for (int i = 0; i < 2; ++i)
		dogs[i].speak();
	//...
	//동물이 여러 개일 경우 for문이 기하급수적으로 늘어난다.

	Animal* my_animals[] = { &cats[0], &cats[1], &cats[2], &cats[3], &cats[4],
					&dogs[0], &dogs[1] };

	for (int i = 0; i < 7; ++i)
		my_animals[i]->speak();
	return 0;
}

 

부모의 함수에 virtual을 넣으면 부모 포인터로 선언해도 자식 클래스의 메소드처럼 작동한다.

 

 

 

12.2 가상 함수Virtual Functions와 다형성Polymorphism

 

 

#include <iostream>
using namespace std;
class A
{
public:
	void print() { cout << "A" << endl; }
};

class B : public A
{
public:
	void print() { cout << "B" << endl; }
};

class C : public B
{
public:
	void print() { cout << "C" << endl; }
};

class D : public C
{
public:
	void print() { cout << "D" << endl; }
};

int main() 
{
	A a;
	a.print();

	B b;
	b.print();

	C c;
	c.print();

	D d;
	d.print();
	return 0;
}

 

int main() 
{
	A a;
	B b;
	C c;
	D d;

	A& ref = b; //B를 A의 레퍼런스에 담으면 A인 것처럼 작동함.
	ref.print();
	return 0;
}

 

 

class A
{
public:
	virtual void print() { cout << "A" << endl; }
};

//만약 이렇게 하고
A &ref = b;
ref.print();
//를 호출하면

위와 같이 나온다.

virtual을 넣으면 자식 클래스에 오버라이딩 된 함수를 사용한다.

 

상속이 여러 번 이루어져도, 가장 상위 클래스의 기능이 virtual이면 바로 상위 클래스의 함수도 virtual처럼 작동한다.

헷갈림을 방지하기 위해 가장 상위 클래스의 기능이 virtual이면 그 하단 클래스 기능 앞에도 virtual을 붙여주는 것이

관습적이다.

 

 

 

리턴 타입을 가지곤 오버라이딩 할 수 없음.

virtual은 스택이 아니라 테이블을 따로 만들어 호출하기 때문에 속도가 느리다.

 

 

12.3 override, final, 공변 반환값Covariant

 

#include <iostream>
using namespace std;
class A
{
public:
	virtual void print(int x) { cout << "A" << endl; }
};

class B : public A
{
public:
	virtual void print(short x) { cout << "B" << endl; }
};

//class C : public B
//{
//public:
//	virtual void print() { cout << "C" << endl; }
//};
//
//class D : public C
//{
//public:
//	virtual int print() { cout << "D" << endl; return 0; }
//};

int main() 
{
	A a;
	B b;
	
	A& ref = b;
	ref.print(1);

	//b에 있는 함수가 나오지 않고 A가 나온다.
	return 0;
}

함수는 파라미터가 다르면 오버라이딩 할 수 없다(하위 클래스에서 메소드 재정의 불가)

즉 오버로딩을 한 것처럼 인식한다.

override를 할 때 override가 안 되는 에러를 잡아준다.

final은 해당 메소드를 하위 클래스에서 오버라이딩 못하도록 막는다.

 

#include <iostream>
using namespace std;
class A
{
public:
	void print() { cout << "A" << endl; }
	virtual A* getThis() { 
		cout << "A::getThis()" << endl;
		return this; }
};

class B : public A
{
public:
	void print() { cout << "B" << endl; }
	virtual B* getThis() { 
		cout << "B::getThis()" << endl;
		return this; }
	//보통 return 타입이 다르면 overriding이 안 되지만
	//두 함수 return 타입 모두, A, B 즉 상속 관계이기 때문에 오버라이딩이 됨.
};

int main() 
{
	A a;
	B b;
	
	A& ref = b;
	b.getThis()->print();
	ref.getThis()->print();

	cout << typeid(b.getThis()).name() << endl;
	cout << typeid(ref.getThis()).name() << endl;
	return 0;
}

 

b.getThis() -> print(); //B로 나옴

ref.getThis() -> print(); //원래 A형이기 때문에 getThis()에서 B의 포인터로 리턴을 해줘도

//내부에서 A의 포인터로 바꿔주는 것.

 

 

12.4 가상 소멸자

 

#include <iostream>
using namespace std;

class Base
{
public:
	~Base()
	{
		cout << "~Base()" << endl;
	}
};

class Derived : public Base
{
private:
	int* m_array;
public:
	Derived(int length)
	{
		m_array = new int[length]; //동적 할당
	}
	~Derived()
	{
		cout << "~Derived" << endl; 
		delete[] m_array;//동적 할당이므로 직접 메모리를 제어할 땐
			//delete가 중요
	}
};
int main() 
{
	Derived derived(5);
	return 0;
}

여기서 소멸자의 호출 순서는 생성자의 반대

#include <iostream>
using namespace std;

class Base
{
public:
	~Base()
	{
		cout << "~Base()" << endl;
	}
};

class Derived : public Base
{
private:
	int* m_array;
public:
	Derived(int length)
	{
		m_array = new int[length]; //동적 할당
	}
	~Derived()
	{
		cout << "~Derived" << endl; 
		delete[] m_array;//동적 할당이므로 직접 메모리를 제어할 땐
			//delete가 중요
	}
};
int main() 
{
	//Derived derived(5);

	Derived* derived = new Derived(5);
	Base* base = derived;
	delete base;
	//Derived가 여러 가지일 수 있기 때문에
	//지울 때는 base에서 지워주는 것이 좋다.

	return 0;
}

 

그러나 이럴 경우 Base의 소멸자만 호출됨.

Derived에 관해선 호출되지 않으므로 메모리 누수가 발생한다.

 

이럴 경우

class Base
{
public:
	virtual ~Base()
	{
		cout << "~Base()" << endl;
	}
};

소멸자 자체를 virtual로 만들면 해결된다.

 

소멸자의 경우, 상위 클래스에서 virtual을 붙이지 않고 하위 클래스에서 override를 붙이면 오버라이딩이 안 된다.

소멸자는 이름이 다를 수 밖에 없기 때문.

 

 

12.5 동적 바인딩과 정적 바인딩

 

#include <iostream>
using namespace std;

int add(int x, int y)
{
	return x + y;
}

int subtract(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;

	//static binding (early binding)

	//이렇게 모든 변수명이나 함수 이름들이 빌드 타임에
	//깔끔하게 정의되어 있을 때.
	int result;
	switch (op)
	{
	case 0: result = add(x, y); break;
	case 1: result = subtract(x, y); break;
	case 2: result = multiply(x, y); break;
	}

	cout << result << endl;
	return 0;
}

 

int main() 
{
	int x, y;
	cin >> x >> y;
	int op;
	cout << "0 : add, 1 : subtract, 2 : multiply" << endl;
	cin >> op;

	//Dynamic binding (late binding)
	int (*func_ptr)(int, int) = nullptr;
	switch (op)
	{
	case 0: func_ptr = add; break;
	case 1: func_ptr = subtract; break;
	case 2: func_ptr = multiply; break;
	}

	cout << func_ptr(x, y) << endl;
	return 0;
}

컴파일 타임에 결정되지 않고, 런타임에 결정된다.

 

 

속도 면에서 static binding에서 더 빠르다.

실행시킬 때에도 주소를 타고 가기 때문에. 대신 dynamic binding에서 프로그래밍이 더 유연해진다.

 

 

12.6 가상(함수) 표 Virtual Tables

 

Der은 Base를 상속 받고, func1만 오버라이딩하고 있다.

컴파일러 내부적으로 virtual 기능이 선언되면 직접 정적 바인딩을 하는 것이 아니라,

virtual function의 표를 만들고 동적 바인딩을 시킨다.

 

만약 Base 인스턴스 가 만들어져서 my_base.func1()를 한다면 함수를 직접 호출하는 것이 아니라,

일단 *_vptr을 통해 가상 함수 테이블을 찾는다.

그 함수 테이블에서 해당 함수의 주소를 찾아, 그 함수의 instruction을 실행한다.

 

자식 클래스로 생성된 객체를 부모 클래스의 포인터 레퍼런스에 집어넣어도,

테이블이 바뀌지 않기 때문에 부모에도 가고, 자식에도 가는 구조를 유지한다.

 

 

 

 

 

 

12.7 순수Pure 가상 함수, 추상Abstract 기본 클래스, 인터페이스Interface 클래스

 

순수 가상 함수 - body가 없어서 자식 클래스에서 오버라이딩을 해야 함

추상 기본 클래스 - 순수 가상 함수가 포함된 클래스.

인터페이스 클래스 - 순수 가상 함수로만 이루어진 클래스

 

class Animal
{
protected:
	string m_name;
public:
	Animal(std::string name)
		: m_name(name)
	{}
public:
	string getName() {	return m_name;	}
	virtual void speak() const = 0;//function의 정의를 없애버림
	// = 0 을 해 놓은 함수를 pure virtual function이라고 함
};

void Animal::speak() const //the body of the pure virtual func
{
	cout << m_name << " ??? " << endl;
}
#include <iostream>
using namespace std;
class Animal
{
protected:
	string m_name;
public:
	Animal(std::string name)
		: m_name(name)
	{}
public:
	string getName() {	return m_name;	}
	virtual void speak() const = 0;//function의 정의를 없애버림
	// = 0 을 해 놓은 함수를 pure virtual function이라고 함
};


class Cat : public Animal
{
public:
	Cat(string name)
		: Animal(name)
	{}

	void speak() const
	{
		cout << m_name << " Meow " << endl;
	}
};


class Dog : public Animal
{
public:
	Dog(string name)
		: Animal(name)
	{}

	void speak() const
	{
		cout << m_name << " Woof " << endl;
	}
};


class Cow : public Animal
{
public:
	Cow(string name)
		: Animal(name)
	{}
};

int main()
{
	//Animal ani("Hi"); //불가
	//abstract 클래스는 인스턴스를 만들 수 없다.
	
	//Cow cow("hello"); 
	//자식 클래스에서 speak 함수를 오버라이딩하지 않으면 인스턴스를
	//만들 수 없다.
	//즉 순수 가상 함수면 무조건 자식 클래스가 해당 함수를 구현해야 한다.
	return 0;
}

 

상속 구조를 설계하도록 도와주는 키워드

 

#include <iostream>
using namespace std;

//인터페이스는 모든 함수가 순수 가상 함수이다.
//외부에서 사용을 할 때 이러이러한 기능이 있을 거다라고 예측할 수 있는
//중계기 역할을 할 수 있기 때문에 interface라고 불림

class IErrorLog
{
public:
	virtual bool reportError(const char* errorMessage) = 0;
	virtual ~IErrorLog() {}
};

class FileErrorLog : public IErrorLog
{
public:
	bool reportError(const char* errorMessage) override
	{
		cout << "Writing error to a file " << endl;
		return true;
	}
};

void doSomething(IErrorLog& log)
{
	log.reportError("Runtime error!!");
}

class ConsoleErrorLog : public IErrorLog
{
public:
	bool reportError(const char* errorMessage) override
	{
		cout << "Printing error to a console " << endl;
		return true;
	}
};
int main()
{
	FileErrorLog file_log;
	ConsoleErrorLog console_log;

	doSomething(file_log);
	doSomething(console_log);
	return 0;
}

 

 

12.8 가상 기본 클래스와 다이아몬드 상속 문제

Virtual base classes and The diamond problem

 

 

#include <iostream>
using namespace std;

class PoweredDevice
{
public:
	int m_i;
	PoweredDevice(int power)
	{
		cout << "PoweredDevice: " << power << '\n';
	}
};

class Scanner : public PoweredDevice
{
public:
	Scanner(int scanner, int power)
		: PoweredDevice(power)
	{
		cout << "Scanner: " << scanner << '\n';
	}
};

class Printer : public PoweredDevice
{
public:
	Printer(int printer, int power)
		: PoweredDevice(power)
	{
		cout << "Printer: " << printer << '\n';
	}
};


class Copier : public Scanner, public Printer
{
public:
	Copier(int scanner, int printer, int power)
		: Scanner(scanner, power), Printer(printer, power)
	{
	}
};
int main()
{
	Copier cop(1, 2, 3);
	cout << &cop.Scanner::PoweredDevice::m_i << endl;
	cout << &cop.Printer::PoweredDevice::m_i << endl;
	return 0;
}

 

#include <iostream>
using namespace std;

class PoweredDevice
{
public:
	int m_i;
	PoweredDevice(int power)
	{
		cout << "PoweredDevice: " << power << '\n';
	}
};

class Scanner : virtual public PoweredDevice
{
public:
	Scanner(int scanner, int power)
		: PoweredDevice(power)
	{
		cout << "Scanner: " << scanner << '\n';
	}
};

class Printer : virtual public PoweredDevice
{
public:
	Printer(int printer, int power)
		: PoweredDevice(power)
	{
		cout << "Printer: " << printer << '\n';
	}
};


class Copier : public Scanner, public Printer
{
public:
	Copier(int scanner, int printer, int power)
		: Scanner(scanner, power), Printer(printer, power), PoweredDevice(power)
	{
	}
};
int main()
{
	Copier cop(1, 2, 3);
	cout << &cop.Scanner::PoweredDevice::m_i << endl;
	cout << &cop.Printer::PoweredDevice::m_i << endl;
	return 0;
}

다이아몬드 상속을 하면 원래 있었던 클래스 A에 해당하는 것이 두 개가 생성될 수 있는데,

virtual 키워드를 넣어 상속받음으로서 그걸 하나만 호출시킬 수 있다.

 

 

12.9 객체 잘림Object slicing과 reference wrapper

 

부모 클래스에 자식 클래스를 casting할 경우, 자식 클래스의 멤버나 기능이 잘릴 수 있다 = 객체 잘림

 

#include <iostream>
using namespace std;

class Base
{
public:
	int m_i = 0;
	virtual void print()
	{
		cout << "I'm Base" << endl;
	}
};

class Derived : public Base
{
public:
	int m_j = 1;
	virtual void print() override
	{
		cout << "I'm derived" << endl;
	}
};


void doSomething(Base& b)
{
	b.print();
}

int main()
{
	Derived d;
	Base& b = d;
	b.print(); //다형성 발현
	return 0;
}

 

int main()
{
	Derived d;
	Base b = d;
	b.print(); 
	return 0;
}

 

레퍼런스가 아닌 단순 대입인 경우 복사가 된다.

즉 완전한 Base형이 되는 것. d만 갖고 있는 정보를 b는 가질 수 없게 된다.

= 객체 잘림, 다형성 사라짐

 

 

void doSomething(Base& b)
{
	b.print();
}

int main()
{
	Derived d;
	doSomething(d); // I;m derived
	return 0;
}
void doSomething(Base b)
{
	b.print();
}

int main()
{
	Derived d;
	doSomething(d); // I;m Base
	return 0;
}

즉 의도적이 아니라면 꼭 reference로 받아야 한다.

 

vector의 경우, push_back을 하면 slicing이 일어난다.

 

vector는 reference를 넣을 수 없다.

 

이 경우 다형성이 정상적으로 구현된다.

만약 어떻게 해도 vector에 reference를 넣고 싶은 경우,

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

class Base
{
public:
	int m_i = 0;
	virtual void print()
	{
		cout << "I'm Base" << endl;
	}
};

class Derived : public Base
{
public:
	int m_j = 1;
	virtual void print() override
	{
		cout << "I'm derived" << endl;
	}
};


void doSomething(Base b)
{
	b.print();
}

int main()
{
	Derived d;
	Base b;
	std::vector<reference_wrapper<Base>> my_vec;
	//Base의 reference를 저장하는 벡터가 될 수 있다.

	my_vec.push_back(b);
	my_vec.push_back(d);
	for (auto& ele : my_vec)
		ele.get().print();
	return 0;
}

 

 

12.10 동적 형변환

 

#include <iostream>
#include <string>

using namespace std;

class Base
{
public:
	int m_i = 0;
	virtual void print()
	{
		cout << "I'm Base" << endl;
	}
};

class Derived1 : public Base
{
public:
	int m_j = 1024;
	virtual void print()
	{
		cout << "I'm derived" << endl;
	}
};

class Derived2 : public Base
{
public:
	string m_name = "Dr. Two";
	virtual void print() override
	{
		cout << "I'm derived" << endl;
	}
};

int main()
{
	Derived1 d1;
	d1.m_j = 2048;
	Base* base = &d1;
	//이 경우 base의 원소엔 접근할 수 있지만
	//derived 타입의 원소엔 접근할 수 없다.

	//이 경우 다시 derived로 변경해야 할 때 dynamic_cast를 할 수 있다.
	auto* base_to_d1 = dynamic_cast<Derived1*>(base);
	cout << base_to_d1->m_j << endl;
	//다시 접근 가능
	base_to_d1->m_j = 256;
	cout << d1.m_j << endl;
	return 0;
}

 

 

 

 

int main()
{
	Derived1 d1;
	d1.m_j = 2048;
	Base* base = &d1;
	//이 경우 base의 원소엔 접근할 수 있지만
	//derived 타입의 원소엔 접근할 수 없다.

	//이 경우 다시 derived로 변경해야 할 때 dynamic_cast를 할 수 있다.
	auto* base_to_d1 = dynamic_cast<Derived2*>(base);
	//dynamic_cast는 캐스팅에 실패하면 null ptr을 넣는다.
	//Derived1에서 Base에 넣었다가 Derived2에 넣으면 캐스팅에 실패한다.
	return 0;
}
int main()
{
	Derived1 d1;
	d1.m_j = 2048;
	Base* base = &d1;
	//이 경우 base의 원소엔 접근할 수 있지만
	//derived 타입의 원소엔 접근할 수 없다.

	auto* base_to_d1 = static_cast<Derived2*>(base);
	//이 때 static_cast는 최대한 형변환을 시켜준다
	//그러므로 위의 형 변환도 가능.
	//그러나 런타임에서 에러를 잡을 수 없다.
	return 0;
}

 

일반적으로 동적 캐스팅도 가급적 쓰지 말자는 흐름이 있다.

 

 

 

12.11 유도 클래스에서 출력 연산자 사용하기

 

#include <iostream>
#include <string>

using namespace std;

class Base
{
public:
	Base() {}
	//friend 키워드가 붙은 함수는 엄밀히 말하면 멤버가 아니기 때문에
	//오버라이딩이 불가하다.
	friend std::ostream& operator << (ostream& out, const Base& b)
	{
		return b.print(out);
	}
	//대신 멤버로 가질 수 있는 함수를 만들고
	//이것을 자식 클래스에서 오버라이딩할 수 있다.
	virtual std::ostream& print(std::ostream& out) const
	{
		out << "Base";
		return out;
	}
};

class Derived : public Base
{
public:
	Derived() {}
	virtual ostream& print(ostream& out) const override
	{
		out << "Derived";
		return out;
	}
};
int main()
{
	Base b;
	cout << b << endl;
	Derived d;
	cout << d << endl;
	Base& bref = d;
	cout << bref << endl;
	return 0;
}

virtual 함수 여러 개를 만들어 자식 클래스가 이 함수들을 조립해서 새로운 기능을 만들도록 하는 경우도 있다.