배움 저장소

[홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해 본문

Programming Language/C++

[홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해

시옷지읏 2021. 12. 26. 23:58

10.1 객체들의 관계

Object Relationship


객체 지향 프로그램 설계 순서

(1) 프로그램이 수행하는 기능을 정의

(2) 기능을 수행하는데 필요한 여러 객체를 정의하고 상호작용을 결정

 

객체의 관계 다음과 같은 특성으로 구분할 수 있다

(1) 속한다: 객체 내부의 Instance를 다른 곳에서도 사용하는가?

(2)    관리: 해당 객체의 소멸을 관계있는 객체가 결정하는가?

(3) 방향성: 객체가 다른 객체의 멤버변수/함수를 활용하는기?

 

10.2 구성 관계

Composition


다음 예제에서 class Monster와 class Position2D는 Composition(구성관계)이다

 main.cpp

#include "Monster.h"
using namespace std;

int main() {
    // Goblin Move
    Monster mob1("Goblin", 10, 15);
    cout << mob1 << endl;          // >> Goblin(10, 15)

    mob1.MoveTo(5,5);
    cout << mob1 << endl;          // >> Goblin(5, 5)

    // Slime Move
    Position2D location(30,40);
    Monster mob2("Slime", location);
    cout << mob2 << endl;          // >> Slime(30, 40)

    mob2.MoveTo(Position2D(20,40));
    cout << mob2 << endl;          // >> Slime(20, 40)
}

Monster.h

#pragma once
#include <string>
#include "Position2D.h"

class Monster {
private:
    std::string m_name; // class, char* + unsigned 
    //int m_x,m_y;        // How about implement this to class?
    Position2D m_location;

public:
    Monster(const std::string &name, Position2D location):m_name(name), m_location(location) {}
    Monster(const std::string &name, const int& x, const int& y) :m_name(name), m_location(Position2D(x, y)) {}

    void MoveTo(const int& x, const int& y) { m_location.set(x,y); }
    void MoveTo(Position2D location) { m_location.set(location); }

    friend std::ostream& operator <<(std::ostream& out, const Monster& monster) {
        out << monster.m_name << monster.m_location;
        return out;
    }
};

Position2D.h

#pragma once
#include <iostream>

class Position2D {
private:
    int m_x, m_y;
public:
    Position2D (const int& x, const int& y):m_x(x), m_y(y){};

    void set(const int& x, const int& y) {
        m_x = x;
        m_y = y;
    }
    void set(const Position2D& location){
        // m_x = location.m_x; 
        // m_y = location.m_y;
        set(location.m_x, location.m_y); // easy to management
    }

    friend std::ostream& operator <<(std::ostream& out, const Position2D& position2d) {
        out << "(" << position2d.m_x << "," << position2d.m_y << ")";
        return out;
    }
};

 

10.3 집합 관계

Aggregation


composition 관계에서 내부 객체의 정보를 다른 객체와 공유하지 않는다

- composition 관계를 구현할 때 인자 혹은 매개변수는 복사되어 사용된다. 아래 Instance "hermione"은 Instance "lect1"과 Instance "lect2"에서 사용된다. 이 때 Lecture 내부에서 Hermione Instance 정보를 어떻게 변경하든 원본은 변하지 않는다. 

Student hermione("Hermione", 2);

Lecture lect1("Lecture One");
lect1.assignTeacher(Teacher("Prof. Snape"));
lect1.registerStudent(hermione);

Lecture lect2("Lecture Two");
lect2.assignTeacher(Teacher("Prof. Hong"));
lect2.registerStudent(hermione);

 

외부에서 가져온 객체를 사용할 때 원본값도 함께 바꾸어주려면 포인터를 활용해주면 된다. 이와같이 외부 객체와 상호작용하는 객체의 관계를 Aggregation(집합관계)라 한다. 다음은 두 객체의 집합관계를 표현한 예제이다.

 이 때 참조는 사용할 수 없다. 참조는 선언과 동시에 참조할 대상이 정해져야 하기 때문이다.

 

 main.cpp

#include "Lecture.h"
using namespace std;

int main() {
    Teacher prof_snape("Prof. Snape");
    Teacher prof_hong("Prof. Hong");
    Student potter("Potter", 0);
    Student ron("Ron", 1);
    Student hermione("Hermione", 2);

    Lecture lect1("Lecture One");
    lect1.assignTeacher(&prof_snape);
    lect1.registerStudent(&potter);
    lect1.registerStudent(&ron);
    lect1.registerStudent(&hermione);

    Lecture lect2("Lecture Two");
    lect2.assignTeacher(&prof_hong);
    lect2.registerStudent(&potter);
    {
        cout << lect1 << endl;
        cout << lect2 << endl;

        lect1.IncreaseGrade();
        lect2.IncreaseGrade();

        cout << lect1 << endl;
        cout << lect2 << endl;
    }
}

위에서 Teacher와 Student는 지역변수이다. 값을 유지하려면 동적할당을 사용하자

Teacher *prof_snape = new Teacher("Prof. Snape");
Teacher *prof_hong = new Teacher("Prof. Hong");
Student *potter = new Student("Potter", 0);
Student *ron = new Student("Ron", 1);
Student *hermione = new Student("Hermione", 2);

// ...

delete everything above;

 

Lecture.h

  아래 예제에서 포인터 대신 참조를 사용하려면 초기화 될 때 참조값이 결정되어야 한다.

#pragma once
#include "Student.h"
#include "Teacher.h"
#include <vector>

class Lecture{
private:
    std::string m_name;
    //Teacher teacher;               // copied value 
    //std::vector<Student> students; // can't interact with outside

    Teacher *teacher;
    std::vector<Student*> students;

public:
    Lecture(const std::string& name = "None_class"):m_name{name}, teacher{nullptr} {}
    ~Lecture() {}

    //void assignTeacher(const Teacher& const teacher_input) { teacher = teacher_input; } copied val
    //void registerStudent(const Student& const student) { students.push_back(student); } copied val
    
    void assignTeacher(Teacher* const teacher_input) { teacher = teacher_input; }
    void registerStudent(Student* const student) { students.push_back(student); }

    void IncreaseGrade() {
        std::cout << "After Study, Students get one more grade" << std::endl;
        for (auto*& student : students) 
        { student->setGrade(student->getGrade() + 1); }
        //{ (*student).setGrade((*student).getGrade()+1); } // same as above
    }

    friend std::ostream& operator << (std::ostream& out, const Lecture& lecture) {
        out << "Lecture name" << lecture.m_name << std::endl;
        out << *(lecture.teacher) << std::endl;
        for(unsigned int i=0; i<lecture.students.size(); ++i){ 
            out << *(lecture.students[i]);
            if(i != lecture.students.size()-1){ out << std::endl; }
        }
        return out;
    }
};

Student.h

#pragma once
#include <string>
#include <iostream>

class Student{
private:
    int m_grade;
    std::string m_name;
public:
    Student(const std::string& name="None_Student", const int& grade = 0) :m_name(name), m_grade(grade) {}
    void setName(const std::string& name) { m_name = name; }
    const std::string& getName() { return m_name; }

    void setGrade(const int& grade) { m_grade = grade; }
    const int& getGrade() { return m_grade; }

    friend std::ostream& operator << (std::ostream& out, const Student& student) {
        out << student.m_name << " " << student.m_grade;
        return out;
    }
};

Teacher.h

#pragma once
#include <string>
#include <iostream>

class Teacher {
private:
    std::string m_name;
public:
    Teacher(const std::string& name="None_Teacher") :m_name(name) {}
    void setName(const std::string &name) { m_name = name; }
    const std::string& getName(){ return m_name; }

    friend std::ostream& operator << (std::ostream& out, const Teacher &teacher) {
        out << teacher.m_name;
        return out;
    }
};

 

vector<type> VS vector<type*>

 vector<type>을 선언한 이후 vector.push_back( value )에서 reference 매개변수를 value에 입력하였다. reference 매개변수를 입력하였으므로 저장된 값은 원본과 메모리를 공유할 것으로 예상했다. 예상은 틀렸다. 복사된 값이 저장되었다.어떤 값이 vector 내부에 저장되는지는 선언 때 선택한 자료형을 따른다.

 

  vector<type>는 동적할당 메모리에 해당 값을 복사하여 넣는다

void Test(int& i) {
    cout << & i << endl;   // >> 008FFE4C

    vector<int> v;
    v.push_back(i);
    cout << &v[0] << endl; // >> 00A64C38
}

int main() {
    int x = 5;
    cout << &x << endl;    // >> 008FFE4C
    Test(x);
}

 

vector<type*>는 동적할당 메모리에 해당 값(주소)를 넣는다

int x = -1;
const int const_x = 10;
cout << &x << " " << &const_x << endl;              // >> 00D3FB3C 00D3FB30

vector<const int*> v_pointer;   
v_pointer.push_back(&x);
v_pointer.push_back(&const_x);
cout << v_pointer[0] << " " << v_pointer[1] << endl;// >> 00D3FB3C 00D3FB30

 

분산처리(서버나 네트워크에서 사용)를 할 때는 저장공간이 분리되어 있어 데이터(student or teacher) 사본이 여러군데 존재해야 한다. 한 곳에서 특정 데이터를 업데이트하면 동기화 작업이 별도로 필요하다. 이를 고려하지 않고 집합관계로 구현하면 의도하지 않은 결과를 만날 수 있다

 

 

10.4 제휴 관계

Association


한 코드 파일 내에서 두 객체가 서로를 내부 변수로 사용하려면 한 객체는 전방선언을 해주어야 한다. 전방선언은 디버깅을 어렵게 만들지만 제휴관계일 때 피하기 어렵다. 만약 헤더-코드파일로 나눈다면 헤더 파일에서 서로를 전방선언을 해주어야 한다. 전방선언을 피하기 위하여 두 class를 관리해주는 새로운 class를 만들기도 한다

 

다음은 예제 코드이다

#include <string>
#include <iostream>
#include <vector>
using namespace std;

class Doctor; // forward declaration

class Patient {
private:
    std::string m_name;
    std::vector<Doctor*> m_doctors;
public:
    friend class Doctor;

    Patient(const std::string& name) :m_name(name) {}
    void addDoctor(Doctor* const doctor) { m_doctors.push_back(doctor); }

    void meetDoctor();    // below code makes error!
    //void meetDoctor() { // Error! Can't figure out Doctor's member var
    //    for (auto& doctor : m_doctors) 
    //    { cout << "meet Doctor :" << doctor->m_name << endl; }
    //}
};

class Doctor {
private:
    std::string m_name;
    std::vector<Patient*> m_patients;
public:
    friend class Patient;

    Doctor(const std::string& name) :m_name(name) {}
    void addPatient(Patient* const patient) { m_patients.push_back(patient); }
    void meetPatient() { // OK, knowing patient's member var
        for (auto*& patient : m_patients) 
        { cout << m_name << " meet Patient :" << patient->m_name << endl; }
    }
};


void Patient::meetDoctor() {
    for (auto*& doctor : m_doctors)
    { cout << m_name << " meet Doctor :" << doctor->m_name << endl; }
}

int main() {
    Patient* p1 = new Patient("Kim");
    Patient* p2 = new Patient("Jason");
    Patient* p3 = new Patient("Christina");

    Doctor* d1 = new Doctor("Doctor Lee");
    Doctor* d2 = new Doctor("Doctor Huberman");

    p1->addDoctor(d1); d1->addPatient(p1);
    p2->addDoctor(d2); d2->addPatient(p2);

    p3->addDoctor(d1); d1->addPatient(p3);
    p3->addDoctor(d2); d2->addPatient(p3);
    
    p1->meetDoctor();
    p2->meetDoctor();
    p3->meetDoctor();

    d1->meetPatient();
    d2->meetPatient();

    delete p1; delete p2; delete p3;
    delete d1; delete d2;
}

 

Reflecive Association

- Instance가 자신과 동일한 자료형 Instance를 멤버변수로 가질 때이다.

 

 

10.5 의존 관계

Dependencies


객체 내부 멤버함수에서 필요한 특정 객체를 사용할 수 있다. 이 때 특정 객체를 부-객체, 멤버함수를 가지고 있는 객체를 주-객체라 부르자. 의존관계에서 부 객체는 주 객체의 멤버변수가 아니다. 부 객체는 임시로 주 객체 내부에서 생성되었다 사라진다. 따라서 주 객체의 선언에 부 객체는 나타나지 않는다.

 

main.cpp

#include "Worker.h"

int main() {
    Worker worker;
    worker.TimeCost();
}

Worker.h

#pragma once
class Worker {
public:
    void TimeCost();
};

Worker.cpp

#include "Worker.h"
#include "Timer.h"

void Worker::TimeCost() {
    Timer timer; // Start elapse

    timer.elapse(); // End elapse
}

Time.h

#pragma once
#include <iostream>
#include <chrono> // the god of time

class Timer {
    using clock_t = std::chrono::high_resolution_clock;
    using second_t = std::chrono::duration<double, std::ratio<1>>;
    std::chrono::time_point<clock_t> start_time = clock_t::now(); // capture current time for start
public:
    void elapse() {
        std::chrono::time_point<clock_t> end_time = clock_t::now(); // capture current time for end
        std::cout << std::chrono::duration_cast<second_t>(end_time - start_time).count() << std::endl;
    }
};

 

10.6 컨테이너 클래스

Container Classes


C++는 다양한 standard library container를 제공하고 있다. https://en.cppreference.com/w/cpp/container

직접 구현해보자

#include <iostream>
#include <initializer_list>
#include <cassert>
using namespace std;

class IntArray {
private:
    int m_length;
    int *m_data;

public:
    IntArray(const initializer_list<int>& list){
        initialize(list);
        cout << "created! on " << &m_data << " val:" << m_data << endl;

    }
    ~IntArray() {
        if(m_data != nullptr) delete [] m_data;
        cout << "deleted on " << &m_data << " val:" << m_data << endl;

        m_data = nullptr;
    }

    void initialize(const initializer_list<int>& list){
        m_length = list.size();
        m_data = new int[m_length];

        int i = 0;
        for (auto& e : list) {
            m_data[i] = e;
            ++i;
        }
    }
    
    void reset() {
        m_length = 0;

        if(m_data == nullptr) return;

        delete[] m_data;
        m_data = nullptr;
    }

    void resize(int size) {
        int *temp = new int[size];
        m_length = (m_length < size)? m_length : size;

        if (m_data != nullptr) {
            for (int i = 0; i < m_length; ++i){
                temp[i] = m_data[i];
            }
        }

        for (int i = m_length; i < size; ++i){ 
            temp[i] = 0; 
         }

        if (m_data != nullptr) delete [] m_data;
        m_length = size;
        m_data = temp;
    }

    void insertBefore(const int& value, const int& index) {
        assert(index < m_length+1);

        resize(m_length+1);

        for (int i = m_length-1; index<i; --i) {
            m_data[i] = m_data[i-1];
        }
        m_data[index] = value;
    }

    void remove(const int &index) {
        assert(index<m_length);

        int *temp = new int[m_length-1];

        for (unsigned int i = 0; i < index; ++i) {
            temp[i] = m_data[i];
        }

        for (unsigned int i = index; i<m_length-1; ++i) {
            temp[i] = m_data[i+1];
        }

        delete [] m_data;
        m_data = temp;
        m_length = m_length-1;
    }

    void push_back(const int& value) {
        resize(m_length+1); // ++m_length;
        m_data[m_length-1] = value;
    }

    friend ostream& operator << (ostream& out, const IntArray& intarray) {
        cout << "size = " << intarray.m_length << " add=" << intarray.m_data << "  ";
        for(int i=0; i< intarray.m_length; ++i)
            out << intarray.m_data[i] << " ";
        return out;
    }
};

int main() {
    IntArray test{ 0,1,2,3 };
    cout << test << endl;

    test.remove(0);
    cout << test << endl;

    test.insertBefore(-1,0);
    cout << test << endl;

    test.push_back(10);
    cout << test << endl;

    test.reset();
    cout << test << endl;

    test.insertBefore(100,0);
    cout << test << endl;
}

 

배운점

- 아래 << 연산자 오버로딩을 구현할 때 IntArray를 참조로 가져오지 않아 얕은복사가 실행되었다. 이 함수가 종료되고 나면 소멸자 호출된다. 얕은복사가 된 IntArray 내부 동적할당 메모리가 해제된다. 나중에 원본이 삭제될 차례가 되면 이미 해체된 동적할당 메모리를 다시 한 번 해제하게 되므로 에러가 발생한다.

friend ostream& operator << (ostream& out, IntArray intarry) {
    for(int i=0; i<intarry.m_length; ++i)
        out << intarry.m_data[i] << " ";
    return out;
}

 

정리

Comments