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를 붙인다면, 반환 값을 상수로 지정한다. 즉, 반환된 값을 수정할 수 없도록 만든다.
- 참고사항) 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 |