
복합 명령문
복합 명령문이란 하나의 단일 명령문인 것처럼 처리되는 명령문의 그룹이다
{ }으로 구성되어 있으며 안에 명령문들이 배치된다, 또한 { }의 뒤에는 ;이 붙지 않는다
ex) 함수에서의 { }, if문에서의 { }
void foo()
{
}
함수는 다른 함수 안에서 중첩될 수 없지만 { }자체는 중첩이 가능하다 (계속 중첩 가능 ex) 2중 if()문)
int main()
{
int a{ 10 };
{
int a{ 20 };
}
}
이러한 복합 명령문이 가장 많이 사용되는 곳은 바로 if문이다
물론 이러한 복합 명령문이 아주 많이 중첩되도 컴파일에는 전혀 지장이 없지만 3단계 이하로 유지하는것이 좋다 (early return 등을 이용하여 중첩 단계를 낮추기, 함수 리팩토링이 필요)
명명충돌
앞에서 정리했듯 식별자 명명 충돌은 같은 범위에서 동일한 식별자 이름을 사용했을 때 컴파일러가 해당 식별자명을 사용했을때 어떤 식별자를 사용할 지 모호할 때 발생한다 (LNK error)
이러한 명명충돌은 프로젝트가 커질수록 더 자주 발생할 수 밖에 없다
대표적인 예로 내가 foo()라는 함수를 만들었는데 third party library에 foo()라는 같은 식별자명을 쓰는 함수가 존재하면 이는 LNK에러로 이어진다
//foo.cpp
void foo()
{
}
//goo.cpp
void foo()
{
}
//main.cpp
void foo(); //forward declaration
int main()
{
foo(); //LNK error
}
컴파일러 입장에서는 foo.cpp, goo.cpp 어디에 있는 foo()를 사용해야 할 지 모호하기 때문에 LNK 에러가 발생한다
이러한 현상을 방지하기 위해서 namespace를 만들어 사용한다
namespace
사용자 정의 namespace는 다음과 같은 방식으로 만들어 사용한다
namespace 이름
{
}
namespace의 이름은 대문자로 작성하는것이 좋다 (C++20 표준이기도 하고 클래스와 같은 사용자 정의 타입은 대문자로 시작하는게 관례이다, 또한 System이나 Library의 소문자 이름들과의 충돌 가능성을 줄인다)
namespace는 전역이나 다른 namespace 내부에서 정의되어야 한다, 함수와 마찬가지로 내용은 들여쓰기하여 작성한다
그렇다면 위 코드에서 각각의 namespace Foo, Goo를 만들고 내부에 함수를 넣으면 어떨까?
//Foo.h
namespace FooNameSpace
{
int Foo();
}
//Foo.cpp
int FooNameSpace::Foo()
{
return 0;
}
//Goo.h
namespace GooNameSpace
{
int Foo();
}
//Goo.cpp
int GooNameSpace::Foo()
{
return 0;
}
//main.cpp
int Foo();
int main()
{
Foo();
return 0;
}
결과는 똑같이 LNK error가 발생한다, 왜냐하면 전역범위에서 해당 foo()의 정의를 링커가 찾을 수 없기 때문이다 (전역 범위에서 namespace 범위로 들어갔기 때문)
따라서 ::인 범위 지정 연산자를 이용한다 (물론 namespace가 정의된 file을 include 해야 한다)
Foo::foo();
이는 Foo라는 namespace안에 있는 foo()함수를 호출하고 싶다고 컴파일러에게 알리는 것이다
직접적으로 어떤 namespace안에 어떤것을 사용하고 싶다고 알리기 때문에 모호함이 없어 굉장히 좋은 방식이다
이때 앞에 namespace이름을 사용하지 않는다면 전역 범위에 있는 식별자를 사용한다는 의미가 된다
::foo(); //전역 범위에 정의된 foo()를 호출
namespace 안에 함수를 정의할 때 전방선언도 같은 namespace 안에 해주어야 한다
//Add.h
namespace Foo
{
int foo(int a, int b);
}
//Add.cpp
namespace Foo
{
int foo(int a, int b)
{
return a + b;
}
}
//or
int Foo::foo(int a, int b)
{
return a + b;
}
//main.cpp
#include "Add.h" //헤더 include 필요
int main()
{
Foo::foo(10, 20);
}
같은 namespace를 각각 다른 file에서 정의할 수 있다 (단 namespace 내부의 식별자들의 이름이 같으면 LNK에러 발생)
//foo.h
namespace Test
{
constexpr int a{ 10 };
}
//goo.h
namespace Test
{
constexpr int b{ 20 };
}
//main.cpp
#include "foo.h"
#include "goo.h"
int main()
{
Test::a;
Test::b;
}
이러한 규칙은 namespace std가 아주 잘 보여주고 있다, 방대한 기능의 std의 모든 내용을 단일 파일에서 관리할 수 없기 때문에 다른 파일들에서 같은 namespace인 std가 정의된 걸 확인할 수 있다
(std namespace에는 사용자 정의 기능 추가 금지)
namespace안에는 복합 명령문으로 다른 namespace가 중첩될 수 있다
namespace Foo
{
namespace Goo
{
int a{ 10 };
}
}
Foo::Goo::a;
//C++17
namespace Foo::Goo
{
int a{ 10 };
}
namespace Foo
{
int c{ 30 };
}
이렇게 중첩이 되면 될 수록 namespace 내부의 식별자를 사용할 때 굉장히 길어지게 되는데 이를 namespace aliases(별칭)를 사용하여 줄일 수 있다
Foo::Goo::a;
namespace Test = Foo::Goo;
Test::a;
이렇게 하면 namespace의 중첩 구조가 변경된다 해서 namespace aliases만 변경하면 사용된 코드들 전부를 쉽게 변경할 수 있는 장점이 있다
이런 namespace는 명명 충돌을 방지하고 모듈화에도 사용하기 좋다 ex) math module 제작
local variable
지역변수는 { }범위를 가진다, 지역변수의 수명이 해당 { }범위라는 의미이다
int main()
{
int a{ 10 };
int b{ 20 };
return;
}
이때 함수의 매개변수는 함수의 본문에 정의되어 있지 않지만 함수 본문 { }를 범위로 본다
int test(int a, int b)
{
//a, b 매개변수의 범위
}
같은 scope내의 변수들의 이름은 고유해야 한다 (같으면 모호해 지기 때문에 에러 발생)
따라서 매개변수와 지역변수의 이름도 서로 달라야 한다 (둘이 같은 scope임)
void someFunction(int x)
{
int x{}; //error
}
이러한 지역변수들은 { }를 생성(인스턴스화)되고 파괴되는 수명으로 가진다
(자동)
위에서 설명했듯 중첩 scope에서도 지역변수를 정의할 수 있다
int foo()
{
int a{ 10 };
{
int a{ 20 };
}
}
마찬가지로 { }가 각 지역변수의 범위이자 수명이 된다, 따라서 같은 foo() { }에 있지만 a는 서로 다른 { }에 존재하기 때문에 식별자명이 같아도 에러가 발생하지 않는다
내부에 중첩된 { }에서는 외부의 블록에 있는 데이터에 접근이 가능하다 (역은 성립하지 않는다)
지역 변수는 굉장히 제한된 범위에서 정의하는것이 좋다 (최대한 사용하는곳 바로 위, 그리고 제한되게 정의한다)
int main()
{
{
int x{ 10 };
std::cout << x < '\n';
}
}
scope를 잘 관리해서 지역변수를 사용하면 Active한 변수의 수가 줄어들기 때문에 프로그램의 복잡도가 낮아진다, 또한 언제 어디서 변수를 사용하는지 알아보기 쉽다
global variable
{ } scope 밖에서 선언된, global namespace에 선언된 변수를 전역변수라고 한다
이때 관례적으로 global variable은 파일의 #include 밑에 선언한다
#include <iostream>
int foo{ 10 }; //global variable
int main()
{
}
전역변수의 범위는 global namespace의 범위를 가진다, 한마디로 모든 file에서 전부 접근이 가능하다는 의미이다
namespace Foo
{
int a{}; //namespace 내부에 선언되었어도 전역변수이다
}
int main()
{
Foo::a = 100;
}
전역변수는 수명으로 static duration을 가진다, 프로그램이 시작될 때 생성되고 main()이 호출되고 종료될 때 소멸한다
전역변수 네이밍에 g prefix를 붙히는 경우도 있는데 해당 조직의 rule에 맞춰 사용하자 (쓰는게 반드시 좋고 안쓰는게 좋다고 판단할 수 없음, 물론 global variable을 나타내기에는 좋다)
지역변수는 선언 후 초기화 하지 않으면 해당 메모리 주소에 남아있는 쓰레기값이지만 전역변수는 초기화 하지 않아도 0으로 초기화 된다
단 전역변수도 일반 변수와 마찬가지로 상수로 선언이 가능하다, 이때는 반드시 초기화를 해주어야 한다
const int x{ 10 };
constexpr int ex{ 20 };
그렇다면 지역변수와 전역변수의 이름이 같으면 어떻게 될까?
int a{ 10 };
void foo()
{
std::cout << a << '\n'; //10
}
int main()
{
int a{ 20 };
std::cout << a << '\n'; //20
std::cout << ::a << '\n'; //10
}
이름이 같다면 사용한 식별자가 자신의 scope에 존재하는지 확인하고 존재한다면 그 식별자를 사용한다, 만약 존재하지 않는다면 전역변수를 사용한다, 이때 ::을 이용하여 global namespace의 식별자인 전역 변수를 명시적으로 사용할 수 있다
하지만 일단 이렇게 전역과 지역변수의 이름이 동일해지는 상황을 피해야한다, 잘못된 변수의 사용 및 수정이 발생할 수 있기 때문이다 (이래서 전역변수에 g_ prefix를 쓰는 사람이 있음)
이러한 전역변수는 프로그램 언제 어디서나 사용이 가능하며 수명이 유지되기 때문에 굉장히 편하다고 생각할 수 있지만 비상수 전역변수의 사용은 피해야 한다
Internal Linkage
Internal Linkage란 변수나 함수가 링크 시점에서 정의된 파일 내부에서만 링크 가능할 때 사용하는 개념이다
(링크 단계에서 외부 파일에서 링크 불가, 오직 선언/정의된 파일 내부에서만 링크 가능, 다른 파일에서 식별자 사용 시 링커에 표시가 안됨)
상수가 아닌 전역변수를 Internal Linkage하게 만들기 위해서는 static 키워드를 사용한다
(상수인 전역변수(const, constexpr)는 default Internal Linkage임, 비상수 전역변수는 default가 external linkage임)
//foo.cpp
static int a{ 10 };
//main.cpp
int a{ 20 };
int main()
{
std::cout << a << '\n';
}
원래 위 코드에서 static int a가 아닌 int a 전역변수라면 LNK 에러가 발생한다, 하지만 static 전역 변수로 만들어 Internal Linkage하게 되었기 때문에 static int a는 foo.cpp에서만 접근이 가능해진다, 따라서 식별자 사용에 있어서 모호함이 없어지기 때문에 LNK에러가 발생하지 않는다
이러한 static, extern, mutable과 같은 키워드를 Storage Class Specifier (저장소 클래스 지정자)라고 한다
왜 상수 변수는 default로 Internal Linkage일까?
C++11에서 해당 의문에 대한 근거를 제시한다, 상수 변수는 컴파일 타임에 평가될 수 있기 때문에 초기값이 반드시 필요하다, 이때 컴파일러가 상수값을 사용하려면 변수의 선언으로는 충분하지 않고 정의를 볼 수 있어야 한다
const변수는 주로 헤더파일에 정의되고 다른 파일에서 사용된다, 이때 Internal Linkage로 다른 파일에서 링크 시점에 해당 변수에 링크가 불가능하기 때문에 참조하는 각 소스파일이 독립적으로 const변수의 사본을 생성하여 충돌없이 동작한다 (ODR위배 X, Interanl Linkage 전역 변수를 #include하게 되면 각각 독립적인 변수가 생성되고 링크되기 때문에 원본 전역 변수의 수정이 발생하지 않는다)
하지만 C++17에서는 inline 키워드가 도입되어 const도 external linkage로 사용할 수 있다 (사본 생성 없이 동일한 const변수를 링크 단계에서 외부 파일에서 링크가 가능해짐, ODR도 위반하지 않음)
함수는 default로 External Linkage이다, 하지만 변수와 마찬가지로 static 키워드를 사용하여 Internal Linkage로 사용할 수 있다
//foo.cpp
static int foo(int a, int b)
{
return a + b;
}
//main.cpp
int foo(int, int); //forward declaration
int main()
{
foo(10, 20); //LNK error
}
foo.cpp의 foo()가 static function이기 때문에 Internal Linkage 속성을 가진다, 따라서 main.cpp에서 전방선언을 하더라도 Link하지 못한다
사실 static을 사용하여 Internal Linkage 속성을 부여하는 방식은 별로 선호되지 않는다, unnamed-namespace를 사용하여 더 광범위한 식별자에 Internal Linkage 속성 부여가 가능하여 더 적합하다
namespace
{
int a{};
int foo(int input1, int input2)
{
return input1 + input2;
}
}
이 unnamed-namespace의 a, foo는 링크단계에서 해당 파일 내부에서만 링크가 가능하다
static 키워드를 통해 Internal Linkage속성을 부여하려면 모든 변수들마다 static 키워드를 사용해야 하며 static은 변수나 함수에만 사용할 수 있지만 unnamed-namespace는 class ,struct 등 다양한 곳에 사용이 가능하다
왜 Internal Linkage 속성이 필요할까?
명명충돌을 방지하며 캡슐화 및 은닉성을 향상시킨다
이러한 방식으로 식별자의 범위를 제한하는 방식은 modern C++에서 권장한다