LearnCPP - 7

Justin·2026년 2월 15일

LearnCPP.com

목록 보기
7/22

복합문(Compound statement)

  • 복합문(Compound statement), 또는 블록(Block)이라고 부르는 것은 0개 이상의 문장들을 하나로 묶은 그룹을 말해요.
  • 아주 중요한 점은, 컴파일러가 이 묶음을 마치 하나의 문장인 것처럼 취급한다는 거예요.
  • 우리가 함수를 만들 때 이미 블록을 사용해 왔답니다.
  • 함수의 몸체(Body)가 바로 블록이니까요.
  • 함수 안에 또 다른 함수를 정의할 수는 없지만, 블록 안에 또 다른 블록을 넣는 것은 가능해요. 이걸 '중첩된다'라고 표현하죠.
  • 블록이 중첩되었을 때, 바깥쪽을 감싸고 있는 블록을 외부 블록(Outer block)이라 하고,
  • 그 안에 들어있는 블록을 내부 블록(Inner block) 혹은 중첩 블록(Nested block)이라고 부릅니다.

조건에 따라 여러 문장 실행하기

  • 기본적으로 if 문은 조건이 '참(True)'일 때 딱 하나의 문장만 실행하게 되어 있어요.
  • 하지만 조건이 맞았을 때 여러 가지 일을 한꺼번에 처리하고 싶다면 어떻게 해야 할까요?
  • 이때 그 '하나의 문장' 자리에 문장들의 묶음인 '블록'을 넣어주면 된답니다.
if (1)
	std::cout << "하나의 문장";
    
if (1) { //내부 블록
	std::cout << "하나의 문장";
    std::cout << "하나 이상의 문장";
 }

블록 중첩 레벨 (Nesting levels)

  • 블록 안에 블록을 넣고, 그 안에 또 블록을 넣는 것도 가능할까요? 네, 가능합니다!
  • 여기서 중첩 레벨(Nesting level) 혹은 중첩 깊이(Nesting depth)라는 용어가 나와요.
  • 이건 함수 내의 어떤 지점에서 '지금 내가 최대 몇 개의 블록 안에 감싸여 있는가'를 나타내는 숫자예요.
  • C++ 표준(Standard)에 따르면 컴파일러는 무려 256단계의 중첩까지 지원해야 한다고 해요.
  • 하지만 모든 컴파일러가(예: 작성 시점 기준 Visual Studio 등) 이를 완벽히 지원하는 건 아닐 수 있어요.
  • 기술적으로 가능하다고 해서 깊게 만드는 게 좋을까요?
  • 아닙니다. 중첩 레벨은 3단계 이하로 유지하는 것이 좋습니다.
  • 함수의 길이가 너무 길어지면 여러 개의 작은 함수로 나누는 '리팩토링(Refactoring)'을 하듯이,
  • 블록의 중첩이 너무 깊어지면 코드를 읽기가 아주 힘들어져요.
  • 이럴 때도 가장 깊은 곳에 있는 블록들을 떼어내어 별도의 함수로 만드는 리팩토링을 하는 것이 좋습니다.

나만의 네임스페이스 정의하기

  • C++에서는 namespace라는 키워드를 사용해 우리만의 네임스페이스를 만들 수 있어요.
  • 이렇게 여러분이 직접 만든 네임스페이스를 흔히 사용자 정의 네임스페이스(User-defined namespaces)라고 불러요.
  • 더 정확히는 '프로그램 정의 네임스페이스'라고 하는 게 맞겠지만요.
  • 문법은 아주 간단해요.
  • namespace 키워드를 쓰고, 그 뒤에 네임스페이스의 식별자를 적은 다음, 중괄호 { } 안에 내용을 넣으면 끝이에요.
namespace NamespaceIdentifier
{
    // 네임스페이스의 내용이 여기에 들어갑니다
}

범위 지정 연산자 ::로 네임스페이스 접근하기

  • 특정 네임스페이스 안에 있는 식별자를 찾으라고 컴파일러에게 지시하는 가장 좋은 방법은 범위 지정 연산자::를 사용하는 거예요.
std::cout << Goo::doSomething(4, 3) << '\n'; // Goo 네임스페이스에 있는 doSomething()을 사용

이름 없이 범위 지정 연산자 사용하기

  • 범위 지정 연산자 앞에 ::doSomething 처럼 아무런 네임스페이스 이름도 적지 않을 수도 있어요.
  • 이렇게 하면 컴파일러는 전역 네임스페이스에서 그 이름을 찾으라는 뜻으로 이해합니다.

네임스페이스 내부에서의 식별자 찾기

  • 네임스페이스 안에서 어떤 식별자를 사용했는데, 앞에 범위 지정(Foo:: 같은 것)을 안 해줬다면 어떻게 될까요?
  • 컴파일러는 일단 지금 있는 그 네임스페이스 안에서 정의를 찾으려고 노력해요.
  • 만약 못 찾으면? 그 네임스페이스를 감싸고 있는 상위 네임스페이스를 차례대로 뒤져보고, 마지막으로 전역 네임스페이스까지 확인합니다.
#include <iostream>

void print() // 전역 네임스페이스의 print()
{
	std::cout << " there\n";
}

namespace Foo
{
	void print() // Foo 네임스페이스의 print()
	{
		std::cout << "Hello";
	}

	void printHelloThere()
	{
		print();   // Foo 네임스페이스 안의 print()를 먼저 호출함
		::print(); // 전역 네임스페이스의 print()를 호출함
	}
}

int main()
{
	Foo::printHelloThere();
	return 0;
}

네임스페이스 내용의 전방 선언(Forward declaration)

  • 헤더 파일을 사용해서 전방 선언을 할 때도 주의할 점이 있어요.
  • 네임스페이스 안에 있는 식별자를 전방 선언하려면, 선언도 똑같은 네임스페이스 안에서 해줘야 해요.
// add.h

#ifndef ADD_H
#define ADD_H

namespace BasicMath
{
    // add() 함수는 BasicMath 네임스페이스의 일부입니다
    int add(int x, int y);
}

#endif
// add.cpp

#include "add.h"

namespace BasicMath
{
    // add() 함수를 BasicMath 네임스페이스 안에서 정의합니다
    int add(int x, int y)
    {
        return x + y;
    }
}
// main.cpp

#include "add.h"  // BasicMath::add()를 위해 포함
#include <iostream>

int main()
{
    std::cout << BasicMath::add(4, 3) << '\n';

    return 0;
}

여러 개의 네임스페이스 블록

  • 같은 이름의 네임스페이스 블록을 여러 곳(여러 파일 또는 같은 파일 내 여러 곳)에 나눠서 작성해도 괜찮아요.
  • 모두 같은 네임스페이스의 가족으로 취급됩니다.

중첩된 네임스페이스 (Nested namespaces)

  • 네임스페이스 안에 또 다른 네임스페이스를 만들 수도 있어요.
  • C++17부터는 이렇게 중첩된 네임스페이스를 훨씬 간단하게 선언할 수 있어요.
  • GooFoo 안에 있으니까 Foo::Goo::add라고 주소를 길게 써서 접근해야 해요.
#include <iostream>

namespace Foo::Goo // Foo 안에 Goo가 있다는 것을 한 번에 표현 (C++17 스타일)
{
    int add(int x, int y)
    {
        return x + y;
    }
}

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

네임스페이스 별칭 (Namespace aliases)

  • 중첩된 네임스페이스 이름이 너무 길어서 타이핑하기 힘들다면, 네임스페이스 별칭을 써서 짧은 별명을 붙여줄 수 있어요.
  • 별칭의 좋은 점은 코드를 유지 보수할 때도 드러납니다.
  • 만약 Foo::Goo의 기능을 다른 곳(V2)으로 옮기고 싶다면, 별칭이 가리키는 곳만 V2로 바꿔주면 돼요.
  • 코드 전체를 뒤져서 Foo::Goo를 일일이 바꿀 필요가 없죠.
#include <iostream>

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

int main()
{
    namespace Active = Foo::Goo; // 이제 Active는 Foo::Goo를 가리킵니다

    std::cout << Active::add(1, 2) << '\n'; // 실제로는 Foo::Goo::add()가 호출됨

    return 0;
} // Active 별칭은 여기서 끝납니다

네임스페이스 사용 팁

  • C++의 네임스페이스는 원래 정보를 계층적으로 정리하라고 만든 게 아니라, 주로 이름 충돌을 막기 위해 설계되었어요.
  • 개인적인 작은 프로그램을 만들 때는 굳이 네임스페이스를 안 써도 괜찮아요.
  • 하지만 외부 라이브러리를 많이 갖다 쓰는 큰 프로젝트라면 이름 충돌을 피하기 위해 코드를 네임스페이스로 감싸주는 게 좋습니다.
  • 다른 사람에게 배포할 코드는 반드시 네임스페이스를 사용해서, 통합될 때 충돌이 나지 않게 해야 합니다.
  • 보통 Foologger처럼 최상위 네임스페이스 하나면 충분해요.
  • 이렇게 하면 사용자가 Foologger라고 쳤을 때 자동 완성 기능으로 라이브러리의 모든 기능을 쉽게 볼 수 있다는 장점도 있죠.

지역 변수 (Local variables)

  • 지난 2챕터 지역 범위 소개에서 우리는 지역 변수에 대해 배웠습니다.
  • 함수 내부(함수의 매개변수 포함)에 정의된 변수들을 말하죠.
  • 범위라는 개념도 배웠어요.
  • 어떤 식별자(변수 이름 등)의 범위란 소스 코드 내에서 그 식별자에 접근할 수 있는 영역을 뜻해요.
  • 접근할 수 있다면 "범위 내에 있다(In scope)"라고 하고,
  • 접근할 수 없다면 "범위를 벗어났다(Out of scope)"라고 합니다.
  • 범위는 컴파일 타임에 결정되는 속성이라서, 범위를 벗어난 식별자를 사용하려고 하면 컴파일 에러가 발생해요.

지역 변수는 블록 범위(Block scope)를 가집니다.

  • 즉, 변수가 정의된 지점부터 그 변수가 포함된 블록이 끝나는 지점까지만 유효하다는 뜻이에요.

지역 변수는 자동 저장 기간을 가집니다.

  • 변수의 저장 기간은 변수가 언제 생성되고(메모리에 할당되고) 언제 소멸될지를 결정하는 규칙이에요.
  • 대부분의 경우, 이 저장 기간이 변수의 수명을 결정합니다.
  • 지역 변수는 자동 저장 기간(Automatic storage duration)을 가집니다.
  • 쉽게 말해, 변수가 정의되는 시점에 자동으로 생성되고, 정의된 블록이 끝날 때 자동으로 소멸된다는 뜻이죠.
  • 이런 이유로 지역 변수를 자동 변수(Automatic variable)라고 부르기도 해요.

지역 변수는 연결(Linkage)이 없습니다.

  • 식별자(변수 이름 등)에는 연결이라는 또 다른 속성이 있어요.
  • 지역 변수는 연결이 없습니다.
  • 즉, 연결이 없는 식별자는 이름이 같더라도 각각의 선언이 서로 다른 고유한 객체나 함수를 의미합니다.
  • 프로그래밍에서 '연결'이란, A 구역과 B 구역에 이름이 똑같은 변수(또는 함수)가 있을 때, 이 둘이 실제로 한 몸처럼 똑같은 녀석인지, 아니면 우연히 이름만 같을 뿐 각자 따로 노는 녀석인지를 판가름하는 성질을 말해요.

변수는 가장 제한적인 범위에서 정의해야 합니다.

  • 변수가 특정 중첩 블록 안에서만 사용된다면, 그 변수는 반드시 그 중첩 블록 안에서 정의해야 합니다.
  • 변수의 범위를 제한하면 활성화된 변수의 수가 줄어들어 프로그램의 복잡도가 낮아집니다.
  • 블록 안에 정의된 변수는 그 블록(과 그 내부 블록) 안에서만 영향을 미치므로 프로그램을 이해하기가 더 수월해집니다.
#include <iostream>

int main()
{
    // 여기에 y를 정의하지 마세요.
    {
        // y는 오직 이 블록 안에서만 사용되므로, 여기서 정의합니다.
        int y { 5 };
        std::cout << y << '\n';
    }
    // 그렇지 않으면 y가 필요 없는 이 곳에서도 y를 사용할 수 있게 됩니다.
    return 0;
}

전역 변수 소개 (Introduction to global variables)

  • C++에서는 변수를 함수 외부에서도 선언할 수 있습니다. 이러한 변수들을 전역 변수(Global variables)라고 부릅니다.

전역 변수 선언하기 (Declaring global variables)

  • 관례적으로 전역 변수는 파일의 맨 위, #include 구문들 바로 아래의 전역 네임스페이스에 선언해요.

전역 변수의 범위 (The scope of global variables)

  • 전역 네임스페이스에 선언된 식별자는 전역 네임스페이스 범위를 갖습니다.
  • 이를 보통 전역 범위(Global scope)라고 부르고, 때로는 비공식적으로 파일 범위(File scope)라고도 해요.
  • 이는 변수가 선언된 시점부터 해당 변수가 선언된 파일이 끝날 때까지 어디서든 접근할 수 있다는 뜻입니다.
  • 전역 변수는 사용자가 직접 정의한 네임스페이스 안에서도 정의할 수 있습니다.
  • 전역 변수는 전역 네임스페이스에 바로 정의하기보다는, 가급적 네임스페이스안에 정의하는 것을 권장합니다.

전역 변수는 정적 지속 시간을 갖습니다.

  • 전역 변수는 프로그램이 시작될 때(main() 함수가 실행되기도 전에) 생성되고, 프로그램이 종료될 때 파괴됩니다.
  • 이것을 정적 지속 시간(Static duration)이라고 부릅니다.
  • 이러한 정적 지속 시간을 가진 변수들을 종종 정적 변수(Static variables)라고도 부른답니다.

전역 변수 초기화 (Global variable initialization)

  • 기본적으로 값이 초기화되지 않는 지역 변수와는 달리, 정적 지속 시간을 가진 변수들은 기본적으로 0으로 초기화가 됩니다.
int g_x;       // 명시적 초기화 없음 (기본적으로 0으로 초기화됨)
int g_y {};    // 값 초기화 (결과적으로 0으로 초기화됨)
int g_z { 1 }; // 특정 값으로 리스트 초기화됨

상수 전역 변수 (Constant global variables)

  • 지역 변수와 마찬가지로, 전역 변수도 상수가 될 수 있습니다.
  • 그리고 모든 상수가 그렇듯, 상수 전역 변수는 반드시 초기화되어야만 합니다.
#include <iostream>

const int g_x;     // 오류: 상수 변수는 반드시 초기화되어야 합니다
constexpr int g_w; // 오류: constexpr 변수는 반드시 초기화되어야 합니다

const int g_y { 1 };     // const 전역 변수 g_y, 특정 값으로 초기화됨
constexpr int g_z { 2 }; // constexpr 전역 변수 g_z, 특정 값으로 초기화됨

void doSomething()
{
    // 전역 변수는 파일 내 어디서든 보고 사용할 수 있습니다
    std::cout << g_y << '\n';
    std::cout << g_z << '\n';
}

int main()
{
    doSomething();

    // 전역 변수는 파일 내 어디서든 보고 사용할 수 있습니다
    std::cout << g_y << '\n';
    std::cout << g_z << '\n';

    return 0;
}
// 여기서 g_y와 g_z의 범위가 끝납니다

변수 섀도잉

  • C++에서 각각의 코드 블록 {} 은 자신만의 유효 범위(Scope)를 가집니다.
  • 그렇다면 만약 바깥쪽 블록에 있는 변수와 똑같은 이름을 가진 변수를 안쪽(중첩된) 블록에 새로 만들면 어떤 일이 일어날까요?
  • 이런 상황이 발생하면, 두 변수가 모두 유효한 영역에서는 안쪽에 있는 변수가 바깥쪽에 있는 변수를 "가려버리게" 됩니다.
  • 우리는 이 현상을 이름 가리기(Name hiding) 또는 섀도잉(Shadowing)이라고 부릅니다.

지역 변수 섀도잉 (Shadowing of local variables)

  • 아래 코드를 보며 함께 이해해 볼까요?
#include <iostream>

int main()
{ // 바깥쪽 블록 시작
    int apples { 5 }; // 바깥쪽 블록의 apples 변수입니다.

    { // 안쪽(중첩된) 블록 시작
        // 여기서는 아직 안쪽 apples가 정의되지 않았으므로, 바깥쪽 apples를 가리킵니다.
        std::cout << apples << '\n'; // 바깥쪽 apples의 값(5)을 출력합니다.

        int apples{ 0 }; // 안쪽 블록의 유효 범위에 새로운 apples를 정의합니다.

        // 이제부터 apples는 안쪽 블록의 apples를 가리킵니다.
        // 바깥쪽 블록의 apples는 일시적으로 가려집니다(hidden).

        apples = 10; // 바깥쪽이 아닌, 안쪽 블록의 apples에 10을 할당합니다.

        std::cout << apples << '\n'; // 안쪽 블록의 apples 값을 출력합니다.
    } // 안쪽 블록이 끝나며 안쪽 apples 변수는 소멸(Destroy)됩니다.

    std::cout << apples << '\n'; // 다시 바깥쪽 블록의 apples 값을 출력합니다.

    return 0;
} // 바깥쪽 블록이 끝나며 바깥쪽 apples 변수도 소멸됩니다.
  • 위 프로그램에서 우리는 먼저 바깥쪽 블록에 apples라는 이름의 변수를 선언했습니다.
  • 이 변수는 안쪽 블록에서도 여전히 보이므로, 처음에는 그 값인 5가 정상적으로 출력됩니다.
  • 하지만 안쪽 블록에서 이름이 똑같은 또 다른 apples 변수를 선언하면 상황이 달라집니다.
  • 이 선언 시점부터 안쪽 블록이 끝날 때까지 apples라는 이름은 바깥쪽 변수가 아니라 새로 만든 '안쪽 변수'를 가리키게 됩니다.
  • 안쪽 블록 안에서 똑같은 이름으로 변수를 가려버린 상태라면, 가려진 바깥쪽 지역 변수에 직접 접근할 수 있는 방법은 없습니다.

전역 변수 섀도잉 (Shadowing of global variables)

  • 안쪽 블록의 변수가 바깥쪽 블록의 변수를 가리는 것과 똑같은 원리로, 전역 변수와 똑같은 이름을 가진 지역 변수를 만들면, 그 지역 변수가 유효한 범위 안에서는 전역 변수가 가려집니다.
  • 지역 변수 섀도잉과는 다르게, 전역 변수는 전역 네임스페이스에 속해 있기 때문에 가려진 상태에서도 접근할 방법이 하나 있습니다.
  • 바로 접두사 없이 범위 지정 연산자(Scope operator, ::)를 사용하는 것입니다.
  • 이렇게 하면 컴파일러에게 "내가 말하는 건 지역 변수가 아니라 전역 변수야!"라고 명확히 알려줄 수 있어요.

내부 링크 (Internal linkage)

  • 전역 변수와 함수 식별자는 내부 링크외부 링크 중 하나를 가질 수 있습니다.
  • 내부 링크를 가진 식별자는 오직 하나의 단일 번역 단위(보통 하나의 .cpp 소스 파일) 안에서만 볼 수 있고 사용할 수 있으며, 다른 번역 단위에서는 접근할 수 없어요.
  • 즉, 두 개의 서로 다른 소스 파일에 내부 링크를 가진 똑같은 이름의 식별자가 있더라도, 이 둘은 완전히 독립적인 것으로 취급된다는 뜻이랍니다.

내부 링크를 가진 전역 변수 (Global variables with internal linkage)

  • 내부 링크를 가진 전역 변수는 종종 내부 변수(Internal variables)라고도 불립니다.
  • 상수가 아닌 전역 변수를 내부 변수로 만들려면, static 키워드를 사용하면 됩니다.
  • constconstexpr 전역 변수는 기본적으로 내부 링크를 가집니다.
  • 따라서 굳이 static 키워드를 붙일 필요가 없어요.
#include <iostream>

static int g_x{}; // 상수가 아닌 전역 변수는 기본적으로 외부 링크를 가지지만, static 키워드를 통해 내부 링크를 부여할 수 있습니다.
const int g_y{ 1 }; // const 전역 변수는 기본적으로 내부 링크를 가집니다.
constexpr int g_z{ 2 }; // constexpr 전역 변수는 기본적으로 내부 링크를 가집니다.

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

내부 링크를 가진 함수 (Functions with internal linkage)

  • 앞서 말씀드렸듯이, 함수 식별자 역시 링크를 가집니다.
  • 함수는 기본적으로 외부 링크를 가지지만, static 키워드를 사용하면 내부 링크를 가지도록 설정할 수 있습니다.
  • 다른 파일에서 함수 전방 선언을 통해 접근하려고 시도하면 실패하게 됩니다.

왜 굳이 식별자에 내부 링크를 부여할까요?

  • 일반적으로 식별자에게 내부 링크를 부여하는 이유는 크게 두 가지가 있습니다.
  1. 다른 파일에서 접근하지 못하도록 확실히 차단하고 싶은 식별자가 있을 때. 함부로 값이 변경되면 안 되는 전역 변수나, 외부에서 몰래 호출되면 안 되는 헬퍼 함수(Helper function)가 좋은 예입니다.
  1. 이름 충돌(Naming collisions)을 철저하게 방지하기 위해서. 내부 링크를 가진 식별자는 링커(Linker)에 노출되지 않기 때문에, 프로그램 전체가 아니라 오직 같은 번역 단위 안에서만 이름이 충돌할 가능성이 존재합니다.
  • 많은 최신 개발 가이드에서는 "다른 파일에서 사용할 목적이 아닌 모든 변수와 함수에는 내부 링크를 부여하라"고 권장하고 있습니다.
  • 만약 이를 꾸준히 지킬 수 있다면 아주 훌륭한 프로그래밍 습관이 될 거예요.
  • 하지만 당장은 초보자분들을 위해 최소한의 가벼운 접근법을 추천해 드릴게요.
  • 다른 파일에서의 접근을 차단해야 할 명확한 이유가 있는 식별자에게만 내부 링크를 부여해 보세요.

외부 링크(External linkage)

  • 외부 링크를 가진 식별자는 정의된 파일뿐만 아니라, 전방 선언을 통해 다른 코드 파일에서도 보고 사용할 수 있습니다.

외부 링크를 가진 전역 변수

  • 외부 링크을 가진 전역 변수는 때때로 외부 변수(External variables)라고도 불립니다.
  • 전역 변수를 외부 변수로 만들어서 다른 파일에서도 접근할 수 있게 하려면, extern 키워드를 사용할 수 있어요.
  • 상수가 아닌 전역 변수는 기본적으로 외부 연결을 가지기 때문에, 굳이 extern으로 표시할 필요가 없습니다.
int g_x { 2 }; // 상수가 아닌 전역 변수는 기본적으로 외부 연결을 가집니다 (extern 키워드 불필요)
extern const int g_y { 3 }; // const 전역 변수는 extern으로 정의하여 외부 연결을 갖게 만들 수 있습니다
extern constexpr int g_z { 3 }; // constexpr 전역 변수도 extern으로 정의할 수 있습니다 (하지만 별로 유용하지 않아요. 다음 섹션의 경고를 참고해 주세요)

int main()
{
    return 0;
}

extern 키워드를 통한 변수 전방 선언

  • 다른 파일에 정의된 외부 전역 변수를 실제로 사용하려면, 해당 변수를 사용하려는 모든 파일에 변수의 전방 선언을 작성해야 합니다.
  • 변수의 경우, 전방 선언을 만들 때도 extern 키워드를 사용해요 (이때 초기화 값은 넣지 않습니다).
#include <iostream>

extern int g_x;       // 이 extern은 어딘가 다른 곳에 정의된 g_x라는 변수의 전방 선언입니다.
extern const int g_y; // 이 extern은 어딘가 다른 곳에 정의된 g_y라는 const 변수의 전방 선언입니다.

int main()
{
    std::cout << g_x << ' ' << g_y << '\n'; // 2 3을 출력합니다.

    return 0;
}
  • 그리고 이 변수들의 정의는 다음과 같습니다.
// 전역 변수 정의
int g_x { 2 };              // 상수가 아닌 전역 변수는 기본적으로 외부 연결을 가집니다.
extern const int g_y { 3 }; // 이 extern 키워드는 g_y에게 외부 연결을 부여합니다.

참고로 함수 전방 선언에는 extern 키워드가 필요하지 않습니다. 컴파일러는 함수 본문(Body)이 제공되는지 여부에 따라 새로운 함수를 정의하는 것인지, 아니면 전방 선언을 하는 것인지 쉽게 구별할 수 있거든요.

반면, 변수 전방 선언extern 키워드가 반드시 필요합니다. 왜냐하면 초기화되지 않은 변수 정의와 변수 전방 선언이 겉보기에는 똑같이 생겼기 때문에 이를 구별해 주어야 하거든요.

// 상수 아님 (Non-constant)
int g_x;        // 변수 정의 (초기화 없음)
int g_x { 1 };  // 변수 정의 (초기화 있음)
extern int g_x; // 전방 선언 (초기화 없음)

// 상수 (Constant)
extern const int g_y { 1 }; // 변수 정의 (const는 반드시 초기화가 필요합니다)
extern const int g_y;       // 전방 선언 (초기화 없음)

인라인 함수와 변수 (Inline functions and variables)

  • 사용자로부터 입력을 받거나, 파일에 무언가를 출력하거나, 특정 값을 계산하는 등 개별적인 작업을 수행하는 코드를 작성해야 하는 상황을 생각해 볼게요. 이런 코드를 구현할 때, 우리에게는 기본적으로 두 가지 선택지가 있습니다.
  1. 기존 함수 안에 그 코드를 직접 작성하는 방법 (이를 '제자리(in-place)' 또는 '인라인(inline)'으로 코드를 작성한다고 해요).
  2. 해당 작업을 처리할 새로운 함수(필요하다면 하위 함수들까지)를 만드는 방법.
  • 새로운 함수를 만들어서 코드를 분리하면 여러 가지 잠재적인 장점이 생겨요. 함수를 작게 만들면 다음과 같은 이점이 있거든요.
  1. 전체 프로그램의 맥락에서 코드를 읽고 이해하기가 훨씬 쉬워집니다.
  2. 함수는 본질적으로 모듈화되어 있기 때문에 재사용하기가 쉽습니다.
  3. 코드를 한 곳에서만 수정하면 되므로 업데이트하기가 편해집니다.
  • 하지만, 새로운 함수를 사용하는 것에도 단점은 있어요. 함수가 호출될 때마다 어느 정도의 성능 저하가 발생한다는 점이죠.
  • 이렇게 어떤 작업(이 경우에는 함수 호출)을 설정하고, 실행을 돕고, 끝난 뒤 정리하기 위해 추가로 발생하는 모든 작업과 비용을 오버헤드(Overhead) 라고 부릅니다.
  • 크기가 아주 작은 함수들의 경우, 함수 내부의 코드를 실행하는 시간보다 오버헤드로 인한 비용이 더 클 수도 있습니다.
  • 작은 함수가 아주 빈번하게 호출되는 상황이라면, 함수를 따로 만드는 것이 오히려 같은 코드를 제자리에 직접 쓰는 것보다 눈에 띄는 성능 저하를 가져올 수 있답니다.

인라인 확장 (Inline expansion)

  • 다행히도 C++ 컴파일러에는 이런 오버헤드 비용을 피할 수 있는 방법이 있어요.
  • 바로 인라인 확장 이라는 과정입니다.
  • 이는 함수 호출 부분을 호출된 함수의 실제 내부 코드로 통째로 바꿔치기하는 것을 말해요.

역사 속의 인라인 키워드

  • 과거에는 컴파일러들이 인라인 확장이 이득이 될지 스스로 판단할 능력이 없거나, 있더라도 성능이 썩 좋지 않았습니다.
  • 이 때문에 C++는 inline이라는 키워드를 제공했어요.
  • 이 키워드의 원래 목적은 개발자가 컴파일러에게 "이 함수는 인라인으로 확장하는 게 (아마도) 성능에 좋을 거야"라고 힌트를 주는 용도였답니다. inline 키워드를 사용해 선언된 함수를 인라인 함수(Inline function) 라고 부릅니다.
#include <iostream>

inline int min(int x, int y) // inline 키워드는 이 함수가 인라인 함수임을 의미해요
{
    return (x < y) ? x : y;
}

int main()
{
    std::cout << min(5, 6) << '\n';
    std::cout << min(3, 2) << '\n';
    return 0;
}
  • 하지만 현대의 C++에서는 더 이상 함수를 인라인 확장해 달라고 요청하기 위해 inline 키워드를 사용하지 않습니다.

현대적인 의미의 인라인 키워드 (The inline keyword, modernly)

  • 현대의 C++에서 inline이라는 용어는 진화하여 "이 함수는 여러 번 정의해도 허용됩니다" 라는 뜻을 가지게 되었습니다.
  • 즉, 인라인 함수는 (ODR을 위반하지 않으면서) 여러 번역 단위에 정의될 수 있도록 특별히 허락받은 함수예요.
  • 인라인 함수가 지켜야 할 주요 요구 사항은 두 가지입니다.
  1. 컴파일러는 인라인 함수가 사용되는 각 번역 단위마다 그 함수의 '전체 정의'를 볼 수 있어야 합니다.
    단, 한 번역 단위 내에서는 하나의 정의만 존재해야 합니다. 그렇지 않으면 컴파일 오류가 납니다.

  2. (기본적으로 함수가 가지는) 외부 연결성을 가진 인라인 함수에 대한 모든 정의는 완전히 동일해야 합니다.
    조금이라도 다르면 정의되지 않은 동작이 발생해요.

  • 링커는 각 파일에 퍼져 있는 인라인 함수의 정의들을 모아서 단 하나의 정의로 합쳐줍니다.
  • 그래서 결과적으로 단일 정의 규칙을 만족시키게 되죠.
//main.cpp
#include <iostream>

double circumference(double radius); // 전방 선언(Forward declaration)

inline double pi() { return 3.14159; }

int main()
{
    std::cout << pi() << '\n';
    std::cout << circumference(2.0) << '\n';

    return 0;
}
//math.cpp
inline double pi() { return 3.14159; }

double circumference(double radius)
{
    return 2.0 * pi() * radius;
}
  • 두 파일 모두에 pi() 함수가 정의되어 있는 것을 눈여겨보세요.
  • 하지만 이 함수는 inline으로 표시되어 있기 때문에 문제가 되지 않고, 링커가 알아서 중복을 제거해 줍니다.
  • 만약 두 곳의 pi() 정의에서 inline 키워드를 지워버린다면, 인라인이 아닌 함수를 중복 정의한 것이 되므로 ODR 위반 에러가 발생할 거예요.
  • 인라인 함수는 일반적으로 헤더 파일에 정의합니다.
  • 그래야 함수의 전체 정의를 알아야 하는 모든 코드 파일의 상단에 쉽게 #include 할 수 있으니까요.
  • 이 방식을 쓰면 인라인 함수의 모든 정의가 완벽하게 똑같다는 보장도 할 수 있습니다.
  • C++17부터는 인라인 변수(Inline variables) 라는 개념이 도입되었습니다.
  • 인라인 함수처럼 여러 파일에서 여러 번 정의할 수 있도록 허락된 변수들이죠.
  • 인라인 변수도 인라인 함수와 똑같은 요구 사항을 갖습니다

여러 파일에서 전역 상수 공유하기 (인라인 변수 사용)

  • 프로그래밍을 하다 보면, 어떤 기호 상수들은 단 한 곳이 아니라 코드 전체에서 두루 사용되어야 할 때가 있습니다.
  • 변하지 않는 물리나 수학 상수 원주율 파이(pi) 아보가드로 수 가 될 수도 있고, 특정 프로그램에 맞춰진 "조정용" 값 마찰 계수 중력 계수 이 될 수도 있죠.
  • 이러한 상수가 필요할 때마다 매번 새로운 파일에 다시 정의하는 것은 중요한 프로그래밍 원칙인 DRY를 위반하는 것입니다.
  • 대신, 중앙의 한 곳에 한 번만 선언하고 필요한 곳 어디서든 가져다 쓰는 것이 훨씬 좋은 방법입니다.

인라인 변수로 전역 상수 사용하기 (C++17 이상)

  • 인라인 변수는 모든 정의가 동일하기만 하다면 여러 번 정의되는 것을 허용하는 특별한 변수입니다.
  • 우리의 constexpr 변수들을 inline으로 만들면, 헤더 파일에 한 번 정의해 두고 필요한 모든 .cpp 파일에 자유롭게 #include 할 수 있습니다. 이 방법을 사용하면 ODR 위반도 피하고, 변수가 불필요하게 중복 복사되는 단점도 해결할 수 있습니다!

정적 지역 변수 (Static local variables)

  • 지역 변수는 기본적으로 자동 지속 기간을 가진다고 배웠습니다.
  • 변수가 정의되는 시점에 생성되고, 그 변수가 속한 블록을 벗어나면 자동으로 소멸된다는 의미예요.
  • 하지만 지역 변수에 static 키워드를 사용하면, 그 지속 기간이 자동에서 정적(Static)으로 바뀝니다.
  • 이렇게 되면 지역 변수는 마치 전역 변수처럼 프로그램이 시작될 때 생성되고, 프로그램이 끝날 때 소멸하게 돼요.
  • 그 결과, 이 정적 변수는 자신의 범위를 벗어나더라도 예전 값을 잃어버리지 않고 계속 기억하게 됩니다.
#include <iostream>

void incrementAndPrint()
{
    static int s_value{ 1 }; // static 키워드를 통해 정적 지속 기간을 가집니다. 이 초기화 코드는 단 한 번만 실행됩니다.
    ++s_value;
    std::cout << s_value << '\n';
} // s_value는 여기서 소멸되지 않지만, 범위를 벗어나므로 접근할 수는 없게 됩니다.

int main()
{
    incrementAndPrint();
    incrementAndPrint();
    incrementAndPrint();

    return 0;
}
  • 이 코드에서 s_valuestatic으로 선언되었기 때문에 프로그램이 시작될 때 한 번만 생성됩니다.
    • 상수 표현식 초기화 값을 가진 정적 지역 변수는 프로그램 시작 시에 초기화될 수 있습니다.
    • 초기화 값이 없거나 상수 표현식이 아닌 초기화 값을 가진 정적 지역 변수는 프로그램 시작 시 영 초기화됩니다.
    • 이후 해당 변수의 정의를 처음 마주칠 때 다시 초기화되죠.
    • 이후에 함수가 호출될 때는 이 정의 부분을 건너뛰기 때문에 더 이상 초기화가 발생하지 않습니다.
    • 정적 지속 기간을 가지므로, 명시적으로 초기화하지 않으면 기본적으로 0으로 초기화된답니다.
  • 여기서 s_value1이라는 상수 표현식 초기화 값을 가지기 때문에 프로그램 시작 시점에 초기화됩니다.

핵심 통찰 (Key insight)

  • 정적 지역 변수는 여러 번의 함수 호출 사이에서도 지역 변수의 값을 기억해야 할 때 사용합니다.
  • 정적 지역 변수는 꼭 초기화해 주세요.
  • 정적 지역 변수는 전체 프로그램 동안 한 번만 초기화되며, 이후 함수 호출에서는 초기화되지 않고 기존 값을 유지합니다.

정적 지역 상수 (Static local constants)

  • 정적 지역 변수도 const constexpr로 선언할 수 있습니다.
  • const 정적 지역 변수가 유용하게 쓰이는 대표적인 경우는, 함수에서 어떤 상수 값을 써야 하는데 그 객체를 생성하거나 초기화하는 비용이 매우 클 때입니다.
  • 일반 지역 변수를 사용했다면 함수가 실행될 때마다 변수를 만들고 초기화해야 했을 것입니다.
  • 하지만 const/constexpr 정적 지역 변수를 사용하면, 비용이 많이 드는 객체를 딱 한 번만 생성해서 초기화한 후, 함수가 호출될 때마다 계속 재사용할 수 있습니다.

using 선언 (Using-declarations)

  • std::를 반복해서 타이핑하는 수고를 덜어주는 첫 번째 방법은 using 선언문을 활용하는 것입니다.
  • using 선언은 우리가 스코프가 없는 비한정 이름을 마치 한정된 이름의 별칭처럼 사용할 수 있게 해줍니다.
  • using 선언은 선언된 지점부터 해당 선언이 포함된 스코프가 끝날 때까지 활성화됩니다.
#include <iostream>

int main()
{
   using std::cout; // 이 using 선언은 컴파일러에게 cout이 std::cout을 의미한다고 알려줍니다.
   cout << "Hello world!\n"; // 따라서 여기서는 std:: 접두사가 필요하지 않아요!

   return 0;
} // 이 using 선언은 현재 스코프가 끝나는 시점에서 효력이 만료됩니다.

using 지시자 (Using-directives)

  • using 지시자는 특정 네임스페이스 안에 있는 모든 식별자를 using 지시자가 있는 스코프 내에서 한정자 없이 사용할 수 있게 해줍니다.
#include <iostream>

int main()
{
   using namespace std; // 이제 std 네임스페이스의 모든 이름을 한정자 없이 접근할 수 있습니다.
   cout << "Hello world!\n"; // 따라서 여기서는 std:: 접두사가 필요하지 않아요.

   return 0;
} // 이 using 지시자는 현재 스코프가 끝나는 시점에서 만료됩니다.
  • using namespace std; 라는 지시자는 컴파일러에게 std 네임스페이스 안의 모든 이름들을 현재 스코프(이 경우 main() 함수 내부)에서 한정자 없이 접근할 수 있도록 하라고 지시합니다.

using 문의 스코프 (The scope of using-statements)

  • 만약 using 선언이나 using 지시자가 어떤 블록 {} 안에서 사용되었다면, 그 이름들은 오직 해당 블록 안에서만 유효합니다
  • 반면, 네임스페이스 안에서(전역 네임스페이스 포함) 사용되었다면, 그 이름들은 해당 파일의 나머지 전체 부분에 적용됩니다 (파일 스코프를 가집니다).

헤더 파일이나 #include 지시자 이전에 using 문을 사용하지 마세요!

  • 헤더 파일 안에서나 #include 지시자 앞에서는 절대로 using 문을 사용해서는 안 됩니다.
  • 만약 여러분이 헤더 파일의 전역 네임스페이스에 using 문을 넣는다면, 그 헤더를 #include 하는 모든 다른 파일들도 강제로 그 using 문을 갖게 됩니다.
  • 헤더 파일 내부의 네임스페이스 안에 넣는 것도 정확히 같은 이유로 피해야 합니다.

이름 없는 네임스페이스

  • 이름 없는 네임스페이스에 선언된 모든 내용은 마치 부모 네임스페이스의 일부인 것처럼 취급됩니다.
  • 부모 네임스페이스에 전역 네임스페이스가 포함됩니다.
  • 네임스페이스의 진짜 중요한 효과는, 그 안의 모든 식별자가 내부 연결을 가진 것처럼 취급된다는 점입니다.
profile
안녕하세요.

0개의 댓글