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
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 함수는 컨테이너에서 특정 값을 검색한다. 값을 찾으면 해당 위치를 나타내는 반복자를 반환하며, 찾지 못하면 컨테이너의 끝 반복자를 반환한다.
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;
}
#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;
}
#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;
}