5. [C++] 이름공간(namespace)

2024. 4. 11. 13:36[C++]/C++ 언어 기초

🍃 이름 공간의 등장 배경

프로그램이 대형화되어 가면서 이름의 충돌문제가 등장했다.

세 개의 회사에서 은행관리 시스템을 개발하는데, 이들은 일을 구분하여 독립적으로 진행하고 6개월 뒤 모여서 하나의 프로젝트를 완성하기로 약속했다.

 

6개월이라는 시간이 지난 뒤, 각 회사가 구현한 모듈을 하나로 묶고 부족한 부분을 완성할 때가 되었다. 그러나 문제가 생겼다. 1번 회사에서 정의한 함수와 2번 회사에서 정의한 함수의 이름이 같이 "이름 충돌"이 난 것이다.

함수의 이름을 변경하기에는 시간이 부족하다. 게다가 3번 회사에서 정의한 이름까지 충돌이 나게 되었다.

  • 그렇다면 무엇이 해결책이었을까?
  • 프로젝트 진행 전 함수 및 변수의 이름을 모두 정해 이름충돌이 발생하지 않게 하는 것이 해결책인가?
  • 이는 근본적인 해결책이 되지 못한다.

그래서 C++의 표준에서는 '이름공간(namespace)' 이라는 문법을 정의해 이에 대한 근본적인 해결책을 제시한다.

 

 

🍃이름공간의 기본원리

  • 한 집에 철수라는 이름을 가진 사람이 두 명 산다면, 철수야!하고 불렀을 때 누구를 부르는지 모른다.
  • 하지만 202호에 사는 철수야! 라는 식으로 구분을 지어준다면 문제가 발생하지 않는다.
  • 이것이 이름공간의 기본 원리이다.

namespace1.cpp

#include <iostream>

namespace BestComImpl{
    void SimpleFunc(void) {
        std::cout<<"BestCom이 정의한 함수"<<std::endl;
    }
}

namespace ProgComImpl{
    void SimpleFunc(void) {
        std::cout<<"ProgCom이 정의한 함수"<<std::endl;
    }
}

int main(void) {
    BestComImpl::SimpleFunc();
    ProgComImpl::SimpleFunc();
    return 0;
}

 

위의 예제를 보면 아래와 같이 이름공간을 마련한 것을 볼 수있다.

namespace BestComImpl{
    void SimpleFunc(void) {
        std::cout<<"BestCom이 정의한 함수"<<std::endl;
    }
}

위를 보면 BestComImpl이라는 이름공간을 마련했고, 이 안에 함수 SimpleFunc를 정의했다. 따라서 이 함수는 'BestComImpl::SimpleFunc()'이라고 지칭한다.

namespace ProgComImpl{
    void SimpleFunc(void) {
        std::cout<<"ProgCom이 정의한 함수"<<std::endl;
    }
}

위를 보면 ProgComImpl이라는 이름공간을 마련했고, 이 안에 함수 SimpleFunc를 정의했다. 따라서 이 함수는 'ProgComImpl::SimpleFunc()'이라고 지칭한다.

 

 

예제에서 사용된 연산자 :: 을 가리켜 '범위지정 연산자(scope resolution operator)'라 한다.

 

 

🍃 이름공간 기반의 함수 선언과 정의의 구분

  • 함수는 선언과 정의를 분리하는 것이 일반적이다.
  • '함수의 선언'은 헤더파일에, '함수의 정의'는 소스파일에 저장하는 것이 일반적이다.

namespace2.cpp

#include <iostream>

namespace BestComImpl{
    void SimpleFunc(void);
}

namespace ProgComImpl{
    void SimpleFunc(void);
}

int main(void) {
    BestComImpl::SimpleFunc();
    ProgComImpl::SimpleFunc();
    return 0;
}

void BestComImpl::SimpleFunc(void) {
    std::cout<<"BestCom이 정의한 함수"<<std::endl;
}

void ProgComImpl::SimpleFunc(void) {
    std::cout<<"ProgCom이 정의한 함수"<<std::endl;
BestCom이 정의한 함수
ProgCom이 정의한 함수

 

 

namespace3.cpp

#include <iostream>

namespace BestComImpl{
    void SimpleFunc(void);
}

namespace BestComImpl{
    void PrettyFunc(void);
}

namespace ProgComImpl{
    void SimpleFunc(void);
}

int main(void) {
    BestComImpl::SimpleFunc();
    return 0;
}

void BestComImpl::SimpleFunc(void) {
    std::cout<<"BestCom이 정의한 함수"<<std::endl;
    PrettyFunc(); //동일 이름공간
    ProgComImpl::SimpleFunc(); //다른 이름공간
}

void BestComImpl::PrettyFunc(void) {
    std::cout<<"So Pretty!!"<<std::endl;
}

void ProgComImpl::SimpleFunc(void) {
    std::cout<<"ProgCom이 정의한 함수"<<std::endl;
}
BestCom이 정의한 함수
So Pretty!!
ProgCom이 정의한 함수

 

namespace BestComImpl{
    void SimpleFunc(void);
}

namespace BestComImpl{
    void PrettyFunc(void);
}
  • 둘 이상의 영역에 나누어서 함수를 추가할 수 있다.
  • SimpleFunc, PrettyFunc는 BestComImpl이라는 동일한 이름공간에 존재한다.

 

 

🍃 이름공간의 중첩

이름공간은 다른 이름공간 안에 삽입될 수 있다. 다음과 같은 형태로 말이다.

namespace Parent {
	int num=2;
    namespace SubOne {
    	int num=3;
    }
    namespace SubTwo {
    	int num=4;
    }
}
  • 위의 코드에는 num이 3개가 존재한다. 하지만 각각이 선언된 이름공간이 다르기에 이름충돌이 일어나지 않았다.
  • 다음의 문장을 실행하면 순서대로 2, 3, 4가 출력된다.
std::cout << Parent::num <<std::endl;
std::cout << Parent::SubOne::num <<std::endl;
std::cout << Parent::SubTwo::num <<std::endl;

 

🍃 std::cout, std::cin, std::endl

 

지금까지 콘솔 입출력을 진행할 때 std::cout, std::cin을 사용해 왔다. 그런데 이제 ::연산자의 의미를 이해했기 때문에 다음 세가지가 뜻하는 바를 설명할 수 있다.

std::cout
std::cin
std::endl

 

순서대로,

"이름공간 std에 선언된 cout"
"이름공간 std에 선언된 cin"
"이름공간 std에 선언된 endl"

 

따라서 다음과 같은 이름공간의 구성을 생각할 수 있다.

namespace std {
    cout . . . .
    cin . . . .
    endl . . . .
}
  • 헤더파일 < iostream > 에 선언되어 있는 cout, cin 그리고 endl은 이름공간 std안에 선언되어 있다는 결론을 내릴 수 있다.
  • 이렇듯 이름충돌을 막기 위해, C++ 표준에서 제공하는 다양한 요소들은 이름공간 std안에 선언되어 있다.

 

 

🍃 using을 이용한 이름공간의 명시

  • cout, cin, endl을 참조할 때마다 std::를 앞에 붙여야 하는 이유를 알게 되었다.
  • 하지만 쓸 때마다 붙이는 것은 귀찮은 일이다.
  • 이에 using 키워드를 사용할 수 있다.

UsingDcl1.cpp

#include <iostream>

namespace Hybrid {
    void HybFunc(void) {
        std::cout<<"So Simple function!"<<std::endl;
        std::cout<<"In namespace Hybrid!"<<std::endl;
    }
}

int main(void) {
    using Hybrid::HybFunc;
    HybFunc();
    return 0;
}
So Simple function!
In namespace Hybrid!
  • 이 예제에서는 Hybrid라는 이름공간 안에 선언된 함수를 범위지정 없이 그냥 호출할 수 있음을 보이고 있다.
using Hybrid::HybFunc;
  • 키워드 using을 이용해 '이름공간 Hybrid에 정의된 HybFunc를 호출할 때에는, 이름공간을 지정하지 않고 호출하겠다!'는 것을 명시(선언)하고 있다.
HybFunc();
  • 이렇게 using선언을 통해 이름공간의 지정 없이 HybFunc() 함수를 호출하고 있다.
  • 이때, 위 예제에서는 using 선언이 main 함수 내에 존재하는데, 이러한 경우 지역변수의 선언과 마찬가지로 선언된 이후부터 효력을 발휘하며, 선언된 지역을 벗어나면, 선언의 효력은 잃게 된다.
  • 따라서 프로그램 전체영역에 효력을 미치게 하려면 전역변수와 마찬가지로 함수 밖에 선언을 해야한다.

 

 

UsingDcl2.cpp

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

int main(void)
{
	int num=20;
	cout<<"Hello World!"<<endl;
	cout<<"Hello "<<"World!"<<endl;
	cout<<num<<' '<<'A';
	cout<<' '<<3.14<<endl;
	return 0;
}
Hello World!
Hello World!
20 A 3.14
  • 예제를 보면 using 선언을 함수 밖에 전역의 형태로 삽입했다. 따라서 이제부터는 cin, cout, endl의 사용에 있어 이름공간의 지정이 불필요하고, 이를 통해 코드의 구성이 한결 간단해졌다.
  • 만약에 위 예제에서 보이는 것처럼 일일이 using 선언을 하기가 귀찮다면, '이름공간 std에 선언된 모든 것에 대해 이름공간 지정의 생략'을 명령할 수 있다.
using namespace std;
  • 일일이 std 이름공간에 있다는 것을 지정하는 게 너무 귀찮고 코드가 지저분해 보였는데 드디어 한결 편안한 코딩을 할 수 있게 되었다.

 

UsingDcl3.cpp

#include <iostream>
using namespace std;

int main(void)
{
	int num=20;
	cout<<"Hello World!"<<endl;
	cout<<"Hello "<<"World!"<<endl;
	cout<<num<<' '<<'A';
	cout<<' '<<3.14<<endl;
	return 0;
}
  • 우리가 볼 때 위 예제의 using namespace 선언이 매우 매력적으로 보일거다.
  • 하지만 너무 빈번한 using namespace의 선언은 이름의 충돌을 막기위한 이름공간의 선언을 의미 없게 만든다.
  • 따라서 상황을 판단해서 적절히 혼용하도록 제한적으로 사용할 필요가 있다.

 

 

🍃 이름공간의 별칭 지정

이름공간이 중첩되면서까지 과도하게 사용되는 경우는 드물지만, 과도하게 사용되었을 때의 예를 들어보자.

 

NameAlias.cpp

#include <iostream>
using namespace std;

namespace AAA {
    namespace BBB {
        namespace CCC {
            int num1;
            int num2;
        }
    }
}

int main(void) {
    AAA::BBB::CCC::num1=20;
    AAA::BBB::CCC::num2=30;

    namespace ABC = AAA::BBB::CCC;
    cout<<ABC::num1<<endl;
    cout<<ABC::num2<<endl;
    return 0;
}
20
30
  • num1, num2에 접근할 때 다음과 같이 접근하게 되면 불편하다.
AAA::BBB::CCC::num1=20;
AAA::BBB::CCC::num2=30;
  • 이러한 경우, AAA::BBB::CCC에 별칭을 줄 수 있다.
namespace ABC = AAA::BBB::CCC;
  • 이렇게 되면 num1, num2에 다음과 같이 접근이 가능하다.
ABC::num1=10;
ABC::num2=20;

 

 

 

🍃 범위지정 연산자(Scope Resolution Operator)의 또 다른 기능

  • 지역변수의 이름이 전역변수의 이름과 같은 경우, 전역변수는 지역변수에 의해 가려진다.
int val=100;

int SimpleFunc(void) {
	int val=20; // 지역변수
    val+=3;     // 지역변수 val의 값 3 증가
}
  • 위의 코드에서 보이듯이 SimpleFunc내에서 전역변수와 이름이 동일한 지역변수 val이 선언되었으므로 val+=3;은 전역변수 val이 아닌, 지역변수 val의 값을 3 증가시킨다.
  • 그렇다면, 전역변수 val에 접근하려면  '범위지정 연산자'를 사용하면 된다.
int val=100;

int SimpleFunc(void) {
	int val=20; // 지역변수
    val+=3;     // 지역변수 val의 값 3 증가
    ::val+=7;   // 전역변수 val의 값 7 증가
}
반응형