본문 바로가기

c언어

클래스 사용 예시

예시를 적기 위해서는 상황설정 먼저 해야할 것이다.

 

우리는 이제부터 학교의 선생이 된다. 학생 점수 입력 프로그램을 통해 국,영,수 세가지 점수를 입력받고 출력하는 기능을 만들것이다. 또한 그것을 볼 수 있는 전체조회 기능도 만들것이다. 그리고 한번에 모든 학생을 입력하긴 시간이 없으니, 한번 입력하고 나중에 또다시 입력할 수 있게 만들어 보겠다.

 

즉 내가 필요한 데이터와 기능은 세가지이다.

데이터

1.학생 이름

2.국어점수

3.영어점수

4.수학점수

 

기능

1. 초기 입력

2. 추가입력

3. 전체 출력

 

이제부터 생각해 봐야한다.

첫째로 총 학생이 얼마나 있을지 모르기때문에 동적할당을 사용해야할 것이다.

그리고 입력을 할 때 가장 먼저 이번에 입력할 학생의 숫자를 먼저 입력받고 동적할당한 뒤, 이름과 국영수 순서대로 입력할것이다.

또한 추가 입력을 할때 똑같이 추가할 학생수를 입력하고 그만큼 넓힌 공간을 할당하고 추가하는 학생은 기존 학생의 뒤에 저장할것이다.

 

 

그럼 이제 클래스를 몇개 만들어야할지 생각해보자. 먼저 학생에 대한 클래스를 만들어야할 것이다. 위에 적은 기능과 함수는 모두 학생 클래스에 있으면 될것이다.

 

그리고 학생 클래스를 담는 메인프로그램 클래스를 만들것이다. 이것은 학생 클래스의 데이터를 조작하는 일련의 과정을 총괄하는 역할을 맡길것이다.

 

 

클래스를 만들기 이전에 한가지 더 준비할게 있다.

"미리 컴파일된 헤더" 이다.

이것은 visual studio 2019이후 없어진 형태이지만 항상 참조하거나 정의해야하는 기능들을 한번에 관리하기 수월하기 때문에 보통 만들어둔다.

 

보통 stdafx.h와 .cpp로 만들어져있고 내가 쓰는 두개의 파일의 형태는 다음과 같다.

stdafx.h

#pragma once

#include <iostream>

#define SAFE_DELETE(p) if(p){delete p; p=nullptr;}
#define SAFE_DELETE_ARRAY(p) if(p){delete[] p; p=nullptr;}


#ifndef DBG_NEW 

#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ ) 
#define new DBG_NEW 

#endif


using namespace std;

첫줄부터 설명하겠다.

 

1. #pragma once

 

이것은 이 파일을 참조한 파일이 또다른 이 파일을 참조한 파일을 참조한다면 중복 참조되어 이 파일 내 선언된 함수라던지 다른 무언가가 중복 선언되는 오류를 막기 위해 "이 파일이 참조된적이 있다면 다시 참조하지 마세요" 라는 의미로 적어둔다.

 

이해하기 힘들 수 있다.

만약 a와 b파일 모두에서 stdafx.h를 참조하고있는데 a가 b를 참조한다면 b를 참조하는 과정에서 또다시 stdafx를 참조할 수 있기 때문에 중복 참조가 된다. 그 경우를 막기 위한 코드이다.

 

2. #include <iostream>

iostream이라는 콘솔 입출력 장치를 쓰는 라이브러리이다. 이 프로그램에서는 사용하기 때문에 참조하였고 추가적으로 모든 프로그램에서 참조하고 싶은 파일이 있다면 이렇게 선언하면 된다.

 

3,4번 줄 #define SAFE_DELETE(p)

이것은 동적할당한 데이터를 자동으로 초기화 및 널포인트화 시켜주기 위한 매크로로 매개변수로 포인터를 받아

if(p)포인터가 널이 아니라면

delete p; 포인터를 삭제하고

p= nullptr; 널포인트화 한다는 뜻이다.

 

4번줄도 동일한 기능이지만 이것은 포인터 배열을 삭제하는 구문이기때문에 조금 다르다.

 

5~8 #ifndef DBG_NEW ~~#endif

이것의 동작 원리는 나도 잘 모른다. 다만 이 코드와 이후 메인에 적을 구문까지 함께 적으면 프로그램이 종료되었을 때 메모리가 누수되는지 몇바이트씩 몇개 누수되는지 확인할 수 있어 누수 감시용으로 사용한다.

 

마지막 줄 using namespace std;

이것은 기본 라이브러리를 사용할때 함수에 따로 경로를 지정하지 않는다면 std라이브러리의 함수를 사용하겠다는뜻이다.

 

그리고 cpp파일이다.

stdafx.cpp

#include "stdafx.h"

끝이다.

난 stdafx.h에 함수를 선언한 적이 없기 때문에 아무런 말도 적을게 없다.

 

이렇게 두 파일을 적고 두개의 파일을 ctrl을 이용해 같이 선택하여

우클릭-속성- c/c++-"미리 컴파일된 헤더" 항목에서

미리 컴파일된 헤더항목의 "만들기(/Yc)"를 선택하면 된다.

 

또한 나머지 파일은 같은 설정을 "사용(/Yu)"를 선택하고 바로 밑 미리 컴파일된 헤더 파일 항목에 stdafx.h를 적어놓는다면 자동으로 이 파일을 컴파일해주지는 않고, 컴파일하지 않으면 컴파일러가 알려준다.

 

그리고 메인파일이다.

main.cpp

#include "stdafx.h"

int main(void){
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	return 0;
}

여기있는 _CrtSetDbgFlag어쩌고가 누수를 알려주는 프로그램이다.

 

이 세개의 파일을 모두 모아 하나의 프로젝트에 담고

위의 기능 선택 바에 프로젝트-템플릿 내보내기 를 통해 따로 저장해 둔다면 추후에 새로운 파일을 만들때 이 템플릿을 사용하면 편하다.

 

 

자 이제 모든 준비가 끝났으니 학생클래스를 만들어보자

class.h

#pragma once

class cStudent {
public:
	cStudent();
	~cStudent();
private:
	char m_cName[16];
	int m_iKor;
	int m_iEng;
	int m_iMath;

public:
	void initialize();
	void update();
	void release();
	void primaryInput();
	void addInput();
	void printAll();

};

클래스 cStudent를 만들고 public 멤버함수로 생성자와 소멸자를 선언해두었고,

 

private 멤버변수 캐릭터 16바이트 하나와 각각의 성적을 담을 int 세개를 만들었다. 물론 필요에 따라 총점이나 평균을 담을 변수들도 만들어줘도 된다.

또한 멤버변수의 경우 멤버변수라는 뜻의 m_를 앞에 붙여 다른 변수들과 구분되게 해두었다.

 

 

그리고 또다시 public 멤버함수 여섯개가 있다.

 

먼저 앞의 세개는 앞서 말한 세가지 기능과는 별개로 항상 만들어두는 함수들이다.

각각의 기능은 다음과 같다.

멤버변수의 할당을 담당할 initialize();

상황의 진행에 따라 값을 변화시켜줄 update();

상황이 종료되었을때 멤버변수의 동적배열을 할당 해제시켜줄 release();

이 함수는 보편적으로 많이 쓰는 목적들과 그에따른 함수명이다. 필요 없더라도 일단 만들어두기로 한다.

 

그리고 그 뒤 세가지가 앞서 말한 세가지 기능들이다.

초기 입력을 담당할 primartInput();

추가 입력을 담당할 addInput();

전체 출력을 담당할 printAll();

이 있다. 일단 선언해 두고 필요에 의해 매개변수를 받아낼 수도 있을것이다. 그러니 나중의 파일은 지금과 다를 수 있다.

 

이제 cpp파일을 만들어야하는데 사실 함수들을 드래그 한 뒤 우클릭하여

"빠른 작업 및 리팩터링" 을 누르고 "선언/정의 만들기" 를 선택하면 자동으로 동일한 이름의 cpp파일을 만들어 각각의 함수를 정의해준다. 물론 내용은 빈칸이다.

 

그렇게 자동으로 나온 student.cpp파일이다.

#include "Student.h"

cStudent::cStudent()
{

}

cStudent::~cStudent()
{
}

void cStudent::initialize()
{
}

void cStudent::update()
{
}

void cStudent::release()
{
}

void cStudent::primaryInput()
{
}

void cStudent::addInput()
{
}

void cStudent::printAll()
{
}

이제부터 함수 하나씩 적어보겠다.

 

먼저 생성자를 건드려보자.

생성자가 할 일이 무엇이 있을까??

일단 학생을 입력하지 않았을 경우 쓰레기값이 들어가는것이 불편하니 초기화를 해 줄 수 있을것이다. 

cStudent::cStudent()
{
	strcpy_s(m_cName, sizeof(m_cName), "");
	m_iKor = 0;
	m_iEng = 0;
	m_iMath = 0;
}

m_cName="";하는 실수를 범하지 말자. 

char형 배열은 =를 이용한 대입이 불가능하고 strcpy_s를 통한 복사만 가능하다.

 

 

소멸자에는 만일 멤버변수가 할당되었다면 할당 해제를 시켜줄 함수인 release()를 불러놓자.

cStudent::~cStudent()
{
	release();
}

 

하지만 이번에는 initialize를 통해 할당할 데이터가 없을것같다. 그러니 initialize는 비워두고

 

release함수또한 아직 할당한 데이터가 없기 때문에 해제할것이 없으니 release함수는  비워두도록 하자.

소멸자에 release를 불러놓은 이유는 혹시모를 다른 데이터의 할당을 대비하기 위함이라고 해둔다.

 

update가 남아있지만 이또한 매 프레임마다 수정해야할 일이 없기 때문에 비워두도록 하자.

 

이번엔 primaryInput함수이다. 이함수는 초기 입력을 위한 함수로 만들것이다. 그러니 입력 시퀀스를 넣으면 될것이다.

 

하지만 학생이 몇명인지 이때 고르는것은 아니다. 왜냐하면 class cStudent의 내용물은 학생 한명이기 때문이다.

그러니 한명을 입력하는 시퀀스를 만들겠다.

void cStudent::primaryInput()
{
	cout << "학생의 이름을 입력하세요" << endl;
	cin >> m_cName;
	cout << "국어 점수를 입력하세요" << endl;
	cin >> m_iKor;
	cout << "영어 점수를 입력하세요" << endl;
	cin >> m_iEng;
	cout << "수학 점수를 입력하세요" << endl;
	cin >> m_iMath;
}

이렇게 한다면 각각의 멤버변수에 입력한 값을 넣을 수 있게 된다.

 

이제 추가 입력을 만들...려고 했으나. 아까 말했듯 이 클래스의 멤버변수는 학생 한명이므로 추가입력이라는 개념이 있을 수가 없게 되었다. 만일 추가입력을 한다고 하더라도 primaryInput과 다를것이 없다. 그러니 addInput함수는 지워버리고, primaryInput은 그냥 Input으로 변경시키겠다.

 

남은것은 이제 전체 출력을 위한 printAll이다. 형식에 맞춰 예쁘게 출력해보겠다.

void cStudent::printAll()
{
	cout << "이름" << m_cName << endl;
	cout << "국어점수" << m_iKor << endl;
	cout << "영어점수" << m_iEng << endl;
	cout << "수학점수" << m_iMath << endl;
}

 

 

좋다 이렇게 필요한 모든 기능을 만들었다. 최종 cStudent의 헤더파일과 cpp파일은 다음과 같다.

Student.h

#pragma once

class cStudent {
public:
	cStudent();
	~cStudent();
private:
	char m_cName[16];
	int m_iKor;
	int m_iEng;
	int m_iMath;

public:
	void initialize();
	void update();
	void release();
	void Input();
	void printAll();

};

Student.cpp

#include "stdafx.h"
#include "Student.h"

cStudent::cStudent()
{
	strcpy_s(m_cName, sizeof(m_cName), "");
	m_iKor = 0;
	m_iEng = 0;
	m_iMath = 0;
}

cStudent::~cStudent()
{
	release();
}

void cStudent::initialize()
{

}

void cStudent::update()
{
}

void cStudent::release()
{

}

void cStudent::Input()
{
	cout << "학생의 이름을 입력하세요" << endl;
	cin >> m_cName;
	cout << "국어 점수를 입력하세요" << endl;
	cin >> m_iKor;
	cout << "영어 점수를 입력하세요" << endl;
	cin >> m_iEng;
	cout << "수학 점수를 입력하세요" << endl;
	cin >> m_iMath;
}

void cStudent::printAll()
{
	cout << "이름" << m_cName << endl;
	cout << "국어점수" << m_iKor << endl;
	cout << "영어점수" << m_iEng << endl;
	cout << "수학점수" << m_iMath << endl;
}

 

그럼 이제 모든것을 총괄한다고 했던 mainProgram클래스를 만들어보겠다.

 

 

일단 이 클래스에선 어떤 작업을 할것인지 생각해보자.

 

이미 입력 시퀀스들은 모두 설정되었고 여기서 해야할일은 다음과 같다.

1. 프로그램을 끊임없이 계속 진행하는것과 기본적 선택지를 제공하는것

2. 학생수를 입력받고 그에맞는 공간을 할당받는것,

3. 할당받은 공간을 프로그램 종료시 해제하는것.

 

이미 매 프레임마다 데이터를 상황에 맞게 조작시켜주는 update라는 멤버함수를 습관적으로 넣게 하겠다고 했으니 1번 과제는 update에서 하면 될 것이다.

할당 시점은 mainProgram 동작중 입력하겠다는 선택을 했을시에 학생의 수를 받아 그만큼의 공간을 할당받아야하니 그때가서 생각하도록 하자.

할당받을 공간을 해제하는것은 release()함수를 이용하기로 하였고, 해제하는 시점은 프로그램이 종료되었을때이니 소멸자에 넣어주면 될것이다.

그 외 기능들이 필요할지 아직은 잘 모르겠으니 멤버함수는 없는채로 두자. 물론 initialize,update,release와 생성자,소멸자는 기본이다.

 

 

그러니 mainProgram클래스의 구조는 다음과 같이 생각할 수 있다.

 

멤버변수 

학생클래스의 학생(들)

학생의 숫자

 

학생클래스를 하나만이 아닌 여러개를 할당받아야하니 포인터로 받아 배열취급을 해주는것이 좋을것 같다.

 

그렇게 만든 코드는 다음과 같다.

mainProgram.h

#pragma once
#include "Student.h"

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

private :
	cStudent* m_student;
	int m_iSize;

public :
	void initialize();
	void update();
	void release();

};

mainProgram.cpp

#include "mainProgram.h"

cMainprogram::cMainprogram()
{
}

cMainprogram::~cMainprogram()
{
}

void cMainprogram::initialize()
{
}

void cMainprogram::update()
{
}

void cMainprogram::release()
{
}

먼저 헤더파일을 보면 cStudent라는 클래스의 존재를 알려주기 위해 Student.h를 include한것을 볼 수 있다.

안하면?? cStudent의 존재를 모르기때문에 컴파일오류가 난다.

 

자 이제 아까와 같은 과정을 거치겠다.

 

생성자먼저 보자, 생성자는 멤버 변수의 값을 초기화시키기 위한것이다.

하지만 이전 학생 클래스때와는 달리 이 mainProgram은 학생클래스를 멤버변수로 포함하고 있다. 그래서 initialize에서 바로 할당하면 이후 상호참조에서 오류가 발생하니 일단 nullptr화 하고, 추후에 할당하기로 하자.

cMainprogram::cMainprogram()
{
	m_student = nullptr;
	m_iSize;
}

왜 바로 할당하지 않나요?? 라고 할 수 있다. 두가지 이유가 있는데

1. 아직 학생 수를 모르는데 어떻게 할당해요

2. 생성자에 할당하는것은 아주 위험한 일입니다. 다음기회에 상호 참조라는 실수를 설명드리겠습니다.

 

소멸자의 경우 이번엔 무조건 할당할 요소가 있다. 학생이다. release를 불러놓자

cMainprogram::~cMainprogram()
{
	release();
}

다음은 할당을 담당하는 initialize인데 아까말했듯 아직 학생수를 모르니 일단 넘어가자.

 

그다음은 update이다. 매 프레임마다 멤버변수의 값을 수정하는 용도라고 했지만 메인프로그램의 경우 상황을 진행시켜주기위한 반복문 용도로 사용한다.

 

그러니 무한반복을 열고, 선택지를 줘야 할 것이다. 프로그램 사용자가 할 일이 무엇인가??

이전에 말한 초기입력, 추가입력, 전체출력이 될것이다.

그러고 나서 초기입력의 경우 학생의 수를 입력받아야할 것이다. 그리고 동적할당 또한 받아야 할 것이다.

그 뒤 할당받은 위치에 cStudent클래스의 Input함수를 통해 학생의 정보를 입력해야할 것이다.

그렇게 초기 입력이 끝나면 추가입력을 받아야한다.

하지만 이렇게 생각하니 어차피 초기입력과 추가입력의 차이를 모르겠다. 

입력을 하나로 통일하여 기존에 데이터가 없다면 초기입력을 있다면 추가입력을 하도록 만들어도 될것같다.


하지만 추가입력을 하면 기존보다 더 큰 공간을 할당받고 기존의 데이터를 복사하여야 한다. 그렇지 않으면 먼저 입력한 학생의 데이터가 삭제될것이다.

그리고 출력버튼을 누르면 모든 학생이 출력되어야할것이다.

 

왜 이 update 함수에서는 많은 기능을 하나요??

아니다 이 update함수는 아까 말한대로 상황의 진행을 위해 사용된다.

그러니 이 update함수는 선택지를 주고 그 선택지에 따른 분기를 만들어주는게 목적인 것이다.

일단 분기문의 첫번째까지만 만들어보겠다.

 

void cMainprogram::update()
{
	int iChoice;
	while (true) {
		cout << "원하시는 기능을 선택해 주세요 \n1.입력\t2.전체출력\t3.종료" << endl;
		cin >> iChoice;

		switch (iChoice) {
		case 1:
			break;
		case 2:
			break;
		case 3:
			break;
		default:
			break;

		}
	}
}

일단 선택지를 받기 위해 iChoice를 선언하였고 사용자에게 선택지를 알려주기위해 문구를 출력하였다.

그리고 iChoice에 숫자를 입력받고 그 숫자가 무엇인가에 따라 행동이 결정될 것이다.

 

이제 입력을 눌렀을때의 코드를 생각해보자

아까 말했듯 먼저 학생의 수를 입력받아야 할 것이다. 거기까지만 일단 적어보자.

case 1:
	cout << "이번에 입력할 학생은 몇명인가요?" << endl;
	cin >> m_iSize;
break;

근데 이 뒤에는 입력받은 크기만큼 할당을 받아야한다. 할당을 담당하는 initialize를 만들어보자

void cMainprogram::initialize()
{
	m_student = new cStudent[m_iSize];
}

좋다. 입력받은만큼의 학생 데이터를 저장할 공간을 할당받을 수 있게 되었다.

그럼 다시 update 분기문에 initialize를 호출하고 마저 진행하자.

다음 과제는 입력을 받는것이다. 입력받는것은 이미 학생클래스의 Input함수가 있으니 불러주면 된다.

case 1:
    cout << "이번에 입력할 학생은 몇명인가요?" << endl;
    cin >> m_iSize;
    initialize();
    for (int i = 0; i < m_iSize; ++i) {
        m_student[i].Input();
    }
    break;

학생 수를 입력받고, 학생수만큼 반복하여 배열을 통해 각각 입력받는 함수가 완성되었다.

 

좋다. 입력은 끝났다.

하지만 문제가 있다.

 

추가입력은 어떻게 할 것인가??

만일 이미 데이터가 있는 시점에 다시 입력을 누르면 기존 할당된 공간은 반납되지 못하고 사라지고, 새로 할당받은 공간에 새로운 학생의 데이터만이 저장될 것이다.

 

앞에 조건문을 붙여 이 코드는 기존 데이터가 없을때만 동작하게 만들어두자.

case 1:
    if (!m_student) {
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> m_iSize;
        initialize();
        for (int i = 0; i < m_iSize; ++i) {
            m_student[i].Input();
        }
    }
    break;

if(!포인터) 가 익숙하지 않을 수 있다. 이것은 if(포인터 == nullptr)과 같다. 즉 m_student가 nullptr이면 동작하고 그렇지 않으면 동작하지 않는다. 다만 헷깔릴 수 있기 때문에 익숙해 지기 전까지는 if(포인터 == nullptr)과 같이 쓰는것도 좋다.

그리고 난 이미 생성자에서 m_student를 nullptr로 넣어주었기 때문에 처음 입력하는 시점에만 이 코드가 동작할 것이다.

 

그럼 초기입력은 성공했다.

이제 추가입력이다. 이전과 동일하게 복붙하고 살펴보겠다.

case 1:
    if (!m_student) {
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> m_iSize;
        initialize();
        for (int i = 0; i < m_iSize; ++i) {
            m_student[i].Input();
        }
    }
    else{
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> m_iSize;
        initialize();
        for (int i = 0; i < m_iSize; ++i) {
            m_student[i].Input();
        }
    }
    break;

일단 else를 적어 nullptr이 아닐때 즉 이미 입력된 값이 있을때에만 동작하는 코드를 만들었다.

똑같이 입력할 학생의 수를 받아 보지만 문제가 생긴다.

이번에 입력할 학생의 수를 입력받을것인데 m_iSize로 받으면 기존의 학생 수에 대한 데이터는 사라지게 된다. 즉 추가가 아니고 새로운 값으로 덮어써진다는 것이다.

 

그러면 방법은 두가지 있다.

1. 입력 할 때마다 추가된 학생까지의 총 학생수를 입력받던지,

2. 새로운 변수에 추가될 학생을 입력받아 기존 학생에 추가시키던지

 

둘 중 하나인데. 1번을 선택했을때 그 다음 과정을 생각해보자

총 학생수를 새로 입력받았다. 그럼 이제 새로운 공간을 할당받고, 그 공간에 기존 데이터를 추가한 뒤, 새로운 학생의 데이터를 저장해야한다.

근데 기존 학생수가 날아갔다. 그럼 기존 데이터를 보존할때 어디까지 기존 데이터인지 알기 힘들게 된다.

따라서 2번 방법을 통해 기존 학생수를 남겨놓고 추가하는 방식으로 진행하는게 좋을것 같다.

 

case 1:
    {
        int iSize;
        if (!m_student) {
            cout << "이번에 입력할 학생은 몇명인가요?" << endl;
            cin >> m_iSize;
            initialize();
            for (int i = 0; i < m_iSize; ++i) {
                m_student[i].Input();
            }
        }
        else {
            cout << "이번에 입력할 학생은 몇명인가요?" << endl;
            cin >> iSize;
            initialize();
            for (int i = 0; i < m_iSize; ++i) {
                m_student[i].Input();
            }
        }
        break;
    }

iSize를 선언할때 case 안에서 선언할경우 중괄호를 쳐줘야한다. 그렇지 않으면 중복 선언의 가능성이 있다. case문의 특징이다.

 

좋다. iSize를 선언받아 기존데이터는 m_iSize에, 새로운 학생은 그냥 iSize에 들어있게 될것이다. 그런데 이제 할당할때 문제가 생긴다.

할당하는 initialize의 경우 멤버변수인 m_iSize만을 알고 iSize는 모른다. 그래서 추가할당이 불가능하게 된다.

그래서 initialize를 수정하기로 하자.

void cMainprogram::initialize(int iSize)
{
	m_student = new cStudent[iSize];
}

이렇게 하면 initialize는 숫자를 매개변수로 받고 그 숫자만큼의 크기를 할당하게 될것이다.

당연히 헤더 파일의 멤버함수 정의부에도 매개변수를 넣어주어야한다.

 

그러면 update코드는 다음처럼 바뀐다.

case 1:
    {
        int iSize;
        if (!m_student) {
            cout << "이번에 입력할 학생은 몇명인가요?" << endl;
            cin >> iSize;
            initialize(iSize);
            for (int i = 0; i < iSize; ++i) {
                m_student[i].Input();
            }
        }
        else {
            cout << "이번에 입력할 학생은 몇명인가요?" << endl;
            cin >> iSize;
            initialize(m_iSize+iSize);

        }
        m_iSize += iSize;
        break;
    }

이렇게 하면 초기입력때 iSize를 입력받고 그만큼 할당하고, 데이터를 입력받을것이고,

if문을 빠져나와 처음에 0으로 초기화 되어있는 m_iSize에다가 iSize만큼을 더해주니 현재 학생수가 되었다.

그리고 추가입력할때에는 iSize를 입력받고 기존 학생수 + 추가되는 학생수만큼 할당받을 수 있게 된다.

 

그럼 이제 학생을 입력하기만 하면 끝이네!!

라고 생각했던 시점이 저에게도 있었습니다. 다시보니 이렇게하면 기존 학생을 저장했던 위치의 포인터가 날아가 기존 학생의 데이터가 날아가게 되는군요

 

그럼 어쩔 수 없이 다시한번 initialize를 손봐야겠습니다. 

cStudent* cMainprogram::initialize(int iSize)
{
	cStudent* cTemp = new cStudent[iSize];
	return cTemp;
}

반환값을 학생 데이터 포인터를 반환하는 겁니다. 그래서 임시 학생 포인터인 cTemp를 선언해서 할당받고 그 값을 반환해주는 모습입니다.

 

이러면 update도 다시 바뀌어야겠지요?

 

case 1:
{
    int iSize;
    if (m_student) {
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> iSize;
        m_student = initialize(iSize);
        for (int i = 0; i < iSize; ++i) {
            m_student[i].Input();
        }
    }
    else {
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> iSize;
        cStudent* cTemp = initialize(m_iSize + iSize);

    }
    m_iSize += iSize;
    break;
}

처음에는 입력받은 수만큼 할당받은 데이터를 바로 멤버변수인 m_student에 넣어주고

추가입력일때는 임시 포인터인 cTemp에 넣어주었습니다. 이러면 기존 데이터를 지킬 수 있게 되었습니다.

 

어우 너무 당황한 나머지 구어체로 바뀌었네요 다시 딱딱하게 가보겠습니다.

 

이제 남은 과제는 기존 데이터를 새로운 공간에 입력하고 그뒤에 추가데이터를 입력하는것뿐이다.

기존 데이터를 옮기는것은 생각보다 쉽다.

for (int i = 0; i < m_iSize; ++i) {
    cTemp[i]=m_student[i];
}

혹은

memcpy(cTemp, m_student, sizeof(cStudent) * m_iSize);

위의 방식은 for문을 통해 기존학생수 만큼 학생데이터를 cTemp로 옮기는 과정이다.

밑의 방식은 메모리를 복사하는 함수인데 자세한 설명은 이전 동적할당 신버전에있다.

cTemp에 m_student의 데이터를 학생클래스의 데이터량*기존 학생수 만큼 복사한것이다. 즉 기존 학생의 데이터들을 복제하고 나머지는 빈 공간이다.

 

이제 복제가 끝났으니 새로운 학생의 입력을 받아야한다. 이는 이전과 다르지 않지만 주의해야할 점이 있다.

i를 0부터 새로운 학생수만큼 for문을 돌려서 cTemp[i] 하게되면 첫번째 학생이 있던 자리에 덮어쓰게 되니 기존 데이터가 손상된다. 그래서 다음과 같이 진행해야 한다.

for (int i = m_iSize; i < m_iSize + iSize; ++i) {
    cTemp[i].Input();
}

즉 기존 학생수 부터 기존학생수+새로운 학생수 만큼 반복을 돌리기 때문에 기존학생이 저장된 바로 다음 값부터 새로운 학생수만큼의 학생 데이터를 입력받는것이다.

 

그리고 또하나 해야할 일이 있다.

cTemp는 이 case1을 벗어나면 사라지게 된다. 그러니 멤버변수인 m_student에 넣어주어야한다.

또한 주의해야할 점은 이미 m_student에 할당되어있는 공간은 동적할당이기 때문에 반납 후 nullptr화 해주어야한다. 그러니 다음과같은 코드를 추가한다.

SAFE_DELETE_ARRAY(m_student);
m_student = cTemp;

이미 우리가 stdafx.h에 매크로화 시켜놓았던 SAFE_DELETE 코드인데, 이경우 동적 배열을 할당받았기 때문에 배열 해제 매크로 SAFE_DELETE_ARRAY를 이용하여 손쉽게 반납과 nullptr화 시켜주었고, 비어있는 m_student에 cTemp의 값을 넘겨주어 새로운 데이터를 멤버변수에 넣어주었다.

 

여기까지 오는데 아주 고생 많았고 "추가"기능 코드 전체를 보자.

case 1:
{
    int iSize;
    if (!m_student) {
        system("cls");
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> iSize;
        m_student = initialize(iSize);
        for (int i = 0; i < iSize; ++i) {
            m_student[i].Input();
        }
    }
    else {
        system("cls");
        cout << "이번에 입력할 학생은 몇명인가요?" << endl;
        cin >> iSize;
        cStudent* cTemp = initialize(m_iSize + iSize);
        memcpy(cTemp, m_student, sizeof(cStudent) * m_iSize);

        for (int i = m_iSize; i < m_iSize + iSize; ++i) {
            cTemp[i].Input();
        }
        SAFE_DELETE_ARRAY(m_student);
        m_student = cTemp;
    }
    m_iSize += iSize;
    break;
}

 

좋다. 이로서 추가하는 기능의 코드는 완성되었다.

 

남은 과제는 출력하는것이다. 어렵지 않다 조금만 더 가자

case 2:
    for (int i = 0; i < m_iSize; ++i) {
        m_student[i].printAll();
    }
    break;

이미 cStudent 클래스에서 전체출력을 해주는 함수를 만들었기 때문에 반복문을 통해 학생수만큼 순서대로 출력해주면 되는일이다. 

 

이전 추가가 너무 힘들어서그런지 매우 쉽다.

 

다 끝났다. 이제 종료만 남았다.

그냥 끄면 되는거 아님?? 할 수도 있지만 아직 m_student가 할당되어있다. 그러니 할당을 해제시켜주는 코드를 만들어야겠다.

우리는 할당 해제 함수인 release가 있다. 그러니 그곳에 할당해제 코드를 적어주자.

void cMainprogram::release()
{
	SAFE_DELETE_ARRAY(m_student);
}

그리고 update 분기문에서 3번을 누르면 종료하는 코드를 넣어주자.

case 3:
    return;

어?? 여기서 release함수를 불러줘야하는거 아닌가요??

 

일단 넘어가자

 

갈땐 가더라도 열심히 짠 mainProgram의 헤더와 cpp파일을 보고가자.

mainProgram.h

#pragma once
#include "Student.h"

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

private :
	cStudent* m_student;
	int m_iSize;

public :
	cStudent* initialize(int iSize);
	void update();
	void release();

};

mainProgram.cpp

#include "stdafx.h"
#include "mainProgram.h"

cMainprogram::cMainprogram()
{
	m_student = nullptr;
	m_iSize=0;
}

cMainprogram::~cMainprogram()
{
	release();
}

cStudent* cMainprogram::initialize(int iSize)
{
	cStudent* cTemp = new cStudent[iSize];
	return cTemp;
}

void cMainprogram::update()
{
	int iChoice;
	while (true) {
		system("cls");
		cout << "원하시는 기능을 선택해 주세요 \n1.입력\t2.전체출력\t3.종료" << endl;
		cin >> iChoice;

		switch (iChoice) {
		case 1:
		{
			int iSize;
			if (!m_student) {
				system("cls");
				cout << "이번에 입력할 학생은 몇명인가요?" << endl;
				cin >> iSize;
				m_student = initialize(iSize);
				for (int i = 0; i < iSize; ++i) {
					m_student[i].Input();
				}
			}
			else {
				system("cls");
				cout << "이번에 입력할 학생은 몇명인가요?" << endl;
				cin >> iSize;
				cStudent* cTemp = initialize(m_iSize + iSize);
				memcpy(cTemp, m_student, sizeof(cStudent) * m_iSize);

				for (int i = m_iSize; i < m_iSize + iSize; ++i) {
					cTemp[i].Input();
				}
				SAFE_DELETE_ARRAY(m_student);
				m_student = cTemp;
			}
			m_iSize += iSize;
			break;
		}
		case 2:
			for (int i = 0; i < m_iSize; ++i) {
				m_student[i].printAll();
			}
			system("pause");
			break;
		case 3:
			return;
		default:
			break;

		}
	}
}

void cMainprogram::release()
{
	SAFE_DELETE_ARRAY(m_student);
}

중간중간 보이는 system("cls") 는 콘솔 화면을 모두 지우는역할을 한다. 테스트과정에서 화면이 너무 난잡해지니 정리할 목적으로 추가했다.

또한 system("pause")는 시스템의 일시정지를 말한다. 출력하고 일시정지하지 않으면 그 결과를 사람이 보기도 전에 사라지기 때문이다.

 

 

이렇게 코드를 짰으니 호기롭게 빌드를 실행시키면 아무일도 일어나지 않는다. 아직 우리는 메인이 남아있다.

 

별거 없고 이거만 하면 진짜 끝이다. 조금만 기다려줘

#include "stdafx.h"
#include "mainProgram.h"

int main(void) {
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	cMainprogram mp;
	
	mp.update();
	return 0;
}

cMainprogram클래스를 호출하기 위해 mainProgram.h를 include 한것을 볼 수 있다.

그리고 cMainprogram을 mp라는 이름으로 선언했고, update 만을 호출했다.

 

아마 동일한 코드를 짰을 경우 입력과 출력 등 원하는대로 성공했을것이다.

 

또한 분기에서 종료를 눌렀는데

 {204} normal block at 0x00000294F26AFD50, 뭐 이런 코드가 안나왔다면 누수는 없는것이다.

 

왜 종료버튼을 눌렀을때 반납 안했는데도 누수가 안나죠??

메인 함수에 mp.update(); 이후에 바로 return이 있기 때문에

자동으로 cMainprogram의 소멸자가 호출되었고

소멸자에서 우리는 release를 호출했고

release에 SAFE_DELETE_ARRAY로 할당 해제했기 때문에

누수가 나지 않은것이다.

 

 

 

만일 실패했다면 다음 몇가지의 경우일 수 있다.

1. 입력을 눌렀는데 입력받지 않고 바로 다시 선택지로 돌아와요

 

입력을 눌렀을때 반복문의 조건을 봐라. iSize와 m_iSize가 있는데 m_iSize는 0이기 때문에 초기입력시 잘못 입력되었을 가능성이 있다.

 

2. 추가입력 후 출력을 눌렀는데 새로 입력받은 값이 나오고 그 뒤는 이상한 값이 나와요

 

추가입력시 데이터를 복제하는 과정을 잘 봐라, cTemp에 m_student를 넣어야한다. 반대로 했을 가능성이 있다.

 

3. 컴파일타임에 include 뭐시기뭐시기 해요

stdafx.h를 include 하지 않은 cpp가 있는경우이다.

 

4. 정확하진 않으나 (/Yc) 혹은 (/Yu)라고 하며 이상한 말을 해요

"미리 컴파일된 헤더" 의 오류이다. 설정에 가서 수정해보자

stdafx헤더와 cpp는 "만들기(/Yc)"이고

그 이외 모든 파일은 "사용하기(/Yu)"이다.

 

 

다름이 아니고 내가 겪은 오류들이다.

 

혹시 다른 오류가 있다면 구글에 검색해보자.

 

이건 쥬신에서 내준 숙제이고, 클래스를 처음 다루고+수업을 들은지 얼마 안된 시점이기 때문에 시간적 여유가 있어 다시 복기할겸 적어보았다. 이후로도 예시가 될만한 숙제가 있고, 내가 시간적 여유가 있다면 이렇게 코드 곱씹기 포스팅도 종종 해보겠다.

 

이상으로 클래스 사용 예시를 모두 마친다. 긴 글 읽어주시느라 고생 많으셨다.