카테고리 없음

[3주차] C++ 객체지향 프로그래밍

2026. 4. 12. 23:57

VEDA 주간 학습 정리

[3주차] C++ 객체지향 프로그래밍

이번 주 학습을 한 문장으로 정리하면: 객체지향 프로그래밍의 핵심 개념, C++ 클래스 구조, STL 컨테이너와 iterator 이해

작성일: 2026.04.12

주차 이동

◀ 저번주차: [2주차] C 언어 기본 문법

◀ 다음주차: [4주차] Qt 프레임워크

이번 주 학습 목표

  • 객체지향 프로그래밍 이해하기
  • C++ 클래스 이해하기
  • 컨테이너 이해하기

1. 객체지향 프로그래밍 이해하기

추상화 (Abstraction)

  • 정의: 복잡한 현실의 사물에서 필요한 속성만 골라 단순화하여 표현하는 과정
  • 핵심: 모든 정보를 다 담는 것이 아니라, 목적에 맞는 정보만 남기는 것
  • 예시: 자동차를 레이싱 게임에서 볼 때는 속도와 연료량이 중요하지만, 중고차 매매 사이트에서는 연식과 사고 이력이 더 중요하다

정리

추상화는 객체지향 설계의 출발점이다. 어떤 정보를 남기고 어떤 세부사항을 숨길지 결정해야 이후의 클래스 설계와 상속 구조도 자연스럽게 이어진다.

관계 (Relationships)

  • Has-a 관계: 한 객체가 다른 객체를 자신의 일부로 가지는 관계이다. 보통 멤버 변수로 다른 객체를 포함하여 표현한다.
  • 예시: 컴퓨터는 CPU와 RAM을 가지고 있다. 즉, Computer has a CPU라고 볼 수 있다.
  • Is-a 관계: 어떤 객체가 다른 객체의 한 종류일 때 성립하는 관계이다. 상속 구조에서 가장 자주 등장한다.
  • 예시: 스마트폰은 전화기의 일종이다. 따라서 SmartPhone is a Phone이라고 표현할 수 있다.
  • Use-a 관계: 한 객체가 특정 기능을 수행하는 동안 다른 객체를 일시적으로 사용하는 관계이다.
  • 예시: 요리사가 요리할 때 프라이팬을 사용한다. 요리사는 프라이팬을 영구히 소유하는 것이 아니라 필요할 때만 사용한다.

객체지향 프로그래밍 3대 핵심 특성

캡슐화 (Encapsulation)

  • 정의: 데이터와 함수를 하나로 묶고, 외부에서 내부 구현에 직접 접근하지 못하도록 제한하는 개념
  • 목적: 내부 상태를 보호하고, 필요한 인터페이스만 외부에 노출하여 안전하게 사용하게 만드는 것
  • 예시: 리모컨은 내부 회로를 몰라도 버튼만 누르면 TV를 조작할 수 있다
+---------------------------+
|        RemoteControl      |
|---------------------------|
| - circuitState   (private)|
| - batteryLevel   (private)|
|---------------------------|
| + powerOn()      (public) |
| + powerOff()     (public) |
| + volumeUp()     (public) |
| + volumeDown()   (public) |
+---------------------------+

tvRemote.powerOn();
tvRemote.volumeUp();

상속성 (Inheritance)

  • 정의: 상위 클래스의 특성을 하위 클래스가 물려받아 재사용하는 것
  • 장점: 공통 기능을 중복해서 작성하지 않아도 되고, 계층 구조를 통해 코드의 의미가 더 분명해진다
  • 예시: 전기차는 자동차의 주행 기능을 물려받고, 배터리 충전 기능을 새로 추가할 수 있다
            +------------------------+
            |        Car             |
            |------------------------|
            | - speed                |
            | - fuel                 |
            |------------------------|
            | + drive()              |
            | + stop()               |
            +------------------------+
                        ^
                        |
        +-----------------------------+
        |        ElectricCar          |
        |-----------------------------|
        | - batteryLevel              |
        |-----------------------------|
        | + chargeBattery()           |
        | + drive()                   |
        +-----------------------------+

다형성 (Polymorphism)

  • 정의: 같은 메시지나 함수 호출이라도 실제 객체 타입에 따라 다르게 동작하는 성질
  • 방식: 주로 상속과 가상 함수, 오버라이딩을 통해 구현된다
  • 예시: AnimalmakeSound()Dog는 멍멍, Cat은 야옹으로 다르게 구현할 수 있다
          +------------------------+
          |        Animal          |
          |------------------------|
          | + makeSound()          |
          +------------------------+
               ^              ^
               |              |
       inherits|              |inherits
               |              |
   +----------------+   +----------------+
   |      Dog       |   |      Cat       |
   |----------------|   |----------------|
   | + makeSound()  |   | + makeSound()  |
   |   -> "멍멍"     |   |   -> "야옹"     |
   +----------------+   +----------------+

Animal* a1 = new Dog();
Animal* a2 = new Cat();

a1->makeSound();   // "멍멍"
a2->makeSound();   // "야옹"

특성 간의 상관관계

  • 추상화 → 상속: 공통 개념을 먼저 추출해야 부모 클래스를 설계할 수 있다
  • 캡슐화 → 추상화: 내부 구현을 숨겨야 추상화된 인터페이스가 더 명확해진다
  • 상속 → 다형성: 다형성은 상속 관계에서 같은 이름의 함수를 서로 다르게 구현하면서 실현된다
  • 결과: 코드 재사용성과 유지보수성이 높아진다

추가 개념: SOLID 원칙

원칙 의미
S 단일 책임 원칙: 한 클래스는 하나의 책임만 가져야 한다
O 개방-폐쇄 원칙: 확장에는 열려 있고 변경에는 닫혀 있어야 한다
L 리스코프 치환 원칙: 자식 클래스는 부모 클래스를 대체할 수 있어야 한다
I 인터페이스 분리 원칙: 사용하지 않는 기능에 의존하지 않도록 인터페이스를 잘게 나눈다
D 의존역전 원칙: 구체 구현보다 추상화에 의존해야 한다

2. C++ 클래스

클래스는 특정 객체를 만들기 위한 설계도로, 속성(데이터)행동(함수)을 함께 정의한다. 객체지향 프로그래밍에서는 클래스를 기반으로 실제 객체를 생성하고, 이를 통해 현실의 개념을 코드로 표현한다.

class Parent {
public:
    // 생성자: 객체가 만들어질 때 부모 클래스의 데이터 x, y를 초기화
    Parent(int a, int b)
        : x(a), y(b) {
    }

    // virtual 소멸자:
    // 부모 타입 포인터로 자식 객체를 지울 때 자식 소멸자까지 안전하게 호출되게 함
    virtual ~Parent() {
    }

    // 다형성:
    // 자식 클래스가 calc()를 자기 방식으로 다시 정의할 수 있도록 virtual 사용
    virtual int calc() const {
        return x + y;
    }

protected:
    // 캡슐화:
    // 외부에서는 직접 접근하지 못하게 숨기고, 상속받은 자식만 사용할 수 있게 protected 사용
    int x;
    int y;
};

// 상속성:
// MyClass는 Parent를 상속받아 부모의 공통 데이터와 기능을 물려받음
class MyClass : public Parent {
public:
    // Parent(a, b): 부모 클래스 부분 먼저 초기화
    // size(size): 자식 클래스의 멤버 변수 초기화
    MyClass(int a, int b, int size)
        : Parent(a, b),
          size(size) {
        // size 크기만큼 정수 배열을 동적으로 생성
        arr = new int[size];

        // 배열 안에 0, 1, 2, ... 값을 저장
        for (int i = 0; i < size; ++i) {
            arr[i] = i;
        }
    }

    // 생성자에서 new[]로 만든 메모리를 소멸자에서 해제
    ~MyClass() override {
        delete[] arr;
    }

    // 다형성:
    // 부모의 calc()를 자식 버전으로 재정의(오버라이딩)
    // 같은 calc() 호출이어도 실제 객체가 MyClass이면 이 함수가 실행됨
    int calc() const override {
        int sum = x + y;
        for (int i = 0; i < size; ++i) {
            sum += arr[i];
        }
        return sum;
    }

private:
    // 캡슐화:
    // 객체 내부 구현 세부사항이므로 외부에서 직접 건드리지 못하게 private으로 숨김
    int size;
    int* arr;
};

접근 지정자 (Access Specifiers)

접근 지정자 클래스 내부 자식 클래스 외부 객체 비고
public O O O 누구나 접근 가능
protected O O X 상속 관계까지만 허용
private O X X 해당 클래스 내부에서만 접근 가능

객체 생성 시 메모리 관점

  • 코드 영역 (Code Segment): 멤버 함수와 생성자, 소멸자 같은 실행 코드가 저장되며 모든 객체가 이를 공유한다
  • 스택 영역 (Stack Segment): 지역 객체가 생성될 때 멤버 변수 크기만큼 스택 공간이 잡히고, 함수 호출 정보도 함께 저장된다
  • 힙 영역 (Heap Segment): new로 동적 할당한 메모리는 힙에 생성되며, 직접 delete 또는 delete[]로 해제해야 한다

주의

위 예제의 MyClass는 동적 배열을 직접 관리하므로 복사 생성자와 대입 연산자까지 고려하지 않으면 얕은 복사 문제가 발생할 수 있다. 실무에서는 보통 std::vector나 스마트 포인터를 활용해 이런 위험을 줄인다.

this 포인터

비정적 멤버 함수가 호출될 때는 호출된 객체 자신을 가리키는 this 포인터가 함께 전달된다. 매개변수 이름과 멤버 변수 이름이 같을 때 특히 자주 사용된다.

void setRadius(double radius) {
    // 왼쪽 radius는 멤버 변수, 오른쪽 radius는 매개변수
    // this->를 붙이면 "현재 객체의 멤버 변수"라는 뜻이 분명해진다
    this->radius = radius;
}

// 의사 코드
// setRadius(ClassName* const this, double radius);

virtual 키워드와 오버라이딩

  • virtual은 자식 클래스가 함수를 재정의할 수 있음을 나타낸다
  • 부모 타입의 포인터나 참조로 자식 객체를 다룰 때, 실행 시점에 실제 객체 타입을 보고 알맞은 함수를 호출한다
  • 이것이 바로 다형성을 코드 수준에서 구현하는 핵심 장치다

3. 중첩 클래스

  • 클래스 내부에 또 다른 클래스를 선언하면 이를 중첩 클래스(Nested Class)라고 한다
  • 바깥에서 사용할 때는 Outer::Inner 형태로 접근한다
  • 특정 클래스와 강하게 연결된 타입을 정의할 때 유용하며, 대표적으로 iterator 구현에서 자주 사용된다
#include <iostream>

template <typename T>
class Vector {
public:
    // 중첩 클래스:
    // Vector 안에서만 쓰는 iterator 타입을 내부에 정의
    class iterator {
    public:
        // 반복자가 현재 가리킬 위치를 포인터로 저장
        iterator(T* ptr = nullptr) : cur(ptr) {}

        // *it 형태로 현재 원소에 접근
        T& operator*() { return *cur; }

        // ++it 형태로 다음 원소로 이동
        iterator& operator++() {
            ++cur;
            return *this;
        }

        // 반복문에서 현재 위치와 끝 위치를 비교할 때 사용
        bool operator!=(const iterator& other) const {
            return cur != other.cur;
        }

    private:
        T* cur; // 현재 원소의 주소
    };

    Vector(int n)
        : size_(n) {
        // 원소 n개를 저장할 배열을 힙에 생성
        data_ = new T[size_];

        // 모든 원소를 기본값으로 초기화
        for (int i = 0; i < size_; ++i) {
            data_[i] = T();
        }
    }

    ~Vector() {
        // 생성자에서 할당한 메모리 반납
        delete[] data_;
    }

    // 첫 번째 원소를 가리키는 반복자
    iterator begin() { return iterator(data_); }

    // 마지막 원소의 다음 위치를 가리키는 반복자
    // 반복이 끝났는지 확인하는 기준점 역할을 함
    iterator end() { return iterator(data_ + size_); }

    // 배열처럼 v[idx]로 접근할 수 있게 해주는 연산자
    T& operator[](int idx) { return data_[idx]; }

private:
    int size_; // 원소 개수
    T* data_;  // 실제 데이터가 저장된 배열
};

4. 컨테이너

컨테이너 (Container)

  • 정의: 같은 타입의 여러 객체를 저장하는 데이터 구조
  • 역할: 데이터를 저장하고 관리하며, 원소에 접근하기 위한 멤버 함수를 제공한다
  • 특징: 탐색, 정렬, 복사 등은 STL 알고리즘과 함께 사용하면 더 강력해진다

컨테이너의 분류

순차 컨테이너 (Sequence Containers)

  • array: 크기가 고정된 정적 배열
  • vector: 끝에서의 삽입과 삭제가 빠른 가변 길이 배열
  • deque: 앞과 뒤 모두에서 삽입과 삭제가 빠른 구조
  • list: 노드 기반의 이중 연결 리스트

연관 컨테이너 (Associative Containers)

  • set, multiset: 정렬된 집합 형태로 데이터를 관리한다
  • map, multimap: 키와 값의 쌍으로 저장되며 키 기준으로 자동 정렬된다

컨테이너 어댑터 (Container Adapters)

  • stack: 후입선출(LIFO)
  • queue: 선입선출(FIFO)
  • priority_queue: 우선순위가 높은 데이터부터 먼저 꺼낸다
컨테이너 특징 적합한 상황
std::array 크기가 고정되어 있고 내부 메모리가 연속적이다 원소 개수가 컴파일 시점에 정해져 있을 때
std::vector 동적 크기 조절이 가능하고 접근 속도가 빠르다 가장 일반적인 배열 대체 용도
std::deque 앞뒤 삽입과 삭제가 모두 효율적이다 양쪽 끝에서 데이터 처리가 자주 일어날 때
std::map 키와 값을 쌍으로 저장하고 키 기준 정렬을 유지한다 이름, 번호처럼 키로 값을 빠르게 찾고 싶을 때
std::list 중간 삽입과 삭제가 효율적이지만 임의 접근은 느리다 원소 이동과 삽입/삭제가 자주 일어날 때

Iterator란 무엇인가?

Iterator는 컨테이너의 원소를 가리키고 이동하기 위한 표준화된 객체이다. 배열에서는 포인터가 비슷한 역할을 하지만, list처럼 메모리가 연속적이지 않은 구조도 있기 때문에 STL은 포인터 대신 iterator라는 공통 인터페이스를 제공한다.

  • *: 현재 가리키는 원소의 값을 읽거나 참조한다
  • ++: 다음 원소로 이동한다
  • ->: 현재 원소가 객체일 때 그 멤버에 접근한다

관련 용어 정리

타입 이름 역할 설명
iterator 읽기/쓰기 탐색자 원소를 읽을 수도 있고 값을 수정할 수도 있다
const_iterator 읽기 전용 탐색자 원소를 읽기만 가능하고 수정할 수는 없다
difference_type 차이 타입 두 iterator 사이의 거리나 차이를 표현하는 정수형이다
reverse_iterator 역방향 탐색자 컨테이너의 뒤에서 앞으로 이동하며 순회할 때 사용한다. 보통 rbegin()rend()를 함께 사용한다

요약

vector<int>::iterator는 단순한 포인터가 아니라, 컨테이너 내부 원소에 접근하고 이동하기 위한 기능을 갖춘 클래스 타입이다. 그래서 STL 알고리즘은 컨테이너 종류가 달라도 iterator만 맞으면 비슷한 방식으로 동작할 수 있다.

예제

주차 예제 실행

객체지향 개념 문제!

문제 및 해결 과정

문제 1 (가상의 문제)

부모 클래스 포인터로 자식 객체를 다루는데, 기대했던 자식 클래스 버전의 함수가 실행되지 않고 부모 클래스 함수만 호출되는 문제가 있었다.

원인 분석

부모 클래스의 함수에 virtual 키워드가 없으면 컴파일 시점에 호출할 함수가 결정된다. 그래서 부모 타입 포인터가 자식 객체를 가리키더라도, 실행할 때 실제 객체 타입을 확인하지 않고 부모 버전의 함수가 호출된다.

해결 방법

부모 클래스의 함수를 virtual로 선언하고, 자식 클래스에서는 override를 사용해 재정의한다. 그러면 같은 함수 호출이라도 실제 객체 타입에 맞는 함수가 실행되어 다형성이 올바르게 동작한다.

문제 2 (가상의 문제)

대규모 데이터를 저장하는 구조를 vector로 만들었는데, 앞쪽이나 중간에서 삽입과 삭제가 자주 일어나면서 전체 동작이 예상보다 느려지는 문제가 있었다.

원인 분석

vector는 내부 메모리를 연속적으로 유지하는 컨테이너라서, 앞이나 중간에 원소를 넣거나 지우면 뒤쪽 원소들을 한꺼번에 이동시켜야 한다. 데이터가 많아질수록 이 이동 비용이 커져 성능 저하가 눈에 띄게 발생한다.

해결 방법

삽입과 삭제가 양쪽 끝에서 자주 일어나는 상황이라면 deque를 사용하는 것이 더 적합하다. 컨테이너는 저장 방식이 서로 다르기 때문에, 데이터 크기뿐 아니라 어떤 연산이 자주 발생하는지도 함께 고려해 선택해야 한다.