선언(declarations)은 C 프로그래밍에서 중요한 규칙(central role)이다. 변수와 함수를 선언하는 것으로, 컴파일러가 프로그램에 대한 잠재적 에러를 확인하고 object code로 해석하기 위해 필수적인 정보들을 전달한다.
선언은 컴파일러에게 식별자의 의미에 대한 정보를 전달한다.
int i;
위와 같이 작성했을 때, 우리는 컴파일러에게 현재의 scope내에서 이름 i
는 자료형 in
의 변수를 나타낸다는 것을 알려준다.
float f(float);
위의 선언은 컴파일러에게 f
는 float
값을 반환하고 하나의 argument를 가지는데, 이또한 자료형이 float
라는 것을 알려준다.
일반적으로 선언은 아래와 같은 모습을 가진다.
declaration-specifiers declarators ;
Declaration specifiers
는 선언된 변수나 함수의 속성을 나타낸다. Declarators
는 이름을 부여하고 속성에 대한 추가적인 정보를 제공한다.
Declaration specifier는 3개의 카테고리로 나눌 수 있다.
auto
, static
, extern
, register
이다. 한 선언에서는 최대 1개의 storage class가 나타날 수 있다. 만약 존재한다면, 선언보다 더 앞에 나와야 한다.const
와 volatile
이다. C99에는 3번째 qualifier인 restrict
를 가지고 있다. 하나의 선언은 0개이거나 아니면 여러 개의 type qualifiers를 가질 수 있다.void
, char
, short
, int
, long
, float
, double
, signed
, unsigned
는 모두 type specifiers이다. 이 단어들은 Chapter 7에서 설명한 것처럼 결합할 수 있다. 단어가 나타나는 순서는 문제가 되지 않는다(int unsigned long
과 long unsigned int
와는 동일하다). type specifiers은 구조체, 공용체, 열거형의 specification또한 포함한다(예를 들어, struct point { int x, y; }
, struct { int x, y; }
, struct point
등). typedef
를 사용하여 생성된 자료형 이름 또한 type specifiers이다.(C99는 declaration specifiers의 4번째 종류인 function specifier
를 가지는데, 이는 오직 함수 선언에서만 사용된다. 이 카테고리는 오직 하나의 멤버인 키워드 inline
을 가진다.) type qualifiers와 type specifiers는 storage class 이후에 나와야 하지만, 이 순서에 대한 다른 제한은 없다.
Declarators는 identifiers(단순한 변수의 이름), identifiers 이후에 나오는 []
(배열 이름), *
이후에 나오는 identifiers(포인터 이름), identifiers 이후에 나오는 ()
(함수 이름)을 포함한다. Declarators는 콤마(,
)에 의해 분리된다. 변수를 표현하는 declarator의 뒤에는 initializer가 등장할 수 있다.
이 규칙들을 설명하는 여러 예시를 보자. 아래의 선언은 storage class와 3개의 declarators를 가진다.
아래의 선언은 type qualifier를 가지지만 storage class를 가지지 않는다. 그리고 initializer를 가진다.
아래의 선언은 storage class와 type qualifier를 모두 가진다. 그리고 3개의 type specifiers를 가지는데, 순서는 중요하지 않다.
변수 선언과 비슷하게, 함수 선언은 storage class, type qualifiers, type specifiers를 가질 수 있다. 아래의 선언은 storage class와 type specifier을 가진다.
Storage class는 변수, 함수, parameter에 대해 지정될 수 있다. 일단 변수를 알아보자.
Section 10.3을 생각해보면 block
이라는 용어가 함수의 body(중괄호로 둘러싸인 부분)나 선언을 가지는 것이 가능한 복합 구문을 나타내는 것이었다. C99에서는 선택 구문(if
와 switch
)과 반복 구문(while
, do
, for
)또한 block으로써 여겨졌다.
C프로그램 내부의 모든 변수는 3개의 속성을 가진다.
Storage duration : 변수의 storage duration은 언제 변수에 대한 메모리가 설정되었는지, 언제 그 메모리가 해제되었는지를 결정한다. automatic storage duration
인 변수에 대한 공간은 둘러싸고있는 block이 실행되었을 때 할당된다. block이 종료되었을 때 storage가 할당해제되고 변수는 값을 잃는다. static storage duration
인 변수는 프로그램이 실행중일 동안에는 동일한 storage location에 있고, 무기한으로 값을 보유한다.
Scope : 변수의 scope는 변수가 참조될 수 있는 프로그램 텍스트의 부분이다. 변수는 block scope
(변수가 선언된 시점부터 block이 닫히는 시점까지 변수가 이용가능한(visible)) 또는 file scope
(변수가 선언된 시점부터 파일이 닫히는 시점까지 변수가 이용가능한(visible))을 가진다.
Linkage : 변수의 linkage는 프로그램의 다른 부분에서 공유될 수 있는 정도를 결정한다. external linkage
인 변수는 프로그램 내부의 몇몇 파일(아마도 모든파일)에서 공유될 수 있다. internal linkage
인 변수는 단일 파일로 제한되지만, 그 파일 내부의 함수에 의해 공유될 수도 있다. (만약 또다른 파일에서 동일한 이름의 변수가 나타난다면, 다른 변수로 취급된다.) 단일 함수에 속해있는 no linkage
인 변수는 모든 것과 공유되지 않는다.
변수의 default storage duration, scope, and linkage는 어디서 선언되었느냐에 따라 다르다.
block 내부에서 선언된 변수는 automatic storage duration, block scope, no linkage이다.
프로그램의 가장 바깥쪽 수준에서 어떤 블록에도 속하지 않은 곳에 선언된 변수는 static storage duration, file scope, external linkage를 가진다.
많은 변수들이 default storage duration, scope, and linkage를 만족한다. 만약 그렇지 않을 때에는, explicit storage class인 auto
, static
, extern
, register
를 명시하는 것으로 이 속성을 수정할 수 있다.
auto
Storage Classauto
storage class는 block에 속해있는 변수에만 규칙에 어긋나지 않는다. auto
변수는 automatic storage duration, block scope, no linkage이다. auto
storage class는 대부분 명시적으로 특정하지 않는데, 왜냐하면 block 내부에서 선언되는 변수에 대한 default가 auto
이기 때문이다.
static
Storage Classstatic
storage class는 어디에서 선언되었든 상관없이 모든 변수들에 사용할 수 있다. 그렇지만 block 안에서 선언된 변수와 block 밖에서 선언된 변수에 대한 효과가 다르다. block 바깥에서 사용되었을 때, static
은 internal linkage를 가진 변수를 명시한다. block 내부에서 사용되었을 때, static
은 변수의 storage duration을 automatic에서 static으로 바꾼다.
아래의 그림은 i
와 j
가 static
으로 선언된 것의 효과를 보여준다.
block의 밖에서 사용되었을 때, static
은 변수가 선언된 파일 내부에서 변수를 숨긴다. 같은 파일에 나타나는 함수만 이 변수를 볼 수 있다. 아래의 예시에서, 함수 f1
과 f2
는 둘다 i
에 접근할 수 있지만, 다른 파일에 있는 함수들은 그렇지 못한다.
static int i;
void f1(void)
{
/* has access to i */
}
void f2(void)
{
/* has access to i */
}
이 static
의 사용은 정보 은닉(information hiding)이라고 알려진 기법을 구현하는 것에 도움을 준다.
block 내부에서 선언된 static
변수는 프로그램 실행내내 같은 storage location에 존재한다. 프로그램이 둘러싸고 있는 block을 떠나면 값을 잃는 automatic 변수와 다르게, static
변수는 무기한으로 값을 가지고 있는다. static
변수는 흥미로운 속성을 가진다.
block 내부의 static
변수는 프로그램 실행 전 오직 한번 초기화될 수 있다. auto
변수는 존재하게 될 때마다 초기화될 수 있다.(당연하게도 initializer를 가진다.)
함수가 재귀적으로 호출될 때마다, 새로운 auto
변수의 집합을 얻게된다. 하지만 static
변수를 가진다면, 변수는 함수의 호출 내내 공유된다.
함수가 auto
변수를 가리키는 포인터를 반환하면 안되지만, static
변수를 가리키는 포인터를 반환하는 것은 아무 문제가 없다.
변수들 중 하나를 static
으로 선언하는 것은 프로그램의 나머지 부분에 접근할 수 없는 "숨겨진" 영역 내부에서 일어나는 호출들 사이의 정보를 함수가 가질 수 있도록 한다. static
을 사용하여 프로그램을 더 효율적으로 만들 수 있다. 아래의 예시를 보자.
char digit_to_hex_char(int digit)
{
const char hex_chars[16] = "0123456789ABCDEF";
return hex_chars[digit];
}
digit_to_hex_char
함수가 호출될 때마다, 문자들 0123456789ABCDEF
는 hex_chars
배열로 복사되고, 그 배열을 초기화한다. 그러면 이제 배열을 static
으로 만들어보자.
char digit_to_hex_char(int digit)
{
static const char hex_chars[16] = "0123456789ABCDEF";
return hex_chars[digit];
}
static
변수가 오직 한번 초기화되기 때문에, digit_to_hex_char
의 속도가 증가한다.
extern
Storage Classextern
storage class는 몇몇 소스파일이 같은 변수를 공유할 수 있도록 한다.
Section 15.2에서 extern
의 필수적인 사용에 대해 알아보았었다.
extern int i;
위의 선언은 컴파일러에게 i
가 int
변수이지만, i
에 대한 메모리를 할당하지 않도록 한다는 것을 알려준다. C의 용어상으로, 이 선언은 i
의 정의가 아니다. 이것은 단순히 컴파일러에게 어딘가에 정의된 변수에 접근할 필요가 있다는 것을 알릴 뿐이다(같은 파일의 나중에 나타나거나, 아니면 또다른 파일에 나타나거나). 변수는 프로그램 내부에서 많은 선언을 가질 수 있지만, 정의는 오직 한번만 가져야 한다.
변수의 extern
선언은 정의가 아니라는 것에 대한 예외 규칙이 있다. 변수를 초기화하는 extern
선언은 변수의 정의와 같은 역할을 한다.
extern int i = 0;
int i = 0;
전자와 후자는 효과상으로는 동일하다.
이 규칙은 변수를 다양한 방법으로 초기화하는 다중 extern
선언을 막는다.
extern
선언 변수는 항상 static storage duration을 가진다. 변수의 scope는 선언의 위치에 의존한다. 선언이 block 내부에 있다면, 변수는 block scope를 가진다. 그렇지 않다면 file scope를 가진다.
extern
변수의 linkage를 결정하는 것은 조금 어렵다. 만약 변수가 static
으로 파일의 앞부분에 선언되었다면(어떤 함수에도 속하지 않는 정의), internal linkage를 갖는다. 그렇지 않는다면 변수는 external linkage를 갖는다.
register
Storage Class변수의 선언 내부에서 register
storage class를 사용하는 것은 컴파일러가 다른 변수처럼 주(main) 메모리에 저장하는 것이 아니라, 레지스터(register) 내부에 변수를 저장하도록 요청한다. (register
는 컴퓨터의 CPU에 위치한 storage 영역이다. 레지스터에 저장된 데이터는 일반적인 메모리에 저장된 데이터보다 더 빠르게 접근가능하고 더빠르게 업데이트된다.) 변수의 변수에 register
라는 storage class를 명시하는 것은 요청이고(request), 명령(command)가 아니다. 컴파일러가 선택한다면 register
변수는 메모리에 자유롭게 저장될 수 있다.
register
storage class는 block내부에 선언된 변수에만 규칙에 어긋나지 않는다. register
변수는 auto
변수처럼 동일한 storage duration, scope, linkage를 가진다. 그렇지만 register
변수는 auto
변수가 가진 것보다 하나 더 적은 속성을 가진다. 레지스터는 주소를 가지지 않기 때문에 register
변수의 주소를 얻기 위해 &
연산자를 사용하는 것은 규칙에 어긋난다. 이 제한은 컴파일러가 변수를 메모리에 저장했을 때에도 적용된다.
register
는 빈번히 접근되거나 업데이트되는 변수들에 사용되는 것이 가장 좋다. 예를 들어 for
구문 내부의 루프 조정 변수는 register
를 사용하기에 아주 좋다.
int sum_array(int a[], int n)
{
register int i;
int sum = 0;
for (i = 0; i < n; i++)
sum += a[i];
return sum;
}
register
는 예전만큼 C 프로그래머들 사이에서 인기가 있지는 않다. 요즘의 컴파일러는 옛날 컴파일러보다 훨씬 더 정교해서, 많은 컴파일러들이 자동적으로 변수를 레지스터에 저장할만큼의 이득이 있는지 결정할 수 있다. 여전히, register
를 사용하는 것은 컴파일러가 프로그램의 성능을 최적화 하는 것에 도움을 줄 수 있다. 특히 컴파일러가 register
변수가 주소를 취하지 못한다는 것을 알고 있기 때문에, register
변수는 포인터를 통해 수정될 수 없다. 이러한 측면에서는, register
키워드가 C99의 restrict
키워드와 관련이 있다.
함수 선언(그리고 정의)는 변수 선언과 비슷하게 storage class를 포함할 수 있는데, 선택할 수 있는 storage class는 extern
과 static
뿐이다. 함수 선언의 시작부분에 extern
을 붙이는 것은 함수가 external linkage를 가지도록 하여 다른 파일에서 호출 가능하도록 한다. static
은 internal linkage를 나타내는데, 함수의 이름을 그 함수가 정의된 파일에서만 사용하도록 한정한다. 만약 storage class가 명시되지 않았다면, 함수가 external linkage라고 여겨진다.
아래의 함수 선언을 보자.
extern int f(int i);
static int g(int i);
int h(int i);
f
가 external linkage를 가지고, g
가 internal linkage를 가지고, h
(기본적으로)가 external linkage를 가진다. g
는 internal linakge를 가지기 때문에, g
가 정의된 파일 외부에서 직접적으로 호출할 수 없다 (g
를 static
으로 선언한 것이 다른 파일에서 호출되는 것을 완벽하게 막는 것은 아니다. 함수 포인터를 통한 간접적인 호출은 여전히 가능하다.)
함수를 extern
으로 선언하는 것은 변수를 auto
로 선언하는 것과 비슷한데, 이는 어떤 의도도 가지지 않는다. 그래서 우리의 예제에서는 함수 선언에 extern
을 사용하지 않을 것이다. 그렇지만 어떤 프로그래머들은 extern
을 사용하는데, 이로 인한 피해는 없다.
함수를 static
으로 선언하는 것은 꽤 유용하다. 다른 파일에서 호출되지 않도록 의도하는 어떠한 함수를 선언할 때에는 static
을 사용하는 것을 추천한다. 뿐만 아니라 static
을 사용하는 것으로 아래의 이득을 얻을 수 있다.
유지보수가 쉬워진다. : 함수 f
를 static
으로 선언하는 것은 f
가 정의가 나타난 파일이 아닌 다른 파일에서 사용할 수 없다는 것을 보장한다. 결과적으로, 나중에 프로그램을 수정하는 누군가가 f
를 수정하여도 다른 파일에 영향을 미치지 않는다는 것을 알게 한다.(한가지 예외가 있는데, f
를 가리키는 포인터가 전달된 또다른 파일의 함수는 f
에 영향을 미칠 수도 있다. 이 상황은 f
가 정의된 파일에서 쉽게 발견되는데, f
를 전달하는 함수는 반드시 같은 파일에 정의될 것이기 때문이다.)
"name space pollution"을 감소시킨다. : static
으로 선언된 함수가 internal linkage를 갖기 때문에 이 함수들의 이름이 다른 파일에서 다시 사용될 수 있다. 아마도 어떤 다른 의도로 함수의 이름을 의도적으로 재사용하지는 않을 것이지만, 큰 프로그램에서 이를 피하는 것은 어려울 수 있다. external inkage를 가진 과도한 이름의 개수는 C 프로그래머가 "name space pollution"이라고 부르는 결과를 낸다. "name space pollution"은 다른 파일에 있는 이름이 각각 서로와 충돌하는 것이다. static
은 이 문제를 예방하는 것에 도움이 된다.
함수 parameter는 auto
변수와 같은 속성을 가진다. automatic storage duration, block scope, no linkage이다. parameter가 가질 수 있는 storage class는 오직 register
뿐이다.
다양한 storage class에 대해 알아보았었는데, 우리가 아는 것들을 요약해보자.
int a;
extern int b;
static int c;
void f(int d, register int e)
{
auto int g;
int h;
static int i;
extern int j;
register int k;
}
Table 18.1
Name Storage Duration Scope Linkage
a static file external
b static file ~
c static file internal
d automatic block none
e automatic block none
g automatic block none
h automatic block none
i static block none
j static block ~
k automatic block none
~
: b
와 j
의 정의가 나타나지 않았는데, 이로 인해 이 변수들의 linkage를 결정하는 것은 불가능하다. 대부분의 경우에, 또다른 파일에 정의된 변수들은 external linkage를 가진다.
4개의 storage class중에 가장 중요한 것은 static
과 extern
이다. auto
는 어떠한 효과도 없고, 현대의 컴파일러는 register
가 덜 중요하게 만들기 때문이다.
2개의 type qualifier가 있다. const
와 volatile
이다. (C99는 3번째 type qualifier인 restrict
가 있는데 이는 포인터에만 사용 가능하다.) volatile
의 사용이 low-level 프로그래밍에 한정되어 사용되기 때문에, 여기에 대해선 Section 20.3에서 설명할 것이다.
const
는 변수와 비슷하지만, "read-only"인 object를 선언하는 것에 사용된다. 프로그램이 const
object의 값에 접근할수는 있지만 이 값을 수정할 수는 없다.
const int n = 10;
위의 선언은 이름이 n
이고 값이 10인 const
object를 생성한다.
const int tax_brackets[] = {750, 2250, 3750, 5250, 7000};
위의 선언은 tax_brackets
이라는 이름의 const
배열을 생성한다.
const
인 object를 선언하는 것은 몇가지 이점이 있다.
문서화의 형태가 된다. 프로그램을 읽는 누군가에게 object가 read-only 속성이라는 것을 경고할 수 있다.
컴파일러가 프로그램이 실수로 object의 값을 수정하는 시도를 하지 않도록 체크할 수 있다.
프로그램이 특정한 형태의 어플리케이션(특히, 임베디드 시스템)으로 작성되었을 때, 컴파일러는 ROM(read-only memory)에 저장되는 데이터를 확인하기 위해 cosnt
를 사용할 수 있다.
척 보기에는, const
가 #define
directive처럼 상수에 대한 이름을 생성하는 역할을 하는 것 처럼 보일 수 있다. 그렇지만 #define
과 const
사이에는 중요한 차이점이 있다.
우리는 숫자, 문자, 문자열 상수에 대한 이름을 생성하기 위해 #define
을 사용할 수 있다. const
는 배열, 포인터, 구조체, 공용체를 포함한 어떠한 자료형의 read-only object든 만드는 것에 사용된다.
const
object는 변수처럼 동일한 scope 규칙을 따른다. #define
을 사용해 만들어진 상수는 그렇지 않다. 특히, 우리는 block scope의 상수를 만드는 것에 #define
을 사용할 수 없다.
const
object의 값은 macro의 값과는 다르게 디버거(debugger)에서 볼 수 있다.
macro와 다르게, const
object는 상수 표현식으로 사용할 수 없다. 우리는 아래와 같이 작성할 수 없는데, 왜냐하면 배열의 범위는 반드시 상수 표현식이여야 하기 때문이다.(C99에서 아래의 예제는 규칙에 어긋나지 않는데, a
가 automatic storage duration을 가지고, 가변 길이 배열로써 처리되기 때문이다. 하지만 static storage duration을 가진다면 이것은 규칙에 어긋난다.)
const int n = 10;
int a[n]; /*** WRONG ***/
&
)를 const
object에 적용하는 것은 규칙에 어긋나지 않는데, 왜냐하면 주소를 가지기 때문이다. 하지만 macro는 주소를 가지지 않는다.언제 #define
을 쓰고 언제 const
를 사용하는지에 대한 지시를 해주는 절대적인 규칙은 없다. 숫자나 문자를 표현하는 상수에는 #define
을 사용하는 것을 추천한다. 배열 dimension이나 switch
구문, 그리고 상수 표현식이 필요한 다른 장소에는 #define
을 사용하는 것이 좋다.