티스토리 뷰
[C++ 기본] 클래스와 상속, friend, virtual, template 키워드 등
Unikys 2016. 5. 10. 16:42* 이전글
2016/05/06 - [C++ 기본] Hello World 시작
* 면접이 조만간 잡히게 될 것 같아 우선적으로 복습이 필요한 내용인 클래스와 상속에 대해서 문법을 간단하게 살펴보고 hackerrank.com이나 기본 소팅 알고리즘을 구현해보는 것을 할 예정이다.
* 클래스 선언
class Car { private: int wheels; int price; public: void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); };
* 클래스 정의
#include "car.h" void Car::setWheels(int _wheels) { this->wheels = _wheels; } void Car::setPrice(int _price) { this->price = _price; } int Car::getWheels(void) { return this->wheels; } int Car::getPrice(void) { return this->price; }
: 위의 getter와 setter 함수에서 사용하고 있는 this 키워드는 현재 클래스의 인스턴스를 가리키는 포인터를 의미하며, 포인터이기 때문에 포인터의 속성을 접근할 때 사용하는 -> 연산자로 각 속성 변수들에 접근해야 한다.
* 클래스 시험
: 위의 클래스를 시험하기 위하여 아래와 같이 간단한 함수들을 main 함수에서 호출하도록 하면 된다. cap.cpp의 아래에 main 함수를 넣고 시험하는 코드를 짜보자.
#include <iostream>/* 위의 car.cpp 내용 넣기 * (중략) **/ int main(int argc, char* argv[]) { Car myCar; myCar.setWheels(4); myCar.setPrice(10000); std::cout << "My car has " << myCar.getWheels() << " wheels.\nIt's price is " << myCar.getPrice() << " dollars" << std::endl; return 0; }
: 위와 같이 실행하면 아래와 같이 정상적으로 변수들이 설정되고 가져와서 출력하는 것을 확인할 수 있다. 정상 동작을 확인하였다.
* 클래스 생성자
: 이번에는 생성자와 소멸자를 아래와 같이 추가로 정의하였다.
/* Car.h */ class Car { private: int wheels; int price; public: Car(); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); };
: 아래는 car.cpp의 내용이다.
/* car.cpp */ #include <iostream>#include "car.h" Car::Car() { this->wheels = 0; this->price = 0; std::cout << "Instance is created" << std::endl; } Car::~Car() { std::cout << "Instance is deleted" << std::endl; } void Car::setWheels(int _wheels) { this->wheels = _wheels; } void Car::setPrice(int _price) { this->price = _price; } int Car::getWheels(void) { return this->wheels; } int Car::getPrice(void) { return this->price; } int main(int argc, char* argv[]) { Car myCar; std::cout << "My car has " << myCar.getWheels() << " wheels for default." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars for default." << std::endl; myCar.setWheels(4); myCar.setPrice(10000); std::cout << "My car has " << myCar.getWheels() << " wheels." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars." << std::endl; return 0; }
: 위와 같이 생성자와 소멸자를 생성한 다음, 다시 컴파일해서 실행하면 생성자와 소멸자가 자동으로 호출되는 것을 확인할 수 있다. 이렇게 생성자는 초기 값을 설정하는데 사용되며, 소멸자는 클래스 안에서 동적으로 메모리를 할당하는 경우 해제하는 소스를 넣으면 된다.
* 클래스 생성자 오버로딩(Overloading)
/* car.h */ class Car { private: int wheels; int price; public: Car(); Car(int, int); // overriding ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); };
: 그 다음에 car.cpp에 해당 생성자의 초기화 소스 코드를 구현하면 된다.
/* car.cpp */ #include <iostream>
#include "car.h" Car::Car() { this->wheels = 0; this->price = 0; std::cout << "Instance is created" << std::endl; } /* declare the overloading constructor */ Car::Car(int _wheels, int _price) { this->wheels = _wheels; this->price = _price; std::cout << "Instance is created with wheels(" << _wheels << "), price(" << _price << ")" << std::endl; } Car::~Car() { std::cout << "Instance is deleted" << std::endl; } void Car::setWheels(int _wheels) { this->wheels = _wheels; } void Car::setPrice(int _price) { this->price = _price; } int Car::getWheels(void) { return this->wheels; } int Car::getPrice(void) { return this->price; } int main(int argc, char* argv[]) { Car myCar; std::cout << "My car has " << myCar.getWheels() << " wheels for default." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars for default." << std::endl; myCar.setWheels(4); myCar.setPrice(10000); std::cout << "My car has " << myCar.getWheels() << " wheels." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars." << std::endl; Car initCar(6, 20000); return 0; }
: 위와 같이 오버로딩하는 생성자를 정의하고, main 함수에서는 Car 변수를 정의할 때 함수 호출하듯이 정의하면 오버로딩된 생성자를 호출하여 객체를 초기화하게 된다. 위에 대한 실행 결과는 아래와 같이 나온다.
* new, delete 키워드
int main(int argc, char* argv[]) { Car* newCar = new Car(); Car* newInitCar = new Car(8, 30000); delete newCar; delete newInitCar; return 0; }
* 클래스 상속
/* suv.h */ #include "car.h" class SUV : public Car { private: int spareWheels; public: void setSpareWheels(int); int getSpareWheels(void); };
: 위의 경우 보면 car.h를 include 하고 있고, main 함수에서 suv.h를 include하게 되면 전체적으로 : dcar.h는 두 번 로드를 하게 된다. 이렇기 때문에 오류가 일어난다면 한번만 include 될 수 있도록 전처리 단계에서 #ifndef 를 사용하면 좋다.
/* car.h */ #ifndef CAR_H #define CAR_H class Car { private: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); }; #endif
: 위와 같이 CAR_H에 대해서 ifndef로 검사하여 정의되지 않았으면 CAR_H를 정의하면서 car.h 파일의 내용들을 선언하고, 만약 CAR_H가 정의되어있다면, #endif로 넘어가서 재선언을 하지 않고 넘어가게 된다. 그러면 이어서 suv.cpp의 예를 살펴보면 아래와 같다.
#include "suv.h" void SUV::setSpareWheels(int _spareWheels) { this->spareWheels = _spareWheels; } int SUV::getSpareWheels(void) { return this->spareWheels; }
: 그리고 main 함수를 수정하여 해당 클래스가 시험이 될 수 있도록 아래와 같이 구현하였다.
int main(int argc, char* argv[]) { SUV* newSUV = new SUV(); newSUV->setSpareWheels(2); std::cout << "Spare wheels are " << newSUV->getSpareWheels() << std::endl; newSUV->setPrice(5000); std::cout << "The price is " << newSUV->getPrice() << std::endl; delete newSUV; return 0; }
/* suv.h */ #include "car.h" class SUV : public Car { private: int spareWheels; public: void setSpareWheels(int); int getSpareWheels(void); int getTotalWheels(void); };
int SUV::getTotalWheels(void) { return this->spareWheels + this->wheels; }
: 오류 설명을 보면 'Car'의 private 멤버변수인 wheels에 접근하려고 해서 오류가 일어나고 있다. 그러면 여기서 private으로 정의된 변수들을 protected로 바꾸면 오류가 안 일어난다. 아래와 같이 car.h와 시험을 위한 케이스로 car.cpp의 메인 함수를 수정하였다.
/* car.h */ #ifndef CAR_H #define CAR_H class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); }; #endif
: main 함수의 시험 케이스는 아래와 같이 구현하였다. SUV를 생성해서 spareWheels를 설정하고 wheels를 설정한 다음, SUV의 getTotalWheels를 호출해보는 순서대로 이루어진다.
int main(int argc, char* argv[]) { SUV* newSUV = new SUV(); newSUV->setSpareWheels(2); std::cout << "Spare wheels are " << newSUV->getSpareWheels() << std::endl; newSUV->setWheels(4); std::cout << "The wheels are " << newSUV->getWheels() << std::endl; std::cout << "The total number of wheels are: " << newSUV->getTotalWheels() << std::endl; delete newSUV; return 0; }
: 위와 같이 private 변수를 protected로 수정하면 이제 정상적으로 컴파일되고 실행되는 것을 확인할 수 있다.
* 클래스 friend
/* engineer.h */ #include "car.h" class Engineer { public: int getWheelsInCar(Car*); };
/* engineer.cpp */ #include "engineer.h" int Engineer::getWheelsInCar(Car* car) { return car->wheels; }
/* car.h */ #ifndef CAR_H #define CAR_H class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); friend class Engineer; }; #endif
int main(int argc, char* argv[]) {} SUV* newSUV = new SUV(); newSUV->setWheels(4); std::cout << "The wheels are " << newSUV->getWheels() << std::endl; Engineer engineer; std::cout << "There are " << engineer.getWheelsInCar(newSUV) << " wheels in this car" << std::endl; delete newSUV; return 0; }
: 이렇게 private 변수에 접근해야 될 필요성이 있을 때에는 friend 키워드를 사용해서 다른 클래스에 접근 권한을 부여해줄 수 있지만, 이렇게 변수에 직접 접근하도록 하는 것보다는 상속이나 함수를 통해서 변수에 접근할 수 있는 별도의 인터페이스를 제공하는 것이 객체지향 프로그래밍에서는 조금 더 바람직할수도 있다. 그리고 이를 encapsulation이라고도 한다.
* Polymorphism = 다형성
: Polymorphism은 한글로 다형성이라고 하며, 이는 같은 부모의 클래스를 두고 있는 여러 개의 클래스들이 같은 기능을 하는 함수를 오버라이딩해서 밖에서 봤을 때에는 같지만, 내부적으로 동작하는 로직이 다를 때 Polymorphism을 사용한다고 이야기한다. 아래는 간단한 Car를 부모클래스도 사용하는 새로운 함수 Motorcycle 클래스를 추가한 예이다. 먼저 motorcycle.h에서 새로운 클래스를 정의한 예이다. 지금은 사용하지 않을 것이지만, 나중에 beep 함수를 사용할 예정인 beep 함수를 미리 선언 해두었다.
/* motorcycle.h */ #include "car.h" class Motorcycle : public Car { public: void beep(); };
: 그리고 아래는 motorcycle.cpp에서 beep 함수를 아래와 같이 정의하였다.
/* motorcycle.cpp */ #include <iostream>#include "motorcycle.h" void Motorcycle::beep() { std::cout << "BEEP!" << std::endl; }
: 그리고 이를 시험하기 위한 main함수는 아래와 같이 작성하였다. 기존의 SUV와는 조금 다르게 이번에는 new Motorcycle()로 초기화를 하지만, 그 부모의 포인터 형인 Car*로 받고 있는 것을 확인할 수 있다.
int main(int argc, char* argv[]) { SUV* newSUV = new SUV(); newSUV->setWheels(4); std::cout << "The wheels are " << newSUV->getWheels() << std::endl; Car* motorcycle = new Motorcycle(); motorcycle->setWheels(2); std::cout << "The wheels are " << motorcycle->getWheels() << std::endl; delete newSUV; delete motorcycle; return 0; }
: 이렇게 자식 클래스인 Motorcycle 클래스의 인스턴스를 생성하였지만, Car의 포인터로 받을 수 있으며, 이렇게 되면 Car에서 정의한 함수들을 그대로 사용할 수 있다. 이 때에 Car의 포인터이기 때문에 Motorcycle에 있는 함수와 변수들에 대한 정보는 없어서 Motorcycle에 있는 함수, 위에서는 beep 함수에 접근할 수 없다는 것을 참고하면 좋을 것이다. 이에 대한 실행 결과는 아래와 같다.
: 여기서 Car 함수에 beep 함수를 추가해서 Motorcycle의 beep 함수가 오버라이딩하게 하면 아래와 같이 소스에 car.h와 car.cpp에 추가할 수 있을 것이다.
/* car.h */ #ifndef CAR_H #define CAR_H class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); void beep(void); friend class Engineer; }; #endif
: 그리고 car.cpp는 아래와 같이 정의를 추가하면 된다.
void Car::beep(void) { std::cout << "No horne" << std::endl; }
: main 함수에서 beep 함수가 어떻게 호출되는지 학인하기 위하여 아래와 같이 구현하였다. 하나의 Motorcycle 인스턴스는 원래의 Motorcycle 포인터에 할당해주고, 또 다른 인스턴스는 Car*에 할당해주었다.
int main(int argc, char* argv[]) { Car* polyMotorcycle = new Motorcycle(); Motorcycle* motorcycle = new Motorcycle(); polyMotorcycle->beep(); motorcycle->beep(); delete motorcycle; delete polyMotorcycle; return 0; }
: 이렇게 함수를 오버라이딩 했는데 부모 클래스의 포인터로 함수를 호출하면 부모클래스의 함수가 호출되는 현상을 해결하고 자식클래스에 정의된 함수가 호출되도록 도와주는 키워드는 virtual 키워드이다.
* virtual 키워드
/* car.h */ #ifndef CAR_H #define CAR_H class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); virtual void beep(void); friend class Engineer; }; #endif
: 이렇게 하고나서 위의 main 함수를 다시 실행해보면 결과가 바뀌어서 원래 Motorcycle 인스턴스가 생성된대로 Motorcycle 클래스의 beep 함수가 실행되는 것을 확인할 수 있다.
: 지금과 같이 간단한 상황에서는 virtual을 사용해야 하는 이유를 잘 모를 수 있지만, 복잡한 프로그램을 개발할 때 일반적인 말로 "컴포넌트"화 시켜서 모듈들을 관리할 때 이 virtual 키워드를 사용하는 것은 필수일 것이다. 일반적으로 모듈들은 하나의 공통된 부모 클래스를 두고 각각의 종류에 따라서 동작을 특성화시켜서 변경하기 때문에 그럴 때 이 virtual 키워드를 사용하면 되는 것이다.
* 클래스 operator
: 객체지향 프로그래밍을 할 때 자주 사용하는 기능 중 하나는 바로 operator이다. 이 operator는 처음 접하면 잘 모르겠다가도, 일단 익숙해지면 아주 편리해지는 재미있는 기능이다. 가장 많이 사용해야 하는 operator는 아마 = operator로 일반적으로 특정 클래스의 인스턴스에 =이 들어왔을 때 어떻게 동작할지 알려주는 operator 함수이다. 아래와 같이 Car의 = operator를 정의할 수 있다.
/* car.h */ #ifndef CAR_H #define CAR_H class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); virtual void beep(void); Car& operator=(const Car&); friend class Engineer; }; #endif
: 다른 클래스의 operator=을 선언할 때에도 유사하게 하면 된다. 아래와 같이 내부 로직을 car.cpp에서 정의하였다.
Car& Car::operator=(const Car& rightCar) { this->wheels = rightCar.wheels; this->price = rightCar.price; return *this; }
: 위의 소스만 보면 쉽게 이해하기 힘들면 아래의 실제 사용되는 상황을 보고 이해하면 된다.
int main(int argc, char* argv[]) { Car* myCar = new Car(4,10000); Car usedCar = *myCar; std::cout << "Used car has " << usedCar.getWheels() << " wheels, and the price is " << usedCar.getPrice() << std::endl; return 0; }
: 위의 Car usedCar = *myCar로 되어있는 부분을 보면 usedCar가 = 연산자의 왼쪽 객체이고, 위의 operator= 함수가 호출되는 주체이다. 따라서, 위의 operator= 함수 안에서 사용하고 있는 this는 usedCar로 보면 된다. 그리고 rightCar의 인자로 들어오는 Car는 operator=의 오른쪽 객체로 *myCar이다. 여기서 myCar는 wheels는 4로, price는 10000로 초기화 되어서 인자로 넘어간다. 그리고 operator= 함수 안에서 오른쪽의 인자로 넘어온 객체에 대한 멤버변수의 값들을 왼쪽 객체로 옮겨주면 operator= 함수에 대한 처리는 끝난다. 이렇게 operator= 함수는 클래스 객체를 대상으로 =을 사용하게 되면 호출되는 것이다. 다른 유용한 operator 함수는 아마 위의 std::cout에서 자주 쓰고 있는 << operator 일 것이다. 이 operator는 =과는 달리 멤버함수로 오버라이트하는 것이 아니라 외부에 operator<< 함수에서 Car를 인자로 받는 함수에서 friend 키워드로 접근하도록 권한을 부여하여 출력이 가능하도록 할 것이다. 아래는 먼저 car.h에서 friend로 operator<<함수를 선언한 것이다.
/* car.h */ #ifndef CAR_H #define CAR_H #include <iostream>
class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); virtual void beep(void); Car& operator=(const Car&); friend std::ostream& operator<<(std::ostream&, const Car&); friend class Engineer; }; #endif
: ostream의 입출력 값을 가지기 때문에 iostream의 include를 추가하였고, 이 선언은 멤버함수로서 선언한다기 보다는 외부에 있는 operator<< 함수가 멤버변수들에 접근 가능하도록 외부 함수에 friend로 권한을 부여한 것이라고 생각하면 된다. car.cpp에는 아래와 같이 operator<< 함수를 정의하였다.
std::ostream& operator<<(std::ostream& os, const Car& car) { os << "This car has " << car.wheels << " wheels, and the price is " << car.price; return os; }
: 주의 할점은 Car의 멤버함수로 Car::operator<<를 한 것이 아니라 바로 operator<<로 외부에 있는 함수로 정의한 것이다. 왜냐하면 std::cout 등의 ostream에서 사용해야 하기 때문에 외부 함수로 정의한 것이다. 그리고 이를 시험하기 위하여 아래와 같이 main 함수의 로직을 작성하였다.
int main(int argc, char* argv[]) { Car* myCar = new Car(4,10000); Car usedCar = *myCar; std::cout << usedCar << std::endl; delete myCar; return 0; }
: 원래 std::cout 에서 문자열들을 복잡하게 넣었던 내용들이 이제 간단하게 클래스의 인스턴스 자체를 넣고도 출력이 가능하게 되어 개발하는데 있어서 편리하게 활용할 수 있을 것이다. 이렇게 operator에 따라서 =이나 ++과 같은 binary 연산자도 있지만, +나 - 등과 같은 unary 연산자에 대해서 다르게 정의하고 인자를 정의해야 하니 각각에 대해서는 각 operator의 예제를 참고하거나 레퍼런스를 찾아보면 좋다.
* template T 클래스
: template T 클래스는 위의 다형성을 사용하는 경우와는 다르게 특정 클래스가 다른 여러 가지 클래스의 입력을 유동적으로 받아서 관리할 수 있도록 하는 기능을 수행한다. 이에 대하여 다형성과 함께 활용하면 좋기도 하지만, 템플릿 클래스 T는 클래스가 아닌 int나 double 등과 같은 기본형들도 활용할 수 있어서 활용 범위가 조금 더 넓기도 하다. 하지만 소스의 가독성은 조금 떨어질 수도 있어서 정말 필요한 경우가 아니면 많이 사용하는 경우는 없을 수 있다. 이것을 가장 많이 이용하는 경우는 트리나 해쉬맵 등과 같은 데이터구조이고, 일반적으로는 많이 사용하지는 않는다. 그래도 일반적으로 사용되는 경우를 앞서 살펴본 예를 확장해서 보면 아래와 같이 활용할 수 있을 것이다. 예를 들면, 앞서 사용한 Engineer가 다루는 기계를 서로 다른 클래스로 하고자 할 때, Engineer를 template 클래스로 아래와 같이 정의가 가능하다.
/* engineer.h */ #ifndef ENGINEER_H #define ENGINEER_H #include "car.h" templateclass Engineer { private: T mechanic; public: int getWheelsInCar(Car*); void setMechanic(T); T getMechanic(void); }; #endif
: 위와 같이 template을 클래스 상단에 정의하고 해당 템플릿 클래스와 관련된 변수, 함수들의 정의가 가능하다. engineer.cpp에는 아래와 같이 기존의 함수 정의에 template 키워드를 추가하여 정의가 가능하다.
/* engineer.cpp */ #include "engineer.h" templateint Engineer ::getWheelsInCar(Car* car) { return car->wheels; } template void Engineer ::setMechanic(T _mechanic) { this->mechanic = _mechanic; } template T Engineer ::getMechanic(void) { return this->mechanic; };
: 그리고 기존에 Engineer를 friend로 추가했던 Car 클래스도 Engineer가 템플릿 클래스로 변경이 되었으므로 템플릿으로 바꿔줘야 한다. 아래는 수정된 car.h 파일이다.
/* car.h */ #ifndef CAR_H #define CAR_H #include <iostream>class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); virtual void beep(void); Car& operator=(const Car&); friend std::ostream& operator<<(std::ostream&, const Car&); template friend class Engineer; }; #endif
: 이제 준비는 어느 정도 끝났고, Car말고 다른 기계를 다루는 Engineer 클래스를 정의하기 위하여 Computer 클래스를 새로 정의하였다. 아래는 computer.h이다.
/* computer.h */ class Computer { };
: 이제 main 함수에서 시험해보기 위하여 아래와 같이 구현이 가능하다.
int main(int argc, char* argv[]) { Car* myCar = new Car(4,10000); EngineercarEngineer; carEngineer.setMechanic(*myCar); delete myCar; Engineer computerEngineer; Computer myComputer; computerEngineer.setMechanic(myComputer); return 0; }
: 이상이 없어 보이지만, 위의 경우 컴파일을 해보면 링크에러가 일어난다. 왜냐하면 Engineer
#include "engineer.cpp"
: 이렇게 하면 정상적으로 동작하는 것을 확인할 수 있다. 하지만 위와 같이 템플릿을 사용하게 되는 경우에는 일반적으로 T를 이용해서 별도로 부가적인 함수 호출이나 기능 제공을 하기가 어렵고, 앞서 이야기한대로 데이터구조, 데이터 관리, 즉 모델의 관점에서는 유용하게 활용이 가능하나, 비즈니스 로직을 넣기에는 부족함이 많다. 따라서 이러한 템플릿의 사용이 필요한 부분은 대부분 이미 STL 등과 같은 라이브러리에서 제공해주고 있고, 비즈니스 로직에 대한 구현이 필요한 부분은 다형성 정도의 객체지향 개념만 활용해도 충분히 커버 가능할 것이다.
끝.
* 다음편 예고
: 다음에는 C++로 기본적인 자료구조를 구현하는 것과 각 정렬들을 구현해보도록 할 것이고, 그 이후에 다시 C++ 기본에 맞게 문법관련해서 정리할 예정이다.
* 다음편
2016/05/13 - [C++ 응용] 퀵, 머지, 힙, 버블, 선택, 삽입 정렬 알고리즘 구현
2016/05/19 - [C++ 기본] 변수형과 기본 자료 구조(vector, map, set)
- Total
- Today
- Yesterday
- php
- 안드로이드
- 속깊은 자바스크립트 강좌
- Javascript
- google app engine
- 탐론 17-50
- 뽐뿌
- Android
- lecture
- HTML5
- 서울
- 삼식이
- 자바스크립트
- GX-10
- ny-school
- TIP
- java
- mini project
- 팁
- 사진
- gae
- gre
- K100D
- 샷
- Python
- Writing
- 안드로이드 앱 개발 기초
- c++
- HTML5 튜토리얼
- 강좌
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |