C++ TIL day 8

2024. 12. 26. 20:17·C++

1. STL 기초

1. 기본 컨테이너

  •  벡터
    • 기본생성 & 특정값으로 초기화
    • 아래 예시처럼 실행하면, row가 3이고 col이 4이며, 모든 값이 7로 대입된 2차원 벡터가 생성된다.
vector<vector<int>> vec2d(3, vector<int>(4, 7));

 

  • 맵
    • TreeMap(균형잡힌이진트리) 자료구조로 이루어져 있다.
    • Key 순서대로 정렬된다.
    • insert로 삽입하는 경우 중복된 key라면 무시된다. 
    • 그러나 [] 연산자를 사용한 경우 새로운 value로 기존의 value를 덮어쓴다
#include <iostream>
#include <map>

using namespace std;

int main() 
{
    map<int, string> myMap;
    myMap[5] = "E";
    myMap[2] = "B";
    myMap[8] = "H";
    myMap[1] = "A";
    myMap[3] = "C";    
    
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it) 
    {
        cout << it->first << ": " << it->second << endl;
    }

    myMap.insert({ 1, "AA" });
    cout << "-------------\n";
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it)
    {
        cout << it->first << ": " << it->second << endl;
    }

    myMap[1] = "AAA";
    cout << "-------------\n";
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it)
    {
        cout << it->first << ": " << it->second << endl;
    }
    return 0;
}

실행결과

2. 기본 알고리즘

  • sort
    • 사용자 지정 sort
    • 아래 코드는 내림차순으로 정렬하는 방법 
      • "a > b 를 앞에 있는 값이 뒤에 있는 값보다 크다" 라고 생각하기
    • 참고
      • int & 로 매개변수를 받아 불필요한 복사를 방지한다.
      • 또한 const를 붙여 값이 변경되지 않도록 막아준다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool cmp(const int& a, const int& b)
{
	return a > b;
}

int main()
{
	vector<int> vec = { 3,5,1,7,4,10 };
    
	sort(vec.begin(), vec.end());
	for (const auto& v : vec)
		cout << v << " ";
        
	cout << "\n---------------\n";	
    
	sort(vec.begin(), vec.end(), cmp);
	for (const auto& v : vec)
		cout << v << " ";
}

  • find
    • return 값은 iterator이고, 만약 찾지 못하면 end 위치를 return 해준다.
find(시작점, 끝점, 찾고자 하는 값)

3. 반복자

  • 순방향

  • 역방향
    • 기억할만한 것은 rbegin에서 rend로 가는 것도 iterator를 더해주는 방향으로 간다는 것
    • 아래 숙제 코드를 보면 무슨 말인지 알 수 있다.

4. 숙제

#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}
    };

    for (auto it = vec.begin(); it != vec.end(); ++it)
        cout << *it << " ";

    cout << "\n------------------\n";

    for (auto it = vec.rbegin(); it != vec.rend(); ++it)
        cout << *it << " ";

    cout << "\n------------------\n";

    for (auto it = mp.begin(); it != mp.end(); ++it)
        cout << it->first << " " << it->second << "\n";

    cout << "\n------------------\n";

    for (auto it = mp.rbegin(); it != mp.rend(); ++it)
        cout << it->first << " " << it->second << "\n";

    return 0;
}

실행결과

2. 객체지향적 설계

1. 응집도

  • 클래스 내 모듈들이 얼마나 관련되어 있는지를 나타내는 말
  • 응집도가 높아지도록 클래스를 구현하는 것이 중요하다.
  • 예를 들면,
    • printMessage, cacluateSum, reverseString 이라는 세개의 함수가 있고, 여기에 calculateProduct를 추가하는 경우
    • 응집도가 낮은 클래스 설계는 모두 다 같은 Utility 라는 클래스에 들어 있는 경우이다. 
      • 이렇게 구성한 경우 코드에서 에러가 발생하면, 어떤 부분이 잘못된 것인지 알아내기가 어렵고, 
      • 함수가 하나 추가되고, 변수가 하나 추가될 때 마다 클래스가 계속해서 수정되어야 한다.
    • 응집도가 높은 클래스 설계는
      • 기능별로 유사한 함수끼리 묶어서 세 개의 다른 클래스로 만드는 것이다.
        • printMessage 함수를 가지는 MessageHandler 클래스
        • calculateSum과 calculateProduct를 가지는 Calculator 클래스
        • reverseString을 가지는 StringManipulator 클래스 

2. 결합도

  • 클래스와 클래스 간의 결합이 어떠한지를 나타내는 말
  • 결합도가 높을 때
    • 변경사항이 많다면 수정 범위가 커지게 된다.
    • Engine 클래스와 Car 클래스의 결합도가 높다면,
      • 새로운 엔진 클래스(ElectricEngine)를 추가한 다음
      • Car 클래스도 수정을 해야만 사용을 할 수 있게 된다.
  • 예시 코드
class Engine
{
public:
	Engine() :
		state("off")
	{}
	void Start()
	{
		state = "on";
		cout << "Engine Started\n";
	}
	string GetState()
	{
		return state;
	}
private:
	string state;
};

class Car
{
public:
	void StartCar()
	{
		if (engine.GetState() == "off")
		{
			engine.Start();
			cout << "Car Started\n";
		}
	}
private:
	Engine engine;
};

위의 코드에서 ElectricEngine 클래스를 추가하면, 아래처럼 구성할 수 있을 것이고, 만약 Car가 Electric Engine을 가지게 하고 싶다면, Car class 또한 수정해야 한다.

class Engine
{
public:
	Engine() :
		state("off")
	{}
	void Start()
	{
		state = "on";
		cout << "Engine Started\n";
	}
	string GetState()
	{
		return state;
	}
private:
	string state;
};

class ElectricEngine
{
public:
	ElectricEngine() :
		state("off")
	{}
	void Start()
	{
		state = "on";
		cout << "Electric Engine Started\n";
	}

	string GetState()
	{
		return state;
	}
private:
	string state;
};

class Car
{
public:
	void StartCar()
	{
		if (engine.GetState() == "off")
		{
			engine.Start();
			cout << "Car Started\n";
		}
	}
private:
	//Engine engine; // ElectricEngine을 사용하기 위해 바꿔주어야 함
	ElectricEngine engine;
};
  • 결합도가 낮을 때
    • 확장성이 높아지고, 수정의 범위를 작게 할 수 있다. 
    • 인터페이스 클래스(추상 클래스)를 이용하여 구성하는 방법이 있다.
    • 위의 예시와 같은 상황일 때, Engine 클래스를 추상클래스로 구현한다면, 
      • 새로운 엔진을 계속해서 추가하더라도
      • Car 클래스의 수정은 필요하지 않다.
  • 예시 코드
    • 아래처럼 추상 클래스로 인터페이스 클래스를 구현해둔다면, 새로운 Engine 을 추가하더라도, 추가적인 수정없이, 새로운 클래스만 작성하면 된다는 이점이 있다.
    • 이때 unique_ptr을 이용해서 Engine 객체의 수명을 Car와 같도록 구현하였다.
class Engine //추상 클래스
{
public:
	Engine()
		:state("off")
	{}
	virtual ~Engine() = default;
	virtual void Start() = 0;
	string GetState()
	{
		return state;
	}
	void SetState(string st)
	{
		state = st;
	}
private:
	string state;
};

class DiselEngine : public Engine
{
public:
	DiselEngine()		
	{
		cout << "Disel Engine state: ";
		cout << GetState() << "\n";
	}
	void Start() override
	{
		SetState("on");
		cout << "Disel Engine Started :";
		cout << GetState() << "\n";
	}
};

class ElectricEngine : public Engine
{
public:
	ElectricEngine()
	{
		cout << "Electric Engine state: ";
		cout << GetState() << "\n";
	}
	void Start() override
	{
		SetState("on");
		cout << "Electric Engine Started : ";
		cout << GetState() << "\n";
	}
};

class Car
{
public:
	Car(unique_ptr<Engine> eng)
		:engine(move(eng))
	{}
	void StartCar()
	{
		engine->Start();		
		cout << "Car Started\n";
	}
private:
	unique_ptr<Engine> engine;
};

3. SOLID 원칙

  • 객체 지향을 설계할 때 주요한 원칙 5가지를 말한다.
  • SOLID원칙의 목적은 
    • 유지보수성 및 확장성 향상
    • 변경에 유연하게 만들기
  • 단일 책임 원칙(Single Responsibility Principle)
    • 각 클래스는 하나의 책임을 가져야 한다는 원칙
    • 클래스의 역할을 잘 분리해서 변경이 필요한 경우에만 필요한 클래스를 수정하도록 한다.
    • 클래스 별로 각자 클래스에 맞는 작업을 하도록 분리하기
    • 예시
      • Student 라는 클래스에는 Student에 대한 정보만 가지고 있어야 하고 (setName, getName)
      • calculateGrade, displayDetails 등의 함수는 다른 클래스에 정의하는 것이 좋다
#include <iostream>
#include <string>

class Student 
{
public:
    void setName(const std::string& name) 
    {
        this->name = name;
    }

    std::string getName() const 
    {
        return name;
    }

private:
    std::string name;
};

class GradeCalculator 
{
public:
    void calculateGrade(int score) 
    {
        if (score >= 90) 
        {
            std::cout << "Grade: A" << std::endl;
        } else if (score >= 80) 
        {
            std::cout << "Grade: B" << std::endl;
        } else 
        {
            std::cout << "Grade: C" << std::endl;
        }
    }
};

class StudentPrinter 
{
public:
    void displayDetails(const Student& student) 
    {
        std::cout << "Student Name: " << student.getName() << std::endl;
    }
};
  • 개방 폐쇄 원칙 (Open/Closed Principle) -> 가장 기초적(중요)
    • 확장에는 열려있어야 하고, 수정에는 닫혀있어야 한다는 원칙
    • 새로운 클래스를 추가하더라도, 기존의 클래스들은 수정하지 않으면서 사용할 수 있도록 구현하기
    • 대체로 인터페이스를 통해 실현할 수 있다.
    • 예시
      • ShapeManager라는 클래스가 drawShape라는 함수를 가지고 있어서 특정 상황마다 다른 그리는 함수를 구현하도록 구현하는 것이 아닌
      • Shape라는 인터페이스를 만들어서 추가할 새로운 클래스들을 Shape를 상속받아서 구현하도록 한다.
class IShape 
{
public:
    virtual void draw() = 0;
};

class Circle : public IShape 
{
public:
    void draw() 
    {
        // 원 그리기
    }
};

class Square : public IShape 
{
public:
    void draw() 
    {
        // 사각형 그리기
    }
};

class ShapeManager 
{
public:
    void drawShape(IShape& shape) 
    {
        shape.draw();
    }
};
  • 리스 코프 치환 원칙 (Liskov Substitution Principle)
    • 자식 클래스는 부모 클래스에서 기대되는 행동을 보장해야 한다는 원칙
    • 부모 클래스 객체를 자식 클래스 객체로 대체해도, 프로그램의 동작이 바뀌지 않아야 한다.
    • 예시1
      • Rectangle 클래스가 있고, Square 클래스가 이를 상속받은 형태로 구현한다면
      • Rectangle에 있는 setWidth나 setHeight 함수는 Square에서 기대하는 행동을 보장하지 못하게 된다.
      • 그렇기 때문에, Shape라는 인터페이스를 만들고 함께 공유되는 특성인 getArea 함수만 가지도록 구현하는 것이 좋다.
    • 예시2
      • Bird 클래스가 있고, Bird에 Fly라는 함수가 있을 때, Sparrow, Penguin 클래스를 Bird 클래스를 상속하여 만들었다면, Penguin은 Fly함수를 오버라이드 할 수 없기 때문에
      • FlightlessBird라는 새로운 인터페이스를 정의하고, Penguin은 Bird가 아닌 FlightlessBird를 상속한다면 LSP 원칙을 지킬 수 있다.
  • 예시1의 코드 예제
#include <iostream>

class Shape 
{
public:
    virtual int getArea() const = 0;
};

class Rectangle : public Shape 
{
public:
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
    int getWidth() const { return width; }
    int getHeight() const { return height; }
    int getArea() const override { return width * height; }

private:
    int width = 0;
    int height = 0;
};

class Square : public Shape 
{
public:
    void setSide(int s) { side = s; }
    int getSide() const { return side; }
    int getArea() const override { return side * side; }

private:
    int side = 0;
};

void testShape(Shape& shape) 
{
    std::cout << "Area: " << shape.getArea() << std::endl;
}

int main() 
{
    Rectangle rect;
    rect.setWidth(5);
    rect.setHeight(10);
    testShape(rect); // Area: 50

    Square square;
    square.setSide(7);
    testShape(square); // Area: 49
    return 0;
}
  • 인터페이스 분리 원칙 (Interface Segregation Principle)
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙
    • 쉽게 말해서 불필요한 메서드를 구현하지 말자는 의미
    • 예시
      • Machine이라는 클래스를 만들 때, Print라는 함수와 Scanner라는 함수를 만들면 이를 상속받은 자식들을 구현할 때 Print와 Scanner를 둘 다 구현해야 한다.
      • 그러나 필요하지 않은 경우가 있을 수 있지만, 강제로 구현을 해야하는 경우가 생긴다.
      • 이를 해결하기 위해서는 Printer와 Scanner를 각각 구현한 후 두가지 동작을 모두 하는 클래스를 새롭게 추가하여 구현하는 방식이 있다.
class Printer 
{
public:
    virtual void Print() const = 0;
    virtual ~Printer() = default;
};

class Scanner 
{
public:
    virtual void Scan() const = 0;
    virtual ~Scanner() = default;
};

class BasicPrinter : public Printer 
{
public:
    void Print() const override 
    {      
    }
};

//포인터를 이용한 방식
class MultiFunctionDevice 
{
private:
    Printer* printer;
    Scanner* scanner;

public:
    MultiFunctionDevice(Printer* p, Scanner* s) : printer(p), scanner(s) {}

    void Print() 
    {
        if (printer) 
            printer->Print();
    }

    void scan() 
    {
        if (scanner) 
            scanner->Scan();
    }
};

//상속을 이용한 방식
class MultiFunctionDevice : public Printer, public Scanner
{
public:
    void Print() const override
    {
        //print   
    }
    void Scan() const override
    {
        //scan        
    }
};

 

  • 의존 역전 원칙 (Dependency Inversion Principle)
    • 고수준 모듈(인터페이스 클래스)은 저수준 모듈(인터페이스를 구현하는 클래스)에 의존하지 않고 둘 다 추상화에 의존해야 한다는 원칙
    • 의존성을 줄이고, 모듈 간 결합도를 낮추는 것이 목적이다.
    • 강한 결합으로 구현된 코드의 예시
      • 아래 코드의 문제점은 Computer 클래스는 무조건 Keyboard랑 Monitor를 가지고 있어야 한다는 것이다.
#include<string>

class Keyboard {
public:
    std::string getInput() 
    {
        return "입력 데이터";
    }
};

class Monitor 
{
public:
    void display(const std::string& data) 
    {
        // 출력
    }
};

class Computer 
{
    Keyboard keyboard;
    Monitor monitor;

public:
    void operate() 
    {
        std::string input = keyboard.getInput();
        monitor.display(input);
    }
};
  • 약한 결합으로 구현된 코드의 예시
#include<string>

class InputDevice 
{
public:
    virtual std::string GetInput() = 0;
    virtual ~InputDevice() = default;
};

class OutputDevice 
{
public:
    virtual void Display(const std::string& data) = 0;
    virtual ~OutputDevice() = default;
};

class Keyboard : public InputDevice 
{
public:
    std::string GetInput() override 
    {
        return "키보드 입력 데이터";
    }
};

class Monitor : public OutputDevice 
{
public:
    void Display(const std::string& data) override 
    {
        // 화면에 출력
    }
};

class Computer 
{
private:
    InputDevice* inputDevice;
    OutputDevice* outputDevice;

public:
    Computer(InputDevice* input, OutputDevice* output)
        : inputDevice(input), outputDevice(output) {}

    void Operate() 
    {
        std::string data = inputDevice->GetInput();
        outputDevice->Display(data);
    }
};
  • 결론
    • SOLID 원칙이 인터페이스를 어떻게 잘 구현해서 사용해야 하는지에 대한 내용이다.
    • 그렇게 함으로써 유지보수와 확장이 쉬운 프로그램을 만드는 것이 목적이다.

4. 숙제

  • 숙제1: SOLID원칙을 적용한 클래스 구현
    • 참고사항) const 키워드
      • GetName, GetAge, GetInfo 와 같이 print만 하고, 멤버 변수의 수정을 하지 않는 함수들은 꼭 뒤에 const 붙여주는 것이 좋다.
      • 추가적으로 앞에 const를 붙인다면, 반환 값을 상수로 지정한다. 즉, 반환된 값을 수정할 수 없도록 만든다. 
#include <iostream>
#include <string>
using namespace std;

//single responsibility principle
class Student
{
public:
	Student(string studentName, int studentAge)
		:name(studentName)
		,age(studentAge)
	{		
	}
	string GetName() const
	{
		return name;
	}
	int GetAge() const
	{
		return age;
	}	
	const string GetInfo() const
	{
		return "학생 이름: " + name + "\n학생 나이: " + to_string(age);
	}
private:
	string name;
	int age;
};

class StudentPrinter
{
public:
	void Print(const Student& student)
	{
		cout << student.GetInfo() << "\n";
	}
};

int main()
{
	Student student("John Doe", 20);
	StudentPrinter pr;

	pr.Print(student);
}
  • 숙제2: TO-DO LIST 구현
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Task
{
public:
	Task(const string& desc)
		:description(desc)
		,completed(false)
	{}
	void Complete()
	{
		completed = true;
	}
	string GetDescription() const
	{
		return description;
	}
	bool IsCompleted() const
	{
		return completed;
	}
private:
	string description;
	bool completed;
};

class IStorage
{
public:
	virtual void AddTask(Task _task) = 0;
	virtual vector<Task> GetTasks() const = 0;
	virtual string GetStorageType() const = 0;	
	virtual ~IStorage() = default;
};

class MemoryStorage :public IStorage
{
public:
	void AddTask(Task _task) override
	{
		tasks.push_back(_task);
	}
	vector<Task> GetTasks() const override
	{
		return tasks;
	}
	string GetStorageType() const override
	{
		return "메모리 저장소";
	}

private:
	vector<Task> tasks;
};

class DBStorage :public IStorage
{
public:
	void AddTask(Task _task) override
	{
		cout << "DB에 할 일 추가: " << _task.GetDescription() << "\n";
		tasks.push_back(_task);
	}
	vector<Task> GetTasks() const override
	{
		cout << "DB에서 할 일 가져오기\n";
		return tasks;
	}
	string GetStorageType() const override
	{
		return "DB 저장소 (시뮬레이션)";
	}

private:
	vector<Task> tasks;
};

class TaskManager
{
public:
	TaskManager(IStorage* _storage)
		:storage(_storage)
	{}
	void AddTask(string description)
	{
		storage->AddTask(Task(description));
	}

	void ShowTasks()
	{
		cout << "저장 방식: " << storage->GetStorageType() << "\n";

		vector<Task> tasks = storage->GetTasks();
		for (size_t i = 0; i < tasks.size(); ++i)
		{
			cout << i + 1 << ". " << tasks[i].GetDescription();
			if (tasks[i].IsCompleted())
				cout << " [완료]";
			cout << "\n";
		}
	}

	void CompleteTask(size_t index)
	{
		vector<Task> tasks = storage->GetTasks();
		tasks[index - 1].Complete();
		storage->AddTask(tasks[index - 1]);
	}

private:
	IStorage* storage;
};

int main()
{
	cout << "=== MemoryStorage로 작업 ===\n";
	MemoryStorage memoryStorage;
	TaskManager manager1(&memoryStorage);

	manager1.AddTask("C++ 과제 작성하기");
	manager1.AddTask("SOLID 원칙 공부하기");

	cout << "\n현재 할 일 목록:\n";
	manager1.ShowTasks();

	manager1.CompleteTask(1);

	cout << "\n업데이트된 할 일 목록:\n";
	manager1.ShowTasks();

	cout << "\n=== DBStorage로 작업 ===\n";
	DBStorage dbStorage;
	TaskManager manager2(&dbStorage);

	manager2.AddTask("DB 작업 테스트");
	manager2.AddTask("To-Do 목록 추가");

	cout << "\n현재 할 일 목록:\n";
	manager2.ShowTasks();

	manager2.CompleteTask(2);

	cout << "\n업데이트된 할 일 목록:\n";
	manager2.ShowTasks();

	return 0;
}

실행 결과

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

C++ 코딩 스탠다드  (2) 2024.12.30
C++ TIL day 9  (3) 2024.12.27
Smart Pointer 보충  (0) 2024.12.24
C++ TIL day 7  (2) 2024.12.24
C++ 상속  (0) 2024.12.23
'C++' 카테고리의 다른 글
  • C++ 코딩 스탠다드
  • C++ TIL day 9
  • Smart Pointer 보충
  • C++ TIL day 7
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (184)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (27)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 과제용 깃허브
    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    gamestate
    enhanced input system
    addonscreendebugmessage
    map을 vector로 복사
    motion matching
    blend pose
    const
    character animation
    BFS
    싱글턴
    additive animation
    매크로 지정자
    DP
    Vector
    상속
    템플릿
    applydamage
    cin함수
    C++
    actor 클래스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
C++ TIL day 8
상단으로

티스토리툴바