· C++ 공부 1일차 - C++ 기초: 함수와 파일 - 챕터 1 & 2
· C++ 공부 2일차 - 기본 데이터 유형 - 챕터 4
· C++ 공부 3일차 - 상수 변수 - 챕터 5
· C++ 공부 4일차 - 연산자 - 챕터 6
· C++ 공부 5일차 - 비트 조작 - 챕터 O
· C++ 공부 6일차 - 유효 범위, 수명, 그리고 연결 - 챕터 7
복합문(Compound statement)
• 복합문(Compound statement), 또는 블록(Block)이라고 부르는 것은 0개 이상의 문장들을 하나로 묶은 그룹을 말해요.
• 아주 중요한 점은, 컴파일러(Compiler)가 이 묶음을 마치 하나의 문장인 것처럼 취급한다는 거예요.
• 우리가 함수를 만들 때 이미 블록을 사용해 왔답니다.
• 함수의 몸체(Body)가 바로 블록이니까요.
• 함수 안에 또 다른 함수를 정의할 수는 없지만, 블록 안에 또 다른 블록을 넣는 것은 가능해요. 이걸 '중첩된다'라고 표현하죠.
• 블록이 중첩되었을 때, 바깥쪽을 감싸고 있는 블록을 외부 블록(Outer block)이라 하고,
• 그 안에 들어있는 블록을 내부 블록(Inner block) 혹은 중첩 블록(Nested block)이라고 부릅니다.
조건에 따라 여러 문장 실행하기
• 기본적으로 if 문은 조건이 '참(True)'일 때 딱 하나의 문장만 실행하게 되어 있어요.
• 하지만 조건이 맞았을 때 여러 가지 일을 한꺼번에 처리하고 싶다면 어떻게 해야 할까요?
• 이때 그 '하나의 문장' 자리에 문장들의 묶음인 '블록'을 넣어주면 된답니다.
블록 중첩 레벨 (Nesting levels)
• 블록 안에 블록을 넣고, 그 안에 또 블록을 넣는 것도 가능할까요?네, 가능합니다!
• 여기서 중첩 레벨(Nesting level) 혹은 중첩 깊이(Nesting depth)라는 용어가 나와요.
• 이건 함수 내의 어떤 지점에서 '지금 내가 최대 몇 개의 블록 안에 감싸여 있는가'를 나타내는 숫자예요.
• C++ 표준(Standard)에 따르면 컴파일러는 무려 256단계의 중첩까지 지원해야 한다고 해요.
• 하지만 모든 컴파일러가(예: 작성 시점 기준 Visual Studio 등) 이를 완벽히 지원하는 건 아닐 수 있어요.
• 기술적으로 가능하다고 해서 깊게 만드는 게 좋을까요?
• 아닙니다. 중첩 레벨은 3단계 이하로 유지하는 것이 좋습니다.
• 함수의 길이가 너무 길어지면 여러 개의 작은 함수로 나누는 '리팩토링(Refactoring)'을 하듯이,
• 블록의 중첩이 너무 깊어지면 코드를 읽기가 아주 힘들어져요.
• 이럴 때도 가장 깊은 곳에 있는 블록들을 떼어내어 별도의 함수로 만드는 리팩토링을 하는 것이 좋습니다.
나만의 네임스페이스 정의하기
• C++에서는 namespace라는 키워드를 사용해 우리만의 네임스페이스를 만들 수 있어요.
• 이렇게 여러분이 직접 만든 네임스페이스를 흔히 사용자 정의 네임스페이스(User-defined namespaces)라고 불러요.
• 더 정확히는 '프로그램 정의 네임스페이스'라고 하는 게 맞겠지만요.
• 문법은 아주 간단해요.
• namespace 키워드를 쓰고, 그 뒤에 네임스페이스의 이름(식별자)을 적은 다음, 중괄호 { } 안에 내용을 넣으면 끝이에요.
namespace NamespaceIdentifier
{
// 네임스페이스의 내용이 여기에 들어갑니다
}
• 역사적으로 네임스페이스 이름은 소문자로 쓰는 게 관례였고, 많은 스타일 가이드가 여전히 그걸 권장해요.
• 하지만 최근의 추세는 조금 다릅니다.
네임스페이스 이름을 대문자로 시작하는 것을 선호하는 이유
• 사용자가 정의한 '타입(Type)'들은 대문자로 시작하는 게 관례예요.
네임스페이스도 똑같이 대문자로 시작하면 일관성이 생기죠.
(특히 Foo::x처럼 쓸 때 Foo가 네임스페이스인지 클래스인지 헷갈리지 않게 해줘요.)
• 시스템이나 라이브러리가 제공하는 소문자 이름들과 겹치는 걸 막아줘요.
• C++20 표준 문서와 C++ Core 가이드라인 문서에서도 이 스타일을 사용하고 있어요.
범위 지정 연산자 (::)로 네임스페이스 접근하기
• 특정 네임스페이스 안에 있는 식별자를 찾으라고 컴파일러에게 지시하는 가장 좋은 방법은 범위 지정 연산자(::)를 사용하는 거예요.
std::cout << Goo::doSomething(4, 3) << '\n'; // Goo 네임스페이스에 있는 doSomething()을 사용
이름 없이 범위 지정 연산자 사용하기 (::identifier)
• 범위 지정 연산자 앞에 아무런 네임스페이스 이름도 적지 않을 수도 있어요 (예: ::doSomething).
• 이렇게 하면 컴파일러는 전역 네임스페이스(Global Namespace)에서 그 이름을 찾으라는 뜻으로 이해합니다.
네임스페이스 내부에서의 식별자 찾기
• 네임스페이스 안에서 어떤 식별자를 사용했는데, 앞에 범위 지정(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)
• 헤더 파일을 사용해서 전방 선언을 할 때도 주의할 점이 있어요.
• 네임스페이스 안에 있는 식별자를 전방 선언하려면,
• 선언도 똑같은 네임스페이스 안에서 해줘야 해요.
여러 개의 네임스페이스 블록
• 같은 이름의 네임스페이스 블록을 여러 곳(여러 파일 또는 같은 파일 내 여러 곳)에 나눠서 작성해도 괜찮아요.
• 모두 같은 네임스페이스의 가족으로 취급됩니다.
중첩된 네임스페이스 (Nested namespaces)
• 네임스페이스 안에 또 다른 네임스페이스를 만들 수도 있어요.
• C++17부터는 이렇게 중첩된 네임스페이스를 훨씬 간단하게 선언할 수 있어요.
• Goo가 Foo 안에 있으니까 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챕터 — 지역 범위(Local scope) 소개에서 우리는 지역 변수(Local variable)에 대해 배웠습니다.
• 함수 내부(함수의 매개변수 포함)에 정의된 변수들을 말하죠.
• 우리는 2.5강에서 범위(Scope)라는 개념도 배웠어요.
• 어떤 식별자(변수 이름 등)의 범위(Scope)란 소스 코드 내에서 그 식별자에 접근할 수 있는 영역을 뜻해요.
• 접근할 수 있다면 "범위 내에 있다(In scope)"라고 하고,
• 접근할 수 없다면 "범위를 벗어났다(Out of scope)"라고 합니다.
• 범위는 컴파일 타임(Compile-time)에 결정되는 속성이라서, 범위를 벗어난 식별자를 사용하려고 하면 컴파일 에러가 발생해요.
지역 변수는 블록 범위(Block Scope)를 가집니다
• 지역 변수는 블록 범위(Block scope)를 가집니다.
• 즉, 변수가 정의된 지점부터 그 변수가 포함된 블록이 끝나는 지점까지만 유효하다는 뜻이에요.
지역 변수는 자동 저장 기간(Automatic Storage Duration)을 가집니다
• 변수의 저장 기간(Storage duration)은 변수가 언제 생성되고(메모리에 할당되고) 언제 소멸될지를 결정하는 규칙이에요.
• 대부분의 경우, 이 저장 기간이 변수의 수명(Lifetime)을 결정합니다.
• 지역 변수는 자동 저장 기간(Automatic storage duration)을 가집니다.
• 쉽게 말해, 변수가 정의되는 시점에 자동으로 생성되고, 정의된 블록이 끝날 때 자동으로 소멸된다는 뜻이죠.
• 이런 이유로 지역 변수를 자동 변수(Automatic variable)라고 부르기도 해요.
지역 변수는 연결(Linkage)이 없습니다
• 식별자(변수 이름 등)에는 연결(Linkage)이라는 또 다른 속성이 있어요.
• 어떤 식별자의 연결이란, 다른 범위에서 동일한 이름으로 선언된 식별자가 '같은 객체(또는 함수)'를 가리키는지 아닌지를 결정하는 성질이에요.
• 지역 변수는 연결이 없습니다(No linkage). 즉, 연결이 없는 식별자는 이름이 같더라도 각각의 선언이 서로 다른 고유한 객체나 함수를 의미합니다.
변수는 가장 제한적인 범위에서 정의해야 합니다
• 변수가 특정 중첩 블록 안에서만 사용된다면, 그 변수는 반드시 그 중첩 블록 안에서 정의해야 합니다.
• 변수의 범위를 제한하면 활성화된 변수의 수가 줄어들어 프로그램의 복잡도가 낮아집니다.
• 블록 안에 정의된 변수는 그 블록(과 그 내부 블록) 안에서만 영향을 미치므로 프로그램을 이해하기가 더 수월해집니다.
#include <iostream>
int main()
{
// 여기에 y를 정의하지 마세요.
{
// y는 오직 이 블록 안에서만 사용되므로, 여기서 정의합니다.
int y { 5 };
std::cout << y << '\n';
}
// 그렇지 않으면 y가 필요 없는 이 곳에서도 y를 사용할 수 있게 됩니다.
return 0;
}
댓글 0