[VEDA] 10일차

나우히즈·2025년 3월 31일

VEDA

목록 보기
9/16

7장. 객체지향 프로그래밍

✅ 1. 추상화 (Abstraction)

🔹 정의

복잡한 내부 구현은 감추고, 중요한 정보나 기능만 외부에 제공하는 것

🔹 예시 (C++ 코드)

class RemoteControl {
public:
    void turnOnTV();    // 외부에 보여주는 동작 (인터페이스)
    void turnOffTV();
};
  • 사용자는 turnOnTV()가 어떻게 작동하는지 몰라도 사용할 수 있음
  • 내부의 복잡한 코드(회로 제어 등)는 숨기고, 기능만 제공

✅ 2. 캡슐화 (Encapsulation)

🔹 정의

  • 데이터와 그 데이터를 처리하는 함수를 하나의 객체 안에 묶고, 외부에서는 직접 접근하지 못하게 막는 것

🔹 예시 (C++ 코드)

class BankAccount {
private:
    int balance;  // 외부에서 직접 접근 불가

public:
    void deposit(int amount) {
        if (amount > 0) balance += amount;
    }

    int getBalance() const {
        return balance;
    }
};
•	balance는 외부에서 직접 접근 불가
•	대신 공식 함수(deposit, getBalance)를 통해서만 접근 가능
•	→ 데이터 무결성 유지, 오류 방지

✅ 생성자와 소멸자

✅ 1. 생성자(Constructor)

🔹 정의

객체가 생성될 때 자동으로 호출되는 특수한 함수. 객체의 초기화를 담당.

class MyClass {
public:
    MyClass();  // 생성자
};
  • 반환형 없음 (void도 X)
  • 클래스 이름과 이름이 같음
  • 객체가 만들어질 때 자동 호출

🔹 생성자의 역할

  • 멤버 변수 초기화
  • 동적 메모리 할당 (필요한 경우)
  • 객체 상태를 설정

🔹 생성자의 종류

  1. 기본 생성자
    인자 없음 (또는 모두 기본값). MyClass();

  2. 매개변수 생성자
    인자를 받아 초기화. MyClass(int x);

  3. 복사 생성자
    다른 객체로부터 복사 MyClass(const MyClass& other);

  4. 이동 생성자 (C++11~)
    자원을 이동 MyClass(MyClass&& other);

  5. 위임 생성자 (C++11~)
    다른 생성자 호출 MyClass() : MyClass(0) {}

위임 생성자와 약간 성격은 다르지만 부모 생성자를 호출하여 값을 초기화하는 경우도 존재함.

✅ 2. 소멸자(Destructor)

🔹 정의

객체가 수명 종료 시 자동으로 호출되는 특수한 함수.
객체가 차지한 리소스를 해제하는 역할

class MyClass {
public:
    ~MyClass();  // 소멸자
};
  • 클래스 이름 앞에 ~ 붙임
  • 반환형 없음, 인자도 없음
  • 객체가 스코프에서 벗어나거나 delete로 제거될 때 호출됨

🔹 소멸자의 역할

  • 동적 할당된 메모리 해제 (delete, delete[])
  • 파일, 네트워크, 리소스 닫기
  • 로그 출력, 정리 등 후처리

생성자는 “객체가 태어날 때 자동 호출 → 초기화 담당”
소멸자는 “객체가 죽을 때 자동 호출 → 정리 및 리소스 해제”

✔️ 생성자는 여러 개 정의 가능(오버로딩),
✔️ 소멸자는 반드시 하나만 존재

✅ 초기화 리스트란?

생성자의 본문이 실행되기 전에 멤버 변수들을 초기화하는 특별한 문법

class MyClass {
private:
    int x;
public:
    MyClass(int value) : x(value) {}  // ← 이 부분이 초기화 리스트!
};

위 코드에서 : x(value) 부분이 바로 초기화 리스트입니다.

✅ 초기화 리스트를 쓰는 이유

  1. 초기화는 대입보다 빠르고 효율적
  • 초기화 리스트는 직접 초기화
  • 생성자 본문에서는 기본 생성 후 대입 → 비효율
  1. const 멤버, 참조형 멤버 초기화는 반드시 초기화 리스트로만 가능
class A {
    const int x;
    int& ref;
public:
    A(int v) : x(v), ref(r) {}    // ✅ 가능
    // A(int v) { x = v; ref = r} // ❌ 컴파일 에러
};
  1. 기본 생성자가 없는 멤버 객체의 초기화
  • 멤버 클래스가 기본 생성자를 제공하지 않으면 초기화 리스트 필요

초기화 리스트는 생성자 본문이 실행되기 전에 실행됩니다.

✅ 예시 1: 일반 변수 초기화

class Point {
private:
    int x, y;
public:
    Point(int x_, int y_) : x(x_), y(y_) {}
};
  • x = x, y = y 라고 생성자 본문에서 대입해도 되지만,
  • 초기화 리스트를 사용하면 더 빠르고 정확한 초기화 가능

✅ 예시 2: const 멤버 변수 초기화

class Circle {
private:
    const double pi;
public:
    Circle() : pi(3.14159) {}  // const는 무조건 초기화 리스트로!
};

✅ 예시 3: 참조형 멤버 변수 초기화

class Wrapper {
private:
    int& ref;
public:
    Wrapper(int& r) : ref(r) {}
};
  • 참조는 대입이 아닌 반드시 초기화가 필요하므로 초기화 리스트 필수

✅ 예시 4: 클래스형 멤버의 생성자 호출

class Engine {
public:
    Engine(int power) {
        std::cout << "Engine(power=" << power << ") 생성됨\n";
    }
};

class Car {
private:
    Engine engine;
public:
    Car() : engine(150) {  // Engine 생성자 호출
        std::cout << "Car 생성됨\n";
    }
};

✅ 멤버 초기화 순서 주의!

❗️ 초기화 리스트에서 적어준 순서대로가 아닌,
→ 클래스 내 멤버 변수 정의 순서대로 초기화됨!

class Weird {
private:
    int x;
    int y;
public:
    Weird(int a, int b) : y(b), x(a) {}  // x가 먼저 초기화됨 (정의 순서!)
};
따라서 정의 순서와 초기화 리스트 순서를 일치시키는 것이 좋습니다 (버그 방지)

💡 언제 쓰나?

  • 항상 쓰는 것이 가장 좋습니다 (성능 + 안정성)
  • 특히 아래 경우엔 반드시 초기화 리스트 사용해야 함:
  • const 멤버
  • 참조(&) 멤버
  • 멤버 객체가 기본 생성자 없음

클래스 내 static

✅ 클래스 내 static 멤버

클래스 내에 static으로 선언된 멤버는 “객체마다 따로 존재하지 않고, 클래스 전체에서 공유되는 멤버”입니다.

즉, 클래스 이름을 통해 접근하고, 객체를 생성하지 않아도 사용할 수 있는 멤버가 됩니다.

✅ static 멤버 변수 (정적 멤버 변수)

🔹 특징

  • 공유성 : 모든 객체가 같은 변수 하나를 공유
  • 객체 없이 접근 : 객체 없이도 클래스이름::변수명으로 접근 가능
  • 초기화 위치 : 클래스 내부에서는 선언만 하고, 정의는 클래스 외부에서 해야 함 (단, C++17 이후는 예외 있음)

🔹 선언 & 정의 예시

class Counter {
public:
    static int count;  // 선언만 함
};

int Counter::count = 0;  // 클래스 외부에서 정의 & 초기화

✅ static 멤버 함수

🔹 특징

  • 객체 없이 호출 가능 : 클래스이름::함수명()으로 호출 가능
  • 일반 멤버 접근 불가 : this 포인터 없음 → 비-static 멤버에 접근 불가
  • 정적 변수/전역 함수처럼 동작 : 유틸리티 함수나 클래스 전체 대상의 기능에 적합

🔹 예시

class Math {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

int result = Math::add(3, 4);  // 객체 없이 호출 가능

✅ 예시: 객체 수 카운트

#include <iostream>
class Person {
private:
    static int count;  // 객체 수 카운트용
public:
    Person() {
        count++;
    }

    ~Person() {
        count--;
    }

    static int getCount() {
        return count;
    }
};

int Person::count = 0;  // 반드시 클래스 밖에서 정의!

int main() {
    Person p1, p2;
    std::cout << "현재 객체 수: " << Person::getCount() << '\n';
}

✅ 출력: 현재 객체 수: 2

✅ static 멤버의 사용 목적

  • 전체 객체에 공통 데이터 저장
  • 객체 생성 없이 함수 사용
  • 클래스 간 공유 리소스

✅ C++17부터는 inline static 가능

class Config {
public:
    inline static int version = 1;  // C++17 이상부터 선언과 동시에 초기화 가능
};
  • C++17부터는 inline static으로 클래스 내부에서 초기화 가능
  • 더 이상 외부 정의(int Class::var = ...) 안 해도 됨

✅ 주의할 점

  1. static 멤버는 객체와 무관하게 존재하므로, 일반 멤버(비-static 변수/함수)에는 접근할 수 없음
  2. 멤버 변수는 클래스 외부에서 반드시 정의해야 함 (C++17 이전까지)
  3. 객체별 상태를 저장할 수 없고, 클래스 단위의 상태/기능에만 적합

💬 보너스

  • main()이 실행되기 전에 static 변수들이 초기화됨
  • 소멸은 프로그램 종료 시 자동 처리됨 (전역처럼 취급)

✅ 복사 생성자 (Copy Constructor)

✅ 복사 생성자란?

자신과 같은 타입의 객체를 인자로 받아, 자기 자신을 그 객체의 복사본으로 초기화하는 생성자

🔹 문법

ClassName(const ClassName& other);
  • 매개변수는 같은 클래스 타입의 참조(const 참조)로 받아야 함
  • 복사 생성자는 객체가 복사될 때 자동 호출

✅ 언제 호출되나? (복사 생성자 호출 시점)

  • 같은 타입의 객체로 선언 - 초기화 : Class a = b; 또는 Class a(b);
  • 값 전달 방식으로 함수 인자 전달 : func(obj);
  • 함수가 객체를 값으로 반환할 때 : return obj;
  • STL 컨테이너가 객체를 복사할 때 : 내부적으로 복사 생성자 사용

✅ 기본 복사 생성자 (컴파일러가 자동 생성)

class A {
public:
    int x;
};

A a1;
A a2 = a1;  // 복사 생성자 자동 호출됨
  • 컴파일러가 기본 제공
  • 멤버들을 “얕게(shallow)” 복사 → 단순히 값 복사

✅ 사용자 정의 복사 생성자가 필요한 경우

주로 동적 메모리를 사용하는 클래스에서 직접 구현해야 함

예시: 얕은 복사 문제

class MyArray {
private:
    int* data;
public:
    MyArray(int size) {
        data = new int[size];
    }

    ~MyArray() {
        delete[] data;
    }
};

MyArray a1(5);
MyArray a2 = a1;  // ❌ 얕은 복사 → data 포인터 주소만 복사됨 → double delete 오류 발생 가능

→ a1과 a2가 같은 data를 가리키게 되어, 소멸자에서 중복 해제 문제 발생

✅ 사용자 정의 복사 생성자 예시 (깊은 복사)

class MyArray {
private:
    int* data;
    int size;

public:
    MyArray(int s) : size(s) {
        data = new int[size];
    }

    // 복사 생성자 (깊은 복사)
    MyArray(const MyArray& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; ++i)
            data[i] = other.data[i];
    }

    ~MyArray() {
        delete[] data;
    }
};

✅ 1. this 포인터

✅ 개념

this 포인터는 클래스의 멤버 함수 내부에서 암묵적으로 사용 가능한 포인터로, 자기 자신 객체의 주소를 가리킵니다.

즉, 멤버 함수가 호출된 객체 자신을 가리켜요.

✅ 예시

class MyClass {
private:
    int value;

public:
    void setValue(int value) 
    {
        this->value = value;  // 멤버변수 value ← 매개변수 value 구분
    }
};

🔸 여기서 this->value는 멤버 변수, value는 매개변수

✅ 언제 사용하나?

  • 멤버 변수와 매개변수 이름이 같을 때, 구분하려고 사용 (this->x = x)
  • 자기 자신의 주소를 리턴할 때, return *this; ← 자기 객체 리턴
  • 체이닝 (연결된 연산) 구현할 때 return *this; 해서 a.set().do().log(); 가능

✅ 2. 연산자 오버로딩

✅ 개념

연산자 오버로딩은 C++에서 기호(+, -, == 등)를 클래스에 맞게 재정의하는 기능입니다.
즉, 객체도 일반 타입처럼 연산자 사용이 가능하도록 만들어줍니다.

✅ 예시: + 연산자 오버로딩

class Point {
private:
    int x, y;

public:
    Point(int a, int b) : x(a), y(b) {}

    // + 연산자 오버로딩
    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }

    void show() const {
        std::cout << "(" << x << ", " << y << ")\n";
    }
};

int main() {
    Point p1(1, 2), p2(3, 4);
    Point p3 = p1 + p2;
    p3.show();  // 출력: (4, 6)
}

✅ 연산자 오버로딩 문법

리턴타입 클래스이름 :: operator기호 (매개변수)

예: Point operator+(const Point&);

✅ this와 연산자 오버로딩의 관계

this오버로딩 함수 내부에서 자기 객체의 멤버에 접근하거나,
자기 자신을 리턴할 때 매우 자주 사용됩니다.

class MyClass {
public:
    MyClass& operator=(const MyClass& other) {
        // 자기 자신에 대한 할당 방지
        if (this != &other) {
            // 멤버 복사
        }
        return *this;  // 자기 자신을 리턴 → 연속 대입 가능
    }
};

*this를 리턴하면 a = b = c; 같은 연속 대입이 가능해집니다.


C++에서 friend객체지향의 정보 은닉(encapsulation)을 선택적으로 해제할 수 있는 예외적인 키워드예요.

즉, 특정 클래스나 함수에 “내 private/protected 멤버에 접근해도 돼!” 라고 허용해주는 역할을 합니다.

✅ friend 키워드란?

클래스의 멤버가 아님에도 불구하고, 그 클래스의 private/protected 멤버에 접근할 수 있게 허용하는 키워드입니다.

사용할 수 있는 대상:

  • 비멤버 함수
  • 다른 클래스
  • 멤버 함수 (다른 클래스의)

✅ 왜 필요한가?

  • <<, >> 연산자 오버로딩 : 입출력 연산자는 보통 비멤버로 구현해야 하지만, 내부 접근이 필요함
  • 두 클래스가 긴밀히 협력할 때 : 서로의 private 멤버를 직접 참조해야 할 필요가 있을 때
  • 디버그 도구, 내부 API 등 : 외부에서 클래스 내부 구조에 접근해야 할 특별한 상황에서 사용

✅ 1. friend 함수 예시 (입출력 연산자 오버로딩)

#include <iostream>
class Point {
private:
    int x, y;

public:
    Point(int x_, int y_) : x(x_), y(y_) {}

    // friend 함수 선언
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};

// friend 함수 정의 (클래스 밖)
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

✅ operator<<는 Point의 멤버가 아니지만, friend로 선언되었기 때문에 private 멤버인 x, y에 접근 가능

✅ 2. friend 클래스 예시

class Secret {
private:
    int code = 1234;

    friend class Hacker;  // 🔓 Hacker는 Secret의 private 멤버에 접근 가능
};

class Hacker {
public:
    void steal(const Secret& s) {
        std::cout << "훔친 코드: " << s.code << '\n';
    }
};
  • Hacker 클래스는 Secret의 private 멤버에 자유롭게 접근 가능

⚠️ 사용 시 주의점

  • friend는 “예외적인 신뢰 관계”를 의미합니다.
    → 너무 많이 쓰면 캡슐화 원칙을 깨뜨리고 설계가 엉성해질 수 있어요.
  • 보통 입출력 연산자 오버로딩이나 유틸리티 도구에서 최소한으로 사용합니다.

✅ 상속(Inheritance)이란?

기존 클래스(부모, 기반 클래스)속성과 기능(멤버 변수, 함수)
새로운 클래스(자식, 파생 클래스)가 물려받아 재사용하는 객체지향 개념입니다.

상속을 통해 중복을 줄이고, 공통 기능은 부모 클래스에,
세부 기능은 자식 클래스에 넣어서 코드를 효율적으로 관리할 수 있어요.

✅ 기본 문법

class 부모클래스 {
    // 공통 멤버들
};

class 자식클래스 : [접근지정자] 부모클래스 {
    // 자식만의 멤버들
};

예:

class Animal {
public:
    void eat() { std::cout << "냠냠\n"; }
};

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

→ Dog는 Animal의 기능을 상속받음
→ Dog 객체는 eat()도, bark()도 사용 가능!

✅ 상속의 접근 지정자 (public / protected / private 상속)

👉 이건 **“부모의 멤버들이 자식 클래스에서 어떤 수준으로 보이느냐”**를 결정합니다.

🔹 1. public 상속 → 가장 일반적이고 권장

class A {
public:
    void show() {}
protected:
    void protect() {}
private:
    void hide() {}
};

class B : public A {
    // A의 public → 그대로 public
    // A의 protected → 그대로 protected
    // A의 private → 접근 불가
};
  • ✅ 부모의 public → 자식에서도 public
  • ✅ 부모의 protected → 자식에서도 protected
  • ❌ 부모의 private → 자식에서 접근 불가

🔹 2. protected 상속 → 외부엔 숨기되, 자식과 손자, 그 아래 모두 접근 가능

class B : protected A {
    // A의 public → protected로 바뀜
    // A의 protected → 그대로 protected
    // A의 private → 접근 불가
};
  • 부모의 public 멤버가 자식에게는 protected로 들어옴
    • 외부에서 obj.show()처럼 접근 불가
    • 자식 안에서는 접근 가능
    • 🔸 상속은 하되 외부에 공개하고 싶지 않을 때

🔹 3. private 상속 → 거의 캡슐화 목적

class B : private A {
    // A의 public → private으로 바뀜
    // A의 protected → private으로 바뀜
    // A의 private → 접근 불가
};
  • 부모의 모든 멤버가 자식 클래스에서 private으로 바뀜
  • 자식의 자식은 상속한 내용에 접근할 수 없음
  • 외부에서도 사용 불가
  • 🔸 “부모 기능을 내부 구현으로만 쓰고 싶을 때” 사용

0개의 댓글