no image
챕터 3-1: 디자인 패턴
디자인 패턴이란?반복적으로 등장하는 개발 문제를 해결하기 위한 일반화된 솔루션. 디자인 패턴의 분류생성 패턴(Creational Patterns)객체 생성과 관련된 패턴.예: 싱글톤 패턴구조 패턴(Structural Patterns)객체 간의 관계를 구성하는 패턴.예: 데코레이터 패턴행동 패턴(Behavioral Patterns)객체 간의 상호작용을 정의하는 패턴.예: 옵저버 패턴싱글톤 패턴 (Singleton Pattern)목표객체를 오직 하나만 생성하고 전역적으로 접근 가능하도록 함.주요 개념생성자를 private으로 설정하여 외부에서 객체 생성을 제한.getInstance() 메서드를 통해 유일한 인스턴스에 접근.코드 예제#include using namespace std;class Airplane..
2025.01.06
챕터 2-4: 객체지향적 설계
1. 객체지향 설계의 중요성오픈소스 이해를 돕고 학습 효율 증가구현 시간 단축 및 유지보수 용이기능 변경에 유연함2. 응집도(Cohesion)정의: 클래스 내 모듈들이 얼마나 관련있는지높은 응집도: 관련된 기능만 포함낮은 응집도: 무관한 기능 섞임예시: 높은 응집도class PizzaDelivery: def calculate_route(self): # 배달 경로 계산 pass def estimate_time(self): # 배달 예상 시간 측정 pass def respond_to_customer(self): # 고객 문의 대응 pass3. 결합도(Coupling)정의: 모듈 간 의존성 정도낮은 결합도: 변경이 다른..
2025.01.03
no image
챕터 2-3 : STL 기초
더보기목표STL의 구성요소를 이해하고 효율적인 코드를 작성할 수 있다.컨테이너와 알고리즘을 활용하여 생산성을 높이는 프로그래밍 방식을 학습한다.STL의 개요C++ 표준 라이브러리로 컨테이너, 알고리즘, 반복자 등을 포함.템플릿 기반으로 다양한 데이터 타입에 대해 사용 가능.동적 메모리 관리를 내부적으로 처리.반복자를 통해 컨테이너 구현 방식과 관계없이 동일한 문법으로 접근 가능.컨테이너데이터를 저장하고 관리하기 위한 자료구조.사용 목적에 따라 다양한 형태 제공(배열형, 연결 리스트형, 키-값 쌍 등).템플릿으로 구현되어 타입에 상관없이 사용 가능.메모리 관리를 자동으로 처리.반복자 지원으로 내부 구현 방식과 관계없이 데이터 접근 가능.1. 벡터(Vector)동적 배열 형태의 컨테이너로 크기가 자동으로 조..
2025.01.02
no image
챕터 2-2 : 템플릿
1. 함수 오버로딩함수 오버로딩이란?C++에서 동일한 이름의 함수를 정의할 수 있는 기능.함수 이름뿐 아니라 매개변수의 타입과 개수를 기준으로 구분.C언어와 달리, C++은 함수 이름 + 매개변수 정보로 판단. 함수 오버로딩이 되는 조건매개변수 타입이 다를 경우.매개변수 개수가 다를 경우.예제: 매개변수 타입이 다른 경우void display(int value);void display(double value);void display(string value);​display(10) 호출 시: int 버전 실행.display(3.14) 호출 시: double 버전 실행.display("Hello!") 호출 시: string 버전 실행.예제: 매개변수 개수가 다른 경우void printSum(int a);vo..
2024.12.31
no image
챕터 2-1 : 자원 관리하기
1. 스택 메모리 일반 변수는 대부분 스택 메모리를 사용한다.스택 메모리는 변수의 생존 주기가 끝나면 자동으로 메모리를 회수한다.사용자가 별도로 메모리를 관리할 필요가 없다.(장점)변수의 생존 주기는 선언된 위치부터 가장 가까운 마침 괄호( } )까지다.#include using namespace std;void nestedFunction() { int b = 20; // 지역 변수 'b', stack 메모리에서 관리됨 cout  BUT, 단점: 스택은 메모리 영역 자체가 크지 않다. 생존 영역을 벗어나면 자동으로 해지 된다. 2. 힙 메모리힙 메모리는 위의 단점을 해결:선언 시 new 연산자를, 해제시 delete 연산자를 사용한다. 스택처럼 자동으로 해지하지 않는다. (리스크가 존재함) ..
2024.12.30
C++ 추가 자료
포인터1.1 메모리와 변수메모리는 RAM의 일부로, 여러 칸으로 구성됨. 각 칸은 고유한 주소를 가짐.변수는 메모리 칸에 데이터를 저장하는 이름표 역할.변수 주소는 &를 통해 접근 가능.int a = 10; // a: 값 10 저장, &a: a의 주소1.2 포인터 기본 개념포인터는 다른 변수의 메모리 주소를 저장하는 변수.주요 연산:참조(&): 변수의 주소 얻기.역참조(*): 포인터가 가리키는 주소의 값 얻기.int* ptr = &a; // ptr은 a의 주소 저장1.3 메모리와 포인터 예제예시: 변수 a와 포인터 ptra가 0x100 주소에 저장되어 있고, 값은 10.ptr은 a의 주소(0x100)를 저장, *ptr은 10 반환.| 이름 | 주소 | 값 | 타입 || ---- | ----..
2024.12.27
챕터 1-3 : 객체지향 프로그래밍
학습 목표상속의 개념과 활용 이해기본 클래스와 파생 클래스 정의 및 구현접근 제어자(protected)의 역할과 사용법 학습다형성의 원리와 실제 활용 방안 숙지1. 상속1.1 상속 없는 차량 관리 프로그램기존 방식의 문제점중복 코드 증가유지보수 및 확장성 부족class Bicycle { string color; int speed; bool hasBasket;public: Bicycle(string c, int s, bool basket) : color(c), speed(s), hasBasket(basket) {} void move() { cout 문제점 요약공통 속성(color, speed)과 동작(move)이 각 클래스에 중복되어 구현됨.새로운 차량 클래스 추가 시 기존 코드..
2024.12.26
챕터 1-2 : Class 개념
학습 목표클래스의 역할 이해클래스와 객체를 정의하고 활용접근 제어자(public, private) 사용 1. 객체 없이 성적 관리 프로그램기존 방식은 데이터 노출 및 재사용성 부족 문제 존재double getAvg(int kor, int eng, int math) { return (kor + eng + math) / 3.0; }int getMax(int kor, int eng, int math) { return max({kor, eng, math}); }문제: 과목 추가 시 코드 수정 필요, 데이터가 외부에 노출됨. 2. 클래스 정의 및 멤버 구성클래스 구조멤버 변수: 데이터를 포함멤버 함수: 동작 정의class Student { int kor, eng, math; // 멤버 변수 ..
2024.12.24

챕터 3-1: 디자인 패턴

언리얼 거토
|2025. 1. 6. 19:53

디자인 패턴이란?
반복적으로 등장하는 개발 문제를 해결하기 위한 일반화된 솔루션.

 

디자인 패턴의 분류

  1. 생성 패턴(Creational Patterns)
    • 객체 생성과 관련된 패턴.
    • 예: 싱글톤 패턴
  2. 구조 패턴(Structural Patterns)
    • 객체 간의 관계를 구성하는 패턴.
    • 예: 데코레이터 패턴
  3. 행동 패턴(Behavioral Patterns)
    • 객체 간의 상호작용을 정의하는 패턴.
    • 예: 옵저버 패턴

싱글톤 패턴 (Singleton Pattern)

목표

  • 객체를 오직 하나만 생성하고 전역적으로 접근 가능하도록 함.

주요 개념

  1. 생성자를 private으로 설정하여 외부에서 객체 생성을 제한.
  2. getInstance() 메서드를 통해 유일한 인스턴스에 접근.

코드 예제

#include <iostream>
using namespace std;

class Airplane {
private:
    static Airplane* instance; // 유일한 인스턴스를 가리키는 정적 포인터
    int positionX;             // X 좌표
    int positionY;             // Y 좌표

    Airplane() : positionX(0), positionY(0) { // private 생성자
        cout << "Airplane Created at (" << positionX << ", " << positionY << ")" << endl;
    }

public:
    // 복사 방지: 복사 생성자와 대입 연산자 삭제
    Airplane(const Airplane&) = delete;
    Airplane& operator=(const Airplane&) = delete;

    // 유일한 인스턴스 반환
    static Airplane* getInstance() {
        if (instance == nullptr) { // 아직 생성되지 않았다면
            instance = new Airplane();
        }
        return instance;
    }

    void move(int deltaX, int deltaY) {
        positionX += deltaX;
        positionY += deltaY;
        cout << "Airplane moved to (" << positionX << ", " << positionY << ")" << endl;
    }

    void getPosition() const {
        cout << "Airplane Position: (" << positionX << ", " << positionY << ")" << endl;
    }
};

Airplane* Airplane::instance = nullptr;

int main() {
    Airplane* airplane = Airplane::getInstance();
    airplane->move(10, 20);
    airplane->getPosition();

    Airplane* sameAirplane = Airplane::getInstance();
    sameAirplane->move(-5, 10);
    sameAirplane->getPosition();

    return 0;
}

코드 설명

  • static 포인터로 유일한 객체를 관리.
  • getInstance()에서 처음 호출 시 객체를 생성하고 이후에는 동일한 객체를 반환.
  • 장점: 자원을 효율적으로 관리.
  • 단점: 멀티스레드 환경에서는 동기화 필요.

데코레이터 패턴 (Decorator Pattern)

목표

  • 객체의 기능을 동적으로 확장하거나 변경할 수 있도록 함.

주요 개념

  1. 기본 컴포넌트: 핵심 기능을 가진 클래스.
  2. 데코레이터 클래스: 기존 객체를 감싸 추가 기능을 제공.

코드 예제

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

// 기본 피자 클래스
class Pizza {
public:
    virtual ~Pizza() {}
    virtual string getName() const = 0;
    virtual double getPrice() const = 0;
};

class BasicPizza : public Pizza {
public:
    string getName() const { return "Basic Pizza"; }
    double getPrice() const { return 5.0; }
};

class PizzaDecorator : public Pizza {
protected:
    Pizza* pizza;
public:
    PizzaDecorator(Pizza* p) : pizza(p) {}
    virtual ~PizzaDecorator() { delete pizza; }
};

class CheeseDecorator : public PizzaDecorator {
public:
    CheeseDecorator(Pizza* p) : PizzaDecorator(p) {}
    string getName() const { return pizza->getName() + " + Cheese"; }
    double getPrice() const { return pizza->getPrice() + 1.5; }
};

class PepperoniDecorator : public PizzaDecorator {
public:
    PepperoniDecorator(Pizza* p) : PizzaDecorator(p) {}
    string getName() const { return pizza->getName() + " + Pepperoni"; }
    double getPrice() const { return pizza->getPrice() + 2.0; }
};

int main() {
    Pizza* pizza = new BasicPizza();
    pizza = new CheeseDecorator(pizza);
    pizza = new PepperoniDecorator(pizza);

    cout << "Pizza: " << pizza->getName() << endl;
    cout << "Price: $" << pizza->getPrice() << endl;

    delete pizza;
    return 0;
}

코드 설명

  • PizzaDecorator가 Pizza 객체를 감싸 동적으로 기능 추가.
  • 장점: 기존 코드를 수정하지 않고도 기능 확장 가능.

옵저버 패턴 (Observer Pattern)

목표

  • 객체 상태가 변경되면, 이를 관찰하는 다른 객체들에게 자동으로 알림.

주요 개념

  1. Subject: 상태를 관리하며, 관찰자를 등록 및 관리.
  2. Observer: Subject의 상태 변경을 감지.

코드 예제

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

class Observer {
public:
    virtual void update(int data) = 0;
};

class ExcelSheet {
private:
    vector<Observer*> observers;
    int data;
public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }
    void setData(int newData) {
        data = newData;
        for (Observer* obs : observers) obs->update(data);
    }
};

class BarChart : public Observer {
public:
    void update(int data) {
        cout << "BarChart updated with data: " << data << endl;
    }
};

int main() {
    ExcelSheet sheet;
    BarChart barChart;
    sheet.attach(&barChart);

    sheet.setData(5);
    return 0;
}

코드 설명

  • ExcelSheet에서 데이터 변경 시 모든 Observer가 알림을 받음.
  • 장점: 느슨한 결합으로 객체 간 관계 관리.

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

Unreal Engine에서 Tick 사용법  (0) 2025.01.08
챕터 3-2: Unreal Engine 기본 개념  (2) 2025.01.07
챕터 2-4: 객체지향적 설계  (1) 2025.01.03
챕터 2-3 : STL 기초  (1) 2025.01.02
챕터 2-2 : 템플릿  (1) 2024.12.31

챕터 2-4: 객체지향적 설계

언리얼 거토
|2025. 1. 3. 20:43

1. 객체지향 설계의 중요성

  • 오픈소스 이해를 돕고 학습 효율 증가
  • 구현 시간 단축 및 유지보수 용이
  • 기능 변경에 유연함

2. 응집도(Cohesion)

  • 정의: 클래스 내 모듈들이 얼마나 관련있는지
  • 높은 응집도: 관련된 기능만 포함
  • 낮은 응집도: 무관한 기능 섞임

예시: 높은 응집도

class PizzaDelivery:
    def calculate_route(self):
        # 배달 경로 계산
        pass

    def estimate_time(self):
        # 배달 예상 시간 측정
        pass

    def respond_to_customer(self):
        # 고객 문의 대응
        pass

3. 결합도(Coupling)

  • 정의: 모듈 간 의존성 정도
  • 낮은 결합도: 변경이 다른 모듈에 영향 없음
  • 높은 결합도: 변경 시 다수의 모듈 수정 필요

예시: 낮은 결합도

class PaymentProcessor:
    def process_payment(self, payment_method):
        # 결제 처리 (의존성 최소화)
        payment_method.pay()

class CreditCard:
    def pay(self):
        # 카드 결제
        print("Paying with Credit Card")

processor = PaymentProcessor()
processor.process_payment(CreditCard())

4. SOLID 원칙

  • SRP: 하나의 클래스는 하나의 책임만
  • OCP: 확장에는 열림, 변경에는 닫힘
  • LSP: 부모 클래스는 자식으로 대체 가능
  • ISP: 클라이언트에 맞는 인터페이스 분리
  • DIP: 추상화에 의존, 구체화 피하기

4.1 SRP (단일 책임 원칙)

  • 정의: 클래스는 하나의 책임만 가져야 함.
  • 이유: 변경 이유를 하나로 제한해 유지보수 용이.

예시:

class Order:
    def add_item(self, item):
        # 주문 항목 추가
        pass

class Invoice:
    def generate_invoice(self, order):
        # 송장 생성
        pass

2. OCP (개방-폐쇄 원칙)

  • 정의: 확장에는 열려 있고, 변경에는 닫혀 있어야 함.
  • 이유: 기존 코드를 수정하지 않고 새로운 기능 추가 가능.

예시:

class Discount:
    def apply(self, price):
        return price

class SeasonalDiscount(Discount):
    def apply(self, price):
        return price * 0.9  # 10% 할인

class Cart:
    def __init__(self, discount: Discount):
        self.discount = discount

    def calculate_total(self, price):
        return self.discount.apply(price)

cart = Cart(SeasonalDiscount())
print(cart.calculate_total(100))

3. LSP (리스코프 치환 원칙)

  • 정의: 자식 클래스는 부모 클래스를 대체할 수 있어야 함.
  • 이유: 상속 구조에서 일관성 유지.

예시:

class Bird:
    def fly(self):
        print("I can fly!")

class Sparrow(Bird):
    pass

def let_bird_fly(bird: Bird):
    bird.fly()

sparrow = Sparrow()
let_bird_fly(sparrow)  # Sparrow도 Bird처럼 행동

4. ISP (인터페이스 분리 원칙)

  • 정의: 클라이언트에 불필요한 인터페이스를 강요하지 말아야 함.
  • 이유: 특정 요구에 맞춘 인터페이스 분리로 코드 유연성 증가.

예시:

class Printer:
    def print_document(self):
        pass

class Scanner:
    def scan_document(self):
        pass

class MultiFunctionDevice(Printer, Scanner):
    def print_document(self):
        print("Printing document")

    def scan_document(self):
        print("Scanning document")

5. DIP (의존성 역전 원칙)

  • 정의: 고수준 모듈은 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존해야 함.
  • 이유: 변경에 강한 구조 설계 가능.

예시:

class NotificationService:
    def send(self, message):
        pass

class EmailService(NotificationService):
    def send(self, message):
        print(f"Email sent: {message}")

class NotificationManager:
    def __init__(self, service: NotificationService):
        self.service = service

    def notify(self, message):
        self.service.send(message)

email_service = EmailService()
manager = NotificationManager(email_service)
manager.notify("Hello, Dependency Inversion!")

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

챕터 3-2: Unreal Engine 기본 개념  (2) 2025.01.07
챕터 3-1: 디자인 패턴  (0) 2025.01.06
챕터 2-3 : STL 기초  (1) 2025.01.02
챕터 2-2 : 템플릿  (1) 2024.12.31
챕터 2-1 : 자원 관리하기  (0) 2024.12.30

챕터 2-3 : STL 기초

언리얼 거토
|2025. 1. 2. 20:00

 

더보기

목표

  • STL의 구성요소를 이해하고 효율적인 코드를 작성할 수 있다.
  • 컨테이너와 알고리즘을 활용하여 생산성을 높이는 프로그래밍 방식을 학습한다.

STL의 개요

  • C++ 표준 라이브러리로 컨테이너, 알고리즘, 반복자 등을 포함.
  • 템플릿 기반으로 다양한 데이터 타입에 대해 사용 가능.
  • 동적 메모리 관리를 내부적으로 처리.
  • 반복자를 통해 컨테이너 구현 방식과 관계없이 동일한 문법으로 접근 가능.

컨테이너

  • 데이터를 저장하고 관리하기 위한 자료구조.
  • 사용 목적에 따라 다양한 형태 제공(배열형, 연결 리스트형, 키-값 쌍 등).
  • 템플릿으로 구현되어 타입에 상관없이 사용 가능.
  • 메모리 관리를 자동으로 처리.
  • 반복자 지원으로 내부 구현 방식과 관계없이 데이터 접근 가능.


1. 벡터(Vector)

  • 동적 배열 형태의 컨테이너로 크기가 자동으로 조정됨.
  • 임의 접근 가능하며 삽입과 삭제는 주로 맨 끝에서 수행하는 것이 효율적.
  • 배열과 달리 크기가 고정되지 않으며 다양한 메서드 제공.

1.1 벡터 선언 방법

  • 기본 생성 및 초기화
vector<int> vec1;                // 빈 벡터 생성
vector<int> vec2(5, 10);         // 크기 5, 모든 원소가 10으로 초기화
vector<int> vec3 = {1, 2, 3, 4, 5}; // 특정 값으로 초기화

 

  • 다른 벡터 복사 및 대입
vector<int> vec3 = {1, 2, 3, 4, 5};
vector<int> vec4(vec3);          // vec3의 복사본 생성
vector<int> vec5 = vec3;         // 복사 대입

  • 2차원 벡터 선언
vector<vector<int>> vec2D(3, vector<int>(4, 7)); // 3x4 행렬, 모든 원소가 7로 초기화

1.2 벡터 주요 메서드(동작)

  • push_back: 끝에 원소 추가
vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
// 출력: 10 20 30
  • pop_back: 끝에서 원소 제거
vector<int> vec = {10, 20, 30};
vec.pop_back(); // 마지막 요소(30) 제거
  • size: 현재 크기 확인
vector<int> vec = {10, 20, 30};
cout << vec.size(); // 출력: 3
  • erase: 특정 위치 원소 제거
vector<int> vec = {10, 20, 30, 40};
vec.erase(vec.begin() + 1); // 두 번째 원소(20) 제거

 


2. 맵

핸드폰 연락처를 생각해봅시다.

  • 연락처를 검색할 때 이름을 기준으로 전화번호를 찾는 방식과 유사.
  • 맵은 키(Key)를 기반으로 값(Value)을 저장하며, 배열이 인덱스를 사용하는 것과 유사한 역할 수행.

2.1 맵(Map)의 특징

  1. 키-값(Key-Value) 쌍으로 이루어진 컨테이너
    • 배열의 인덱스처럼 키를 사용해 값에 접근.
  2. 키 기준 자동 정렬
    • 사용자가 별도 정렬 작업을 하지 않아도 항상 정렬된 상태 유지.
  3. 중복된 키 허용 불가
    • 동일 키를 가진 데이터는 하나만 저장 가능.

2.2 맵 선언 방법

  • 맵 선언 시 두 가지 타입(키와 값)이 필요하며, 이 둘은 같아도 되고 달라도 됨.

코드 예제: 다양한 선언 방법

#include <map>
#include <vector>
using namespace std;

int main() {
    // 1. 기본 선언
    map<string, int> studentScores;

    // 2. 선언과 동시에 초기화
    map<int, string> idToName = {{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}};

    // 3. 키: char, 값: double
    map<char, double> gradeToGPA;

    // 4. 키: int, 값: vector<int>
    map<int, vector<int>> studentMarks;

    // 5. 키: pair<int, int>, 값: string
    map<pair<int, int>, string> coordinateToName;

    return 0;
}

 

2.3 맵의 주요 동작

  • 키 기준 자동 정렬
    • 맵은 삽입된 데이터를 키 값 기준으로 자동 정렬.
map<int, string> myMap;
myMap[5] = "E";
myMap[2] = "B";
myMap[8] = "H";
myMap[1] = "A";

for (auto it = myMap.begin(); it != myMap.end(); ++it) {
    cout << it->first << ": " << it->second << endl;
}
// 출력: 1: A, 2: B, 5: E, 8: H
  • insert로 원소 추가
map<int, string> myMap;
myMap.insert(make_pair(1, "Apple"));
myMap.insert(make_pair(2, "Banana"));
  • find로 특정 키 검색
map<int, string> myMap = {{1, "Apple"}, {2, "Banana"}};
auto it = myMap.find(2);
if (it != myMap.end()) {
    cout << "Found: " << it->first << ": " << it->second << endl;
} else {
    cout << "Key not found!" << endl;
}
  • size로 크기 확인
cout << "Map size: " << myMap.size() << endl;
  • clear로 모든 원소 삭제
myMap.clear();
cout << "Map size after clear: " << myMap.size() << endl;
  • 맵 복사
    • 복사 생성자, 대입 연산자, insert 사용 가능.
map<int, string> originalMap = {{1, "Apple"}, {2, "Banana"}};
map<int, string> copiedMap1(originalMap); // 복사 생성자
map<int, string> copiedMap2 = originalMap; // 대입 연산자
map<int, string> copiedMap3;
copiedMap3.insert(originalMap.begin(), originalMap.end()); // insert 사용

알고리즘

C++ STL(Standard Template Library)은 컨테이너뿐만 아니라 이를 효과적으로 활용할 수 있는 다양한 알고리즘을 제공한다. 이 알고리즘들은 반복자(iterator)를 기반으로 동작하며, 컨테이너의 내부 구현을 알 필요 없이 동일한 방식으로 사용할 수 있다는 점이 큰 장점이다.

STL 알고리즘은 다음과 같은 작업을 지원한다:

  • 정렬(Sort): 데이터를 정렬하여 특정 기준에 맞게 배치.
  • 탐색(Find): 특정 값을 찾아 위치를 반환.
  • 수치 연산(Numeric): 데이터의 합계, 곱셈 등.
  • 변환(Transform): 데이터를 변환하여 새로운 결과 생성.

이를 통해 C++ 개발자는 반복적인 작업을 간결하고 효율적으로 구현할 수 있다. 이번 섹션에서는 가장 자주 사용되는 Sort(정렬)Find(탐색) 알고리즘을 중심으로 살펴보겠다.


1. Sort

정렬은 STL에서 가장 널리 사용되는 알고리즘 중 하나로, 컨테이너 내부의 데이터를 특정 기준에 따라 정렬한다. 오름차순이 기본 동작이며, 필요 시 사용자 정의 정렬 기준을 지정할 수 있다.

(1) 기본 타입 배열 정렬

  • 정렬 기준 없음 (오름차순 기본값):
#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int size = sizeof(arr) / sizeof(arr[0]);

    sort(arr, arr + size);

    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    return 0;
}
  • 정렬 기준 있음 (내림차순):
#include <iostream>
#include <algorithm>
using namespace std;

bool compare(int a, int b) {
    return a > b; // 내림차순 정렬
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int size = sizeof(arr) / sizeof(arr[0]);

    sort(arr, arr + size, compare);

    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    return 0;
}

(2) 벡터 정렬

벡터 역시 배열과 동일한 방식으로 정렬 가능하다.

  • 오름차순 정렬:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> vec = {5, 2, 9, 1, 5, 6};
    sort(vec.begin(), vec.end());

    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}
  • 사용자 정의 정렬 기준:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool compare(int a, int b) {
    return a > b; // 내림차순
}

int main() {
    vector<int> vec = {5, 2, 9, 1, 5, 6};
    sort(vec.begin(), vec.end(), compare);

    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}

(3) 사용자 정의 타입 정렬

사용자 정의 클래스나 구조체도 정렬할 수 있으며, 정렬 기준은 비교 함수로 정의한다.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Person {
private:
    string name;
    int age;

public:
    Person(string name, int age) : name(name), age(age) {}
    string getName() const { return name; }
    int getAge() const { return age; }
};

bool compareByAgeAndName(const Person& a, const Person& b) {
    if (a.getAge() == b.getAge()) {
        return a.getName() < b.getName(); // 이름 기준
    }
    return a.getAge() < b.getAge(); // 나이 기준
}

int main() {
    vector<Person> people = {
        Person("Alice", 30),
        Person("Bob", 25),
        Person("Charlie", 35),
        Person("Alice", 25)
    };

    sort(people.begin(), people.end(), compareByAgeAndName);

    for (const Person& person : people) {
        cout << person.getName() << " (" << person.getAge() << ")" << endl;
    }
    return 0;
}

2. Find

STL의 find 함수는 컨테이너에서 특정 값을 검색한다. 값을 찾으면 해당 위치를 나타내는 반복자를 반환하며, 찾지 못하면 컨테이너의 끝 반복자를 반환한다.

(1) 벡터에서 값 찾기

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};
    auto it = find(vec.begin(), vec.end(), 30);

    if (it != vec.end()) {
        cout << "값 30이 벡터에서 발견됨, 위치: " << (it - vec.begin()) << endl;
    } else {
        cout << "값 30이 벡터에 없음" << endl;
    }
    return 0;
}

(2) 배열에서 값 찾기

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

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    auto it = find(arr, arr + size, 4);

    if (it != arr + size) {
        cout << "값 4가 배열에서 발견됨, 위치: " << (it - arr) << endl;
    } else {
        cout << "값 4가 배열에 없음" << endl;
    }
    return 0;
}

(3) 문자열에서 문자 찾기

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

int main() {
    string str = "hello world";
    auto it = find(str.begin(), str.end(), 'o');

    if (it != str.end()) {
        cout << "문자 'o'가 문자열에서 발견됨, 위치: " << (it - str.begin()) << endl;
    } else {
        cout << "문자 'o'가 문자열에 없음" << endl;
    }
    return 0;
}

반복자 (Iterators)

  • **반복자(iterator)**는 C++에서 컨테이너의 원소를 순차적으로 탐색하는 객체로, 컨테이너의 구현에 관계없이 알고리즘을 동일하게 적용할 수 있게 해준다. 이를 통해 코드의 재사용성과 효율성을 높일 수 있다.

1. 순방향 반복자 (Forward Iterator)

  • 기능: 컨테이너의 원소를 처음부터 끝까지 순차적으로 탐색.
  • 시작 위치: begin()
  • 끝 위치: end()
  • 특징: end()는 탐색 범위의 끝을 나타내며, 찾을 수 없을 경우 반환된다.

예시 1: 벡터에서 짝수만 출력

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

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 순방향 반복자로 짝수만 출력
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        if (*it % 2 == 0) {  // 짝수 판별
            cout << *it << " ";  // 출력
        }
    }
    return 0;
}
// 출력: 2 4 6 8 10

예시 2: 맵에서 키-값 쌍 출력

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

int main() {
    map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};

    // 순방향 반복자를 사용해 키-값 쌍 출력
    for (auto it = scores.begin(); it != scores.end(); ++it) {
        cout << it->first << ": " << it->second << endl;  // 키와 값 출력
    }
    return 0;
}
// 출력:
// Alice: 90
// Bob: 85
// Charlie: 88

2. 역방향 반복자 (Reverse Iterator)

  • 기능: 컨테이너의 원소를 끝에서부터 처음까지 순차적으로 탐색.
  • 시작 위치: rbegin()
  • 끝 위치: rend()
  • 특징: rend()는 첫 번째 원소 이전을 나타내며, 뒤에서부터 찾지 못한 경우 반환된다.

예시 1: 벡터에서 짝수만 출력 (역방향)

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

int main() {
    vector<int> numbers = {10, 15, 20, 25, 30};

    // 역방향 반복자로 짝수만 출력
    for (auto it = numbers.rbegin(); it != numbers.rend(); ++it) {
        if (*it % 2 == 0) {  // 짝수 판별
            cout << *it << " ";  // 출력
        }
    }
    return 0;
}
// 출력: 30 20 10

예시 2: 맵에서 값이 88 이상인 항목만 출력 (역방향)

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

int main() {
    map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};

    // 역방향 반복자로 값이 88 이상인 항목만 출력
    for (auto it = scores.rbegin(); it != scores.rend(); ++it) {
        if (it->second >= 88) {  // 값이 88 이상인 항목만 출력
            cout << it->first << ": " << it->second << endl;  // 출력
        }
    }

    return 0;
}
// 출력:
// Charlie: 88
// Alice: 90

3. find()와 역방향 반복자

  • 기능: find() 함수는 지정된 값을 찾는 데 사용되며, 역방향 반복자와 함께 사용할 경우 뒤에서부터 탐색이 진행된다.

예시 1: 역방향에서 문자열 찾기

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

int main() {
    vector<string> words = {"apple", "banana", "cherry", "date"};
    string target = "banana";

    // 역방향에서 "banana" 찾기
    auto it = find(words.rbegin(), words.rend(), target);

    if (it != words.rend()) {
        cout << "Word \"" << target << "\" found at reverse index " 
             << distance(words.rbegin(), it) << " (from the back)" << endl;
        cout << "Word \"" << target << "\" found at forward index " 
             << distance(words.begin(), it.base()) - 1 << " (from the front)" << endl;
    } else {
        cout << "Word \"" << target << "\" not found." << endl;
    }

    return 0;
}
// 출력:
// Word "banana" found at reverse index 2 (from the back)
// Word "banana" found at forward index 1 (from the front)

↓ 숙제: 반복자를 화용하여 각 컨테이너를 순회하기

더보기
  • 숙제 설명아래 코드스니펫에 작성된 초기화된 벡터와 맵을 순방향 반복자와 역방향 반복자를 활용해서 출력하는 코드를 작성하세요
  • STL 컨테이너는 반복자를 통해 거의 동일한 코드로 내부 구현에 의존하지 않고 순회할수 있다는 것을 배웠습니다.

#include <iostream>
#include <vector>
#include <map>

using namespace std;

int main() {
    // 벡터와 맵 데이터 정의
    vector<int> vec = { 10, 20, 30, 40, 50 };
    map<string, int> mp = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };

    // 문제: 아래 부분을 완성하세요

    return 0;
}

 

답:

#include <iostream>
#include <vector>
#include <map>

using namespace std;

int main() {
    // 벡터와 맵 데이터 정의
    vector<int> vec = { 10, 20, 30, 40, 50 };
    map<string, int> mp = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };

    // 문제: 아래 부분을 완성하세요

    // 벡터 출력
    cout << "Vector:\n";
    cout << "[Forward]: ";
    for (vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    cout << "[Backward]: ";
    for (vector<int>::const_reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;

    // 맵 출력
    cout << "\nMap:\n";
    cout << "[Forward]: ";
    for (map<string, int>::const_iterator it = mp.begin(); it != mp.end(); ++it) {
        cout << "(" << it->first << ", " << it->second << ") ";
    }
    cout << endl;

    cout << "[Backward]: ";
    for (map<string, int>::const_reverse_iterator rit = mp.rbegin(); rit != mp.rend(); ++rit) {
        cout << "(" << rit->first << ", " << rit->second << ") ";
    }
    cout << endl;

    return 0;
}

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

챕터 3-1: 디자인 패턴  (0) 2025.01.06
챕터 2-4: 객체지향적 설계  (1) 2025.01.03
챕터 2-2 : 템플릿  (1) 2024.12.31
챕터 2-1 : 자원 관리하기  (0) 2024.12.30
C++ 추가 자료  (1) 2024.12.27

챕터 2-2 : 템플릿

언리얼 거토
|2024. 12. 31. 18:00

1. 함수 오버로딩

함수 오버로딩이란?

  • C++에서 동일한 이름의 함수를 정의할 수 있는 기능.
  • 함수 이름뿐 아니라 매개변수의 타입개수를 기준으로 구분.
  • C언어와 달리, C++은 함수 이름 + 매개변수 정보로 판단. <- 시그니쳐

함수 오버로딩이 되는 조건

  • 매개변수 타입이 다를 경우.
  • 매개변수 개수가 다를 경우.

예제: 매개변수 타입이 다른 경우

void display(int value);
void display(double value);
void display(string value);​
  • display(10) 호출 시: int 버전 실행.
  • display(3.14) 호출 시: double 버전 실행.
  • display("Hello!") 호출 시: string 버전 실행.

예제: 매개변수 개수가 다른 경우

void printSum(int a);
void printSum(int a, int b);
void printSum(int a, int b, int c);
  • printSum(10) 호출: 매개변수 1개 버전 실행.
  • printSum(10, 20) 호출: 매개변수 2개 버전 실행.
  • printSum(10, 20, 30) 호출: 매개변수 3개 버전 실행.

함수 오버로딩이 불가능한 경우

  • 반환 타입만 다를 경우: 컴파일 에러
int getValue();
double getValue(); // 반환 타입만 다름 -> 오류.
  • 디폴트 매개변수로 모호한 경우
void printMessage(string message = "Default");
void printMessage(); // 호출 모호 -> 오류.
  • 포인터와 배열로만 차이 나는 경우
void process(int* arr);
void process(int arr[]); // 포인터와 배열 구분 불가 -> 오류.

 

함수 오버로딩 호출 순서

 

1. 정확히 일치하는 타입을 먼저 찾음.

2. 암묵적 타입 변환이 가능한 함수로 이동.

 

예제: 호출 순서

#include <iostream>
using namespace std;

void process(int value) {
    cout << "(int) 호출" << value << endl;
}

void process(int value, int value2) {
    cout << "(int, int) 호출" << value * value2 << endl;
}

void process(float value) {
    cout << "(float) 호출" << value << endl;
}
 void process(float value, float value2) {
    cout << "(float, float) 호출" << value * value2 << endl;
}


int main() {

    process(10);    //(int) 호출
    process(3, 4.3);//(int, int) 호출

    return 0;
}

2. 템플릿 (Template)

템플릿이란?

  • 타입에 구애받지 않는 일반화된 코드 작성을 위한 문법.
  • 문법:
template <typename T>
  • T는 타입 파라미터로, 실제 타입으로 대체됨.
  • 함수나 클래스를 타입에 상관없이 정의 가능.

템플릿 함수 예제

  • 두 수 더하기
template <typename T>
T add(T x, T y) {
    return x + y;
}

int main() {
    cout << add(3, 4) << endl;         // 정수형 덧셈
    cout << add(3.3, 4.2) << endl;     // 실수형 덧셈
    return 0;
}
  • 최댓값 구하기
template <typename T>
T getMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    cout << getMax(10, 20) << endl;     // 정수형
    cout << getMax(3.5, 2.7) << endl;   // 실수형
    cout << getMax('a', 'z') << endl;   // 문자형
    return 0;
}
  • 배열 합 구하기
template <typename T>
T sumArray(T arr[], int size) {
    T sum = 0;
    for (int i = 0; i < size; ++i) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int intArr[] = {1, 2, 3, 4, 5};
    double doubleArr[] = {1.1, 2.2, 3.3, 4.4};
    cout << sumArray(intArr, 5) << endl;     // 정수형 배열
    cout << sumArray(doubleArr, 4) << endl;  // 실수형 배열
    return 0;
}

템플릿 활용 예제

  • 값 교환하기
template <typename T>
void swapValues(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swapValues(x, y);  // 정수형
    cout << "x = " << x << ", y = " << y << endl;

    double p = 3.14, q = 2.71;
    swapValues(p, q);  // 실수형
    cout << "p = " << p << ", q = " << q << endl;
    return 0;
}
  • 값 비교하기
template <typename T>
bool isEqual(T a, T b) {
    return a == b;
}

int main() {
    cout << boolalpha;
    cout << isEqual(10, 10) << endl;   // 정수 비교
    cout << isEqual(3.5, 2.7) << endl;  // 실수 비교
    cout << isEqual('a', 'b') << endl;  // 문자 비교
    return 0;
}

템플릿 클래스 예제

  • 배열 관리 클래스
template <typename T>
class MyArray {
private:
    T* arr;
    int capacity;
    int size;

public:
    MyArray(int cap) : capacity(cap), size(0) {
        arr = new T[capacity];
    }

    ~MyArray() {
        delete[] arr;
    }

    void push(T value) {
        if (size < capacity) {
            arr[size] = value;
            size++;
        } else {
            cout << "배열이 가득 찼습니다!" << endl;
        }
    }

    void pop() {
        if (size > 0) {
            size--;
        } else {
            cout << "배열이 비어 있습니다!" << endl;
        }
    }

    void print() const {
        for (int i = 0; i < size; i++) {
            cout << arr[i] << " ";
        }
        cout << endl;
    }
};

int main() {
    MyArray<int> intArr(5);
    intArr.push(10);
    intArr.push(20);
    intArr.print();  // 정수형 배열

    MyArray<double> doubleArr(3);
    doubleArr.push(3.14);
    doubleArr.push(2.71);
    doubleArr.print();  // 실수형 배열
    return 0;
}

 

↓숙제: 오버로딩 된 함수 템플릿으로 변경하기

더보기

숙제 설명

  • 오버로딩된 함수를 템플릿으로 변경 해봅시다.
  • 우리는 타입이 동일한 두개의 인자를 받고 이를 더해서 반환하는 템플릿 함수를 구현해야 합니다.
  • 기존에 제공된 오버로딩 된 함수를 분석해서 하나의 템플릿 함수로 구현하시면 됩니다.
  • 템플릿 함수로 변경해야 할 오버로딩 된 함수
#include <iostream>

using namespace std;

//아래 3개의 함수를 하나의 템플릿 함수로 통합하세요
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

std::string add(const std::string& a, const std::string& b) {
    return a + b;
}


//아래 테스트 코드는 변역하지 마세요
int main() {
    // 정수 더하기
    cout << "3 + 5 = " << add(3, 5) << endl;

    // 실수 더하기
    cout << "2.5 + 4.3 = " << add(2.5, 4.3) << endl;

    // 문자열 합치기
    cout << "\"Hello, \" + \"World!\" = " << add(string("Hello, "), string("World!")) << endl;

    // 아래 코드는 컴파일 에러가 발생해야 함
    // cout << add(true, false) << endl;

    return 0;
}

 

답:

template<typename T>
T add(T a, T b) {
    return a + b;
}

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

챕터 2-4: 객체지향적 설계  (1) 2025.01.03
챕터 2-3 : STL 기초  (1) 2025.01.02
챕터 2-1 : 자원 관리하기  (0) 2024.12.30
C++ 추가 자료  (1) 2024.12.27
챕터 1-3 : 객체지향 프로그래밍  (1) 2024.12.26

챕터 2-1 : 자원 관리하기

언리얼 거토
|2024. 12. 30. 16:03

1. 스택 메모리

 

  • 일반 변수는 대부분 스택 메모리를 사용한다.
  • 스택 메모리는 변수의 생존 주기가 끝나면 자동으로 메모리를 회수한다.
    • 사용자가 별도로 메모리를 관리할 필요가 없다.(장점)
  • 변수의 생존 주기는 선언된 위치부터 가장 가까운 마침 괄호( } )까지다.
#include <iostream>
using namespace std;

void nestedFunction() {
    int b = 20;  // 지역 변수 'b', stack 메모리에서 관리됨
    cout << "nestedFunction: b = " << b << endl;
}  // 'b'는 nestedFunction() 종료 후 소멸됨

void functionExample() {
    int a = 10;  // 지역 변수 'a', stack 메모리에서 관리됨
    cout << "functionExample: a = " << a << endl;

    for (int i = 0; i < 3; ++i) {  // 반복문 내에서 매번 생성되는 지역 변수 'i'
        int temp = i * 10;  // 반복문 내에서만 유효한 'temp'
        cout << "Iteration " << i << ": temp = " << temp << endl;
    }  // 반복문이 끝날 때마다 'temp'는 소멸됨

    nestedFunction();  // 중첩 함수 호출
}  // 'a'는 functionExample() 종료 후 소멸됨

int main() {
    functionExample();  // functionExample() 호출
    return 0;
}

 

  • BUT, 단점:
    • 스택은 메모리 영역 자체가 크지 않다.
    • 생존 영역을 벗어나면 자동으로 해지 된다.

 

2. 힙 메모리

힙 메모리는 위의 단점을 해결:

  • 선언 시 new 연산자를, 해제시 delete 연산자를 사용한다.
  • 스택처럼 자동으로 해지하지 않는다. (리스크가 존재함)
  • 생존주기는 사용자가 선언한 순간부터 해지하기 전 까지이다.

#include <iostream>
using namespace std;

void handleDynamicMemory() {
    int size;
    cout << "배열 크기를 입력하세요: ";
    cin >> size;  // 사용자로부터 배열 크기 입력받기

    if (size > 0) {
        int* arr = new int[size];  // 배열 크기만큼 동적 할당
        for (int i = 0; i < size; ++i) {
            arr[i] = i * 10;  // 배열 초기화
            cout << "arr[" << i << "] = " << arr[i] << endl;
        }
        delete[] arr;  // 동적 할당한 배열 메모리 해제
    } else {
        cout << "잘못된 크기입니다!" << endl;
    }

    int* ptr = new int(20);  // 정수 하나 동적 할당
    cout << "값: " << *ptr << endl;
    delete ptr;  // 동적 할당한 메모리 해제
}

int main() {
    handleDynamicMemory();
    return 0;
}

 

3. Dangling Pointer

 

  • 비유 설명:
    • 어떤 식당을 가려고 하는데, 이미 그 식당이 철거된 상황이라 가게에 도착했을 때 허탕을 치는 상황.
  • C++에서의 유사한 문제:
    • 메모리 해지 후, 해지된 메모리 영역에 대한 정보가 특별히 알림 없이 그대로 남아 있을 수 있음.
  • 위험성:
    • 해지된 메모리 영역을 참조하면, 예상치 못한 결과를 초래할 수 있음.
    • 이런 포인터를 **Dangling Pointer**라고 부름.

 

#include <iostream>
using namespace std;

void func() {
    int* ptr = new int(30);  // 정수 30에 대한 메모리 할당
    int* ptr2 = ptr;         // 같은 메모리를 가리키는 두 번째 포인터

    cout << "삭제 전 값: " << *ptr << endl;

    delete ptr;              // 첫 번째 해제

    // 아래 라인을 주석 해제하면 이중 해지 오류가 발생합니다
    // delete ptr;            // 두 번째 해지 (이중 해지 오류)

    cout << "첫 번째 삭제 후 값: " << *ptr2 << endl;  // Dangling Pointer 사용
}

int main() {
    func();
    return 0;
}

 

↓ 숙제 1: 메모리 누수 발생 코드 분석, 보완 

더보기
#include <iostream>
using namespace std;

class MyClass {
private:
    int* ptr;

public:
    // 생성자
    MyClass() {
        ptr = new int(10); // 동적 메모리 할당
        cout << "메모리 할당 완료!" << endl;
    }

    // 소멸자
    ~MyClass() {
      
    }

    void print() const {
        cout << "값: " << *ptr << endl;
    }
};

int main() {
    MyClass obj;
    obj.print();

    // main 함수 종료
    return 0;
}

 

답: 

 ~MyClass() {
        delete ptr;  // 동적 메모리 해제 (ptr이 가리키는 메모리)
        cout << "메모리 해제 완료!" << endl;
    }

main 함수 종료 시, obj의 소멸자가 호출되어 메모리 해제가 이루어진다. 그러나 소멸자에서 delete ptr;가 없으면, ptr이 가리키는 동적 메모리가 해제되지 않아 메모리 누수가 발생한다. 동적 메모리를 할당할 때는 반드시 소멸자에서 메모리를 해제해야 한다.


4. 스마트 포인터(unique_ptr & shared_ptr)

 

  • Heap은 여러 가지 장점이 있지만, 메모리를 직접 관리해야 하는 부담이 있음.
  • Dangling Pointer 방지: 자동으로 메모리 관리가 되어 Dangling Pointer가 발생하지 않도록 하는 방식이 필요함.
  • C++의 스마트 포인터: 스마트 포인터는 레퍼런스 카운터를 이용해 메모리를 자동으로 관리함.
  • 스마트 포인터의 원리:
    • delete를 직접 호출하지 않고,
    • 자신을 참조하는 포인터의 개수가 0이 되면 자동으로 메모리를 해제함.

 

4.1 unique_ptr

 

  • 레퍼런스 카운터가 최대 1인 스마트 포인터.
  • 소유권을 한 번에 하나의 객체만 가질 수 있음, 안전하게 관리
  • 복사 불가능: unique_ptr은 복사나 대입이 불가능하며, 이를 시도하면 컴파일 에러가 발생함.
  • 소유권 이전 가능: move를 사용하여 소유권을 다른 unique_ptr로 이전할 수 있음.
  • 자동 메모리 해제: unique_ptr은 범위를 벗어나면 자동으로 메모리를 해제하여 관리 용이.

 

#include <iostream>
#include <memory>  // unique_ptr 사용

using namespace std;

class MyClass {
public:
    MyClass(int val) : value(val) {
        cout << "MyClass 생성: " << value << endl;
    }
    ~MyClass() {
        cout << "MyClass 소멸: " << value << endl;
    }
    void display() const {
        cout << "값: " << value << endl;
    }

private:
    int value;
};

int main() {
    // 1. 기본적인 unique_ptr 사용
    unique_ptr<int> ptr1 = make_unique<int>(10);  
    cout << "ptr1의 값: " << *ptr1 << endl;

    // 2. unique_ptr 소유권 이동
    unique_ptr<int> ptr2 = move(ptr1);  
    if (!ptr1) {
        cout << "ptr1은 이제 비어 있습니다." << endl;
    }
    cout << "ptr2의 값: " << *ptr2 << endl;

    // 3. 일반 클래스에서 unique_ptr 사용
    unique_ptr<MyClass> myObject = make_unique<MyClass>(42);  
    myObject->display();

    unique_ptr<MyClass> newOwner = move(myObject);
    if (!myObject) {
        cout << "myObject는 이제 비어 있습니다." << endl;
    }
    newOwner->display();

    return 0;
}

 

  • 기본 사용법:
    • unique_ptr<int> ptr1 = make_unique<int>(10);
      unique_ptr을 사용하여 동적 메모리 할당.
    • *ptr1로 값을 출력하고, 소유권 이동은 move를 통해 처리.
  • 소유권 이동 (move):
    • unique_ptr은 소유권을 이동할 수 있으며, move(ptr1)로 ptr1의 소유권을 ptr2로 이동.
    • 이동 후 ptr1은 비어있게 됨.
  • 클래스에서 사용:
    • unique_ptr<MyClass>를 사용하여 객체를 관리하고, 범위를 벗어나면 자동 메모리 해제가 이루어짐.

 

4.2 shared_ptr

 

  • 여러 포인터가 동일한 메모리를 공유하는 상황에서 유용
    • 레퍼런스 카운트가 여러 개 존재할 수 있는 스마트 포인터.
    • 복사 및 대입이 가능하고, use_count()로 참조 카운트를 확인할 수 있음. 
  • reset()을 통해 포인터를 초기화하고 참조 카운트를 조정할 수 있음.
  • 범위를 벗어나면 자동 해제됨.
#include <iostream>
#include <memory>  // shared_ptr 사용
using namespace std;

class MyClass {
public:
    MyClass(int val) : value(val) {
        cout << "MyClass 생성: " << value << endl;
    }
    ~MyClass() {
        cout << "MyClass 소멸: " << value << endl;
    }
    void display() const {
        cout << "값: " << value << endl;
    }
private:
    int value;
};

int main() {
    // 1. 기본적인 shared_ptr 사용
    shared_ptr<int> ptr1 = make_shared<int>(10);
    cout << "ptr1의 참조 카운트: " << ptr1.use_count() << endl;  // 출력: 1

    // 2. 복사 및 참조 카운트 증가
    shared_ptr<int> ptr2 = ptr1;
    cout << "ptr2 생성 후 참조 카운트: " << ptr1.use_count() << endl;  // 출력: 2

    // 3. reset을 통한 참조 카운트 감소
    ptr2.reset();
    cout << "ptr2 해제 후 참조 카운트: " << ptr1.use_count() << endl;  // 출력: 1

    // 4. 클래스에서 shared_ptr 사용
    shared_ptr<MyClass> obj1 = make_shared<MyClass>(42);
    shared_ptr<MyClass> obj2 = obj1;
    cout << "obj1과 obj2의 참조 카운트: " << obj1.use_count() << endl;  // 출력: 2

    obj2->display();  // 출력: 값: 42

    // 5. obj2 해제 후 obj1이 객체를 유지
    obj2.reset();
    cout << "obj2 해제 후 obj1의 참조 카운트: " << obj1.use_count() << endl;  // 출력: 1

    return 0;
}

 

기본 사용법

  • shared_ptr<int> ptr1 = make_shared<int>(10);
    shared_ptr을 사용해 동적 메모리 할당.
  • use_count()로 참조 카운트를 확인할 수 있으며, 참조 카운트의 변화에 따라 리소스 공유 상태를 알 수 있음.

복사 및 참조 공유

  • shared_ptr은 복사 가능하며, 여러 포인터가 하나의 리소스를 공유함.
    • 예: shared_ptr<int> ptr2 = ptr1;
  • 복사 후 참조 카운트가 증가하고, 동일 객체를 여러 포인터가 공유하게 됨.

reset()으로 참조 카운트 관리

  • reset()을 사용하여 shared_ptr을 초기화하고, 참조 카운트를 조정할 수 있음.
    • 예: ptr2.reset(); — ptr2를 해제하고 ptr1의 참조 카운트를 유지.

클래스에서의 사용

  • shared_ptr은 클래스를 관리할 때도 동일하게 사용됨. 여러 포인터가 동일한 객체를 공유하고, 참조 카운트에 따라 메모리가 자동으로 해제됨.
  • 객체를 관리하는 클래스에서 shared_ptr을 사용할 때, 포인터가 소멸하면 객체도 자동으로 해제됨.

5. 얕은 복사와 깊은 복사

얕은 복사(Shallow Copy)

  • 얕은 복사는 대입 연산자를 사용하여 두 포인터가 동일한 메모리 위치를 공유하는 방식.
    • 예: int* B = A; — B는 A가 가리키는 메모리 영역을 공유.
  • 이 방식에서 A가 메모리를 해제하면 B는 더 이상 유효한 메모리를 가리키지 않게 되어 Dangling Pointer가 발생할 수 있음.
  • Dangling Pointer는 해제된 메모리를 참조하게 되므로 Undefined Behavior를 일으킬 수 있음.

#include <iostream>
using namespace std;

int main() {
    int* A = new int(30);  // A가 30을 가리키는 메모리 할당
    int* B = A;            // B는 A가 가리키는 메모리 공유

    cout << "A의 값: " << *A << endl; // 출력: 30
    cout << "B의 값: " << *B << endl; // 출력: 30

    delete A;  // A가 메모리 해제

    cout << "B의 값 (dangling): " << *B << endl; // 위험: 정의되지 않은 동작
    return 0;
}

 

 

깊은 복사(Deep Copy)

  • 깊은 복사는 독립적인 메모리 영역을 새로 할당하고, 그 내용만 복사하는 방식.
    • 예: int* B = new int(*A); — B는 A의 값을 복사하여 독립된 메모리 영역을 관리.
  • 이 방식에서는 A가 메모리를 해제하더라도 B는 여전히 독립적으로 메모리를 관리하므로 Dangling Pointer가 발생하지 않음.
  • 각 포인터가 독립적인 메모리 공간을 할당받기 때문에, 안전하게 메모리 관리를 할 수 있음. 
#include <iostream>
using namespace std;

int main() {
    int* A = new int(30);  // A가 30을 가리키는 메모리 할당
    int* B = new int(*A);  // B는 A의 값을 복사하여 독립된 메모리 할당

    cout << "A의 값: " << *A << endl; // 출력: 30
    cout << "B의 값: " << *B << endl; // 출력: 30

    delete A;  // A의 메모리 해제
    cout << "B의 값 (깊은 복사 후): " << *B << endl; // 출력: 30

    delete B;  // B의 메모리 해제
    return 0;
}

 

↓ 숙제2: 스마트 포인터를 활용한 로그분석기 구현

더보기
  • 요구사항은 아래와 같습니다.
    • 로그 메시지는 중요도에따라(Info,Warning,Error)로 분류되어 기록될 수 있습니다. 로그 앞에 중요도가 태그가 붙어 표시됩니다.
      • [Info] 사용자 메시지
      • [Warning]사용자 메시지
      • [Error] 사용자 메시지
    • 로그 기록기는 단 하나의 인스턴스만 존재해야 하며, unique_ptr를 통해 이를 보장해야 합니다.
    • 지금까지 기록된 로그의 총 개수를 출력할 수 있어야 합니다.
  • 로그 기록기는 프로그램에서 발생한 중요한 사건을 기록합니다. 로그 기록기의 몇가지 기능을 구현해봅시다.

[로그 출력 예시]

[INFO]: System is starting.

[WARNING]: Low disk space.

[ERROR]: Unable to connect to the server.

Total logs recorded: 3

Logger instance destroyed.

 

답: 

#include <iostream>
#include <memory> // unique_ptr를 사용하기 위해 포함
#include <string> // std::string 사용을 위해 포함

using namespace std;

class Logger {
private:
    int logCount; // 총 로그 개수를 저장하는 변수

public:
    // 생성자: logCount를 0으로 초기화
    Logger() : logCount(0) {}

    // 일반 정보 로그: 로그 메시지를 출력하고 logCount 증가
    void logInfo(const string& message) {
        logCount++;
        cout << "[INFO]: " << message << endl;
    }

    // 경고 로그: 로그 메시지를 출력하고 logCount 증가
    void logWarning(const string& message) {
        logCount++;
        cout << "[WARNING]: " << message << endl;
    }

    // 에러 로그: 로그 메시지를 출력하고 logCount 증가
    void logError(const string& message) {
        logCount++;
        cout << "[ERROR]: " << message << endl;
    }
    //const를 사용하여 전달된 문자열이 수정되지 않음을 보장하며, 참조를 사용해 복사를 방지하여 성능을 최적화.

    // 총 로그 개수를 출력
    void showTotalLogs() {
        cout << "Total logs recorded: " << logCount << endl;
    }

    // 소멸자: Logger 객체가 소멸될 때 호출되며 로그를 출력
    ~Logger() {
        cout << "Logger instance destroyed." << endl;
    }
};

int main() {
    // Logger 인스턴스를 unique_ptr로 관리
    // unique_ptr는 스마트 포인터로, 동적 메모리를 자동으로 관리하며 복사가 불가능
    unique_ptr<Logger> logger = make_unique<Logger>();

    // 다양한 로그 기록
    logger->logInfo("System is starting.");       // 일반 정보 로그
    logger->logWarning("Low disk space.");       // 경고 로그
    logger->logError("Unable to connect to the server."); // 에러 로그

    // 총 로그 개수 출력
    logger->showTotalLogs();

    // unique_ptr은 복사가 불가능하므로 아래 코드는 에러 발생:
    // unique_ptr<Logger> anotherLogger = logger;

    return 0;
}

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

챕터 2-3 : STL 기초  (1) 2025.01.02
챕터 2-2 : 템플릿  (1) 2024.12.31
C++ 추가 자료  (1) 2024.12.27
챕터 1-3 : 객체지향 프로그래밍  (1) 2024.12.26
챕터 1-2 : Class 개념  (0) 2024.12.24

C++ 추가 자료

언리얼 거토
|2024. 12. 27. 20:15
  1. 포인터

1.1 메모리와 변수

  • 메모리는 RAM의 일부로, 여러 칸으로 구성됨. 각 칸은 고유한 주소를 가짐.
  • 변수는 메모리 칸에 데이터를 저장하는 이름표 역할.
  • 변수 주소는 &를 통해 접근 가능.
  • int a = 10; // a: 값 10 저장, &a: a의 주소

1.2 포인터 기본 개념

  • 포인터는 다른 변수의 메모리 주소를 저장하는 변수.
  • 주요 연산:
    • 참조(&): 변수의 주소 얻기.
    • 역참조(*): 포인터가 가리키는 주소의 값 얻기.
    int* ptr = &a;  // ptr은 a의 주소 저장

1.3 메모리와 포인터 예제

  • 예시: 변수 a와 포인터 ptr
    • a가 0x100 주소에 저장되어 있고, 값은 10.
    • ptr은 a의 주소(0x100)를 저장, *ptr은 10 반환.
    | 이름 | 주소   | 값     | 타입  |
    | ---- | ------ | ------ | ----- |
    | a    | 0x100  | 10     | int   |
    | ptr  | 0x200  | 0x100  | int*  |

1.4 코드 예제

#include <iostream>
using namespace std;

int main() {
    int a = 10;       // 변수 a 선언
    int* ptr = &a;    // ptr은 a의 주소 저장

    cout << "a의 값: " << a << endl;         // 10
    cout << "a의 주소: " << &a << endl;     // 0x100 (a의 주소)
    cout << "ptr의 값: " << ptr << endl;    // 0x100 (ptr이 가리키는 주소)
    cout << "ptr이 가리키는 값: " << *ptr << endl; // 10 (ptr이 가리키는 값)
}

1.5 주의 사항

  • int* ptr에서 *는 포인터 타입 선언 의미.
  • *ptr에서 *는 역참조 연산 의미.

 

2. 오버로딩(Overloading)과 오버라이딩(Overriding)

2.1 오버로딩(Overloading)

개념

  • 같은 이름의 함수나 연산자를 여러 번 정의하는 것을 의미.
  • 매개변수 목록(개수 또는 타입)이 달라야 함.
  • 함수 호출 시 전달된 인자에 따라 적절한 함수가 호출됨.

특징

  • 같은 클래스 내에서 사용.
  • 반환값만 다르다고 해서 오버로딩이 성립되지 않음.
  • 컴파일 타임에 결정(정적 바인딩).

예시

<함수 오버로딩>

#include <iostream>
using namespace std;

class Calculator {
public:
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
};

int main() {
    Calculator calc;

    cout << "정수 덧셈: " << calc.add(3, 4) << endl;        // 7
    cout << "실수 덧셈: " << calc.add(2.5, 3.5) << endl;  // 6.0
    cout << "세 정수 덧셈: " << calc.add(1, 2, 3) << endl; // 6

    return 0;
}

<연산자 오버로딩>

#include <iostream>
using namespace std;

class Complex {
private:
    double real, imag;

public:
    Complex(double r, double i) : real(r), imag(i) {}

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

    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    Complex c1(1.0, 2.0), c2(3.0, 4.0);
    Complex c3 = c1 + c2;

    c3.display(); // 4.0 + 6.0i

    return 0;
}

2.2 오버라이딩(Overriding)

개념

  • 기반 클래스의 메서드를 파생 클래스에서 재정의.
  • 부모 클래스의 메서드와 동일한 이름, 반환 타입, 매개변수 목록을 가짐.
  • 다형성을 구현하기 위해 사용.

특징

  • 기반 클래스와 파생 클래스 간 사용.
  • 반드시 상속 관계가 있어야 함.
  • 런타임에 결정(동적 바인딩).
  • 기반 클래스의 메서드는 virtual 키워드로 선언해야 함.

예시

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "Animal speaks" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Animal* animal;
    Dog dog;
    Cat cat;

    animal = &dog;
    animal->speak(); // Dog barks

    animal = &cat;
    animal->speak(); // Cat meows

    return 0;
}

2.3 오버로딩 vs 오버라이딩 차이점

구분오버로딩(Overloading)오버라이딩(Overriding)

정의 같은 클래스 내에서 함수 이름이 같지만 매개변수가 다르게 정의 파생 클래스에서 부모 클래스의 메서드를 재정의
적용 대상 같은 클래스 다른 클래스(상속 관계 필요)
매개변수 목록 반드시 달라야 함 동일해야 함
반환 타입 무관 동일해야 함
키워드 없음 virtual, override 사용 가능
바인딩 시점 컴파일 타임(정적 바인딩) 런타임(동적 바인딩)

2.4 객체 지향 프로그래밍과의 관계

오버로딩

  • 캡슐화와 관련.
  • 같은 이름의 메서드를 다양한 매개변수로 제공하여 사용자에게 단순한 인터페이스 제공.

오버라이딩

  • 상속과 다형성의 원칙 활용.
  • 부모 클래스의 메서드를 자식 클래스에서 재정의하여 객체의 동작을 맞춤화.

예시: 다형성과 오버라이딩

#include <iostream>
using namespace std;

class Vehicle {
public:
    virtual void start() {
        cout << "Vehicle is starting" << endl;
    }
};

class Car : public Vehicle {
public:
    void start() override {
        cout << "Car is starting" << endl;
    }
};

class Bike : public Vehicle {
public:
    void start() override {
        cout << "Bike is starting" << endl;
    }
};

void startVehicle(Vehicle* v) {
    v->start();
}

int main() {
    Car car;
    Bike bike;

    startVehicle(&car); // Car is starting
    startVehicle(&bike); // Bike is starting

    return 0;
}

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

챕터 2-2 : 템플릿  (1) 2024.12.31
챕터 2-1 : 자원 관리하기  (0) 2024.12.30
챕터 1-3 : 객체지향 프로그래밍  (1) 2024.12.26
챕터 1-2 : Class 개념  (0) 2024.12.24
챕터 1-1 : 프로그래밍 기초  (1) 2024.12.23

챕터 1-3 : 객체지향 프로그래밍

언리얼 거토
|2024. 12. 26. 15:59

학습 목표

  • 상속의 개념과 활용 이해
  • 기본 클래스와 파생 클래스 정의 및 구현
  • 접근 제어자(protected)의 역할과 사용법 학습
  • 다형성의 원리와 실제 활용 방안 숙지

1. 상속

1.1 상속 없는 차량 관리 프로그램

기존 방식의 문제점

  • 중복 코드 증가
  • 유지보수 및 확장성 부족
class Bicycle {
    string color;
    int speed;
    bool hasBasket;

public:
    Bicycle(string c, int s, bool basket) : color(c), speed(s), hasBasket(basket) {}
    void move() { cout << "The bicycle is moving at " << speed << " km/h." << endl; }
    void ringBell() { cout << "Bicycle bell: Ring Ring!" << endl; }
};

class Truck {
    string color;
    int speed;
    int cargoCapacity;

public:
    Truck(string c, int s, int capacity) : color(c), speed(s), cargoCapacity(capacity) {}
    void move() { cout << "The truck is moving at " << speed << " km/h." << endl; }
    void loadCargo() { cout << "Truck loading cargo. Capacity: " << cargoCapacity << " tons." << endl; }
};

문제점 요약

  • 공통 속성(color, speed)과 동작(move)이 각 클래스에 중복되어 구현됨.
  • 새로운 차량 클래스 추가 시 기존 코드 수정 필요.

1.2 상속을 활용한 클래스 정의

클래스 구조

  • 기본 클래스(Vehicle): 공통 속성과 동작 정의
  • 파생 클래스(Bicycle, Truck): 고유 특성 추가
class Vehicle {
protected:
    string color;
    int speed;

public:
    Vehicle(string c, int s) : color(c), speed(s) {}
    void move() { cout << "The vehicle is moving at " << speed << " km/h." << endl; }
};

Bicycle 클래스

class Bicycle : public Vehicle {
private:
    bool hasBasket;

public:
    Bicycle(string c, int s, bool basket) : Vehicle(c, s), hasBasket(basket) {}
    void ringBell() { cout << "Bicycle bell: Ring Ring!" << endl; }
};

Truck 클래스

class Truck : public Vehicle {
private:
    int cargoCapacity;

public:
    Truck(string c, int s, int capacity) : Vehicle(c, s), cargoCapacity(capacity) {}
    void loadCargo() { cout << "Truck loading cargo. Capacity: " << cargoCapacity << " tons." << endl; }
};

1.3 접근 제어자(protected)의 사용

  • protected: 파생 클래스에서 접근 가능하지만 외부에서는 접근 불가.
  • 효과: 공통 속성을 외부에 노출하지 않으면서 파생 클래스에서 재사용 가능.
class Vehicle {
protected:
    string color;
    int speed;
};

전체 프로그램 구현

int main() {
    Bicycle b("Yellow", 30, true);
    Truck t("Blue", 40, 95);

    b.move();
    b.ringBell();
    t.move();
    t.loadCargo();

    return 0;
}

결과: 공통 동작과 속성을 상속으로 관리하여 코드 중복을 제거하고 확장성을 확보.


2. 다형성 (Polymorphism)

2.1 다형성 정의

  • 다형성: 대표 클래스(부모 클래스)를 정의하고, 세부 구현은 이를 상속받는 파생 클래스에서 담당하도록 하는 기법.
  • 다양한 데이터 타입을 하나의 인터페이스로 처리할 수 있으며, 유지보수성과 확장성을 극대화함.

2.2 다형성 적용 전 코드

문제점

  • 동물마다 별도의 클래스와 함수를 생성해야 함.
  • 새로운 동물 클래스 추가 시, 기존 코드에 변경 사항이 생김.
  • 코드 중복이 발생하고, 유지보수가 어려움.
class Lion {
public:
    Lion(string word) : m_word(word) {}
    void bark() { cout << "Lion: " << m_word << endl; }
private:
    string m_word;
};

class Wolf {
public:
    Wolf(string word) : m_word(word) {}
    void bark() { cout << "Wolf: " << m_word << endl; }
private:
    string m_word;
};

class Dog {
public:
    Dog(string word) : m_word(word) {}
    void bark() { cout << "Dog: " << m_word << endl; }
private:
    string m_word;
};

void print(Lion lion) {
    lion.bark();
}

void print(Wolf wolf) {
    wolf.bark();
}

void print(Dog dog) {
    dog.bark();
}

int main() {
    Lion lion("ahaaaaaa!");
    Wolf wolf("ohhhhh");
    Dog dog("oooooooooooooops");

    print(lion);
    print(wolf);
    print(dog);

    return 0;
}

2.3 다형성 적용

개선 방법

  • 공통 부모 클래스 생성: 모든 동물 클래스가 상속받는 Animal 클래스 정의.
  • 가상 함수 사용: 부모 클래스에 virtual 키워드를 사용해 공통 메서드(bark) 정의.
  • 동적 바인딩: 부모 클래스 포인터를 사용하여 적절한 자식 클래스의 메서드 호출.
class Animal {
public:
    virtual void bark() = 0; // 순수 가상 함수
};

class Lion : public Animal {
private:
    string m_word;

public:
    Lion(string word) : m_word(word) {}
    void bark() override { cout << "Lion: " << m_word << endl; }
};

class Wolf : public Animal {
private:
    string m_word;

public:
    Wolf(string word) : m_word(word) {}
    void bark() override { cout << "Wolf: " << m_word << endl; }
};

class Dog : public Animal {
private:
    string m_word;

public:
    Dog(string word) : m_word(word) {}
    void bark() override { cout << "Dog: " << m_word << endl; }
};


void print(Animal* animal) { //부모 클래스와 상속 객체를 가리킬 수 있다//
    animal->bark(); //동적 바인딩//
}

int main() {
    Lion lion("ahaaaaaa!");
    Wolf wolf("ohhhhh");
    Dog dog("oooooooooooooops");

    print(&lion);
    print(&wolf);
    print(&dog);

    return 0;
}

3. 포인터 개념과 활용

3.1 포인터란?

  • 포인터(pointer)는 변수의 메모리 주소를 저장하는 특별한 변수.
  • C++에서 포인터는 강력하면서도 위험한 도구로, 잘못된 사용 시 프로그램 충돌이나 메모리 누수 등을 초래할 수 있음.

포인터 기본 문법

int main() {
    int a = 10;
    int* p = &a; // 'p'는 'a'의 메모리 주소를 저장

    cout << "a의 값: " << a << endl; // 10 출력
    cout << "a의 주소: " << &a << endl; // 메모리 주소 출력
    cout << "p가 가리키는 값: " << *p << endl; // 'p'가 가리키는 값, 즉 10 출력

    *p = 20; // 'p'를 통해 'a'의 값을 수정
    cout << "a의 새로운 값: " << a << endl; // 20 출력

    return 0;
}

3.2 포인터와 배열

  • 배열은 포인터와 밀접한 연관이 있음.
  • 배열 이름은 첫 번째 요소의 주소를 가리키는 포인터로 사용 가능.
int main() {
    int arr[3] = {1, 2, 3};
    int* p = arr; // 배열 이름은 첫 번째 요소의 주소를 나타냄

    cout << *p << endl; // 1 출력
    cout << *(p + 1) << endl; // 2 출력
    cout << *(p + 2) << endl; // 3 출력

    return 0;
}

3.3 포인터의 위험성

  • 초기화되지 않은 포인터 사용
  • 메모리 해제 후 포인터 사용
  • 잘못된 주소에 접근

스마트 포인터

  • C++11에서 도입된 스마트 포인터는 메모리 누수를 방지하는 데 유용.
  • 주요 타입: std::unique_ptr, std::shared_ptr, std::weak_ptr.
#include <memory>
#include <iostream>
using namespace std;

int main() {
    unique_ptr<int> uptr = make_unique<int>(42); // 동적 메모리 자동 관리
    cout << *uptr << endl; // 42 출력

    return 0;
}

 

4. 다형성을 활용한 게임 스킬 사용 프로그램(숙제)

4.1 숙제 개요

다형성을 활용하여 다양한 모험가들이 고유의 스킬을 사용하는 프로그램을 구현합니다. 이는 객체지향 프로그래밍(OOP)의 핵심 개념인 상속과 다형성을 실제로 적용하는 예제입니다.


4.2 프로그램 구현

기본 클래스

Adventure라는 기본 클래스를 정의하며, 모든 모험가 클래스의 부모로 사용

class Adventure {
public:
    virtual void useSkill() = 0; // 순수 가상 함수
    virtual ~Adventure() {} // 가상 소멸자
};

파생 클래스

각 직업(전사, 마법사, 궁수)의 스킬을 재정의

class Warrior : public Adventure {
public:
    void useSkill() override { 
        cout << "Warrior uses Slash!" << endl; 
    }
};

class Mage : public Adventure {
public:
    void useSkill() override { 
        cout << "Mage casts Fireball!" << endl; 
    }
};

class Archer : public Adventure {
public:
    void useSkill() override { 
        cout << "Archer shoots an Arrow!" << endl; 
    }
};

다형성 구현

Adventure 포인터를 사용하여 다양한 모험가 객체를 가리키고, 반복문으로 각 객체의 스킬을 호출

int main() {
    // 모험가 객체 생성
    Warrior warrior;
    Mage mage;
    Archer archer;

    // 모험가 리스트 생성
    Adventure* adventurers[] = {&warrior, &mage, &archer};

    // 모든 모험가의 스킬 호출
    for (Adventure* adventurer : adventurers) {
        adventurer->useSkill();
    }

    return 0;
}

4.3 실행 결과

프로그램 실행 시, 각 모험가의 스킬이 호출됨

Warrior uses Slash!
Mage casts Fireball!
Archer shoots an Arrow!

4.4 학습 포인트

  1. 다형성의 이점: 공통된 인터페이스(Adventure)를 통해 코드를 간결하고 확장 가능하게 유지.
  2. 가상 함수 활용: 각 파생 클래스의 고유 동작을 동적으로 호출.
  3. 순수 가상 함수: 추상 클래스를 통해 강력한 구조적 제약 제공.

최종 요약

위의 예제는 다형성을 활용한 객체지향 프로그래밍의 핵심 원리를 잘 보여줍니다. 다양한 객체를 동일한 방식으로 처리할 수 있으며, 코드의 재사용성과 유지보수성을 높입니다. 이를 통해 실무적인 응용 프로그램에서도 구조적 효율성을 높이는 방법을 배웠습니다.

 

 

 

과제 임시 저장

필수 과제:

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() = 0;  // 순수 가상 함수
    virtual ~Animal() {}  // 가상 소멸자
};

class Dog : public Animal {
public:
    void makeSound() override { cout << "Bow Wow" << endl; } //overrisde로 makeSound 재정의
};

class Cat : public Animal {
public:
    void makeSound() override { cout << "Meow Meow" << endl; }
};

class Cow : public Animal {
public:
    void makeSound() override { cout << "Moo Moo" << endl; }
};

int main() {
    Dog dog;
    Cat cat;
    Cow cow;
    
    //다형성
    Animal* animals[] = { &dog, &cat, &cow };
    for (Animal* animal : animals) {  // animals 배열 안에 있는 Animal 타입의 animal의 주소들
        animal->makeSound();
    }
}

 

도전 과제:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

// Animal 기본 클래스
class Animal {
public:
    virtual void makeSound() = 0;  // 순수 가상 함수
    virtual ~Animal() {}  // 가상 소멸자
};

// Dog 클래스
class Dog : public Animal {
public:
    void makeSound() override { cout << "Bow Wow" << endl; }
};

// Cat 클래스
class Cat : public Animal {
public:
    void makeSound() override { cout << "Meow Meow" << endl; }
};

// Cow 클래스
class Cow : public Animal {
public:
    void makeSound() override { cout << "Moo Moo" << endl; }
};

// Zoo 클래스
class Zoo {
private:
    Animal* animals[10];  // 동물 객체를 저장하는 동적 배열
    int animalCount = 0;   // 현재 동물 개수

public:
    // 동물을 동물원에 추가하는 함수
    void addAnimal(Animal* animal) {
        if (animalCount < 10) {
            animals[animalCount++] = animal;  // 동물 객체를 배열에 추가
        } else {
            cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
        }
    }

    // 동물원에 있는 모든 동물의 행동을 수행하는 함수
    void performActions() {
        for (int i = 0; i < animalCount; ++i) {  // 모든 동물 객체에 대해 순차적으로 소리 내기
            animals[i]->makeSound();
        }
    }

    // Zoo 소멸자
    ~Zoo() {
        for (int i = 0; i < animalCount; ++i) {
            delete animals[i];  // 동적 할당된 동물 객체 메모리 해제
        }
    }
};

// 랜덤 동물을 생성하는 함수
Animal* createRandomAnimal() {
    int random = rand() % 3;  // 랜덤 수를 3을 나눈 나머지. 0, 1, 2 중 하나가 생성됨

    // 난수 값에 따라 Dog, Cat, Cow 객체를 동적으로 생성하고 그 주소값(포인터)을 반환
    if (random == 0) {
        return new Dog();
    } else if (random == 1) {
        return new Cat();
    } else {
        return new Cow();
    }
}

int main() {
    srand(static_cast<unsigned int>(time(0))); // 난수 생성, 랜덤 수를 양수로 형 변형

    Zoo zoo;

    // 동물 5마리를 랜덤으로 생성하여 동물원에 추가
    for (int i = 0; i < 5; ++i) {
        Animal* randomAnimal = createRandomAnimal();
        zoo.addAnimal(randomAnimal);  // 동물원에 동물 추가
    }

    // 동물들의 소리와 행동을 실행
    zoo.performActions();

    return 0;
}
더보기

Zoo 클래스

class Zoo {
private:
    Animal* animals[10];  // 동물 객체를 저장하는 동적 배열
    int animalCount = 0;   // 현재 동물 개수

public:
    void addAnimal(Animal* animal) {
        if (animalCount < 10) {
            animals[animalCount++] = animal;  // 동물 객체를 배열에 추가
        } else {
            cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
        }
    }

    void performActions() {
        for (int i = 0; i < animalCount; ++i) {
            animals[i]->makeSound();  // 모든 동물 객체에 대해 순차적으로 소리 내기
        }
    }

    ~Zoo() {
        for (int i = 0; i < animalCount; ++i) {
            delete animals[i];  // 동적 할당된 동물 객체 메모리 해제
        }
    }
};
 
  • Zoo 클래스는 동물들을 관리하는 클래스입니다.
  • animals[10]: 최대 10마리 동물을 저장할 수 있는 Animal* 타입의 고정 크기 배열입니다.
  • animalCount: 현재 동물원에 저장된 동물의 개수를 추적하는 변수입니다. 이를 통해 동물의 추가와 삭제를 관리합니다.

addAnimal 함수

void addAnimal(Animal* animal) {
    if (animalCount < 10) {
        animals[animalCount++] = animal;  // 동물 객체를 배열에 추가
    } else {
        cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
    }
}
  • addAnimal 함수는 동물 객체를 받아 animals 배열에 추가하는 함수입니다.
  • animalCount가 10 미만일 때만 동물을 추가할 수 있도록 조건문을 사용하여 배열 크기를 초과하는 동물이 추가되지 않도록 방지합니다.
  • 동물 객체가 추가될 때마다 animalCount를 증가시켜, 추가된 동물의 개수를 추적합니다.

~Zoo 소멸자

~Zoo() {
    for (int i = 0; i < animalCount; ++i) {
        delete animals[i];  // 동적 할당된 동물 객체 메모리 해제
    }
}
  • ~Zoo() 소멸자는 Zoo 객체가 소멸될 때 호출됩니다. 이때 동물원에 추가된 동물 객체들이 동적으로 생성되었기 때문에 delete를 통해 메모리를 해제합니다.
  • animalCount만큼 반복하여, animals 배열에 저장된 각 동물 객체의 메모리를 해제합니다.

이 코드는 동물 객체를 생성하고, 동물원에 추가하며, 동물들의 소리와 행동을 출력하는 프로그램입니다. 각 동물 클래스는 Animal 클래스를 상속받아 다형성을 구현하며, Zoo 클래스는 동물 객체들을 관리하고 소멸 시 동적 메모리 해제를 통해 메모리 누수를 방지합니다.

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

챕터 2-2 : 템플릿  (1) 2024.12.31
챕터 2-1 : 자원 관리하기  (0) 2024.12.30
C++ 추가 자료  (1) 2024.12.27
챕터 1-2 : Class 개념  (0) 2024.12.24
챕터 1-1 : 프로그래밍 기초  (1) 2024.12.23

챕터 1-2 : Class 개념

언리얼 거토
|2024. 12. 24. 20:00

학습 목표

  • 클래스의 역할 이해
  • 클래스와 객체를 정의하고 활용
  • 접근 제어자(public, private) 사용

 

1. 객체 없이 성적 관리 프로그램

  • 기존 방식은 데이터 노출 및 재사용성 부족 문제 존재
double getAvg(int kor, int eng, int math) { return (kor + eng + math) / 3.0; }
int getMax(int kor, int eng, int math) { return max({kor, eng, math}); }
  • 문제: 과목 추가 시 코드 수정 필요, 데이터가 외부에 노출됨.

 

2. 클래스 정의 및 멤버 구성

  • 클래스 구조
    • 멤버 변수: 데이터를 포함
    • 멤버 함수: 동작 정의
class Student {
    int kor, eng, math;       // 멤버 변수
    double getAvg();          // 멤버 함수
    int getMaxScore();
};

.

3. 클래스 구현

  • 클래스 내부 구현
class Student {
    int kor, eng, math;
    double getAvg() { return (kor + eng + math) / 3.0; }
    int getMaxScore() { return max({kor, eng, math}); }
};

 

  • 클래스 외부 구현
double Student::getAvg() { return (kor + eng + math) / 3.0; }
int Student::getMaxScore() { return max({kor, eng, math}); }

 

4. 접근 제어자

  • public: 외부 접근 가능
  • private: 외부 접근 불가
class Student {
public:
    double getAvg();
    int getMaxScore();
private:
    int kor, eng, math;
};

 

5. Getter와 Setter

  • 사용 이유: private 멤버 접근 및 수정
class Student {
public:
    void setKor(int k) { kor = k; }
    int getKor() { return kor; }
private:
    int kor, eng, math;
};

 

 

6. 생성자(Constructor)

생성자는 객체가 생성될 때 호출되는 특수한 멤버 함수로, 클래스와 동일한 이름을 가지며 반환 타입이 없다. 생성자를 활용해 객체를 다양한 방식으로 초기화할 수 있으며, 이를 **생성자 오버로딩(Constructor Overloading)**이라고 한다.

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

class Car {
    string brand;
    int year;
public:
    Car() : brand("Unknown"), year(0) {} // 기본 생성자
    Car(string b, int y) : brand(b), year(y) {} // 매개변수가 있는 생성자

    void display() {
        cout << "Brand: " << brand << ", Year: " << year << endl;
    }
};

int main() {
    Car car1; // 기본 생성자 호출
    Car car2("Hyundai", 2023); // 매개변수가 있는 생성자 호출

    car1.display();
    car2.display();

    return 0;
}

기본 생성자는 초기값을 설정하며, 매개변수가 있는 생성자는 입력받은 값을 기반으로 멤버 변수를 초기화한다.

 

7. 헤더와 소스 파일 분리

C++에서는 코드 가독성과 유지보수를 위해 클래스 선언과 구현을 분리하는 것이 일반적이다. 헤더 파일(.h)에는 클래스 선언을, 소스 파일(.cpp)에는 해당 클래스의 구현을 작성한다.

예제: Car 클래스 헤더와 소스 파일 분리

  • Car.h (헤더 파일)
#ifndef CAR_H
#define CAR_H

#include <string>
using namespace std;

class Car {
    string brand;
    int year;
public:
    Car();
    Car(string b, int y);
    void display();
};

#endif

 

  • Car.cpp (소스 파일)
#include "Car.h"
#include <iostream>
using namespace std;

Car::Car() : brand("Unknown"), year(0) {}

Car::Car(string b, int y) : brand(b), year(y) {}

void Car::display() {
    cout << "Brand: " << brand << ", Year: " << year << endl;
}

 

  • main.cpp (메인 파일)
#include "Car.h"

int main() {
    Car car1;
    Car car2("Hyundai", 2023);

    car1.display();
    car2.display();

    return 0;
}

위 구조에서는 클래스를 사용하는 코드를 main.cpp에 작성하며, Car.h와 Car.cpp가 각각 선언과 구현을 담당한다.

 

4. 결론

  • 오늘 학습한 주요 내용:
    1. 생성자를 활용한 객체 초기화.
    2. 클래스 설계를 파일로 나누어 재사용성 및 유지보수성을 강화.
    3. 헤더 및 소스 파일 구조를 통한 효율적인 코드 관리.
  • 생성자는 객체의 초기화를 간단하게 하고, 파일 분리 기법은 대규모 프로젝트에서 필수적이다.

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

챕터 2-2 : 템플릿  (1) 2024.12.31
챕터 2-1 : 자원 관리하기  (0) 2024.12.30
C++ 추가 자료  (1) 2024.12.27
챕터 1-3 : 객체지향 프로그래밍  (1) 2024.12.26
챕터 1-1 : 프로그래밍 기초  (1) 2024.12.23