본문 바로가기
IT/C++(CPP)

CPP 초급 강좌 25. 객체지향 프로그램밍 클래스, 생성자, 소멸자, 파일분할, 클래스 탬플릿

by 신림83 2020. 11. 4.
반응형

CPP 초급 강좌 25. 객체지향 프로그램밍 클래스, 생성자, 소멸자, 파일 분할, 클래스 탬플릿

23강에서 본 지식을 기억하며, 간단한 스택 자료형을 struct로 구현해 보았다.

#include <iostream>
using namespace std;

struct Stack
{
private:
	int data[10];
	int index;

public :
	void init() { index = 0; }
	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }
};

int main()
{
	Stack myStack;
	myStack.init();
	
	myStack.push(10);

	cout << myStack.pop() << endl;
}

기존 stsck 는 int 형을 저장할 수 있고 index가 현재 스택이 데이터중 어디를 가르치는지를 저장한다. 그래서 초기화 init 코드를 불려주면 index는 초기화된다.

 

클래스

 먼저 struct를 class 로 수정하고, 차이점을 간략하게 알아보자.

//struct Stack
class Stack
{
private:
	int data[10];
	int index;

public :
	void init() { index = 0; }
	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }
};

요렇게 수정하면 똑같이 구동된다. 현제 사용함에 있어 큰 차이점은 없다.

속성의 차이라 말할 만한 것은 struct 같은 경우는 내부 영역이 기본이 모두 public: 이지만, class 같은 경우는 모두 private: 이다. 위의 코드는 명시적으로 접근자들이 선언되어 있어서 기본 속성은 적용되지 않는다.

 

클래스 생성자

  struct를 생성할 때, init() 코드는 불뤄줘야 한다. 이와 같이 생성 시 무조건 호출돼야 되는 조건을 코드가 존재하면, 클래스의 생성자에서 처리할 수 있다. 다음 코드에서 생성자를 보자.

class Stack
{
private:
	int data[10];
	int index;

public :

	Stack() 
	{
		index = 0; 
		
		cout << "Stack()" << endl;
	} 

	//void init() { index = 0; }

	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }
};

int main()
{
	Stack myStack;
	
	//myStack.init();
	cout << "//myStack.init();" << endl;
	
	myStack.push(10);

	cout << myStack.pop() << endl;
}

반환형이 없는 맴버함수, 그리고 그 멤버 함수의 이름은 클래스의 이름이다. 생성자는 이렇게 구현할 수 있다.

생성자는 객체가 생성이 되면 그때 바로 호출이 된다.

 

기존 초기화 코드인 init을 지웠다. 해당 초기화 코드들은 생성자로 이동되었다.

 

생성자 호출과 순서가 출력된다.

생성자 또한 오버라이딩을 할 수 있다. 생성자에서 data의 크기를 지정할 수 있게 코드를 바꿔보자.

class Stack
{
private:
	int* data;
	int index;

	void makeData(int dataSize)
	{
		data = new int[dataSize];
	}

public :

	Stack() 
	{
		index = 0;
		makeData(10);
	} 

	Stack(int dataSize)
	{
		index = 0;
		makeData(dataSize);
	}

	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }
};

int main()
{
	Stack myStack0;
	Stack myStack1(100);
	
	myStack0.push(10);
	cout << myStack0.pop() << endl;
}

일단 내부 data를 포인터형으로 바꾸고 초기화될 때 동적으로 메모리를 할당하게 코드를 수정하였다.

내부에서만 호출되는 makeData 함수에서 실제 메모리를 할당한다.

 

생성자를 두가지 버전으로 나누었다.

 

Stack() 버전은 index를 초기화하고 기존과 같이 10 공간을 할당시킨다.

Stack(int datasize) 버전은 index를 초기화하고, datasize가 요청한 만큼을 대이터를 할당시킨다.

 

myStatk0과 그냥 생성하면 Stack() 생성자가 호출되고 내부에서 10의 공간이 할당된다.

myStack1(100)과 같이 인자를 붙여 호출하는 경우, 오버라이딩된 해당 생성자를 호출하며, Stack(100)으로 호출되어 내부 데이터는 100의 공간을 할당하게 된다.

 

해당 코드는 문제가 있다. 메모리 할당을 하지만 지워주는 코드가 없다. 그럼 어떻게 할까? data에 직접 접근이 안되니 할당된 메모리를 지워주는 함수를 만들고 호출해야 할까?

void deleteData() //stack의 내부함수
{
	delete[] data;
}

int main()
{
	Stack myStack0;
	Stack myStack1(100);
	
	myStack0.push(10);
	cout << myStack0.pop() << endl;

	myStack0.deleteData(); //a
	myStack1.deleteData(); //b
}  
    

 deleteData() 라는 함수를 스택의 내부함수로 만들었다고 가정하고 실제 스택을 사용한 뒤 프로그램 종료 전 동적 생성된 데이터를 지우는 것이 올바르다. 

 

소멸자

 그런데 좀 그렇잖아. 데이터 생성은 생성자에서 한건데 지우는 건 따로 호출해줘야 하다니.

c++  class에선 생성자의 반대 계념인 소멸자가 있다. 소멸자란 객체가 파괴될 때 자동으로 호출이 되는 코드이다. 이러한 경우에 사용할 수 있다.

 소멸자는 "~클래스 이름()" 의 모양을 가진다. 다음 코드를 보자.

#include <iostream>
using namespace std;

class Stack
{
private:
	int* data;
	int index;

	void makeData(int dataSize)
	{
		data = new int[dataSize];
		cout << "동적 매모리 생성" << endl;
	}

	void deleteData() 
	{
		delete[] data;
		cout << "동적 매모리 삭제" << endl;
	}

public :

	Stack() 
	{
		index = 0;
		makeData(10);
	} 

	Stack(int dataSize)
	{
		index = 0;
		makeData(dataSize);
	}

	~Stack()
	{
		deleteData();
	}

	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }
};

int main()
{
	Stack myStack0;
	Stack myStack1(100);
	
	myStack0.push(10);
	cout << myStack0.pop() << endl;
}

소멸자를 구현했다. 위에서 제작한 동적 매모리 삭제 코드를 부른다. 그리고 main에서 명시적으로 호출되던 deleteData 코드 또한 삭제하였고, 접근할 수 없는 private 영역으로 옮긴다. 소멸자가 호출하도록 한다.

 실제 기대했던 정상 동작을 하는지 확인을 위하여 택스트 출력을 해본다.

 

출력결과

기대했던 동작을 함을 확인했다.

 

선언부와 구현부 나누기, 그리고 파일 나누기

 c++에서는 .h .cpp 파일을 통하여 클래스 제공을 많이 한다. .h에는 주로 멤버 변수와 멤버 함수의 선언을 넣어서 사용하고,. cpp에는 실제 내용을 구현하여 배포를 하는 방식을 많이 사용한다.

 

 소스가 커서 먼저 정리부터 조금 하겠다. 동적 매모리 생성, 삭제 코드를 생성자 소멸자로 옮기고 두개엿던 생성자도 하나로 줄이는 것부터 해보자.

class Stack
{
public :
	Stack() 
	{
		index = 0;
		data = new int[10];
	} 

	~Stack()
	{
		delete[] data;
	}

	void push(int value) { data[index++] = value; }
	int pop() { return data[--index]; }

private:
	int* data;
	int index;
};

정리한 스택 소스, 이후 설명을 위해서 좀 가볍게 고쳤다.

 

 위에서 제작했던 스택을 먼저 선언부와 구현부를 나누어 보자.

class Stack
{
public :
	Stack();
	~Stack();

	void push(int value); 
	int pop(); 

private:
	int* data;
	int index;
};

Stack::Stack()
{
	index = 0;
	data = new int[10];
}

Stack::~Stack()
{
	delete[] data;
}

void Stack::push(int value) { data[index++] = value; }
int Stack::pop() { return data[--index]; }

 함수들의 선언만 남겨두고 실제 구현은 클래스의 외부에 두었다. 외부 구현부에는 함수의 이름 앞에 어디 소속인지 알 수 있도록 Stack:: 를 붙여준다.

 

실제 파일로 배포한다고 가정하면

Stack.h 의 내용은 아래와 같고

class Stack
{
public :
	Stack();
	~Stack();

	void push(int value); 
	int pop(); 

private:
	int* data;
	int index;
};

Stack.cpp 의 내용은 아래와 같을 것이다.

#include "Stack.h"

Stack::Stack()
{
	index = 0;
	data = new int[10];
}

Stack::~Stack()
{
	delete[] data;
}

void Stack::push(int value) { data[index++] = value; }
int Stack::pop() { return data[--index]; }

Stack.cpp의 내용들은 Stack의 선언부를 알아야 해서 꼭, include "Stack.h"가 필요하다.

 

클래스 탬플릿

 이번에는 위에서 코딩한 Stack을 좀 다양한 형태의 데이터를 담을 수 있는 버전으로 바꾸어 보자. 위의 스택 코드는 현제 int형 만 담을 수 있다. 이전에 배운 탬플릿 관련 코드를 응용해서 작업해보자.

 

아래를 보아요.

template<typename T> //수정
class Stack
{
public :
	Stack()
	{
		index = 0;
		data = new int[10];
	}
	~Stack()
	{
		delete[] data;
	}

	void push(int value) { data[index++] = value; }
	T pop() { return data[--index]; } //수정

private:
	T* data; //수정
	int index;
};

int main()
{
	Stack<float> myStack0; //수정
	
	myStack0.push(10.0f); //수정
	cout << myStack0.pop() << endl;
}

template<typename T>를 코딩했다. 그리고 아래 int로 사용하던 부분들을 모두 T로 교체해줬다.

그리고 main에서 사용시 <float> 로 사용하도록 했다.

 

참고로 컴파일하며 사용되는 T에 따라 해당 코드들이 생성되는 것이다. 위의 코드를 경우 float 버전만 생성된다.

 

참고로 클래스 탬플릿 같은 경우는 .h 파일 안에 모든 구현이 존재해야 한다. 

 

내용이 많구만.

하지만 맛보기만 한 것..ㅠㅠ

 

배워두면 다 쓸모가 있습...

봐주셔서 감사합니다.

반응형

댓글