헤더와 소스 파일 분리
C++ 프로그램의 규모가 커지면 모든 코드를 하나의 파일에 작성하는 것은 유지보수 측면에서 비효율적입니다. 이를 해결하기 위해 선언부와 구현부를 분리하여 관리하는 것이 일반적인 관행입니다.
헤더 파일(.h)의 역할
헤더 파일은 다음과 같은 선언부를 포함합니다:
•
클래스 선언
•
함수 원형(프로토타입)
•
상수 정의
•
전역 변수 선언
소스 파일(.cpp)의 역할
소스 파일은 실제 구현부를 포함합니다:
•
함수 구현
•
클래스 메서드 정의
파일 분리 예제
앞서 작성했던 Player 클래스를 헤더 파일과 소스 파일로 분리해보겠습니다.
player.h
#ifndef PLAYER_H
#define PLAYER_H
#include <iostream>
using namespace std;
class Player
{
public:
Player(const char* name, int height);
Player();
Player(const Player& another);
~Player();
void Display();
private:
char mName[256];
int mHeight;
};
#endif
C++
복사
player.cpp
#include "player.h"
#include <cstring>
Player::Player(const char* name, int height)
{
// 문자열 복사
int len = strlen(name);
for (int i = 0; i < len; i++)
mName[i] = name[i];
mName[len] = '\0'; // null 종료 문자 추가
mHeight = height;
}
Player::Player()
{
strcpy(mName, "나달");
mHeight = 183;
}
Player::Player(const Player& another)
{
strcpy(mName, another.mName);
mHeight = another.mHeight;
}
Player::~Player()
{
cout << mName << " 객체가 소멸되었습니다." << endl;
}
void Player::Display()
{
cout << "선수의 이름: " << mName << endl;
cout << "선수의 키: " << mHeight << "cm" << endl;
}
C++
복사
main.cpp
#include "player.h"
int main()
{
Player federer("페더러", 184);
federer.Display();
Player nadal(federer);
nadal.Display();
return 0;
}
C++
복사
헤더 파일 보호자(Header Guard)
헤더 파일 상단의 #ifndef, #define, #endif는 헤더 파일 보호자입니다. 이는 헤더 파일이 여러 번 포함(include)되는 것을 방지하여 중복 정의 오류를 막아줍니다.
작동 원리:
1.
#ifndef PLAYER_H: PLAYER_H가 정의되지 않았다면
2.
#define PLAYER_H: PLAYER_H를 정의하고
3.
헤더 내용 포함
4.
#endif: 조건부 컴파일 종료
파일 분리의 장점
1.
가독성 향상: 선언부와 구현부가 분리되어 코드 구조 파악이 용이합니다.
2.
재사용성: 헤더 파일을 다른 프로젝트에서도 쉽게 사용할 수 있습니다.
3.
컴파일 최적화: 헤더만 변경되지 않으면 소스 파일만 재컴파일할 수 있습니다.
4.
인터페이스 명확화: 헤더 파일만 보고도 클래스의 사용법을 파악할 수 있습니다.
생성자의 개념과 선언
생성자란?
생성자(Constructor)는 객체가 생성될 때 자동으로 호출되어 멤버 변수를 초기화하는 특수한 멤버 함수입니다. 클래스의 객체를 사용 가능한 상태로 만드는 것이 주된 목적입니다.
생성자의 선언 규칙
1.
이름: 생성자의 이름은 클래스 이름과 동일해야 합니다.
2.
반환 타입 없음: 반환 타입을 명시하지 않으며, void도 사용하지 않습니다.
3.
매개변수: 초기화에 필요한 값을 매개변수로 받습니다.
4.
오버로딩 가능: 매개변수가 다르면 여러 개의 생성자를 정의할 수 있습니다.
생성자 예제
class Player
{
public:
// 기본 생성자
Player()
{
strcpy(mName, "Unknown");
mHeight = 0;
}
// 매개변수가 있는 생성자
Player(const char* name, int height)
{
int len = strlen(name);
for (int i = 0; i < len; i++)
mName[i] = name[i];
mName[len] = '\0';
mHeight = height;
}
void Display()
{
cout << "이름: " << mName << ", 키: " << mHeight << "cm" << endl;
}
private:
char mName[256];
int mHeight;
};
C++
복사
char mName[256];
int mHeight;
생성자의 정의 (헤더와 소스 분리)
헤더 파일과 소스 파일을 분리할 경우, 생성자는 다음과 같이 정의합니다:
// player.h
class Player
{
public:
Player();
Player(const char* name, int height);
void Display();
private:
char mName[256];
int mHeight;
};
// player.cpp
Player::Player()
{
strcpy(mName, "Unknown");
mHeight = 0;
}
Player::Player(const char* name, int height)
{
strcpy(mName, name);
mHeight = height;
}
C++
복사
범위 지정 연산자(::)를 사용하여 어느 클래스의 멤버 함수인지 명시합니다.
객체 생성 방법
int main()
{
// 방법 1: 암시적 호출
Player nadal("나달", 183);
nadal.Display();
// 방법 2: 명시적 호출
Player federer = Player("페더러", 184);
federer.Display();
return 0;
}
C++
복사
실행 결과:
이름: 나달, 키: 183cm
이름: 페더러, 키: 184cm
class Example
{
public:
Example(int value); // 매개변수가 있는 생성자만 정의
};
int main()
{
Example obj1(10); // OK
Example obj2; // 오류! 디폴트 생성자가 없음
}
C++
복사
디폴트 생성자 정의 방법
방법 1: 매개변수가 없는 생성자 정의
class Player
{
public:
Player()
{
strcpy(mName, "나달");
mHeight = 183;
}
Player(const char* name, int height)
{
strcpy(mName, name);
mHeight = height;
}
private:
char mName[256];
int mHeight;
};
C++
복사
방법 2: 디폴트 인수 사용
class Player
{
public:
Player(const char* name = "나달", int height = 183)
{
strcpy(mName, name);
mHeight = height;
}
private:
char mName[256];
int mHeight;
};
C++
복사
디폴트 인수를 사용하면 하나의 생성자로 여러 호출 형태를 지원할 수 있습니다.
디폴트 생성자로 객체 선언하기
디폴트 생성자로 객체를 선언할 때는 다음과 같은 형태를 사용합니다:
// 올바른 방법 1: 괄호 없이 선언
Player nadal;
// 올바른 방법 2: 명시적 호출 후 대입
Player nadal = Player();
// 올바른 방법 3: 동적 할당
Player* ptr_nadal = new Player;
// 사용 후 반드시 메모리 해제
delete ptr_nadal;
// 잘못된 방법: 함수 선언으로 인식됨
Player nadal(); // 이렇게 하면 안 됨!
C++
복사
Player nadal();은 함수 선언으로 해석되므로 주의해야 합니다.
사용 예제
int main()
{
// 디폴트 생성자 사용
Player player1;
player1.Display();
// 매개변수가 있는 생성자 사용
Player player2("페더러", 184);
player2.Display();
return 0;
}
C++
복사
실행 결과:
이름: 나달, 키: 183cm
이름: 페더러, 키: 184cm
class Player
{
public:
// 일반 생성자들
Player();
Player(const char* name, int height);
// 복사 생성자
Player(const Player& another);
~Player();
void Display();
private:
char mName[256];
int mHeight;
};
C++
복사
복사 생성자 구현
Player::Player(const Player& another)
{
// 멤버 변수를 하나하나 복사
strcpy(mName, another.mName);
mHeight = another.mHeight;
cout << "복사 생성자 호출: " << mName << endl;
}
C++
복사
복사 생성자에서는 원본 객체의 멤버 변수 값을 새 객체로 복사합니다. 참조(&)를 사용하는 이유는 객체 복사 시 불필요한 오버헤드를 방지하기 위함입니다.
복사 생성자가 호출되는 경우
1.
객체를 다른 객체로 초기화할 때
Player federer("페더러", 184);
Player nadal(federer); // 복사 생성자 호출
C++
복사
1.
객체를 함수에 값으로 전달할 때
void PrintPlayer(Player p) // 복사 생성자 호출
{
p.Display();
}
int main()
{
Player federer("페더러", 184);
PrintPlayer(federer); // 복사 생성자 호출
}
C++
복사
1.
함수에서 객체를 값으로 반환할 때
Player CreatePlayer()
{
Player temp("임시", 180);
return temp; // 복사 생성자 호출
}
C++
복사
전체 예제
int main()
{
// 원본 객체 생성
Player federer("페더러", 184);
cout << "원본 객체:" << endl;
federer.Display();
cout << endl;
// 복사 생성자를 통한 복사
Player nadal(federer);
cout << "복사된 객체:" << endl;
nadal.Display();
return 0;
}
C++
복사
실행 결과:
원본 객체:
이름: 페더러, 키: 184cm
복사 생성자 호출: 페더러
복사된 객체:
이름: 페더러, 키: 184cm
페더러 객체가 소멸되었습니다.
페더러 객체가 소멸되었습니다.
// player.h
class Player
{
public:
Player();
Player(const char* name, int height);
Player(const Player& another);
~Player(); // 소멸자 선언
void Display();
private:
char mName[256];
int mHeight;
};
// player.cpp
Player::~Player()
{
cout << mName << " 객체가 소멸되었습니다." << endl;
}
C++
복사
소멸자가 호출되는 시점
1.
지역 객체: 함수나 블록이 끝날 때
void Function()
{
Player temp("임시", 180);
// 함수 종료 시 temp의 소멸자 호출
}
C++
복사
1.
전역 객체: 프로그램 종료 시
2.
동적 할당 객체: delete 호출 시
Player* ptr = new Player("동적", 175);
delete ptr; // 이 시점에 소멸자 호출
C++
복사
소멸자의 중요성
소멸자는 다음과 같은 상황에서 특히 중요합니다:
1.
동적 메모리 할당: 생성자에서 new로 할당한 메모리를 소멸자에서 delete로 해제
2.
파일 작업: 열린 파일을 닫기
3.
네트워크 연결: 소켓 연결 종료
4.
리소스 해제: 기타 시스템 리소스 정리
동적 메모리를 사용하는 예제
지금 당장은 중요하지 않습니다. 동적 할당이라는 개념이 있다는 정도만 이해하고 넘어가시길 추천합니다.
동적 할당에 대해서는 추후 링크드 리스트에서 자세히 배웁니다.
class DynamicArray
{
public:
DynamicArray(int size)
{
mSize = size;
mData = new int[size];
cout << "배열 생성: " << size << "개 요소" << endl;
}
~DynamicArray()
{
delete[] mData;
cout << "배열 해제" << endl;
}
private:
int* mData;
int mSize;
};
C++
복사
이 예제에서 소멸자는 생성자에서 할당한 동적 배열을 해제합니다. 소멸자가 없다면 메모리 누수(Memory Leak)가 발생합니다.
전체 실행 예제
int main()
{
cout << "=== 프로그램 시작 ===" << endl;
Player federer("페더러", 184);
federer.Display();
cout << endl;
Player nadal(federer);
nadal.Display();
cout << "\n=== 프로그램 종료 ===" << endl;
// 여기서 nadal과 federer의 소멸자가 역순으로 호출됨
return 0;
}
C++
복사
실행 결과:
객체는 생성의 역순으로 소멸됩니다. 즉, 가장 나중에 생성된 객체가 먼저 소멸됩니다(LIFO: Last In, First Out).
주의사항
1.
동적 할당과 해제의 짝: new로 할당한 메모리는 반드시 delete로 해제해야 합니다.
2.
배열 해제: new[]로 할당한 배열은 delete[]로 해제해야 합니다.
3.
소멸자 내 예외: 소멸자에서는 예외를 발생시키지 않는 것이 좋습니다.
4.
가상 소멸자: 상속을 사용하는 경우, 기본 클래스의 소멸자는 가상 함수로 선언해야 합니다.
