ex) GeometricObject라는 기본 클래스와 이 클래스에서 파생된 Circle, Ractangle 2개의 파생 클래스가 있을 때, GeometricObject가 상위 유형, Circle, Ractangle이 하위 유형에 속한다.
기본 클래스 유형(상위 유형)의 매개변수에 파생클래스(하위 유형)의 인스턴스를 항상 전달할 수 있다. 하지만 그 반대는 불가능하다. ex) 모든 Circle은 GeometricObject에 속하지만, GeometricObject의 일부만 Circle에 속하기 때문.
가상 함수와 동적 결합
가상 함수는 시스템이 객체의 실제 유형에 기초하여 실행 시 어느 함수를 호출할지 결정할 수 있도록 한다.
가상 함수 사용은 [virtual] 키워드를 사용하여 함수를 선언하면 됩니다. ex) virtual string toString();
위의 virtual 키워드로 선언된 함수를 파생 클래스에서 재정의 할 수 있으며, 컴파일러는 실행 시에 어떤 함수 구현을 사용할 것인지를 동적으로 결정합니다. 이렇게 재정의 하는 것을 함수 오버라이딩이라 하고, 동적으로 결정하는 것을 동적 결합이라 합니다.
동적 결합의 조건은 1. 함수는 기본 클래스에서 virtual로 정의되어야 한다. 2. 객체를 참조하는 변수는 가상 함수에서 참조에 의해 전달되거나 포인터로서 전달되어야 한다.
만약 전달할 때 참조나 포인터로 전달하지 않고 값에 의해 전달된다면, 전달된 객체의 함수가 작동하는게 아닌, 기본 클래스의 함수가 작동하게 됩니다. ex) virtual string toString() 함수를 사용할 때 참조 or 포인터 : Geometric, Circle, Rectangle 값 : Geometric, Geometric, Geometric (Geometric이 기본 클래스, Circle, Rectangle이 파생 클래스)
기본 클래스에서 virtual로 정의된 경우, 그 기본 클래스의 모든 파생 클래스에서 자동으로 virtual이 된다.
가상 함수는 기본적으로 실행 시에 함수 구현을 동적으로 결합하며 시간과 시스템 자원을 기본 함수를 쓸 때보다 더 많이 사용하게되기 때문에 함수를 파생 클래스에서 재정의하지 않는다면 가상 함수를 선언하지 않는 것이 더 효과적이다.
protected 키워드
protected 키워드는 public, private와 비슷한 키워드로, 클래스에서 사용하는 키워드입니다.
public은 객체 선언만 하면 접근이 가능하고, private는 객체를 선언해도 접근이 불가능하지만, protected는 파생 클래스에서 접근이 가능합니다.
public, private, protected 키워드는 클래스와 클래스 멤버에 접근할 수 있는지를 명시하므로, 가시성(visibility) or 접근성(accessibility) 키워드라고 합니다. 기본적으로 private, protected, public 순으로 가시성이 증가합니다.
순수 가상 함수와 추상 클래스
순수 가상 함수(pure virtual function)은 추상 함수(abstract function)라고 하기도 하며, 해당 함수를 포함하는 클래스는 추상 클래스가 됩니다.
순수 가상 함수는 위 가상 함수의 예시 virtual string toString();에서 = 0를 추가하면 됩니다. ex) virtual string toString() = 0;
순수 가상 함수는 기본 클래스에서 함수의 몸체나 구현 내용이 없고, 파생 클래스에서 재정의해서 사용합니다.
해당 함수를 포함하는 추상 클래스는 객체를 생성하는 데 사용될 수 없지만, 제네릭 프로그래밍이 가능하도록 함수에서 매개변수에 대한 데이터 유형으로 추상 클래스를 사용할 수 있습니다.
- 기본 클래스 유형의 객체를 파생 클래스 유형으로 형변환하기 위해서 static_cast 와 dynamic_cast 연산자를 사용할 수 있으며, static_cast는 컴파일 시에, dynamic_cast는 실행 유형 확인을 위해 실행 시에 수행됩니다. dynamic_cast의 경우 다형성 유형(즉, 가상 함수를 포함하는 유형)에 대해서만 수행될 수 있습니다.
객체지향 프로그래밍에서는 기존 클래스로부터 새로운 클래스를 정의할 수 있는데, 이를 클래스 상속이라 합니다.
여기서, 새로운 클래스는 파생 클래스, 자식 클래스, 하위 클래스라 하고, 기존 클래스를 기본클래스, 부모 클래스, 상위 클래스라고 합니다.
파생 클래스의 객체는 기본 유형 매개변수의 객체가 요구되는 곳이면 어디든지 전달될 수 있습니다. 그러면 객체 인수의 넓은 범위에서 함수를 포괄적으로 사용할 수 있게 되는데, 이를 제네릭 프로그래밍이라 합니다.
형식은 class [파생 클래스 이름]: public [기본 클래스 이름]의 형식을 띄고있다.
생성자와 소멸자의 경우, - 생성자는 자신의 코드를 실행하기 전에 먼저 기본 클래스의 생성자를 호출한다. (기본 -> 파생) -> 생성자 연쇄적 처리 - 소멸자는 파생 클래스의 소멸자가 자신의 코드를 실행하고 나서 기본 클래스의 소멸자를 자동으로 호출한다. (파생 -> 기본) -> 소멸자 연쇄적 처리
기본 클래스에 정의한 함수를 파생 클래스에서 재정의 할 수 있으며, 재정의된 함수는 기본 클래스에 있는 함수와 동일한 서명을 갖고 반환 유형이 같아야 합니다.
함수와 클래스에서 유형을 매개변수화하는 기능으로, 컴파이럴에 의해 구체적 유형으로 대체될 수 있는 제네릭 유형입니다.
형식은 template<typename T>입니다.
아래 예시와 같이 왼쪽에 여러개로 오버로딩된 maxValue 함수들이 오른쪽의 템플릿을 사용한 하나의 maxValue로 간략하게 포현할 수 있습니다.
템플릿의 대표적인 예시.
클래스 템플릿은 클래스의 이름 위에 template<typename T>라고 작성하면 되며, 만약 .h와 .cpp 파일로 나눈다면 .h에만 template<typename T>를 작성하면 되고, .cpp 파일에서 범위 지정 연산자 앞에있는 클래스 이름에 <T>를 붙여야됩니다. ex) Stack<T>::push
template<typename T = dataType> 의 형식으로 default dataType를 정할 수 있습니다. 또한, template<typename T, int capacity> 의 capacity처럼 비형식 매개변수도 사용할 수 있습니다. 해당 템플릿의 예시는 Stack<std::string str, 500> 이런식으로 사용할 수 있습니다.
- 벡터(vector)
벡터는 배열처럼 사용할 수 있으며, 벡터의 크기가 필요할 경우 자동으로 증가시킬 수 있습니다.
vector<elementType> vectorName의 형식으로 선언할 수 있습니다.
vector도 string처럼 하나의 객체입니다. 그렇기 때문에 #include <vector>를 입력해야 사용할 수 있습니다.
일반 배열처럼 사용가능하며, 별도의 함수들도 존재합니다. ex) size(), push_back(), pop_back() etc...
만약 크기가 고정된다면 배열이 더 효율적이지만, 크기가 유동적이라면 vector가 더 효율적입니다.
- 포인터는 다른 변수의 메모리 주소를 저장하는 변수입니다. 기본적으로 주소를 저장하기 때문에 4byte를 사용합니다.
- 선언은 dataType* pVarName 의 형식을 사용합니다.
- 포인터는 다른 변수의 메모리 주소를 저장하기 때문에 주소를 반환하는 & 주소 연산자를 사용합니다.
- 포인터 변수는 * 간접 연산자를 사용하여 역참조를 할 수 있습니다. 방법은 (*pVarName)입니다.
- 배열 이름은 실제로 배열의 시작 주소를 나타냅니다. ex) list[6]의 list == 배열의 시작 주소.
- this 포인터는 호출 객체 자신을 가리킵니다. 보통 클래스의 숨겨진 데이터 필드를 참조할 때 사용합니다.
typedef - 기존의 데이터 유형에 대한 새로운 동의어를 정의하는 구문입니다. ex) typedef int* intPointer; intPointer p; == int* p;
동적 영구 메모리 할당
- new 연산자를 사용하여 원시 유형의 값, 배열, 객체에 대해 실행 시 영구적인 메모리를 생성할 수 있다.
- dataType* pValueName = new dataType(size), 배열이면 new dataType[size] 로 사용 가능하며, 객체일 경우 className* pValueName = new className() 혹은 new className 로 사용 가능합니다.
- 동적 영구 메모리 할당한 변수들은 delete 연산자를 이용하여 명시적으로 삭제해야 합니다.
- 만약 포인터가 가리키는 메모리를 삭제하기 전에 실수로 포인터를 재할당한다면, 포인터가 전에 가리키던 메모리는 삭제할 수 없게 되는데, 이를 메모리 누설이라고 한다.
- guessCh : 입력받은 문자 ch와 word를 비교하여 맞는 문자가 있는지 확인합니다. 만약 1개라도 맞는게 있다면 star에 *를 문자로 교체하고, collectCount를 증가시킵니다. 만약 맞는게 없다면 해당 문자 ch는 word에 없는 문자라고 출력하고 missCount를 증가시킵니다.
- playHangman : while문을 사용하여 checkStar가 false를 반환하기 전까지 계속 반복합니다.
main.cpp
#include "hangman.h"
int main() {
srand(time(0));
while (true) {
std::string words[10] = { "write", "that", "java", "korea", "master", "location", "program", "consider", "hello", "world"};
Hangman game(words[rand() % 10]);
game.playHangman();
std::cout << std::endl << "Do you want to guess for another word? Enter y or n>";
char answer;
std::cin >> answer;
if (answer == 'n') break;
}
return 0;
}
기본적으로 무한 루프를 사용합니다.
임의의 단어들을 생성하여 Hangman 객체인 game에 1개의 단어를 입력합니다.
그리고 playHangman 함수를 실행하여 게임을 시작하고, 게임이 끝나면 다시 한번 더 할 것인지 물어봅니다. 만약 사용자가 y를 입력하며 다시 hangman 게임을 시작하고, n을 입력하며 무한 루프문을 탈출하여 프로그램을 종료합니다.
- class는 객체 지향 프로그래밍에서 객체의 속성과 행동을 정의해주며, 즉 객체에 대한 설계도입니다.
- class도 데이터 유형입니다. 그래서 객체의 선언과 생성을 위해 클래스를 사용할 수 있습니다.
- class의 생성자는 class의 이름과 동일해야 하며, 생성자는 2가지로 나뉩니다.
인수가 없는 생성자 : 인수를 가지지 않는 생성자로, 굳이 작성을 안해도 기본적으로 제공하는 생성자입니다.
인수가 있는 생성자 : 인수를 가지는 생성자로, 객체를 생성할 때 인수를 같이 넣어서 생성해야된다.
- 객체는 클래스의 인스턴스로, 이름을 통해 객체의 멤버에 접근하기 위해서는 점 연산자(.)를 사용한다.
- 객체의 상태 : 현재 값의 데이터 필드(속성)로 표현 / 행동 : 함수 세트로 정의
- class는 public 과 private 키워드를 사용합니다.
public : 모든 클라이언트에서 접근 가능
private : 클래스 안에서만 접근 가능
- 헤더 파일에서 class를 정의하고, 다른 파일에서 클래스를 구현이 가능하다. 이렇게 구현함으로써 클래스 정의와 구현을 분리할 수 있습니다.
- 헤더 파일이 여러 번 포함되는 것을 방지하기 위해 #ifndef 지시자를 사용할 수 있다.
- 보통 접근자에는 함수 이름에 get을 붙이고, 변경자에는 함수 이름에 set를 붙인다.
- 문제 풀이 -
9.11(EvenNumber 클래스) 다음 내용을 포함하여 짝수를 표시하는 EvenNumber 클래스를 정의하여라. - 객체에 저장된 정수 값을 표시하는 int 형의 value 데이터 필드 - 0 값으로 EvenNumber 객체를 생성하는 인수 없는 생성자 - 특정 값으로 EvenNumber 객체를 생성하는 생성자 - 객체에 대해 int 값을 반환하는 getValue() 함수 - 객체에서 현재 짝수 값 이후의 다음 짝수 값을 나타내는 EvenNumber 객체를 반환하는 getNext() 함수 - 객체에서 현재 짝수 값 이전의 앞 짝수 값을 나타내는 EvenNumber 객체를 반환하는 getPrevious() 함수 클래스 구현 후, 입력받은 수에 대한 getNext() 와 getPrevious() 함수를 호출하여 이들 수를 화면에 출력하는 테스트 프로그램을 작성하여라.
28 ~ 21 : 순서대로 행 검사, 열 검사, 대각선 검사, sub-대각선 검사를 합니다.
32 : 함수 종료.
문제 8.7의 풀이 코드(3)
- sameRow : 입력받은 행렬의 row가 0 or 1로만 이루어져있는지 확인합니다.
35 ~ 47 : 입력받은 행렬은 기본적으로 4 X 4의 크기이므로 4번 반복하는 for문을 이용했습니다.
38번 줄의 조건문은 임의의 row의 0번째 index ~ 3번째 index까지의 값들이 모두 같은지 확인합니다. 그리고 0번째 index의 값이 0이라면 41번 줄의 출력문을, 1이라면 43번 줄의 출력문을 출력합니다. 어차피 다 같은 값들이므로 0번째 index의 값만 확인하면 됩니다.
bool형 변수 miss는 0 or 1로만 이루어져있는 row가 없을 때 47번 줄의 출력문을 출력하기 위한 변수입니다. 만약 하나라도 있다면 조건문내에서 miss = true로 저장합니다.
- sameColumn : 입력받은 행렬의 column이 0 or 1로만 이루어져있는지 확인합니다. 코드의 구조는 sameRow 함수와 동일합니다.
문제 8.7의 풀이 코드(4)
- sameDiagonal : 입력받은 행렬의 대각선이 0 or 1로만 이루어져있는지 확인합니다. 기본적인 구조는 sameRow와 동일합니다.
67 : 대각선만 같은지 확인하면 되기 때문에 [0][0] ~ [3][3]이 같은지 확인하도록 수정했습니다.(왼쪽 위 -> 오른쪽 아래)
- sameSubDiagonal : 입력받은 행렬의 sub-대각선이 0 or 1로만 이루어져있는지 확인합니다. 기본적인 구조는 sameRow와 동일합니다.
80 : 67번 줄의 이유와 동일하지만, sub-대각선은 오른쪽 위 -> 왼쪽 아래 방향이므로 [0][3] ~ [3][0]이 같은지 확인하도록 수정했습니다.
아래는 문제 8.7 풀이 코드의 출력 결과입니다.
문제 8.7의 출력 결과
위의 결과를 잘 이해하기 쉽도록 사진을 준비했습니다.
Matrix의 행, 열, 대각선들을 설명.
row와 column은 알고 있겠지만, Major Diagonal(= Diagonal)과 Sub-Diagonal이 뭔지 몰라서 찾아봤더니
Major Diagonal = 왼쪽 위 -> 오른쪽 아래, Sub-Diagonal = 오른쪽 위 -> 왼쪽 아래
로 이어져있는 선들을 말합니다. 그러면 출력 결과를 다시 보면
문제 8.7의 출력 결과
0번째 row가 1로만 이뤄져있기 때문에 All 1's on row 0
1번째 column이 1로만 이뤄져있기 때문에 All 1's on column 1
오른쪽 위 -> 왼쪽 아래로 이어져있는 Sub-Diagonal이 1로만 이뤄져있기 때문에 All 1's on the sub-diagonal