기억 부류

old/C Grammer 2010. 11. 18. 11:31

 

1. 지역 변수


1) 전역 변수와 지역 변수

기억 부류(Storage Class)란 변수가 저장되는 위치에 따라 결정되는 변수의 여러 가지 성질을 의미한다. 변수가 어디에 생성되는가에 따라 통용 범위와 파괴 시기 등의 특징이 결정된다. 이 내용은 C의 문법 체계를 이해하는데 상당히 중요한 비중을 차지하므로 숙독하여 완전히 이해하도록 하자. 기억 부류에는 4가지 종류가 있는데 일단 도표로 특성을 요약하였다.

기억 부류

전역

지역

정적

레지스터

지정자

extern

auto

static

register

저장 장소

정적 데이터 영역

스택

정적 데이터 영역

CPU 레지스터

선언 위치

함수의 외부

함수의 내부

함수의 내부

함수의 내부

통용 범위

프로그램 전체

함수의 내부

함수의 내부

함수의 내부

파괴 시기

프로그램 종료시

함수 종료시

프로그램 종료시

함수 종료시

초기값

0으로 초기화

초기화되지 않음

0으로 초기화

초기화되지 않음



#include <Turboc.h>

void func();

int global;                    // 함수 외부에서 선언되었으므로 전역변수

void main()
{
     int local;                 // main 함수의 지역변수
     global=1;                // 가능
     local=2;                  // 가능
     i=3;                        // 불가능
}

 void func()
{
     int i;                       // func 함수의 지역변수 
     global=1;                // 가능
     local=2;                  // 불가능
     i=3;                             // 가능
}



(1)
변수의 선언 위치가 다르다. 두 부류의 가장 뚜렷한 차이점인데 전역변수는 함수 바깥에서 선언하고 지역변수는 함수 내부에서 선언한다. 위 예제에서 global 변수는 main 함수 이전에 선언되었으므로 전역변수이고 local과 i는 각각 main 함수와 func 함수 내부에서 선언되었으므로 지역변수이다.

(2) 변수의 통용 범위가 다르다. 통용 범위란 변수가 사용될 수 있는 범위를 지칭한다. 전역변수는 특정한 함수 내부에서 선언된 것이 아니므로 함수에 속하지 않고 프로그램 전체가 공유한다. 따라서 변수가 선언된 위치 이후에는 어디서든지 이 변수를 사용할 수 있다. 위 예제에서 보다시피 global은 main 함수나 func 함수에서 자유롭게 읽고 쓸 수 있다.

, 아무리 전역변수라 하더라도 자신이 선언된 이후에만 사용할 수 있다. 만약 위 예제에서 global 변수를 main 다음에 선언한다면 main에서는 global을 참조할 수 없다. 물론 func에서는 참조할 수 있다. C 컴파일러는 1패스 방식으로 동작하기 때문에 변수든 함수든 사용하기 전에 항상 선언을 먼저 해야 한다.

반면 지역변수는 자신이 선언된 함수에 소속되어 있기 때문에 함수 외부에서는 이 변수를 사용할 수 없다. 변수의 값을 읽지도, 쓰지도 못하며 변수의 존재 자체가 알려지지 않기 때문에 이 변수를 들먹거리는 것조차 허용되지 않는다. 지역변수는 함수가 자신의 임무를 수행하기 위해 잠시 쓰고 버리는 것이다.

main에서 자신의 지역변수 local에 값을 대입하거나 func 함수에서 자신의 지역변수 i를 사용하는 것은 가능하다. 그러나 main에서 i 를 참조하거나 func에서 local을 참조하는 것은 불가능하다. main 함수에게 i라는 변수는 없는 것과 마찬가지이다.

(3) 변수의 파괴 시기가 다르다. 변수는 값을 기억하기 위해 메모리를 할당받아 사용한다. 변수를 다 사용했으면 파괴되는데 변수를 파괴한다는 것은 이 변수가 차지하고 있던 메모리를 회수한다는 뜻이다. 시스템의 메모리가 무한하지 않으므로 변수는 필요할 때만 메모리를 차지하며 다 사용하면 다른 변수를 위해 자리를 내 주어야 한다. 변수의 메모리가 회수되면 변수의 존재 자체가 사라진다.

전역변수는 프로그램에 소속되어 있고 모든 함수에서 사용 가능해야 하므로 프로그램이 실행중인 동안에는 파괴되지 않는다. 실행 직후에 생성되어 프로그램이 실행되는 동안에는 계속 메모리를 차지하고 있으며 프로그램이 종료되면 비로소 파괴된다. 즉, 전역변수는 프로그램과 운명을 같이 한다.

지역변수는 특정 함수 내부에서만 사용되므로 함수가 실행중일 때만 메모리를 차지하며 함수가 끝나면 변수의 생명도 끝이 난다. 함수의 임무를 위해 생성되는 임시 기억 장소이기 때문에 함수가 종료되면 더 이상 이 변수를 유지할 필요가 없어진다. 즉, 지역변수는 자신이 속해 있는 함수와 운명을 같이 한다. 함수가 호출되면 생성되고 함수가 끝나면 파괴된다. 그러다가 또 함수가 호출되면 생성되고 파괴되기를 계속 반복한다.

위 예제에서 global은 프로그램 종료시에 파괴되며 local은 main 함수 종료시에, i는 func 함수 종료시에 파괴된다. main의 지역변수는 우연히 전역변수와 생성, 파괴 시기가 거의 동일한데 이는 main 함수 자체가 프로그램 그 자체이기 때문이다.

(4) 변수가 생성되는 기억 장소가 다르다. 전역변수는 한 번 정해진 메모리 위치에 계속 남아 있어야 하므로 정적 데이터 영역에 생성된다. 정적 데이터 영역이란 프로그램의 코드 바로 다음에 위치하는 실행 파일의 한 부분인데 프로그램이 실행될 때 메모리로 로드되어 실행중에 계속 유지된다. 지역변수는 프로그램 실행중에 생성, 파괴를 반복하므로 스택에 생성된다. 스택(Stack)이라는 용어는 조금 어려운 개념인데 일단 데이터의 임시 저장소라고 생각하기 바란다.

프로그램은 실행에 필요한 임시적인 정보들을 스택에 차곡차곡 저장한다. 지역변수, 인수, 함수 실행후 돌아갈 번지 등이 스택에 생성되었다가 사라졌다가를 반복한다. 지역변수는 임시 저장소인 스택에 생성되기 때문에 통용 범위가 함수로 국한되고 함수가 종료되면 같이 사라지는 것이다.

(5) 초기화 여부가 다르다. 전역변수는 별도의 초기식이 없더라도 0으로 초기화된다. 위 예제의 global은 생성되자 마자 0이 될 것이다. 물론 int global=5; 와 같이 별도의 초기식을 주면 이 값으로 초기화된다. 전역변수는 컴파일될 때 컴파일러에 의해 초기화된 채로 실행 파일에 새겨지므로 초기화에 걸리는 시간은 0이다. 전역변수는 프로그램 전체에 걸쳐 사용되는 중요한 변수이므로 초기값을 지정하지 않더라도 안전을 위해 쓰레기값을 치워 0으로 초기화한다.

반면 지역변수는 별도의 초기식이 없을 경우 초기화되지 않는다. 따라서 무슨 값을 가지게 될 지 알 수 없는데 이때 초기화되지 않은 값을 쓰레기값(garbage)이라고 한다. 물론 선언할 때 초기값을 명시적으로 지정하면 초기화할 수도 있다. 초기값이 없을 때 지역변수를 초기화하지 않는 이유는 전역변수와는 달리 함수가 호출될 때마다 변수가 새로 생성되기 때문이다. 매번 변수를 초기화하자면 그만큼 실행 속도가 느려지므로 초기화를 하지 않는다.

지역변수는 함수 내부에서만 사용되므로 설사 지역변수의 쓰레기값이 문제가 된다 하더라도 그 함수만 점검하면 되지만 전역변수가 쓰레기값을 가지게 되면 프로그램 전체에 걸쳐 말썽을 부릴 수도 있다. 그래서 전역변수에 대해서는 초기값을 주지 않아도 쓰레기를 치우지만 지역변수는 그렇게 하지 않는다. 만약 지역변수에 쓰레기값이 들어가는 것이 싫다면 명시적으로 초기화를 해야 한다.


2) 지역 변수의 장점

지역변수는 함수 내부에서만 사용할 수 있고 함수가 끝나면 파괴되는데 비해 전역변수는 모든 함수에서 자유롭게 사용할 수 있고 프로그램이 실행중인 동안은 계속 유지된다. 통용 범위도 넓고 지속 기간도 길기 때문에 모든 면에서 지역변수보다는 사용하기 편리하며 지역변수로 할 수 있는 거의 대부분의 일은 전역변수로도 할 수 있다.

그렇다면 지역변수라는 것은 아예 사용하지 말고 모든 변수를 전역으로 사용하면 될 것이다. 지역변수를 쓰지 않고도 얼마든지 프로그램을 작성할 수 있으며 실제로 지역변수를 지원하지 않는 언어도 있다. 어셈블리에서는 사실 변수라는 개념 자체가 없고 모든 것이 메모리 주소이기 때문에 모든 값은 전역이다(스택에 의도적으로 임시 변수를 생성할 수는 있다). 고전적인 베이직 언어에서도 지역변수라는 것이 없다. 하지만 C나 파스칼, 자바, 비주얼 베이직 같은 근대적인 언어들은 모두 지역변수의 개념을 지원한다.

심지어는 PHP나 ASP, 자바 스크립트 같은 스크립트 언어들까지도 지역변수를 지원한다. 왜 이런 언어들이 지역변수를 지원하는가 하면 프로그램의 구조화에 큰 도움을 주고 유지, 보수를 쉽게 해주는 등의 여러 가지 장점이 있기 때문이다.

장점

(1) 함수의 독립성을 높인다. 프로그램은 함수로 구성되고 함수는 프로그램의 부품이라고 했다. 부품은 불가피한 경우를 제외하고는 가급적이면 스스로 작동할 수 있도록 만들어야 재활용하기 좋다. 부품끼리 공유하는 것(전역변수)이 많아지다 보면 의존 관계를 가지게 되므로 서로 얼키고 설켜서 좋지 않은 구조를 만들어 낸다. 부품은 전체를 구성하는 한 부분으로서 다른 부품과 상관없이 자신에게 부여된 임무를 독립적으로 수행할 수 있어야 한다. 이름만 봐도 무엇을 하는 함수인지 쉽게 짐작할 수 있을 것이며 본체의 코드를 보면 함수의 동작을 한눈에 파악할 수 있을 것이다. 재활용할 함수가 단독 함수라면 그래도 가져갈만 하겠지만 만약 일련의 함수군을 재활용하려면 어떻게 되겠는가? 이 함수군들이 사용하는 모든 전역변수 목록을 조사하고 이름이 충돌하는지 살펴보고 본체의 변수 참조문을 다 바꿔야 한다. 게다가 단순 변수도 아닌 구조체나 사용자 정의 타입이라면 재활용하는 것보다 차라리 다시 작성하는게 더 속편할 것이다.

지역변수는 함수가 자신이 필요로 하는 모든 정보를 다 가질 수 있도록(Self Contained) 해 줌으로써 함수의 독립성을 높여 준다. 그렇다면 이런 식으로 함수를 완전히 독립적으로 작성한다면 함수끼리의 정보 교환은 어떻게 하는가? 부품은 혼자 동작하는 게 아니므로 부품간의 정보 교환은 반드시 필요할 것인데 이럴 경우라도 전역변수는 사용할 필요가 없다. 왜냐하면 이럴 때 쓰라고 만들어 놓은 인수와 리턴값이라는 좋은 장치가 있기 때문이다. 함수끼리 정보를 주고 받기 위해서는 인수와 리턴값을 사용하는 것이 정석이다.

(2) 지역변수는 디버깅 효율을 향상시킨다. 버그, 즉 논리적인 에러가 발생하는 원인의 십중팔구는 변수를 잘못 조작한 것이다. 전역변수가 편하다고 남발하다 보면 디버깅을 할 때 살펴 봐야 할 변수의 수가 많아진다. 프로그램 하나를 만들기 위해서 필요한 변수는 보통 수백개, 많으면 수천개가 되는데 이 변수들이 전부 다 전역이라면 디버깅은 정말 끔찍한 작업이 될 것이다. 더구나 전역변수는 통용 범위가 프로그램 전체이기 때문에 어떤 함수가 이 변수를 잘못 건드렸는지 찾아내기가 아주 어렵다.

그러나 지역변수는 디버깅하기 아주 쉽다. 일단 지역변수를 많이 쓰면 전역변수의 수가 상대적으로 줄어들게 되므로 관찰 대상 변수의 범위가 대폭 좁아진다. 또한 지역변수는 말썽을 부려봐야 자신이 소속된 함수안에서만 유효하므로 그야말로 뛰어봤자 벼룩이고 부처님 손바닥안의 손오공이다. 특정 지역변수가 말썽을 부린다면 그 함수 내부만 정밀하게 점검해 보면 금방 문제점을 발견할 수 있다.

프로그램이 대략 1000줄 정도만 되도 프로그램 개발 속도를 좌지우지하는 중요한 관건은 디버깅 속도이다. 프로그램 개발 기간에 소요되는 시간 중의 70%가 에러를 잡아내는 디버깅이라고 하지 않는가? 대형 프로젝트나 팀 프로젝트에서는 디버깅을 얼마나 빨리 할 수 있는가가 프로젝트의 성공을 결정한다. 디버깅의 간편함이 가지는 의미는 상상외로 크다.

(3) 지역변수는 메모리를 절약한다. 전역변수는 프로그램이 실행될 때 같이 생성되며 계속 값을 유지해야 하므로 그만큼의 메모리를 항상 차지하게 된다. 지역변수는 함수가 호출될 때만 생성되며 함수가 종료되면 즉시 파괴되므로 자신이 속해 있는 함수가 실행중일 때만 메모리를 차지한다. 그래서 전역변수 대신 지역변수를 많이 쓰면 메모리를 절약할 수 있다.

(4) 재귀 호출이나 상호 호출같은 특별한 기법은 지역변수가 있어야만 사용할 수 있다. 이런 기법에 대해서는 다음에 배우게 되겠지만 함수가 호출될 때마다 새로운 변수가 생성되어야만 가능한 기법이다. 재귀 호출이 가능하기 위해서는 각 호출시마다 고유의 값을 유지해야 하는데 전역변수로는 이런 기법을 구사할 수 없다.

 지역변수가 전역변수에 비해 월등히 많은 장점을 가지고 있다. 전역변수는 편리하기는 하지만 복잡한 문제를 일으킬 수 있기 때문에 전역변수의 사용은 가급적이면 자재하는 것이 좋다. 전역변수를 전혀 사용하지 않고도 프로그램을 작성할 수 있다는 것이 이미 수학적으로 증명되어 있으며 전역변수를 병적으로 싫어하는 개발자들도 있다.


3) 외부 변수

[지정자] 타입 변수명;

지정자(Specifier)는 기억 부류를 비롯하여 상수 지정, 최적화 금지 등 변수의 여러 가지 성질을 지정하는 키워드인데 필요없을 경우 생략할 수도 있다. 기억 부류를 지정할 때는 auto, extern, static, register 등의 키워드를 사용하는데 먼저 지역변수를 지정하는 auto 키워드에 대해 알아보자.

auto int i, sum;

변수를 지역변수로 선언할 때는 변수의 타입앞에 auto 키워드를 붙인다. 일반적으로 티폴트값이 auto이기 때문에 우리는 생략해서 사용한다.

- 함수 내부에서 사용되면 지역변수가 된다.
- 함수 외부에서 사용되면 전역변수가 된다.

extern int value;

extern 키워드는 변수가 외부 어딘가에 선언되어 있다는 것을 알리는 역할을 한다.
외부에 선언되어 있는 경우에도 사용하지만 뒷쪽에 선언되어 있는 변수를 사용할 때에도 쓰인다.


2. 정적 변수


1) 정적 변수

정적변수(Static Variable)는 전역변수와 지역변수의 성격을 동시에 가지는 좀 특별한 기억 부류이다. 앞의 도표에 기록되어 있는 정적변수의 특징들을 살펴보자.

선언 위치는 지역변수와 마찬가지로 함수의 선두이다.
통용 범위는 지역변수와 마찬가지로 함수 내부로 국한된다.
저장 장소는 전역변수가 저장되는 정적 데이터 영역이다.
정적 데이터 영역에 저장되므로 프로그램 실행중에 항상 존재한다.
초기값 지정이 없으면 0으로 초기화되고 프로그램 실행시 단 한 번만 초기화된다. 

정적변수의 성질을 요약하자면 저장 장소는 전역변수이되 통용 범위는 지역변수라 할 수 있다. 정적변수를 선언할 때는 반드시 static이라는 지정자를 붙여야 한다.

static int i;
static double d;

이렇게 선언하면 i나 d는 정적변수가 되어 정적 데이터 영역에 저장되며 통용 범위는 선언문이 있는 함수 내부로 국한된다.

정적변수에서 한가지 유의할 점은 이 변수가 언제 초기화되는가 하는 점이다. 함수 선두에서 정적변수를 선언하고 있으므로 함수가 호출될 때마다 초기화될 것 같지만 그렇지 않으며 함수가 최초로 호출될 때 단 한 번만 초기화된다. 사실 초기화라는 말 자체에 이미 일회성의 의미가 내포되어 있지 않은가? 초기화되는 코드가 호출될 때마다 실행된다면 이 변수가 값을 계속 유지하지 못할 것이다.

또 만약 정적변수 선언문에서 초기화를 하지 않으면 전역변수와 마찬가지로 0으로 자동 초기화된다. 함수 내부에서 큰 배열을 선언하고 초기화할 때는 초기화 시간을 절약하기 위해 정적으로 선언하여 한 번만 초기화하도록 해야 한다. 그렇지 않으면 함수가 호출될 때마다 큰 배열이 매번 생성, 초기화, 파괴를 반복하므로 느려진다.

함수 내부에서 선언된 정적변수를 내부 정적변수라고 한다. 정적변수는 그 특성상 특정 함수 전용으로 선언하는 경우가 많기 때문에 보통 정적변수라고 하면 내부 정적변수를 의미한다. 흔하지는 않지만 함수 외부에서도 정적변수를 선언할 수 있는데 이렇게 선언된 변수를 외부 정적변수라고 한다. 외부 정적변수는 특정 함수에 소속되어 있지 않으므로 일반적으로 전역변수와 같은 성질을 가진다.

다만 전역변수와 다른 점은 extern 선언에 의해 외부 모듈로 알려지지 않는다는 점이다. 즉 자신이 선언된 모듈에서만 사용할 수 있는 모듈 전역변수가 된다. 외부에서 extern 선언을 하더라도 이 변수를 참조할 수 없게 된다.

전역변수이면서도 외부에 알려서는 안되는 그런 변수가 필요할 때 외부 정적변수를 사용한다. 주로 모듈의 재활용성을 높이기 위해 사용하는데 구체적인 예를 들어 보자. 그래픽 관련 함수들을 제공하는 graphic.cpp와 graphic.h를 아주 공들여서 제작했는데 이 모듈에서 mode, color 같은 전역변수를 사용하고 있다고 하자. 이 모듈을 쓰고 싶은 사람이 graphic.* 파일만 복사해서 사용하면 되도록 하고 싶다.

그런데 이 모듈을 사용하는 프로젝트에 이미 mode, color라는 전역변수가 사용되고 있다면 명칭의 충돌이 발생하게 될 것이고 양쪽 중 하나는 변수의 이름을 바꾸어야 한다. 이럴 때 mode, color 변수를 외부 정적변수로 선언하면 다른 모듈에는 알려지지 않으므로 이름의 충돌을 방지할 수 있게 되고 이 모듈은 아무 프로젝트에서나 재활용하기 쉬워진다.


2) 레지스터 변수

레지스터형 변수는 앞에서 논한 세 개의 기억 부류와 좀 다른 유별난 점이 있다. 지역, 전역, 정적변수들은 정적 데이터 영역이든 스택이든 어쨌든 메모리의 한 구석에 생성되지만 레지스터형 변수는 메모리가 아닌 CPU의 레지스터에 저장된다. 레지스터(Register)란 CPU를 구성하는 부품 중 하나이며 CPU가 데이터를 처리하기 위해 사용하는 임시 작업장이라고 생각하면 된다.

컴퓨터의 가장 핵심 부품인 CPU의 한 가운데에 있는 기억 장소이기 때문에 레지스터의 속도는 메모리와 비교가 되지 않을 정도로 빠르다. 값을 읽거나 쓰는데 수십억분의 1초 정도밖에 걸리지 않는다. CPU의 종류에 따라 다르지만 레지스터는 보통 10개~20개 정도밖에 없는 아주 귀한 기억 장소인데 여기에 변수를 저장하면 이 변수를 참조하는 문장의 속도가 빨라진다.

레지스터의 크기는 CPU의 비트수를 결정하는 중요한 기준인데 레지스터가 32비트면 32비트 CPU라고 부른다. 386이후부터 최신의 팬티엄 4까지 현재까지 우리가 사용하는 CPU는 대부분 32비트이므로 레지스터들도 전부 32비트이고 따라서 레지스터에 저장할 수 있는 변수의 타입은 int, unsigned, 포인터 형 등의 32비트형뿐이다. double같은 실수형은 저장할 수 없으며 구조체나 배열 따위는 당연히 안된다. 에러는 아니지만 지정해 봐야 무시당한다.

CPU의 레지스터 개수가 많지 않기 때문에 레지스터형 변수는 두 개까지만 선언할 수 있다. 컴파일러나 플랫폼에 따라 레지스터형 변수를 위해 할당하는 레지스터가 다른데 인텔 플랫폼에서는 많이 사용되지 않는 ESI, EDI 레지스터를 사용한다. 만약 세 개 이상의 레지스터형 변수를 선언하면 최초 두 개까지만 레지스터형이 되고 나머지는 지역변수가 된다.

register int r;               // 레지스터형 변수 r선언
register double d;        // 실수는 레지스터형 변수가 될 수 없음. 지역변수로 선언된다.
register a,b,c;             // a,b만 레지스터형 변수가 되고 c는 지역변수가 된다.

레지스터는 한정된 자원이기 때문에 일시적으로 사용할 지역변수에만 지정할 수 있으며 전역변수에는 레지스터 기억 부류를 지정할 수 없다. 프로그램과 생명을 같이 하는 전역변수가 레지스터 하나를 차지한다면 프로그램 실행중인 동안 레지스터 하나가 묶여 버리게 될 것이다. 전역변수에 register 기억 부류를 지정하면 명백한 에러로 처리된다. 지역변수 또는 함수의 형식 인수에 대해서만 이 기억 부류를 사용할 수 있다.

레지스터형 변수를 사용하는 이유는 조금이라도 더 빠른 속도를 얻기 위해서이다. 대규모의 루프를 돌린다거나 할 때 루프 제어 변수를 레지스터형으로 선언하면 이 변수의 읽기, 증감 속도가 빨라지므로 전체 루프의 실행 속도가 빨라질 것이다.

레지스터형 변수는 메모리에 생성되는 것이 아니므로 &연산자는 사용할 수 없다. 레지스터는 CPU 내부에 있기 때문에 번지를 가지지 않으므로 &연산자로 이 변수의 메모리 주소를 조사할 수 없다. 그러나 레지스터형 포인터 변수가 번지를 기억할 수는 있으므로 *연산자를 사용하는 것은 가능하다. C 스팩에는 레지스터형 변수와 &, * 연산자의 관계가 이렇게 규정되어 있으며 상식적으로 이해가 갈 것이다.


3) 정적 함수

기억 부류 중에 함수에 적용되는 것은 정적(static) 기억 부류밖에 없다. 정적 함수는 특정 모듈에서만 사용할 수 있는데 앞에서 살펴본 외부 정적변수의 특성과 유사하다. 함수 정의문 앞에 static이라는 지정자만 붙이면 이 함수는 정적 함수가 된다.

static void func()
{
     ....
}

정적 함수와 반대되는 개념에 대해 별다른 명칭은 없고 굳이 이름을 붙인다면 비정적 함수나 외부 함수 정도가 될 것이다. 외부 함수는 별다른 지정이 없는 한 외부로 항상 알려지며 원형 선언만 하면 어떤 모듈에서나 이 함수를 호출할 수 있다.

그러나 정적 함수는 특정 모듈에서만 사용하도록 정의된 것이므로 외부에서 원형을 선언한다 하더라도 이 함수를 호출할 수 없다. 외부에서 그 존재를 알 수 없도록 해야 하는 이유는 외부 정적변수의 경우와 마찬가지로 이름 충돌을 방지하기 위해서이다. 재사용을 위해 작성한 모듈에서 ReadFile이라는 함수를 사용하는데 이 이름이 너무 일반적이어서 프로젝트내의 다른 함수명과 충돌될 것 같으면 이 함수를 static으로 선언하면 된다.


3. 통용 범위


1) 통용 범위 규칙

변수나 함수, 태그 같은 명칭은 상호 구분되어야 하므로 중복되어서는 안된다. 그래서 같은 이름을 가진 두 개의 변수를 선언할 수 없다. 다음과 같이 작성하면 에러로 처리된다.

void main()
{
     int i;
     double i; 

i라는 명칭으로 정수형 변수와 실수형 변수를 동시에 선언했다. 이유를 설명할 필요도 없이 이 코드는 에러로 처리된다.

'i' : redefinition; different basic types

i라는 명칭이 정수형으로 선언되었다가 실수형이라는 다른 타입으로 중복 선언되었다는 뜻이다. 이름이 중복되면 다음에 i를 참조할 때 정수형 변수 i를 의미하는 것인지 실수형 변수 i를 의미하는 것인지 구분할 수 없는 모호함이 발생할 것이다. 컴퓨터 프로그램의 논리에 모호함이란 절대 있을 수 없으므로 명칭의 중복은 허락되지 않는다.

명칭이 중복되지 말아야 한다는 것은 지극히 상식적이다. 그러나 이 법칙에 예외가 있는데 통용 범위가 다른 명칭끼리는 같은 이름을 가질 수도 있다. 다음 예를 보자.

 void func()
{
     int i;
     .... 

void proc()
{
     double i;
     .... 

func 함수에서는 i를 정수형으로 선언했고 proc 함수에서는 같은 이름의 i를 실수형으로 선었했지만 논리적으로 아무 문제가 없다. 둘 다 지역변수이고 통용 범위가 분명히 다르기 때문에 같은 명칭 i를 참조하더라도 func 함수에서는 정수형으로, proc 함수에서는 실수형으로 구분할 수 있어 모호함이 발생하지 않는다.


2) 블록 범위

앞에서 지역변수란 함수 내부에서 선언된 변수라고 했는데 이는 어디까지나 편의상 이해하기 쉽도록 설명한 것이다. 이제 통용 범위까지 살펴 봤으므로 지역변수를 좀 더 정확하게 정의해 보도록 하자. 지역변수는 { } 괄호안의 블록에 선언된 변수를 의미하며 변수가 선언된 블록 내부에서만 통용된다. { } 괄호안에서만 통용되는 범위를 블록 범위라고 하는데 { } 괄호가 보통 함수의 시작과 끝을 나타내고 함수 선두에 지역변수를 선언하는 경우가 많기 때문에 지역변수의 통용 범위는 함수 내부가 되는 것이다.

지역변수의 통용 범위는 정확하게 함수 내부가 아니고 선언된 블록의 내부이다. 그래서 함수 내부안에 { } 블록을 만들고 이 안에 또 다른 지역변수를 만들 수도 있다.

#include <Turboc.h>

void main()
{
     int i;                           // 함수 범위
     i=5;
     {
          int i;                       // 블록 범위
          i=3;
          printf("i=%d\n",i);
     }
     printf("i=%d\n",i);
}


3) 선언과 정의

이쯤에서 선언과 정의에 대해 구분해 보자. 아주 비슷한 용어인 것 같지만 달라도 한참 다르며 다소 헷갈리는 용어라 정리가 필요하다. 일단 도표로 선언과 정의의 특성을 정리해 보자.

 

역할

메모리

정보의 완전성

중복 가능성

선언

알린다.

사용 안함

불완전해도

가능

정의

생성한다.

할당

항상 완전해야

불가능

선언과 정의의 대상이 되는 것에는 함수, 변수, 타입, 매크로, 태그 등 여러 가지가 있으나 주로 함수와 변수가 주 대상이다.

선언(Declaration) - 컴파일러에게 대상에 대한 정보를 알린다. 함수가 어떤 인수들을 전달받으며 어떤 타입을 리턴하는지를 알리는 원형 선언이 대표적인 선언이다. 컴파일러에게 정보만 제공하는 것이므로 본체를 가지지 않으며 실제 코드를 생성하지도 않는다. 그래서 다음처럼 여러 번 중복되어도 상관없다.

int Max(int a, int b);
int Max(int a, int b);

물론 똑같은 내용을 일부러 이렇게 중복 선언할 경우는 없겠지만 헤더 파일을 여러 번 포함하다 보면 중복 선언될 경우가 있다. 이렇게 본의 아니게 중복 선언되더라도 문제는 없다. 단, 중복 선언할 경우 앞의 선언과 뒤의 선언이 달라서는 안된다. 앞에서는 int Max(...); 로 선언해 놓고 다시 선언할 때는 double Max(...)로 선언할 수는 없다.

정의(Definition) - 대상에 대한 정보로부터 대상을 만든다. int i; 정의문에 의해 4바이트를 할당하며 int Max(int, int) { } 정의로부터 함수의 본체를 컴파일하여 코드를 생성한다. 정의는 변수의 타입, 함수의 인수 목록을 컴파일러에게 알려 주기도 하므로 항상 선언을 겸한다. 그래서 함수를 호출부보다 더 앞쪽에서 정의하면 컴파일러가 이 함수의 본체를 만들면서 모든 정보를 파악할 수 있으므로 별도의 원형 선언을 하지 않아도 된다.

정의는 실제 대상을 만들어 내기 때문에 중복되어서는 안된다. 전체 프로그램을 통해 단 한 번만 나타나야 하며 두 번 이상 중복할 필요도 없다. 만약 정의를 두 번 반복하면 컴파일러는 왜 똑같은 함수를 두 번 정의하느냐는 에러 메시지를 출력할 것이다.

이렇듯 선언과 정의는 분명히 다른 용어이지만 실제로는 명확하게 구분하지 않고 대충 사용하는 경향이 있다. 지역변수의 경우 정의와 선언이 완전히 일치하며 만든 영역에서만 사용하므로 별도의 선언을 할 수도 없고 할 필요도 없다. 그래서 지역변수는 정의만 가능한 대상이지만 일반적으로 "선언한다"라고 하지 "정의한다"라고는 하지 않는다. 전역변수의 경우는 int i;가 정의이고 extern int i;가 선언으로 분명히 구분되지만 관습적으로 전역변수 정의문인 int i; 도 선언문이라고 부른다.

매크로의 경우도 실제 메모리를 할당하는 것은 아니므로 선언이 맞지만 일반적으로 정의라고 표현한다. 매크로를 정의하는 명령어가 #define이지 #declare가 아닌 것만 봐도 그렇다. typedef에 의한 사용자 정의 타입도 키워드에 def라는 단어가 포함되어 있기는 하지만 원칙적으로 선언이라는 용어가 옳다. 그러나 표준 문서에서 조차도 typedef를 사용자 선언 타입이라고는 표현하지 않으며 타입을 정의한다고 표현한다. 구조체나 열거형의 태그는 주로 선언한다고 하지만 가끔 정의한다는 표현을 쓰기도 한다. 이렇듯 두 용어는 분명히 다르지만 실제로는 별 구분없이 사용되는 경향이 있다.


4) 설계 원칙

함수를 작성하는 문법과 호출하는 방법, 인수를 받아들이고 리턴하는 방법을 익히는 것은 그다지 어렵지 않다. 그러나 함수를 정말로 함수답게 잘 나누고 디자인하는 것은 무척 어렵고 단기간에 체득되지 않는다. 함수는 프로그램을 구성하는 단위로서 잘 나누어 놓으면 프로그램의 구조가 탄탄해지고 확장하기도 쉽고 재사용성도 좋아진다. 잘 짜여진 프로그램을 분석해 보면 함수의 분할 구조가 감탄스러울 정도로 잘 되어 있음을 볼 수 있고 그런 함수를 만드는 능력이 부러워지기까지 한다.

그러나 함수를 잘못 디자인하면 코드는 더 커지고 프로그램은 더 느려지며 조금이라도 수정하려면 어디를 건드려야 할지 판단하기 힘든 나쁜 구조가 만들어진다. 함수에 의해 코드는 꼬이기만 하고 엉망이 된 코드 사이로 버그가 창궐할 수 있는 환경만 만들어지니 아예 함수를 만들지 않느니만도 못한 상태가 되기도 한다.

프로그래밍에 처음 입문한 사람들에게 함수 디자인이라는 주제는 아주 어렵고 힘든 고비이다. 책을 읽어서 비법을 얻는 것은 불가능하고 잘 하는 사람에게 개인 지도를 받아도 어렵고 혼자서 연습해 보기는 더욱 더 어렵다. 함수 디자인은 오로지 많은 분석과 실습만으로 얻어지는 경험이다. 그래서 꾸준한 연습만이 해결책이다. 다음은 함수를 잘 만드는 기본적인 지침들이다.

(1) 함수의 이름을 최대한 설명적으로 작성하여 이름만으로 무엇을 하는 함수인지, 이왕이면 어떻게 쓰는 것인지도 알 수 있도록 한다. 마치 함수의 이름이 주석인 것처럼 해야 한다. 함수는 이름으로 호출되므로 좋은 이름을 붙여 두면 함수를 관리하는 사람과 쓰는 사람 모두가 편해진다. 아주 간단한 규칙인 것 같지만 함수를 설계하는 첫 번째 원칙이 될 정도로 좋은 이름을 붙이는 것은 중요하다. 특히 팀 작업을 하거나 오랫동안 관리해야 할 코드라면 더욱 더 정성스럽게 이름을 붙여야 한다.

함수명은 보통 동사와 목적어 그리고 약간의 수식어로 구성된다. GetScore, DrawScreen, TestGameEnd 등은 이름만으로 어떤 동작을 하는지 쉽게 알 수 있으므로 좋은 함수명이다. 동작이 좀 더 구체적이라면 GetHighestScore, GetAverageScore 등의 수식어를 붙이는 것도 좋다. 이런 이름은 무엇을 어떻게 하는지를 분명히 표현한다.

반면 Score, Draw, Test 따위는 점수나 그리기와 상관이 있는 동작을 하는 것 같아 보이기는 하지만 구체적으로 무엇을 어떻게 하는지를 표현하지 못하므로 좋지 않다. 함수를 만든 사람은 당장은 이 함수들을 이해할 수 있다 하더라도 조금만 시간이 지나면 함수의 본체를 다 읽어 봐야 무엇을 하는 함수인지 알 수 있으므로 코드를 유지 및 확장하기 어려워진다. 함수에 좋은 이름을 붙이는 것은 어려운 기술이 아니라 조금의 관심만 기울이면 누구나 할 수 있는 기술이며 이름을 구성하는 적절한 영어 단어만 잘 선정하여 조립하면 된다.

(2) 두 번 이상 중복된 코드는 반드시 함수로 분리한다. 똑같은 코드를 중복된 채로 내버려 두면 프로그램의 크기가 쓸데없이 커진다. 10줄짜리 코드를 10번 반복한다면 나머지 90줄은 불필요하게 용량만 차지하는 것이다. 용량의 문제보다 더 심각한 것은 코드를 유지, 확장하기가 아주 곤란해진다는 점이다. 요구가 바뀌어 해당 동작을 수정해야 한다고 해 보자. 이 동작을 함수로 분리해 두었으면 함수만 고치면 되지만 중복되어 있다면 일일이 찾아가서 고쳐야 한다. 실수로 한 곳을 고치지 않으면 이것이 바로 버그의 원흉이 된다.

중복되는 회수에 상관없이 앞에서 이미 만들었던 코드와 비슷한 코드를 또 작성해야 한다면 일단 그 부분을 함수로 만들고 기존 코드를 함수 호출로 수정해야 한다. 즉 다음과 같이 구조를 만든다.

아니! 고작 두 번 중복되었을 뿐인데 이런 것들도 함수로 분리해야 한단 말인가 하는 생각이 들지도 모르겠다. 그 대답은 당연히 그렇다이다. 한 번 중복된 코드는 조만간 다시 필요해질 가능성이 아주 높다. 뿐만 아니라 십중팔구 그 코드는 잠시 후 확장되어야 한다. 그래서 중복이 발견되는 즉시, 그것이 단 두 군데 뿐이더라도 무조건 함수로 분리하는 습관을 가져야 한다. 네 번, 다섯 번 중복될 때 분리하겠다고 생각한다면 이미 프로그램은 엉망이 되어 가고 있는 것이다.

(3) 반복되지 않더라도 한 단위로 볼 수 있는 작업은 함수로 만든다. 설사 이 함수를 딱 한 번만 호출하고 다른 곳에서 호출할 확률이 아주 희박하더라도 이렇게 하는 것이 좋다. 예를 들어 어떤 구조체나 배열을 초기화하는 코드 덩어리는 Init~ 라는 이름으로 분리하고 화면을 출력하는 코드 덩어리는 Draw~ 따위의 이름을 주어 분리한다.

함 함수의 소스가 아주 길어져서 수백줄이 되면 그 많은 코드들의 어떤 부분이 어떤 작업을 하는지 얼른 파악되지 않는다. 게다가 다른 일을 하는 코드들이 한 곳에 섞여 있으면 필시 꼬이게 마련이며 이런 복잡한 코드는 대체로 메인 코드인 경우가 많다. 이 코드들의 그룹을 나누어 함수로 분리해 두면 메인 코드는 이 함수들을 조립하는 수준으로 간단해진다.

이렇게 분리되면 메인 코드를 읽기 쉬워지고 이미 완성된 코드들은 더 이상 신경쓰지 않아도 되는 이점이 있다. 또한 이 함수들이 현재 프로젝트에서는 반복되지 않더라도 다른 프로젝트에서는 재사용되기 쉽다.

(4) 함수는 한 번에 하나의 작업만 해야 한다. 함수는 프로그램을 구성하는 부품이며 부품이란 전체를 구성하는 원자적인 단위이다. 물론 한 함수가 두 가지 일을 동시에 수행할 수도 있고 그렇게 하는 것이 더 효율적일 때도 있다. 그러나 이 함수를 나누어 더 작은 부품을 만들어 놓으면 두 일을 각각 따로 실행해야 할 필요가 있을 때 작은 부품을 활용할 수 있다. 만약 꼭 여러 가지 일을 한꺼번에 해야 하는 함수가 필요하다면 각각의 함수를 만든 후 이 함수들을 호출하는 함수를 하나 더 만들면 된다.

이렇게 되면 이 함수를 호출하는 기존의 코드는 영향을 받지 않으면서 새로운 작은 단위의 작업을 호출할 수도 있게 된다.

(5) 입력과 출력이 직관적이고 명확해야 한다. 인수는 함수에게 주어지는 작업거리인데 함수가 하는 일에 꼭 필요한 정보만 최소한의 인수로 받아들여야 한다. 나머지 인수로부터 알 수 있는 값이나 연산에 사용하지 않는 불필요한 정보는 전달될 필요가 없다. 예를 들어 화면의 특정 위치에 메시지를 출력하는 OutMessage 함수를 작성한다고 해 보자. 

void OutMessage(int x, int y, char *str, int len)

이 함수에서 메시지의 길이 len은 불필요한 인수이다. 세 번째 인수 str이 널 종료 문자열이라면 str로부터 길이를 계산할 수 있다. 물론 메시지의 일부만을 출력하는 기능이 있다면 이럴 때는 len이 필요할 것이다. 함수의 작업 결과는 가급적이면 리턴값으로 보고해야 하는데 설사 그 값을 호출원에서 사용하지 않는다 하더라도 일단 보고할 내용이 있다면 보고하는 것이 좋다.

(6) 함수는 자체적으로 에러 처리를 해야 한다. 함수는 독립된 작업을 하며 재사용 가능한 부품이므로 그 자체로서 완벽하게 동작할 수 있어야 한다. 현재 작성중인 프로젝트에서 잘 실행된다고 해서 이 함수가 안전하고 완벽하게 동작한다는 것을 보장할 수는 없다. 특히 입력된 인수의 유효성을 잘 점검해야 한다. 포인터의 경우 NULL이 전달될 수도 있고 정수가 터무니없이 크다거나 음수가 될 수 없는 값에 대해 음수가 전달되는 경우에도 에러 처리를 해야 한다. 그래야 어떤 프로젝트로 가져 가든 별도의 수정없이 재사용 가능한 부품이 된다.

여기서 논한 함수 설계에 대한 지침은 어디까지나 일반적인 참고사항일 뿐이다. 특수한 실무 환경에서는 이 지침과는 다르게 함수를 만들어야 하는 불가피한 경우도 존재한다. 이 지침이 함수 설계를 잘 하고 싶은 사람들에게 아주 중요한 내용인 것은 사실이지만 이 지침들을 몽땅 외운다고 해서 함수 설계 경험이 금방 늘어나는 것은 아니다. 마치 "여성의 마음을 사로잡는 100가지 비법"을 달달 외운다고 해서 사교계의 달인이 될 수 없는 것과 같다. 일단 이 권고안을 머리속에 새겨 두고 끊임없이 분석, 연습해 봐야 한다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

표준 함수  (0) 2010.11.19
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
Posted by jazzlife
,