헤더파일과 소스파일을 분리하는 이유에 대해 알아보기 전에 선언과 정의에 대해 먼저 알아보자
선언과 정의
C++을 사용하면 int a; 와 같이 변수를 선언해 주라는 말을 들었을 것이다.
선언은 말 그대로 어떤 변수나 함수를 사용하겠다고 컴파일러에게 변수의 존재와 타입을 알려주는 것이고, 정의는 변수나 함수가 어떤 값이나 동작을 하는지 정의해 메모리에 할당하는 것이다.
선언과 정의의 예는 아래와 같다.
int a(int); // 선언 했지만 정의는 하지 않음
extern const int a; // 선언 했지만 정의는 하지 않음
extern const int b = 1; // b를 정의함
struct S
{
int n; // S구조체의 n을 정의함
static int i; //선언은 했지만, 정의하지 않음
inline static int x; S의 x를 정의함
}; // S구조체를 정의함
int S::i; //S구조체의 i를 정의함
이제 선언과 정의의 예를 간단하게 알아봤으니 가장 궁금했던 내용에 대해 알아보자
헤더파일, 소스파일의 분리
왜 헤더파일에서는 선언을 하고 선언한 함수 등에 대한 자세한 내용은 따로 소스파일을 만들어서 정의할까?
이는 소스코드를 빌드(build)하는 과정과 관계가 있다.
빌드 단계 내용 참고
여기서 보면 전처리 단계에서 #include 에 지정한 파일의 내용이나 #define에서 정의한 매크로를 변환하는 모습을 볼 수 있다.
매크로와 const의 차이
매크로는 전처리 단계에서 실제로 매크로로 선언한 내용을 전부 변환하고, const는 메모리에 할당해 사용한다는 차이가 있다.
대신, const를 사용할 경우 타입에 대한 안전성이 보장된다.
// "a.h" 파일의 내용
void func1()
{
// func1에 대해 정의
}; //func1 함수 선언
// main.cpp
#include "a.h" //내가 만든 헤더 파일은 ""로 부른다. <>는 include 폴더에서 헤더파일을 가져옴
int main() {
func1();
return 0;
}
위와 같은 소스코드를 빌드 한다면 전처리 단계에서는 헤더 파일을 변환을 한다.
(아래 코드와 같은 의미가 됨)
// main.cpp
void func1()
{
// func1에 대해 정의
}; //func1 함수 선언
int main() {
func1();
return 0;
}
즉, #include "a.h"가 a.h 파일에 있는 내용으로 바뀐다고 생각하면 된다.
이때, 헤더파일에 func1 정의를 한다면 전처리 단계에서 정의가 여러 번 들어가게 되는데, 만약 다른 파일에서 a.h라는 파일을 참고하게 되면 a.h의 func1을 여러 번 정의하게 된다.
문제는 링킹 단계에서는 func1 함수를 정의한 func1과 연결 시켜주는데 여러번 정의가 돼서 어떤 func1을 의미하는지 모르게 된다.
즉, 중복 정의를 방지하기 위해서 소스코드와 헤더 파일을 분리한다.
이러한 문제를 방지하기 위해 include guards와 #pragma once라는 방법이 있다.
// a.h
#ifndef A_H //a.h 파일이 정의되지 않았다면 아래를 실행
#define A_H //a.h 를 정의
void func1(); //func1 함수 선언
#endif // 정의 끝
// a.h
#pragma once
void func1();
위와 같이 사용할 경우 이미 헤더파일이 정의됐다면, 해당 헤더 파일 내용을 무시하게 한다.
ex) 구조체나 변수 등을 헤더파일에 정의할 경우 중복 정의를 방지하기 위해 사용할 수 있다.
위 처럼 include guards와 #pragma once를 사용할 경우 헤더파일에 함수등을 정의하더라도 문제가 생기지 않겠지만
컴파일 단계에서 헤더파일의 내용이 바뀌면 다시 컴파일을 하게 된다.
만약 헤더파일에 선언만 하고 정의를 따로 소스파일에 할 경우 컴파일 시간을 절약할 수 있게 된다.
그리고 유지보수성과 가독성 측면에서 헤더파일과 소스파일을 분리하는 것이 좋다.
+ 추가 내용
인라인(inline) 함수
인라인 함수는, 함수를 호출할 경우 함수를 호출하지 않고 실제로 변환할 수도 있게 해주는 기능이다.
즉, 아래와 같은 코드가 있을 경우를 예로 들어보자
#include<iostream>
using namespace std;
inline int sum(int a, int b)
{
return a+b;
}
int main()
{
sum(5,6);
sum(7,8);
return 0;
}
이 경우 컴파일러가 인라인 함수를 변환하는게 낫다고 판단 되면 아래와 같이 바꾼다.
int main()
{
cout<<5+6;
cout<<7+8;
return 0;
}
그렇기 때문에 인라인 함수는 헤더파일에 정의해야 한다.
왜냐하면 컴파일 단계에서 실제로 변환을 할지 판단을 해야하는데, 인라인 함수가 선언만 됐을 경우 정의를 알 수 없기 때문이다.
+ 요즘 컴파일러는 똑똑해서 인라인 함수를 사용하지 않아도 변환하는게 낫다는 판단이 들면 알아서 처리해준다고 한다...
혹시 잘못된 내용이 있으면 알려주세요!
'C++' 카테고리의 다른 글
[프로그래밍/C++] 스레드(Thread) 사용하기 (2) (0) | 2024.02.04 |
---|---|
[프로그래밍/C++] 스레드(Thread) 사용하기 (0) | 2024.02.03 |