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;
}
| 이름 | 주소 | 값 | 타입 |
| ---- | ------ | ------ | ----- |
| 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 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;
}
가상 함수 사용: 부모 클래스에 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 학습 포인트
다형성의 이점: 공통된 인터페이스(Adventure)를 통해 코드를 간결하고 확장 가능하게 유지.
가상 함수 활용: 각 파생 클래스의 고유 동작을 동적으로 호출.
순수 가상 함수: 추상 클래스를 통해 강력한 구조적 제약 제공.
최종 요약
위의 예제는 다형성을 활용한 객체지향 프로그래밍의 핵심 원리를 잘 보여줍니다. 다양한 객체를 동일한 방식으로 처리할 수 있으며, 코드의 재사용성과 유지보수성을 높입니다. 이를 통해 실무적인 응용 프로그램에서도 구조적 효율성을 높이는 방법을 배웠습니다.
과제 임시 저장
필수 과제:
#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;
}
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 클래스는 동물 객체들을 관리하고 소멸 시 동적 메모리 해제를 통해 메모리 누수를 방지합니다.
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