본문 바로가기

CS연구소👨‍💻

[C++] Code Splitting, Forward Declaration : #ifndef, #endif 가 뭘까?

 

들어가며

C++을 처음시작했거나 공부하는 사람들은 으레 알고리즘부터 시작하게 된다. 사실 알고리즘 만큼 프로그래밍 언어 공부하기 좋은 것도 없긴하다 하지만 이제 배운 언어로 응용프로그램을 만들고 싶다거나하면 이런 코드를 읽게된다.

#ifndef _H264_VIDEO_STREAM_DISCRETE_FRAMER_HH
#define _H264_VIDEO_STREAM_DISCRETE_FRAMER_HH

#ifndef _H264_OR_5_VIDEO_STREAM_DISCRETE_FRAMER_HH
#include "H264or5VideoStreamDiscreteFramer.hh"
#endif

class H264VideoStreamDiscreteFramer: public H264or5VideoStreamDiscreteFramer {
public:
  static H264VideoStreamDiscreteFramer*
  createNew(UsageEnvironment& env, FramedSource* inputSource,
        Boolean includeStartCodeInOutput = False, Boolean insertAccessUnitDelimiters = False);

protected:
  H264VideoStreamDiscreteFramer(UsageEnvironment& env, FramedSource* inputSource,
                Boolean includeStartCodeInOutput, Boolean insertAccessUnitDelimiters);
      // called only by createNew()
  virtual ~H264VideoStreamDiscreteFramer();

private:
  // redefined virtual functions:
  virtual Boolean isH264VideoStreamFramer() const;
};

#endif

이런 내용들은 알고리즘 푸는 정도로는 절대 사용할 일이 없지만 프로젝트 단위의 코드에서는 반드시 등장한다. 결국 모든 개발자의 목표는 프로젝트이니 만큼 반드시 알아야하는 내용이다.

처음 이 코드들을 읽게되면 내가 너무 초보자고 앞에 큰 벽이 있는 것처럼 느껴진다. 하지만 막상 공부해보면 이런 코드들 자체는 어려운 부분이 아니란걸 쉽게 알 수 있다.

Code Splitting

 

Code Splitting이 무엇인가

C++에는 .cpp 외에도 .h, .hpp 등의 확장자가 존재한다 이들은 헤더파일이란 코드이며 소스파일의 구조를 담당하는 역할을 한다.

코드 Splitting의 이유

Roadmap.sh : 코드의 구조화, 유지보수성, 가독성을 올려준다.

왜냐?  #include <iostream> 등도 code splitting 이기 때문이다.

cpp같은 소스파일은 컴파일을 통해 오브젝트 파일이 된다. 그리고 오브젝트 파일이 되면서 cpp파일은 include 선언한 파일들을 읽어오고 파일 앞에 붙여넣기 시작한다. 그렇다면 만약 include가 없다면 매 소스 파일을 만들때마다 include 해야할 파일들을 코드 앞에 붙여놔야한다. 그러니 code splitting은 코드의 구조화, 유지 보수성을 높여주는 것이다.

헤더 파일의 역할

#include <iostream>을 사용하면 'iostream'이라는 헤더 파일의 모든 내용을 복사해오도록 요청한다. 이렇게 하면 헤더 파일의 내용을 코드 파일에서 사용할 수 있다.

출처: https://boycoding.tistory.com/144 [소년코딩:티스토리]

Roadmap.sh : 여러 소스 파일들에서 클래스, 함수, 변수를 선언한다. 그리고 헤더파일은 코드의 다른 부분들을 이어주는 인터페이스적인 역할을 한다.
한마디로 의존성을 관리하기 쉽게 해주고 중복되는 코드를 줄여준다.

// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H

class Example {
public:
    void printMessage();
};

#endif

보면 바로 모르는게 나온다. #ifndef가 뭐냐 #endif는 뭐고, #define은 또 뭘까?

이걸 모르면 가독성을 높여준다는 헤더파일 이해 자체가 불가능하다.

ifndef는 중복 의존성 방지를 위해 사용한다

C++에서 의존성을 사용하는 것부터 얘기해야겠다. C++의 의존성은 오브젝트 파일이 되는 소스파일, 헤더파일등에서 #include를 하면서 일어난다. include의 뜻은 오브젝트 파일을 만들때 컴파일러가 그 안의 파일들을 복사해 오는 것을 의미한다.

그런데 이 #include가 꼬이고 하다보면 여러번 include를 하게되는 문제가 발생할 수 있음

전처리기는 구조체에 point정의를 두 번 받게되니 에거가 생길 수밖에 없다.

endif는 생각보다 엄청 간단하다.

ifndef는 if not define 의 약어일뿐

나머지도 그냥 if문처럼

#endif 가 나올때까지 기다리기만하면된다

IFNDEF를 사용하는 것은 자기를 위해서가 아니라 다른 파일을 위해서다

이 헤더파일에서 기본적으로 ifndef를 하는것은 다른파일에서 얘를 include할때 여러번 하는것에서 일어나는일이다.

/**********
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)

This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
more details.

You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
**********/
// "liveMedia"
// Copyright (c) 1996-2023 Live Networks, Inc.  All rights reserved.
// A source object for AMR audio sources
// C++ header

#ifndef _AMR_AUDIO_SOURCE_HH
#define _AMR_AUDIO_SOURCE_HH

#ifndef _FRAMED_SOURCE_HH
#include "FramedSource.hh"
#endif

class AMRAudioSource: public FramedSource {
public:
  Boolean isWideband() const { return fIsWideband; }
  unsigned numChannels() const { return fNumChannels; }

  u_int8_t lastFrameHeader() const { return fLastFrameHeader; }
  // The frame header for the most recently read frame (RFC 4867, sec. 5.3)

protected:
  AMRAudioSource(UsageEnvironment& env, Boolean isWideband, unsigned numChannels);
    // virtual base class
  virtual ~AMRAudioSource();

private:
  // redefined virtual functions:
  virtual char const* MIMEtype() const;
  virtual Boolean isAMRAudioSource() const;

protected:
  Boolean fIsWideband;
  unsigned fNumChannels;
  u_int8_t fLastFrameHeader;
};

#endif

예를 들어 이코드

#define _AMR_AUDIO_SOURCE_HH

얘가 이제 뭘까 싶은데 아마 이제 endif까지 코드를 _AMR_AUDIO_SOURCE_HH 이라고 정의한다 라는 것같음.

헤더 파일도 import , include를 할 수 있다.

근데 이제 include를 두개가 존재한다.

“” 소스 파일이 있는 디렉터리에서 헤더 파일을 include 시키도록 전처리기에게 지시

<> : 컴파일러와 함께 제공되는 헤더 파일을 include할 때 사용

즉 내꺼면 “” , 다른 라이브러리면 <>인듯?

그럼 이제 헤더파일은 클래스를 구현하는 것이라고 하는데. 소스파일은 헤더파일에서 정의된 실제 함수를 구현하는 일을 한다고 적혀있다.

컴파일해서 오브젝트파일로 만들어지는 파일은 헤더파일이 아니라 소스파일이다. 거기에 자세한 함수가 적혀있기 떄문이다.

헤더파일은 말 그대로 구조를 짜는 역할을한다.

근데 그럼 헤더파일은 소스파일과 1 : N 매칭이 될 수 있다 어떤 파일을 오브젝트 파일로 만드냐에 따라서 같은 헤더파일의 메소드가 다른 함수가 될 수 있다.

근데 이렇게 따지면 한 define이 헤더파일에 적혀있을때.

#ifndef "__A__"
#define "__A__"

class A {
public:
    void sayHello();
}

#endif

두개의 소스파일에서 해당 헤더파일을 구체화했다고 치면

#include "A.hh"
#include <iostream>

void A::sayHello() {
    std::cout << "Hello, code splitting!" << std::endl;
}
#include "example.h"
#include <iostream>

void A::sayHello() {
    std::cout << "I'm not hello!" << std::endl;
}

이상태에서 이제 다른 코드가 A를 include한다고 해볼떄

#ifndef "__A__"
#include "A.hh"
#endif

class B {
    public:
    void saySomeThing() {
        A::sayHello();
    }


}

이런 함수가 나온다면 2개의 소스파일을 가진 얘는 어떻게 반응하게됨?

바로 컴파일 오류가 난다.

 

 

FORWARD DECLARATION

roadmap.sh : a way of declaring a symbol (class, function, or variable) before defining it in the code.

It helps the compiler understand the type, size, and existence of the symbol

아마 헤더같은 곳에서 #ifndef 나, #define 하기전에 미리 클래스를 한번 말해주는 것 같다

왜 미리 선언하냐

미리 선언해놓는 것 만으로도 다른 곳에서 선언판 클래스, 변수를 참조하고 포인터로서 쓸 수 있음

This declaration is particularly useful when we have cyclic dependencies or to reduce compilation time by avoiding unnecessary header inclusions in the source file.

cyclic Dependencies에서 특히 유용하다고 한다. 소스파일에서 쓸데없이 헤더를 inclusion하는 과정을 줄임으로써 컴파일 타임을 줄인다고하는듯. 근데 왜

// A.h
#ifndef A_H
#define A_H

class B;  // Forward declaration of class B

class A {
    B* b;  // Use of class B pointer
};

#endif  // A_H

순환 참조할때는 얘가 쓸모가 있다. 말그대로 컴파일러가 include를 만나면 실제로 코드를 복사 붙여넣기 하면서 컴파일을 한다고 할 수 있는데. 원래는 B.h에서 A를 include하는 코드를 읽을때 쯤 에러가 발생할텐데 미리 Declaration을 해버린다면 그 만큼 안읽어도 되는 코드들을 줄일 수 있다.