[Advanced C++] 5. 헤더파일, 헤더가드

dev.kelvin·2024년 12월 16일
1

Advanced C++

목록 보기
5/74
post-thumbnail

1. 헤더파일

헤더파일

C++에서는 cpp와 헤더파일을 사용하여 프로그래밍 한다, 보통 헤더파일은 .h의 확장자를 갖지만 가끔 .hpp확장자나 확장자가 없는 경우도 존재한다

헤더파일에 선언을 하고 해당 헤더파일을 include하여 필요한 곳에서 가져와 사용이 가능하다, 이로써 여러 파일에서의 전방선언 입력을 줄일 수 있다

	#include <iostream>

    int main()
    {
        std::cout << "Hello, world!";
        return 0;
    }

위 코드에서 std::cout은 따로 정의하지 않았지만 사용이 가능하다, 이는 iostream 헤더파일에서 선언되었기 때문에 사용이 가능한 것이다

앞에서 정리했듯 전처리기가 iostream이라는 파일의 모든 내용을 해당 파일에 복사 처리하는 개념이다

iostream은 왜 .h 확장자를 include하지 않을까?

iostream include는 .h파일을 include하지 않는다, 왜 일까?

C++이 처음 만들어졌을때 Standard Library의 모든 헤더는 .h확장자를 가졌다, 따라서 원래 cout, cin도 iostream.h에 선언이 되었었다

하지만 식별자 명명 충돌을 방지하기 위해 std namespace로 옮기게 되었고 이때 기존의 iostream.h를 사용하는 프로젝트에서 문제가 생기게 되어 .h가 없는 헤더파일인 iostream을 도입하게 된 것이다

만약 헤더파일을 사용하지 않고 전방선언만을 이용하여 위 코드를 컴파일 하기 위해서는 std::cout과 연관된 모든 선언을 수동으로 선언해야 한다, 이는 함수 프로토타입이 추가 or 변경되었을 때 전부 수동으로 업데이트 해야하기 때문에 굉장히 비효율적이다

	//foo.cpp
    
    int foo(int a, int b)
    {
    	return a + b;
    }
    
    //main.cpp
    
    int foo(int a, int b);
    
    int main()
    {
    	foo(10, 20);
    }

C++에서 cpp파일만 직접적으로 빌드 대상에 포함된다, 이때 헤더파일은 #include 지시문을 통해 cpp에 포함되어 간접적으로 빌드된다

따라서 명시적으로 헤더파일을 컴파일 명령에 추가하면 안된다

헤더파일은 cpp파일과 짝을 이루며 해당 cpp파일에 대한 전방선언을 제공한다 (짝을 이루기 때문에 헤더와 cpp는 동일한 이름의 파일로 구성되어야 한다)

	//Add.h
    int foo(int a, int b);
    
    //Add.cpp
    int foo(int a, int b)
    {
    	return a + b;
    }
    
    //main.cpp
    #include "Add.h"
    
    int main()
    {
    	foo(10, 20);
    } 

프로그래머가 직접 만든 헤더파일을 include할때는 C++ library를 include하는 방식과 다르게 ""안에 경로를 입력해야 한다, < >는 Standard Library나 외부 Library 헤더를 include할 때 사용한다

""를 사용하면 전처리기가 프로젝트 경로를 먼저 검색 후 include directory를 검색한다, 하지만 <>를 사용하게 되면 전처리기는 include directory에서만 검색한다

위의 예시에서는 main.cpp에는 Add.h의 내용이 전부 복사되고 이때 foo(int a, int b)에 대한 전방선언이 포함되어 있기 때문에 main.cpp에서 foo()를 호출할 수 있게 되는것이다

이 과정을 도식화하면 다음과 같다

헤더파일 작성 시 주의할 점

헤더파일에 함수나 변수의 정의를 넣는것은 피하는것이 좋다, 이는 헤더파일이 두개 이상의 소스파일에 include된다면 단일 정의 규칙인 ODR을 위배하기 때문이다

예를들어

	//Add.h
    
    int foo(int a, int b)
    {
    	return a + b;
    }
    
    //Add.cpp
    #include "Add.h"
    
    //main.cpp
    #include "Add.h"
    
    int main()
    {
    	foo(10, 20);
    }

이렇게 되면 링크단계에서 Add.cpp와 main.cpp에 동일한 정의가 있다는것을 알게되고 ODR을 위배하게 된다 (같은 범위에서의 함수, 변수, type 혹은 template들은 하나의 정의만 가질 수 있다)

헤더파일에서 안전하게 정의할 수 있는 방법은 추후 정리해보겠다 (헤더 가드)

cpp파일에서는 쌍을 이루는 헤더파일을 #include해야 한다, 이는 링크 타임이 아닌 컴파일 타임에서 오류를 발견할 수 있기 때문이다

	//Test.h
    int Test(int);
    
    //Test.cpp
    #include "Test.h"
    
    void Test(int)
    {
    	
    }

Test.h를 include하기 때문에 Test.cpp에서의 Test() 의 return type이 일치하지 않는 컴파일 오류를 표시한다, 만약 헤더파일을 include하지 않았다면 링크단계에서 발견하기 때문에 시간이 낭비된다

그렇다면 .cpp를 include하면 어떨까?

결론적으로 말하면 .cpp는 include하면 안된다, 이유는 다음과 같다

  • ODR 위배 (cpp내부에 있는 전역 변수, 함수, 클래스의 중복 정의 가능성)
  • include 된 cpp파일을 변경 시 cpp파일과 이 파일을 include하는 모든 파일을 recompile하기 때문에 시간이 오래 걸린다

헤더에 다른 헤더 include

헤더에는 다른 헤더가 include될 수 있다, 이때 include된 헤더파일에 include된 다른 헤더파일도 같이 include되게 된다, 이는 명시적이지 않고 암묵적으로 include되기 때문에 transitive includes (전이적 include)라고 칭한다

	//Test.h
    #include "Study.h"
    
	//foo.h
    #include "Test.h"
    
    //main.cpp
    #include "foo.h" //Test.h와 Study.h 파일이 같이 들어오는 Transitive Includes이다

이러한 Transitive Includes를 통한 내용들도 해당 cpp에서 사용이 가능하다, 하지만 의존해서는 안된다 왜냐하면 이렇게 간접적으로 include된 내용은 변경될 수 있고 추후에 컴파일되지 않을 수 있기 때문이다

Transitive Includes에 의존하지 않기 위해서는 해당 cpp파일에서 컴파일하는데 필요한 모든 헤더파일을 명시적으로 include해야 한다

헤더파일은 다음과 같은 순서로 include하는게 좋다

  1. cpp와 페어링 된 헤더파일
  2. 같은 프로젝트의 다른 헤더
  3. third party library 헤더
  4. standard library 헤더

#include 누락 시 컴파일 에러 발생 가능성이 높아 오류 수정이 더 쉽다

헤더파일은 다음과 같은 권장사항을 지켜 작성하는게 좋다

  • 헤더 가드를 항상 포함

  • 소스 파일과 연결된 헤더 파일의 이름을 동일하게 지정합니다

  • 각 헤더 파일에는 특정 작업이 있어야 하며 가능한 한 독립적이어야 한다 예를 들어, A 기능과 관련된 모든 선언을 A.h에 넣고 B 기능과 관련된 모든 선언을 B.h에 넣어야 한다 이렇게 하면 나중에 A만 신경 쓰면 Ah만 포함하고 B와 관련된 내용은 하나도 가져오지 않아도 된다

  • 의도치 않은 Transitive Include를 피하기 위해, cpp에서 사용하는 기능에 대해 명시적으로 포함해야 하는 헤더를 주의 깊게 살펴야 한다

  • 헤더 파일은 필요한 기능을 포함하는 다른 헤더를 #include해야 한다, 이러한 헤더는 .cpp 파일에 단독으로 #include될 때 성공적으로 컴파일되어야 한다

  • 필요한 것만 include해야 한다

  • cpp 파일을 #include하지 말 것

헤더에 어떤 것이 무엇을 하는지 또는 어떻게 사용하는지에 대한 주석을 다는것도 좋다


2. 헤더 가드

헤더 가드

동일 범위 내에서 변수 혹은 함수 식별자는 하나의 정의만 가질 수 있다 (ODR), 따라서 식별자를 두 번 이상 동일한 정의하는 프로그램은 컴파일 에러를 발생시킨다

헤더파일을 include 시킬 때 위와 같은 문제가 발생할 수 있다

예를들어 정리해보자

	//foo.h
    
    int foo()
    {
    	return 10;
    }
    
    //test.h
    #include "foo.h"
    
    
    //main.cpp
    #include "foo.h"
    #include "test.h"
    
    int main()
    {
    	return 0;
    }

위의 코드는 보기에 문제가 없어보이지만 컴파일 되지 않는다, 왜냐하면 동일한 헤더가 include되기 때문이다
(foo.h의 중복 include)

이러한 중복 헤더 include 문제를 헤더 가드를 통해 해결할 수 있다

헤더 가드는 조건부 컴파일 지시문을 이용한다

	#ifndef TEST
    #define TEST
    
    //헤더파일 작성
    
    #endif
    
    #ifndef FOO_H
    #define FOO_H
    
    int foo()
    {
    	return 10;
    }
    
    #endif

TEST가 처음에는 define되지 않았기 때문에 ifndef로 들어가 헤더가 include되지만 한번 include되면 ifndef로 들어가지 않기 때문에 중복 헤더 include가 방지되는 방식이다

실제로 C++ standard library에서 위와 같은 헤더 가드를 사용하는걸 확인할 수 있다

하지만 위 방식보다 더 간단한 방법의 헤더가드도 지원한다

	//헤더
    #pragma once

#pragma once 한줄로 위의 조건부 컴파일 지시문 헤더가드와 같은 효과를 볼 수 있다

거의 그럴일 없지만 #pragma once가 실패하는 경우는 특정 헤더파일의 사본이 다른 directory에 존재하고 이 두개의 경로의 헤더파일을 같이 include하는 경우에는 서로 다른 파일로 간주하여 #pragma once가 동작하지 않게 된다

	#include "src/include/header1.h"
    #include "lib/include/header1.h"

하지만 조건부 컴파일 지시문 헤더가드는 위와 같은 상황도 방어한다

추가로 #pragma once는 컴파일러에 따라 구현되지 않을 수 있다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글