C++ 연산자 오버로딩, 상속

윤준혁·2025년 3월 1일

연산자 오버로딩(Operator Overloading)

연산자 오버로딩이란?

  • C++에서 기존연산자(+, -, *, / 등)의 동작을 사용자가 정의한 데이터 타입(클래스 또는 구조체)에서도 사용할 수 있도록 재정의하는 기능을 의미
  • 기본적으로 연산자는 내장 타입(int, float, double 등)에서만 동작하지만, 연산자 오버로딩을 통해 사용자 정의 타입에서도 연산자 기능을 활용할 수 있다

연산자 오버로딩의 필요성

  • 객체 간 연산을 직관적으로 표현 가능
  • 코드의 가독성을 높인다
  • 복잡한 연산을 간결하게 처리
class ClassName {
public:
    ReturnType operatorOp(const ClassName& other) {
        // 연산자 동작 구현
    }
};

오버로딩이 가능한 연산자

  • + : 덧셈 연산자
  • - : 뺄셈 연산자
  • * : 곱셈 연산자
  • / : 나눗셈 연산자
  • % : 나머지 연산자
  • +=, -=, *=, /=, %= : 복합 대입 연산자
  • ++, -- : 증가 및 감소 연산자
  • ==, !=, <, >, <=, >= : 비교 연산자
  • &&, ! : 논리 연산자
  • &, ^, ~, <<, >> : 비트 연산자
  • [] : 배열 인덱스 연산자
  • () : 함수 호출 연산자
  • -> : 멤버 접근 연산자

오버로딩이 불가능한 연산자

  • . : 멤버 접근 연산자
  • :: : 범위 지정 연산자 (Scope resolution)
  • sizeof : 크기 연산자
  • typeid : 런타임 타입 연산자
  • .* :멤버 포인터 연산자
  • new, delete : 메모리 할당/해제 연산자 (단, new[], delete[]는 오버로딩 가능)

프렌드 연산자(Friend Operator Overloading)

  • 일반적으로 연산자 오버로딩은 클래스의 멤버 함수로 구현할 수 있지만, 두 개의 다른 객체를 비교하거나 좌항이 사용자 정의 클래스가 아닐 때는 프렌드 함수를 사용해야 한다
#include <iostream>
class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // Friend function으로 연산자 오버로딩
    friend Complex operator+(const Complex& c1, const Complex& c2);
    void display() { std::cout << real << " + " << imag << "i\n"; }
};

Complex operator+(const Complex& c1, const Complex& c2) {
    return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

int main() {
    Complex c1(3, 4), c2(1, 2);
    Complex c3 = c1 + c2;
    c3.display();  // 출력: 4 + 6i
    return 0;
}

필요성

  • 두 개의 객체를 비교하는 연산자를 구현할 때(==, !=, <, > 등)
  • 클래스 외부에서 오버로딩된 연산자로 접근해야 할 때

삽입 연산자(Insertion Operator Overloading)

  • C++에서는 << 연산자를 활용하여 객체를 std::cout을 통해 출력할 수 있다
  • 하지만 기본적으로 std::cout는 사용자 정의 클래스를 출력하는 방법을 모른다
  • 이를 해결하기 위해 << 연산자를 오버로딩한다
#include <iostream>
class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // << 연산자 오버로딩
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};

std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;
}

int main() {
    Complex c1(3, 4);
    std::cout << "Complex number: " << c1 << std::endl; // Complex number: 3 + 4i
    return 0;
}

필요성

  • 사용자 정의 객체를 쉽게 출력할 수 있다
  • 가독성이 향상된다
  • 디버깅 시 객체의 상태를 출력하기 용이하다

상속(Inheritance)

상속이란?

  • 기존 클래스를 확장하여 새로운 클래스를 생성하는 객체지향 프로그래밍의 핵심 개념 중 하나
  • 코드의 재사용성을 높이고 유지보수를 용이하게 할 수 있다
  • 기존 클래스(부모 클래스 또는 기본 클래스)의 속성과 동작을 새로운 클래스(자식 클래스 또는 파생 클래스)에서 물려받아 사용할 수 있으며, 필요에 따라 일부 기능을 수정하거나 확장할 수 있다

상속의 장점

  • 코드 재사용성 증가 : 동일한 코드 중복을 줄이고 유지보수를 쉽게 한다
  • 구조화된 프로그램 : 계층적 구조를 통해 클래스를 체계적으로 설계 가능
  • 확장성 제공 : 기존 클래스를 기반으로 기능을 확장할 수 있다

상속의 생성자

  • 상속을 사용할 때, 부모 클래스의 생성자가 먼저 실행되고, 그 후 자식 클래스의 생성자가 실행
  • 부모 클래스의 속성이 먼저 초기화되어야 자식 클래스에서 이를 사용할 수 있기 때문
#include <iostream>
using namespace std;

class Parent {
public:
    Parent() { cout << "부모 생성자 실행\n"; }
};

class Child : public Parent {
public:
    Child() { cout << "자식 생성자 실행\n"; }
};

int main() {
    Child obj;
    return 0;
}

출력 예)
부모 생성자 실행
자식 생성자 실행

isA vs hasA

  • 객체지향 프로그래밍에서는 상속(Inheritance)과 구성(Composition)을 구별하는 것이 중요하다
  • 일반적으로 isA관계는 상속을 사용하고, hasA관계는 멤버 변수를 활용하여 구성한다

isA 관계(상속)

  • 'A는 B이다'라는 관계를 의미
  • class Car : public Vehicle -> 자동차는 차량이다
  • class Dog : public Animal -> 개는 동물이다

hasA 관계(구성)

  • 'A는 B를 포함한다'라는 관계를 의미
  • class Car { Engine engine; } -> 자동차는 엔진을 포함한다
  • class House { Room room; } -> 집은 방을 포함한다

접근 제한자의 효과

  • C++에서 상속을 할 때, 부모 클래스의 멤버 변수와 함수의 접근 범위는 접근 제한자에 의해 결정
    • public : 모든 곳에서 접근 가능(상속 가능)
    • protected : 상속받은 클래스에서 접근 가능(상속 가능)
    • private : 외부 및 자식 클래스에서 접근 불가능(상속 불가능)
class Parent {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class Child : public Parent {
public:
    void access() {
        publicVar = 10;   // 접근 가능
        protectedVar = 20; // 접근 가능
        // privateVar = 30; // 오류 발생: private 멤버는 접근 불가
    }
};

상속 타입

  • 상속의 유형은 여러가지가 있으며, 대표적인 4가지 유형이 있다

단일 상속(Simple/Single)

  • 하나의 부모 클래스로부터 상속

계층적 상속(Hierarchical)

  • 하나의 부모 클래스를 여러 자식 클래스가 상속

다단계 상속(Multilevel)

  • 부모 -> 자식 -> 손자 관계로 다중 상속

다중 삭속(Multiple)

  • 여러 개의 부모 클래스를 한 클래스에서 상속

상속 방법

  • 상속 방법에 따라 부모 클래스의 멤버 접근 가능 여부가 달라진다

일반화와 전문화(Generalization and Specialization)

  • 상속을 설계할 때 일반화와 전문화 개념을 이해하는 것이 중요하다

일반화(상향식 접근법)

  • 여러 개의 하위 클래스로부터 공통적인 특징을 추출하여 상위 클래스를 만드는 방식
  • 예제 : Car, Bike, Bus 클래스에서 공통 속성을 추출하여 Vehicle 클래스를 생성

전문화(하향식 접근법)

  • 기존 상위 클래스를 기반으로 세분화된 하위 클래스를 만드는 방식
  • 예제 : Employee 클래스를 기반으로 Manager, Engineer 등의 하위 클래스를 생성

Base Class Pointer Derived Class Object

Base Class Pointer Derived Class Object란?

  • 부모 클래스의 포인터를 사용하여 자식 클래스 객체를 가리키는 것
  • 다형성과 동적 바인딩을 활용할 수 있다
  • 이는 C++의 업캐스팅 개념을 활용한 것이다
#include <iostream>
using namespace std;

class Parent {
public:
    void show() { cout << "부모 클래스 함수\n"; }
};

class Child : public Parent {
public:
    void show() { cout << "자식 클래스 함수\n"; }
};

int main() {
    Child obj;
    Parent* ptr = &obj; // 부모 클래스 포인터로 자식 객체를 가리킴

    ptr->show(); // 부모 클래스 함수 호출 (정적 바인딩)
    return 0;
}

출력 예)
부모 클래스 함수

  • Parent* ptr = &obj; 를 통해 부모 클래스 포인터가 자식 클래스 객체를 가리킨다
  • 그러나 ptr -> show(); 호출 시 부모 클래스의 함수가 실행된다
  • 이는 정적 바인딩 때문

가상 함수를 이용한 동적 바인딩

  • 부모 클래스의 함수를 virtual로 선언하면, 자식 클래스의 함수가 실행된다
  • 이를 동적 바인딩(Dynamic Binding) 또는 런타임 다형성(Runtime Polymorphism)이라고 한다
#include <iostream>
using namespace std;

class Parent {
public:
    virtual void show() { cout << "부모 클래스 함수\n"; } // 가상 함수
};

class Child : public Parent {
public:
    void show() override { cout << "자식 클래스 함수\n"; } // 오버라이딩
};

int main() {
    Child obj;
    Parent* ptr = &obj; // 부모 클래스 포인터가 자식 객체를 가리킴

    ptr->show(); // 자식 클래스 함수 호출 (동적 바인딩)
    return 0;
}

출력 예)
자식 클래스 함수

  • virtual void show() 로 선언하면, 객체가 가리키는 실제 클래스(자식 클래스)의 함수가 실행된다
  • 이를 통해 다형성을 구현할 수 있다

업캐스팅과 다운 캐스팅

업캐스팅

  • 부모 클래스 포인터로 자식 클래스 객체를 가리키는 것
  • 안전한 방식이며, 동적 바인딩을 활용 가능
Parent* ptr = new Child(); // Upcasting (정상 작동)

다운캐스팅

  • 자식 클래스 포인터가 부모 클래스 포인터를 다시 자식으로 변환하는 것
  • static_cast 또는 dynamic_cast를 사용해야 하며, 잘못된 경우 런타임 오류가 발생할 수 있다
Child* cptr = static_cast<Child*>(ptr); // Downcasting

순수 가상 함수와 추상 클래스

  • 추상 클래스(Abstract Class)란 한 개 이상의 순수 가상 함수(Pure Virtual Function)를 포함하는 클래스이다
  • 추상 클래스를 상속받은 클래스는 반드시 가상함수를 구현해야 한다
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() = 0; // 순수 가상 함수
};

class Dog : public Animal {
public:
    void sound() override { cout << "멍멍!\n"; }
};

int main() {
    Animal* ptr = new Dog();
    ptr->sound(); // 멍멍!

    delete ptr;
    return 0;
}
  • virtual void sound() = 0; 선언으로 인해 Animal 클래스는 추상 클래스가 된다
  • Dog 클래스는 sound() 함수를 반드시 구현해야 한다

0개의 댓글