반응형

 

멤버함수 동작원리

- C++의 객체의 멤버변수는 객체 내에 존재하지만 멤버함수는 다음과 같은 관계를 갖는다.

  • 멤버함수는 메모리의 한 공간에 별도로 위치한다.
  • 멤버함수가 정의된 클래스의 모든 객체가 이를 공유하는 형태를 취한다. (함수포인터를 생각하면 이해하기 편하다.)

 

 

 

 

가상함수 동작원리

- 예제 소스는 다음과 같다.

#include <iostream>
using namespace std;

class Parent
{
public:
    virtual void Func_1()
    {
        cout << "Parent : Func_1" << endl;
    }
    virtual void Func_2()
    {
        cout << "Parent : Func_2" << endl;
    }
};

class Son : public Parent
{
public:
    virtual void Func_1()
    {
        cout << "Son : Func_1" << endl;
    }
    void Func_3()
    {
        cout << "Son : Func_3" << endl;
    }
};

void main()
{
    Parent* parentPtr = new Parent();
    parentPtr->Func_1();
    
    Son* sonPtr = new Son();
    sonPtr->Func_1();
    
    //결과
    //Parent : Func_1
    //Son : Func_1
}

- 위의 코드에서 가상함수를 확인할 수 있는데, 한 개 이상의 가상함수를 포함하는 클래스에 대해서는 컴파일러가 '가상함수 테이블'이라는 것을 만든다. (V-Table이라고도 한다.)

- 가상함수 테이블은 Key와 Value를 가지고 있다.

  • Key : 호출하고자 하는 함수를 구분지어주는 구분자의 역할을 한다.
  • Value : 구분자에 해당하는 함수의 주소정보를 알려주는 역할을 한다.

- 따라서, Parent 클래스와 Son의 클래스의 가상함수 테이블은 다음과 같이 구성된다.

  • Parent 객체의 Func_1 함수를 호출하는 경우, Parent 테이블에서 첫 번쨰 행의 정보를 참조하여 Func_1 함수를 호출하게 된다.
  • Son 객체의 테이블을에서는 오버라이딩 된 가상함수의 Func_1에 대한 정보가 존재하지 않는다.
  • 즉, 오버라이딩 된 가상함수의 주소정보는 자식 클래스의 가상함수 테이블에 포함되지 않는다.
  • 결과적으로 오버라이딩된 가상함수를 호출하면, 가장 마지막에 오버라이딩을 한 자식 클래스의 멤버함수가 호출되게된다.

 

 

 

 

가상함수 테이블이 참조되는 방식

- 예제 소스가 실행되면, main함수가 호출되기 전에 가상함수 테이블이 메모리 공간에 할당된다.

  ※ 참고로 가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당된다. (가상함수 테이블이 멤버함수의 호출에 사용되는 일종의 데이터이기 때문.)

- main 함수가 호출되어 객체가 생성되고 나면, 각 Parent, Son 객체는 해당 클래스의 가상함수 테이블의 주소값이 저장된다.

  ※ 가상함수 테이블의 주소값은 우리가 직접 참조할수 있는 주소값이 아니고, 내부적 필요에 의해 참조되는 주소값이다.

- Parent 객체에서 Func_1함수를 호출하게되는 과정.

  • Parent 객체가 Parent 클래스의 가상함수 테이블 참조 → 100번지에 위치한 함수 실행

- Son 객체에서 Func_1함수를 호출하게 되는 과정.

  • Son객체가 Son 클래스의 가상함수 테이블 참조 → 200번지에 위치한 함수 실행

 

 

 

 

가상함수 테이블에 의한 속도의 저하

- 클래스에 가상함수가 포함되어 가상함수 테이블이 생성되면, 이 테이블을 참조하여 호출될 함수가 결정되기 때문에 실행속도가 감소하게된다.

- 하지만 극히 미미하고 속도차이를 감안하더라도 많은 장점을 제공하기 때문에 유용하게 활용되는것이다.

 

반응형

'Stack > C++' 카테고리의 다른 글

C++ friend  (0) 2021.12.05
C++ 다중상속  (0) 2021.11.22
C++ 순수 가상함수, 추상 클래스  (0) 2021.11.11
C++ 업캐스팅, 오버라이딩, 가상함수  (0) 2021.11.08
C++ 클래스 상속  (0) 2021.11.04
반응형

 

순수 가상함수(Pure Virtual Function)

- 함수의 몸체가 정의되지 않은 함수를 의미한다.

- 부모 클래스에서 사용하지 않는 함수이지만 자식 클래스는 무조건 동일한 이름으로 함수를 오버라이딩을 적용하게 강제하는 방법.

 

사용법

class A
{
public:
    virtual void Func1() = 0;
    virtual void Func2() = NULL;
    virtual void Func3() abstract;
};
  • 가상(virtual) 함수에 NULL(0) 값을 대입하거나 abstract를 붙여주면 해당 함수는 순수가상함수로 취급된다.
  • 순수 가상함수를 가진 클래스를 상속받은 클래스는 순수 가상함수의 몸체를 무조건 정의해주어야 한다.

 

 

추상 클래스(Abstract Class)

- 클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다.

- 멤버함수 중, 하나 이상 순수 가상함수로 선언하면 해당 클래스는 추상 클래스가 된다.

class B : public A
{
public:
    void Func1() override
    {
        cout << "Func1" << endl;
    }
    void Func2() override
    {
        cout << "Func2" << endl;
    }
    void Func3() override
    {
        cout << "Func3" << endl;
    }
};

void main()
{
    A a;	//컴파일 에러
    B b;
    b.Func1();
    b.Func2();
    b.Func3();
    
    //결과
    //Func1
    //Func2
    //Func3
}
  • A 클래스는 순수 가상함수를 포함하여 추상 클래스이기 때문에, 객체 생성시에 컴파일 에러가 발생한다.

 

 

반응형

'Stack > C++' 카테고리의 다른 글

C++ 다중상속  (0) 2021.11.22
C++ 멤버함수, 가상함수 동작원리  (0) 2021.11.17
C++ 업캐스팅, 오버라이딩, 가상함수  (0) 2021.11.08
C++ 클래스 상속  (0) 2021.11.04
C++ 함수 오버로딩, Deault 매개변수  (0) 2021.10.24
반응형

 

업캐스팅 (UpCasting)

- C++에서, A 클래스 형 포인터 변수는 A 클래스의 객체 또는 A 클래스를 직, 간접적으로 상속하는 모든 객체를 가리킬 수 있다. (자식 클래스 객체의 주소값을 부모 클래스 포인터 변수에 담아 사용할 수 있다.)

- IS-A 상속 관계의 성립으로 인해서, 부모 클래스를 직, 간접적으로 상속하는 객체를 부모 클래스 객체의 일종으로 간주한다.

- 여러 자식 클래스들의 부모 클래스가 동일 할 경우 해당 부모 클래스에 여러 자식 클래스를 일괄적으로 처리가 가능하다.

#include <iostream>
using namespace std;

class Parent
{
public:
    void ShowParent()
    {
        cout << "Parent class" << endl;
    }
};

class Son : public Parent
{
public:
    void ShowSon()
    {
        cout << "Son class" << endl;
    }
};

class Daughter : public Parent
{
public:
    void ShowDaughter()
    {
        cout << "Daughter class" << endl;
    }
};

void main()
{
    Parent* ptr[2] = { new Son(), new Daughter() };
    for (int i = 0; i < 2; i++)
    {
        ptr[i]->ShowParent();
    }
    
    //결과
    //Parent class
    //Parent class
}

 

 

※ 업캐스팅 사용시 주의사항

- 위와 같이 업캐스팅하여 함수를 호출할 때 다음과 같은 경우는 컴파일 에러를 일으킨다.

ptr[0]->ShowSon();
ptr[1]->ShowDaughter();

 

Why?

- 포인터의 형이 존재하는 이유는 메모리를 참조하는 기준이 된다. (C에서 포인터를 했을때 확인했던 개념이다.)

- 즉, Son, Daughter의 객체를 참조하는 ptr은 Person의 메모리 크기만을 참조하기 때문이다.

 

 

 

 

오버라이딩(Overriding)

- 부모 클래스의 함수를 자식 클래스가 동일한 이름으로 만들어 해당 함수의 내용을 재정의한다.

#include <iostream>
using namespace std;

class Parent
{
public:
    void SayHello()
    {
        cout << "Parent : 'Hello'" << endl;
    }
};

class Son : public Parent
{
public:
    void SayHello()
    {
        cout << "Son : 'Hello'" << endl;
    }
};

void main()
{
    Parent parent;
    Son son;
    
    parent.SayHello();
    son.SayHello();
    
    //결과
    //Parent : 'Hello'
    //Son : 'Hello'
}
  • 위의 코드를 기준으로 자식 클래스에서 부모 클래스의 함수를 오버라이딩 했다고 해서 업캐스팅으로 자식 클래스의 객체를 참조했을때 오버라이딩 된 함수가 호출되지 않는다.
  • 이유는 업캐스팅 주의사항에서 언급했던 것과 동일하다. 그리고 이러한 문제를 해결해주는게 가상(virtual)함수이다.

 

 

 

 

가상함수(Virtual Function)

- 자식 클래스를 부모 포인터에 업캐스팅 했을 시, 오버라이딩된 함수를 사용하게 해주는 방법

- 부모 클래스에 virtual을 사용해준다.

  • virtual 반환형 함수명 (매개변수)

- 소멸자 함수도 virtual을 사용해 줘야한다. (사용하지 않으면 자식 클래스의 소멸자가 호출되지 않는다.)

#include <iostream>
using namespace std;

class Parent
{
public:
    virtual void SayHello()
    {
        cout << "Parent : 'Hello'" << endl;
    }
    void SayBye()
    {
        cout << "Parent : 'Bye'" << endl;
    }
    virtual ~Parent()
    {
        cout << "Parent Destructor" << endl;
    }
};

class Son : public Parent
{
public:
    void SayHello()
    {
        cout << "Son : 'Hello'" << endl;
    }
    void SayBye()
    {
        cout << "Son : 'Bye'" << endl;
    }
    ~Son()
    {
        cout << "Son Destructor" << endl;
    }
};

void main()
{
    Parent* ptr = new Son();
	
    ptr->SayHello();
    ptr->SayBye();
    delete ptr;
	
    //결과
    //Son : 'Hello'
    //Parent : 'Bye'
    //Son Destructor
    //Parent Destructor
}
  • 동적할당을 해제할 때 부모 클래스의 virtual 유무에 따라 자식 클래스의 소멸자가 호출되는지 확인해보는것도 좋다.

 

※※ 추가 : 멤버, 가상함수의 동작원리

 

C++ 멤버함수, 가상함수 동작원리

멤버함수 동작원리 - C++의 객체의 멤버변수는 객체 내에 존재하지만 멤버함수는 다음과 같은 관계를 갖는다. 멤버함수는 메모리의 한 공간에 별도로 위치한다. 멤버함수가 정의된 클래스의 모

srdeveloper.tistory.com

 

 

반응형

'Stack > C++' 카테고리의 다른 글

C++ 멤버함수, 가상함수 동작원리  (0) 2021.11.17
C++ 순수 가상함수, 추상 클래스  (0) 2021.11.11
C++ 클래스 상속  (0) 2021.11.04
C++ 함수 오버로딩, Deault 매개변수  (0) 2021.10.24
C++ this 포인터  (0) 2021.10.24
반응형

 

상속(Inheritance)

- 다른 클래스의 멤버 변수, 멤버 함수를 자신의 멤버인 것처럼 사용이 가능.

- B라는 클래스가 A 클래스를 상속하게 되면, B 클래스는 A 클래스가 지니고 있는 모든 멤버를 물려받는다.

- 클래스의 재사용과 관련이 있다.

 

상속 예제 코드

#include <iostream>
#include <string>
using namespace std;

class Person
{
private:
    int age;
    int height;
public:
    Person(int _age, int _height) : age(_age), height(_height) { }
};

class Employee : public Person
{
private:
    string department;
    string position;
public:
    Employee(string _department, string _position, int _age, int _height) : Person(_age, _height)
    {
        department = _department;
        position = _position;
    }
};

void main()
{
    Employee employee("R&D", "Staff", 20, 180);
}
  • Person 클래스를 상속한 Employee 클래스의 생성자는 부모 클래스인 Person 클래스의 멤버까지 초기화해야할 의무가 있다.
  • 자식 클래스의 생성자는 부모 클래스의 생성자를 호출해서 부모 클래스의 멤버를 초기화하는 것이 좋다.
  • (자식 클래스는 이니셜라이저(콜론 초기화)를 이용해서 상속 클래스의 생성자 호출을 명시할 수 있다.)

 

상속 용어 정리

Person Employee
상위 클래스 하위 클래스
기초(Base) 클래스 유도(Derived) 클래스
슈퍼(Super) 클래스 서브(Sub) 클래스
부모 클래스 자식 클래스
  • 일반적으로 기초 클래스와 유도 클래스라는 표현을 사용한다.

 

 

 

상속 생성자 호출 순서 예제 코드

#include <iostream>
using namespace std;

class Base
{
private:
    int baseNum;
public:
    Base(int n) : baseNum(n)
    {
        cout << "Base(int n)" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
};

class Derived : public Base
{
private:
    int derivNum;
public:
    Derived(int n1, int n2) : Base(n1), derivNum(n2)
    {
        cout << "Derived(int n1, int n2)" << endl;
    }
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
};

void main()
{
    Derived derived(10, 20);
    
    //결과
    //Base(int n)
    //Derived(int n1, int n2)
    //~Derived()
    //~Base()
}
  • 자식 클래스에서 생성자가 호출될 때, 이니셜라이저에 부모 클래스의 생성자가 호출이 되는지 확인한 후, 이니셜라이저에 있는 생성자가 호출될지 void형 생성자가 호출될지 정해진다.
  • 자식 클래스를 만든다고 해서 자식 클래스의 생성자가 먼저 호출되지 않고, 부모 클래스의 생성자가 먼저 호출 되는것을 확인할 수 있다.
  • 소멸자는 생성자와 반대로 자식 클래스, 부모 클래스 순으로 호출된다.

 

 

 

상속과 접근지시자

- public > protected > private

- 상속 관계에서는 public과 protected가 차이를 보이지 않는다.

- protected에 선언된 멤버는 자식 클래스에서도 자유롭게 사용할 수 있지만, 부모 클래스와 자식 클래스 사이에서도 '정보은닉'은 지켜지는게 좋다.

- 상속을 명시할 때 사용하는 접근지시자는 다음과 같은 특성을 가지고 있다.

  • public : public 범위의 멤버를 public으로 변경시켜서 상속.
  • protected : protected 보다 넓은(public) 접근 범위의 멤버를 protected로 변경시켜서 상속
  • private : private 보다 넓은(public, protected) 접근 범위의 범버를 private로 변경시켜서 상속

- public 이외의 상속은 다중상속과 같이 특별한 경우가 아니라면 잘 사용하지 않는다.

 

 

 

 

상속의 조건

- 상속은 다음과 같은 조건에서 사용한다.

  • IS-A((자식)은 (부모)이다) : Employee(자식 클래스)는 Person(부모 클래스)이다. 
  • HAS-A((자식)은 (부모)를 갖고있다) : Police(자식 클래스)는 Gun(부모 클래스)를 갖고있다.

- HAS-A의 관계는 다음과 같은 이유로 상속보다는 멤버로 갖고있는것이 더 좋은 모델이 될 수 있다.

  • 총을 소유하지 않은 경찰을 표현해야 할 수도 있다.
  • 경찰이 총이 아니라, 3단봉을 소유할 수도 있다.

- 결론 : 상속은 IS-A 관계의 표현에 적합. HAS-A 관계에서도 사용될 수 있으나, 프로그램 변경에 많은 제약을 가져다 줄 수 있다.

반응형

'Stack > C++' 카테고리의 다른 글

C++ 순수 가상함수, 추상 클래스  (0) 2021.11.11
C++ 업캐스팅, 오버라이딩, 가상함수  (0) 2021.11.08
C++ 함수 오버로딩, Deault 매개변수  (0) 2021.10.24
C++ this 포인터  (0) 2021.10.24
C++ 복사 생성자 심화  (0) 2021.10.17
반응형

 

함수 오버로딩(Function overloading)

- C에서는 동일한 이름의 함수가 정의되는것을 허용하지 않는다.

- C++에서는 함수에 전달되는 인자를 통해서 호출하려는 함수의 구분이 가능하다.

- 그렇기 때문에 매개변수의 선언형태가 다르다면 동일한 이름의 함수를 정의할 수 있다.

  ※ C에서는 함수의 이름만 이용해서 호출되는 대상을 찾는 반면, C++에서는 함수의 이름과 매개변수 선언 두 가지 정보를 이용해 호출 대상을 찾는다.

#include <iostream>
using namespace std;

void Func(int n)
{
    cout << n << endl;
}

void Func(char c)
{
    cout << c << endl;
}

void main()
{
    Func(10);
    Func('A');
    
    //결과
    //10
    //A
}

 

 

 

 

Default 매개변수

- 단어 그대로 기본적으로 설정되어 있는 매개변수라고 생각하면 된다.

- 함수를 호출했을 때, 매개변수 값을 전달하지 않으면 자동으로 기본값을 가진다.

- Default 값은 함수의 원형에만 위치시켜야한다.

#include <iostream>
using namespace std;

class Test
{
public:
    void Func(int n1 = 10, int n2 = 20);
};

void Test::Func(int n1, int n2)
{
    cout << n1 + n2 << endl;
}

void main()
{
    Test test;
    test.Func();
    
    //결과 30
}

 

 

부분적 Defualt 값 설정

- 반드시 모든 매개변수가 Default 값을 가질 필요는 없다.

- 다음과 같은 경우에 부분적 Default 값을 저장할 수 있다.

  • 반드시 오른쪽 매개변수의 Default 값부터 채우는 형태로 정의한다.
void Func(int n1, int n2 = 20, int n3 = 10)
  • Default 매개변수로 인해 형성되는 모호한 오버로딩은 허용하지 않는다.
#include <iostream>
using namespace std;

class Test
{
public:
    void Func();
    void Func(int n1 = 10, int n2 = 20);
};

void Test::Func()
{
    cout << 100 << endl;
}

void Test::Func(int n1, int n2)
{
    cout << n1 + n2 << endl;
}

void main()
{
    Test test;
    test.Func(); //에러
}
반응형

'Stack > C++' 카테고리의 다른 글

C++ 업캐스팅, 오버라이딩, 가상함수  (0) 2021.11.08
C++ 클래스 상속  (0) 2021.11.04
C++ this 포인터  (0) 2021.10.24
C++ 복사 생성자 심화  (0) 2021.10.17
C++ 파일 입출력 ofstream, ifstream  (0) 2021.10.16

+ Recent posts