memo
해야 할 것
copy construct
1. Polymorphism (1)
Function/Construct overloading
같은 이름을 가진 함수를 각각 다른 자료형의 return, parameter를 갖게 구현하는 것을 function overloading이라고 한다.
construct overloading도 마찬가지이다.
Operator overloading
<매우 도움이 된다>
class도 하나의 자료형이다.
class A가 있고, A의 instance로 a1과 a2가 있다고 가정해보자.
그렇다면 a1+a2는 어떻게 정의할까?
operator에 대해서도 overloading이 구현되어야 할 것이다.
(한글로는 연산자 중복이라고 한다고 한다)
지금부터 이것에 대해 알아볼 것이다.

연산자는 binary, postfix, prefix 세 가지의 형태로 나눌 수 있다.
이에 따라 argument가 바뀌게 된다. 잘 구분해서 사용하도록 하자.
기존 쓰임을 거의 비슷하게 따르게 되니 기존에 어떻게 쓰였는지를 바탕으로 참고하면 될 것 같다.
(참고) friend 함수
class 내부에 정의되어서 class 내의 모든 member 함수, 변수에 접근 가능하지만 class의 member 함수가 아닌 함수를 의미한다. (따라서 상속 되지 않는다)
complex class에서의 operator overloading에 대해서 알아보자.
complex constructor의 coverloading은 밑과 같이 정의되어있는 상태이다.
Complex::Complex(int r, int i) { m_r = new int(r); m_i = new int(i); } Complex::~Complex() { if (m_r) delete m_r; if (m_i) delete m_i; m_r=m_i=nullptr; } Complex::Complex() : Complex(0, 0) { } Complex::Complex(int r) : m_r{ new int(r) }, m_i{ new int(0) } { } Complex::Complex(const Complex & rhs) : Complex(*rhs.m_r, *rhs.m_i) { } void Complex::print() const { cout << *m_r << (*m_i < 0 ? "" : "+") << *m_i << "j" << endl; }
(+) operator
먼저 complex instance의 member variable을 불러와서 작업을 수행하는 방법이다. 다음과 같이 정의할 수 있다.
class Complex { int* m_r; // real part int* m_i; // imaginary part public: Complex operator+(Complex c2); Complex operator+(Complex& c2); Complex operator+(const Complex& c2); }; Complex Complex::operator+(const Complex& c2) { Complex result; *(result.m_r) = *(m_r) + *(c2.m_r); *(result.m_i) = *(m_i) + *(c2.m_i); return result; }
주의 : 반환 값은 result instance이다. 이러한 반환 값이 operator 마다 다를 수 있으니 유의해보도록 하자.
다음은 int, 또는 double 값을 받아와 작업을 수행하는 방법이다. 다음과 같이 정의할 수 있다.
Complex Complex::operator+(int r) { Complex result; *(result.m_r) = *(m_r) + r; *(result.m_i) = *(m_i); return result; } Complex Complex::operator+(double r) { return Complex((static_cast<int> (r)) + *(m_r), *(m_i)); }
두 가지 방법으로 구현이 가능하다. static_cast<int>는 double을 int로 바꿔주는 함수이다. 첫 번째 방법에서는 반환 값이 없는 constructor를 호출하고 있고, 두 번째 방법에서는 반환 값이 있는 constructor를 호출하고 있다.
마지막으로 왼쪽에 객체가 아닌 int나 double이 오는 경우이다.
교수님 설명 : argument 왼쪽에 객체가 아닌 것이 오면 member function을 사용할 수 없기 때문에 friend 함수를 사용해야 한다. (교수님이 이거 그냥 외우라고 하셨다,, 교수님도 모르시는게 아닐까)
블로그 설명 : argument에 class instance 아닌 것이 오면 member를 이용할 수 없으므로 friend 함수를 사용해야 한다.
난 둘 다 이해 안 간다.. 나중에 차근차근 이해해보자..
→ 남수민 피셜 : this가 안들어가니까 friend로 정의한다. 이걸로 우선 이해하고 넘어가기,,
다음과 같이 정의할 수 있다.
class Complex { int* m_r; // real part int* m_i; // imaginary part public: // Constructors (omitted) Complex operator+(double r); friend Complex operator+(double r, const Complex& c); }; Complex Complex::operator+(double r) { return Complex((static_cast<int> (r)) + *(m_r), *(m_i)); } Complex operator+(double r, const Complex& c) { Complex result((static_cast<int> (r)) + *(c.m_r), *(c.m_i)); return result; }
반환 값은 마찬가지로 result instance이다.
(== / +=) operator
== 연산자는 반환 값이 bool이고, += 연산자는 반환 값이 존재하지 않는다.
bool Complex::operator==(const Complex & rhs) { if ((*m_r == *rhs.m_r) && (*m_i == *rhs.m_i)) return true; else return false; } void Complex::operator+=(const Complex & rhs) { *m_r += *rhs.m_r; *m_i += *rhs.m_i; }
반환 값이 어떤 형태인지 잘 알아두도록 하자.
(++ / prefix, postfix) operator
prefix ++는 return 값을 자기 자신 참조 값으로 넘겨준다. postfix는 자기 자신을 copy 했던 것을 넘겨준다.
postfix가 copy value를 넘겨주는 이유는 무엇일까?
postfix는 연산이 끝난 이후에 값을 증가 시킨다. 따라서 기존 값을 전달 할 필요가 있다. 이를 위해 기존 값을 copy해 두었다가 return 시키는 것이다.
class Complex { int* m_r; // real part int* m_i; // imaginary part public: // Constructors (omitted) // + operator overloading (omitted) Complex& operator++(); // prefix Complex operator++(int dummy); // postfix }; Complex& Complex::operator++() { (*m_r)++; return *this; } Complex Complex::operator ++(int dummy) { Complex ret(*this); (*m_r)++; return ret; }
postfix의 argument인 dummy는 그냥 규칙이다. 아무 역할도 하지 않는다.
2. Polymorphism (2)
Operator overloading (2)
저번에 다룬 operator 뿐만 아니라 다양한 operator들의 overloading이 가능하다.

이번 PPT에서는 <<, [], = 연산자의 overload를 해볼 것이다.
(<<) operator
<<도 전 PPT의 +(double, pointer)에서와 마찬가지로 자기 객체(this)를 이용하지 않았기 때문에 friend로 정의하였다.
class Complex { int* m_r; // real part int* m_i; // imaginary part public: …… friend ostream& operator<< (ostream& o, Complex c); }; ostream& operator<< (ostream& o, Complex c) { o << *c.m_r << (*c.m_i < 0 ? "" : "+") << *c.m_i << "j" ; // 11 line return o; } int main(){ cout << Complex(23, 7) << endl; cout << Complex(-5, 20) << endl; return 0; }
위와 같이 정의하면 원하는 형식의 output을 정의해서 사용할 수 있다.
11번째 줄의 o는 cout을 의미한다. (cout은 ostream의 object이다)
([]) operator
[]는 primaitive type에는 적용되어있다. 하지만 우리가 정의한 (밑에서의 vector class) class에서는 정의되어있지 않다. 따라서 이도 마찬가지로 overload 해주어야 한다.
코드는 다음과 같다.
class Vector { int* m_data; // pointer to dynamic array int m_size; // array size public: Vector() : m_data(new int[100]{ 0, }), m_size(100) { }; // data를 100개 생성하고 // 0으로 초기화 시킨다. // size는 100으로 update한다. ~Vector() { delete [] m_data; } int& operator[] (int); }; int& Vector::operator[] (int index) { if (index >= m_size) { cout << "Error: index out of bound"; // index 100을 넘어가면 에러를 출력해준다. exit(0); } return m_data[index]; } int main(){ Vector v; cout << v[1]; // 0 v[1] = 20; cout << v[1]; // 20 }

L-value : 직접적으로 access할 수 있는 값
R-value : complier에 의해 임시적으로 할당된 memory. 우리 코드에서는 직접적으로 access할 수 없다.
In assignment “=” : L-value는 LHS, RHS 어디든 올 수 있다. R-value는 RHS에만 올 수 있다.
b+c는 R-value이다. b, c 개별로는 각각 L-value이다.
++a는 prefix이기 때문에 접근 가능(L-value)하다.
a++는 post이기 때문에 접근 불가능(R-value)하다. copy되고 사라지기 때문이다.
(==) operator
코드는 다음과 같다.
지금 이해하고 싶지 않으니 다음에 보기,,
class Complex { int* m_r; // real part int* m_i; // imaginary part public: …… Complex & operator=(const Complex & c); }; Complex & Complex ::operator=(const Complex & c) { if (this == &c) return *this; delete m_r; delete m_i; m_r = new int(*c.m_r); m_i = new int(*c.m_i); return *this; } int main() { int a = 1; int b = 1111; int c = 2; int d = 2222; int e = 3; int f = 3333; Complex c10(a, b); Complex c11(c, d); Complex c12(e, f); (c12 = c11) = c10; //Expect 1.1111 c10.print(); c11.print(); c12.print(); return 0; }
Std::string

string class를 이용하면 더 다양하게 string을 다룰 수 있다.
string class에는 다양한 operator들이 overloading 되어있다. 우리는 그것들을 활용하면 된다.
- + : string을 합치는 역할을 해준다
- += : 마찬가지의 역할을 해준다.
- == : 두 string 같다면 true, 틀리다면 false를 출력하는 역할을 해준다.
- <,>,≤,≥ : boolalpha와 주로 같이 사용된다. boolalpha는 참이라면 true, 틀리다면 false를 출력해준다.
- [] : 해당 index에 있는 value를 출력해준다. (index는 0부터 시작한다)
- substr(a, b) : 2부터 시작해서 4개의 string 가져온다. (ex. ABCDEFG라면 CDEF를 출력해준다)
- find / rfind : find는 일치하는 가장 왼쪽 값의 시작 index를 return해준다. rfind는 오른쪽 값의 시작 index를 return 해준다.
3. Inheritance (1)
OOP
Abstraction(추상화) : 공통의 속성이나 기능을 묶어 이름을 붙인다. (”일반화 한다”라고 생각해도 된다)
Encapsulation(캡슐화) : Object의 implemention을 구체화 시킨다. (ex. member function, member variable)
Polymorphism(다형성) : 하나의 interface가 다양한 type을 가질 수 있다. (overloading, overriding)
Inheritance(상속) : class가 서로 부모 자식 관계를 가진다. (is a relationship)
- is a relationship vs has a relationship
has-a : association 관계일 때 적합하다.
is-a : inheritance 관계일 때 적합하다.
Inheritance
상속이 필요한 이유는 다음과 같다.
- reuse code (class)
- maintain hierarchical class structure
- need more specified objects
밑과 같은 상황에서 사용한다.
- is-a relationship일 때
- has-a relationship일 때도 가능은 하다.

위와 같이 공통된 부분은 상속을 통해서 구현하고 이후에는 각각의 class마다 필요한 것들을 specialized 시킨다. (중복 구현을 안하게 해준다!)
문법은 다음과 같다. ‘자식 클래스의 이름’ : ‘ 상속 타입 ‘ ‘부모 클래스’

inheritance type(상속 타입)에 따라 child class의 member access 범위가 달라진다.
참고 : protected는 자식 class 내에서 사용 가능하고, 자식 class 내에서 private 취급을 받는다.
private는 외부 뿐만 아니라 자식 class에서도 사용 불가능하다.

parent의 private 변수/함수는 child에서 접근 불가능하다.
protected인 변수/함수는 상속 타입에 따라 child에서 protected 또는 private가 될 수 있다.
public인 변수/함수는 상속 타입을 그대로 따라간다.
EX 1.
class Person { private: int* m_Id; protected: string m_name; public: Person() { m_Id = new int(1111); m_name = "None"; }; string GetName() { return m_name; }; int GetID() { return *(this->m_Id); } void SetName(string name) { m_name = name; } void SetID(int id) { *(this->m_Id) = id; } }; class Student : public Person{ private: //Nothing public: string GetStudentName() { return this->m_name; } //OK }; class Teacher : public Person { private: //Nothing public: int GetTeacherID() { return *(this->m_Id); } //Error }; int main() { Student s1; Teacher t1; s1.SetName("Tom"); t1.SetName("Jenny"); cout << s1.GetStudentName() << endl; cout << t1.GetTeacherID() << endl; return 0; }
Student class를 살펴보자. 이 class에서는 GetStudentName()함수를 정의하고 있다. 이 함수는 m_name 변수를 불러오고 있다. Student class의 상속 타입은 public이다. 따라서 protected(public에서는 protected to protected) 영역에 있는 m_name 변수에 접근 가능하다. 따라서 위 implementation에서는 error가 발생하지 않는다.
Teacher class를 살펴보자. 이 class에서는 GetTeacherID()함수를 정의하고 있다. 이 함수는 m_id 변수를 불러오고 있다. m_id 변수는 private영역에 정의되어 있다. 따라서 상속 타입이 public이어도 자식 class에서는 접근이 불가하다. 위 implementation은 error를 발생시킬 것이다. m_id 변수를 protected로 바꾸거나 m_id 변수 대신 GetID() 함수를 사용하면 정상 동작 시킬 수 있다.
EX 2.
class Person { private: protected: string m_name; int* m_Id; public: Person() { m_Id = new int(1111); m_name = "None"; }; string GetName() { return m_name; }; int GetID() { return *(this->m_Id); } void SetName(string name) { m_name = name; } void SetID(int id) { *(this->m_Id) = id; } }; class Student : public Person{ private: //Nothing public: string GetStudentName() { return this->m_name; } }; class Teacher : protected Person { private: //Nothing public: int GetTeacherID() { return this->GetID(); } }; int main() { Student s1; Teacher t1; s1.SetName("Tom"); t1.SetName("Jenny"); //Error cout << s1.GetStudentName() << endl; cout << t1.GetTeacherID() << endl; return 0; }
main 함수를 살펴보자. t1이라는 Teacher class 인스턴스를 통해 SetName 함수를 호출하고 있다. 이 때 Teacher class는 상속 타입이 protected이다. protected일 때는 부모 class의 public 영역 또한 protected가 되므로 SetName 함수는 Teacher class 내에서 private 취급을 받게 된다. 따라서 위 main은 error를 발생시킨다. 상속 타입을 public으로 바꾸거나 Teacher class의 public 영역에 SetTName함수를 추가시키는(이 때는 main에서도 SetTName 함수를 불러와야 할 것이다) 등의 방법으로 문제를 해결할 수 있다.
Type Conversion (1) - up casting
기본 자료형에서도 int↔float 등의 type conversion이 가능했다. class에서도 이가 가능하다.
우리는 여기서 type을 바꿨을 때 발생하는 문제에 대해 잘 알아야 한다.
up casting : 자식 클래스(derived)의 reference 혹은 pointer를 부모 클래스(base class)로 바꾸는 것이다.
class Base { }; class Derived : public Base { }; Derived d; Derived * pD = &d; Base * pB = &d;
Base * pB = &d;이러한 방식으로 up casting을 할 수 있다.
int main() { Student s1; Teacher t1; s1.SetName("Tom"); s1.SetID(5555); t1.SetName("Jenny"); t1.SetID(6666); Person* p_arr[] = { &s1, &t1 }; // upcasting 부분 for (int i = 0; i < 2; i++) { cout << p_arr[i]->GetID() << "\t" << p_arr[i]->GetName() << endl; } //cout << p_arr[0]->GetStudentName() << endl; //Error //cout << p_arr[1]->GetTeacherID() << endl; //Error return 0; }
Person* p_arr[] = { &s1, &t1 }; 위 코드에서는 이 부분을 통해 upcasting을 진행 하였다.
아래 코드에서 에러가 발생하는 이유는 무엇일까?
//cout << p_arr[0]->GetStudentName() << endl; //Error
//cout << p_arr[1]->GetTeacherID() << endl; //Error
p_arr[0]은 upcasting을 했기 때문에 type이 Person형 pointer로 바뀌었을 것이다. 따라서 Studernt와 Teacher에 specialized된 member 함수에 접근하지 못한다.
<up casting을 하는 이유>
코드의 ‘재사용성을 높이기 위해서’라고 한다. 나중에 자세히 알아보자.
Type Conversion (2) - down casting
down casting : 부모 클래스(base class)의 reference 혹은 pointer를 자식 클래스(derived)로 바꾸는 것이다. (주의 : down casting은 up casting 보다 불안정하다)
예시를 들어 설명하자면
Person is a student → Student is a person는 괜찮지만
Student is a person → Person is a student는 살짝 어색하다.
class Base { }; class Derived : public Base { }; Derived d; Base * pB = &d; Derived * pD = (Derived *) pB;
Derived * pD = (Derived *) pB;이러한 방식으로 dow casting을 할 수 있다.
int main() { Student s1; Teacher t1; s1.SetName("Tom"); s1.SetID(5555); t1.SetName("Jenny"); t1.SetID(6666); Person* p_arr[] = { &s1, &t1 }; //p_arr[0] is Student, p_arr[1] is Teacher cout << ((Student*)p_arr[0])->GetStudentName() << endl; cout << ((Teacher*)p_arr[0])->GetTeacherID() << endl; cout << ((Teacher*)p_arr[1])->GetTeacherID() << endl; cout << ((Student*)p_arr[1])->GetStudentName() << endl; return 0; }
(Student*)p_arr[0], (Teacher*)p_arr[0], (Teacher*)p_arr[1], (Student*)p_arr[1] 위 코드에서는 이 부분을 통해 downcasting을 진행 하였다.
위 코드에서는 error가 발생하지 않는다. 그러나 person class가 teacher, student 모두로 downcast 되는 것을 확인할 수 있다. 이러한 점 때문에 down casting을 불안정하다고 하는ㄴ 것이다.
Constructor
기본적으로 base의 constructor이 불려진 다음에 chlid의 constructor이 불려진다.
class Base { public: Base(){ cout << “base constructor\n”; } }; class Derived : public Base { public: Derived() { cout << “derived constructor\n”; } }; Derived d; // constructor
위는 Implicit case이다. 아무런 정보가 없다면, default constructor이 불려진다.
class Base { public: Base(int a){ cout << “base constructor\n”; } }; class Derived : public Base { public: Derived() : Base(10) { cout << “derived constructor\n”; } }; Derived d; // constructor
위는 Explicit case이다. 구체적인 정보가 있다면, specified된 constructor가 불리어진다.
다음은 예시 코드이다.
class Person { private: protected: string m_name; int* m_Id; public: Person() { m_Id = new int(1111); m_name = "None"; cout << "Person constructor 1 called" << endl; }; Person(int id) { m_Id = new int(id); m_name = "None"; cout << "Person constructor 2 called" << endl; } string GetName() { return m_name; }; int GetID() { return *(this->m_Id); } void SetName(string name) { m_name = name; } void SetID(int id) { *(this->m_Id) = id; } }; class Student : public Person { private: //Nothing public: string GetStudentName() { return this->m_name; } Student() : Person(10) { cout << "Student constructor called" << endl; }; }; class Teacher : public Person { private: //Nothing public: int GetTeacherID() { return this->GetID(); } Teacher() { cout << "Teacher constructor called" << endl; }; }; int main() { Student s1; Teacher t1; return 0; } ---- Person constructor 2 called Student constructor called Person constructor 1 called Teacher constructor called
Destructor
[여기 다시 하기]
Constructor와 반대 이다. child destructor가 불려진 다음에 base의 destructor가 불려진다.
여기서는 한 가지 문제가 발생한다.
int main() { Teacher* t1 = new Teacher(); delete t1; Person* p_person = new Student(); // upcasting delete p_person; return 0; }
만약 upcast 이후에 delete를 한다고 생각해보자. 이 때는 person의 destructor만 불려질 것이다. 만약 student에서 지워져야 할 내용이 있다면 곤란해진다. 이는 뒤에서 다룰 virtual keyword를 이용해서 해결할 수 있다.
order of constructor/destructor calls 이다.
class Base { public: Base(){ cout << "1. base constructor" << endl; } ~Base(){ cout << "2. base destructor" << endl; } }; class Derived : public Base { public: Derived() { cout << "3. derived constructor" << endl; } ~Derived() { cout << "4. derived destructor" << endl; } }; int main() { Derived *pD = new Derived; cout << "5. instance created\n"; delete pD; return 0; } ---- 1. base constructor 3. derived constructor 5. instance created 4. derived destructor 2. base destructor
Multiple Inheritance
여러 번의 상속을 할 수도 있다.
이 때는 밑과 같은 문제가 발생할 수 있으니 주의해야 한다.

4. Inheritance (2)
Overloading vs Overriding
상속에서의 다형성에 대해 다루어볼 것이다.
Overloading과 Overriding에 대한 설명은 다음과 같다.

overloading : 다른 타입의 파라미터를 사용하고 싶을 때 사용한다. / Inheritance가 없을 때도 사용할 수 있다. / 같은 함수에 대해 signiture를 바꾸며 다양한 정의를 할 수 있다.
overriding : 자식 클래스에서 함수의 기능을 바꾸고 싶을 때 사용한다. / 상속 되었을 때만 사용할 수 있다. / 같은 signiture를 가지는 것들을 각각 다른 기능을 하도록 바꾼다.
밑은 각각 overloading과 overriding의 예시이다.
- overloading example

- overriding example

overloading은 polymorphism 부분을 참고하도록 하자.
overriding의 예시이다. 같은 파라미터를 가진 같은 이름의 함수를 서로 다른 기능을 가지도록 정의한 것을 확인할 수 있다.
class Person { protected: int* m_Id; string m_name; public: Person() { m_Id = new int(1111); m_name = "None";}; Person(int id) { m_Id = new int(id); m_name = "None";} string GetName() { return m_name; }; int GetID() { return *m_Id; } void SetName(string name) { m_name = name; } void SetID(int id) { *m_Id = id; } void work() { cout << "Person Work" << endl; } string GetClassName() { return "Person"; } }; class Student : public Person { public: Student() : Person(10) {}; string GetStudentName() { return this->m_name; } void work() { cout << "Student studies" << endl; } string GetClassName() { return "Student"; } }; class Teacher : public Person { public: Teacher() {}; int GetTeacherID() { return this->GetID(); } void SetTeacherName(string new_name) { this->SetName(new_name); } string GetTeacherName() { return this->GetName(); } void work(){ cout << "Teacher teaches" << endl; } string GetClassName() { return "Teacher"; } }; int main() { Student s1; Teacher t1; s1.work(); t1.work(); cout << s1.GetClassName() << endl; cout << t1.GetClassName() << endl; return 0; } ---- Student studies Teacher teaches Student Teacher
이때 up casting을 진행하게 되면 어떻게 될까?
다음은 up casting 코드 예시이다.
class Person { public: … void work() { cout << "Person Work" << endl; } string GetClassName() { return "Person"; } }; class Student : public Person { public: … void work() { cout << "Student studies" << endl; } string GetClassName() { return "Student"; } }; class Teacher : public Person { public: … void work(){ cout << "Teacher teaches" << endl; } string GetClassName() { return "Teacher"; } }; int main() { Student s1; Teacher t1; s1.work(); cout << s1.GetClassName() << endl; t1.work(); cout << t1.GetClassName() << endl; Person* p_arr[] = { &s1, &t1 }; // up casting p_arr[0]->work(); cout << p_arr[0]->GetClassName() << endl; p_arr[1]->work(); cout << p_arr[1]->GetClassName() << endl; return 0; } ---- Student studies Student Teacher teaches Teacher Person work Person Person work Person
(주의 : constructor에는 base class의 것이 먼저 출력되지만, overriding에서는 child의 것이 먼저 출력된다)
우리는 Person work 위 결과에서의 부분의 출력을 원하지 않는다. 이 때는 virtual keyword를 사용하면 된다.
Virtual Function
base class 내에서 선언되고 derived class에서 override된 멤버 함수는 base class 내에서 virtual function이라 정의할 수 있다.
이후 derived class의 객체가 base class를 통한 포인터나 참조를 통해 사용될 때, 재정의(override)된 가상 함수를 호출하면 derived class에서의 함수 동작이 실행된다. (대충 up casting해도 파생 class의 함수를 호출할 수 있다는 이야기이다)
virtual function은 dynamic binding 형태이며 run-time 내에 실행된다.

다음은 virtual 함수를 사용한 예시이다.
class Person { public: virtual void work() { cout << "Person Work" << endl; } virtual string GetClassName() { return "Person"; } //Covered in later lecture //virtual void work() = 0; //virtual string GetClassName() = 0; //This is also ok }; class Student : public Person { public: void work() { cout << "Student studies" << endl; } string GetClassName() { return "Student"; } }; class Teacher : public Person { public: void work(){ cout << "Teacher teaches" << endl; } string GetClassName() { return "Teacher"; } }; int main() { Student s1; Teacher t1; s1.work(); cout << s1.GetClassName() << endl; t1.work(); cout << t1.GetClassName() << endl; Person* p_arr[] = { &s1, &t1 }; for (int i = 0; i < 2; i++) { p_arr[i]->work(); cout << p_arr[i]->GetClassName() << endl; } return 0; } ---- Student studies // up casting 안 한 결과 Student Teacher teaches Teacher Student studies // up casting 한 이후의 결과 Student Teacher teaches Teacher
up casting 하기 전과 후가 같게 동작함을 확인할 수 있다.
(질문 : 그럼 derived class를 그냥 사용하면 되지 왜 굳이 up casting을 거칠까? 인터넷에 따르면 다형성을 통해 코드의 재사용성을 높이기 위해서라고 한다. 이에 대해서는 나중에 다시 알아보자)
virtual의 개념은 deconstructor에서도 적용된다, 예시는 다음과 같다. up casting 이후에 본래의 deconstructor이 불려지지 않는 문제를 해결할 수 있다.
class Person { private: int* m_Id; string m_name; public: Person() { m_Id = new int(1111); m_name = "None"; cout << "Person constructor 1 called" << endl; }; Person(int id) { m_Id = new int(id); m_name = "None"; cout << "Person constructor 2 called" << endl; } virtual ~Person() { cout << "Person Destructor called" << endl; } }; class Student : public Person { public: Student() : Person(10) { cout << "Student constructor called" << endl; }; ~Student() { cout << "Student Destructor called" << endl; } }; class Teacher : public Person { public: Teacher() { cout << "Teacher constructor called" << endl; } ~Teacher() { cout << "Teacher Destructor called" << endl; } }; int main() { Teacher* t1 = new Teacher(); delete t1; Person* p_person = new Student(); //Upcasting delete p_person; return 0; } ---- Person constructor 1 called Teacher constructor called Teacher Destructor called Person Destructor called Person constructor 2 called Student constructor called Student Destructor called Person Destructor called
Pure Virtual Function

base class에서 function을 implementation할 수 없는 경우가 있다. 그럴 때는 pure virtual을 사용하면 된다.
(주의 : 이 때 ‘하나 이상’의 pure virtual를 가지는 class를 abstract class라고 한다. 추상적으로 밖에 디자인 되지 않은 class라는 것을 의미한다. 이 class 스스로는 instance, 즉 객체가 될 수 없다. function이 define되지 않았으니 당연하다)
다시 설명하자면 pure function은 디테일한 부분이 child class에서 정의되어야 할 때 사용된다. (자식 클래스에는 이러한 기능이 ‘꼭’ 정의되어 있어야 한다)
다음은 예시이다.
class Person { public: virtual void work( ) = 0; }; class Student : public Person{ public: //no implementation }; class Teacher : public Person{ public: void work( ) { } }; int main() { Person p1; // error due to the pure function Student s1; // error due to no implementation of the pure function Teacher t1; return 0; }
위 코드는 abstract class인 person의 객체를 생성하려하고 있다. 또한 pure function인 work를 teacher derived class에서 재정의 해주지 않고 있다. 따라서 위 코드에서는 error가 출력된다
S,L은 P의 자식, D는 S의 자식이라고 생각해보자.
P
^
S L
|
D
이 때 D를 불러올 때 S에만 work가 있으면 D에 work가 없어도 잘 동작한다. 만약 D에만 work가 있으면 어떨까? 이 때도 잘 동작한다. (메모는 이렇게 되어 있으나 확실하지 않으니 나중에 한 번 확인해보자)
Etc…
interface class : virtual function만을 가지는 class이다.

final function : 마지막 function이다. 자식 class에서는 더 이상 이 함수를 가질 수 없음을 의미한다.

final class : 더 이상 자식을 가질 수 없는 class이다.

override keyword : 함수가 override되었는지 안되었는지 확인해주는 키워드이다.
만약 override가 안되었다면 error를 출력시킨다. 따라서 override가 된 함수에서만 사용해야한다. 단순히 override가 된 함수인지 아닌지 시각적으로 빨리 판단하기 위해서 사용한다.

5. Exceptional Handling
Type of Errors
c++에서 발생하는 에러의 종류이다. 크게 세 가지로 나눌 수 있다.

Syntax error는 문법 오류이다. Run-time error는 실행 이후에 ‘어떠한 이유’로 생기는 에러이다 (내가 C++를 다룰 때 가장 발생하는 오류인 것 같다. 이를 잘 다루는게 중요하다. 이 오류는 에러를 출력시키지 않을 때도 있다). Semantic error 또는 Logical error는 의도에 맞지 않게 결과가 출력되는 오류를 의미한다.
예시 사진은 다음과 같다.

Try-Throw-Catch
위 error말고도 handling 해주어야 할 것들이 있다. (텀 프로젝트 과제로 주어졌던 ATM에서 handling 했던 것 처럼)

만약 위 사항들을 if else문으로 처리한다면 코드가 매우 복잡해지고, 길어진다는 단점이 있다.

- Try-Throw-Catch
Try-Throw-Catch 문법을 이용하면 이러한 것들을 좀 더 손쉽게 handling할 수 있다.
문법은 다음과 같다. try 안에서 error 사항을 잡고, catch 부분에서 error 사항을 처리하면 된다.

- multi-Try-Throw-Catch
다중 Try-Throw-Catch문은 다음과 같이 정의하면 된다. throw한 것과 일치하는 파라미터 타입을 가지고 있는 catch문에 들어가서 error 처리를 진행한다.
catch(…)는 위 throw가 위의 어떤 catch문에도 걸리지 않았을 때 실행된다. default 값으로 사용한다고 생각해도 된다. 만약 이것도 없다면 computer 내의 default를 수행한다. ( “terminate called after throwing an instance of ‘int’”라는 에러를 출력하며 종료할 것이다)

- nested trying and catching
밑처럼 복잡하게 얽혀있을 때 어느 부분에서 try-catch가 일어나는지 잘 살펴보아야 한다.

Example
Try-Throw-Catch문의 예시이다.
Example1 - Throw without Catch
#include <iostream> using namespace std; int main() { int age; cout << "Your age ? "; cin >> age; if (age < 0 || age >= 150) throw age; cout << "Age: " << age; }
이 예시에서는 throw 부분은 있지만 catch 부분이 없는 것을 확인할 수 있다. 위 상황에서 예외 사항 (ex 1111)이 입력된다면 “terminate called after throwing an instance of ‘int’”라는 에러가 출력되며 프로그램이 종료된다.
Example2 - Try-Throw-Catch
#include <iostream> using namespace std; int main() { int age; cout << "Your age ? "; try{ cin >> age; if (age < 0 || age >= 150) throw age; cout << "Age: " << age; } catch(int i) { cout << "Age( " << age << " ) is not valid. "; } }
위 코드에서는 catch 부분이 잘 정의되어 있다. 이 경우에는 Age( 1111 ) is not valid가 잘 출력된다. (만약 try 없으면 어떻게 될까? 오류가 생긴다. try-chach는 단짝이다)
Example3 - multiple catches
int main() { string name; int age; try { cout << "Your name ? "; cin >> name; if (name.length() > 10) throw name; cout << "Your age ? "; cin >> age; if (age < 0 || age >= 150)throw age; cout << "Age: " << age; } catch (int i) { cout << "Age( " << i << " ) is not valid. "; } catch (string s) { cout << "Name( " << s << " ) is not valid. "; } }
위 코드는 다중 에러 처리를 진행한다. name은 string 변수이니 catch (string s)에서 에러 처리가 될 것이다. age는 int 변수이니 catch (int i)에서 에러 처리가 될 것이다.
Etc…
- stack unwinding
함수 내에 catch가 없으면 main 함수로 돌아가서 catch문을 수행한다.
(이 현상을 ‘함수를 호출한 영역으로 예외 데이터가 전달되는 현상’ stacking unwiding, 스택 풀기 라고 한다)
예시는 다음과 같다.
//===== Ex1. ===== void func01() { throw “func01"; } int main(){ try{ func01(); } catch (const char* ex) { cout << "Exception at " << ex << endl; } } //===== Ex2. ===== void func03() { throw “func03"; } void func02() { func03(); } void func01() { func02(); } int main(){ try{ func01(); } catch (const char* ex) { cout << "Exception at " << ex << endl; } }
- MyException
아래와 같이 catch의 파라미터를 class의 포인터의 값으로 해서 사용할 수도 있다.
class MyException { int errNo; string errFunc, errMsg; public: MyException(int n, string f, string m): errNo{ n }, errFunc{ f }, errMsg{ m }{} virtual ~MyException() {} void what(){ cout << "Error[" << errNo << "] : " << errMsg << " at " << errFunc << endl; } }; class MyDivideByZero : public MyException{ public: MyDivideByZero(string f) : MyException(100, f, "Divide by Zero") {} }; int main() { int n1{ 10 }, n2{ 0 }; cin >> n2; try { if (n2 == 0) { throw MyException(100, "main()", "Zero"); //throw MyDivideByZero("main()"); //TEST } } catch (MyException& e) { e.what(); } }
Standard Exception Class / Etc(2)…
c++에는 이미 이러한 Exception을 handling하기 위한 class들이 준비되어있다.

- Example - Exception Class
밑은 이미 구현되어 있는 exception class를 이용해서 error를 handling한 예시이다.throw가 쓰이지 않았지만 내부적으로 throw해준다.
#include<exception> int main() { int nSize; char* arr; cout << "Enter Array Size: "; cin >> nSize; try { arr = new char[nSize]; cout << "Array (" << _msize(arr) << ") is created."; delete[] arr; } catch (bad_alloc & e) { cout << e.what() << endl; } }
- no except
no except는 예외를 던지지 않는 함수를 의미한다.
(시험에는 나오지 않은 것 같다. 나중에 시간 남으면 한 번 읽어보자)

- Uncatched Exception
default를 설정해주기 위한 방법 중 하나이다. catch(…) 또는 밑의 방법을 사용하면 된다.

6. Template
Templates

위 사진을 살펴보자. 위는 overloading과 overriding을 함께 사용했을 때 나타나는 예시이다.
overloading만을 사용했다면 hello()가 잘 출력되었을 것이다. 하지만 위해서는 overriding을 하기 위해서 virtual keyword를 사용하였다. 때문에 자식 클래스에서 파라미터가 없는 hello 함수를 출력했을 때 오류가 생긴다. 이는 Option1 또는 Option2를 통해서 해결할 수 있다.
Template는 이를 해결해 줄 수 있다. Templete를 이용하면 같은 기능을 하는 함수를 굳이 여러 파라미터에 대해 정의해주지 않아도 되고, 따라서 위와 같은 실수도 예방할 수 있게 된다.
문법은 다음과 같다. 맨 위에 template<typename T>를 적고, 사용할 때 Typename<Type> 으로 타입을 정해서 사용하면 된다.

다음은 함수에 대한 Template사용 예시이다.
void Swap(int& a, int& b) { int tmp; tmp = a; a = b; b = tmp; } void Swap(double& a, double& b) { double tmp; tmp = a; a = b; b = tmp; } =========================== 기존에는 위 처럼 정의했다. Template를 사용하면 밑 처럼 간략하게 정의할 수 있다. template<typename T> void Swap(T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } int main() { int a = 10, b = 5; Swap<int>(a, b); cout << “a=“ << a << “ b=“ <<b; double c = 1.0, d = 2.0; Swap<double>(c, d); cout << “\nc=“ << c << “ d=“ <<d; } ---- a=5b=10 c=2d=1
class에서도 template를 활용할 수 있다. 예시는 다음과 같다.
#include <iostream> template <typenameT> class Point { T x; T y; public: Point( T xx = 0, T yy = 0) : x(xx), y(yy) { } T getX() { return x; } T getY() { return y; } }; int main() { Point<int> pt_i{ 1, 2 }; cout << pt_i.getX() << endl; Point<double> pt_d{ 1.2, 3.4 }; cout << pt_d.getX() << endl ; }
변수 두 개를 이용할 수도 있다. 예시는 다음과 같다.
template<typename T1, typename T2> class Student { T1 id; T2 name; public: Student(T1 id, T2 name) : id(id), name(name) {} void Print() { cout << "id: " << id << " name: " << name; } }; int main() { Student<int, const char*> st1{ 201911000, "Alice" }; Student<const char*, const char*> st2{"A001", "Carol"}; st1.Print(); st2.Print(); }
변수 두 개를 사용하되 하나를 default 값으로 설정할 수도 있다. 예시는 다음과 같다.
(주의 : default는 무조건 두 번째 변수만 가능하다. 만약 T1을 default로 설정하고 T2를 default로 설정하지 않으면 오류가 발생하게 된다)
template<typename T1, typename T2 = const char*> class Student { T1 id; T2 name; public: Student(T1 id, T2 name) : id(id), name(name) {} void Print() { cout << "id: " << id << " name: " << name; } }; int main() { Student<int> st1{ 201911000, "Alice" }; Student<const char*> st2{"A001", "Carol"}; st1.Print(); st2.Print(); }
type 대신 value를 넣어줄 수도 있다. 예시는 다음과 같다.
#include <iostream> template<typename T, int dim> class PointND { T* coordinates; public: PointND() { coordinates = new T[dim]; } ~PointND() { delete[] coordinates; } }; int main() { PointND<double, 2> pt_2d; PointND<double, 3> pt_3d; }
template를 specialization할 수도 있다. 예시는 다음과 같다.
#include <iostream> #include <cstring> #include <string> template<typename T> T Add(T n1, T n2) { return n1 + n2; } template<> const char* Add<const char*>(const char* s1, const char* s2) { string str= s1; str += " "; str += s2; char* cstr = new char[str.length() + 1]; memcpy(cstr, str.c_str(),str.length()+1); return cstr; } int main() { cout << Add<int>(10, 20) << std::endl; cout << Add<double>(1.5, 2.5) << std::endl; cout << Add<const char *>("Hello", "World") << endl; }
char + char는 불가능하기 때문에 const char* Add 함수를 따로 만들어준 것이다.
그럼 다른 기능을 제공할거면 template 대신 overloading을 쓰는게 더 편하지 않을까?
챗지피티의 말로는 template은 컴파일 타임에 동작하기 때문에 더 효율적인 동작이 가능하다고 한다.
7. STL(1)
STL
STL Standard template library의 약자로 우리에게 제공되는 라이브러리이다.
algorithm, container(컨테이너가 무엇인지는 뒤에서 다룰 것이다), functions, iterators 4가지 요소를 우리에게 제공해준다.
container란 object를 담을 수 있는 통을 의미한다. 이는 자료형을 섞어서 담을 수 있는 general한 array이다. 파이썬의 list와 비슷하다고 이해해도 될 것 같다. 어떤 type도 담을 수 있어야 하므로 templete로 구현한다.
STL이 제공해주는 것을 다이어그램으로 표현하면 다음과 같다.

Container
특징은 위에서 언급했듯이 다음과 같다.
- object의 holder이다.
- templates으로 구현한다.
- 메모리 관리와 접근은 iterators로 한다.
다음은 컨테이너의 종류이다.
- sequential container
순차적 접근이 가능한 container이다. (ex. vector, list, foward_list…)
- associative container
sort된 data를 바탕으로 빠르게 search할 수 있는 container이다. (ex. set, multiset, map…)
- unordered container
hashed된 data를 바탕으로 빠르게 search할 수 있는 container이다. (ex. unordered_set, unordered_map…)
다음은 우리가 사용할 vector를 포함하는 Sequential container에 대한 자세한 설명이다.

push_back은 append 역할을 하는 함수이다. size는 len 역할을 하는 함수이다.

insert를 하면 linked list 방식으로 element가 삽입된다.

Container - Vector

위와 같은 종류의 constructor가 있다. template를 이용했기 때문에 앞에 <type>을 붙여서 타입을 정의해준다.

vertor에는 여러 method들이 있다.
at : arr[i]와 유사한 역할을 한다. (arr[i]를 사용해도 된다)
back/front : 마지막/앞을 구경한다.
capacity : byte 수를 세준다. (이건 사용을 지양하자)
size: len() 역할을 한다.
empty : empty인지 아닌지 알려준다.
shrink_to_fit : un use memory를 삭제해준다.
push_back : 맨 뒤에 원소를 삽입해준다.
pop_back : 맨 뒤에껄 뺀다.
erase : 모두 지운다.
operator : at과 비슷한 역할을 한다.
clear : erase와 비슷한 역할을 한다.
insert(위치, 원소) : 원하는 위치에 원소를 삽입한다.
begin() : 시작
end() : 끝 (end-1까지 돈다. 파이썬과 비슷하다)
색으로 표시해 둔 것들은 iterator가 필요한 것들이다.
다음은 vector의 사용 예시이다.
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v1; v1.push_back(1); v1.push_back(2); cout << "At 0: " << v1.at(0) << "\tAt 1: " << v1.at(1) << endl; cout << "Front" << v1.front() << "\tBack" << v1.back() << endl; cout << "V1 capacity " << v1.capacity() << "\tV1 Size : " << v1.size() << endl; vector<int> v2(v1); cout << "V2 capacity " << v2.capacity() << "\tV2 Size: " << v2.size() << endl; v1[0] = 3; v1.pop_back(); cout << "first loop" << endl; for (int& a : v1) { cout << a << " "; } v1.push_back(4); cout << "\nsecond loop" << endl; for (int& a : v1) { cout << a << " "; } v1.clear(); cout << "\nthird loop" << endl; for (int& a : v1) { cout << a << " "; }} ---- first loop 3 second loop 3 4 third loop
Iterator
포인터와 바슷하게 동작한다.
각 요소에 대한 접근을 제공하는 ‘객체’이다.
다음 예시를 보면 포인터와 비슷하게 동작함을 확인할 수 있다. vector<int>::iterator의 형태로 선언하고 사용해야 한다.

예제 코드이다.
#include <iostream> #include <vector> using namespace std; int main(){ vector<int> v{ 1,2,3,4 }; vector<int>::iterator iter; for (iter = v.begin(); iter != v.end(); ++iter) // 전위 연산자를 쓰고 있다. { cout << *iter << " "; *iter -= 1; } vector<int>::const_iterator citer{ iter }; // cout << *iter; for (citer = v.begin(); citer != v.end(); ++citer) // 만약에 end일 때 까지로 설정한다면 run-time error가 생기게 된다 { cout << *citer << " "; //*citer -= 1; } }
for문 안에 밑의 코드를 삽입하여 동작 시킬 수도 있다.
if (*iter == 3) iter = v.erase(iter) ---- iter = v.begin() v.insert(iter,s)
for문을 위의 방식 말고도 다르게 정의할 수 있다.
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v{ 1,2,3,4 }; // for (int i=0; i < v.size(); ++i) { v[i] … } for (vector<int>::iterator iter = v.begin(); iter != v.end(); ++iter) { cout << *iter << " "; } for (auto iter = v.begin(); iter != v.end(); ++iter) { cout << *iter << " "; } for (auto& e : v) cout << e << " "; }
8. STL(2)
Container - Maps
Map을 포함하고 있는 associative container에 관한 설명이다.
파이썬의 dictionary와 같은 형태를 가지고 있다. 이 특성을 활용해서 좀 더 빠른 탐색이 가능하도록 하였다.

다음과 같은 형태로 사용할 수 있다.

지금까지는 vector와 map에 대해서 알아보았다. 이 외에도 다양한 container들이 있다. 밑 표는 각 container가 수행하는 작업에 대한 시간 복잡도를 나타낸 것이다.

Algorithms
STL에서 제공하는 알고리즘을 가져다 쓸 수도 있다.
다음 표를 참고해서 사용하면된다.
PPT에 다양한 예제들이 있으니 참고하도록 하자.

