CS/C++

C++ 공부 섹션10 객체관계 : 홍정모의 따배씨쁠쁠

샤아이인 2022. 1. 16.

내돈내고 내가 공부한것을 올리며, 시간을 들여 배운과정을 복습하기보다는 간결히 공부한 흔적은 남긴다 하고 생각하고 써갈 예정입니다. 모든 내용을 이곳에 올릴수는 없으며, 그중 기억남은 몇가지 내용 위주만 올리겠다.

1) 섹션9

​확실히 생각을 다시 곱씹어가며 한줄 한줄 블로그에 정리하며 공부하는 것 크게 도움되는 것 같다.

이번 시간에는 객체지향에 대한 보편적인 사용법과 방식들을 공부하게 되었다. class와 object들의 관계를 위주로 배우게 되었다.

◆ 10-1 class에는 크게 4가지의 관계들이 있었다. 구성, 집합, 연계, 의존 관계가 있었으며 각각에 대해서는 코드를 보면 더욱 명확히 히해할수 있다.

출처 - 홍정모의 따배씨쁠쁠 강의

간단히 실생활 예로 먼저 생각해보면,

1. 구성: 뇌(부품)와 사람(전쳬) 에서는 뇌가 사람의 일부를 구성하는 관계이다.

2. 집합: 자동차(부품)은 사람(전체)이 소유하는 것으로 일부분이라 할수 있다.

3. 연계: 환자와 의사는 서로 상호 의존적이다. 환자는 의사의 의술이 필요하며, 의사는 환자가 내는 비용으로 살아간다.

4. 의존: 목발을 생각하면 될것같다. 필요한 경우에만 사용하며, 다 치료되어 사용하지 않아도 목발은 사라지지 않는다.

 

 10-2 구성관계

하나의 monster class와 그에 맞는 헤더 파일이 하나 존재한다.

 

monster.h

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

using namespace std;

class Monster {
	string m_name;
	int m_x; // 위치정보
	int m_y;
public:
	Monster(const string name_in, const int& x_in, const int& y_in)
		: m_name(name_in), m_x(x_in), m_y(y_in) {}

	void moveTo(const int& x_target, const int& y_target)
	{
		m_x = x_target;
		m_y = y_target;
	}

	friend ostream& operator << (ostream& out, const Monster & monster) {
		out << monster.m_name << " " << monster.m_x << " " << monster.m_y << endl;
		return out;
	}
};
 

monster.cpp

#include "monster.h"

int main()
{
	Monster mon1("simson", 0, 0);

	// game 진행중
	{
		// event 발생
		mon1.moveTo(1, 1);
		cout << mon1 << endl;
	}

	return 0;
}
 

위의 코드를 이용하면 monster 객체를 생성할수 있고, 특정 이벤트가 발생하면 몬스터의 위치를 moveTo 멤버함수를 통해 이동시킬 수가 있다.

하지만 여기서 멈추지 않고 위치정보를 하나의 sub_class로 만들면 어떨까?

위에서 사용하는 std::string도 사실은 char* data와 length 정보를 갖고있으며, 이로인해 함수의 인자로 보내거나, 초기화 할때 간편해 짐을 알 수 있다. 어짜피 위치정보 x와 y는 항상 같이다니며 사용될 것이며, monster 뿐만 아니라 다른 class에서도 재사용 할수 있도록 따로 class로 구현해주는 것 이다.

 

< 변경후 >

position2d.h 의 구현

#pragma once
#include <iostream>

using namespace std;

class position2d { // 이 class는 몬스터뿐만 아니라 다른 기사, 왕 등의 class에서 재사용 가능
	int m_x;
	int m_y;
public:
	position2d(const int& x_in, const int& y_in) 
		: m_x(x_in), m_y(y_in) {}

	void set(const position2d& pos_target) {
		set(pos_target.m_x, pos_target.m_y); // 기존의 set함수 재사용, 이후 수정에서도 편리하다
	}

	void set(const int& x_target, const int& y_target) {
		m_x = x_target;
		m_y = y_target;
	}

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

monster.h

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

using namespace std;

class Monster {
	string m_name;
	position2d m_location;  // 위치정보 
public:
	Monster(const string name_in, const position2d& pos_in)
		: m_name(name_in), m_location(pos_in) {}

	void moveTo(const position2d& pos_target)
	{
		m_location.set(pos_target);
	}

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

monster.cpp

#include "monster.h"

int main()
{
	Monster mon1("Simson", position2d(0, 0));

	cout << mon1 << endl;

	// game 진행중
	{
		// event 발생
		mon1.moveTo(position2d(1,1));
		cout << mon1 << endl;
	}

	return 0;
}
 

새로만든 position2d(부품)은 monster(전체)의 구성원인 구성관계가 된다.

 

부품의 입장에서는 전체가 어떻게 작동하는지 알필요가 없다. 부품 스스로의 기능만 충실하게 작동하면된다.

이와 마찬가지로 상위 class인 monster는 자기가 뭘 할지만 알면 sub_class를 사용하면 될 뿐 어떻게 작동하는지는 알필요가 없다.

이렇게 기능을 분리 했다는 것이 중요하다.

 

이렇게 분리된 기능을 갖는 position2d class는 다른 왕, 기사 등의 새로운 class를 생성할때 재사용 될 수 있다.

 

또한 m_location은 monster의 이름에 관심이 없다. monster 객체가 하나 생성될때 위치 정보를 갖게되며, 객체가 사라지면서 위치 정보 또한 함께 사라지게 된다. 이는 어쩌면 구성 관계(composition)의 중요한 특징이다.

 

 10-3 집합 관계

composition관계로 class를 구현할경우 단점이 생기게 된다. 만약 jiwoo라는 학생(지능:0)이 있다 해보자.

수업1 인스턴스와 수업2 인스턴스에 모두 포함된 jiwoo는 수업1에서 지능이 1로 증가하면 수업2에서도 지능이 1이여야 한다.

하지만 composition관계에서는 이렇게 작동하지 않는다.

 

수업1에 속한 jiwoo학생은 지능이 1 이지만, 수업 2에서의 jiwoo 학생은 지능이 0으로 바뀌지 않는 것 이다. 이러한 문제점이 composition관계에 존제한다.

 

우선 composition의 코드를 확인후 변경된 aggregation의 코드를 확인해 보자.

 

teacher.h

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

using namespace std;

class Teacher {
	string m_name;
public:
	Teacher(const string &name_in) : m_name(name_in) {}

	void setName(const string& name_in) { m_name = name_in; }
	string getName() { return m_name; }
	friend ostream& operator << (ostream& out, const Teacher& teacher) {
		out << teacher.m_name;
		return out;
	}
};
 

student.h

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

using namespace std;

class Student {
	string m_name;
	int m_intel;
public:
	Student(const string& name_in = "No Name", const int &intel_in = 0)
		: m_name(name_in), m_intel(intel_in) {}

	void setName(const string& name_in) { m_name = name_in; }
	void setIntel(const int& intel_in) { m_intel = intel_in; }
	int getIntel() { return m_intel; }

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

lecture.h

#pragma once
#include <vector>
#include "student.h"
#include "teacher.h"

class Lecture {
	string m_name;
	Teacher teacher;
	vector<Student> students; // 학생 여러명
public:
	Lecture(const string& name_in) : m_name(name_in) {}
	~Lecture() {} // 뒤에서 사용예정

	void assignTeacher(const Teacher& const teacher_input){
		teacher = teacher_input;
	}

	void registerStudent(const Student& const student_input){
		students.push_back(student_input);
	}

	void study(){
		cout << m_name << "Study " << endl << endl;

		for (auto& element : students) // reference로 element받음, value로 받아오면 값이 업데이트가 안된다.
			element.setIntel(element.getIntel() + 1);
	}

	friend std::ostream& operator << (std::ostream& out, const Lecture& lecture){
		out << "Lecture name: " << lecture.m_name << endl;

		out << lecture.teacher << endl;
		for (auto element : lecture.students)
			out << element << endl;

		return out;
	}
};
 

source.cpp

#include <iostream>
#include <vector>
#include <string>
#include "Lecture.h"

int main()
{
	using namespace std;

	Student std1("Jack Jack", 0);
	Student std2("Dash", 1);
	Student std3("Violet", 2);

	Teacher teacher1("Prof. Hong");
	Teacher teacher2("Prof. Good");

	//composition relationship
	Lecture lec1("Introduction to computer programming");
	lec1.assignTeacher(Teacher("Prof. Hong"));
	lec1.registerStudent(Student("Jack Jack", 0));
	lec1.registerStudent(Student("Dash", 1));
	lec1.registerStudent(Student("Violet", 2));

	Lecture lec2("Computational Thinking");
	lec2.assignTeacher(Teacher("Prof. Good"));
	lec2.registerStudent(Student("Jack Jack", 0));

	{
		cout << lec1 << endl;
		cout << lec2 << endl;

		//event
		lec2.study();

		cout << lec1 << endl;
		cout << lec2 << endl;
	}

	return 0;
}
 

위의 코드를 실행시키면 Computational Thinking의 jack jack은 지능이 1 증가했지만, Introduction to computer programming에서는 0으로 유지된다.

< 변경후 >

이를 이제 aggregation관계로 바꿔보자.

 

위의 코드에서 lecture 헤더를 보면 vector로 students(복수)의 정보를 담고 있다. 이는 value값의 벡터형식이다.

class Lecture {
	string m_name;
	Teacher teacher;
	vector<Student> students; // 학생 여러명
public:
 

value의 값은 registerStudent(const Student& const student_input) 에서 참조형태로 인자가 전달된다. 다음코드를 봐보자!

	void registerStudent(const Student& const student_input){
		students.push_back(student_input);
	}
 

문제는 이렇게 참조형으로 전달된 인자가 students.push_back(student_input); 의 인자로 전달될때 복사되어 값이 들어간다는 점이다

따라서 값을 복사하지말고, 포인터를 이용하여 포인터 벡터를 만든다면 역참조를 통하여 원본에서도 값의 수정이 가능해진다.

 

또한 기존의 composition에서는 lecture의 인스턴스가 사라지면 그에 속한 학생과 선생이 모두 사라졌지만, student의 포인터를 활용한다면 가리키는 포인터는 사라질 지언정 대상인 값 자체는 사라지지 않는다.

 

lecture.h

#pragma once
#include <vector>
#include "student.h"
#include "teacher.h"

class Lecture {
	string m_name;
	Teacher* teacher; // 포인터를 받음
	vector<Student*> students; // 포인터 벡터
public:
	Lecture(const string& name_in) : m_name(name_in) {}
	~Lecture() {} // 뒤에서 사용예정

	void assignTeacher(Teacher* const teacher_input){ // 주소를 받음
		teacher = teacher_input;
	}

	void registerStudent(Student* const student_input){ // 주소를 받음
		students.push_back(student_input);
	}

	void study(){
		cout << m_name << "Study " << endl << endl;

		for (auto& element : students) // reference로 element받음, value로 받아오면 값이 업데이트가 안된다.
			(*element).setIntel((*element).getIntel() + 1); // 값을 역참조로 받아옴
	}

	friend std::ostream& operator << (std::ostream& out, const Lecture& lecture){
		out << "Lecture name: " << lecture.m_name << endl;

		out << lecture.teacher << endl;
		for (auto element : lecture.students)
			out << *element << endl; // 역참조 하여 출력

		return out;
	}
};
 

따라서 main에서 다음과 같이 주소값을 전달해 주면 된다.

출처 - 홍정모의 따배씨쁠쁠 강의

 

 10-4 제휴 관계

Association는 서로 동등한 자격을 갖고있기에 서로 종속이 되거나 집합관계가 되지 않는다.

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

class Doctor; //forward declaration

class Patient
{
	string m_name;
	vector<Doctor*> m_doctors; // 의자들의 주소 벡터
public:
	Patient(string name_in)
		: m_name(name_in) {}

	// 만나야할 의사 기록
	void addDoctor(Doctor* new_doctor){
		m_doctors.push_back(new_doctor);
	}

	// 만나야할 의사 출력 
	void meetDoctors(){
		for (auto& ele : m_doctors)
		{   
			cout << "Meet doctor : " << ele->m_name << endl;
		}
	}

	friend class Doctor;
};

class Doctor
{
	string m_name;
	vector<Patient*> m_patients;
public:
	Doctor(string name_in)
		: m_name(name_in){}

	//의사는 진료할 환자목록이 필요
	void addPatient(Patient* new_patient){
		m_patients.push_back(new_patient);
	}

	void meetPatients(){
		for (auto& ele : m_patients)
		{
			cout << "Meet patient : " << ele->m_name << endl;
		}
	}

	friend class Patient; // patient에서 doctor의 멤버에 접근 가능
};

int main()
{
	Patient* p1 = new Patient("Jack Jack");
	Patient* p2 = new Patient("Dash");
	Patient* p3 = new Patient("Violet");

	Doctor* d1 = new Doctor("Doctor K");
	Doctor* d2 = new Doctor("Doctor L");

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

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

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

	//patients meet doctors
	p1->meetDoctors();

	//doctors meet patients
	d1->meetPatients();

	//delets
	delete p1;
	delete p2;
	delete p3;

	delete d1;
	delete d2;

	return 0;
}
 

문제 1)

우선 전방선언으로 Patient class위에 Doctor를 선언해 주었다. 따라서 뒤이은 Patient class에서는 Doctor라는 class까지는 존제하는것을 알고있다. 하지만 다음 코드에서 문제가 생긴다.

위의 코드는 ele->m_name에서 오류가 발생하고 있다. 

Doctor를 전방 선언해주어서 Doctor라는 class가 있다는 것은 알지만 Doctor class 외부에서 Doctor의 멤버변수인 string m_name에 접근할수가 없는 것 이다. 

이러한 문제는 Doctor class안에 friend class Patient; 를 선언해줌으로써 해결된다.

 

문제 2)

하지만 아직도 문제가 있다. 전방선언은 되어있지만 코드는 위에서 부터 읽어오기때문에 ele->m_name을 읽는 시점에서는 멤버변수로 m_name이 있다는것을 알 방법이 없다. 따라서 코드를 분리해 줘야 한다. 다음과 같이 코드를 바꿔주면 된다.

class Doctor; //forward declaration

class Patient
{
	string m_name;
	vector<Doctor*> m_doctors; // 의자들의 주소 벡터
public:
	Patient(string name_in)
		: m_name(name_in) {}

	// 만나야할 의사 기록
	void addDoctor(Doctor* new_doctor){
		m_doctors.push_back(new_doctor);
	}
	// 만나야할 의사 출력 
	void meetDoctors();  // 이부분 주목!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

	friend class Doctor;
};

class Doctor
{
	string m_name;
	vector<Patient*> m_patients;
public:
	Doctor(string name_in)
		: m_name(name_in){}

	//의사는 진료할 환자목록이 필요
	void addPatient(Patient* new_patient){
		m_patients.push_back(new_patient);
	}

	void meetPatients(){
		for (auto& ele : m_patients)
		{
			cout << "Meet patient : " << ele->m_name << endl;
		}
	}

	friend class Patient; // patient에서 doctor의 멤버에 접근 가능
};

void Patient::meetDoctors() {
	for (auto& ele : m_doctors)
	{
		cout << "Meet doctor : " << ele->m_name << endl;
	}
}
 

 10-5 의존 관계

Dependency 관계는 가장 약한 관계성을 갖으며, 코딩을 하다보면 가장 많이 만나게게 될 패턴 중 하나라고 말씀해 주셨다.

의존 관계에서는 class를 구현하는 부분에서는 서로 존제함을 알필요가 없다. 즉, class A와 class B가 있을때 A.h와 B.h에 각각을 선언해 줄때 상대의 헤더파일을 포함하지 않아도 된다. 가령 A.h를 구현할때 안에 #include "B.h"라고 하지 않아도 된다는 말이다.

 

대신 body를 구현하는 정의 부분에서는 헤더파일을 포함시켜주어야 한다. 코드가 매우 간결해지는 것을 확인할 수 있었다.

 

2) 나의 현황

● 오늘 강의 시간이 짧았음에도 블로그에 정리하면서 듣느라 시간이 너무오래 걸렸다.

원래 블로그에 쓰던 목적과는 달라진거 같아 회의감이 든다. 하나하나 다시 혼자 생각해가면서 블로그에 공부하는것은 매우 도움이 된다.

하지만 배보다 배꼽이 더 커지는 것 같다. 인강 말고도 명품 C++까지 병행하여 진도에 맞게 같이 나가고 있엇서 만약 계속 오늘처럼 정리하는대 시간을 너무 들인다면 명품 C++은 따로 강의를 다 듣고 해야될거 같다.

다음 글부터는 이전처럼 간략히 써보도록 노력해야 겠다.

이글의 모든 사진과 내용의 출처는 홍정모 교수님께 있습니다.

홍정모 교수님 블로그:

 

홍정모 연구소 : 네이버 블로그

안녕하세요! 홍정모 블로그에 오신 것을 환영합니다. 주로 프로그래밍 관련 메모 용도로 사용합니다. 강의 수강하시는 분들은 홍정모 연구소 카페로 오세요.

blog.naver.com

 

댓글