함수의 범위와 기간 (namespace 와 linkage)

미리 말하지만 코드 예시때문에 좀 길어보일 뿐 별로 많지 않다. 부담갖지 말고 다시 볼 때 슥슥 넘겨보자..

 

 

namespace 소개

두 개의 동일한 식별자가 동일한 범위에 도입되면 이름 지정 충돌이 발생하며 컴파일러는 어느 식별자를 사용할지 명확하게 구분할 수 없다. 

이 때 컴파일러나 링커는 모호성을 해결하기에 충분한 정보가 없기 때문에 오류를 생성한다.

 

 

이러한 범위 문제를 해결하고자 C++에서는 namespace 키워드를 통해 자체 네임스페이스를 정의할 수 있다. 

자신의 프로그램에서 생성한 네임스페이스는 일반적으로 사용자 정의 네임스페이스 라고 한다.

네임스페이스 네임스페이스식별자
{
    // 여기에 네임스페이스 내용이 있음
}

 

네임스페이스는 전역 범위나 다른 네임스페이스 내부에서 정의된다. 

 

 

하지만 서로 다른 네임스페이스 안에서 같은 이름의 함수를 선언할 경우, 이번엔 컴파일러는 만족했지만 링커는 전역 네임스페이스에서 정의를 찾을 수 없다.

 

이와 같은 경우, 컴파일러에게 특정 네임스페이스에서 식별자를 찾도록 지시할 수 있는 방법이 있다. 

범위 확인 연산자 (::)를 사용하는 것이다. 

범위 확인 연산자는 오른쪽 피연산자가 지정한 식별자를 왼쪽 피연산자의 범위에서 찾아야 함을 컴파일러에 알린다.

namespace Foo // define a namespace named Foo
{
    // This doSomething() belongs to namespace Foo
    int doSomething(int x, int y)
    {
        return x + y;
    }
}

namespace Goo // define a namespace named Goo
{
    // This doSomething() belongs to namespace Goo
    int doSomething(int x, int y)
    {
        return x - y;
    }
}

int main()
{
    std::cout << Foo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Foo
    return 0;
}

 

범위 확인 연산자는 네임스페이스에 이름을 제공하지 않고 식별자 앞에서 사용할 수도 있다. 이 경우 식별자는 전역 네임스페이스에서 검색된다.

 

 

 

네임스페이스 내에서 식별자 확인하기

void print() // this print() lives in the global namespace
{
	std::cout << " there\n";
}

namespace Foo
{
	void print() // this print() lives in the Foo namespace
	{
		std::cout << "Hello";
	}

	void printHelloThere()
	{
		print();   // calls print() in Foo namespace
		::print(); // calls print() in global namespace
	}
}

int main()
{
	Foo::printHelloThere();

	return 0;
} 

네임스페이스 내의 식별자가 사용되고 범위 확인이 제공되지 않으면 컴파일러는 먼저 동일한 네임스페이스에서 일치하는 선언을 찾으려고 시도한다. 

 

일치하는 식별자가 없으면 컴파일러는 #include된 각 네임스페이스를 순서대로 확인하여 일치하는 항목이 있는지 확인하고 전역 네임스페이스를 마지막으로 확인한다.

 

 

또한 네임스페이스는 다른 네임스페이스 내에 중첩될 수 있다.

namespace Foo
{
    namespace Goo // Goo is a namespace inside the Foo namespace
    {
        int add(int x, int y)
        {
            return x + y;
        }
    }
}

int main()
{
    std::cout << Foo::Goo::add(1, 2) << '\n';
    return 0;
}

 

 

중첩은 아래와 같이 표현할 수도 있다. 무엇을 선택할지는 그냥 스타일에 따라 다르다.

namespace Foo::Goo // Goo is a namespace inside the Foo namespace (C++17 style)
{
    int add(int x, int y)
    {
        return x + y;
    }
}

namespace Foo
{
     void someFcn() {}; // This function is in Foo only
}

int main()
{
    std::cout << Foo::Goo::add(1, 2) << '\n';
    return 0;
}

 

이렇게 계속 중첩된 네임스페이스 내에서 변수나 함수의 정규화된 이름을 입력하는 것은 어려울 수 있기 때문에 C++에서는 별칭을 생성할 수 있다.

namespace Foo::Goo
{
    int add(int x, int y)
    {
        return x + y;
    }
}

int main()
{
    namespace Active = Foo::Goo; // active now refers to Foo::Goo

    std::cout << Active::add(1, 2) << '\n'; // This is really Foo::Goo::add()

    return 0;
} // The Active alias ends here

 

 

네임스페이스는 정보 계층 구조를 구현하기 위해 설계된 것이 아니라 이름 충돌을 방지하기 위한 메커니즘으로 설계됐다. (표준 라이브러리 전체가 단일 최상위 네임스페이스std 아래에 있음)

주로 네임스페이스 내에서 이름 충돌을 피하기 위해 중첩된 네임스페이스를 사용한다.

 

 

 


 

 

 

전역변수

 

일반적으로 전역변수는 전역 global 네임스페이스에서 선언한다.

그런데 사용자 정의 네임스페이스 내에서 전역변수를 정의할 수도 있다.

#include <iostream>

namespace Foo // Foo is defined in the global scope
{
    int g_x {}; // g_x is now inside the Foo namespace, but is still a global variable
}

 

이제 비록 식별자 g_x는 namespace Foo의 범위로 제한되지만, 그 이름은 여전히 전역에서 엑세스할 수 있다. 

(Foo::g_x ) 이거때문에 여전히 전역변수라고 말한것.

즉, 네임스페이스 내부에 선언된 변수도 전역변수이다.

이러한 전역변수는 main이 실행되기 전 프로그램이 시작될 때 생성되고, 프로그램이 끝나면 소멸한다.

그리고 기본적으로 초기화되지 않는 지역 변수와 달리, 정적 기간을 갖는 변수는 기본적으로 0으로 초기화된다. 하지만 상수 전역 변수(const, constexpr)인 경우 당연히 초기화해야 한다.

 

 

 

변수 섀도잉 (이름 숨기기)

 

각 블록은 자체 범위 영역을 정의한다. 그렇다면 외부 블록의 변수와 이름이 같은 변수가 중첩 블록 내부에 있으면 어떻게 되는걸까? 이런 일이 발생하면 중첩된 변수는 둘 다 범위 내에 있는 영역에서 외부 변수를 "숨긴다". 이것을 shadowing이라 한다.

int main()
{ // outer block
    int apples { 5 }; // here's the outer block apples

    { // nested block
        // apples refers to outer block apples here
        std::cout << apples << '\n'; // print value of outer block apples

        int apples{ 0 }; // define apples in the scope of the nested block

        // apples now refers to the nested block apples
        // the outer block apples is temporarily hidden

        apples = 10; // this assigns value 10 to nested block apples, not outer block apples

        std::cout << apples << '\n'; // print value of nested block apples
    } // nested block apples destroyed


    std::cout << apples << '\n'; // prints value of outer block apples

    return 0;
} // outer block apples destroyed

이렇게 블럭 내부에서 한번 전역변수와 이름이 같은 지역변수를 선언해버리면, 이와 같은 경우는 더이상 전역변수에 접근할 수 있는 방법이 사라진다.(일단은 이렇게 알고있자) 그럼 어떤 결과가 나올까?

위의 경우 5, 10, 5가 출력된다. 내부 블럭 안에서 apple을 호출할 때는 전역변수를 사용했고, 그 후 블럭 안에서 따로 같은 이름을 정의했기 때문에 블럭 내의 지역변수로 작용하여 블럭이 끝나면 전역변수에 영향을 끼치지 못하고 파괴됐다.

 

 

 

하지만 이걸 봐라

int main()
{ // outer block
    int apples{5}; // here's the outer block apples

    { // nested block
        // apples refers to outer block apples here
        std::cout << apples << '\n'; // print value of outer block apples

        // no inner block apples defined in this example

        apples = 10; // this applies to outer block apples

        std::cout << apples << '\n'; // print value of outer block apples
    } // outer block apples retains its value even after we leave the nested block

    std::cout << apples << '\n'; // prints value of outer block apples

    return 0;
} // outer block apples destroyed

5, 10, 10을 출력한다. 왜일까. 처음에도 전역변수를 사용했고, 내부 블럭 내에서 지역변수의 값 변경이 아닌 전역변수의 값을 변경했다. 변수의 추가적인 선언이 없었기 때문에 이런 결과가 나왔다.

 

 

전역변수 섀도잉

아까의 예시와 반대로, 중첩된 블록의 변수가 외부 블록의 변수를 숨길 수 있는 것과 유사하게, 전역 변수와 이름이 같은 지역 변수는 지역 변수가 범위 내에 있는 모든 전역 변수를 가리게 된다.

int value { 5 }; // global variable

void foo()
{
    std::cout << "global variable value: " << value << '\n'; // value is not shadowed here, so this refers to the global value
}

int main()
{
    int value { 7 }; // hides the global variable value until the end of this block

    ++value; // increments local value, not global value

    std::cout << "local variable value: " << value << '\n';

    foo();

    return 0;
} // local value is destroyed
지역 변수 값: 8
전역 변수 값: 5

 

main 안의 블럭에서 선언된 변수도 당연히 지역변수이므로, 여기서 전역변수를 호출할 수 있는 방법은 foo()를 호출하는 것 이 있다.

 

foo() 호출과 비슷한 개념으로

전역 변수는 전역 네임스페이스의 일부이기 때문에 접두사 없이 범위 연산자(::)를 사용하여 컴파일러에게 지역 변수 대신 전역 변수를 의미한다고 알릴 수 있다.

int value { 5 }; // global variable

int main()
{
    int value { 7 }; // hides the global variable value
    ++value; // increments local value, not global value

    --(::value); // decrements global value, not local value (parenthesis added for readability)

    std::cout << "local variable value: " << value << '\n';
    std::cout << "global variable value: " << ::value << '\n';

    return 0;
} // local value is destroyed

 

지역 변수 값: 8
전역 변수 값: 4

 

솔직히 ::value로 호출할 줄은 생각못했다. 역시 이렇게 또 배워가네..

 

지역 변수의 섀도잉은 잘못된 변수가 사용되거나 수정되는 경우 의도하지 않은 오류가 발생할 수 있으므로 일반적으로 피해야 한다.

지역 변수 섀도잉을 피하는 것과 같은 이유로 전역 변수 섀도잉도 피하는 것이 좋다. 모든 전역 이름에 "g_" 접두사를 사용하여 피하는 것이 좋다.(관례)

 

 

 

연결 linkage

 

식별자의 연결은, 해당 이름의 다른 선언이 동일한 객체를 참조하는지 여부를 결정한다. 우리는 이제 내부 또는 외부 연결에 대해 알아보자.

 

내부 연결은 static과 같이 사용한다. 알기 쉽게 예를 들어보자

a.cpp 파일

[[maybe_unused]] constexpr int g_x { 2 }; // this internal g_x is only accessible within a.cpp

main.cpp 파일

#include <iostream>

static int g_x { 3 }; // this separate internal g_x is only accessible within main.cpp

int main()
{
    std::cout << g_x << '\n'; // uses main.cpp's g_x, prints 3

    return 0;
}

여기서 main 프로그램은 constexpr으로 선언된 cpp파일의 내부 변수에 접근할 수 없다. 각각의 파일 내부이기 때문에 이름이 지정된 변수가 있는지 main.cpp은 알 수 없다.

이러한 내부 변수는 constexpr 외에도 const, static 이 있다. 이런 전역 변수는 기본적으로 내부 연결을 갖는다.

 

함수는 기본적으로 외부 연결이지만 static 키워드로 인해 내부 연결로 설정할 수 있다.

[[maybe_unused]] static int add(int x, int y)
{
    return x + y;
}

이 함수는 static으로 선언됐다. 때문에 이 파일 안에서만 사용 가능하다. 다른 파일에서 이 함수에 접근할 시 오류임

 

(그런데 최근 C++에서는 static사용보단 이름 없는 네임스페이스가 더 선호된다고 한다. 명명되지 않은 네임스페이스는 더 넓은 범위의 식별자에 내부 연결을 제공할 수 있으며 많은 식별자에 내부 연결을 제공하는 데 더 적합하다.)

 

왜 굳이 식별자에 내부 연결을 제공해야 할까?

  • 다른 파일에 액세스할 수 없는지 확인하려는 식별자가 있다. 이는 우리가 건들고 싶지 않은 전역 변수일 수도 있고, 호출하고 싶지 않은 도우미 함수일 수도 있다.
  • 이름 충돌을 피하기 위한 방법이다. 내부 연결이 있는 식별자는 링커에 노출되지 않으므로 전체 프로그램이 아닌 동일한 번역 단위의 이름에만 충돌할 수 있다.

 

 

 

외부연결이 있는 식별자는 해당 식별자가 정의된 파일과 다른 코드 파일(정방향 선언을 통해) 모두에서 확인하고 사용할 수 있다. 우리가 사용하는 진짜 전역이다. 전역변수에 기본적으로 내부연결이 있듯이, 함수에는 기본적으로 외부 연결이 있다. 이런 함수를 static 키워드를 사용하여 내부적으로 만들 수 있다.

 

만약 다른 파일에 정의된 함수를 호출하려면, 해당 함수를 사용하려는 다른 파일에 해당 함수에 대한  forward declaration 를 배치해야 한다. 전방 선언은 컴파일러에게 함수의 존재를 알리고 링커는 함수 호출을 실제 함수 정의에 연결한다.

#include <iostream>

void sayHi() // this function has external linkage, and can be seen by other files
{
    std::cout << "Hi!\n";
}
void sayHi(); // forward declaration for function sayHi, makes sayHi accessible in this file

int main()
{
    sayHi(); // call to function defined in another file, linker will connect this call to the function definition

    return 0;
}

 

 

 

여기서 전역변수를 외부 연결로 사용하고 싶다면, 전역변수에 키워드 extern을 사용해야 한다.

(const가 아닌 전역 변수는 기본적으로 외부 변수이다. const를 사용하면 extern 키워드가 무시되는 작동방식)

 

위에서 다른 파일에 정의된 경우 전방 선언을 사용했듯이, 다른 파일에 정의된 외부 전역 변수 또한 사용하기 위해선 사용하려는 파일에 전방선언을 해줘야 한다. 그런데 변수의 경우 전방선언 생성도 extern키워드를 통해 수행된다.

// global variable definitions
int g_x { 2 }; // non-constant globals have external linkage by default
extern const int g_y { 3 }; // this extern gives g_y external linkage
#include <iostream>

extern int g_x; // this extern is a forward declaration of a variable named g_x that is defined somewhere else
extern const int g_y; // this extern is a forward declaration of a const variable named g_y that is defined somewhere else

int main()
{
    std::cout << g_x << ' ' << g_y << '\n'; // prints 2 3

    return 0;
}

위 작은 파일에서 전역 변수를 선언해줬고, 밑의 파일에서 참조하여 사용했다. 근데 어디서 선언이고 어느 부분이 전방선언인지 헷갈린다.

extern은 "이 변수에 외부 연결을 제공한다" 는 의미 외에 "다른 곳에 정의된 외부 변수에 대한 전방 선언"을 의미하기도 하다.

extern 조심할점

더보기

초기화되지 않은 non-const 전역 변수를 정의하려면 extern 키워드를 사용하면 안된다. 안그러면 C++에서는 변수에 대한 전방 선언을 시도한다고 판단한다.

 

constexpr 변수는 extern 키워드를 통해 외부 연결을 제공할 수 있지만 constexpr로 전방 선언될 수는 없다. 이는 컴파일러가 (컴파일 타임에) constexpr 변수의 값을 알아야 하기 때문이다. 해당 값이 다른 파일에 정의된 경우 컴파일러는 해당 다른 파일에 정의된 값이 무엇인지 알 수 없다.

그러나 constexpr 변수를 const로 선언할 수 있으며, 컴파일러는 이를 런타임 const로 처리한다. 근데 별로 사용되진 않는다..

 

함수의 전방 선언에는 따로 extern 키워드가 필요하지 않다. 컴파일러는 함수 본문을 제공하는지 여부에 따라 새 함수를 정의하는지 또는 전방 선언을 하는지 여부를 알 수 있다. 

변수 전방 선언에는 초기화되지 않은 변수 정의를 변수 전방 선언과 구별하는 데 도움이 되는 키워드extern이 필요하다 (그 외 다른 점은 비슷하다)

 

연결 정리

내부 연결이 있는 식별자는, 동일한 번역 단위 내에서 동일한 식별자의 선언이 동일한 개체 또는 함수를 참조한다.

정적 전역 변수(초기화 또는 초기화되지 않음)
정적 함수
Const 전역 변수
이름이 지정되지 않은 네임스페이스 내에 선언된 함수
이름이 지정되지 않은 네임스페이스 내에 선언되고 타입이 정의된 (열거형 및 클래스) 프로그램

외부 연결이 있는 식별자는, 전체 프로그램 내에서 동일한 식별자가 선언되어 동일한 개체 또는 기능을 참조한다.

const가 아닌 전역 변수(초기화 또는 초기화되지 않음)
외부 const 전역 변수
인라인 const 전역 변수

 

 

const가 아닌 전역변수는 왜 안좋은가?

 

일반적으로 프로그래머는 전역변수 사용을 피한다. 대규모 프로그램에서 문제가 되는 경우가 많기 때문이다. 지금부터 더 명확한 이유들을 알아보자.

 

전역변수가 const가 아닐 때 위험한 이유는 그 값이 호출되는 함수에 의해 변경될 수 있고(실수로? 무려 전역변수가) 심지어 프로그래머는 이런 일이 발생했는지 쉽게 알 수 없기 때문이다.

int g_mode; // declare global variable (will be zero-initialized by default)

void doSomething()
{
    g_mode = 2; // set the global g_mode variable to 2
}

int main()
{
    g_mode = 1; // note: this sets the global g_mode variable to 1.  It does not declare a local g_mode variable!

    doSomething();

    // Programmer still expects g_mode to be 1
    // But doSomething changed it to 2!

    if (g_mode == 1)
    {
        std::cout << "No threat detected.\n";
    }
    else
    {
        std::cout << "Launching nuclear missiles...\n";
    }

    return 0;
}

main에서 변수를 1로 설정한 다음 함수를 호출했다. 여기서 명시적인 지식이 없다면 변수의 값이 바뀔거라고 예상하지 못할 것이다. 

즉, 전역 변수는 프로그램의 상태를 예측할 수 없게 만든다. 모든 함수 호출은 잠재적으로 위험하므로 프로그래머는 어떤 것이 위험하고 어떤 것이 위험하지 않은지 쉽게 알 수 없다. 지역 변수는 다른 함수가 직접적으로 영향을 미칠 수 없기 때문에 훨씬 안전하다. 언제 바뀌는지 알기 어렵기 때문에 보통 전역변수는 그걸 사용하는 함수 바로 위에 선언한다.

 

정적 변수의 초기화는 프로그램이 시작과 동시에 발생한다. 이를 두 가지 단계로 나눌 수 있다.

첫 번째 단계를 정적 초기화라고 한다. 여기서는 리터럴을 포함한 constexpr 초기화가 있는 전역 변수가 해당 값으로 초기화된다. 물론 초기화 프로그램이 없는 전역 변수는 0으로 초기화된다.

두 번째 단계를 동적 초기화라고 한다. 여기서는 간단하게 말하면 constexpr이 아닌 초기화 프로그램을 사용하는 전역 변수가 초기화된다. (함수 리턴값으로 초기화 등)

왜 초기화 순서가 중요한지 예시를 보자

int initX();  // forward declaration
int initY();  // forward declaration

int g_x{ initX() }; // g_x is initialized first
int g_y{ initY() };

int initX()
{
    return g_y; // g_y isn't initialized when this is called
}

int initY()
{
    return 5;
}

int main()
{
    std::cout << g_x << ' ' << g_y << '\n';
}

이건 0  5 를 출력한다. 

이것 외에도 여러 파일의 초기화 순서 등 많은 문제를 야기한다. 그냥 가능하면 동적 초기화를 피하자.

 

 

네임스페이스를 사용하여 전역 변수를 사용할 수 있다.

constant.h

#ifndef CONSTANTS_H
#define CONSTANTS_H

// define your own namespace to hold constants
namespace constants
{
    // constants have internal linkage by default
    constexpr double pi { 3.14159 };
    constexpr double avogadro { 6.0221413e23 };
    constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif

여기서 main.cpp파일의 상수에 엑세스하려면 범위확인 연산자 ::를 통하여 사용한다.

main.cpp

#include "constants.h" // include a copy of each constant in this file

#include <iostream>

int main()
{
    std::cout << "Enter a radius: ";
    double radius{};
    std::cin >> radius;

    std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

    return 0;
}

이렇게 여러 파일에서 전역 상수를 공유할 수 있다. 이 경우 인라인 변수 방식이 사용되어 해당 코드 파일에 복사된다.

여기서 변수는 함수 외부에 있기 때문에 포함된 파일 내에서는 전역 변수로 처리되므로 해당 파일의 어느 곳에서나 사용 가능하다.

const 전역에는 내부 연결이 있으므로  각 .cpp 파일은 링커가 볼 수 없는 전역 변수의 독립적인 버전을 가져온다. 대부분의 경우 이들은 const이기 때문에 컴파일러는 단순히 변수를 최적화할 수 있다.

 

 

하지만 이와 같은 방법(헤더를 사용한 내부 변수로서의 전역 상수)은 몇 가지 단점이 있다.

비록 간단하지만, constant.h가 다른 코드파일에 #include 될 때마다 이러한 각 변수가 include된 파일에 복사된다.

따라서 복사되면 될수록 그 만큼 중복된다. 헤더 가드는 헤더가 여러 다른 코드 파일에 한 번 include되는 것이 아니라 단일 include 파일에 두 번 이상 include되는 것을 방지하기 때문에 이런 일을 막지 않는다.

때문에 단일 상수 값을 변경하려면 상수 헤더를 포함하는 모든 파일을 다시 컴파일해야 하므로 대규모 프로젝트의 경우 다시 빌드하는 데 시간이 오래 걸린다. 또한 상수의 크기가 크고 최적화할 수 없는 경우 많은 메모리를 사용할 수 있다.

 

 

그럼 해결은 간단하다. 반대로 행동하면 된다. 상수를 외부 변수로 바꾸자.

그럼 모든 파일에서 공유되는 단일 변수(한 번 초기화됨)를 가질 수 있기 때문이다.

이와 같은 방법은 .cpp 파일에 상수를 정의하고 (정의가 한 위치에만 존재하는지 확인하기 위해) 헤더에 선언을 전달한다(다른 파일에 include됨).

>> constexpr 변수는 외부 연결이 있더라도 전방 선언이 불가능하기 때문에 이 방법에서는 constexpr 대신 const를 사용한다. 이는 컴파일러가 컴파일 타임에 변수 값을 알아야 하는데 전방 선언에서는 이 정보를 제공하지 않기 때문이다.

 

constant.cpp

#include "constants.h"

namespace constants
{
    // actual global variables
    extern const double pi { 3.14159 };
    extern const double avogadro { 6.0221413e23 };
    extern const double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}

constant.h

#ifndef CONSTANTS_H
#define CONSTANTS_H

namespace constants
{
    // since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
    extern const double pi;
    extern const double avogadro;
    extern const double myGravity;
}

#endif

main.cpp

#include "constants.h" // include all the forward declarations

#include <iostream>

int main()
{
    std::cout << "Enter a radius: ";
    double radius{};
    std::cin >> radius;

    std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

    return 0;
}

이제 심볼릭 상수( 코드에서 사용되는 값을 대체하기 위해 사용되는 상수 값 )는 constants.cpp 파일에서 한 번만 선언되고 다른 파일에서는 해당 상수를 다시 선언하지 않는다

이렇게 하면 중복 선언으로 인한 잠재적인 오류를 방지하고, 변경이 필요한 경우 상수를 한 곳에서 수정할 수 있다. 또한, 이러한 상수가 constants.cpp 파일에만 정의되므로, 해당 파일만 다시 컴파일하면 변경 사항이 적용된다.

 

 

이제 끝난줄 알았는데 이 방식에도 문제가 있다고 한다.

이와 같은 상수는 이제 constants.cpp에서 실제로 정의된 파일 내에서만 컴파일 타임 상수로 간주된다. 

다른 파일에서 컴파일러는 상수 값을 정의하지 않는(그리고 링커에서 확인해야 하는) 전방 선언만 볼 수 있다. 즉, 다른 파일에서는 컴파일 타임 상수가 아닌 런타임 상수 값으로 처리된다. 따라서 constants.cpp 외부에서는 컴파일 타임 상수가 필요한 곳에서 이러한 변수를 사용할 수 없다. 

뭔가 설명이 확 안와닿는다.. 추가설명 더보기

더보기
  1. 컴파일 타임 상수(Compile-time constants): 컴파일러는 컴파일 타임 상수를 처리할 때, 해당 상수를 실행 코드에 직접 삽입한다. 이는 상수 값이 컴파일러가 코드를 번역하는 동안 이미 알려져 있고 변경되지 않는다는 것을 의미한다. 이러한 상수들은 일반적으로 코드의 일부로 대체되어 실행 코드가 생성된다.
  2. 런타임 상수(Runtime constants): 런타임 상수는 프로그램이 실행되는 동안 값이 결정되는 상수다. 이러한 상수는 실행 중에 변수로 취급된다.

위 내용들은 이러한 차이점을 말하고 있다. constants.cpp에서 정의된 상수는 컴파일 타임 상수로 처리되어 해당 파일을 컴파일할 때 이미 값이 결정되어있다. 다른 파일에서는 이러한 상수가 실제로 정의되어 있지 않기 때문에 전방 선언만 볼 수 있다. 이것은 링커가 런타임 상수로 처리된다는 것을 의미한다. 따라서 constants.cpp에서 정의된 상수를 사용하기 위해서는 이러한 변수를 사용할 수 없다.

컴파일 타임에서 변수를 사용할 수 있으려면 컴파일러는 전방선언 뿐만 아니라 변수가 어떻게 정의되었는지도 확인해야 한다.

 

코드를 작성할 때 컴파일러는 각 소스 파일을 개별적으로 처리한다. 컴파일러는 현재 컴파일 중인 파일 안에 무엇이 들어 있는지, 어떤 헤더 파일이 들어 있는지만 볼 수 있다.

우리가 변수들을 정의할 때 constant.cpp이라는 상수 파일을 가지고 있다고 상상해 보자. 컴파일러가 main.cpp이라는 다른 파일을 컴파일할 때, 그것은 constant.cpp에 정의된 변수들에 대해 자동으로 알 수 없다. 우리가 constant.h 헤더 파일을 포함해서 명시적으로 말해줘야만 그것들에 대해 알 수 있다.

이제 컴파일 시간 상수인 constexpr 변수를 사용하면 컴파일러는 컴파일 시간에 해당 변수의 정의를 확인해야 한다. 이러한 constexpr 변수를 constant.cpp처럼 별도의 .cpp 파일에 정의하면 컴파일러가 해당 파일을 컴파일할 때 볼 수 없기 때문에 다른 소스 파일은 컴파일 시간 상수로 액세스할 수 없다.

이 한계를 극복하기 위한 한 가지 방법은 헤더 파일에 상수를 정의하여 모든 소스 파일이 컴파일 중에 볼 수 있도록 하는 것이다. 이를 통해 constexpr 변수가 어디에 사용되는지 컴파일 시간 상수로 처리할 수 있다. 그러나 자주 변경되어 컴파일 시간이 길어지는 상수가 있다면, 나머지는 헤더 파일에 보관하면서 해당 상수만 .cpp 파일로 이동하는 것을 고려할 수 있다. 이를 통해 접근성과 컴파일 효율성의 균형을 맞출 수 있다.

 

 

 


 

 

 

위에서 한창 전역 변수에 대해 떠들었는데,,

이제 지역 변수에 대해서 공부해보자. 우선 정적 지역 변수다.

 

지역 변수에 static 키워드를 사용하면 기간이  automatic duration 에서 static duration 로 바뀐다. 이는 이제 변수가 프로그램 시작 시 생성되고 프로그램 종료 시 삭제됨을 의미한다(전역 변수와 마찬가지로). 결과적으로 정적 변수는 범위를 벗어난 후에도 해당 값을 유지한다.

간단한 예시로, 임의의 함수 안에서 변수를 선언했다고 치자. 함수 안에서 변수 값을 변경 후 main에서 출력해도, 함수를 호출할때 마다 항상 같은 값이 나온다.

 

하지만 그냥 변수가 아니라 static 변수를 선언했다고 하면 어떨까?

우선 이 함수 안의 변수는 프로그램 시작시 생성된다. 그리고 초기화를 시작하는데, 명시적으로 초기화되지 않았으면 0으로 초기화되거나 constexpr 초기화 프로그램으로 인해 초기화될 수 있다. 후속 호출에선 초기화되지 않고 무시한다.

( constexpr이 아닌 초기화 프로그램을 사용하는 정적 지역 변수는 변수 정의가 처음 발견될 때 다시 초기화된다. )

그리고 main에서 같은 함수를 계속 호출하면.... 변경된 값 위로 다시 조작된 값이 출력된다!! 

함수 범위를 벗어나도 파괴되지 않는다는 이런 차이가 있다. 전역 변수 앞에 g_를 사용하는 것처럼 정적 지역 변수 앞에 s_를 사용하는 것이 일반적이다.

게다가 정적 지역 변수는 지역 변수이기 때문에 다른 함수에 의해 변경될 수 없다..!

정적 변수는 블록 범위에 대한 가시성을 제한하면서 전역 변수의 일부 이점(프로그램이 끝날 때까지 삭제되지 않음)을 제공한다. 이렇게 하면 값을 정기적으로 변경하더라도 더 쉽게 이해하고 더 안전하게 사용할 수 있다.

 

 

 

정적 지역 변수는 const(또는 constexpr)로 만들 수 있다. const 정적 지역 변수를 잘 사용할 수 있는 방법은,

const 값을 사용해야 하는 함수가 있지만 개체를 만들거나 초기화하는 데 비용이 많이 드는 경우가 있다 ( 데이터베이스에서 값을 읽어야 하는 경우). 만약 일반적인 지역 변수를 사용했다면 함수를 실행할 때마다 변수가 생성되고 초기화된다...

요약하면, const/constexpr 정적 지역 변수를 사용하면 값비싼 개체를 한 번 생성하고 초기화한 다음 함수가 호출될 때마다 다시 사용할 수 있다.

 

변수 정리

더보기

Local variable int x; Block Automatic None  
Static local variable static int s_x; Block Static None  
Dynamic local variable int* x { new int{} }; Block Dynamic None  
Function parameter void foo(int x) Block Automatic None  
External non-constant global variable int g_x; Global Static External Initialized or uninitialized
Internal non-constant global variable static int g_x; Global Static Internal Initialized or uninitialized
Internal constant global variable constexpr int g_x { 1 }; Global Static Internal Must be initialized
External constant global variable extern const int g_x { 1 }; Global Static External Must be initialized
Inline constant global variable (C++17) inline constexpr int g_x { 1 }; Global Static External Must be initialized

 

 

 


 

 

using선언 및 지시문 사용

선언 중 using 선언에 대해 알아보자.

using은 정규화되지 않은 이름(범위없음)을 정규화된 이름의 별칭으로 사용할 수 있다.

여기서 정규화된 이름이란, 연관된 범위를 포함하는 이름이다. std::cout처럼 범위확인 연산자를 사용할 수 있는 네임스페이스이다. 반대로 그냥 변수 x나 cout같은 경우 정규화되지 않은 이름이다.

#include <iostream>

int main()
{
   using std::cout; // this using declaration tells the compiler that cout should resolve to std::cout
   cout << "Hello world!\n"; // so no std:: prefix is needed here!

   return 0;
} // the using declaration expires at the end of the current scope

이 예시와 같이 using 선언을 이용하여  std::cout을 선언해주면 컴파일러는 우리가 객체 cout을 std 네임스페이스로부터 사용할 것임을 알 수 있다.

이렇게 선언한 이후부터는 우리가 cout을 사용할 때 마다 그 의미는 std::cout이 될 것이다. 만약 cout과 std::cout간의 이름 충돌이 일어날 경우 시스템은 std::cout을 더 선호한다.

하지만 접두사 std::를 사용하는 것보다 덜 명시적이긴 하다.

 

 

using 지시문

 

#include <iostream>

int main()
{
   using namespace std; // this using directive tells the compiler to import all names from namespace std into the current namespace without qualification
   cout << "Hello world!\n"; // so no std:: prefix is needed here

   return 0;
} // the using-directive expires at the end of the current scope

네임스페이스 std를 사용하는 using-directive는 컴파일러에게 std 네임스페이스의 모든 이름을 현재 범위(이 경우 함수 main())로 가져오라고 한다. 그런 다음 qualified된 식별자 cout을 사용하면 알아서 std::cout으로 해결된다.


 

그런데 이렇게 권한 없는 식별자들을 굳이 using을 사용해야할 만큼 이득이 있을까? 아까말한 반복적으로 입력하지 않아도 되는 것 외에.. 사실 이것도 명시적이지 않아서 선호되진 않음..

게다가 using 지시문은 네임스페이스에서 모든 이름들을 가져오기 때문에 이름 충돌이 발생할 가능성이 아주 크다..

namespace a
{
	int x{ 10 };
}

namespace b
{
	int x{ 20 };
}

int main()
{
	using namespace a;
	using namespace b;

	std::cout << x << '\n';

	return 0;
}

예를 들자면 이런 경우가 있겠지만 범위 한정자를 사용하여 a::x나 a::로 바꿔서 사용해주면 되긴 하다.

여전히 굳이.. 사용할 필요성은 못느끼겠다.

 만약 사용해야겠다 싶다면 using선언을 블럭 내부에 하여 범위를 좁히는 것이 그나마 좋겠다.

 

 


 

 

using부터 시작해서 그냥 namespace 자체가 딱히 사용할 일이 있나 싶다. 하지만! 알아두면 언젠가 쓸 곳이 있긴 하다.

명명되지 않은 인라인 네임스페이스의 경우가 그것이다.

 

다음 코드는 명명되지 않은 네임스페이스이다.

namespace // unnamed namespace
{
    void doSomething() // can only be accessed in this file
    {
        std::cout << "v1\n";
    }
}

int main()
{
    doSomething(); // we can call doSomething() without a namespace prefix

    return 0;
}

 

이름이 지정되지 않은 네임스페이스에 선언된 모든 콘텐츠는 상위 네임스페이스의 일부인 것처럼 처리된다. 따라서 함수 doSomething()이 이름이 지정되지 않은 네임스페이스에 정의되어 있지만, 함수 자체는 상위 네임스페이스(이 경우 글로벌 네임스페이스)에서 액세스할 수 있으므로 수식자 없이 main()에서 doSomething()을 호출할 수 있다.

이것만 봐서는 아직 쓸모없어 보인다고 할 수 있다. 하지만 기능은 이게 끝이 아니다.

무려 명명되지 않은 네임스페이스 내의 모든 식별자가 내부연결이 있는 것처럼 처리된다! 너무 길다. 그냥 unnamed namespace라고 할걸. 아무튼 결국 이 unnamed namespace 의 콘텐츠는 이 파일 외부에서 볼 수 없다는 말이다.

그냥 unnamed namespace의 모든 함수를 static 정적 함수로 정의하는 것과 같은 의미이다.

 

그럼 언제 사용하느냐. 주로 주어진 파일에 로컬로 유지하려는 콘텐츠가 많을 때 사용한다.

그냥 static으로 하면 될걸 굳이?

모든 static 선언을 개별적으로 표시하는 것보다 이름 없는 단일 네임스페이스에 이러한 콘텐츠를 모아두는 것이 더 쉽기 때문이다.

이것은 프로그램 정의 타입(나중에 배울 내용)을 파일에 로컬로 유지하므로 이에 상응하는 대체 메커니즘이 없다.

 

 

이어서 이 개념을 사용한 인라인 네임스페이스를 알아보자.

이미 만들어진 프로그램의 작동방식을 변경하고 싶은 경우가 있다. 하지만 함부로 변경하다간 이전 버전을 사용하는 기존 프로그램이 손상될 수도 있다. 이 경우 우리는 비슷한 이름을 가진 새 함수를 만들 수 있다. ( doSomething, doSomething_v2, doSomething_v3등…)이런거 말고 다른 대안이 있다. 인라인 네임스페이스는 일반적으로 콘텐츠 버전을 지정하는 데 사용되는 네임스페이스이다. 명명되지 않은 네임스페이스와 마찬가지로 인라인 네임스페이스 내에 선언된 모든 항목은 상위 네임스페이스의 일부로 간주된다. 그러나 이름이 지정되지 않은 네임스페이스와 달리 인라인 네임스페이스는 연결에 영향을 주지 않는다.

#include <iostream>

inline namespace V1 // declare an inline namespace named V1
{
    void doSomething()
    {
        std::cout << "V1\n";
    }
}

namespace V2 // declare a normal namespace named V2
{
    void doSomething()
    {
        std::cout << "V2\n";
    }
}

int main()
{
    V1::doSomething(); // calls the V1 version of doSomething()
    V2::doSomething(); // calls the V2 version of doSomething()

    doSomething(); // calls the inline version of doSomething() (which is V1)

    return 0;
}

여기서 main()의 dosomething()호출은 V1(인라인 버전)을 호출한다. (물론 명시적으로 V2호출 가능)이렇게 기존 프로그램 기능을 보존한 채로 변형을 시도할 수 있다.

 

 

네임스페이스는 인라인일 수도 있고 이름이 없을 수도 있다. 만약 이런 경우, 인라인 네임스페이스 내에 unnamed 네임스페이스를 중첩하는 것이 더 낫다.

#include <iostream>

namespace V1 // declare a normal namespace named V1
{
    void doSomething()
    {
        std::cout << "V1\n";
    }
}

inline namespace V2 // declare an inline namespace named V2
{
    namespace // unnamed namespace
    {
        void doSomething() // has internal linkage
        {
            std::cout << "V2\n";
        }

    }
}

int main()
{
    V1::doSomething(); // calls the V1 version of doSomething()
    V2::doSomething(); // calls the V2 version of doSomething()

    ::doSomething(); // calls the inline version of doSomething() (which is V2)

    return 0;
}