파일을
- 클래스 정의(인터페이스 파일)
- 멤버 함수 정의(구현 파일)
- 애플리케이션(애플리케이션 파일)
이렇게 세 부분으로 나눈다
인터페이스 파일
인터페이스 파일은 클래스 정의(데이터 멤버 선언과 멤버 함수 선언)가 포함된 파일이다
이 파일은 클래스의 형태를 다른 파일에 알려주는 역할을 수행한다
이 파일의 이름은 일반적으로 circle.h처럼 h 확장자를 붙이며 이때 h라는 문자 이름은 헤더 파일을 의미한다
/***************************************************************
* 이 파일은 Circle 클래스의 인터페이스 파일입니다. *
* 데이터 멤버와 멤버 함수의 선언을 포함하고 있습니다. *
* 이 파일은 구현 파일 및 응용 프로그램 파일의 상단에 포함됩니다. *
***************************************************************/
#ifndef CIRCLE_H
#define CIRCLE_H
#include <iostream>
#include <cassert>
using namespace std;
// 클래스 정의
class Circle
{
private:
double radius; // 반지름 (데이터 멤버)
public:
Circle(double radius); // 매개변수 생성자
Circle(); // 기본 생성자
Circle(const Circle& circle); // 복사 생성자
~Circle(); // 소멸자
void setRadius(double radius); // 설정자 함수 (Mutator)
double getRadius() const; // 접근자 함수 (Accessor)
double getArea() const; // 접근자 함수 (면적 계산)
double getPerimeter() const; // 접근자 함수 (둘레 계산)
};
#endif
구현 파일
구현 파일은 멤버 함수 정의가 포함된 파일이다
인터페이스 파일에는 모든 멤버 함수의 선언을 입력한다
이 파일의 이름은 일반적으로 circle.cpp처럼 cpp 확장자를 붙인다
/***************************************************************
* 이 파일은 멤버 함수들의 정의를 담고 있는 구현 파일입니다. *
* 컴파일을 위해 인터페이스 파일을 상단에 포함합니다. *
***************************************************************/
#include "circle.h"
/*****************************************************************
* 매개변수 생성자입니다. 하나의 인수를 받아 반지름을 초기화합니다. *
* assert 함수를 이용해 반지름이 양수인지 검사하며, *
* 조건을 만족하지 않으면 프로그램을 종료합니다. *
*****************************************************************/
Circle::Circle(double rds)
: radius(rds)
{
if (radius < 0.0) {
assert(false); // 반지름이 음수면 프로그램 중단
}
}
/*****************************************************************
* 기본 생성자입니다. 반지름을 0.0으로 초기화합니다. *
* 유효성 검사는 필요하지 않습니다. *
*****************************************************************/
Circle::Circle()
: radius(0.0)
{
}
/*****************************************************************
* 복사 생성자입니다. 다른 Circle 객체의 반지름을 복사하여 *
* 새로운 객체를 생성합니다. 복사되는 원은 이미 유효성이 *
* 검증되었으므로 별도 검증이 필요 없습니다. *
*****************************************************************/
Circle::Circle(const Circle& circle)
: radius(circle.radius)
{
}
/*****************************************************************
* 소멸자입니다. 객체가 소멸될 때 호출되며 정리 작업을 수행합니다. *
*****************************************************************/
Circle::~Circle()
{
}
/*****************************************************************
* setRadius 함수는 반지름 값을 변경하는 설정자입니다. *
* 새로운 반지름도 양수인지 검증해야 합니다. *
*****************************************************************/
void Circle::setRadius(double value)
{
radius = value;
if (radius < 0.0) {
assert(false); // 음수 값 방지
}
}
/***************************************************************
* getRadius 함수는 현재 반지름을 반환하는 접근자입니다. *
* 객체를 변경하지 않도록 const 한정자를 사용합니다. *
***************************************************************/
double Circle::getRadius() const
{
return radius;
}
/*****************************************************************
* getArea 함수는 현재 객체의 면적을 반환하는 접근자입니다. *
* 객체를 변경하지 않기 위해 const 한정자를 사용합니다. *
*****************************************************************/
double Circle::getArea() const
{
const double PI = 3.14;
return (PI * radius * radius);
}
/*****************************************************************
* getPerimeter 함수는 현재 객체의 둘레를 반환하는 접근자입니다. *
* 객체를 변경하지 않기 위해 const 한정자를 사용합니다. *
*****************************************************************/
double Circle::getPerimeter() const
{
const double PI = 3.14;
return (2 * PI * radius);
}
애플리케이션 파일
애플리케이션 파일은 객체를 인스턴스화하고 객체를 활용하는 main 함수의 코드가 포함된 파일이다
애플리케이션 파일은 반드시 cpp 확장자를 사용해야 한다
파일의 이름은 어떻게 지어도 상관 없지만, 나는 애플리케이션 파일은 app.cpp 라는 이름을 붙여서 사용한다
/*****************************************************************
* 이 파일은 애플리케이션 파일로, 객체를 생성하고 *
* 멤버 함수를 통해 객체 스스로 동작하도록 합니다. *
* 컴파일을 위해 인터페이스 파일을 포함해야 합니다. *
*****************************************************************/
#include "circle.h"
int main()
{
// 첫 번째 객체 생성 및 연산 수행
Circle circle1(5.2);
cout << "반지름: " << circle1.getRadius() << endl;
cout << "면적: " << circle1.getArea() << endl;
cout << "둘레: " << circle1.getPerimeter() << endl;
cout << endl;
// 두 번째 객체 생성 (복사 생성자 사용) 및 연산 수행
Circle circle2(circle1);
cout << "반지름: " << circle2.getRadius() << endl;
cout << "면적: " << circle2.getArea() << endl;
cout << "둘레: " << circle2.getPerimeter() << endl;
cout << endl;
// 세 번째 객체 생성 (기본 생성자 사용) 및 연산 수행
Circle circle3;
cout << "반지름: " << circle3.getRadius() << endl;
cout << "면적: " << circle3.getArea() << endl;
cout << "둘레: " << circle3.getPerimeter() << endl;
cout << endl;
return 0;
}
결과는 아래와 같다
실행 결과:
반지름: 5.2
면적: 84.9056
둘레: 32.656
반지름: 5.2
면적: 84.9056
둘레: 32.656
반지름: 0
면적: 0
둘레: 0
참고로 인터페이스 파일에 있는 #ifndef에 대해 알아보겠다.
같은 헤더 파일을 2회 이상 읽어 들이면 컴파일할 때 오류가 발생한다
이러한 상황을 막으려면 헤더 파일 작성시
다음과 같이 define, ifndef, endif라는 3가지 전처리 지시문 (preprocessor directive)을 사용한다
#ifndef CIRCLE_H
#define CIRCLE_H
//circle.h 파일의 내용
#endif
ifndef 지시문은 if 조건문과 비슷하다
만약 플래그가 정의되어 있지 않다면(아직 헤더 파일을 한 번도 안읽었다면)
ifndef의 본문을 읽어 들여 CIRCLE_H의 코드를 포함한다
플래그가 이미 정의되어 있다면(헤더 파일을 이미 읽었었다면)
이후의 내용(circle.h 파일의 내용)을 무시하고 곧바로 endif 지시문 위치 로 이동
왜 파일을 별도로 컴파일해야 할까?
별도로 컴파일해야 객체 지향 프로그래밍의 캡슐화라는 목표를 이룰 수 있기 때문이다
객체 지향 프로그래밍은 캡슐화로 클래스 설계와 클래스 사용을 구분한다
클래스 설계
설계자가 인터페이스 파일과 구현 파일을 만들며 설계자는 인터페이스 파일만 공개한다
구현 파일의 경우는 컴파일한 것만 공개하고 소스 코드는 비공개로 유지한다
설계자는 필요할 때마다 구현 파일을 변경하고 컴파일한 후에 다시 배포한다
클래스 사용
사용자는 인터페이스 파일의 복사본과 컴파일된 구현 파일을 받는다
이를 애플리케이션 파일에서 읽어 들이고 컴파일한다
최종적으로 모든 파일을 연결해서 실행 파일을 생성한다
이유
이를 통해 무엇을 얻을 수 있는가?
설계자는 사용자의 변경으로부터 인터페이스 파일과 구현 파일을 보호할 수 있다
설계자가 컴파일된 구현 파일을 사용자에게 배포하므로, 구현 파일도 사용자가 변경 할 수 없다
컴파일은 단방향으로 이루어지므로, 컴파일된 구현 파일로는 원본 파일을 구할 수 없다
따라서 전체적인 설계는 상자 내부에 캡슐화되어 감추어지며, 사용자가 변경할 수 없다
그럼 사용자는 원본 파일이 없으므로 코드의 사용법에 대해 이해가 어려울 수 있다.
이을 위해 설계자는 일반적으로 공용 인터페이스(public interface)라는 것을 만들어서 제공한다
공용 인터페이스는 사용자가 멤버 함수의 선언과 설명을 정리한 것이다
함수 정의가 적혀 있는 텍스트 파일이다.
아래는 분모, 분자를 설정하고 정규화된 분수를 출력하는 fraction 프로젝트이다.
fraction.h
/***************************************************************
* Fraction 클래스를 정의하는 인터페이스 파일 fraction.h *
***************************************************************/
#ifndef FRACTION_H
#define FRACTION_H
#include <iostream>
using namespace std;
class Fraction
{
// 데이터 멤버
private:
int numer; // 분자
int denom; // 분모
// 공개 멤버 함수
public:
// 생성자들
Fraction(int num, int den); // 매개변수 생성자
Fraction(); // 기본 생성자
Fraction(const Fraction& fract); // 복사 생성자
~Fraction(); // 소멸자
// 접근자 (Getter)
int getNumer() const; // 분자 반환
int getDenom() const; // 분모 반환
void print() const; // 분수 출력
// 설정자 (Setter)
void setNumer(int num); // 분자 설정
void setDenom(int den); // 분모 설정
// 내부에서 사용하는 보조(private) 멤버 함수
private:
void normalize(); // 분수를 정규화 (기약분수로)
int gcd(int n, int m); // 최대공약수 계산
};
#endif
fraction.cpp
/****************************************************************
* 이 파일은 Fraction 클래스의 인스턴스 멤버 함수와 *
* 도우미 함수(helper function)들을 정의한 구현 파일입니다. *
****************************************************************/
#include <iostream>
#include <cmath>
#include <cassert>
#include "fraction.h"
using namespace std;
/***************************************************************
* 매개변수 생성자는 분자와 분모의 값을 받아 객체를 초기화하고, *
* 클래스 불변 조건(invariant)에 따라 분자와 분모를 정규화합니다. *
***************************************************************/
Fraction::Fraction(int num, int den)
: numer(num), denom(den)
{
normalize();
}
/***************************************************************
* 기본 생성자는 분수를 0/1 형태로 생성합니다. *
* 유효성 검사는 필요하지 않습니다. *
***************************************************************/
Fraction::Fraction()
: numer(0), denom(1)
{
}
/*****************************************************************
* 복사 생성자는 기존의 Fraction 객체로부터 새로운 객체를 생성합니다. *
* 원본 객체는 이미 정규화되어 있기 때문에, 별도 정규화는 필요 없습니다. *
*****************************************************************/
Fraction::Fraction(const Fraction& fract)
: numer(fract.numer), denom(fract.denom)
{
}
/***************************************************************
* 소멸자는 Fraction 객체를 정리해 메모리를 재활용하도록 합니다. *
***************************************************************/
Fraction::~Fraction()
{
}
/***************************************************************
* getNumer 함수는 현재 객체의 분자를 반환하는 접근자입니다. *
* 객체를 수정하지 않기 위해 const 한정자가 사용됩니다. *
***************************************************************/
int Fraction::getNumer() const
{
return numer;
}
/***************************************************************
* getDenom 함수는 현재 객체의 분모를 반환하는 접근자입니다. *
* 객체를 수정하지 않기 위해 const 한정자가 사용됩니다. *
***************************************************************/
int Fraction::getDenom() const
{
return denom;
}
/*****************************************************************
* print 함수는 분수 형태를 x/y로 출력하는 접근자입니다. *
* 화면 출력이라는 부수 효과(side effect)가 있습니다. *
*****************************************************************/
void Fraction::print() const
{
cout << numer << "/" << denom << endl;
}
/****************************************************************
* setNumer 함수는 객체의 분자를 변경하는 설정자 함수입니다. *
* 값을 변경한 후에는 반드시 정규화가 필요합니다. *
****************************************************************/
void Fraction::setNumer(int num)
{
numer = num;
normalize();
}
/***************************************************************
* setDenom 함수는 객체의 분모를 변경하는 설정자 함수입니다. *
* 값을 변경한 후에는 반드시 정규화가 필요합니다. *
***************************************************************/
void Fraction::setDenom(int den)
{
denom = den;
normalize();
}
/***************************************************************
* normalize 함수는 다음 세 가지 클래스 불변 조건을 유지합니다. *
* 1) 분모는 0이 아니어야 함 *
* 2) 분모는 항상 양수여야 함 *
* 3) 분자와 분모는 서로소 형태여야 함 *
***************************************************************/
void Fraction::normalize()
{
// 분모가 0인 경우 예외 처리
if (denom == 0) {
cout << "분모가 0입니다. 프로그램을 종료합니다." << endl;
assert(false);
}
// 분모가 음수인 경우 부호 정리
if (denom < 0) {
denom = -denom;
numer = -numer;
}
// 최대공약수로 나누어 기약분수로 만들기
int divisor = gcd(abs(numer), abs(denom));
numer = numer / divisor;
denom = denom / divisor;
}
/***************************************************************
* gcd 함수는 분자와 분모의 최대공약수를 계산합니다. *
***************************************************************/
int Fraction::gcd(int n, int m)
{
int gcd = 1;
for (int k = 1; k <= n && k <= m; k++) {
if (n % k == 0 && m % k == 0) {
gcd = k;
}
}
return gcd;
}
app.cpp
/***************************************************************
* 이 파일은 Fraction 객체를 사용하는 애플리케이션 파일입니다. *
***************************************************************/
#include "fraction.h"
#include <iostream>
using namespace std;
int main()
{
// Fraction 객체 생성
Fraction fract1;
Fraction fract2(14, 21);
Fraction fract3(11, -8);
Fraction fract4(fract3); // 복사 생성자 사용
// 생성된 객체 출력
cout << "생성된 네 개의 분수 출력: " << endl;
cout << "fract1: ";
fract1.print();
cout << "fract2: ";
fract2.print();
cout << "fract3: ";
fract3.print();
cout << "fract4: ";
fract4.print();
// 설정자(Mutator) 사용
cout << "앞의 두 분수를 변경 후 출력:" << endl;
fract1.setNumer(4);
cout << "fract1: ";
fract1.print();
fract2.setDenom(-5);
cout << "fract2: ";
fract2.print();
// 접근자(Accessor) 사용
cout << "변경된 분수의 멤버 확인:" << endl;
cout << "fract1의 분자: " << fract1.getNumer() << endl;
cout << "fract2의 분모: " << fract2.getDenom() << endl;
return 0;
}
결과물
실행 결과:
분수 4개 생성 후 출력:
fract1: 0/1
fract2: 2/3
fract3: -11/8
fract4: -11/8
앞의 두 분수를 변경 후 출력:
fract1: 4/1
fract2: -2/5
변경된 분수의 멤버 확인:
fract1의 분자: 4
fract2의 분모: 5
다음은은 시간을 설정하고 출력하는 Time 프로젝트이다
time.h
/***************************************************************
* Time 클래스의 인터페이스 파일 (time.h) *
***************************************************************/
#ifndef TIME_H
#define TIME_H
#include <iostream>
using namespace std;
class Time
{
private:
int hours; // 시
int minutes; // 분
int seconds; // 초
public:
Time(int hours, int minutes, int seconds); // 매개변수 생성자
Time(); // 기본 생성자
~Time(); // 소멸자
void print() const; // 현재 시간을 출력하는 함수
void tick(); // 1초 증가시키는 함수
private:
void normalize(); // 시간 형식을 정규화하는 보조 함수 (ex. 60초 → 1분)
};
#endif
time.cpp
/***************************************************************
* Time 클래스의 멤버 함수들을 구현한 구현 파일 (time.cpp) *
***************************************************************/
#include <cmath>
#include <cassert>
#include "time.h"
/*****************************************************************
* 매개변수 생성자는 사용자로부터 시간, 분, 초 값을 입력받아 *
* 객체를 초기화합니다. *
* 시간 값들이 유효한 범위에 있도록 normalize 함수를 호출합니다. *
*****************************************************************/
Time::Time(int hr, int mi, int se)
: hours(hr), minutes(mi), seconds(se)
{
normalize();
}
/***************************************************************
* 기본 생성자는 시간을 00:00:00으로 초기화하는 객체를 생성합니다. *
***************************************************************/
Time::Time()
: hours(0), minutes(0), seconds(0)
{
}
/*****************************************************************
* 소멸자는 객체가 소멸되기 전에 정리 작업을 수행합니다. *
*****************************************************************/
Time::~Time()
{
}
/***************************************************************
* print 함수는 시간을 출력하는 접근자 함수이며, *
* 화면에 출력하는 부수 효과(side effect)를 가집니다. *
***************************************************************/
void Time::print() const
{
cout << hours << ":" << minutes << ":" << seconds << endl;
}
/*****************************************************************
* tick 함수는 시간을 1초 증가시키는 설정자(mutator) 함수입니다. *
* 증가 후 정상적인 시간 형식이 되도록 normalize를 호출합니다. *
*****************************************************************/
void Time::tick()
{
seconds++;
normalize();
}
/***************************************************************
* normalize 함수는 클래스 불변 조건(invariant)을 확인하고, *
* 조건이 위반되면 프로그램을 중단하거나 시간 값을 정규화합니다. *
***************************************************************/
void Time::normalize()
{
// 음수 값이 있는 경우 프로그램 종료
if ((hours < 0) || (minutes < 0) || (seconds < 0)) {
cout << "시간 값이 유효하지 않습니다. 프로그램을 종료합니다!" << endl;
assert(false);
}
// 초가 59보다 크면 분으로 환산
if (seconds > 59) {
int temp = seconds / 60;
seconds = seconds % 60;
minutes = minutes + temp;
}
// 분이 59보다 크면 시간으로 환산
if (minutes > 59) {
int temp = minutes / 60;
minutes = minutes % 60;
hours = hours + temp;
}
// 24시간제를 넘으면 0부터 다시 시작
if (hours > 23) {
hours = hours % 24;
}
}
app.cpp
/***************************************************************
* Time 클래스를 사용하는 애플리케이션 파일 (app.cc) *
***************************************************************/
#include "time.h"
int main()
{
// Time 객체 생성 (초기 시간 설정: 4시 5분 27초)
Time time(4, 5, 27);
// 원래 시간 출력
cout << "원래 시간: ";
time.print();
// 143500초를 기존 시간에 더함 (tick()을 반복 호출)
for (int i = 0; i < 143500; i++) {
time.tick();
}
// 143500초가 경과한 후의 시간 출력
cout << "143500초 경과 후 시간: ";
time.print();
return 0;
}
결과물
실행 결과:
원래 시간: 4:5:27
143500초 경과 후 시간: 19:57:7
댓글