External Linkage vs Internal Linkage

Junhyeok Yun·2022년 10월 18일
0

C++

목록 보기
5/5
post-thumbnail

오늘의 목표

Linkage(연결)란 무엇인지, 그리고 그 종류인 External Linkage(외부 연결)과 Internal Linkage(내부 연결)에 대해서 알아보자

먼저 linkage와 Translation Unit에 대해 알아보기 전에 이것들을 이해하는 데 필요한 개념인 scope와 정의, 선언의 차이에 대해 알아보자.


Scope

The context in which a name is visible is called its scope.

예를 들어 클래스나 함수 또는 변수를 선언했을 때, 그것들의 식별자는 프로그램의 특정 영역에서만 "보이고" 사용될 수 있다. 그 특정 영역을 스코프라 한다. 다음은 6가지 스코프의 종류에 대한 설명이다.

  • Global scope : 전역으로 선언된 식별자(a global name)는 모든 클래스, 함수, namespace에 속하지 않으며 그 밖에 선언된 식별자를 말한다. (사실 C++에서 이 전역 식별자들은 암시적인 전역 namespace(implicit global namespace)에 존재한다)
    이러한 전역 식별자들의 스코프를 global scope라 하며 선언된 시점에서부터 식별자가 선언된 파일의 끝까지를 말한다.
    또한 전역 식별자들은 프로그램의 다른 파일에서 접근할 수 있는지를 결정하는 -아래에서 설명할- 연결 규칙(the rules of linkage)에 의해서 접근성이 결정된다.

  • Namespace scope : 모든 클래스나 enum, 함수에 속하지 않으며 namespace 안에 선언된 식별자는 선언된 시점에서부터 namespace 끝까지만 인식된다.
    이 때, 여러 파일에서 같은 이름의 namespace를 지정하면 모두 같은 namespace로 인식된다. 즉, 한 namespace가 여러 파일에 걸쳐서 작성될 수 있다는 것

  • Local scope : 함수나 람다식 안에 선언된 식별자들은 (매개변수 포함) local scope를 가진다. 이 식별자들은 선언된 시점에서부터 함수나 람다식의 끝까지만 인식된다.
    Local scope는 블록 스코프(block scope, 식별자 선언을 { }로 둘러 싸는 것을 말한다)의 일종이다.

  • Class scope : 클래스의 멤버들은 class scope를 가지는데, 선언된 시점과 상관 없이 클래스의 정의 부분 전체를 통틀어서 인식 가능하다. 그래서 다음과 같이 미리 선언해도 컴파일 오류가 발생하지 않는다.

class Shape {
public:
    void paint() {
        draw(); // 컴파일 에러 발생하지 않음
    }
    virtual void draw() {
        cout << "Shape::draw() called" << endl;
    }
};
  • Statement scope : for, if, while, switch문에 선언된 식별자들은 각각의 block이 끝나기 전까지만 인식 가능하다.

  • Function scope : Function scope는 식별자가 선언된 함수 전체에서 인식 가능한 것을 말한다. 선언되기 전에도 그 함수 안에서라면 인식이 가능하다.
    대표적인 예로 label이 있으며 이런 특성 때문에 goto문과 label을 사용할 수 있는 것이다.

💡Tip - goto cleanup
cleanup: label과 goto문을 이용하여 에러 감지 후 에러를 리턴하기 전 동적으로 할당 받은 메모리 반환 같은 작업을 하기도 한다.

- 예시

if (error) goto cleanup;

...

cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;

따라서, 스코프가 다르면 같은 이름으로 식별자를 선언할 수 있으며 static 변수가 아니라면 scope가 인식 범위 뿐 아니라 프로그램 메모리에서 언제 생성되고 언제 제거되는지까지 결정하게 된다.

스코프의 이러한 특성을 이용해서 변수 이름을 숨길 수 있다. (클래스 이름도 같은 스코프 안에 같은 이름의 함수나 객체, 변수, enum을 선언해서 숨길 수 있으나 안좋은 방식이므로 사용하지 않는 것이 좋다.)

바로 블록 스코프를 사용하는 것인데, 아래 그림처럼 i 를 안쪽 블록에 재선언 함으로써 바깥 블록의 i 를 안쪽 블럭이 인식하지 못하게, 즉 숨길 수 있다.

위의 출력은 다음과 같다.

i = 0
i = 7
j = 9
i = 0

또한 블록 스코프 안에 전역 변수와 같은 이름의 변수를 선언함으로써 전역 변수를 숨길 수 있다. 다만, 이 때에도 범위 지정 연산자(::)를 사용하면 전역 변수에 접근할 수 있다.

#include <iostream>

int i = 7;   // i has global scope, outside all blocks
using namespace std;

int main() {
   int i = 5;   // i has block scope, hides i at global scope
   cout << "Block-scoped i has the value: " << i << "\n";
   cout << "Global-scoped i has the value: " << ::i << "\n";
}
Output :
Block-scoped i has the value: 5
Global-scoped i has the value: 7

Declarations and Definitions

선언(Declaration)

선언은 독립체(함수, 변수 ...)의 고유한 이름과 타입 정보, 그리고 다른 특징들을 명시하는 것이다.

  • C++에서의 선언은 Javascript와 같은 언어와 다르게 무조건 타입 정보를 제공해야 한다.

  • 또한 선언된 시점이 컴파일러가 인식할 수 있게되는 시점이므로 나중에 선언된 함수나 클래스를 참조할 수 없다. (하지만 C# 같은 언어는 같은 소스 파일 내에서라면 선언되기 전에도 사용할 수 있다.)

  • 선언된 식별자는 그것의 scope 내에서만 유효하다.

정의(Definition)

정의는 독립체가 프로그램에서 사용될 때, 기계 코드를 생성하는 데에 필요한 모든 정보를 컴파일러에게 제공하는 것이다.

  • Built-in type(int, char...)의 변수들은 선언됨과 동시에 자동으로 정의된다. 그 이유는 컴파일러가 이러한 변수들에 대해서 얼마만큼의 공간을 할당해야 하는지 이미 알고 있기 때문이다.

  • 상수 변수는 선언됨과 동시에 정의되어야 한다. 즉, 선언될 때 값이 할당되어야 한다.

  • 함수의 정의는 함수의 내용을 작성하는 것이고, 클래스의 정의는 클래스의 멤버를 작성하는 것이다.


Translation Unit and Linkage

Translation unit(Compilation unit)은 하나의 실행 파일(.cpp or .cxx)과 그것에 직간접적으로 포함된 모든 헤더 파일(.h or .hpp)로 구성되어 있다.

Translation unit은 컴파일 과정을 거쳐 object file이 되며, 링커가 하나 또는 여러 개의 object file과 라이브러리를 합쳐서 하나의 프로그램으로 만드는 것이다.

C++ 프로그램에는 One Definition Rule(ODR) 이 존재하는데 함수나 변수를 식별할 때 사용하는 이름인 심볼(Symbol)이 그것의 스코프 내에서라면 몇번이든 선언될 수 있지만 정의는 오직 한 번만 되는 것을 말한다.

일반적으로 링킹 과정에서 ODR이 지켜지지 않는 경우가 많다. 그 이유는, 같은 소스 파일이라면 재정의는 컴파일 에러를 발생시키므로 눈에 띄기 쉽지만, 여러 Translation unit에서 같은 식별자가 정의되어 있다면 컴파일 단계에서 알기 쉽지 않고 링커가 에러를 발생시키기 때문이다.

일반적으로, ODR을 어기지 않고 여러 파일에서 하나의 symbol을 접근할 수 있게 하는 가장 좋은 방법은 헤더 파일에 선언한 후, symbol이 필요한 모든 implement file에 #include 지시자를 추가하는 것이다. 이 방법은 각각의 translation unit에 헤더파일에 선언된 symbol이 단 한번씩만 선언되도록 보장한다. 단, 이 symbol에 대한 정의는 하나의 implementation file에서만 정의되어야 한다.

그러나 전역 변수나 클래스를 .cpp 파일에서 선언해야 하는 경우도 존재한다. 이 때 이 전역 symbol이 선언된 파일에서만 인식되게 할지, 아니면 모든 파일에서 인식되게 할지 컴파일러와 링커에게 말해줄 방법이 필요하다.

이 때 사용되는 것이 linkage이며, linkage의 타입은 symbol의 노출 범위(하나의 파일 or 모든 파일)를 명시한다. 이 linkage 개념은 오직 전역 심볼에만 적용되며 스코프 안에 선언된 symbol에는 적용되지 않는다.


External vs. Internal linkage

External linkage : 프로그램의 어떤 translation unit에서도 접근 가능
Internal linkage(or no linkage) : 선언된 translation unit에서만 접근 가능

따라서 external linkage인 symbol들은 프로그램 내의 다른 translation unit에 같은 이름이 존재할 수 없고 internal linkage인 symbol들은 존재할 수 있다.

Default로 external linkage를 가지는 symol :

  1. 상수가 아닌 전역 변수
  2. free function (global or namespace 스코프에 정의된 함수)

global name을 static 으로 선언하면 사용범위가 선언된 파일로 한정되어 다른 파일에서는 접근이 불가능하다. 즉, internal linkage로 바뀐다.

Default로 internal linkage를 가지는 symol :

  1. const 객체
  2. constexpr 객체
  3. typedef 객체
  4. namespace 스코프 안에 존재하는 static 객체

const 객체에 external linkage를 부여하기 위해서는 extern으로 선언하고 값을 할당하면 된다.

extern const int value = 42;

Reference

Scope (C++) | Microsoft Learn
Translation units and linkage (C++) | Microsoft Learn
Is GOTO considered harmless when jumping to cleanup at the end of function? | Stackoverflow

profile
개발 공부 일지

0개의 댓글