추상클래스는 함수 선언은 있는데 구현이 하나라도 없으면 추상 클래스다. 왜 구현을 안해놓냐?? 자식클래스가 구현을 할 거니깐! 만약 자식 클래스가 구현을 안하면 컴파일 에러가난다! 선언만 있고 구현이 없는 함수를 순수(Pure) 함수라고 한다. 위의 Speak함수는 순수함수다. 순수 가상함수는 virtual void Speack() = 0 처럼 virtual과 = 0 을 붙여주면 된다.
추상 클래스를 object로 만들면 당연히 컴파일 에러가 난다. 구현체가 없는데 어떻게 객체를 만들겠나..
객체를 스택에 만들든, 힙에 만들든 무조건 에러다. 하지만 자식 클래스 Cat을 만든 다음 포인터로 캐스팅 하면 된다. 왜냐하면 가상 테이블 때문에 Cat의 Speak을 호출할 걸 알기때문!
인터페이스
C++는 인터페이스가 없다. 그래서 순수 가상 함수만으로 이루어진 클래스를 interface라고 C++ 개발자들 끼리 부르기로 했다.
#include <stdio.h>
#include <iostream>
#include <vector>
class Scores
{
public:
Scores(int score, std::string subject);
int score;
std::string subject;
};
Scores::Scores(int score, std::string subject)
{
this->score = score;
this->subject = subject;
}
int main()
{
std::vector<Scores> scores;
scores.reserve(3);
scores.push_back(Scores(30, "C++"));
scores.push_back(Scores(40, "JAVA"));
scores.push_back(Scores(50, "Python"));
std::vector<Scores>::iterator it = scores.begin();
for (std::vector<Scores>::iterator iter = scores.begin(); iter != scores.end(); ++iter)
{
std::cout << iter->score << (*iter).subject << std::endl;
}
}
위의 코드는 Vector에 Scores 객체를 담는 코드다. 이때 new Scores가 아니라 그냥 Scores로 객체를 만들기 때문에 값 복사가 일어난다(포인터가 아님). Scores 처럼 용량이 작은 객체는 괜찮지만 용량이 큰 객체를 이런식으로 관리하면 값 복사 문제때문에 굉장히 힘들어진다.
위의 그림은 scores 객체의 capacity를 초과해서 메모리 재할당이 일어나는 경우를 나타낸 그림이다.
이렇듯 객체를 직접 보관하면 값복사 때문에 굉장히 느려질 수 있다. 이때의 해결책은 JAVA처럼 포인터를 Vector에 저장하는 방법이다.
위의 그림은 scores의 사본을 만들때 발생하는 값 복사 문제다. 값으로 저장되어 있기 때문에 사본을 그대로 복사한다. 문제다 문제
Vector 포인터(JAVA 방식)
std::vector<Scores*> scores;
scores.reserve(3);
scores.push_back(new Scores(30, "C++"));
scores.push_back(new Scores(40, "JAVA"));
scores.push_back(new Scores(50, "Python"));
std::vector<Scores*>::iterator it = scores.begin();
for (std::vector<Scores*>::iterator iter = scores.begin(); iter != scores.end(); ++iter)
{
std::cout << (*iter)->score << (*iter)->subject << std::endl;
}
위의 코드는 new를 사용해서 포인터를 저장하는 Vector다. 해서 타입도 std::vector<Scores*>이다
여기서 주의해야할 점은 it는 Vector의 첫번째 인덱스의 값 즉 Socre 포인터(Score *)를 가르키는 포인터다. 그래서 값에 접근하기 위해서는 (*iter)->socre와 같이 사용해야한다.
위의 그림과 같이 메모리 복사가 일어나도 포인터 바이트(통상적으로 4바이트) 수만 복사하면 되니깐 아주 가볍다.
하지만 여기서 중요한점은 포인터기 때문에 메모리 제거를 내가 스스로 해줘야한다. clear()라고 해도 알아서 메모리 해제 해주지 않는다.
Animal을 상속받아 만든 Cat 클래스가 있다. Cat 클래스에 Speak 메소드를 오버라이딩 했다.
두개의 오브젝트를 만드는데 타입을 하나는 Cat으로 하나는 부모클래스인 Animal로 했다. 그때 speak()메소드를 호출하면 누가 나올까?
JAVA는 무조건 오버라이딩된 자식 메소드가 나오고 C++는 무늬 즉 타입(포인터)따라 간다.
JAVA의 기본 바인딩은 동적 바인딩이고 C++의 기본 바인딩은 정적 바인딩이다.
정적 바인딩
위의 그림에서 Cat 오브젝트를 만들고 타입도 Cat*다. 그래서 힙에 할당한 모든 메모리를 그냥 쓰면된다. 전혀 문제가 안됨
하지만 위의 그림은 Cat 오브젝트를 만들었지만 타입은 부모 클래스 즉 Animal이다. 이럴때 Animal의 것만 포인팅한다.
Cat의 메모리 영역에 접근하고 싶으면 Type, 포인터를 Cat*으로 변경해야 한다.
정적 바인딩을 그림으로 잘 표현한 예다. C++는 기본적으로 타입 따라간다. 왜냐? 정적바인딩이니깐
가상함수(동적 바인딩)
그렇다면 C++에서 자바처럼 무조건 만들어진 오브젝트 즉 자식 클래스의 메소드가 호출되려면 어떻게 해야 할까?
virtual 키워드를 붙이면 된다. virtual을 붙이면 자바처럼 동적 바인딩을 하겠다는말이다.
참고로 자바는 기본적으로 모든 것이 다 virtual이고 final을 붙이면 정적 바인딩이 된다.
동적 바인딩은 실행 중에 어떤 함수를 호출할지 결정되고 정적 바인딩 보다 느리다. 실행 중에 어떤 함수를 결정하기 위해 가상 테이블이 생성된다.
가상테이블
컴파일시에 virtual을 정의한 메소드에 한해서 가상 테이블이 생성되는데 오브젝트를 생성할 때 해당 클래스의 가상 테이블 주소가 함께 저장된다. (EX __vfptr, 0x00459bd8)
위의 가상테이블을 활용하면 오브젝트가 메소드를 호출할때 가상테이블로 가서 "야! Move 메소드 호출할건데 부모거말고 내걸로 호출해라! 니가 주소 알고 있지? 그 주소로 Jump해서 호출하고와라" 이런식으로 가상 테이블에 접근해서 메소드를 호출한다. 그렇기 때문에 느리다.
python이나 java 등 보편적인 언어는 클래스 객체 생성을 Heap 영역에 하는 반면 C++는 자유도가 높아 객체를 스택에도 생성할 수 있다. 물론 스택에 너무 큰 용량의 객체를 만들면 스택 오버플로우가 나니 조심해야 한다.
class Vector
{
int x;
int y;
}
Vector v;
위의 예제처럼 객체를 만들면 8바이트(int 4 바이트라고 가정) 만큼 스택에 할당한다.
public class Vector
{
int x;
int y;
}
Vector* v = new Vector();
위의 예제처럼 객체를 생성하면 스택영역의 변수 b에 Vector를 할당한 Heap영역의 메모리 첫번째 주소를 담는다.
객체 배열 생성
우측의 C++부터 보면 Heap영역에 Vector 객체 10개를 만들고(8바이트 * 10 = 80바이트) 그 첫번째 주소를 스택의 list 변수에 담는다. 그래서 list 변수의 타입은 Vector *이다.
반면에 Java의 경우 new Vector[10]을 해도 객체를 10개 만들지 않고 Vector 객체를 담을 수 있는 레퍼런스(주소)를 반환한다. (포인터가 4바이트라고 가정하면 4바이트 * 10 = 40바이트) 실제로 값을 담으려면 for문을 10번 돌면서 값을 생성해주면 된다.
정리하면, 힙에 Vector 10개를 바로 만드려면 Vector* list = new Vector[10]을 하면 되고, JAVA에서는 불가능하다. 왜냐하면 모든게 다 레퍼런스 즉 포인터라서.
C++에서 Java의 Vector 객체를 담을 수 있는 레퍼런스(주소) 10개를 반환하는 코드를 만드려면
Vector** list = new Vector*[10] 이렇게 하면된다.
list 타입을 설명하자면 처음의 * 포인터는 배열 그 자체 그러니깐 전체를 나타내는 것이고 두번쨰 * Vector는 각각의 배열은 Vector 객체를 가르키는 포인터를 담는다는 뜻이다.
마지막으로 객체를 삭제할때다. C++에서 delete를 안해주면 큰일난다. Java처럼 자동으로 가비지컬렉터가 지워주지 않기때문이다. 반드시 삭제하도록 하자
이동 생성자의 rhs는 무엇일까? 바로 temp이다. 그러니깐 return temp를 할 때 temp instance를 기반으로 이동 생성자가 불렸다는 뜻. 당연하다 복사 생성자도 temp instance를 기반으로 했으니.
이동 생성자에서는 new로 메모리 할당받아서 Deep Copy이런거 안한다.
그냥 rhs.name(이때 rhs는 temp 임시 객체 r-value 참조)의 주소 값(포인터니깐)을 name에 할당 그러니깐 주소값만 name에 넣어주고 끝이다. 왜냐하면 조만간 사라질 임시 객체니깐! 그리고 그 값을 고스란히 return 하고 아름답게 전사한다. 이때 return 한 임시 객체 값은 + c 의 연산과정에서 또 쓰이게 된다!
이로써 Deep Copy(복사생성)에서 메모리 할당받고 어쩌고 저쩌고를 포인터 하나로 아주 성능 좋게 바꿨다.