구성(요소)Composition 
Part-of 두뇌는 육체의 일부이다.
육체가 없으면 두뇌는 없다

몬스터 클래스에 위치 클래스가 있다고한다면
몬스터가 없어지면 그몬스터의 위치클래스도 없어져야한다.
또, 위치 클래스는 몬스터의 이름을 알지 못한다.

즉, 몬스터 클래스에서 사용된 위치클래스는 그냥 몬스터의 일부이다.

또한 몬스터클래스가 위치클래스의 동작원리를 알 필요가 없다.

Composition 관계는 전체/일부로 생각하면 된다.

몬스터클래스 {(몬스터전체)
위치 클래스 : 몬스터클래스의 정보안에 위치가 존재한다.(몬스터의 일부)
}

 

main.cpp

#include <iostream>
#include "Monster.h"
using namespace std;



int main() {
	//몬스터 정보안에 위치가 들어있다.
	Monster mon1("name", Position2D(0,0));
	mon1.getLocation();
	cout << mon1 << endl;
	Monster mon2("name2", Position2D(0, 0));
	mon2.getLocation();
	{
		mon1.moveTo(Position2D(1, 1));
		cout << mon1 << endl;
	}
	return 0;
}

Monster.h

#pragma once

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

class Monster
{
private:
	std::string m_name;

	//subClass
	Position2D m_location;//m_x,m_y

	//int m_x;//location
	//int m_y;

public:
	//서브클래스로 위치클래스를 사용한다.
	Monster(const std::string name_in, const Position2D & pos_in)
		:m_name(name_in), m_location(pos_in) {

	}
	//set의 원리를 알 수 없다.
	void moveTo(const Position2D & pos_target) {
		m_location.set(pos_target);
	}

	void getLocation () {
		std::cout << m_location << std::endl;
	}


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

Position2D.h

#pragma once
#include <iostream>

class Position2D
{
private:
	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);
	}

	void set(const int & x_in, const int & y_in) {
		m_x = x_in;
		m_y = y_in;
	}

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

이니셜라이져 리스트
사용자정의자료형에서 생성자,대입연산자를 만들때 편하게 사용하는것

array등에 사용하는 {}를 말한다.

initalizer_list를 사용한다.
파라미터 initalizer_list로 받아 변환해준다고 생각하면된다

#include <iostream>
#include <cassert>
#include <initializer_list>

using namespace std;

class IntArray {
private:
	unsigned m_length = 0;
	int *m_data = nullptr;

public:
	IntArray(unsigned length)
		:m_length(length) {
		m_data = new int[m_length];
	}

	IntArray(const std::initializer_list<int> & list) 
	:IntArray(list.size()){
		cout << "initializer_list" << endl;
		int count = 0;
		for (auto & element : list) {
			m_data[count] = element;
			++count;
		}
	}

	IntArray& operator =(const std::initializer_list<int> & list)
	{
		delete[] m_data;

		m_length = list.size();
		m_data = new int[m_length];
		cout << "= initializer_list" << endl;

		if (m_data != nullptr) {
			int count = 0;
			for (auto & element : list) {
				m_data[count] = element;
				++count;
			}
		}
		else {
			m_data = nullptr;
		}

		return *this;
	}

	~IntArray() {
		delete[] this->m_data;
	}

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

int main()
{
	auto il = { 1,2,3 };

	IntArray iA{ 12,3,3 };
	cout << &iA << endl;
	cout << iA << endl;
	iA = { 1,2,3,4,5,6 };
	cout << &iA << endl;
	cout << iA << endl;
	return 0;
}

동적할당된 메모리에 대한 포인터변수를 멤버로 가지고 있는 클래스는
복사하거나 대입할때 깊은복사,얕은복사에 따라 복사생성자,대입연산자구현이 까다로워진다.

기본 복사생성자는 주소값을 복사해준다.

동적메모리할당을 할때는 주의해야한다.

주소값만 복사해주는것을 얕은 복사라고한다.
얕은 복사는 주소값만 복사해주기에 동적할당된 메모리를 반납하게되면 이슈가 생길 수 있다.

주소값을 복사하는대신 메모리를 다시할당 받는것은 깊은 복사라고한다.
다시 주소를 할당받기에 문제가 없다.

변수가 만들어지는순간엔 복사생성자가 실행된다.
이미 있는 변수에 대입하는건 대입 연산자가 실행된다.

 

논리를 잘이해하자 자바등 어느 언어든지 동적메모리할당,반납은 있다.

#include <cassert>
#include <iostream>

using namespace std;

class MyString {
public:
	char *m_data = nullptr;
	int m_length = 0;

public:
	MyString(const char *source = "") {
		assert(source);
		m_length = std::strlen(source)+1;
		m_data = new char[m_length];

		for (int i = 0; i < m_length; i++) {
			m_data[i] = source[i];
		}
		m_data[m_length - 1] = '\0';
	}
	
	MyString(const MyString &source) {
		cout << "Copy constructor" << endl;
		m_length = source.m_length;

		if (source.m_data != nullptr) {
			//주소 새로할당
			m_data = new char[m_length];
			for (int i = 0; i < m_length; i++) {
				m_data[i] = source.m_data[i];
			}
		}
		else {
			m_data = nullptr;
		}
	}

	MyString& operator = (const MyString &source) {

		//shallow copy;
		//this->m_length = source.m_length;
		//this->m_data = source.m_data;
		
		cout << "Assignment operator" << endl;

		//자기자신을 대입했을때
		if (this == &source)
			return *this;
	
		//이미 메모리를 가지고 있을 수 있다.
		delete[] m_data;

		m_length = source.m_length;

		if (source.m_data != nullptr) {
			//주소 새로할당
			m_data = new char[m_length];
			for (int i = 0; i < m_length; i++) {
				m_data[i] = source.m_data[i];
			}
		}
		else {
			m_data = nullptr;
		}

	}

	~MyString() {
		delete[] m_data;
	}

	char* getString() { return m_data; }
	int getLength() { return m_length; }
};

int main()
{

	MyString hello("Hello");

	cout << (int*)hello.m_data << endl;
	cout << hello.getString() << endl;

	{
		//복사생성자 주소값을 복사해준다.
		MyString copy;
		copy = hello;
		cout << (int*)copy.m_data << endl;
		cout << copy.getString() << endl;
		//scope를 나갈때 메모리를 반납한다.
	}

	cout << hello.getString() << endl;
	return 0;
}

프로그래머 편의를위해 생성자를 변환시켜주는 Converting construtor

동적할당 메모리할당 해제하는 delete랑은 다르다.

explicit,delete는 엄격하게 제한할때 사용한다.

생성자의 기본값을 정해주면 파라미터를 모두 넣어주지않아도 된다.
또, int로 파라미터를 설정해놓아도 char로 받을 수 있다.

위 두개의 사항을 엄격하게 제어하기위해
explicit와 delete를 사용한다.

 

#include <iostream>
#include <cassert>

using namespace std;
class Fraction {
private:
	int m_num;
	int m_den;

public:
	Fraction(char) = delete;
	Fraction(char,char) = delete;

	explicit Fraction(int num = 0, int den = 1)
		:m_num(num), m_den(den)
	{
		assert(den != 0);
	}

	Fraction(const Fraction &frc)
		:m_num(frc.m_num), m_den(frc.m_den) {
		cout << "copy constructor called" << endl;
	}

	friend std::ostream & operator << (std::ostream & out, const Fraction & f) {
		out << f.m_num << " / " << f.m_den << endl;
		return out;
	}
};

void doSomething(Fraction frac) {
	cout << frac << endl;
}

int main()
{
	//Fraction('a','a');
	doSomething(Fraction(2,3));
	
	return 0;
}

복사생성자 복사초기화 RVO

자신과 똑같은 타입의 데이터가 들어오면 복사하는 것을 말한다. 


컴파일러의 판단
복사할려는 value가 r_value면 복사하지않고 바로 생성한다.
함수로 return받으면 debug 일때면 복사생성자를 사용하고(다른주소를 가진다) release에선 바로 생성(주소값이 같다)한다.

 

복사생성자 막기

copy constructor를 private로 보내면 복사생성자를 사용할 수 없다. 

#include <iostream>
#include <cassert>

using namespace std;
class Fraction {
private:
	int m_num;
	int m_den;

public:
	Fraction(int num, int den) 
		:m_num(num), m_den(den) 
	{
		assert(den != 0);
	}

	Fraction(const Fraction &frc)
		:m_num(frc.m_num), m_den(frc.m_den) {
		cout << "copy constructor called" << endl;
	}

	friend std::ostream & operator << (std::ostream & out, const Fraction & f) {
		out << f.m_num << " / " << f.m_den << endl;
		return out;
	}
};

Fraction doSomething()
{
	Fraction temp(1, 2);
	return temp;
}

int main()
{
	Fraction frac(3, 5);

	Fraction cpFrac(frac);

	cout << cpFrac << endl;
	return 0;
}

산술연산자뿐 아니라 입출력연산자도 오버로딩이 가능하다.

입출력연산자 오버로딩을 하지않고 print함수로 구현하면 코드의 양이 많아질 수 있다.

int main()
{
	Point p1(3.2, 3.1, 3.5);
	cout << "p1 : " << &p1 << endl;
	Point p2(1.2, 1.1, 1.5);
	cout << "p2 : " << &p2 << endl;

	//print함수사용
	p1.Print();
	cout << " ";
	p2.Print();
	cout << endl;
    return 0;
}


ostream,istream으로 입출력을 정의할 수 있다.
ostream,istream모두 파일입출력에 사용할 수 있어 파일입출력도 같이 사용할 수 있다.

	friend std::ostream& operator << (std::ostream &out, const Point &point) {
		out << point.m_x+1.0 << " " << point.m_y+1.0 << " " << point.m_z + 1.0;
		return out;
	}
	//const아님
	friend std::istream& operator >> (std::istream &in, Point &point) {
		in >> point.m_x >> point.m_y >> point.m_z;
		return in;
	}


파일입출력은 fstream를 include해야한다.

#include <fstream>

입출력 오버로딩 및 기본파일 입출력 코드

#include <iostream>
#include <fstream>

using namespace std;

class Point
{
private:
	double m_x, m_y, m_z;

public:
	Point(double x, double y, double z)
		:m_x(x), m_y(y), m_z(z) {

	}
	double getX() {
		return m_x;
	}
	double getY() {
		return m_y;
	}
	double getZ() {
		return m_z;
	}

	void Print() {
		cout << m_x << " " << m_y << " " << m_z;
	}

	friend std::ostream& operator << (std::ostream &out, const Point &point) {
		out << point.m_x+1.0 << " " << point.m_y+1.0 << " " << point.m_z + 1.0;
		return out;
	}
	//const아님
	friend std::istream& operator >> (std::istream &in, Point &point) {
		in >> point.m_x >> point.m_y >> point.m_z;
		return in;
	}
};

int main()
{
	Point p1(3.2, 3.1, 3.5);
	cout << "p1 : " << &p1 << endl;
	Point p2(1.2, 1.1, 1.5);
	cout << "p2 : " << &p2 << endl;

	//print함수사용
	p1.Print();
	cout << " ";
	p2.Print();
	cout << endl;

	cout << p1 << " " << p2 << endl;
	//파일출력
	ofstream of("out.txt");
	of << p1 << " " << p2 << endl;
	of.close();


	//파일입력
	ifstream is("out.txt");
	is >> p1 >> p2;
	is.close();

	cout << p1 << " " << p2 << endl;
	return 0;
}

int,double등 기본자료형은 산술연산자가 정의되어있다.

사용자정의자료형 ex)class 끼리도 산술연산자를 만들어 줄 수 있다.

 

operator를 사용해서 만들 수 있다.


밖에서 함수로 생성할 수 있다
-외부 함수이므로 public함수로 값을 가져와 더해주어야한다.

//외부함수
Cents operator +(const Cents &c1, const Cents &c2) {
	return Cents(c1.getCents() + c2.getCents());
}



밖의 함수를 friend로 선언할 수 있다.
-friend 선언으로 private 자료형에 접근가능하다.

...
public:
	//class 내부 friend 선언
	friend Cents operator +(const Cents &c1, const Cents &c2);
 ...
 //클래스외부
 //friend선언
Cents operator +(const Cents &c1, const Cents &c2) {
	//private 변수 접근가능
	return Cents(c1.m_cents + c2.m_cents);
}


클래스 내부함수로 this를 사용할 수 있다.
-생성된 함수에서 시작하므로 this로 현재값과 파라미터 값을 더해준다.

//class 내부함수 this사용
public:
	Cents operator +(const Cents &c2) {
		cout << this << endl;
		cout << "this->m_cents : " << this->m_cents << endl;
		cout << "c2.m_cents : " << c2.m_cents << endl;

		return Cents(this->m_cents + c2.m_cents);
	}

내부함수는 첫번째 인자의 주소에서 시작한다.

계속 더해가는 구조라고 생각하면된다.

첫번째 인자 접근 -> 첫번째 인자값(this로 접근) + 두번째 인자값 ->새로운 주소 할당

 

#include <iostream>

using namespace std;

class Cents
{
private:
	int m_cents;

public:
	Cents(int cent): m_cents(cent){}
	int getCents() const { return m_cents; }
	//friend Cents operator +(const Cents &c1, const Cents &c2);
	Cents operator +(const Cents &c2) {
		cout << this << endl;
		cout << "this->m_cents : " << this->m_cents << endl;
		cout << "c2.m_cents : " << c2.m_cents << endl;

		return Cents(this->m_cents + c2.m_cents);
	}
};

////friend선언
//Cents operator +(const Cents &c1, const Cents &c2) {
//	return Cents(c1.m_cents + c2.m_cents);
//}
//
////외부함수
//Cents operator +(const Cents &c1, const Cents &c2) {
//	return Cents(c1.getCents() + c2.getCents());
//}

int main()
{
	Cents c1(4);
	cout << "c1 : " << &c1 << endl;
	Cents c2(5);
	cout << "c2 : " << &c2 << endl;

	cout << (c1 + c2 + Cents(10)+Cents(11)).getCents() << endl;

	return 0;

}

실행시간측정하기

프로그램 생성시 실행시간을 알고싶을 수 있다.
chrono를 사용한 Timer Class를 생성해서 사용하자.

출력되는 시간을 완벽히 믿진말자. 때마다 달라질 수 있다.

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>

using namespace std;

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();

public:
	void elapsed()
	{
		std::chrono::time_point<clock_t> end_time = clock_t::now();

		cout << std::chrono::duration_cast<second_t>(end_time - start_time).count() << endl;
	}

};

int main()
{
	Timer t;
	vector<int> vec(1000);
	for (int i = 0; i < vec.size(); i++) {
		vec[i] = i;
	}
	for (int i = 0; i < vec.size(); i++) {
		cout << vec[i] << endl;
	}
	t.elapsed();
	return 0;
}

익명객체 anonymous

이름이 붙은 변수(R-value)를 사용하지않고 바로사용한다.

한번만 사용할 멤버함수를 class를 인스턴스생성하고 사용하면 비효율적이다.

Class를 R-value처럼 접근해 사용가능하다.
재사용은 불가능하다.

계속 새로 생성한다고 생각하면된다. 생성자,소멸자로 확인 가능하다.

 

#include <iostream>

using namespace std;
class A
{
private:
	int a;
public:
	void print()
	{
		cout << "Hello" << endl;
	}
	A(const int& val)
		:a(val){
		cout << val << endl;
		cout <<this<< " constructor" << endl;
	}
	~A()
	{
		cout << this <<" destructor" << endl;
	}
	int getA() const {
		return a;
	}
};

A add(const A& a1, const A& a2) {
	return A(a1.getA() + a2.getA());
}

int main()
{
	A a(1);
	//아래 두 인스턴스는 주소가 다르다.
	A(1).print();
	A(2).print();
	
	cout << add(A(3), A(4)).getA() << endl;

	return 0;
}

친구 함수와 클래스
여러개의 클래스 복잡하게 상호작용하면 클래스간의 상호작용을 깔끔히 정리하기 어렵다.
이때 친구함수와 친구클래스를 사용한다.

클래스안에 함수를 friend로 선언한다.

A,B 두 class가 있다고 생각해보자.
A class에 B class를 friend로 선언하게되면
B class에서 A class private:(캡슐화)에 관여가 가능하게된다.

friend선언은 class를 통째로도 가능하고 일부함수만 따로도 가능하다.

friend함수 선언할때 함수의 생성되는 위치를 잘보고 구현해야한다.

 

#include <iostream>

using namespace std;
//전방선언
class A;

class B
{
private:
	int m_val = 2;

public:
	//A가 선언되어 있기에 A를 사용가능하다.
	void doSomething(A& a);
};

class A
{
private:
	int m_val = 1;
	//class A가 생성될떄 B를 알 수없다.
	//class통쨰로 선언
	//friend class B;
	//B Class가 먼저 선언되어 있어 함수를 구현할 수 있다.
	//class일부함수만 선언
	friend void B::doSomething(A& a);
};

//A에 m_val변수가 있는지 확인 불가능하기때문에 A Class가 구현된후 함수 구현
void B::doSomething(A& a) {
	a.m_val = 3;
	cout << a.m_val << endl;
}

int main()
{
	A a;
	B b;
	b.doSomething(a);
	

	return 0;
}

+ Recent posts