C++ 수업을 시작한지 5일차이다. 첫 과제는 프로그래밍 문법을 활용한 과제로 금방한 것 같은데 두 번째 과제는 C++의 꽃, 객체지향을 이해하고 있어야해서 공부하는데 많은 시간이 소모되었다. (크리스마스도 15시간 정도 공부만 한 것 같다.) 그래도 시간이 모자랐던 것 같다. 이제 클래스가 무엇인지, 객체, 인스턴스 그리고 클래스선언, 생성자, 소멸자 등 다 이해는 하는데 아직 빌드를 많이 해보지 않아서 그런지 아직 헷갈리는 부분도 많고 힘들었다. 아마 5일간 포인터, 배열등 프로그래밍의 '벽'이라고 할 수 있는 부분도 써야하고 객체지향도 뭔가 개념이 쉽게 잡히지 않아서 더 그런 것 같다. 근 5일동안 너무 지식이 한꺼번에 들어와서 그런가.. 휘발성 메모리(RAM) 마냥 머리속에 쌓이지 못하고 쉽게 날라가는 기분이였다...
그래도 최선을 다해 과제를 한 만큼 그동안 작성했던 코드들을 해석해보자
우선 과제는 다음과 같다.
일단 첫날 이것을 보았을 땐 내 머리속은 '???' 였다. 일단 용어도 거의 모르는 용어로 도배되어 있을 뿐더러 클래스는 RPG의 도적,법사,전사 이런거 인줄 알았지 프로그래밍에서의 클래스는 도저히 무엇인지 감을 잡지 못하였다. 그래서 열심히 공부를 하게되었고 우선 내가 알아본 바에 의하면
그래서 나는 바로 클래스를 만들어 보았다.
class Animal {
public:
// 생성자
Animal() {
cout << "Animal 생성자" << endl;
}
// 순수 가상 함수: 자식 클래스에서 반드시 구현해야 합니다.
virtual void makeSound() = 0;
// 가상 소멸자: 자식 클래스 소멸자가 올바르게 호출되도록 합니다.
virtual ~Animal() {
cout << "Animal 소멸자" << endl;
}
};
나중에 만들 Cat, Dog, Cow
의 부모 클래스인 Animal
이다. class (이름) { };
으로 선언하고 내요은 public, protected, privated 에 따라 접근을 달리 한다.
public
: 클래스, 자식 클래스, 외부 함수등 어디서든 접근가능protected
: 클래스, 상속 받은 자식 클래스 내에서만 접근 가능private
: 클래스 내에서만 접근가능클래스에는 객체가 생성될 때, 소멸될 때 호출되는 특별한 함수가 있는데 생성자와 소멸자이다. 각각 초기상태를 설정하거나, 할당된 메모리를 해제하는 일 등을 한다. 함수 이름은 반드시 클래스 이름과 같고 소멸자는 클래스 이름앞에 '~'를 붙인다. 둘다 반환형은 없다.
이제 가운데에 virtual void makeSound() = 0;
이 있다. 이건 순수 가상 함수로써 virtual
이 앞에 붙으면 가상함수, 그 값을 0을 주면 순수 가상함수가 된다. 순수 가상함수는 부모 클래스에서 구현이 안된다. 그래서 반드시 자식클래스에서 구현해야 한다. 또한 가상함수로써 자식클래스에서 오버라이드(덮어쓰기)를 할 수 있다. 자식클래스에서 같은 함수 시그니처로 부모와는 다르게 구현되는 함수를 오버라이드 한다고 한다.
이제 부모클래스를 봤으니 아래 자식클래스들을 보자.
class Dog : public Animal {
public:
Dog() {
cout << "Dog 생성자" << endl;
}
void makeSound() override {
cout << "왈! 왈!" << endl;
}
~Dog() {
cout << "Dog 소멸자" << endl;
}
};
class Cat : public Animal {
public:
Cat() {
cout << "Cat 생성자" << endl;
}
void makeSound() override {
cout << "냥~ 냥~" << endl;
}
~Cat() {
cout << "Cat 소멸자" << endl;
}
};
class Cow : public Animal {
public:
Cow() {
cout << "Cow 생성자" << endl;
}
void makeSound() override {
cout << "음머~ 음머~" << endl;
}
~Cow() {
cout << "Cow 소멸자" << endl;
}
};
솔직히 자식클래스에서는 크게 어렵거나 볼만한 요소는 없다. 클래스 이름 : pulic Animal 하여 상속받고 makeSound 함수를 오버라이드 하였다.
class Zoo {
private:
Animal* animals[10]; // 동물 객체를 저장하는 포인터 배열
int size; // 동물의 수
public:
Zoo() : size(0) {}
// 동물 객체를 zoo에 추가하는 함수
void addAnimal(Animal* animal) {
if (size >= 10) {
cout << "Zoo is full!" << endl;
return;
}
animals[size++] = animal;
}
// 동물원에 있는 모든 동물의 소리와 행동을 출력하는 함수
void performActions() {
for (int i = 0; i < size; i++) {
animals[i]->makeSound();
}
}
// Zoo 소멸자: 동물 객체 메모리 해제
~Zoo() {
for (int i = 0; i < size; i++) {
delete animals[i];
}
}
};
이 부분이 가장 작성하기 힘들었던 부분이다. 우선 이 Zoo라는 클래스는 동물들의 객체를 관리한다. 위에서 부터 한줄 한줄 정리해보면
Animal* animals[10];
Zoo() : size(0) {}
void addAnimal(Animal* animal)
animals[size++] = animal;
이 핵심인데 만약 size가 0이라면 animals[0] (동물 객체를 저장하는 포인터 배열) 도 빈 공간이게 된다. 여기서 animal
은 함수 매개변수로 전달된 동물 객체의 포인터이다. 따라서 전달받은 포인터를 배열의 현재 위치 size
에 저장하는 것이다. 결과적으로, 배열의 size
번째 칸에 동물 객체의 주소를 저장한다.size++
size는 동물의 수를 추적하는 변수 이다. 후위 연산자이므로 size = 0
일때 animals[0] = animal;
수행 후 size
가 1 증가한다. 다음 동물 객체를 추가하면 animals[1]
에 저장되는 식.
addAnimal
함수에 전달하면 함수가 호출이 되고 전달 받은 포인터 (animal)
를 animals
배열의 현재 size
인덱스에 저장한다. animals[i] -> makeSound();
->
처음보는 연산자가 있는데 , animals[i]가 가리키는 객체의 주소를 통해 그 객체의 멤버함수를 호출하겠다는 뜻이다. 약간 .
연산자와 비슷한데 .
연산자는 객체의 멤버에 직접 접근할 때 사용하고 ->
연산자는 포인터가 가리키는 객체의 멤버에 접근할 때 사용한다. 따라서 for
문을 통해 animals[i]
에 저장 되어 있는 모든 동물 객체의 makeSound
함수를 수행하게 한다. delete animals[i];
(new)
되었기 때문에 , 힙에 저장된다. 따라서 메모리를 수동으로 해제해야 한다. 따라서 for
문을 통해 저장되어있는 동물 객체 모두 순차적으로 해제 하도록 적어주었다. delete animals[i]
Animal* createRandomAnimal() {
int random = rand() % 3; // 0, 1, 2 중 랜덤 선택
if (random == 0) {
return new Dog();
}
else if (random == 1) {
return new Cat();
}
else {
return new Cow();
}
}
우선 실행을 하면 일정하게 출력하는 것이 아니라 랜덤으로 동물 객체를 반환하는 함수를 구현하라고 적혀있었기 때문에 구현해보았다. 사실 이전에는 한번도 랜덤함수를 구현을 해본적이 없어서 인터넷을 통해 공부하여 적용시켜 보았다. (컴퓨터가 완벽한 난수는 생성못한다는게 매우 충격이였던... )
우선 랜덤함수를 구현하기 위해선 rand()
와 srand()
를 사용해서 난수 생성을 하여야 한다.
rand()
함수는 C 라이브러리에서 제공하는 키워드이며 기본적으로 0 ~ RAND_MAX
(32767)사이의 값을 반환한다.
srand()
함수는 난수 생성기의 초기값(seed)를 설정한다. 동일한 seed를 사용하면 항상 동일한 난수 시퀀스가 생성되기 때문에 일반적으로는 time(0)
을 사용하여 현재시간을 seed로 설정한다.
이렇게 하더라도 난수 품질이 안좋아서 최근에는 현대적인 난수 생성기인 <random>
헤더를 사용하는 방법이 있지만 아직 잘모르므로 이렇게 설정하였다. 나중에 한번 알아보고 설정해보고 싶다.
이러한 사실을 기반으로 살펴보면 이따가 알겠지만 메인함수 시작할 때 부터srand(time(0));
로 랜덤 시드를 초기화 하였다. 따라서 매 시간 마다 다른 값을 rand()
이 반환하므로 rand() % 3
의 결과는 0, 1, 2 중 랜덤하게 계속해서 나오게 된다. (실행할 때 마다 달라짐) 따라서 여기에 있는 값 3개중 하나씩 자식 클래스 Dog, Cat, Cow에 각각 부여해서 객체를 동적으로 생성하게 하였다.
int main() {
srand(time(0)); // 랜덤 시드 초기화
Zoo zoo1;
// 동물 랜덤 생성 및 추가
for (int i = 0; i < 5; i++) {
zoo1.addAnimal(createRandomAnimal());
}
// 동물원에 있는 모든 동물들의 소리 출력
zoo1.performActions();
// Zoo 소멸자가 동적 할당된 메모리를 자동으로 해제합니다.
return 0;
}
항상 main 함수가 엄~~~청 길어진 모습만보다가 클래스를 만들고 함수를 밖에서 선언하고 하니깐 매우 간단해졌다. 우선 랜덤 시드 초기화를 먼저 해주고
클래스 Zoo의 객체 zoo1을 생성해준다.(Zoo zoo1;
)
그리고 for 을 통해 동물 객체를 5개만 생성했다. 많이 생성하고 싶으면 자유롭게 조절가능하다 (앞에서 동적 할당을 했으니깐!! 대신 10개보다 많게하려면 Zoo 클래스에서 Size 멤버 변수를 건드려야 한다. 동물 객체가 꽉 차면 자동으로 사이즈를 늘리는 방향도 있겠지만 그러려면 이중 포인터를 써야하는데 그쪽 개념을 알긴하지만 아직 익숙치 않아서 조금 더 나한테 맞는 방향으로 코드를 작성하였다.
이제 모든 코드를 합친 완성된 코드는 다음과 같다.
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
class Animal {
public:
// 생성자
Animal() {
cout << "Animal 생성자" << endl;
}
// 순수 가상 함수: 자식 클래스에서 반드시 구현해야 합니다.
virtual void makeSound() = 0;
// 가상 소멸자: 자식 클래스 소멸자가 올바르게 호출되도록 합니다.
virtual ~Animal() {
cout << "Animal 소멸자" << endl;
}
};
class Dog : public Animal {
public:
Dog() {
cout << "Dog 생성자" << endl;
}
void makeSound() override {
cout << "왈! 왈!" << endl;
}
~Dog() {
cout << "Dog 소멸자" << endl;
}
};
class Cat : public Animal {
public:
Cat() {
cout << "Cat 생성자" << endl;
}
void makeSound() override {
cout << "냥~ 냥~" << endl;
}
~Cat() {
cout << "Cat 소멸자" << endl;
}
};
class Cow : public Animal {
public:
Cow() {
cout << "Cow 생성자" << endl;
}
void makeSound() override {
cout << "음머~ 음머~" << endl;
}
~Cow() {
cout << "Cow 소멸자" << endl;
}
};
class Zoo {
private:
Animal* animals[10]; // 동물 객체를 저장하는 포인터 배열
int size; // 동물의 수
public:
Zoo() : size(0) {}
// 동물을 동물원에 추가하는 함수
void addAnimal(Animal* animal) {
if (size >= 10) {
cout << "Zoo is full!" << endl;
return;
}
animals[size++] = animal;
}
// 동물원에 있는 모든 동물의 소리와 행동을 출력하는 함수
void performActions() {
for (int i = 0; i < size; i++) {
animals[i]->makeSound();
}
}
// Zoo 소멸자: 동물 객체 메모리 해제
~Zoo() {
for (int i = 0; i < size; i++) {
delete animals[i];
}
}
};
Animal* createRandomAnimal() {
int random = rand() % 3; // 0, 1, 2 중 랜덤 선택
if (random == 0) {
return new Dog();
}
else if (random == 1) {
return new Cat();
}
else {
return new Cow();
}
}
int main() {
srand(time(0)); // 랜덤 시드 초기화
Zoo zoo1;
// 동물 랜덤 생성 및 추가
for (int i = 0; i < 5; i++) {
zoo1.addAnimal(createRandomAnimal());
}
// 동물원에 있는 모든 동물들의 소리 출력
zoo1.performActions();
// Zoo 소멸자가 동적 할당된 메모리를 자동으로 해제합니다.
return 0;
}