코딩/C와 C++

[C++ 기초 강좌] 6. 범위에 대한 이해

드리프트 2021. 5. 15. 21:51
728x170

 

 

6. 범위에 대한 이해

 

모든 변수는 여러분의 프로그램이 실행될 때 유한한 수명을 가진다. 변수들은 여러분이 그것들을 선언하는 순간에 태어난 다음에 어떤 순간에 사라지게 된다(늦어도 여러분의 프로그램이 종료되기 전에 사라진다). 하나의 특정한 변수가 얼마나 오랫동안 지속되는지는 저장 기간(storage duration)이라는 속성에 의해서 결정된다. 하나의 변수가 가질 수 있는 저장 기간에는 세 가지의 종류가 있다.

1. 자동 저장기간
2. 정적인 저장기간
3. 동적인 저장기간

이것들 중에 변수가 어떤 것을 가지게 될 것인지는 여러분이 그 변수를 어떻게 생성하는가에 달려 있다. 우리는 동적인 저장 기간에 관한 설명을 4장까지 미룰 생각이다. 그러나 우리는 다른 두 가지의 저장 기간의 특징에 관해서는 이 장에서 살펴볼 것이다.


변수들이 가질 수 있는 또 다른 속성은 범위 (scope)이다. 한 변수의 범위라는 것은 단지 그 변수 이름이 유효하게 사용될 수 있는 여러분 프로그램의 부분을 가리킨다. 변수의 범위 안에서 여러분은 적법하게 그것을 참조하여 그것의 값을 설정하거나, 그것을 표현 안에서 사용할 수 있다. 변수의 범위 밖에서는 여러분이 그 변수의 이름을 참조할 수 없다. 그렇게 하려고 하면 컴파일러 에러가 발생한다. 그러나 심지어 여러분이 그 변수의 이름을 참조할 수 없다고 하더라도 변수는 그것의 범위 밖에서 존재한다는 사실을 명심하라. 우리는 이 설명이 끝난 뒤에 이것에 대한 샘플을 살펴볼 것이다.

 

우리가 지금까지 선언했던 모든 변수는 자동 저장 기간을 가지고 있다. 그러므로 그 변수들을 자동 변수라 부른다. 먼저, 이러한 변수에 대해서 살펴본다.

 

 

6.1 자동 변수

 

우리가 지금까지 선언했던 변수들은 하나의 블록 안에서(즉, 중괄호 사이에서) 선언된 것들이다. 이러한 변수를 자동 변수라고 하고, 로컬 범위 또는 블록 범위를 가진다라고 말한다. 자동 변수는 그것이 선언되는 위치에서부터 그것의 선언을 포함하고 있는 블록이 끝나는 곳까지의 ‘범위 안에' 존재하게 된다.


자동 변수는 그것이 선언될 때 "태어났다가' 그 선언을 포함하고 있는 블록의 끝부분에서 자동적으로 사라진다. 변수가 사라지는 곳은 그 변수에 대한 선언 앞에 위치하는 첫 번째의 시작 중괄호({)와 매치되는 종료 중괄호(})가 된다. 자동 변수에 대한 선언을 포함하고 있는 문장 블록이 실행될 때마다 그 변수는 그때마다 다시 생성된다 그리고 만약 여러분이 자동 변수에 대한 초기값을 지정하였다면 그 변수는 매번 그것이 생성될 때마다 다시 초기화된다.


auto라는 키워드가 있는데, 여러분은 이것을 사용하여 자동 변수를 지정할 수 있다. 그러나 이것은 디폴트로 이미 함축되어 있기 때문에 거의 사용되지 않는다. 지금까지 우리가 살펴본 사항들을 종합하는 샘플을 보도록 한다.

 


자동 변수

우리는 다음의 샘플을 통하여 자동 변수의 범위의 결과에 대하여 알아볼 수 있다.

// EX2_06.cpp
// 변수 법위 나타내기
#include <iostream>

using namespace std;

int main()
{   // 함수 범위가 여기에서 시작한다.

int count1 = 10;
int count3 = 50;
cout << endl
     << "Value of outer count1 = " << count1
     << endl;
     
{   // 새로운 범위가 여기에서 시작한다.
	int count1 = 20;   // 이것은 외부 count1을 가린다.
    int count2 = 30;
    cout << "Value of inner count1 = " << count1
         << endl;
    count1 += 3;   // 이것은 안쪽 count1에 영향을 미친다.
    count3 += count2;
}   // 여기에서 끝난다.

cout << "Value of outer count1 =" << count1
      << endl
      << "Value of outer count3 =" << count3
      << endl;
      
// cout << count2 << endl;    // 에러를 발생시키기 위해 이 부분을 커멘트로 빼라.

return 0;

} // 함수 범위가 여기에서 끝난다.


작동하는 방식

 

이 샘플로부터의 출력은 다음과 같다.

 

처음에 나타나는 두 개의 문장은 초기값으로 각각 10과 50을 가지는 count1과 count3, 두 개의 정수 변수를 정의한다. 이러한 두 개의 변수는 선언된 지점으로부터 프로그램의 끝부분에 있는 종료 중괄호까지는 존재한다. 이러한 변수의 범위도 main()의 끝부분에 있는 종료 중괄호까지에 이른다.

 

* 변수의 수명과 범위는 서로 다른 사항이라는 사실을 명심하라. 이 두 가지 사항을 혼동하지 않는 것이 중요하다.

 

변수를 정의한 다음에 위에 보이고 있는 줄들의 첫 번째 줄을 나타내기 위해서 count1의 값이 출력된다.

 

그런 다음, 새로운 블록을 시작하는 두 번째 중괄호가 나타난다. 초기값으로 각각 20과 30을 가지는 count1과 count2라는 두 개의 변수가 이 블록 안에서 정의된다. 여기서 선언되는 count1 은 처음의 count1과는 다르다. 첫 번째 count1은 여전히 존재하고 있지만, 그것의 이름은 두 번째의 count1에 의해서 덮어 씌워져 있다. 안쪽 블록 안에서 선언에 뒤이어 count1이라는 이름을 사용하면 바로 그 블록 안에서 선언된 count1이 참조된다.


* count1이라는 변수 이름은 단지 어떤 상황이 벌어지는가를 보이기 위해서 여기서 다시 사용해 본 것이다. 비록 이 코드는 아무런 하자가 없지만, 일반적으로 이러한 방식으로 프로그래밍하는 것은 좋지 않다. 이렇게 하면 혼동스러울 뿐만 아니라, 바깥의 범위에서 정의된 변수
를 우연히 덮어쓸 위험이 있는 것이다.

두 번째 출력 줄에 나타나 있는 값은 우리가 안쪽 블록 안에서 안쪽 범위를 가지는 count1을 사용하고 있다는 것을 나타낸다(즉, 가장 안쪽의 중괄호 안에서).

cout << "Value of inner count1 = " << count1
     << endl;

우리가 여전히 바깥쪽의 count1을 사용하고 있었다면 이것은 10이라는 값을 출력할 것이다. 그러면 count1 변수는 다음과 같은 문장에 의해서 증가한다.

count1 += 3; // 이것은 안쪽 count1에 영향을 미친다.

 

값이 증가하는 것은 안쪽 범위에 있는 변수에게 적용된다. 왜냐하면, 바깥쪽 범위는 여전히 숨겨져 있기 때문이다. 그러나 바깥쪽 범위에서 정의된 count3은 어떤 문제도 발생시키지 않고, 다움의 문장에 의해서 증가된다.

count3 += count2;

 

위의 문장은 바깥 범위의 시작 부분에서 정의된 변수를 안쪽 범위에서 액세스 할 수 있다는 것을 나타낸다. 다음과 같은 사실을 명심하라. 만약 count3가 두 번째의 중괄호 쌍 다음에서 선언되었다면 그것은 여전히 바깥쪽 범위 안에 있을 것이다. 그러나 그러한 경우, count3는 위의 문장이 실행될 때 존재하지 않을 것이다.

안쪽 범위에서 중괄호가 끝나면 count2와 안쪽의 count1은 사라진다. count1과 count3 변수는 여전히 바깥쪽 범위 안에 있다. 그리고 출력되는 값은 count3이 정말로 안쪽 범위 안에서 증가되었음을 나타낸다.


만약 여러분이 다음의 줄을 코멘트로 빼버리면

// cout << count2 << endl;   // 에러를 발생시키기 위해 이 부분을 커멘트로 빼라.

프로그램은 더 이상 올바르게 컴파일되지 않을 것이다. 왜냐하면, 프로그램은 존재하지 않는 변수를 출력하려고 하기 때문이다. 여러분은 다움과 같은 에러 메시지를 얻게 될 것이다.

왜냐하면, 이때 count2는 범위를 넘어섰기 때문이다.

 


6.2 변수 선언의 위치 결정하기


여러분은 변수를 어디에서 선언할지를 매우 유연하게 결정할 수 있다. 고려해야 할 가장 중요한 사항은 그 변수가 어떠한 범위를 가질 것인가에 관한 문제이다. 그 문제를 제외하고서 여러분은 일반적으로 그 변수가 프로그램에서 처음으로 사용될 곳의 가까운 곳에서 변수를 선언해야 한다. 여러분은 다른 프로그래머들도 여러분의 프로그램을 가능하면 쉽게 볼 수 있도록 작성해야 한다. 그리고 그렇게 하는 데 있어서 변수를 그것이 사용되는 첫 번째 장소 근처에서 선언하는 것이 도움이 된다.

변수 선언을 프로그램을 구성하는 모든 함수의 바깥쪽에 위치시키는 것도 가능하다. 그것이 관련된 변수에게 어떤 영향을 미치는가에 대해서 살펴본다.

 

 

 


6.3 글로벌 변수


모든 블록과 클래스(클래스에 대해서는 나중에 이 책에서 설명할 것이다) 바깥쪽에서 선언되는 변수들을 글로벌 (global)이라고 부르고, 글로벌 범위(또한, 글로벌 네임 공간 범위 또는 파일 범위라고도 불린다)를 가진다. 이것은 파일에서 그 변수가 선언된 곳 뒤에 나오는 모든 함수가 그 변수를 액세스 할 수 있다는 것을 의미한다. 만약 여러분이 그 변수를 프로그램의 맨 위에서 선언하였다면 그 변수들은 그 파일의 어느 곳에서도 액세스 될 수 있다.


또한, 글로벌은 디폴트로 정적인 저장 기간을 가진다. 정적인 저장 기간을 가지는 글로벌 변수는 프로그램의 실행이 시작될 때부터 프로그램의 실행이 끝날 때까지 존재한다. 만약 여러분이 글로벌 변수의 초기값을 지정하지 않았다면 그것은 디폴트 초기값으로 0을 가진다. 글로벌 변수에게 초기값을 할당하는 작업은 main() 이 실행되기 전에 발생한다. 그러므로 글로벌 변수들은 항상 변수의 범위 안에 있는 어떠한 코드에서도 사용될 수 있는 것이다.

 

다음의 그림은 Example.cpp라는 소스 파일의 내용을 나타내고 있다. 그리고 화살표는 각 변수의 범위를 가리키고 있다.



파일의 처음 부분에 나타나는 value1 변수는 main() 함수 뒤에 나타나는 value4와 같이 글로벌 범위에서 선언된다. 각 글로벌 변수의 범위는 그것이 정의된 곳으로부터 파일의 끝부분까지이다. 비록 실행이 시작될 때 value4가 존재하지만, 그것은 main() 안에서는 참조될 수 없다. 왜냐하면, main()은 그 변수의 범위 안에 존재하지 않기 때문이다. main()이 value4를 사용할 수 있으려면 여러분은 그 선언 부분을 파일의 처음 부분으로 옮겨야 한다. value1과 value4는 기본값으로 0을 가진다. 그러나 자동 변수는 그렇지가 않다. function() 안에 있는 로컬 변수인 value1이 동일한 이름의 글로벌 변수를 가리고 있다.

 

글로벌 변수는 프로그램이 실행되는 한 계속해서 존재하기 때문에 다음과 같은 의문이 생길 수 있다. "모든 변수를 글로벌로 만들어서 이렇게 로컬 변수로 프로그램이 혼란스럽게 되는 것을 방지하면 되지 않는가?" 이러한 생각은 처음에는 그럴듯하게 들린다. 그러나 사이렌 섬의 전설에서도 보듯이 여러분이 그렇게 함으로써 얻을 수 있는 이점을 완전히 무효로 만들 수 있는 심각한 부작용이 있다.

 

일반적으로, 실제적인 프로그램은 많은 수의 문장, 수없는 함수, 그리고 셀 수없이 상당한 양의 변수로 구성된다. 모든 변수를 글로벌 범위로 선언하게 되면 우연히 실수로 다른 변수의 값을 수정하게 될 가능성이 매우 커지게 된다. 또한, 그것들의 이름을 정하는 것도 복잡하게 된다. 그리고 그러한 변수들은 프로그램이 실행되는 동안에 메모리를 차지하게 된다. 변수를 블록이나 함수에 대하여 로컬로서 사용하게 되면 그 변수들은 외부로부터 안전하게 보존될 수 있는 것이다. 그리고 그것들은 선언된 곳으로부터 블록이 끝나는 곳까지만 존재하고, 메모리를 차지하기 때문에 전체적인 개발 과정을 훨씬 쉽게 관리할 수 있다.

만약 우리가 지금까지 생성했던 샘플에 대한 프로젝트 뷰(VS 같은 IDE에서)에서 +를 클릭하여 프로젝트를 확장시켜 본다면 Globals라는 항목을 보게 될 것이다. 여러분이 이것을 확장시키면 프로그램에서 글로벌 범위를 가지고 있는 모든 것에 대한 리스트를 볼 수 있을 것이다. 이 리스트는 모든 글로벌 함수를 포함할 뿐만 아니라, 여러분이 선언했던 모든 글로벌 변수를 포함하고 있다.

 

범위 해석 연산자(scope resolution operator)


우리가 보아왔듯이 글로벌 변수는 로컬 변수에 의해서 동일한 이름으로 숨겨질 수 있다. 그러나 우리가 네임 공간에 대해서 다룰 때 여러분이 1 장에서 보았던 범위 해석 연산자(::)를 사용하여 글로벌 변수를 사용하는 것이 가능하다. 우리는 아까 사용했던 샘플을 약간 수정하여 이 연산자가 어떻게 작동하는지를 살펴볼 수 있다.

 

// EX2 07.CPP
// 변수 법위 연습 샘플
#include <iostream>

using namespace std;
int count1 = 100;  // count1의 글로벌 버전

int main()
{   // 함수 범위가 이 곳에서 시작한다.

	int count1 = 10;
	int count3 = 50;
	cout << endl
		 << "Value of outer count1 = " << count1
		 << endl;
    
    cout << "Value of global count1 = " << ::count1 // 외부 블록으로부터 온 것
         << endl;
         
    {
    	int count1 = 20;      // 새로운 범위가 여기에서 시작한다.
        int count2 = 30;
        cout << "Value of inner count1 = " << count1
             << endl;
        cout << "Value of global count1 = " << ::count1 // 안쪽 블록으로부터 온 것
             << endl;
        count1 += 3;
        count3 += count2;  // 이것은 안쪽 count1에 영향을 미침
   }
   
   cout << "Value of outer count1 = " << count1
        << endl
        << "Value of outer count3 = " << count3
        << endl;
        
// cout << count2 << endl;   // 에러를 발생시키기 위해 이 부분을 커멘트로 빼라.

return 0;

} // 함수 범위는 여기에서 끝남.

 

작동하는 방식

 

여러분이 이 샘플을 컴파일하여 실행시키면 다음과 같은 출력을 얻게 된다.


위 코드에서 설명이 코멘트로 쓰여 있는 부분은 우리가 이전의 샘플을 수정한 부분을 나타낸다. 우리는 단지 그 수정된 코드의 결과에 대해서만 설명하고자 한다. main() 함수 앞에 있는 count1의 선언은 글로벌 범위에서 이루어진다. 그러므로 원칙적으로 그것은 main() 함수 어디에서도 사용할 수 있다. 이 글로벌 변수는 초기값으로 100을 가진다.

int count1 = 100;   // count1의 글로벌 버전

그러나 우리는 이미 main() 안에서 정의되는 count1이라는 다른 두 개의 변수를 가지고 있다. 그러므로 이 프로그램에서 글로벌 count1은 로컬 count1 변수에 의해 가려지게 된다. 처음에 나타나는 출력문

cout << "Value of global count1 = " << ::count1   // 외부 블록에서 온 것
     << endl;

 

은 이 문장이 범위 해석 연산자(::)를 사용하여 컴파일러에게 우리가 로컬 변수가 아닌 글로벌 count1 변수를 참조하고자 한다는 사실을 분명히 알린다. 여러분은 출력되는 값을 통해서 이것이 작동하는 모습을 볼 수 있다.


안쪽 블록에서 글로벌 변수인 count1은 count1이라는 두 개의 변수(안쪽에 있는 count1과 바깥쪽에 있는 count1) 뒤에 가려진다. 우리가 안쪽 블록에 추가한 문장에 의해서 생성되는 출력을 볼 수 있기 때문에 우리는 글로벌 범위 분석 연산자가 안쪽 블록 안에서 자기의 작업을 하는 것을 볼 수 있다.

cout << "Value of global count1 = " << ::count1   // 안쪽 블록에서 온 것
     << endl;

위의 문장은 이전과 같이 100이라는 값을 출력한다. 이러한 방식으로 사용되는 긴 팔을 가지는 범위 분석 연산자는 항상 글로벌 변수를 액세스 한다.


우리가 이 장의 처음 부분에서 네임 공간 std에 대해서 다룰 때 네임 공간에 대해서 언급하였다. 우리는 using 명령을 사용하여 네임
공간 std를 액세스 할 수 있었다. 그 외에, 우리는 범위 분석 연산자를 사용하여 네임 공간을 액세스 할 수 있다. 예를 들어, 우리는 표준 라이브러리에 있는 줄의 끝 연산자(end-of-line operator)를 액세스 하기 위해서 std::endl이라고 쓸 수 있다. 위의 샘플에서 우리는 범위 분석 연산자를 사용하여 count1 변수에 대한 글로벌 네임 공간을 검색한다. 연산자 앞에 네임 공간을 지정하지 않음으로써 컴파일러는 그다음에 따라오는 이름으로 글로벌 네임 공간을 찾아야 한다는 것 을 알게 된다.

이 연산자가 폭넓게 사용되는 객체 지향적인 프로그래밍에 대해서 살펴볼 때 이것에 대해서 더 자세히 살펴볼 것이다. 또한, 6장에서는 여러분만의 네임 공간을 생성하는 방법을 포함한 네임 공간 전반에 관한 사항을 알아볼 것이다.

 

 

6.4 정적인 변수

 

어떤 변수를 여러 분 이 로컬에서 정의하고 사용할 수 있으면서 그 변수가 선언된 블록을 빠져나간 다음에도 그것이 계속 존재하는 그러한
변수를 여러분은 원할 것이다. 다시 말해서, 여러분은 그 변수를 블록 범위 안에서 선언하면서도 그것에게는 정적인 저장 기간을 부여해야 하는 것이다. static이라는 지정자(specifier)는 그렇게 할 수 있는 수단을 제공한다. 그리고 우리가 5장에서 함수를 다룰 때 그렇게 해야 할 필요성은 점차 많아질 것이다.

 

사실, 정적인 변수는 그것이 블록 안에서 선언되었다고 할지라도 프로그램의 수명 동안에 존재하며, 그 블록 안에서만(또는, 그것의 하위 블록 안에서) 사용할 수 있다. 그것은 여전히 블록 범위를 가진다. 그러나 정적인 저장 기간을 가진다. count라는 정적인 변수를 선언하려면 다음과 같이 작성한다.

static int count;

만약 여러분이 정적인 변수를 선언할 때 그것에게 초기값을 제공하지 않았다면 그것은 자동적으로 초기값을 가진다. 여기서 선언된 count 변수는 0이라는 초기값을 가진다. 정적인 변수에 대한 디폴트 초기값은 항상 그 변수에게 사용될 수 있는 형식으로 변환된 0이라는 값이 된다. 그러나 자동 변수인 경우에는 그렇지가 않다는 사실을 명심하라.


* 여러분이 자동 변수를 초기화하지 않았다면 그 변수는 프로그램에 의해서 마지막으로 사용되었던 현재 자기가 점유하고 있는 메모리에 남아 있던 쓰레기 값을 가지게 된다.

그리드형