[C++] 식별자를 알려주는 전방 선언( forward declaration )

반응형

전방 선언( forward declaration )

식별자( identifier )란 변수, 함수, 타입 등의 이름을 말합니다.

int a = 0;		// a: 변수 식별자
int add( int, int );	// add: 함수 식별자
class CSomething;	// CSomething: 타입 식별자

 

이러한 식별자를 사용하려면, 컴파일러는 이 식별자들을 미리 알고 있어야 합니다.

그래서, 다음과 같은 코드는 오류를 만들게 됩니다.

#include <iostream>
using namespace std;

int main(){

    int a = 3;  // a: 식별자
    int b = 5;
    
    int sum = add(a, b);    // add: 식별자
    cout << sum << '\n';
}

int add( int a, int b){
    return a + b;
}

▼출력

error: 'add' was not declared in this scope

위와 같은 오류가 발생하는 이유는 컴파일러가 코드들을 순차적으로 컴파일하기 때문입니다.

따라서, 위의 코드는 main 함수에서 add 함수를 호출할 때까지 컴파일러는 add의 존재를 알지 못하고, 그래서 오류를 발생하는 것입니다.

 

그러므로, 이 오류를 해결하려면, 다음과 같이 컴파일러에게 식별자 add가 어떤 것인지 알려주어야 합니다.

int add( int a, int b){	// main 함수보다 먼저 선언
    return a + b;
}

int main(){

    int a = 3;  // a: 식별자
    int b = 5;
    
    int sum = add(a, b);    // add: 식별자
    cout << sum << '\n';
}

 

하지만, 위와 같은 방법이 항상 유용한 것은 아닙니다.

현실에서는 일반적으로 코드들이 위처럼 같은 파일에 들어있지 않고, 모듈을 기준으로 여러 파일에 나눠지게 됩니다.

따라서, 위와 같이 단순히 코드의 순서를 변경할 수 없습니다.

게다가, 그렇기 때문에, 여러 파일 중에서 먼저 컴파일을 시작해야 하는 파일의 순서를 정하는 엄청난 문제를 해결해야 됩니다.

 

이러한 문제를 해결할 수 있는 방법이 전방 선언( forward declaration )입니다.

이 방법은 식별자가 어딘지는 정확하게 모르지만, 분명히 존재한다는 것을 컴파일러에 알리는 것입니다.

그러면, 컴파일러는 이러한 정보를 가지고 컴파일을 수행합니다.

 

함수 같은 경우, 사실상 컴파일에 중요한 것은 함수 구현의 내용이 아니라, 함수의 이름, 반환 값의 타입, 그리고 매개변수의 타입입니다.

그래서, 함수 전체의 코드를 함수를 호출하는 코드의 앞에 위치시키는 대신에, 다음과 같은 함수 선언만을 함수를 호출하는 코드 앞에 위치시키는 방법을 사용하는 것입니다.

int add( int a, int b); // 전방 선언

int main(){

    int a = 3;      
    int b = 5;
    
    int sum = add(a, b);    // 함수 호출
    cout << sum << '\n';
}

int add( int a, int b){ // 함수 정의
    return a + b;
}

위의 전방 선언엔 함수의 구현 내용이 필요 없으므로, 매개변수의 식별자들이 사용되지 않습니다.

그러므로, 함수의 전방 선언은 아래와 같이 매개변수의 식별자를 생략할 수도 있습니다.

int add( int, int );	// 매개변수의 식별자들을 생략

그렇지만, 일반적으로 매개변수의 이름은 사용되는 용도를 알려주는 중요한 역할을 합니다.

그리고, 위와 같은 전방 선언은 보통 개별적인 파일( 예를 들어 xxx.h )에 따로 모아, #include 지시자를 사용하여 그 파일을 전방 선언이 필요한 코드 앞에 포함시키는 방법으로 사용하게 됩니다.

그리고, 헤더 파일이라고 불리는 이러한 파일들을 사용하여 도움말 문서를 만들어 주는 프로그램을 사용할 수도 있기 때문에 매개변수의 식별자를 유지하는 것이 유리합니다. ( 심지어, 이 경우엔 함수의 정의로부터 복사와 붙여 넣기만 하면 됩니다. )

 

그리고, 전방 선언을 통해서 컴파일러에 알려줄 수 있는 식별자는 함수뿐이 아닙니다.

다음은 변수를 전방 선언을 통해서 사용하는 것을 보여줍니다.

int x;  // 변수의 전방 선언

int main(){

    x = 5;
    cout << x << '\n';
}

 

그리고, 클래스와 같은 사용자 타입도 전방 선언을 통해서 컴파일러에게 식별자를 알려줄 수 있습니다.

#include <vector>

class CParent;  // 사용자 타입을 알려주는 전방 선언

class CChild{
    CParent* m_pParent;
public:
    CChild( CParent* pParent) : m_pParent(pParent){}
};

class CParent{
    std::vector<CChild> m_children;
public:
    void addChild(){
        CChild child(this);
        m_children.push_back(child);
    }
};

위에서 CChild 클래스를 정의하려면 먼저 CParent의 존재를 알고 있어야 합니다.

그런데, CParentCChild 식별자를 알고 있어야 하므로, 이런 경우는 클래스의 순서를 변경한다고 하더라도 상호 식별자를 참조하는 문제를 해결할 수 없습니다.

이럴 때는 사용할 수 있는 방법이 전방 선언뿐입니다.

이 전방 선언은 클래스의 정의를 모두 컴파일러에게 보여줄 필요 없이( 그리고, 그렇게 할 수도 없습니다. ), 클래스 이름만을 알려주는 것으로 식별자를 컴파일러에 알려주어야 하는 목적을 간단히 달성할 수 있습니다.

 

선언( declaration )과 정의( definition )

위에서 말한 것과 같이, 선언( declaration )은 컴파일러에게 식별자와 식별자를 사용하기 위한 타입 정보를 알려줍니다.

다음과 같은 것들이 선언의 예입니다.

int add( int, int );	// 함수의 이름과 함수과 관련된 타입 정보
int x;			// 변수의 이름과 변수의 타입 정보
class CChild;		// 클래스의 이름과 타입 정보

 

그리고, 정의( definition )는 실제 구현된 내용을 가진 선언을 말합니다.

여기서 주목할 것은 정의도 선언이며, 함수의 전방 선언과 같이 정의가 아닌 선언을 순수 선언( pure declaration )이라고 합니다.

int add( int a, int b){ // 함수 정의
    return a + b;
}

int x;	// 변수의 정의

class CChild{	// 타입의 정의
    CParent* m_pParent;
public:
    CChild( CParent* pParent) : m_pParent(pParent){}
};

그리고, 위의 변수 x와 같이, 변수의 선언과 정의의 형태는 같습니다.

즉, 변수의 선언은 선언이자 정의가 됩니다.

 

참고로, 함수의 전방 선언을 통해 컴파일러에게 함수 식별자를 알려준 후, 함수의 정의를 구현하지 않으면 어떻게 될까요?

이런 경우, 컴파일에 필요한 모든 정보는 전방 선언으로부터 얻을 수 있으므로, 컴파일 과정은 무사히 마칠 수 있지만, 함수의 실제 코드를 식별자에 연결하는 링커(linker)는 오류를 만들 것입니다.

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유