[Advanced C++] 63. Access specifiers, 상속 시 접근 지정자, 상속 관계에서의 함수 재정의, Hide inherited function, 상속 후 접근 지정자 변경, 가상 함수 상속과 접근 지정자, 참조&값 캐스팅 차이

dev.kelvin·2025년 6월 17일
1

Advanced C++

목록 보기
63/74
post-thumbnail

1. Access specifiers

접근 지정자

접근 지정자에는 public, private, protected가 있고 public의 멤버는 누구나 접근이 가능하고 private의 멤버는 같은 클래스의 멤버 함수나 friend에 의해서만 접근이 가능하다, protected는 상속관계인 자식에서는 접근이 가능하다

따라서 자식 클래스는 부모 클래스의 private:에 있는 멤버에 접근이 불가능하다

    class Base
    {
    public:
        int m_pw{};
    private:
        int m_id{};
    };

    class Derived : public Base
    {
    public:
        Derived()
        {
            m_pw = 100;
            m_id = 200; //compile error
        }
    };

protected:에 있는 멤버는 자식 클래스에서 접근이 가능하기 때문에 부모 클래스의 구현을 변경하여 자식 클래스의 업데이트가 필요할 때 직접 자식 클래스에서 해당 멤버 업데이트가 가능하다

private:은 해당 클래스 외부뿐만 아니라 자식 클래스에서도 멤버 데이터 접근이 불가능한 접근 지정자이기 때문에 클래스가 반드시 지켜야 할 데이터를 올바르게 유지할 때 사용한다, 다만 외부나 자식 클래스에서 사용해야 한다면 다른 인터페이스를 뚫어줘야 하기 때문에 비용이 발생할 수 있다

일반적으로는 클래스의 멤버는 private:에 만드는것을 권장한다, 다만 자식 클래스에서 해당 private멤버에 접근하기 위한 인터페이스 비용이 클때는 protected:에 만드는것도 권장된다

상속에서의 접근지정자

이전장에 정리했을때 상속 시 public 접근 지정자를 사용했는데 이때도 private:, protected:를 사용할 수 있다

	class Derived : public Base
    {
    	
    };
    
    class Derived : private Base
    {
    	
    };
    
    class Derived : protected Base
    {
    	
    };

이때 상속 접근 지정자를 따로 명시하지 않는다면 자동으로 private으로 상속한다

일반적으로 특별한 이유가 없지않는한 public 상속외에는 잘 사용되지 않는다

  • public 상속
    부모 클래스를 public으로 상속받게 되면 부모 클래스 멤버를 public은 public, protected는 protected, private은 private으로 유지하여 상속받게 된다
    class Base
    {
    public:
        int m_pw{};
    protected:
        int m_nick{};
    private:
        int m_id{};
    };

    class Derived : public Base
    {
    public:
        Derived()
        {
            m_pw = 100; //가능
            m_nick = 200; //가능
            m_id = 300; //m_id는 private이기 때문에 불가능
        }
    };
  • protected 상속
    부모 클래스를 protected로 상속받게 되면 부모 클래스 멤버를 public과 protected를 protected로, private은 private으로 상속받게 된다
    class Derived : protected Base
    {
    public:
        Derived()
        {
            m_pw = 100; //ok
            m_nick = 200; //ok
            m_id = 300; //X
        }
    };

    int main()
    {
        Derived d1{};
        d1.m_pw; //Base클래스에서는 public:이지만 Derived클래스는 Base를 protected:로 상속받았기 때문에 protected로 간주되어 접근이 불가능하다

        return 0;
    }
  • private 상속
    부모 클래스를 private으로 상속받게 되면 부모 클래스의 모든 멤버를 private으로 상속받게 된다
    class Derived : private Base
    {
    public:
        Derived()
        {
            m_pw = 100; //ok
            m_nick = 200; //ok
            m_id = 300; //X
        }
    };

    int main()
    {
        Derived d1{};
        d1.m_pw; //X
        d1.m_nick; //X

		//m_pw는 Base에서는 public:이지만 Derived클래스가 Base를 private으로 상속받았기 때문에 private:으로 취급되어 접근이 불가능하다
        
        return 0;
    }
    class Base
    {
    public:
        int m_pw{};
    protected:
        int m_nick{};
    private:
        int m_id{};
    };

    class Derived : private Base
    {
    public:
        Derived()
        {
            m_pw = 100;
            m_nick = 200;
            m_id = 300;
        }
    };

    class Derived2 : public Derived
    {
        Derived2()
        {
            m_pw = 0; //compile error, 부모 클래스인 Derived를 public으로 상속받아 멤버 데이터의 접근 지정자를 그대로 상속받지만 이미 Derived클래스가 Base를 private으로 상속받아 전부 private:인 상태기 때문에 m_pw에 접근이 불가능하다
        }
    };

자식 클래스에서의 기능 추가

상속의 가장 큰 이점은 코드 재사용이다, 이때 부모 클래스의 코드는 재사용하면서 자식 클래스만의 기능을 추가하려면 어떻게 해야할까?

    class Base
    {
    public:
        Base(int inValue) : baseValue{ inValue }
        {

        }

        void Foo() { std::cout << "Foo()" << '\n'; }

    protected:
        int baseValue{};
    };

    class Derived : public Base
    {
    public:
        Derived(int inValue) : Base(inValue)
        {

        }

        void Bar() { std::cout << "Bar()" << '\n'; }

    private:
        int derivedValue{};
    };

그냥 일반 멤버 함수 정의하듯 자식 클래스에서 함수를 선언하고 정의하면 해당 자식 클래스만의 함수 사용이 가능해진다

따라서 Derived클래스 객체는 Foo(), Bar() 둘 다 사용이 가능하다

만약 Base클래스의 baseValue에 접근해서 무언가를 하고 싶다면 어떤 방식으로 처리하는게 좋을까?

물론 Base클래스는 직접 만든 사용자 정의 클래스이기 때문에 수정을 해서 baseValue를 public:으로 변경한다던가, getter()를 뚫는다던가 하는 방법을 만들 수 있지만 사실 부모 클래스를 직접 건드릴 수 없는 경우도 많이 존재한다

예를들어 써드파티 라이브러리 코드를 이식해서 사용중이라고 가정했을때 해당 라이브러리 원본 코드를 수정하게 되면 추후에 있을 라이브러리 업데이트를 적용하기 굉장히 힘들 수 있으며 이를 수동으로 옮기거나 덮어씌워야 한다

혹은 STL같은 경우에는 코드 수정 자체가 불가능하기 때문에 위와 같은 상황에서 최선은 클래스를 상속받아 원하는 기능을 해당 자식 클래스에서 추가하는것이다

    class Base
    {
    public:
        Base(int inValue) : baseValue{ inValue }
        {

        }

    protected:
        int baseValue{};
    };

    class Derived : public Base
    {
    public:
        Derived(int inValue) : Base(inValue)
        {

        }

        int GetBaseValue() { return baseValue; }

    private:
        int derivedValue{};
    };

이렇게 하면 기반 클래스인 Base는 수정하지 않고 파생 클래스인 Derived만 수정하여 Base클래스의 멤버에 접근이 가능해진다
(위 코드는 단순 예제임)

상속된 함수 호출 및 override

자식 클래스는 부모 클래스의 public:, protected:에 있는 함수들을 상속받아 사용할 수 있다

만약 자식 클래스 객체에서 멤버 함수가 호출되었다면 컴파일러는 가장 먼저 자식 클래스 내부에 해당 이름을 가진 함수가 있는지 확인한다, 이때 함수가 존재한다면 그 이름을 가진 오버로드된 함수들은 전부 호출 고려 대상이 되고 확인 과정을 통해 일치한 함수를 호출한다

해당 이름을 가진 함수가 자식 클래스 내부에 없다면 부모 클래스를 차례대로 같은 방식으로 확인한다

    class Base
    {
    public:
        Base() { }

        void Foo() const { std::cout << "Base::Foo()\n"; }
    };

    class Derived : public Base
    {
    public:
        Derived() { }
    };

    int main()
    {
        Base base{};
        base.Foo(); //Base클래스에 Foo()가 있으므로 바로 Base::Foo()호출

        Derived derived{};
        derived.Foo(); // Derived클래스에 Foo()가 없으므로 Base::Foo()호출

        return 0;
    }

따라서 부모 클래스의 함수만으로 충분하다면 자식 클래스에서는 추가적으로 함수를 정의하지 않고 그대로 사용해주면 된다

그렇다면 부모 클래스의 함수를 자식 클래스에서 재정의 하고싶다면 어떻게 할까?

자식 클래스에서 함수를 재정의 하면 부모 클래스 버전의 함수와 다르게 동작시킬 수 있다

가장 간단한 방법은 동일한 함수 시그니처로 선언 후 정의만 다르게 해주면 된다

    class Base
    {
    public:
        Base() { }

        void Foo() const { std::cout << "Base::Foo()\n"; }
    };

    class Derived : public Base
    {
    public:
        Derived() { }

        void Foo() const { std::cout << "Derived::Foo()\n"; }
    };

함수를 재정의 할 때 자식 클래스의 함수는 부모 클래스의 함수의 접근 지정자를 상속받지 않는다, 이는 곧 부모 클래스에서의 private:멤버 함수를 자식 클래스에서는 public:으로 재정의 가능하다는 뜻이다 (역도 성립한다)

    class Base
    {
    public:
        Base() { }

    private:
        void Foo() const { std::cout << "Base::Foo()\n"; }
    };

    class Derived : public Base
    {
    public:
        Derived() { }

        void Foo() const { std::cout << "Derived::Foo()\n"; }
    };

이렇게 자식 클래스에서 부모 클래스의 함수를 재정의 하고난 다음에 만약 부모 클래스 버전의 함수를 사용하고 싶다면 범위지정연산자 ::를 사용하면 된다

    class Base
    {
    public:
        Base() { }

        void Foo() const { std::cout << "Base::Foo()\n"; }
    };

    class Derived : public Base
    {
    public:
        Derived() { }

        void Foo() const 
        { 
            Base::Foo(); //UE의 Super::()와 같음

            std::cout << "Derived::Foo()\n"; 
        }
    };

이러한 상속 함수 재정의는 다음과 같은 문제가 발생할 수 있다

	class Base
    {
    public:
        void Foo(int) { std::cout << "Base Foo(int)" << std::endl; }
        void Foo(double) { std::cout << "Base Foo(double)" << std::endl; }
    };

    class Derived : public Base
    {
    public:
        void Foo(double) { std::cout << "Derived Foo(double)" << std::endl; }
    };

    int main()
    {
        Derived derived{};
        derived.Foo(10);

        return 0;
    }

derived.Foo(10)은 인자로 int를 넘겼으니 Base::Foo(int)가 호출될 것이라고 예상할 수 있지만 실제로는 Derived::Foo(double)이 호출된다 (컴파일러에 의해 Derived에 Foo함수가 정의되어 있는걸 확인해서 바로 호출해버리기 때문)

하지만 인자만 봤을때는 Base::Foo(int)가 더 적합하다, 이럴때는 어떻게 할까?

단순하게 생각하면 Derived::Foo(int)버전을 하나 더 만들면 되지 않나?라는 생각을 할 수 있지만 너무 번거로운 방식이다, 이럴때는 using을 사용해서 부모 클래스 버전의 함수를 전부 자식 클래스 내부에서 보이도록 하는 방식을 사용한다

	class Base
    {
    public:
        void Foo(int) { std::cout << "Base Foo(int)" << std::endl; }
        void Foo(double) { std::cout << "Base Foo(double)" << std::endl; }
    };

    class Derived : public Base
    {
    public:
        using Base::Foo;
        void Foo(double) { std::cout << "Derived Foo(double)" << std::endl; }
    };
    
    int main()
    {
        Derived derived{};
        derived.Foo(10);

        return 0;
    }

이제 derived.Foo(10)은 Base::Foo(int)를 호출하게 된다

상속 시 friend함수 호출

friend함수는 클래스 내부에 선언한다고 해도 해당 클래스의 멤버 함수로 처리되지 않기 때문에 범위지정연산자 ::를 사용해서 호출이 불가능하다

이럴때는 일시적으로 부모 클래스로 캐스팅해서 사용하는 방법이 있다

    class Base
    {
    public:
        Base() { }

        friend std::ostream& operator<< (std::ostream& out, const Base& inBase)
        {
            out << "Base \n";
            return out;
        }
    };

    class Derived : public Base
    {
    public:
        Derived() { }

        friend std::ostream& operator<< (std::ostream& out, Derived& inDerived)
        {
            out << "Derived \n";
            out << static_cast<Base>(inDerived); //casting후 사용

            return out;
        }
    };

    int main()
    {
        Derived derived{};
        std::cout << derived;
        return 0;
    }

Hide inherited function

상속받은 멤버는 자식 클래스 내부에서 using을 이용해서 접근 지정자 제어가 가능하다

    class Base
    {
    public:
        int value{};

    protected:
        int GetValue() { return value; }
    };

    class Derived : public Base
    {
    public:
        using Base::GetValue; //using을 통해 상속받은 Base::GetValue를 public:접근지정자로 변경
    };

    int main()
    {
        Derived derived{};
        derived.GetValue(); //using으로 접근 지정자를 변경하지 않았다면 protected이기 때문에 외부에서 사용이 불가능했지만 이제 가능하다

        return 0;
    }

단 자식 클래스에서 접근 가능한 public:, protected: 함수만 접근 지정자를 변경할 수 있다

이렇게 using을 이용하여 부모 클래스의 멤버 함수 접근 지정자를 변경할 때 같은 이름으로 오버로딩된 함수들은 전부 다 변경된다 (개별적으로 변경할 방법 없음)

	class Base
    {
    public:
        int value{};

        int GetValue() { return value; }
        int GetValue(int a) { return a; }
        int GetValue(int a, int b) { return a + b };
    };

    class Derived : public Base
    {
    private:
        using Base::GetValue; //3가지 버전의 오버로딩 된 함수들이 전부 private:으로 변경됨
    };

    int main()
    {
        Derived derived{};
        derived.GetValue(); //X
        derived.GetValue(10); //X
        derived.GetValue(10, 20); //X

        return 0;
    }

using으로 접근지정자를 변경하는 방식으로 부모 클래스에 존재하는 기능을 private:에 숨겨서 자식 클래스 객체로는 외부에서 접근할 수 없도록 숨길 수 있다

    class Base
    {
    public:
        int value{};
    };

    class Derived : public Base
    {
    public:
        Derived(int inValue)
        {
            value = inValue; //private:으로 해도  클래스 내부에서는 접근이 가능하기에 사용 가능
        }

    private:
        using Base::value; //public:이었던 Base::value를 Derived클래스에서는 private으로 변경
    };

    int main()
    {
        Derived derived{100};

        return 0;
    }

하지만 이런 방식은 완벽한 캡슐화가 아니다

    int main()
    {
        Derived derived{100};

        Base base{ static_cast<Base>(derived) };
        base.value;

        return 0;
    }

이런 방식으로 casting하여 바로 사용이 가능하기 때문이다

사실 위 코드는 Base::value 자체를 private:에 선언하고 자식 클래스들은 전부 자동으로 private:에 들어가도록 하는게 맞는 방식이다

자식 클래스에서 함수 delete

자식 클래스에서 멤버함수를 delete로 처리하여 자식 객체를 통해서는 해당 함수를 호출할 수 없게 만들수 있다

	class Base
    {
    public:
        void Foo() { std::cout << "BaseFoo()" << '\n'; }
    };

    class Derived : public Base
    {
    public:
        void Foo() = delete;
    };

    int main()
    {
        Derived derived{};

        derived.Foo(); //Derived클래스에서는 Foo()가 delete되었기 때문에 사용 불가

        return 0;
    }

delete키워드는 해당 함수의 존재 자체를 삭제한다, 클래스 내부에서도 호출할 수 없으며 컴파일러의 함수 재정의 확인에서도 제외된다 (private:처리와 다름, private:은 해당 클래스 내부에서 호출이 가능함)

하지만 부모 클래스에서의 함수는 살아있기 때문에 다음과 같은 코드는 동작한다

    Derived derived{};

    derived.Base::Foo();

    static_cast<Base>(derived).Foo();

가상 함수 상속 후 접근 지정자 변경

만약 부모 클래스에 public: 가상 함수가 존재하고 자식 클래스에서 이 가상함수를 private:으로 변경했다고 가정해보자

이럴때도 static_cast<>를 통해 부모 클래스로 캐스팅하면 해당 가상 함수 호출이 가능해진다

    class Base
    {
    public:
        virtual void Foo() { std::cout << "BaseFoo()" << '\n'; }
    };

    class Derived : public Base
    {
    private:
        virtual void Foo() override
        {
            std::cout << "DerivedFoo()" << '\n';
        }
    };

    int main()
    {
        Derived derived{};

        static_cast<Base&>(derived).Foo(); //OK

        return 0;
    }

static_cast<>로 캐스팅을 하면 Base::Foo()가 호출되는 것이므로 허용된다, 하지만 런타임에는 가상 함수 원리에 따라 실제 객체인 Derived::Foo()가 호출된다

여기서 알 수 있는점은 접근 제어는 런타임에 강제되지 않는다는 점이다 (Derived::Foo()는 private이지만 가상 함수 원리에 의해 실제 객체 추적 후 호출됨)

이때 static_cast< Base >으로 값으로 캐스팅한다면 Base::Foo()가 호출된다, 왜냐하면 Derived클래스 객체에서 Base 부분만 떼어내서 완전히 새로운 Base타입의 임시객체를 생성하고 Foo를 호출하기 때문이다
(Slicing 발생)

static_cast<Base&>으로 참조로 캐스팅한다면 새로운 객체가 생성되지 않고 그냥 기존의 Derived클래스 객체를 Base로 부르는 느낌으로 캐스팅 되기 때문에 실제 객체인 Derived::Foo()가 호출되는 것이다

    Derived derived{};

    Base& b1{ static_cast<Base&>(derived) }; 

    Derived* derPtr{ &derived };
    Base* basePtr{ &b1 };
    
    //이 둘의 주소가 똑같음 (참조로 캐스팅 해서), 값으로 캐스팅 한다면 주소가 다르다 (새로운 임시 객체가 생성되기 때문에)

가상함수와 함께 사용 시 주의해야 한다, 또한 값 타입 캐스팅은 불필요한 객체 복사가 발생하기 때문에 성능상 손해다, 따라서 참조나 포인터로 캐스팅하는게 권장되는 방식이다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글