1. Explain Polymorphism, and how to implement it.
다형성이란, 상속 계층 구조에서 다양한 객체들에 대해 마치 객체들이 기반 클래스의 객체인 것처럼 처리하는 것을 말한다.( 이때 각 객체는 자신에게 적합한 작업을 수행하며, 기존의 코드를 수정하지 않고 새로운 클래스를 추가할 수 있는 효율적인 방법이다. )
2. Explain the difference between pure virtual and virtual.
순수 가상함수와 가상함수의 차이점을 설명해보자.
- 순수 가상함수는 상속 계층 구조 관계에서 기본 클래스의 멤버 함수를 파생 클래스에서 사용하고자 할 때, 기본 클래스에서 멤버 함수를 구현하는 것이 무의미할 때 사용된다.
1) 순수 가상함수는 파생 클래스에서 Override하지 않으면 에러가 발생하지만, 일반 가상함수에서는 OVerride하지 않으면 단순히 멤버 함수의 상속만 이루어지고 에러가 발생하지 않는다.
2) 순수 가상함수는 함수 정의 부분이 따로 존재하지 않고, virtual void draw() const = 0; 처럼 선언한다. 반면에 가상함수는 함수 정의 부분이 존재하여 virtual void draw() const { ... } 와 같이 나타낸다.
3) 일반적인 가상함수와는 다르게 순수 가상함수를 포함하는 클래스를 '추상 클래스'라고 한다.
3. Explain the advantages of using a template.
- template를 사용하면 모든 종류의 자료형을 처리할 수 있는 함수나 클래스를 간단하게 설계할 수 있다.
서로 다른 자료형에 대해 같은 일을 수행하는 통일된 오버로드 된 함수를 생성하기 위해 사용된다.- 서로 다른 자료형에 대해 수행되는 여러 종류의 함수나 클래스를 만들어 둘 필요가 없다.
4. Explain public, protected, and private inheritance.(수정하기)
public 상속 : 기본 클래스의 public 멤버는 파생클래스에서도 public으로 사용, protected는 파생클래스에서도 protected로 사용된다.
protected 상속 : 기본 클래스의 멤버의 접근 지정자를 최소 protected로 만든다. 즉 public은 protected로, protected는 그대로 protected로 사용된다.
private : 상속 접근 지정자에서 접근 지정자를 명시하지 않았을 때 default로 적용되며 기본 클래스의 모든 접근 제어 지정자를 private로 만든다.
세가지 경우의 상속에서, 기본 클래스의 private 멤버는 파생 클래스에서 접근이 불가능하다.
5. Write the output of the code below.
#include <iostream>
#include <string>
class CreateDestroy {
private:
std::string name;
public:
CreateDestroy(const std::string& name)
: name(name) {
std::cout << "Call " << name << " constructor" << std::endl;
}
~CreateDestroy() {
std::cout << "Call " << name << " destructor" << std::endl;
}
};
int main() {
CreateDestroy a("A");
CreateDestroy b("B");
return 0;
}
Call A constructor
Call B constructor
Call B destructor
Call A destructor
#include <iostream>
class Base {
public:
Base() {
std::cout << "Call base class constructor" << std::endl;
}
~Base() {
std::cout << "Call base class destructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Call derived class constructor" << std::endl;
}
~Derived() {
std::cout << "Call derived class destructor" << std::endl;
}
};
int main() {
Base* base = new Base();
Base* derived = new Derived();
delete base;
delete derived;
return 0;
}
Call base class constructor // base객체 생성에 의한 Base 생성자 실행
Call base class constructor
Call derived class constructor // derived 객체 생성에 의한 Base생성자 실행
Call base class destructor // delete base에 의한 Base소멸자 호출
Call base class destructor // delete derived에 의한 Base 소멸자 호출
Check Point (상속 구조에서 생성자, 소멸자의 실행 순서)
파생 클래스 객체의 생성 과정
- 기본 클래스의 생성자와 소멸자는 파생클래스에 상속 X
- A->B->C 순서로 상속이 되었다고 가정했을 때, A의 생성자는 마지막으로 호출되고, 처음으로 생성자 실행을 마치고 리턴한다.
C의 객체를 생성할 경우 : C 생성자 호출 -> B 생성자 호출 -> A 생성자 호출 / A 생성자 실행 및 리턴 -> B 생성자 실행 및 리턴 / C 생성자 실행 및 리턴
연쇄적 소멸자 호출 ㅣ 연쇄적 생성자 구조의 반대라고 보면 된다
A B C 의 생성자가 호출된 상태, C의 소멸자가 처음 호출 및 실행된다.
C 소멸자 호출 및 실행 -> B 소멸자 호출 및 실행 -> A 소멸자 호출 및 실행 / A 소멸자 리턴 -> B 소멸자 리턴 -> C 소멸자 리턴
Call base class constructor // base 객체 생성자
Call base class constructor // derived 객체 기반 클래스 생성자
Call derived class constructor // derived 객체 파생 클래스 생성자
Call derived class destructor
Call base class destructor
동적 바인딩에서 생성자, 소멸자 실행 순서
동적 바인딩이란? : 컴파일 시간이 아닌 실행 시간(런타임)에 호출될 함수가 결정, virtual 키워드를 통해 동적 바인딩하는 함수를 가상함수라고 한다. 함수가 가상 함수인 경우에는, 포인터 변수의 자료형이 아닌 가리키고 있는 객체에 따라 호출의 대상이 정해진다.
#include <iostream>
class Parent {
public:
Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
public:
Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main()
{
std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
{ Child c; } // 지역 변수로 Child 객체 만든다.
/*
* Parent 생성자 호출
* Child 생성자 호출
* Child 소멸자 호출
* Parent 소멸자 호출
*/
std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
{
Parent* p = new Child();
delete p; // Child의 소멸자가 호출되지 않는, 메모리 누수가 발생한다. --> virtual을 사용해서 메모리 누수를 막을 수 있다.
// 소멸자를 virtual로 선언하기
}
/*
* Parent 생성자 호출
* Child 생성자 호출
* Parent 소멸자 호출
*/
}
다음과 같이 코드를 짰을 때, Parent 기본 클래스 자료형의 Child 파생 클래스 객체를 생성하면, p 객체를 delete하였을 때 Parent 메모리만 삭제되고, Child 객체의 메모리는 삭제되지 않는 것을 확인할 수 있다.
이 메모리 누수를 방지하기 위해서 기본 클래스의 소멸자에 virtual을 붙이면 된다.
이에 대한 근거를 제시해보겠다.
virtual 키워드가 없으면 A클래스 생성자만 호출된다. (delete는 메모리를 해제하는 키워드인데, 메모리는 A 자료형이므로!)
소멸자도 자식 클래스에서 오버라이딩 된 함수. delete a를 하면 부모 클래스의 소멸자가 호출된다. 이때 virtual 이 사용되면 자식 클래스에서 소멸자가 재정의될 수 있음을 명시하는 것이므로, 포인터의 종류에 상관없이 자식 클래스(핸들이 실제로 가리키고 있는 클래스)의 소멸자가 호출된다.
http://hyacinth.byus.net/moniwiki/wiki.php/C%2B%2B/%EC%86%8C%EB%A9%B8%EC%9E%90%EC%97%90%20virtual%EC%9D%84%20%EC%93%B0%EB%8A%94%20%EC%9D%B4%EC%9C%A0
자식의 소멸자 호출 후 부모의 클래스 의 소멸자가 호출됨.
class Parent {
public:
Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
Practice
1. Solve the problem below with the given code.
- Implement classes that allow arbitrary types with template. 임의의 자료형을 허용하는 클래스를 만들어라
- Overload the + operator with a global function. + 연산자를 전역 함수로 사용하라
- Overload the - operator with a member function. - 연산자를 멤버 함수로 사용하라
- Draw a UML of that code. 클래스 UML 다이어그램 그리기
- Draw an inheritance diagram for that code. 코드의 상속 다이어그램을 그려라?
#ifndef ACE1309_WEEK16_SOL01_COMPLEX_H
#define ACE1309_WEEK16_SOL01_COMPLEX_H
class Complex {
private:
double real;
double imaginary;
public:
Complex(double real = 0, double imaginary = 0);
Complex add(const Complex& complex) const;
void print() const;
Complex subtract(const Complex& complex) const;
};
#endif // ACE1309_WEEK16_SOL01_COMPLEX_H
연산자 오버로딩 복습
class PhoneNumber {
friend ostream& operator <<(ostream&, const PhoneNumber&);
friend istream& operator >> (istream&, PhoneNumber&);
private:
string number;
};
#include <iomanip>
ostream& operator << (ostream & output, const PhoneNumber & number)
{
output << number.number << endl; // number객체의 number 멤버 변수로 설정하기
return output;
}
istream& operator >>(istream& input, PhoneNumber& number)
{
input.ignore();
input >> number.number;
return input;
}
int main() {
PhoneNumber phone;
cout << "Enter the number in the form (123) 456-7890" << endl;
cin >> phone;
cout << "The phone number is : ";
cout << phone << endl;
return 0;
}
추후 해결 코드 게시하기
2. Fill in the blanks so you can see the results below.
book.h
#ifndef ACE1309_WEEK16_SOL02_BOOK_H
#define ACE1309_WEEK16_SOL02_BOOK_H
#include <string>
class Book {
private:
std::string title;
public:
Book(const std::string& title);
virtual ~Book();
std::string get_title() const;
_____ void print() const;
void set_title(const std::string& title);
};
class Novel : public Book {
public:
Novel(const std::string& title);
virtual ~Novel();
virtual void print() const override;
};
class Biography : public Book {
public:
Biography(const std::string& title);
virtual ~Biography();
virtual void print() const override;
};
#endif // ACE1309_WEEK16_SOL02_BOOK_H
book.cpp
#include "book.h"
#include <iostream>
#include <string>
Book::Book(const std::string& title)
: title(title) {
std::cout << "Call book constructor for " << get_title() << std::endl;
}
Book::~Book() {
std::cout << "Call book destructor for " << get_title() << std::endl;
}
std::string Book::get_title() const {
return title;
}
void Book::print() const {
std::cout << "Book" << std::endl;
}
void Book::set_title(const std::string& title) {
this->title = title;
}
Novel::Novel(_____ title)
: Book(title) {
std::cout << "Call novel constructor for " << _____ << std::endl;
}
Novel::~Novel() {
std::cout << "Call novel destructor for " << _____ << std::endl;
}
void Novel::print() const {
std::cout << _____ << std::endl;
}
Biography::Biography(const std::string& title)
: Book(title) {
_____
}
Biography::~Biography() {
_____
}
void Biography::print() const {
std::cout << _____ << std::endl;
}
main.cpp
_____
int main() {
_____
return 0;
}
Output:
Call book constructor for The Alchemist
Call novel constructor for The Alchemist
Call book constructor for Paulo Coelho
Call biography constructor for Paulo Coelho
Novel
Biography
Call novel destructor for The Alchemist
Call book destructor for The Alchemist
Call biography destructor for Paulo Coelho
Call book destructor for Paulo Coelho
해결 과정 및 코드
Key Point
1) static 변수의 생성자 소멸자 호출 시기
: static변수는 전역 변수 처럼 프로그램 시작 부터 종료까지 살아있다.
2) virtual 함수를 이용한 다형성 구현
print함수는 virtual로 구현되어야 Novel과 Biograpy객체에서 print()를 호출했을 때 Novel, Biography 가 출력되는 것이다.
키포인트 맞는지 모르겠지만
3) virtual를 이용한 소멸자 호출
동적 바인딩의 경우 virtual을 사용하지 않으면 기본클래스의 소멸자만 호출이 된다 .
엥 왜 결괏값이 똑같지?
Book* novel = new Novel("THe Alchemist");
Book* biography = new Biography("Pulo Coelho");
delete novel;
delete biography;
이렇게 작성해야 virutal 유무의 영향을 받고 기본클래스의 소멸자만 호출된다.
#include <iostream>
using namespace std;
#include "Book.h"
int main()
{
Novel novel("The Alchemist");
static Biography biography("Pulo Coelho");
Book * handle[2] = { &novel, &biography };
for (int i = 0; i < 2;i++) {
handle[i]->print();
}
return 0;
}
이렇게 main함수를 작성하면 Novel과 Biography 클래스 객체는 그 자체로 생성이 되므로 Book 포인터 배열에 넣는 거랑 상관없이 소멸자가 잘 출력이 된다.
#include "Book.h"
#include <iostream>
#include <string>
Book::Book(const std::string& title)
: title(title) {
std::cout << "Call book constructor for " << get_title() << std::endl;
}
Book::~Book() {
std::cout << "Call book destructor for " << get_title() << std::endl;
}
std::string Book::get_title() const {
return title;
}
void Book::print() const {
std::cout << "Book" << std::endl;
}
void Book::set_title(const std::string& title) {
this->title = title;
}
Novel::Novel(const string & title)
: Book(title) // Member Initializer로 상속 관계의 멤버 변수 초기화해주기
{
std::cout << "Call novel constructor for " << get_title () << std::endl;
}
Novel::~Novel() {
std::cout << "Call novel destructor for " << get_title () << std::endl;
} // Book 클래스의 멤버 변수 title에 접근한다.
void Novel::print() const { // 오버라이드 된 print 함수
std::cout << "Novel" << std::endl;
}
Biography::Biography(const std::string& title)
: Book(title) {
std::cout << "Call biography constructor for " << get_title() << std::endl;
}
Biography::~Biography() {
std::cout << "Call biography destructor for " << get_title() << std::endl;
}
void Biography::print() const {
std::cout << "Biography" << std::endl;
}
#pragma once
#ifndef BOOK_H
#define BOOK_H
#include <iostream>
using namespace std;
#include <string>
class Book {
private:
string title;
public:
Book(const string& title);
virtual ~Book();
string get_title() const;
virtual void print() const; // virtual 쓰는거 맞는지 확인하기
void set_title(const string& title);
};
class Novel : public Book {
public:
Novel(const std::string& title);
virtual ~Novel();
virtual void print() const override;
};
class Biography : public Book {
public:
Biography(const std::string& title);
virtual ~Biography();
virtual void print() const override;
};
#endif
#include <iostream>
using namespace std;
#include "Book.h"
int main()
{
Book* novel = new Novel("The Alchemist");
Book* biography = new Biography("Pulo Coelho");
novel->print();
biography->print();
delete novel;
delete biography;
return 0;
}
dfㄴㄹㅇ
해답 main함수, delete 연산자의 호출을 따로 정의해줌으로써 출력문을 관리
이때 delete를 안쓰면 출력이 안될 뿐 더러 메모리 누수가 일어난다.
+ 내가 생각한 예상 문제
1. << 연산자와 >> 연산자를 오버로딩할 때, 전역함수로 정의하고 friend를 사용하여야 하는 이유를 서술하시오.
2. 복합 관계와 상속 관계에서 생성자와 소멸자
기본 파생에 둘다 복합이 있을 경우에, 기본의 복합이 먼저 생성자 -> 기본의 생성자 -> 파생의 복합 -> 파생의 생성자
#include <iostream>
using namespace std;
class A1{
public:
A1() {
cout << "A1 생성자" << endl;
}
~A1() {
cout << "A1 소멸자" << endl;
}
};
class B
{
private:
A1 a1; // 복합 관계의 클래스
public:
B()
{
cout << "B 생성자" << endl;
}
~B() {
cout << "B 소멸자" << endl;
}
};
class A2 {
public:
A2() {
cout << "A2 생성자" << endl;
}
~A2() {
cout << "A2 소멸자" << endl;
}
};
class C : public B
{
private :
A2 a2; // 복합관계
public:
C()
{
cout << "C 생성자" << endl;
}
~C() {
cout << "C 소멸자" << endl;
}
};
int main()
{
// C가 B를 상속하고, B에는 A1 객체가, C에는 A2객체가 복합관계에 있는 상황
C c;
return 0;
}
class A{
};
class B{
A a;
};
* 복합 관계에서는 A 생성자 -> B 생성자 -> B 소멸자 -> A 소멸자 순으로 출력된다.
3. 연산자 오버로딩에서 left value, right value의 차이
int& Array::operator[] (int subscript) // Array 클래스 자체를 가져와서 내부의 ptr 요소를 변경한다.
{
if (subscript < 0 || subscript >= size)
{
cerr << "\nError : Subscript " << subscript << "out of range" << endl;
exit(1);
}
return ptr[subscript]; // ptr배열의 값을 바꿀 수 있기 때문에 참조형으로 리턴
}
int Array::operator[](int subscript)const { // Array 객체가 right value, 즉 값만 참조해도 되는 역할을 한다. 따라서 Reference로 Array를 가져올 필요가 없고, 객체 내의 멤버 변수의 값을 변경할 필요도 없으니 const로 선언한다.
if (subscript < 0 || subscript >= size)
{
cerr << "\nError : Subscript " << subscript << "out of range" << endl;
exit(1);
}
return ptr[subscript]; // 입력한 int값에 해당하는 ptr 요소가 리턴되어 right value의 값 할당에 사용됨.
}
주석 참고해서 저 두 연산자 오버로딩 함수의 차이 이해하기
int main()
{
Array array1(3);
Array array2(array1);
array1[2] = 100;
cout << array1[2] << endl;
cout << array1 << endl;
}
다음의 main 함수에서, array1
4. 연산자 오버로딩에서 전위 연산자와 후위 연산자 차이
Date& operator++(); // 전위 연산자 멤버 함수 오버로딩
Date operator++(int i); // 후위 연산자 멤버 함수 오버로딩
헤더 파일의 형태는 이러하다.
후위 연산자 오버로딩 시 매개변수에 int를 넣는 이유는?
: 공 매개변수(dummy parameter)를 이용해 접미 증가(postfix)를 구현한다.
C++의 약속, d1++와 같은 형태이고, 이 때 반환하는 객체는 오버로딩 함수에서 임의로 생성되는 temp 객체이므로
1) 접두 연산자(prefix, 전위) : 반환값을 left value로 취급(assign 가능)
// 전위 연산자 오버로딩 : 객체 자체의 값을 변경시키므로 레퍼런스로 가져오기
Date& Date:: operator ++() {
Increment(); // 날짜를 하루 증가시켜주는 함수
return *this;
}
증가시킨 후 반환하면 됨(이때 반환하는 객체는 객체 자신을 참조한 것이므로 함수의 반환형은 Date&이다. )
2) 접미 연산자(postfix, 후위) : 반환 값을 right value로 취급(assign 불가능)
Date Date::operator++(int i)
{
Date temp = *this; // 임의의 객체 생성하고 현재 객체 대입 연산자로 멤버 변수 복사
Increment(); // 실제 객체는 하루 증가한다.
return temp; // 증가되지 않은 원래 객체를 리턴
}
후위 연산자는 반환 값을 right value로 취급한다.
a = a + 1;
이라는 연산에서,
++a이라는 접두 연산자(전위 연산자)는 왼쪽 a(증가된 값)을 리턴받기 원한다. 그래서 반환값을 left value로 취급한다는 표현을 사용하고, return 값이 객체 자체이므로 assign을 받는 것이 가능하다는 표현을 쓴다.
반면 a++이라는 접미 연산자(후위 연산자)는 오른쪽 a (증가되기 전의 값)을 리턴받기 원하고, 그래서 반환 값을 right value 로 취급하고, return값이 객체 자체가 아닌 새로 만들어진 temp(return by value)이므로 assign을 받는 것이 불가능하다는 표현을 쓰는 것 같다.
5. 복사 생성자 정의 시 인수에 const 참조로 안 썼을 때 무한루프에 빠지게 되는 이유?
: 복사 생성자에 정의되는 Call by value 이므로 인자로 들어온 객체의 복사본을 만들기 위해 또 복사 생성자를 호출하며 연쇄적으로 복사 생성자가 호출되는 무한 루프게 빠지게 되기 때문이다.
6. Dangling Pointer에 대해 설명하시오.
복사 생성자에서 ptr을 새로 할당하지 않고 copy 객체의 포인터를 대상 객체의 포인터로 단순히 동적할당한 경우에는 둑 객체가 동일한 메모리를 가리키게 됨. 첫번째 소멸자가 동적할당 된 메모리를 해제하고, 다른 객체의 소멸자가 동일한 메모리를 소멸하는 것을 시도하는 상황을 Dangling Pointer라고 한다. 이 경우에는 runtime 에러가 발생한다.
7. 상속에서 IS-A와 복합에서의 HAS-A에 대해 설명하시오
IS-A에서 파생 클래스의 객체는 기본 클래스의 객체로 여겨지는 것을 말하고, HAS-A에서 객체가 다른 클래스의 객체를 멤버로 포함하는 것을 말한다.
8. Override, 동적 바인딩/정적 바인딩에 대해 서술하시오
동적 바인딩 : 가상함수의 동적 바인딩은 포인터 또는 참조형 핸들에서만 이루어진다. 가상함수르 사용할 때, 프로그램이 동적으로 실행시간에 어떤 클래스의 함수를 사용할 지 결정한다.
정적 바인딩 : 특정 객체가 dot 연사낮를 사용해 멤버 함수를 호출하면, virtual 여부와 관계없이 호출된 함수는 컴파이 시간에 결정된다.
9. #Pragma once에 대해 서술하시오
10. 추상 클래스에 대해 서술하시오.
순수 가상함수를 포함
객체를 생성할 시 컴파일 에러가 생긴다
추상 클래스를 상속하는 모든 파생 클래스는 추상 클래스의 모든 순수 가상함수를 재정의(override)하고, 구체적인 구현을 제공해야 한다. 그렇지 않으면 그 파생 클래스 또한 추상클래스가 된다.
11. 기본 클래스의 헤더파일이 파생 클래스 헤더파일에 Include 되어야 하는 세가지 이유를 대시오
1) 기본 클래스의 존재를 알기 위해
2) 상속된 데이터 멤버의 크기를 알기 위해
3) 상속된 클래스의 멤버가 올바르게 사용되는지 알기 위해
'Major Study > Object Oriented Programming' 카테고리의 다른 글
연산자 오버로딩 / 상속 / 다형성 구현 프로젝트 해결 과정 (0) | 2021.10.08 |
---|---|
객체지향 프로그래밍2 상속, 연산자 오버로딩, 다형성, 템플릿 관련 의문점 정리 및 해결 (2) | 2021.06.15 |
C++ template 내용 정리(열혈 C++, 강의노트, 기타 자료 참고) (0) | 2021.06.15 |
객체지향 프로그래밍 프로젝트 (상속과 다형성, 가상함수) 해결 과정 / 가상 함수, 다형성 개념 정리 (0) | 2021.05.31 |
2021 객체지향 프로그래밍 중간고사 대비 (0) | 2021.04.17 |