변수를 선언함으로써, 우리는 컴파일러가 잠재적인 오류가 있는지 프로그램을 검사하고 object code로 변환하는 데 필요한 중요한 정보를 전달한다. 이전 챕터들까지는 자세하게 알아보지 않고 선언을 사용했는데, 이번 장에서는 듬성듬성한 개념을 채워본다. 선언에서 사용될 수 있는 세련된 옵션들에 대해 알아보고, 변수의 선언이 함수의 선언과 꽤나 공통점이 많다는 것도 알아본다. 또 storage duration, scope, linkage에 대해 더 잘 이해할 수 있는 기반도 마련한다.
18.1 지금까지 미뤄왔던, syntax of declarations in their most general form
그 이후의 4개 장은 선언에 등장하는 item들에 초점을 맞춘다.
18.2 storage classes
18.3 type qualifiers
18.4 declarators
18.5 initializers
18.6 inline keyword(C99)
18.1 Declaration Syntax
선언은 컴파일러에게 identifier의 의미에 대한 정보를 전달한다.
int i;
라고 쓰면, 우리는 컴파일러에게 현재의 scope에서 i라는 이름은 integer 형식의 변수를 나타낸다는 정보를 전달한다.
float f(float);
는 컴파일러에게 f는 float 형식의 값을 반환하는 함수이며 float 형식의 인자 하나를 갖는다는 것을 전달한다.
일반적으로, 선언은 다음과 같은 형태를 갖는다.
declarations-specifiers declarators ;
Declaration specifiers는 선언되는 변수나 함수의 특징을 설명한다. Declarators는 그것들의 이름을 나타내고 그들의 특징에 대해 추가적인 정보를 제공할 수도 있다.
Declaration specifier는 세가지 카테고리로 나뉜다.
Storage classes: 4개의 storage classes가 존재한다: auto, static, extern, register. 선언 하나에는 최대 한개의 storage class가 올 수 있다. 만약 존재한다면, 가장 처음에 와야 한다.
Type qualifiers: C89에서는 두 개의 type qualifer만 존재한다: const, volatile. C99에선 세번째 qualifier가 등장했다: restrict. 선언에는 type qualifier가 하나도 없거나 여러개 존재할 수 있다.
Type specifiers: void, char, short, int, long, float, double, signed, 그리고 unsigned는 모두 type specifier에 속한다. 이 단어들은 Chapter 7에서 설명된 대로 결합될 수 있다; 결합 순서의 영향이 없다(int unsigned long은 long unsigned int와 동일함). type specifier는 structure, union, enumerations도 포함한다. typedef으로 생성된 type name도 type specifier에 속한다.
C99에서는 declaration specifier의 4번째 종류가 있는데, function specifier이며 함수 선언시에만 사용된다. 이 카테고리의 멤버는 inline keyword 뿐이다.
Declaration specifier 구분
storage class |
auto, static, extern, register |
type qualifier |
const, volatile, restrict(C99) |
type specifier |
void, char, short, int, long, float, double, signed, unsigned, struct, union, enum, type name created by typedef |
function specifier(C99) |
inline(only in funcion declraration) |
type qualifier와 type specifier는 storage class 뒤에 와야 하고, 둘 간 순서는 제한이 없다. 이 책에서는 type qualifier를 type specifier 앞에 둔다.
Declarator에는 identifiers(names of simple variables), indentifiers followed by [](배열 이름), identifiers preceded by *(포인터 이름), 그리고 identifiers followed by () (함수 이름)이 있다. declarator는 콤마에 따라 구분된다. 변수를 나타내는 declarator 뒤에는 initializer가 올 수 있다.
18.2 Storage Classes
Storage class는 변수 및 더 작은 범위의 함수와 매개 변수에 대해 지정할 수 있다. 지금은 변수에 대해 집중한다.
10.3에서 다룬 block이란 용어는 함수의 body(중괄호로 묶인)이나 compound statement를 의미하며 그 안에는 선언이 포함되어 있을 수 있다. C99에서는 조건문(if, switch), 반복문(while, do, for)과 그 안에 있는 statements도 block으로 취급된다. storage class의 종류에는 다음 네가지가 있다: auto, static, extern, register. 이 storage class들은 아래 단락에 나오는 변수들의 속성을 지정한다.
Properties of Variables
C 프로그램 내의 모든 변수는 세 가지 속성을 가지고 있다.
1. storage duration. 변수를 위한 메모리가 설정되고 해제되는 시점
automatic storage duration: 변수의 storage는 변수를 감싼 block이 실행되었을 때 할당되고, block이 종료될 때 해제되며 변수가 그 값을 잃는다.
static storage duration: 변수가 프로그램이 실행되는 동안 계속 같은 storage location에 머무르며, 값을 무기한으로 저장한다.
2. scope. program text 중에서 변수가 참조될 수 있는 범위
block scope: 그 변수의 선언 시점부터 변수가 속한 block이 종료되는 지점까지 visible
file scope: 변수가 그 변수의 선언 시점부터 파일이 끝나는 곳까지 visible
3. linkage. 프로그램의 다른 부분들과 공유될 수 있는 범위
external linkage: 변수가 프로그램 내의 여러 개의 파일(아마도 전부)에서 공유될 수 있다.
internal linkage: 변수가 하나의 파일로 제한되어 있지만, 그 파일 안에 있는 함수들 사이에서는 공유될 수 있다. (만약 같은 이름을 가진 변수가 다른 파일에서 등장하면, 둘은 다른 변수로 간주된다.)
no linkage: 변수가 하나의 함수 내에만 속하며 전혀 공유될 수 없다.
세가지 속성의 기본값은 변수의 선언 위치에 따라 다르다.
block 안에서(함수의 body 포함) 선언:automatic storage duration, block scope, no linkage
아무 block에도 속하지 않은 곳(outermost level of a program)에서 선언: static storage duration, file scope, external linkage.
int i; // static storage duration, file scope, external linkage
void f(void) {
int j; // automatic storage duration, block scope, no linkage
}
많은 변수들은 storage duration, scope, linkage의 기본값이 사용자가 원하는 값과 일치한다. 만약 그렇지 않은 경우, storage class를 명시적으로 지정한다.
The auto Storage Class
블록 안에서 변수를 선언할 때만 legal.
automatic storage duration, block scope, no linkage
auto storage class는 명시적으로 지정되는 일이 거의 없는데, block 내에서 선언된 변수의 기본값이 이것이기 때문이다.
The static Storage Class
static storage class는 모든 변수에 대해, 선언되는 장소에 관계없이 사용할 수 있다.
효과는 변수가 block의 바깥에서 선언되는 경우와 block 내부에서 선언된 경우에 대해 다르다.
block 바깥에서 사용: internal linkage
block 안에서 사용: static storage duration
static int i; // static storage duration, file scope, internal linkage
void f(void) {
static int j; // static storage duration, block scope, no linkage
}
information hiding: block 바깥의 선언에서 사용되었을 때, static은 변수를 그것이 선언된 파일 안으로 숨긴다; 같은 파일 내에 있는 함수들만 그 변수를 볼 수가 있다. 함수 내부에서 변수 중 하나를 static으로 선언하는 것은 함수 호출 사이에서 변수의 정보를 프로그램의 다른 부분에서는 접근하지 못하는 "숨겨진" 영역에 저장하도록 해 준다. (19.2)
static 변수의 흥미로운 특징들:
- block 안의 static 변수는 프로그램이 실행되기에 앞서서, 단 한번만 초기화된다. auto 변수는 그것이 존재하게 될 때마다 매번 초기화된다(initializer가 있다는 가정 하에).
- 함수가 재귀적으로 호출될 때 마다, 함수는 새로운 auto 변수들의 집합을 얻는다. 반면 static 변수를 가지고 있다면, 그 변수는 함수의 모든 호출 동안 공유된다.
- 함수가 auto 변수에 대한 포인터를 반환해서는 안되지만 static 변수에 대한 포인터를 반환하는 것은 아무 문제가 없다.
static은 프로그램을 효율적으로 만들기 위해 사용한다.
char digit_to_hex_char(int digit)
{
const char hex_chars[16] = "0123456789ABCDEF";
return hex_chars[digit];
}
매번 digit_to_hex_char 함수가 호출될 때마다, "012...DEF" 문자들이 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의 속도를 개선할 수 있게 된다.
The extern Storage Class
여러개의 소스 파일이 변수에서 공유할 수 있게 한다. 15.2에서 이에 대한 내용을 이미 다룬바 있어서 여기서는 간단히 설명.
extern int i;
라는 선언은 컴파일러에게 i가 int 변수이지만, i에 대한 메모리를 할당하지는 않도록 알린다. C 용어로는, 이 선언은 i의 정의(definition)이 아니다; 단지 콤파일러에게 우리가 다른 어딘가(같은 파일의 뒤쪽, 또는 다른 파일- 이게 더 흔함 -)에 정의되언 변수에 접근하겠다고 알려주는 역할을 한다.
변수는 프로그램 내에서 여러번 선언declaration될 수는 있지만, 정의definition은 단 한번만 될 수 있다.
예외: extern을 사용한 변수 선언이 initializer와 함께 쓰이는 경우. 예를 들어
extern int i = 0;은 int i = 0; 과 동일한 효과이며 변수의 정의로 취급된다. (여러개의 extern declaration에서 제각각 다른 값으로 변수를 초기화하는 것을 방지하는 규칙)
extern으로 선언된 변수는..
storage duration: 항상 static storage duration
scope of variable: block 안에서 선언되면 block scope, 그렇지 않은 경우는 file scope
linkage: 만약 변수가 파일 내 앞선 위치에서 static으로, 모든 함수 정의 바깥에서 이미 선언되어 있는 경우, 변수는 internal linkage
그 이외 경우(일반적인 경우), 변수는 external linkage이다.
extern int i; // static storage duration, file scope, ? linkage
void f(void) {
extern int j; // static storage duration, block scope, ? linkage
}
The register Storage Class
register storage class를 사용해서 변수 선언시 변수를 다른 변수와 같이 메인 메모리에 저장하는 것이 아니라 register에 저장하도록 컴파일러에게 "요청"한다.
(register: CPU 내에 있는 저장 공간. register에 저장되어 있는 데이터는 보통 메모리에 저장된 변수들보다 더 빠르게 접근되고 업데이트될 수 있다.)
변수의 storage calss를 register로 지정하는 것은 요청이며, 명령이 아니다. 컴파일러는 자유롭게 register 변수를 메모리에 저장할 수도 있다.
변수가 block 내에서 선언된 경우에만 legal.
register 변수는 auto 변수와 storage duration, scope, linkage가 동일함.
그러나 register 변수는 auto 변수에 있는 중요한 특징을 가지고 있지 않다: register에는 주소가 없기 때문에, register 변수에 & 연산자를 사용하는 것은 illegal. 이 제한은 컴파일러가 변수를 register가 아닌 메모리에 저장했을 때에도 유효하다.
register는 고빈도로 접근/업데이트 되는 변수를 위해 최선으로 사용된다. 하지만 register는 C 프로그래머 사이에서 예전만큼 대중적으로 사용되지 않는다. 오늘날의 컴파일러는 옛날 컴파일러에 비교하면 훨씬 정교해졌다; 많은 컴파일러들은 자동적으로 어떤 변수를 register에 저장했을 때 가장 이익이 될지를 구분할 수 있다. 그래도 여전히 register를 사용하는 것은 컴파일러가 프로그램의 퍼포먼스를 최적화하는 데 도움을 주는 유용한 정보가 된다. 특히, 컴파일러는 register 변수는 그 주소를 얻을 수 없고, 포인터로 값이 수정될 수 없다는 것을 안다. 이 관점에서는 register 키워드는 C99의 restrict 키워드와 연관되어 있다.
The Storage Class of a Function
함수의 선언(정의)는 변수의 선언과 마찬가지로 storage class를 포함할 수 있다. 하지만 extern과 static 두 종류만 사용할 수 있다.
extern : 함수가 external linkage.
static : 함수가 internal linkage. 함수의 이름 사용이 선언된 파일 내로 제한됨.
storage class를 지정하지 않으면, 기본값으로 external linkage
함수를 static으로 선언한다고 하더라도 다른 파일에서 호출되는 것을 완전히 막지는 못한다; 함수 포인터를 통한 간접적인 호출은 여전히 가능하다.
함수를 extern으로 선언하는 것은 변수를 auto로 선언하는 것과 비슷하게, 어떤 효과가 없고, 사용했을 때 해가 되지도 않는다.
다른 파일에서 사용되지 않을 모든 함수 선언 앞에 static을 붙이는 것이 추천됨. 함수를 static으로 선언하는 것의 장점:
더 쉬운 유지보수. 함수 f를 static으로 선언하면 f는 선언이 있는 다른 파일에서 보이지 않는다는 것이 보장된다. 그 결과, 나중에 프로그램을 수정하는 사람이 f의 수정이 다른 파일에 영향을 주지 않는다는 것을 알 수 있다. (예외: 다른 파일에 들어있는 f의 포인터를 전달받는 함수는 f의 변경에 영향을 받을 수 있다. 다행히 그런 상황은 f가 정의되어 있는 파일을 조사하면 쉽게 찾을 수 있다. f를 전달하는 함수도 그곳에 정의되어 있어야 하기 때문)
"name space pollution"이 덜함.
"name space pollution": 다른 파일에 있는 이름들이 의도치 않게서로 충돌하는 상황
static으로 선언된 함수들이 internal linkage이므로, 그 이름들이 다른 파일에서 재사용될 수 있다.
규모가 큰 프로그램에선 함수의 이름을 다른 목적으로 재사용하는 것을 피하기 어렵고, external linkage를 갖는 이름이 너무 많으면 "name space pollution"을 초래할 수 있다. static은 그러한 상황을 방지하는 데 도움이 된다.
함수의 파라미터는 automatic storage duration, block scope, no linkage(same as auto variables)
파라미터에 지정될 수 있는 storage class는 register밖에 없다.
Summary
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;
}
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를 알 수 없다. 대부분의 경우 변수가 다른 파일에 정의되어 있으며, extern 변수는 external linkage를 갖는다.
4개의 storage class들 중 가장 중요한 것은 static, extern이다. auto는 아무런 효과가 없으며, 최신 컴파일러에서는 register가 덜 중요하다.
18.3 Type Qualifiers
type qualifier의 종류: const, volatile(used for low-level programming), restrict(C99, used only with pointers)
volatile은 20.3에서 다루고 여기선 미룸.
const는 변수와 유사하지만 "읽기 전용"인 object를 선언함.
const로 선언된 object는 constant가 와야 할 표현식에 사용될 수 없다.
const int n = 10;
int a[n]; /*** WRONG ***/
C99에서는, a가 automatic storage duration인 경우에만 VLA로 간주되고 위 표현이 legal.
숫자나 문자에 대한 상수들은 #define을 이용해 매크로를 사용하는 것을 추천함.
18.4 Declarators
declator에는 identifer(선언될 변수나 함수의 이름)이 포함되며 그 앞에 * 문자가 오거나 뒤에 [ ] ( )이 붙는다.
declarator 앞의 * 심볼은 포인터를 나타낸다. declarator 뒤의 대괄호[ ]는 배열을, 괄호( ) 는 함수를 선언함을 나타낸다.
int abs();
void swap();
함수 선언시 괄호를 비어 있게 선언할 수도 있지만, 함수 호출시 알맞은 인자가 전달되었는지 컴파일러가 체크할 수 없게 하기 때문에 거의 쓰이지 않게 된 스타일이다.
Deciphering Complex Declarations
int *(*x[10])(void);
같은 복잡한 선언을 어떻게 해독해야 할까?
두개의 규칙을 통해 복잡한 선언을 해석할 수 있다.
1. declarator는 항상 안에서 바깥으로 읽어라. 우선 선언되는 identifer를 찾고 그 identifer부터 해독을 시작한다.
2. [], ()가 *보다 언제나 우선된다. identifier 앞에 *이 있고 뒤에 []가 있다면, identifer는 배열이고, 포인터가 아니다. identifier 앞에 *이 있고 뒤에 ()가 있다면, 함수이고 포인터가 아니다. (물론 괄호 사용시 모든 연산자에 우선한다)
허용되지 않는 것들
함수는 배열을 리턴할 수 없다.
int f(int)[]; /*** WRONG ***/
함수는 함수를 리턴할 수 없다.
int g(int)(int); /*** WRONG ***/
함수의 배열은 불가능하다.
int a[10](int); /*** WRONG ***/
각각의 경우, 포인터를 사용해 원하는 효과를 얻을 수 있다. 함수는 배열의 포인터를 리턴할 수 있고, 함수가 함수의 포인터를 리턴할 수 있고, 배열은 함수의 포인터들이 원소가 될 수 있다.
Using Type Definitions to Simplify Declarations
복잡한 선언을 단순화하기 위한 방법. 위에서 쓴 예시를 다시 살펴보자
int *(*x[10])(void);
//x의 형식을 더 쉽게 이해하기 위해 다음과 같은 type 정의를 사용한다.
typedef int *Fcn(void);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_array[10];
Fcn_ptr_array x;
이걸 거꾸로 읽으면, x는 Fcn_ptr_array 형식, Fcn_ptr_array는 Fcn_ptr을 원소로 갖는 배열, Fcn_ptr은 Fcn의 포인터, Fcn은 인자가 없고 int값을 리턴하는 함수의 포인터이다.
18.5 Initializers
변수를 선언함과 동시에 변수의 시작 값을 지정할 수 있다. declarator 뒤에 = 기호를 써서 변수를 initialize하며, 이것은 assignment operator와는 다른 것이므로 주의해야 한다; initialization ≠ assignment
simple variable의 경우: 형식이 일치하지 않을 때에는 = 뒤의 expression이 conversion during assignment에 의해 변환된다.
pointer variable: initializer의 형식이 변수와 같은 포인터 형식이거나 void* 이어야 함.
static storage duration 변수의 initializer는 반드시 constant여야 한다.
automatic storage duration 변수의 initializer는 constant가 아니어도 된다.
배열, 구조체, 공용체 등의 괄호로 감싸진 initializer들은 constant expression만 가능하고, 변수나 함수의 호출이 올 수 없다.
automatic sturcture, union의 initializer는 다른 구조체나 공용체가 될 수 있다.
Uninitialized Variables
초기화되지 않은 변수의 값은 항상 undefined일까? 그렇지 않다. 변수의 storage duration에 따라 다르다.
automatic storage duration: initial value의 기본값이 없고 예측할 수 없다. 변수가 존재하게 될 때마다 달라질 수 있다.
static storage duration: 기본값으로 0이 된다. calloc이 모든 bit를 0으로 설정하는것과 달리, 변수의 형식에 맞는 0이 된다. 정수형은 0, float는 0.0, 포인터는 null pointer
변수가 static storage duration이더라도, 위 성질을 이용하지 않는 것이 코드를 보는 사람을 위해서 더 좋다.
18.6 Inline Functions(C99)
C99에서 등장한 새로운 키워드 inline은 기존의 범주와는 다른 새로운 종류의 declaration specifier이다.
함수 호출 전에 기계 단계에서 이뤄지는 instruction들이 몇가지 필요하다.
"overhead": 함수 자체적으로 수행하는 일 이외에 함수 호출시와 리턴받을 때 발생하는 누적되는 작업
함수 호출시 함수 내의 첫번째 지시로 점프한다. 함수를 실행함에 따라 함수 내의 추가적인 지시들이 실행된다. 함수에 인자가 있는 경우, 그 인자들이 복사되어야 한다. 함수로부터 값을 리턴할 때도 호출한 함수와 호출된 함수에서 비슷한 노력이 소요된다.
overhead가 프로그램을 느리게 하는 정도는 미미하지만, 함수가 수백만번 호출될 때, 오래되고 느린 프로세서를 사용할 때(또는 embedded 시스템 사용할 때), 시간상 아주 엄격한 데드라인이 적용될 때에는 그것을 무시할 수 없다.
C89 -> parameterized macro를 이용해서 해결
C99 -> 더 좋은 솔루션인 inline function을 제공함. "inline" 이라는 단어는 컴파일러가 각각의 함수 호출을 machine instruction으로 대체한다는 것을 의미함.
이렇게 해서 함수 호출에 대한 overhead를 피할 수 있음. 대신 컴파일된 프로그램의 크기는 약간 늘어난다.
함수를 inline으로 선언해도 컴파일러가 그 함수를 "inline"하도록 강제하는 것은 아님. 컴파일러가 함수의 호출을 최대한 빨리 수행하도로고 suggest할 뿐임. 컴파일러가 이 suggestion을 받아들일지 무시할지 결정하는 것은 자유임. 이 관점에서 inlinie은 register, restrict 키워드와 유사하다. 컴파일러는 프로그램의 퍼포먼스를 개선하려고 시도할 수도 있고, 무시할 수도 있다.
Inline Definitions
inline 키워드를 declaration specifier로 사용해서 정의한다.
inline double average(double a, double b)
{
return (a + b) / 2;
}
average는 external linkage이지만, average의 정의는 컴파일러에서 external definition으로 취급 안한다(대신 inline definition임).
따라서 다른 파일에서 average를 호출하려고 하면 에러이다.
위 문제의 해결 방법
1. 함수 정의시 static keyword 추가
static inline double average(double a, double b) {...}
이제 average가 internal linkage를 가지므로, 다른 파일에서 호출될 수 없다.
2. average에 대한 external definition을 제공해서 다른 파일에서도 호출될 수 있게 하기.
average의 inline 정의를 헤더 파일(예를 들어 average.h)에 집어넣는다.
#ifndef AVERAGE_H
#define AVERAGE_H
inline double average(double a, double b) {...}
#endif
그리고 대응하는 소스 파일(average.c)에는 다음과 같이 추가
#include "average.h"
extern double average(double a, double b);
이제 다른 파일에서 average 함수를 사용하고 싶은 경우 함수의 inline definition이 들어있는 average.h를 include하기만 하면 된다.
함수 프로토타입 역할: average.h에서 included된 average의 정의는 external definition이 된다.
Restrictions on Inline Functions
일반적인 함수들과 비교해서 inline 함수는 구현되는 방식이 다르기 때문에, 적용되는 규칙과 제한도 다르다.
inline function with external linkage의 제한:
함수는 수정 가능한 static variable을 정의할 수 없다.
함수는 internal linkage를 갖는 변수의 참조를 포함할 수 없다.